From c7c50237e5b259aa870b923e2f75225f44dc5d7e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 13 May 2026 10:32:04 -0700 Subject: [PATCH 1/8] docs: cockpit examples theme sync + Tailwind v4 migration design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec for syncing 32 Angular example apps with cockpit theme via postMessage + migrating Tailwind v3 CDN → v4 centralized in @ngaf/example-layouts. TS-source-of-truth palette via cssVars(theme) relocated to @ngaf/design-tokens. Co-Authored-By: Claude Opus 4.7 --- ...5-13-cockpit-examples-theme-sync-design.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-13-cockpit-examples-theme-sync-design.md diff --git a/docs/superpowers/specs/2026-05-13-cockpit-examples-theme-sync-design.md b/docs/superpowers/specs/2026-05-13-cockpit-examples-theme-sync-design.md new file mode 100644 index 00000000..d1f384bc --- /dev/null +++ b/docs/superpowers/specs/2026-05-13-cockpit-examples-theme-sync-design.md @@ -0,0 +1,216 @@ +# Cockpit Examples Theme Sync + Tailwind v4 Migration — Design + +**Date:** 2026-05-13 +**Scope:** 32 Angular example apps under `cockpit///angular/` +**Status:** Spec — pending implementation plan + +## Goal + +Bring every cockpit example app up to current standards in two stages: + +1. **Theme sync** — each app listens for `ngaf:theme` postMessage from its cockpit host and flips its own `` + CSS variables, so the embedded surface matches whatever theme cockpit is showing. +2. **Tailwind v4 migration** — replace the Tailwind v3 Play CDN in every app with a v4 setup centralized in `@ngaf/example-layouts`, paired with a runtime palette injection driven by `@ngaf/design-tokens` (TS-as-source-of-truth, consistent with cockpit). + +Each app needs to look correct in both light and dark mode after this work. The shell color values match what cockpit applies; templates use CSS-variable utilities (`bg-[var(--ds-canvas)]` etc.) so the swap is a variable update, not a class swap. + +Out of scope: +- Python agent servers under `cockpit///python/` — no UI, no theming work +- The cacheplane example deployment pipeline itself (still publishes to `examples.cacheplane.ai`) +- New design-token values — palette stays exactly what the cockpit dark-mode PR shipped +- Replacing the chat library's internal `--chat-*` variables; only example app templates migrate from `var(--chat-bg)` → `var(--ds-canvas)` etc. + +## Decisions + +| # | Decision | Choice | +|---|---|---| +| 1 | Where theme machinery lives | Centralized in `@ngaf/example-layouts`; a2ui imports the helper directly (97% via layout component, 1 outlier via direct import) | +| 2 | Tailwind version | v4, via `@ngaf/example-layouts/theme.css` consumed by every app | +| 3 | Palette source | TS-as-source-of-truth via `cssVars(theme)` from `@ngaf/design-tokens` (relocated from `@ngaf/ui-react`); applied at runtime by `installEmbeddedTheme()` | +| 4 | Template styling | CSS-variable utilities everywhere (`bg-[var(--ds-canvas)]`, `text-[var(--ds-text-primary)]`); no `dark:` variants in templates | +| 5 | Staging | C — library + 1 pilot app first, then remaining 31 in waves | + +## Architecture + +**Palette flow:** + +1. Tokens defined in TS: `baseTokens`, `lightOverrides`, `darkOverrides` in `@ngaf/design-tokens` (already shipped by cockpit PR) +2. `cssVars(theme: Theme)` resolves the right token set into a `--ds-*` map — moves from `@ngaf/ui-react` to `@ngaf/design-tokens`; `@ngaf/ui-react` re-exports for the React side so cockpit's existing import path keeps working +3. Each example app calls `installEmbeddedTheme()` on boot — applies `cssVars('dark')` to `document.documentElement.style`, sets `data-theme="dark"`, listens for `ngaf:theme` messages, posts `ngaf:theme-request` to parent +4. When cockpit's `` posts a theme update, the embedded app reapplies `cssVars(next)` and updates `data-theme` — visual flip is immediate + +**Tailwind v4 flow:** + +1. `@ngaf/example-layouts/theme.css` is the Tailwind entry: + ```css + @import "tailwindcss"; + @custom-variant dark (&:where([data-theme="dark"] *)); + ``` + No `@theme` block, no palette values — the palette is injected at runtime via `cssVars(theme)`. +2. Each app's `styles.css` becomes: + ```css + @import "@ngaf/example-layouts/theme.css"; + ``` +3. Each app's Angular build (`@angular/build:application`) picks up the imported CSS and runs Tailwind v4 over the templates in the app's `src/`. The `@source` directive (or v4's default behavior) handles scanning consuming app files. +4. Templates use arbitrary-value utilities: `class="bg-[var(--ds-canvas)] text-[var(--ds-text-primary)] border-[var(--ds-border)]"` etc. Tailwind generates these statically at build time. + +**Why this is consistent with cockpit's architecture:** + +| Concern | Cockpit | Examples | +|---|---|---| +| Source of theme truth | TS tokens in `@ngaf/design-tokens` | Same | +| Runtime resolution | `cssVars(theme)` → inline `style` on `` | Same | +| Source of truth swap mechanism | Cookie + RSC refresh | postMessage from parent | +| Variable application | Inline style on `` in root layout | Inline style on `` via `installEmbeddedTheme()` | +| `data-theme` attribute | Set in root layout | Set by `installEmbeddedTheme()` | + +The architectures mirror each other; the only difference is where the source-of-truth signal comes from (cookie vs postMessage). + +## Package changes + +**`@ngaf/design-tokens`:** 0.0.32 → 0.0.33 +- `src/index.ts`: add `export { cssVars, type CssVars } from './lib/css-vars';` +- `src/lib/css-vars.ts`: new file, moved from `@ngaf/ui-react`. Content identical to current implementation in ui-react. + +**`@ngaf/ui-react`:** 0.0.30 → 0.0.31 +- `src/lib/css-vars.ts`: deleted +- `src/index.ts`: replace `export { cssVars, type CssVars } from './lib/css-vars';` with `export { cssVars, type CssVars } from '@ngaf/design-tokens';` +- `src/lib/css-vars.spec.ts`: moved to design-tokens (already covers light + dark resolution) +- All React component exports unchanged: `ThemeProvider`, `useTheme`, ``, `useEmbeddedTheme`, `` + +**`@ngaf/example-layouts`:** patch bump (0.0.x + 1) +- `src/lib/install-embedded-theme.ts`: new file — pure TS, no Angular +- `src/lib/install-embedded-theme.spec.ts`: new unit tests +- `theme.css`: new file at lib root (exported via package.json `exports` field) +- `src/index.ts`: re-export `installEmbeddedTheme` +- `package.json`: add `exports` entry for `./theme.css`; add `tailwindcss` as a `peerDependency` (or `devDependency` plus rely on consuming apps to provide it) +- Existing `example-chat-layout.component.ts` and `example-split-layout.component.ts` — verify they don't need theme changes. Likely they hardcode some dark colors that need to migrate to `var(--ds-*)`. + +## Public surface additions + +```ts +// @ngaf/design-tokens +export { cssVars, type CssVars } from './lib/css-vars'; + +// @ngaf/example-layouts +export { installEmbeddedTheme } from './lib/install-embedded-theme'; +// Also exports the CSS file via package.json exports map: +// "exports": { "./theme.css": "./src/theme.css", ... } +``` + +## `installEmbeddedTheme` contract + +```ts +import { cssVars, type Theme } from '@ngaf/design-tokens'; + +export function installEmbeddedTheme(defaultTheme: Theme = 'dark'): void { + const apply = (theme: Theme) => { + document.documentElement.dataset.theme = theme; + const vars = cssVars(theme) as Record; + for (const [k, v] of Object.entries(vars)) { + document.documentElement.style.setProperty(k, v); + } + }; + apply(defaultTheme); + window.addEventListener('message', (e) => { + if (e.data?.type === 'ngaf:theme' && + (e.data.theme === 'light' || e.data.theme === 'dark')) { + apply(e.data.theme); + } + }); + window.parent?.postMessage({ type: 'ngaf:theme-request' }, '*'); +} +``` + +Idempotent on subsequent message receipts (re-applying the same theme is a no-op visually). No cleanup function needed — the iframe document's lifecycle is bounded by the iframe itself. + +## Per-app changes (pilot + each wave) + +Each of the 32 apps needs the same five-step migration: + +1. **`src/index.html`** — drop the v3 CDN script tag, remove `bg-gray-950 text-gray-100` from `` (the body inherits `--ds-canvas`/`--ds-text-primary` via inline style on `` instead). Leave the rest of `` alone. + +2. **`src/styles.css`** — replace contents with `@import "@ngaf/example-layouts/theme.css";`. If the app has custom global styles, keep them below the import. + +3. **`src/main.ts`** — add `import { installEmbeddedTheme } from '@ngaf/example-layouts'; installEmbeddedTheme();` before `bootstrapApplication(...)`. + +4. **Component templates** — replace dark-only Tailwind utilities and `--chat-*` variable references: + - `bg-gray-950` → `bg-[var(--ds-canvas)]` + - `text-gray-100` → `text-[var(--ds-text-primary)]` + - `text-gray-400` → `text-[var(--ds-text-secondary)]` + - `text-gray-500` → `text-[var(--ds-text-muted)]` + - `border-gray-800` → `border-[var(--ds-border)]` + - `bg-gray-900` → `bg-[var(--ds-surface)]` + - Inline `style="background: var(--chat-bg, #171717)"` → `style="background: var(--ds-canvas)"` (drop the fallback; if vars aren't set the app fails loudly, which is the right signal) + - Inline `style="color: var(--chat-text-muted, #777)"` → `style="color: var(--ds-text-muted)"` + The exact mapping is captured in a sed-style table in the implementation plan; each app applies the same replacements. + +5. **Project.json / build verification** — confirm the Angular build sees the new `@ngaf/example-layouts/theme.css` and runs Tailwind v4 against the app's templates. May require adding `@tailwindcss/postcss` (or `@tailwindcss/vite`, depending on the Angular builder's underlying tool) to the app or workspace devDeps. + +## Pilot + +The pilot app is **`cockpit/chat/timeline`**. Reasons: +- Uses `@ngaf/example-layouts` (`ExampleChatLayoutComponent`) — exercises the centralized layout path +- Renders both `` (sophisticated component with its own CSS) and `` (interactive control) — surfaces issues with nested component theming +- Has visible sidebar surfaces with `--chat-*` variable references — exercises the migration mapping +- Currently visible from cockpit's `langgraph/streaming` capability? Need to confirm the runtime URL / which capability the chat-timeline app maps to in the manifest. Confirm at plan time. + +Pilot success criteria: +- App loads with `data-theme="dark"`, all colors match cockpit's dark palette +- Cockpit toggle to light flips the embedded iframe to light within ~50ms (postMessage round-trip) +- Cockpit toggle back to dark flips embedded iframe back +- No visual regression: in either theme, the app is legible, contrast is acceptable, no left-over `#171717` hardcoded fallbacks bleeding through +- Chrome MCP smoke captures screenshots of the app in cockpit at light + dark; user reviews and signs off before wave 2 + +## Waves (post-pilot) + +Once the pilot is approved, the remaining 31 apps go in waves grouped by library: + +- **Wave 1: chat** — 11 apps (messages, tool-calls, subagents, input, a2ui*, theming, threads, interrupts, generative-ui, debug) +- **Wave 2: langgraph** — 8 apps (memory, durable-execution, streaming, subgraphs, deployment-runtime, interrupts, persistence, time-travel) +- **Wave 3: deep-agents** — 6 apps (sandboxes, subagents, memory, planning, filesystem, skills) +- **Wave 4: render + ag-ui** — 7 apps (computed-functions, element-rendering, repeat-loops, shared, state-management, spec-rendering, registry, streaming) + +(*a2ui is the lone non-`@ngaf/example-layouts` consumer — it imports `installEmbeddedTheme` directly in its `main.ts` instead of relying on the layout component to call it.) + +Each wave is one PR. Within a wave, all apps follow the same five-step migration. Pure mechanical work; high subagent batching potential. + +## Testing + +**Unit:** +- `@ngaf/design-tokens` `cssVars(theme)` tests (moved from ui-react, no behavior change — same 9 assertions) +- `@ngaf/example-layouts` `installEmbeddedTheme` tests: + - Applies default theme on call (data-theme + style attributes set) + - Updates on `ngaf:theme` message receipt + - Ignores malformed/unrelated messages + - Posts `ngaf:theme-request` to `window.parent` on call + +**Per-app build verification:** +- `pnpm nx build ` succeeds after migration +- No reference to `bg-gray-950`, `text-gray-100`, `var(--chat-bg)`, hardcoded `#171717` etc. remain (grep gate in plan) + +**Chrome MCP smoke (pilot + each wave):** +1. `pnpm nx serve cockpit` + the wave's example apps (each app on its own dev port from the manifest) +2. Open chrome to `http://localhost:4201/` +3. Screenshot dark mode (default) +4. Click cockpit's sidebar-footer theme toggle +5. Screenshot light mode +6. Eyeball both — checking: contrast legibility, no hardcoded-dark bleed-through, layout intact, interactive controls visible/usable in both +7. Repeat for every app in the wave +8. Capture screenshots into `docs/superpowers/screenshots/-/` for review + +This is genuinely time-consuming for 32 apps. The pilot does it manually as the gate; the waves can be partly automated (chrome MCP scripted loop that visits each capability + screenshots both themes) but final visual judgment is human. + +## Risks and mitigations + +- **`cssVars` move breaks consumers** — only one consumer in the workspace (cockpit's `layout.tsx`); compiler will flag the missed import. Mitigated by one-line patch in same PR. +- **Tailwind v4 `@source` doesn't pick up Angular templates** — verify against the website's existing v4 config; if v4 default scanning isn't sufficient, add explicit `@source "src/**/*.{ts,html}"` in the `theme.css`. +- **Removing fallback hardcoded colors causes white-on-white** if `installEmbeddedTheme()` fails to run — mitigated by setting the default theme synchronously before bootstrap; if the function throws, we fail loudly rather than render with stale defaults. +- **`--chat-*` variables in the chat library itself** still drive internal chat-component theming — out of scope for this work. Examples migrate AWAY from `--chat-*` in their own templates; the library can continue to use them internally. If the chat library's internal styling is dark-locked, that's tracked as a follow-up. +- **Per-app build pipeline drift** — each Angular app may have slightly different `project.json` config. The plan's first task validates the pattern on the pilot; waves follow the validated pattern. + +## Out-of-scope follow-ups (track but defer) + +- Chat library internal `--chat-*` variables — migrate to consume `--ds-*` instead of having parallel namespace +- `index.html` `` tag review — some apps may need adjustment when iframed +- Example app SEO / favicons / manifest — not theming-related but probably need attention soon +- Migration of `apps/cockpit` itself to whatever theming pattern emerges if these examples reveal something cleaner From a53f7a9b539d34fd68fd4a13a0c739a892af88fd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 13 May 2026 10:35:15 -0700 Subject: [PATCH 2/8] docs: cockpit examples theme sync stage 1 plan (library + pilot) 8 tasks: cssVars relocation to @ngaf/design-tokens, installEmbeddedTheme in @ngaf/example-layouts, theme.css with Tailwind v4 + dark variant + namespace bridge, workspace PostCSS config, chat-timeline pilot migration, version bumps, chrome MCP smoke, PR ship. Co-Authored-By: Claude Opus 4.7 --- ...-13-cockpit-examples-theme-sync-stage-1.md | 709 ++++++++++++++++++ 1 file changed, 709 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-13-cockpit-examples-theme-sync-stage-1.md diff --git a/docs/superpowers/plans/2026-05-13-cockpit-examples-theme-sync-stage-1.md b/docs/superpowers/plans/2026-05-13-cockpit-examples-theme-sync-stage-1.md new file mode 100644 index 00000000..7e67445b --- /dev/null +++ b/docs/superpowers/plans/2026-05-13-cockpit-examples-theme-sync-stage-1.md @@ -0,0 +1,709 @@ +# Cockpit Examples Theme Sync — Stage 1 Plan (Library + Pilot) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land the library plumbing for cockpit example theme sync and migrate one pilot app (`cockpit/chat/timeline/angular`) end-to-end to validate the pattern. After approval, a separate Stage 2 plan migrates the remaining 31 apps in waves. + +**Architecture:** `cssVars(theme)` moves from `@ngaf/ui-react` → `@ngaf/design-tokens` (pure TS, framework-agnostic). `@ngaf/example-layouts` gains `installEmbeddedTheme()` + a centralized `theme.css` (Tailwind v4 entry + dark variant + `--ngaf-chat-*` → `--ds-*` namespace bridge). The pilot app drops the v3 CDN, swaps `bg-gray-950 text-gray-100` for CSS-variable utilities, and calls `installEmbeddedTheme()` from `main.ts`. + +**Tech Stack:** Tailwind v4, `@tailwindcss/postcss`, Angular 21, `@angular/build:application`, Vitest, Vite. + +**Spec:** `docs/superpowers/specs/2026-05-13-cockpit-examples-theme-sync-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|---|---|---| +| Create | `libs/design-tokens/src/lib/css-vars.ts` | `cssVars(theme)` resolution (moved from ui-react) | +| Create | `libs/design-tokens/src/lib/css-vars.spec.ts` | Theme resolution tests (moved from ui-react) | +| Modify | `libs/design-tokens/src/index.ts` | Export `cssVars`, `CssVars` | +| Modify | `libs/design-tokens/package.json` | 0.0.32 → 0.0.33 | +| Delete | `libs/ui-react/src/lib/css-vars.ts` | Moved to design-tokens | +| Delete | `libs/ui-react/src/lib/css-vars.spec.ts` | Moved to design-tokens | +| Modify | `libs/ui-react/src/index.ts` | Re-export `cssVars` from `@ngaf/design-tokens` | +| Modify | `libs/ui-react/package.json` | 0.0.30 → 0.0.31 | +| Modify | `apps/cockpit/src/app/layout.tsx` | Import `cssVars` from `@ngaf/ui-react` still works via re-export — verify no change needed | +| Create | `libs/example-layouts/src/theme.css` | Tailwind v4 entry + dark variant + namespace bridge | +| Create | `libs/example-layouts/src/lib/install-embedded-theme.ts` | postMessage listener + cssVars applier | +| Create | `libs/example-layouts/src/lib/install-embedded-theme.spec.ts` | Unit tests | +| Modify | `libs/example-layouts/src/public-api.ts` | Export `installEmbeddedTheme` | +| Modify | `libs/example-layouts/package.json` | Add `exports` entry for `./theme.css`; patch bump; add `tailwindcss` peerDependency | +| Create | `postcss.config.mjs` (workspace root) OR per-app | Tailwind v4 PostCSS plugin wiring | +| Modify | `cockpit/chat/timeline/angular/src/index.html` | Drop v3 CDN, remove body classes | +| Modify | `cockpit/chat/timeline/angular/src/styles.css` | Replace with `@import "@ngaf/example-layouts/theme.css";` | +| Modify | `cockpit/chat/timeline/angular/src/main.ts` | Call `installEmbeddedTheme()` before bootstrap | +| Modify | `cockpit/chat/timeline/angular/src/app/timeline.component.ts` | Migrate template Tailwind utilities + inline styles to CSS-variable utilities | +| Modify | `cockpit/chat/timeline/angular/project.json` | Verify build target picks up the new styles correctly | + +--- + +## Task 1: Move `cssVars` to `@ngaf/design-tokens` + +**Files:** +- Create: `libs/design-tokens/src/lib/css-vars.ts` +- Create: `libs/design-tokens/src/lib/css-vars.spec.ts` +- Modify: `libs/design-tokens/src/index.ts` +- Delete: `libs/ui-react/src/lib/css-vars.ts` +- Delete: `libs/ui-react/src/lib/css-vars.spec.ts` +- Modify: `libs/ui-react/src/index.ts` + +- [ ] **Step 1: Copy `css-vars.ts` from ui-react to design-tokens** + +Read `libs/ui-react/src/lib/css-vars.ts`. Copy it to `libs/design-tokens/src/lib/css-vars.ts`, changing the imports from `'@ngaf/design-tokens'` to relative paths within the same lib: + +```ts +import { baseTokens } from './base'; +import { lightOverrides } from './light'; +import { darkOverrides } from './dark'; +import type { Theme } from './theme'; + +const overridesByTheme = { + light: lightOverrides, + dark: darkOverrides, +} as const; + +/** + * Resolve design tokens to a flat map of `--ds-*` CSS custom properties + * for the given theme. Apply to `` (or any container) via inline + * style or by iterating the entries and calling + * `element.style.setProperty(key, value)`. + */ +export function cssVars(theme: Theme) { + const t = overridesByTheme[theme]; + const { brand, typography, space, radius, shadows } = baseTokens; + return { + // [exact body copied from libs/ui-react/src/lib/css-vars.ts] + // including all --ds-* keys for surfaces, text, accent family, + // brand colors, typography, shadows, radii, spacing + } as const; +} + +export type CssVars = ReturnType; +``` + +Read the existing file first; preserve its exact body verbatim — only the imports change. + +- [ ] **Step 2: Copy the spec file** + +Copy `libs/ui-react/src/lib/css-vars.spec.ts` to `libs/design-tokens/src/lib/css-vars.spec.ts`. No content changes — same 9 test cases (light values, dark values, key parity, brand invariance, typography invariance). + +- [ ] **Step 3: Add export to design-tokens barrel** + +Edit `libs/design-tokens/src/index.ts` — add: + +```ts +export { cssVars } from './lib/css-vars'; +export type { CssVars } from './lib/css-vars'; +``` + +- [ ] **Step 4: Delete the ui-react versions** + +```bash +rm libs/ui-react/src/lib/css-vars.ts +rm libs/ui-react/src/lib/css-vars.spec.ts +``` + +- [ ] **Step 5: Update ui-react barrel to re-export from design-tokens** + +Edit `libs/ui-react/src/index.ts`. Find the line: + +```ts +export { cssVars, type CssVars } from './lib/css-vars'; +``` + +Replace with: + +```ts +export { cssVars, type CssVars } from '@ngaf/design-tokens'; +``` + +`Theme` is already re-exported from the same module — leave that line alone. + +- [ ] **Step 6: Run tests** + +```bash +pnpm nx test design-tokens +pnpm nx test ui-react +``` + +Expected: design-tokens picks up 9 new tests (now 14 total in that project); ui-react loses 9 css-vars tests (12 → 3). All passing. + +- [ ] **Step 7: Typecheck cockpit (the only `cssVars` consumer)** + +```bash +cd apps/cockpit && npx tsc --noEmit +``` + +Expected: no new errors. `cssVars` is still imported from `@ngaf/ui-react` in `layout.tsx` and resolves via the re-export. + +- [ ] **Step 8: Commit** + +```bash +git add libs/design-tokens/src/lib/css-vars.ts libs/design-tokens/src/lib/css-vars.spec.ts libs/design-tokens/src/index.ts libs/ui-react/src/lib/css-vars.ts libs/ui-react/src/lib/css-vars.spec.ts libs/ui-react/src/index.ts +git commit -m "refactor: move cssVars from @ngaf/ui-react to @ngaf/design-tokens" +``` + +(`git rm` on the deleted files happens automatically when you `git add` after deletion. Verify via `git status` before committing.) + +--- + +## Task 2: Add `installEmbeddedTheme` to `@ngaf/example-layouts` + +**Files:** +- Create: `libs/example-layouts/src/lib/install-embedded-theme.ts` +- Create: `libs/example-layouts/src/lib/install-embedded-theme.spec.ts` +- Modify: `libs/example-layouts/src/public-api.ts` + +- [ ] **Step 1: Write the failing tests first** + +Create `libs/example-layouts/src/lib/install-embedded-theme.spec.ts`: + +```ts +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { installEmbeddedTheme } from './install-embedded-theme'; + +describe('installEmbeddedTheme', () => { + beforeEach(() => { + vi.restoreAllMocks(); + document.documentElement.removeAttribute('data-theme'); + document.documentElement.removeAttribute('style'); + }); + + it('applies the default theme immediately (dark)', () => { + installEmbeddedTheme(); + expect(document.documentElement.dataset.theme).toBe('dark'); + // Verify a representative --ds-* var got set to the dark value + expect(document.documentElement.style.getPropertyValue('--ds-canvas').trim()).toBe('#0e1117'); + }); + + it('accepts a non-default initial theme', () => { + installEmbeddedTheme('light'); + expect(document.documentElement.dataset.theme).toBe('light'); + expect(document.documentElement.style.getPropertyValue('--ds-canvas').trim()).toBe('#fafbfc'); + }); + + it('posts ngaf:theme-request to window.parent on call', () => { + const spy = vi.spyOn(window.parent, 'postMessage'); + installEmbeddedTheme(); + expect(spy).toHaveBeenCalledWith({ type: 'ngaf:theme-request' }, '*'); + }); + + it('updates theme on ngaf:theme message receipt', () => { + installEmbeddedTheme(); + window.dispatchEvent( + new MessageEvent('message', { data: { type: 'ngaf:theme', theme: 'light' } }) + ); + expect(document.documentElement.dataset.theme).toBe('light'); + expect(document.documentElement.style.getPropertyValue('--ds-canvas').trim()).toBe('#fafbfc'); + }); + + it('ignores malformed messages', () => { + installEmbeddedTheme(); + window.dispatchEvent( + new MessageEvent('message', { data: { type: 'ngaf:theme', theme: 'banana' } }) + ); + expect(document.documentElement.dataset.theme).toBe('dark'); // unchanged + }); + + it('ignores unrelated message types', () => { + installEmbeddedTheme(); + window.dispatchEvent( + new MessageEvent('message', { data: { type: 'something-else', theme: 'light' } }) + ); + expect(document.documentElement.dataset.theme).toBe('dark'); // unchanged + }); +}); +``` + +- [ ] **Step 2: Run tests, verify failure** + +```bash +pnpm nx test example-layouts +``` + +Expected: FAIL — `install-embedded-theme` module not found. + +- [ ] **Step 3: Implement the helper** + +Create `libs/example-layouts/src/lib/install-embedded-theme.ts`: + +```ts +import { cssVars, type Theme } from '@ngaf/design-tokens'; + +/** + * Bootstraps an embedded example app's theme. Call once before the + * framework (Angular, Vue, etc.) bootstraps. + * + * Behavior: + * 1. Applies the default theme synchronously (sets `data-theme` and + * every `--ds-*` CSS variable on `document.documentElement`). + * 2. Posts `{ type: 'ngaf:theme-request' }` to `window.parent` so the + * host (cockpit's ``) replies with the current theme + * even if its broadcast ran before this iframe mounted. + * 3. Listens for `ngaf:theme` messages and re-applies on receipt. + * + * Idempotent: subsequent identical messages are no-ops visually. + */ +export function installEmbeddedTheme(defaultTheme: Theme = 'dark'): void { + const apply = (theme: Theme) => { + document.documentElement.dataset.theme = theme; + const vars = cssVars(theme) as Record; + for (const [key, value] of Object.entries(vars)) { + document.documentElement.style.setProperty(key, value); + } + }; + + apply(defaultTheme); + + window.addEventListener('message', (event: MessageEvent) => { + const data = event.data; + if ( + data && + typeof data === 'object' && + data.type === 'ngaf:theme' && + (data.theme === 'light' || data.theme === 'dark') + ) { + apply(data.theme); + } + }); + + window.parent?.postMessage({ type: 'ngaf:theme-request' }, '*'); +} +``` + +- [ ] **Step 4: Re-export from public API** + +Edit `libs/example-layouts/src/public-api.ts` (or `src/index.ts` — read it first to confirm the barrel filename). Add: + +```ts +export { installEmbeddedTheme } from './lib/install-embedded-theme'; +``` + +- [ ] **Step 5: Verify tests pass** + +```bash +pnpm nx test example-layouts +``` + +Expected: PASS, six tests in `install-embedded-theme.spec.ts`. + +- [ ] **Step 6: Commit** + +```bash +git add libs/example-layouts/src/lib/install-embedded-theme.ts libs/example-layouts/src/lib/install-embedded-theme.spec.ts libs/example-layouts/src/public-api.ts +git commit -m "feat(example-layouts): add installEmbeddedTheme for iframed apps" +``` + +--- + +## Task 3: Add `theme.css` to `@ngaf/example-layouts` + +**Files:** +- Create: `libs/example-layouts/src/theme.css` +- Modify: `libs/example-layouts/package.json` + +- [ ] **Step 1: Create the CSS entry** + +Create `libs/example-layouts/src/theme.css`: + +```css +/* + * @ngaf/example-layouts/theme.css + * + * Tailwind v4 entry point for embedded example apps. The host (cockpit) + * applies `--ds-*` variables to `` via `installEmbeddedTheme()`; + * this file provides: + * + * 1. Tailwind v4 import — utility class generation. + * 2. `dark:` variant rule — `dark:bg-foo` lights up when + * ``. + * 3. Namespace bridge — chat library and other internal namespaces + * can be wired to the design-tokens palette here, in one place. + */ +@import "tailwindcss"; + +@custom-variant dark (&:where([data-theme="dark"] *)); + +/* + * Namespace bridge: legacy `--ngaf-chat-*` variables (used inside the + * chat library and existing example components) inherit from the + * design-tokens palette. Removing this bridge requires migrating the + * chat library internals to read `--ds-*` directly — out of scope. + */ +:root { + --ngaf-chat-bg: var(--ds-canvas); + --ngaf-chat-text: var(--ds-text-primary); + --ngaf-chat-text-muted: var(--ds-text-muted); + --ngaf-chat-surface-alt: var(--ds-surface-tinted); + --ngaf-chat-separator: var(--ds-border); + --ngaf-chat-font-family: var(--ds-font-sans); +} +``` + +- [ ] **Step 2: Update `package.json` to export the CSS** + +Read `libs/example-layouts/package.json`. Add a `tailwindcss` peerDependency (or devDependency — the consuming apps need v4, so peerDependency is the honest signal) and an `exports` entry for the CSS: + +```jsonc +{ + "name": "@ngaf/example-layouts", + "version": "0.0.X", // bump patch + "peerDependencies": { + "tailwindcss": "^4.0.0" + // ... preserve existing peerDeps + }, + "exports": { + ".": "./src/public-api.ts", // or whatever the existing barrel entry is + "./theme.css": "./src/theme.css" + } +} +``` + +Preserve all existing fields. Only add `peerDependencies.tailwindcss` and the `exports."./theme.css"` line. + +- [ ] **Step 3: Commit** + +```bash +git add libs/example-layouts/src/theme.css libs/example-layouts/package.json +git commit -m "feat(example-layouts): add theme.css (Tailwind v4 + dark variant + namespace bridge)" +``` + +--- + +## Task 4: Workspace-level Tailwind v4 PostCSS config + +**Files:** +- Create: `postcss.config.mjs` (workspace root) **or** `cockpit/chat/timeline/angular/postcss.config.mjs` +- Modify: workspace `package.json` to add `@tailwindcss/postcss` + `tailwindcss` to devDeps + +The Angular builder (`@angular/build:application`) reads PostCSS config from the workspace root or app root. Workspace-root config is simpler — one file applies to every Angular app that picks up Tailwind through its `styles.css`. + +- [ ] **Step 1: Add dependencies** + +This repo uses `npm` at the root (per memory). Run: + +```bash +npm install --save-dev tailwindcss@^4 @tailwindcss/postcss@^4 +``` + +Expected: `package.json` devDeps gain both packages; `package-lock.json` updates. **Critical:** do NOT regenerate the lockfile from scratch (drops Linux `@next/swc-*` bindings); only let npm add the new entries. + +- [ ] **Step 2: Verify the lockfile preserves Linux SWC bindings** + +```bash +rg '"@next/swc-linux' package-lock.json +``` + +Expected: 1+ matches (the Linux-x64 SWC binaries). If gone, abort — the lockfile shouldn't have been regenerated. + +- [ ] **Step 3: Create the PostCSS config at workspace root** + +Create `/Users/blove/repos/angular-agent-framework/postcss.config.mjs`: + +```js +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; +``` + +Mirror exactly what `apps/website/postcss.config.mjs` looks like (read it first to confirm). + +- [ ] **Step 4: Commit** + +```bash +git add package.json package-lock.json postcss.config.mjs +git commit -m "chore: add Tailwind v4 + @tailwindcss/postcss to workspace devDeps" +``` + +--- + +## Task 5: Migrate the pilot — `cockpit/chat/timeline/angular` + +**Files:** +- Modify: `cockpit/chat/timeline/angular/src/index.html` +- Modify: `cockpit/chat/timeline/angular/src/styles.css` +- Modify: `cockpit/chat/timeline/angular/src/main.ts` +- Modify: `cockpit/chat/timeline/angular/src/app/timeline.component.ts` + +- [ ] **Step 1: Update `index.html`** + +Read the current file. Replace contents: + +```html + + + + + Chat Timeline — Angular + + + + + + + +``` + +Changes from baseline: drop ` - + diff --git a/cockpit/chat/timeline/angular/src/main.ts b/cockpit/chat/timeline/angular/src/main.ts index c8c9b6fb..718dc52e 100644 --- a/cockpit/chat/timeline/angular/src/main.ts +++ b/cockpit/chat/timeline/angular/src/main.ts @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT import { bootstrapApplication } from '@angular/platform-browser'; +import { installEmbeddedTheme } from '@ngaf/example-layouts'; import { appConfig } from './app/app.config'; import { TimelineComponent } from './app/timeline.component'; +installEmbeddedTheme(); + bootstrapApplication(TimelineComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/timeline/angular/src/styles.css b/cockpit/chat/timeline/angular/src/styles.css index bd63176c..60364f79 100644 --- a/cockpit/chat/timeline/angular/src/styles.css +++ b/cockpit/chat/timeline/angular/src/styles.css @@ -1 +1,10 @@ -/* Global styles for the timeline capability demo */ +@import "../../../../../libs/example-layouts/src/theme.css"; + +/* App-specific globals (none for now; add below this line if needed). */ +html, body { + height: 100%; + margin: 0; + background: var(--ds-canvas); + color: var(--ds-text-primary); + font-family: var(--ds-font-sans); +} From 5bea511898b0fe48e1c76e7287fdcbd4b6f4fe3a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 13 May 2026 10:49:14 -0700 Subject: [PATCH 8/8] chore: bump design-tokens, ui-react, example-layouts patch versions --- libs/design-tokens/package.json | 2 +- libs/example-layouts/package.json | 2 +- libs/ui-react/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/design-tokens/package.json b/libs/design-tokens/package.json index 2e19efeb..210dc215 100644 --- a/libs/design-tokens/package.json +++ b/libs/design-tokens/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/design-tokens", - "version": "0.0.32", + "version": "0.0.33", "license": "MIT", "repository": { "type": "git", diff --git a/libs/example-layouts/package.json b/libs/example-layouts/package.json index 59f3225f..3d1283ef 100644 --- a/libs/example-layouts/package.json +++ b/libs/example-layouts/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/example-layouts", - "version": "0.0.29", + "version": "0.0.30", "peerDependencies": { "@angular/core": "^20.0.0 || ^21.0.0", "@angular/common": "^20.0.0 || ^21.0.0", diff --git a/libs/ui-react/package.json b/libs/ui-react/package.json index a668ebed..b251072a 100644 --- a/libs/ui-react/package.json +++ b/libs/ui-react/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/ui-react", - "version": "0.0.30", + "version": "0.0.31", "license": "MIT", "repository": { "type": "git",