diff --git a/cockpit/chat/timeline/angular/src/app/timeline.component.ts b/cockpit/chat/timeline/angular/src/app/timeline.component.ts index 01420df8..7c53474a 100644 --- a/cockpit/chat/timeline/angular/src/app/timeline.component.ts +++ b/cockpit/chat/timeline/angular/src/app/timeline.component.ts @@ -17,14 +17,14 @@ import { environment } from '../environments/environment'; template: ` -
+

Timeline

+ style="color: var(--ds-text-muted);">Timeline

How It Works

-

+ style="color: var(--ds-text-muted);">How It Works +

Each message creates a checkpoint. Use the slider to navigate through conversation history and branch from any point.

diff --git a/cockpit/chat/timeline/angular/src/index.html b/cockpit/chat/timeline/angular/src/index.html index 860eb68e..74aa6ba4 100644 --- a/cockpit/chat/timeline/angular/src/index.html +++ b/cockpit/chat/timeline/angular/src/index.html @@ -5,9 +5,8 @@ Chat Timeline — Angular - - + 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); +} 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 `