diff --git a/apps/cockpit/e2e/dark-mode.spec.ts b/apps/cockpit/e2e/dark-mode.spec.ts index 38ee9515e..ffc4a0360 100644 --- a/apps/cockpit/e2e/dark-mode.spec.ts +++ b/apps/cockpit/e2e/dark-mode.spec.ts @@ -22,7 +22,7 @@ test.describe('dark mode', () => { const canvas = await page .locator('html') .evaluate((el) => getComputedStyle(el).getPropertyValue('--ds-canvas').trim()); - expect(canvas).toBe('#fafbfc'); + expect(canvas).toBe('rgb(255, 255, 255)'); }); test('toggle flips data-theme optimistically and persists across reload', async ({ diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css index e48f647af..a98f818bd 100644 --- a/apps/website/src/app/global.css +++ b/apps/website/src/app/global.css @@ -1,62 +1,5 @@ @import "tailwindcss"; - -@theme { - --color-bg: #f8f9fc; - --color-accent: #004090; - --color-accent-light: #64C3FD; - --color-accent-glow: rgba(0, 64, 144, 0.2); - --color-accent-border: rgba(0, 64, 144, 0.15); - --color-accent-border-hover: rgba(0, 64, 144, 0.3); - --color-accent-surface: rgba(0, 64, 144, 0.06); - --color-text-primary: #1a1a2e; - --color-text-secondary: #555770; - --color-text-muted: #8b8fa3; - --color-sidebar-bg: rgba(255, 255, 255, 0.45); - --color-angular-red: #DD0031; - - --font-garamond: "EB Garamond", Georgia, serif; - --font-inter: Inter, system-ui, sans-serif; - --font-mono: "JetBrains Mono", monospace; - - /* New surfaces — Phase 1 of Statusbrew refactor */ - --color-canvas: #fafbfc; - --color-surface: #ffffff; - --color-surface-tinted: #f4f6fb; - --color-surface-dim: #eef1f7; - --color-border: #e6e8ee; - --color-border-strong: #d2d6e0; - --color-text-inverted: #ffffff; - --color-accent-hover: #003070; - - /* Radii */ - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 14px; - --radius-xl: 20px; - --radius-full: 999px; - - /* Shadows */ - --shadow-sm: 0 1px 2px rgba(15, 23, 41, 0.04), 0 1px 1px rgba(15, 23, 41, 0.03); - --shadow-md: 0 4px 12px rgba(15, 23, 41, 0.06), 0 2px 4px rgba(15, 23, 41, 0.04); - --shadow-lg: 0 12px 32px rgba(15, 23, 41, 0.08), 0 4px 8px rgba(15, 23, 41, 0.05); - --shadow-focus: 0 0 0 3px rgba(0, 64, 144, 0.25); -} - -:root { - --color-bg: #f8f9fc; - --color-accent: #004090; - --color-accent-light: #64C3FD; - --color-accent-glow: rgba(0, 64, 144, 0.2); - --color-accent-border: rgba(0, 64, 144, 0.15); - --color-accent-border-hover: rgba(0, 64, 144, 0.3); - --color-accent-surface: rgba(0, 64, 144, 0.06); - --color-text-primary: #1a1a2e; - --color-text-secondary: #555770; - --color-text-muted: #8b8fa3; - --color-sidebar-bg: rgba(255, 255, 255, 0.45); - --color-angular-red: #DD0031; - -} +@import "@ngaf/design-tokens/theme.css"; * { box-sizing: border-box; diff --git a/docs/superpowers/plans/2026-05-15-website-token-alignment.md b/docs/superpowers/plans/2026-05-15-website-token-alignment.md new file mode 100644 index 000000000..1407c2807 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-website-token-alignment.md @@ -0,0 +1,640 @@ +# Website Token Alignment Implementation Plan + +> **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:** Refresh `@ngaf/design-tokens` light palette to chat-lib aesthetic (pure-white surfaces, near-black text, neutral grays), then migrate `apps/website` from its hand-maintained `@theme` block to a build-time-generated CSS file sourced from the TS token constants. + +**Architecture:** `lightOverrides` in `libs/design-tokens/src/lib/light.ts` and `shadows` in `libs/design-tokens/src/lib/shadows.ts` get new values aligned with chat lib. A new generator script (`libs/design-tokens/scripts/generate-theme-css.ts`) reads `baseTokens` + `lightOverrides` and emits `libs/design-tokens/src/lib/theme.css`. The website's `global.css` imports the generated file instead of redeclaring tokens locally. A drift-guard test re-runs the generator and asserts it matches the committed file. + +**Tech Stack:** TypeScript, Vitest, Tailwind v4, Nx. + +**Spec:** `docs/superpowers/specs/2026-05-15-website-token-alignment-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|---|---|---| +| Modify | `libs/design-tokens/src/lib/light.ts` | Palette values flip to chat-lib aesthetic | +| Modify | `libs/design-tokens/src/lib/shadows.ts` | Shadows shift from `rgba(15, 23, 41, *)` to `rgba(0, 0, 0, *)` | +| Modify | `libs/design-tokens/src/lib/css-vars.spec.ts` | Update expected hex values to match new palette | +| Modify | `apps/cockpit/e2e/dark-mode.spec.ts` | Update light-mode canvas assertion | +| Create | `libs/design-tokens/scripts/generate-theme-css.ts` | TS script that emits `theme.css` from TS constants | +| Create | `libs/design-tokens/src/lib/theme.css` | Generated artifact (committed to repo) | +| Create | `libs/design-tokens/src/lib/generate-theme-css.spec.ts` | Drift-guard test | +| Modify | `libs/design-tokens/package.json` | Add `exports."./theme.css"`, patch bump | +| Modify | `libs/design-tokens/project.json` | Add `generate-theme-css` Nx target | +| Modify | `apps/website/src/app/global.css` | Drop `@theme`/`:root` blocks, import generated CSS | + +--- + +## Task 1: Update `lightOverrides` palette values + +**Files:** +- Modify: `libs/design-tokens/src/lib/light.ts` + +- [ ] **Step 1: Read current file** + +```bash +cat libs/design-tokens/src/lib/light.ts +``` + +- [ ] **Step 2: Rewrite `lightOverrides` with chat-lib aesthetic values** + +Replace the body of `lightOverrides` with: + +```ts +import { baseTokens } from './base'; + +/** + * Theme-variant tokens resolved for the light theme. + * Aligned with @ngaf/chat library's polished consumer aesthetic + * (pure-white surfaces, near-black text, neutral grays) so embedded + * chat surfaces visually unify with cockpit chrome and the marketing + * website. + */ +export const lightOverrides = Object.freeze({ + // Surfaces + canvas: 'rgb(255, 255, 255)', + surface: 'rgb(255, 255, 255)', + surfaceTinted: 'rgb(251, 251, 251)', + surfaceDim: 'rgb(245, 245, 245)', + border: 'rgb(229, 229, 229)', + borderStrong: 'rgb(200, 200, 200)', + + // Text + textPrimary: 'rgb(28, 28, 28)', + textSecondary: 'rgb(70, 70, 70)', + textMuted: 'rgb(115, 115, 115)', + textInverted: 'rgb(255, 255, 255)', + + // Legacy surface aliases + bg: 'rgb(255, 255, 255)', + sidebarBg: 'rgba(255, 255, 255, 0.45)', + + // Semantic accent maps to the navy brand color (unchanged — cockpit identity) + accent: baseTokens.brand.accent, + accentHover: '#003070', + accentGlow: 'rgba(0, 64, 144, 0.2)', + accentBorder: 'rgba(0, 64, 144, 0.15)', + accentBorderHover: 'rgba(0, 64, 144, 0.3)', + accentSurface: 'rgba(0, 64, 144, 0.06)', +} as const); + +export type LightOverrides = typeof lightOverrides; +``` + +- [ ] **Step 3: Commit** + +```bash +git add libs/design-tokens/src/lib/light.ts +git commit -m "refactor(design-tokens): align lightOverrides with chat lib aesthetic" +``` + +--- + +## Task 2: Update shadows to neutral-black + +**Files:** +- Modify: `libs/design-tokens/src/lib/shadows.ts` + +- [ ] **Step 1: Read current file** + +```bash +cat libs/design-tokens/src/lib/shadows.ts +``` + +- [ ] **Step 2: Rewrite with chat-lib shadow values** + +Replace `sm`, `md`, `lg` (`focus` unchanged): + +```ts +export const shadows = Object.freeze({ + /** Subtle — default card */ + sm: '0 1px 2px rgba(0, 0, 0, 0.05)', + /** Moderate — hovered card, dropdown */ + md: '0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + /** Strong — floating elements, hero collage */ + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + /** Keyboard focus ring (accent-tinted; unchanged) */ + focus: '0 0 0 3px rgba(0, 64, 144, 0.25)', +} as const); + +export type Shadows = typeof shadows; +``` + +- [ ] **Step 3: Commit** + +```bash +git add libs/design-tokens/src/lib/shadows.ts +git commit -m "refactor(design-tokens): shift shadows to neutral rgba(0,0,0,*) (chat lib aesthetic)" +``` + +--- + +## Task 3: Update existing token tests to match new palette + +**Files:** +- Modify: `libs/design-tokens/src/lib/css-vars.spec.ts` + +The existing `css-vars.spec.ts` asserts specific hex values for the light theme that will change. Update them. + +- [ ] **Step 1: Read the test file** + +```bash +cat libs/design-tokens/src/lib/css-vars.spec.ts +``` + +- [ ] **Step 2: Update light-theme assertions** + +Find the `describe('light', ...)` block. Change `toBe` values: + +| Variable | Old expected | New expected | +|---|---|---| +| `--ds-canvas` | `'#fafbfc'` | `'rgb(255, 255, 255)'` | +| `--ds-text-primary` | `'#1a1a2e'` | `'rgb(28, 28, 28)'` | +| `--ds-accent` | `'#004090'` | `'#004090'` (unchanged) | + +Update only the changed values; leave dark-theme assertions, brand-color assertions, and typography assertions as-is. + +- [ ] **Step 3: Run tests to verify they pass with the new palette** + +```bash +pnpm nx test design-tokens +``` + +Expected: green. + +- [ ] **Step 4: Commit** + +```bash +git add libs/design-tokens/src/lib/css-vars.spec.ts +git commit -m "test(design-tokens): update css-vars expectations to chat lib palette" +``` + +--- + +## Task 4: Update cockpit e2e light-mode canvas assertion + +**Files:** +- Modify: `apps/cockpit/e2e/dark-mode.spec.ts` + +The cockpit dark-mode e2e suite asserts `--ds-canvas` is `#fafbfc` when the user has the `theme=light` cookie. After Task 1 this becomes `rgb(255, 255, 255)`. + +- [ ] **Step 1: Read the failing assertion** + +```bash +grep -B 5 -A 2 "fafbfc" apps/cockpit/e2e/dark-mode.spec.ts +``` + +- [ ] **Step 2: Update the light-mode canvas assertion** + +Find the line: + +```ts +expect(canvas).toBe('#fafbfc'); +``` + +Replace with: + +```ts +expect(canvas).toBe('rgb(255, 255, 255)'); +``` + +The dark-mode assertion (`'#0e1117'`) stays unchanged. + +- [ ] **Step 3: Commit** + +```bash +git add apps/cockpit/e2e/dark-mode.spec.ts +git commit -m "test(cockpit): update light-mode canvas assertion to rgb(255,255,255)" +``` + +--- + +## Task 5: Generator script + emitted `theme.css` + +**Files:** +- Create: `libs/design-tokens/scripts/generate-theme-css.ts` +- Create: `libs/design-tokens/src/lib/theme.css` + +The generator reads `baseTokens` and `lightOverrides`, emits a Tailwind v4 `@theme { … }` block to `libs/design-tokens/src/lib/theme.css`. Tailwind v4 maps `--color-*` to color utilities, `--font-*` to font, `--radius-*` to rounded, `--shadow-*` to shadow, `--spacing-*` to spacing. The generator uses these category prefixes. + +- [ ] **Step 1: Create the generator** + +Create `libs/design-tokens/scripts/generate-theme-css.ts`: + +```ts +#!/usr/bin/env tsx +/** + * Generates libs/design-tokens/src/lib/theme.css from the TypeScript + * token sources. Tailwind v4 reads `@theme { --color-*, --font-*, ... }` + * to generate utility classes; this script emits those tokens with the + * values pulled from `lightOverrides` + `baseTokens`. + * + * The output is committed to the repo (not gitignored) so consumers + * can import it directly. A drift-guard test re-runs this generator + * and diffs against the committed file to catch stale output. + */ +import { writeFileSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { baseTokens } from '../src/lib/base'; +import { lightOverrides } from '../src/lib/light'; + +const HERE = fileURLToPath(new URL('.', import.meta.url)); +const OUTPUT_PATH = resolve(HERE, '..', 'src', 'lib', 'theme.css'); + +const HEADER = `/* + * @ngaf/design-tokens/theme.css + * + * GENERATED FILE — DO NOT EDIT BY HAND. + * + * Regenerate with: + * pnpm nx run design-tokens:generate-theme-css + * + * Source of truth: + * - libs/design-tokens/src/lib/light.ts + * - libs/design-tokens/src/lib/base.ts + * + * Drift between this file and the TS sources is caught by + * generate-theme-css.spec.ts at test time. + */ +`; + +function buildThemeBlock(): string { + const { typography, radius, shadows, brand } = baseTokens; + + const lines: string[] = ['@theme {']; + + // Colors — surface family + lines.push(' /* Surfaces */'); + lines.push(` --color-canvas: ${lightOverrides.canvas};`); + lines.push(` --color-surface: ${lightOverrides.surface};`); + lines.push(` --color-surface-tinted: ${lightOverrides.surfaceTinted};`); + lines.push(` --color-surface-dim: ${lightOverrides.surfaceDim};`); + lines.push(` --color-border: ${lightOverrides.border};`); + lines.push(` --color-border-strong: ${lightOverrides.borderStrong};`); + + // Colors — text + lines.push(''); + lines.push(' /* Text */'); + lines.push(` --color-text-primary: ${lightOverrides.textPrimary};`); + lines.push(` --color-text-secondary: ${lightOverrides.textSecondary};`); + lines.push(` --color-text-muted: ${lightOverrides.textMuted};`); + lines.push(` --color-text-inverted: ${lightOverrides.textInverted};`); + + // Colors — legacy aliases + lines.push(''); + lines.push(' /* Legacy surface aliases */'); + lines.push(` --color-bg: ${lightOverrides.bg};`); + lines.push(` --color-sidebar-bg: ${lightOverrides.sidebarBg};`); + + // Colors — accent family + lines.push(''); + lines.push(' /* Accent family */'); + lines.push(` --color-accent: ${lightOverrides.accent};`); + lines.push(` --color-accent-hover: ${lightOverrides.accentHover};`); + lines.push(` --color-accent-glow: ${lightOverrides.accentGlow};`); + lines.push(` --color-accent-border: ${lightOverrides.accentBorder};`); + lines.push(` --color-accent-border-hover: ${lightOverrides.accentBorderHover};`); + lines.push(` --color-accent-surface: ${lightOverrides.accentSurface};`); + + // Colors — brand + lines.push(''); + lines.push(' /* Brand */'); + lines.push(` --color-accent-light: ${brand.accentLight};`); + lines.push(` --color-angular-red: ${brand.angularRed};`); + lines.push(` --color-render-green: ${brand.renderGreen};`); + lines.push(` --color-chat-purple: ${brand.chatPurple};`); + + // Fonts + lines.push(''); + lines.push(' /* Fonts */'); + lines.push(` --font-garamond: ${typography.fontSerif};`); + lines.push(` --font-inter: ${typography.fontSans};`); + lines.push(` --font-mono: ${typography.fontMono};`); + + // Radii + lines.push(''); + lines.push(' /* Radii */'); + lines.push(` --radius-sm: ${radius.sm};`); + lines.push(` --radius-md: ${radius.md};`); + lines.push(` --radius-lg: ${radius.lg};`); + lines.push(` --radius-xl: ${radius.xl};`); + lines.push(` --radius-full: ${radius.full};`); + + // Shadows + lines.push(''); + lines.push(' /* Shadows */'); + lines.push(` --shadow-sm: ${shadows.sm};`); + lines.push(` --shadow-md: ${shadows.md};`); + lines.push(` --shadow-lg: ${shadows.lg};`); + lines.push(` --shadow-focus: ${shadows.focus};`); + + lines.push('}'); + return lines.join('\n') + '\n'; +} + +export function generateThemeCss(): string { + return HEADER + buildThemeBlock(); +} + +function main() { + const content = generateThemeCss(); + writeFileSync(OUTPUT_PATH, content); + // eslint-disable-next-line no-console + console.log(`wrote ${OUTPUT_PATH}`); +} + +// Only run main when invoked directly (not when imported by tests) +const invokedDirectly = + process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]); +if (invokedDirectly) { + main(); +} +``` + +- [ ] **Step 2: Run the generator** + +```bash +npx tsx libs/design-tokens/scripts/generate-theme-css.ts +``` + +Expected output: `wrote /Users/.../libs/design-tokens/src/lib/theme.css`. + +- [ ] **Step 3: Inspect the generated file** + +```bash +cat libs/design-tokens/src/lib/theme.css | head -30 +``` + +Expected: `@theme { --color-canvas: rgb(255, 255, 255); ... }` block with all categories. + +- [ ] **Step 4: Commit (generator + generated file)** + +```bash +git add libs/design-tokens/scripts/generate-theme-css.ts libs/design-tokens/src/lib/theme.css +git commit -m "feat(design-tokens): generate theme.css from TS sources" +``` + +--- + +## Task 6: Drift-guard test for the generator + +**Files:** +- Create: `libs/design-tokens/src/lib/generate-theme-css.spec.ts` + +- [ ] **Step 1: Write the drift-guard test** + +Create `libs/design-tokens/src/lib/generate-theme-css.spec.ts`: + +```ts +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { generateThemeCss } from '../../scripts/generate-theme-css'; + +const COMMITTED_PATH = resolve(__dirname, 'theme.css'); + +describe('generate-theme-css', () => { + it('produces output that matches the committed theme.css', () => { + const expected = readFileSync(COMMITTED_PATH, 'utf-8'); + const actual = generateThemeCss(); + expect(actual).toBe(expected); + }); +}); +``` + +- [ ] **Step 2: Run the test** + +```bash +pnpm nx test design-tokens -- --run generate-theme-css.spec +``` + +Expected: 1 test passing. If it fails, the generator output doesn't match the committed file — re-run `npx tsx libs/design-tokens/scripts/generate-theme-css.ts` to update. + +- [ ] **Step 3: Commit** + +```bash +git add libs/design-tokens/src/lib/generate-theme-css.spec.ts +git commit -m "test(design-tokens): drift-guard for generated theme.css" +``` + +--- + +## Task 7: Wire Nx target + package exports + +**Files:** +- Modify: `libs/design-tokens/project.json` +- Modify: `libs/design-tokens/package.json` + +- [ ] **Step 1: Add the `generate-theme-css` Nx target** + +Edit `libs/design-tokens/project.json`. Inside the `targets` object, add: + +```json +"generate-theme-css": { + "executor": "nx:run-commands", + "options": { + "command": "npx tsx libs/design-tokens/scripts/generate-theme-css.ts", + "cwd": "{workspaceRoot}" + } +} +``` + +Place it after the existing `test` target. Make sure JSON commas are correct. + +- [ ] **Step 2: Add the CSS export to `package.json`** + +Edit `libs/design-tokens/package.json`. Add an `exports` field (preserve the existing fields): + +```json +{ + "name": "@ngaf/design-tokens", + "version": "0.0.33", + "license": "MIT", + "exports": { + "./theme.css": "./src/lib/theme.css" + }, + ... +} +``` + +If `exports` already exists from prior work, add the `./theme.css` entry to it. + +- [ ] **Step 3: Test the Nx target runs** + +```bash +pnpm nx run design-tokens:generate-theme-css +``` + +Expected: writes `theme.css` (idempotent — output unchanged from prior run). + +- [ ] **Step 4: Commit** + +```bash +git add libs/design-tokens/project.json libs/design-tokens/package.json +git commit -m "build(design-tokens): wire generate-theme-css Nx target + package export" +``` + +--- + +## Task 8: Migrate `apps/website/src/app/global.css` + +**Files:** +- Modify: `apps/website/src/app/global.css` + +- [ ] **Step 1: Read current file** + +```bash +cat apps/website/src/app/global.css +``` + +Note the line counts: `@theme {…}` block (lines 3-44), `:root {…}` block (lines 46-58), `body` block + further styles starting around line 60. + +- [ ] **Step 2: Replace `@theme` block + `:root` mirror with the generated import** + +Replace the top section (everything before `* { box-sizing: border-box; }`) with: + +```css +@import "tailwindcss"; +@import "@ngaf/design-tokens/theme.css"; +``` + +The body styles, scroll keyframes, animation rules, and everything else BELOW the `:root` block stays unchanged. + +- [ ] **Step 3: Build the website** + +```bash +pnpm nx build website +``` + +Expected: clean build. Tailwind v4 should pick up `@theme` from the imported file and generate utility classes (`bg-canvas`, `text-accent`, etc.) the same way as before. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/src/app/global.css +git commit -m "refactor(website): import design-tokens generated theme.css" +``` + +--- + +## Task 9: Version bump + full check stack + +**Files:** +- Modify: `libs/design-tokens/package.json` + +- [ ] **Step 1: Bump patch version** + +Edit `libs/design-tokens/package.json`. Increment `"version"` from `0.0.33` to `0.0.34`. Patch-only rule — never bump to `0.1.x`. + +- [ ] **Step 2: Run the full check stack** + +```bash +pnpm nx run-many -t lint,test -p design-tokens,ui-react,example-layouts,chat,cockpit,website +``` + +Expected: all green. + +```bash +pnpm nx e2e cockpit +``` + +Expected: all green. The light-mode canvas assertion (Task 4) was updated. + +```bash +pnpm nx build website +``` + +Expected: green. + +```bash +pnpm nx build cockpit-chat-timeline-angular +``` + +Expected: green. + +If anything fails: +- **css-vars.spec.ts** — verify the Task 3 updates landed (`rgb(255, 255, 255)` etc.) +- **dark-mode.spec.ts e2e** — Task 4 update needed +- **Website build** — confirm `@ngaf/design-tokens/theme.css` resolves via npm-workspaces symlink (`ls -la node_modules/@ngaf/design-tokens`) +- **Drift test** — re-run `pnpm nx run design-tokens:generate-theme-css` if the file is stale + +- [ ] **Step 3: Commit** + +```bash +git add libs/design-tokens/package.json +git commit -m "chore: bump design-tokens patch version" +``` + +--- + +## Task 10: Open PR + merge on green + +- [ ] **Step 1: Push branch** + +```bash +git push -u origin website-token-alignment +``` + +- [ ] **Step 2: Open PR** + +```bash +gh pr create --title "refactor: website token alignment + design-tokens light palette refresh" --body "$(cat <<'EOF' +## Summary + +Third (final) PR in the cockpit dark mode + style alignment series (spec: \`docs/superpowers/specs/2026-05-15-website-token-alignment-design.md\`, plan: \`docs/superpowers/plans/2026-05-15-website-token-alignment.md\`). + +- **Refresh \`@ngaf/design-tokens\` light palette** to absorb \`@ngaf/chat\` library's polished consumer aesthetic. Surfaces flip from cool off-white (\`#fafbfc\`) to pure white (\`rgb(255, 255, 255)\`); text primary from blue-tinted (\`#1a1a2e\`) to near-black (\`rgb(28, 28, 28)\`); borders to neutral grays. Shadows shift from cool \`rgba(15, 23, 41, *)\` to neutral \`rgba(0, 0, 0, *)\`. Chat lib's own tokens stay independent; embedded chat in cockpit visually unifies because design-tokens absorbed chat lib's values. +- **Website migration**: drop the hand-maintained \`@theme\` block in \`global.css\`. New build-time generator (\`libs/design-tokens/scripts/generate-theme-css.ts\`) reads TS constants and emits \`libs/design-tokens/src/lib/theme.css\`. Website imports the generated file via \`@import \"@ngaf/design-tokens/theme.css\"\`. Single source of truth; no drift. A drift-guard Vitest test re-runs the generator and asserts the committed file matches. +- **Cockpit**: no source changes. \`cssVars(theme)\` picks up the new light values automatically. Dark palette unchanged. + +PR 1 (cockpit polish, #307) and PR 2 (chat lib polish, #313) shipped earlier in the sequence. This closes the visual-consistency gap. + +## Test plan + +- [x] \`pnpm nx run-many -t lint,test -p design-tokens,ui-react,example-layouts,chat,cockpit,website\` — green +- [x] \`pnpm nx e2e cockpit\` — green (updated light-mode canvas assertion to \`rgb(255, 255, 255)\`) +- [x] \`pnpm nx build website\` — green +- [x] \`pnpm nx build cockpit-chat-timeline-angular\` — green +- [ ] Manual chrome MCP smoke: cockpit light mode + website light mode — surfaces are pure-white; embedded chat matches cockpit chrome with no visible seam + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +- [ ] **Step 3: Wait for green CI** + +```bash +gh pr checks --watch +``` + +- [ ] **Step 4: Squash-merge** + +```bash +gh pr merge --squash --delete-branch +``` + +--- + +## Self-review + +**Spec coverage:** +- ✅ Decision 1 (visual unification direction — update design-tokens to chat-lib aesthetic) → Tasks 1, 2 +- ✅ Decision 2 (build-time CSS generation) → Tasks 5, 6, 7 +- ✅ Decision 3 (keep `--ds-*` prefix) → no rename task; cockpit consumes `--ds-*` unchanged +- ✅ Decision 4 (cockpit no source changes) → not a task; cockpit picks up new values via `cssVars(theme)` automatically; the only cockpit touch is Task 4's e2e assertion update +- ✅ Decision 5 (OG image regen) → not a task; Satori re-renders at request time after deploy. No manual regen needed. + +**Adjustments from spec during plan-prep exploration:** +1. **Shadows are in a separate file** (`shadows.ts`), not inline in `base.ts`. Task 2 modifies `shadows.ts` directly. Spec implied `base.ts` shadow modification; plan corrects to actual file. +2. **`css-vars.spec.ts` asserts specific hex values for light** that change with the palette refresh. Task 3 added to update assertions. Spec called out this risk; plan implements it. +3. **No `light.spec.ts` exists** (spec mentioned one); the relevant assertions live in `css-vars.spec.ts` and `tokens.spec.ts`. Task 3 covers `css-vars.spec.ts`; `tokens.spec.ts` only asserts key-set parity, not values, so unaffected. + +**Placeholder scan:** No "TBD" / "TODO" / "fill in details". Generator code is complete; test code is complete; commit messages are spelled out. + +**Type consistency:** `lightOverrides`, `baseTokens`, `Shadows`, `LightOverrides` — consistent. Function name `generateThemeCss` used identically in Tasks 5 and 6. File path `libs/design-tokens/src/lib/theme.css` consistent across tasks. diff --git a/docs/superpowers/specs/2026-05-15-website-token-alignment-design.md b/docs/superpowers/specs/2026-05-15-website-token-alignment-design.md new file mode 100644 index 000000000..3ebcb663b --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-website-token-alignment-design.md @@ -0,0 +1,202 @@ +# Website Token Alignment + Design-Tokens Light Palette Refresh — Design + +**Date:** 2026-05-15 +**Status:** Spec — pending implementation plan +**Spec series:** Third (final) in the cockpit dark mode + style alignment series + +## Goal + +Close the visual-consistency gap between cockpit and the marketing website. Two coupled changes: + +1. **Refresh `@ngaf/design-tokens` light palette** to absorb the chat lib's polished consumer aesthetic — pure-white surfaces, near-black text, neutral grays. Chat lib's tokens stay independent; its values become the canonical light palette by being copied into design-tokens. Embedded chat surfaces in cockpit automatically visually unify with cockpit chrome because both now resolve to the same colors. +2. **Migrate the marketing website (`apps/website`)** from its hand-maintained `@theme` block (duplicate hex values) to consume `@ngaf/design-tokens` values at build time via a generated CSS file. Single source of truth; no drift. + +Out of scope: +- Chat lib palette unification or coupling (chat lib stays independent — decision C from brainstorming) +- Renaming the `--ds-*` prefix (deferred; rationale documented in this spec) +- Doc h1/h2/h3 size tokenization — narrative-docs in cockpit doesn't use explicit sizes; website docs use prose styling; no concrete need surfaced +- `--ds-*` dark palette changes (chat lib's dark theme is chat-specific; design-tokens dark stays as it shipped in #298) +- Brand accent colors (`--ds-accent`, `--ds-accent-light`, brand reds/greens/purples) — these are cockpit identity, not part of chat lib's aesthetic +- Removing the namespace bridge in `@ngaf/example-layouts/theme.css` (now redundant but harmless; track as follow-up) + +## Decisions + +| # | Decision | Choice | +|---|---|---| +| 1 | Visual unification direction | Update `@ngaf/design-tokens` light values to match chat lib aesthetic; chat lib palette stays independent | +| 2 | Website consumption of design-tokens | Build-time CSS generation — `@ngaf/design-tokens` ships a `theme.css` file generated from TS constants; website imports it; existing `@theme` block deleted | +| 3 | `--ds-*` prefix | Keep. Renaming is a cross-cutting sweep that doesn't pay for itself in this PR | +| 4 | Cockpit changes | None to source code. Cockpit consumes `cssVars(theme)` which picks up the new values automatically | +| 5 | OG image regeneration | Re-render website + cockpit OG images so social previews use the new palette | + +## Architecture + +**Token flow (after this PR):** + +1. **Source of truth**: TS constants in `libs/design-tokens/src/lib/{base,light,dark}.ts` +2. **Cockpit (light + dark)**: `cssVars(theme)` resolves tokens at runtime; applied inline on `` in `apps/cockpit/src/app/layout.tsx`. Unchanged behavior; new values automatic. +3. **Website (light only)**: at design-tokens BUILD time, a small script reads `lightOverrides` + `baseTokens` and emits `libs/design-tokens/src/lib/theme.css` containing a `@theme { … }` block matching Tailwind v4's expectations. Website's `apps/website/src/app/global.css` becomes `@import "@ngaf/design-tokens/theme.css";` plus any website-specific overrides. +4. **Chat lib**: continues to declare its own `--ngaf-chat-*` tokens independently. Values happen to match `--ds-*` for the cleanly-mappable subset because we copied them from chat lib. Future divergence is possible but discouraged. + +**Build-time CSS generation:** + +A new file `libs/design-tokens/scripts/generate-theme-css.ts` reads the TypeScript constants and emits `libs/design-tokens/src/lib/theme.css`. Run via: + +- An Nx target `pnpm nx run design-tokens:generate-theme-css` (declared in `libs/design-tokens/project.json`) +- A `prebuild` hook on the design-tokens project — emit the CSS before the lib's main build +- A guard test that re-runs the generator and diffs against the committed file (CI fails if `theme.css` is stale) + +The emitted `theme.css` is committed to the repo (not in `.gitignore`) so consumers can import it without running the generator themselves. + +## Concrete palette changes + +**`lightOverrides`** in `libs/design-tokens/src/lib/light.ts`: + +| Token | Before | After (chat lib value) | +|---|---|---| +| `canvas` | `#fafbfc` | `rgb(255, 255, 255)` | +| `surface` | `#ffffff` | `rgb(255, 255, 255)` (unchanged in effect) | +| `surfaceTinted` | `#f4f6fb` | `rgb(251, 251, 251)` | +| `surfaceDim` | `#eef1f7` | `rgb(245, 245, 245)` (interpolated; chat lib has no direct equivalent — picked a value slightly darker than `surfaceTinted`) | +| `border` | `#e6e8ee` | `rgb(229, 229, 229)` | +| `borderStrong` | `#d2d6e0` | `rgb(200, 200, 200)` | +| `textPrimary` | `#1a1a2e` | `rgb(28, 28, 28)` | +| `textSecondary` | `#555770` | `rgb(70, 70, 70)` (interpolated; chat lib has no direct equivalent — picked between primary and muted) | +| `textMuted` | `#8b8fa3` | `rgb(115, 115, 115)` | +| `textInverted` | `#ffffff` | `rgb(255, 255, 255)` (unchanged in effect) | +| `bg` (legacy alias) | `#f8f9fc` | `rgb(255, 255, 255)` (matches new canvas) | +| `sidebarBg` | `rgba(255, 255, 255, 0.45)` | `rgba(255, 255, 255, 0.45)` (unchanged — glass effect) | + +Accent family (`accent`, `accentHover`, `accentGlow`, `accentBorder`, `accentBorderHover`, `accentSurface`) — **unchanged**. Cockpit/website brand identity; chat lib doesn't define these. + +**`baseTokens.shadows`** in `libs/design-tokens/src/lib/base.ts`: + +| Token | Before | After | +|---|---|---| +| `sm` | `0 1px 2px rgba(15, 23, 41, 0.04), 0 1px 1px rgba(15, 23, 41, 0.03)` | `0 1px 2px rgba(0, 0, 0, 0.05)` (chat lib shadow-sm) | +| `md` | `0 4px 12px rgba(15, 23, 41, 0.06), 0 2px 4px rgba(15, 23, 41, 0.04)` | `0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06)` (chat lib shadow-md) | +| `lg` | `0 12px 32px rgba(15, 23, 41, 0.08), 0 4px 8px rgba(15, 23, 41, 0.05)` | `0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05)` (chat lib shadow-lg) | +| `focus` | `0 0 0 3px rgba(0, 64, 144, 0.25)` | unchanged (accent-family) | + +`baseTokens.radius`, `baseTokens.space`, `baseTokens.typography`: **unchanged**. Chat lib's specific radii (bubble 15px, input 20px) are chat-domain and stay in chat lib's own tokens. + +**`darkOverrides`** in `libs/design-tokens/src/lib/dark.ts`: **unchanged**. Chat lib's dark theme is chat-specific; design-tokens dark stays as shipped in #298 (the cockpit dark mode brand-blue undertone palette). + +## Website migration + +**Before** (`apps/website/src/app/global.css`): + +```css +@import "tailwindcss"; + +@theme { + --color-canvas: #fafbfc; + --color-surface: #ffffff; + --color-accent: #004090; + /* ...30+ more lines of hardcoded hex... */ +} + +:root { + /* mirror of @theme tokens for SSR fallback */ +} +``` + +**After**: + +```css +@import "tailwindcss"; +@import "@ngaf/design-tokens/theme.css"; + +/* Website-specific overrides if any */ +``` + +The generated `@ngaf/design-tokens/theme.css` contains: + +```css +@theme { + --color-canvas: rgb(255, 255, 255); + --color-surface: rgb(255, 255, 255); + --color-accent: #004090; + /* ...generated from light.ts + base.ts at build time... */ +} +``` + +Note the Tailwind v4 convention `--color-*` (not `--ds-*`). The generator maps each design-token name into the `--color-*` / `--font-*` / `--radius-*` / `--shadow-*` / `--spacing-*` categories that Tailwind v4 expects. Tailwind utilities like `bg-canvas`, `text-accent` keep working — they're auto-generated from `@theme`. + +The 48 utility usages in website TSX (`bg-bg`, `text-accent`, etc.) — **no changes needed**. Tailwind v4 reads `--color-*` from `@theme`; the generated file provides them. + +## `@ngaf/design-tokens` package changes + +- **Create**: `libs/design-tokens/scripts/generate-theme-css.ts` — TS script that imports `baseTokens`, `lightOverrides` and emits `theme.css` +- **Create**: `libs/design-tokens/src/lib/theme.css` — generated artifact (committed) +- **Modify**: `libs/design-tokens/package.json` — add `exports."./theme.css"`, add `generate-theme-css` script +- **Modify**: `libs/design-tokens/project.json` — add `generate-theme-css` Nx target; add `prebuild` dependency +- **Modify**: `libs/design-tokens/src/lib/light.ts` — palette values per table above +- **Modify**: `libs/design-tokens/src/lib/base.ts` — shadow values per table above +- **Create**: `libs/design-tokens/src/lib/generate-theme-css.spec.ts` — guard test that re-runs the generator and diffs against committed `theme.css` +- **Modify**: `libs/design-tokens/package.json` — patch bump 0.0.33 → 0.0.34 + +## `apps/website` changes + +- **Modify**: `apps/website/src/app/global.css` — drop `@theme { … }` and `:root { mirror }` blocks; add `@import "@ngaf/design-tokens/theme.css";` +- **Modify**: `apps/website/postcss.config.mjs` — confirm Tailwind v4 still picks up the imported `@theme` block; no changes expected (Tailwind v4's PostCSS plugin scans imported CSS) + +## Cockpit changes + +**None to source code.** `cssVars(theme)` resolves the new `lightOverrides` values automatically. `apps/cockpit/src/app/layout.tsx` already passes `theme` from the cookie. In dark mode (cockpit's default), nothing visually changes. In light mode (after toggle), surfaces are now pure-white instead of cool off-white; text is near-black instead of `#1a1a2e`. + +## OG image regeneration + +`apps/website/src/app/opengraph-image.tsx` reads design-tokens at runtime via Satori. After the value updates, the next render automatically reflects the new palette. CDN cache invalidation happens on deploy. + +`apps/cockpit/src/app/opengraph-image.tsx` reads `darkOverrides` (cockpit OG is canonical dark). No change since dark palette is unchanged. + +## Tailwind v4 naming convention note + +Tailwind v4 auto-generates utility classes from `@theme` tokens matched to its category namespaces: +- `--color-*` → `bg-*`, `text-*`, `border-*` etc. +- `--font-*` → `font-*` +- `--radius-*` → `rounded-*` +- `--shadow-*` → `shadow-*` +- `--spacing-*` → `p-*`, `m-*`, `gap-*` etc. + +The generator emits tokens under the right Tailwind categories so utilities work without per-token mapping. Cockpit uses `bg-[var(--ds-canvas)]` (arbitrary value form) because its tokens are runtime-injected — Tailwind can't see `--ds-canvas` at build time. Website's tokens are at build time, so utilities like `bg-canvas` resolve naturally. + +This means **the two apps DO have a slight naming difference at call sites**: +- Cockpit: `bg-[var(--ds-canvas)]` +- Website: `bg-canvas` + +The values resolve identically. The notational difference is forced by Tailwind v4's build-time vs cockpit's runtime-injection architectural choice. Acceptable. Documented for future contributors. + +## Testing + +**Unit:** +- `libs/design-tokens/src/lib/generate-theme-css.spec.ts` — new test that calls the generator and asserts output matches committed `theme.css` (CI guard against drift) +- Existing `libs/design-tokens/src/lib/light.spec.ts` (or equivalent) — update expected values to match the new palette + +**Build verification:** +- `pnpm nx run design-tokens:generate-theme-css` — emits `theme.css`; output matches committed file +- `pnpm nx build website` — clean; `@theme` block resolved from generated file +- `pnpm nx build cockpit` — clean; no source changes needed +- `pnpm nx run-many -t lint,test -p design-tokens,ui-react,example-layouts,chat,cockpit,website` — green + +**Visual smoke (chrome MCP):** +- Cockpit on 3000, timeline pilot on 4507 +- Cockpit dark mode: unchanged (verify no regression) +- Toggle cockpit to light: surfaces should be pure white (no cool gray cast); text should be near-black; embedded chat should match cockpit chrome exactly (no visible seam where iframe meets shell) +- Website at `localhost:4174` (or wherever it runs): surfaces should be pure white; component-level visual diff is negligible since `--color-canvas` already had `#fafbfc` close to white + +## Risks and mitigations + +- **Visual regression in cockpit light mode.** The change is small (`#fafbfc` → `rgb(255,255,255)`, etc.) but visible. Mitigated by visual smoke and a brief eyeball pass on every cockpit capability route in light mode. +- **Snapshot tests asserting specific hex values.** Cockpit e2e tests in `apps/cockpit/e2e/dark-mode.spec.ts` assert `--ds-canvas` is `#0e1117` in dark and `#fafbfc` in light. The light assertion needs to update to `rgb(255, 255, 255)`. Caught by CI; fix in the same PR. +- **Generator drift.** The `theme.css` committed file could drift from the TS constants. Mitigated by the generate-theme-css.spec.ts guard test. +- **Tailwind v4 doesn't pick up the imported `@theme` block.** Risk if the website's PostCSS config processes the import in the wrong order. Verified by running `pnpm nx build website` after the change. +- **Website's `@theme` block had website-specific tokens** not in design-tokens (e.g., `--font-garamond`, `--font-inter`, `--font-mono`). The generator must emit these too — they're sourced from `baseTokens.typography`. Verify all 30+ keys round-trip. + +## Out-of-scope follow-ups (track but defer) + +- Removing the now-redundant namespace bridge in `@ngaf/example-layouts/theme.css` (the bridge mapped `--ngaf-chat-*` to `--ds-*`; with values matching, the bridge is a no-op but not harmful) +- Renaming `--ds-*` prefix (cross-cutting sweep; do separately if/when needed) +- Doc h1/h2/h3 size tokenization (no concrete need surfaced) +- Chat lib palette unification with `--ds-*` references (decision C from brainstorming — chat lib stays independent) diff --git a/libs/design-tokens/package.json b/libs/design-tokens/package.json index 210dc215f..933d3ba81 100644 --- a/libs/design-tokens/package.json +++ b/libs/design-tokens/package.json @@ -1,7 +1,10 @@ { "name": "@ngaf/design-tokens", - "version": "0.0.33", + "version": "0.0.34", "license": "MIT", + "exports": { + "./theme.css": "./src/lib/theme.css" + }, "repository": { "type": "git", "url": "https://github.com/cacheplane/angular-agent-framework.git", diff --git a/libs/design-tokens/project.json b/libs/design-tokens/project.json index 3c155bf83..f79567447 100644 --- a/libs/design-tokens/project.json +++ b/libs/design-tokens/project.json @@ -22,6 +22,13 @@ "options": { "configFile": "libs/design-tokens/vite.config.mts" } + }, + "generate-theme-css": { + "executor": "nx:run-commands", + "options": { + "command": "npx tsx libs/design-tokens/scripts/generate-theme-css.ts", + "cwd": "{workspaceRoot}" + } } } } diff --git a/libs/design-tokens/scripts/generate-theme-css.ts b/libs/design-tokens/scripts/generate-theme-css.ts new file mode 100644 index 000000000..75b16e67a --- /dev/null +++ b/libs/design-tokens/scripts/generate-theme-css.ts @@ -0,0 +1,128 @@ +#!/usr/bin/env tsx +/** + * Generates libs/design-tokens/src/lib/theme.css from the TypeScript + * token sources. Tailwind v4 reads `@theme { --color-*, --font-*, ... }` + * to generate utility classes; this script emits those tokens with the + * values pulled from `lightOverrides` + `baseTokens`. + * + * The output is committed to the repo (not gitignored) so consumers + * can import it directly. A drift-guard test re-runs this generator + * and diffs against the committed file to catch stale output. + */ +import { writeFileSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { baseTokens } from '../src/lib/base'; +import { lightOverrides } from '../src/lib/light'; + +const HERE = fileURLToPath(new URL('.', import.meta.url)); +const OUTPUT_PATH = resolve(HERE, '..', 'src', 'lib', 'theme.css'); + +const HEADER = `/* + * @ngaf/design-tokens/theme.css + * + * GENERATED FILE — DO NOT EDIT BY HAND. + * + * Regenerate with: + * pnpm nx run design-tokens:generate-theme-css + * + * Source of truth: + * - libs/design-tokens/src/lib/light.ts + * - libs/design-tokens/src/lib/base.ts + * + * Drift between this file and the TS sources is caught by + * generate-theme-css.spec.ts at test time. + */ +`; + +function buildThemeBlock(): string { + const { typography, radius, shadows, brand } = baseTokens; + + const lines: string[] = ['@theme {']; + + // Colors — surface family + lines.push(' /* Surfaces */'); + lines.push(` --color-canvas: ${lightOverrides.canvas};`); + lines.push(` --color-surface: ${lightOverrides.surface};`); + lines.push(` --color-surface-tinted: ${lightOverrides.surfaceTinted};`); + lines.push(` --color-surface-dim: ${lightOverrides.surfaceDim};`); + lines.push(` --color-border: ${lightOverrides.border};`); + lines.push(` --color-border-strong: ${lightOverrides.borderStrong};`); + + // Colors — text + lines.push(''); + lines.push(' /* Text */'); + lines.push(` --color-text-primary: ${lightOverrides.textPrimary};`); + lines.push(` --color-text-secondary: ${lightOverrides.textSecondary};`); + lines.push(` --color-text-muted: ${lightOverrides.textMuted};`); + lines.push(` --color-text-inverted: ${lightOverrides.textInverted};`); + + // Colors — legacy aliases + lines.push(''); + lines.push(' /* Legacy surface aliases */'); + lines.push(` --color-bg: ${lightOverrides.bg};`); + lines.push(` --color-sidebar-bg: ${lightOverrides.sidebarBg};`); + + // Colors — accent family + lines.push(''); + lines.push(' /* Accent family */'); + lines.push(` --color-accent: ${lightOverrides.accent};`); + lines.push(` --color-accent-hover: ${lightOverrides.accentHover};`); + lines.push(` --color-accent-glow: ${lightOverrides.accentGlow};`); + lines.push(` --color-accent-border: ${lightOverrides.accentBorder};`); + lines.push(` --color-accent-border-hover: ${lightOverrides.accentBorderHover};`); + lines.push(` --color-accent-surface: ${lightOverrides.accentSurface};`); + + // Colors — brand + lines.push(''); + lines.push(' /* Brand */'); + lines.push(` --color-accent-light: ${brand.accentLight};`); + lines.push(` --color-angular-red: ${brand.angularRed};`); + lines.push(` --color-render-green: ${brand.renderGreen};`); + lines.push(` --color-chat-purple: ${brand.chatPurple};`); + + // Fonts + lines.push(''); + lines.push(' /* Fonts */'); + lines.push(` --font-garamond: ${typography.fontSerif};`); + lines.push(` --font-inter: ${typography.fontSans};`); + lines.push(` --font-mono: ${typography.fontMono};`); + + // Radii + lines.push(''); + lines.push(' /* Radii */'); + lines.push(` --radius-sm: ${radius.sm};`); + lines.push(` --radius-md: ${radius.md};`); + lines.push(` --radius-lg: ${radius.lg};`); + lines.push(` --radius-xl: ${radius.xl};`); + lines.push(` --radius-full: ${radius.full};`); + + // Shadows + lines.push(''); + lines.push(' /* Shadows */'); + lines.push(` --shadow-sm: ${shadows.sm};`); + lines.push(` --shadow-md: ${shadows.md};`); + lines.push(` --shadow-lg: ${shadows.lg};`); + lines.push(` --shadow-focus: ${shadows.focus};`); + + lines.push('}'); + return lines.join('\n') + '\n'; +} + +export function generateThemeCss(): string { + return HEADER + buildThemeBlock(); +} + +function main() { + const content = generateThemeCss(); + writeFileSync(OUTPUT_PATH, content); + // eslint-disable-next-line no-console + console.log(`wrote ${OUTPUT_PATH}`); +} + +// Only run main when invoked directly (not when imported by tests) +const invokedDirectly = + process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]); +if (invokedDirectly) { + main(); +} diff --git a/libs/design-tokens/src/lib/css-vars.spec.ts b/libs/design-tokens/src/lib/css-vars.spec.ts index 94863b0c3..0b5240a84 100644 --- a/libs/design-tokens/src/lib/css-vars.spec.ts +++ b/libs/design-tokens/src/lib/css-vars.spec.ts @@ -6,7 +6,7 @@ describe('cssVars(theme)', () => { const vars = cssVars('light'); it('uses light canvas color', () => { - expect(vars['--ds-canvas']).toBe('#fafbfc'); + expect(vars['--ds-canvas']).toBe('rgb(255, 255, 255)'); }); it('uses navy accent', () => { @@ -14,7 +14,7 @@ describe('cssVars(theme)', () => { }); it('uses dark text on light surfaces', () => { - expect(vars['--ds-text-primary']).toBe('#1a1a2e'); + expect(vars['--ds-text-primary']).toBe('rgb(28, 28, 28)'); }); }); diff --git a/libs/design-tokens/src/lib/generate-theme-css.spec.ts b/libs/design-tokens/src/lib/generate-theme-css.spec.ts new file mode 100644 index 000000000..2ac73a0b7 --- /dev/null +++ b/libs/design-tokens/src/lib/generate-theme-css.spec.ts @@ -0,0 +1,14 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { generateThemeCss } from '../../scripts/generate-theme-css'; + +const COMMITTED_PATH = resolve(__dirname, 'theme.css'); + +describe('generate-theme-css', () => { + it('produces output that matches the committed theme.css', () => { + const expected = readFileSync(COMMITTED_PATH, 'utf-8'); + const actual = generateThemeCss(); + expect(actual).toBe(expected); + }); +}); diff --git a/libs/design-tokens/src/lib/light.ts b/libs/design-tokens/src/lib/light.ts index 145c602db..f1d6e7d09 100644 --- a/libs/design-tokens/src/lib/light.ts +++ b/libs/design-tokens/src/lib/light.ts @@ -2,28 +2,31 @@ import { baseTokens } from './base'; /** * Theme-variant tokens resolved for the light theme. - * Preserves the current production light palette exactly. + * Aligned with @ngaf/chat library's polished consumer aesthetic + * (pure-white surfaces, near-black text, neutral grays) so embedded + * chat surfaces visually unify with cockpit chrome and the marketing + * website. */ export const lightOverrides = Object.freeze({ - // Surfaces (was libs/design-tokens/src/lib/surfaces.ts) - canvas: '#fafbfc', - surface: '#ffffff', - surfaceTinted: '#f4f6fb', - surfaceDim: '#eef1f7', - border: '#e6e8ee', - borderStrong: '#d2d6e0', + // Surfaces + canvas: 'rgb(255, 255, 255)', + surface: 'rgb(255, 255, 255)', + surfaceTinted: 'rgb(251, 251, 251)', + surfaceDim: 'rgb(245, 245, 245)', + border: 'rgb(229, 229, 229)', + borderStrong: 'rgb(200, 200, 200)', // Text - textPrimary: '#1a1a2e', - textSecondary: '#555770', - textMuted: '#8b8fa3', - textInverted: '#ffffff', + textPrimary: 'rgb(28, 28, 28)', + textSecondary: 'rgb(70, 70, 70)', + textMuted: 'rgb(115, 115, 115)', + textInverted: 'rgb(255, 255, 255)', // Legacy surface aliases - bg: '#f8f9fc', + bg: 'rgb(255, 255, 255)', sidebarBg: 'rgba(255, 255, 255, 0.45)', - // Semantic accent maps to the navy brand color + // Semantic accent maps to the navy brand color (unchanged — cockpit identity) accent: baseTokens.brand.accent, accentHover: '#003070', accentGlow: 'rgba(0, 64, 144, 0.2)', diff --git a/libs/design-tokens/src/lib/shadows.ts b/libs/design-tokens/src/lib/shadows.ts index dd66d1a09..3f095e4a9 100644 --- a/libs/design-tokens/src/lib/shadows.ts +++ b/libs/design-tokens/src/lib/shadows.ts @@ -6,12 +6,12 @@ */ export const shadows = Object.freeze({ /** Subtle — default card */ - sm: '0 1px 2px rgba(15, 23, 41, 0.04), 0 1px 1px rgba(15, 23, 41, 0.03)', + sm: '0 1px 2px rgba(0, 0, 0, 0.05)', /** Moderate — hovered card, dropdown */ - md: '0 4px 12px rgba(15, 23, 41, 0.06), 0 2px 4px rgba(15, 23, 41, 0.04)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', /** Strong — floating elements, hero collage */ - lg: '0 12px 32px rgba(15, 23, 41, 0.08), 0 4px 8px rgba(15, 23, 41, 0.05)', - /** Keyboard focus ring */ + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + /** Keyboard focus ring (accent-tinted; unchanged) */ focus: '0 0 0 3px rgba(0, 64, 144, 0.25)', } as const); diff --git a/libs/design-tokens/src/lib/theme.css b/libs/design-tokens/src/lib/theme.css new file mode 100644 index 000000000..aea823df4 --- /dev/null +++ b/libs/design-tokens/src/lib/theme.css @@ -0,0 +1,66 @@ +/* + * @ngaf/design-tokens/theme.css + * + * GENERATED FILE — DO NOT EDIT BY HAND. + * + * Regenerate with: + * pnpm nx run design-tokens:generate-theme-css + * + * Source of truth: + * - libs/design-tokens/src/lib/light.ts + * - libs/design-tokens/src/lib/base.ts + * + * Drift between this file and the TS sources is caught by + * generate-theme-css.spec.ts at test time. + */ +@theme { + /* Surfaces */ + --color-canvas: rgb(255, 255, 255); + --color-surface: rgb(255, 255, 255); + --color-surface-tinted: rgb(251, 251, 251); + --color-surface-dim: rgb(245, 245, 245); + --color-border: rgb(229, 229, 229); + --color-border-strong: rgb(200, 200, 200); + + /* Text */ + --color-text-primary: rgb(28, 28, 28); + --color-text-secondary: rgb(70, 70, 70); + --color-text-muted: rgb(115, 115, 115); + --color-text-inverted: rgb(255, 255, 255); + + /* Legacy surface aliases */ + --color-bg: rgb(255, 255, 255); + --color-sidebar-bg: rgba(255, 255, 255, 0.45); + + /* Accent family */ + --color-accent: #004090; + --color-accent-hover: #003070; + --color-accent-glow: rgba(0, 64, 144, 0.2); + --color-accent-border: rgba(0, 64, 144, 0.15); + --color-accent-border-hover: rgba(0, 64, 144, 0.3); + --color-accent-surface: rgba(0, 64, 144, 0.06); + + /* Brand */ + --color-accent-light: #64C3FD; + --color-angular-red: #DD0031; + --color-render-green: #1a7a40; + --color-chat-purple: #5a00c8; + + /* Fonts */ + --font-garamond: "EB Garamond", Georgia, serif; + --font-inter: Inter, system-ui, sans-serif; + --font-mono: "JetBrains Mono", monospace; + + /* Radii */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --radius-full: 999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-focus: 0 0 0 3px rgba(0, 64, 144, 0.25); +}