feat(web): Add Tailwind foundation (#1388)

This commit is contained in:
nettee 2026-05-12 21:48:16 +08:00 committed by GitHub
parent 7e2168ed29
commit f621dbbfea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1464 additions and 89 deletions

View file

@ -39,12 +39,15 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@testing-library/react": "^16.3.2",
"@types/jsdom": "^28.0.1",
"@types/node": "^20.17.10",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"jsdom": "29.1.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
},

View file

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View file

@ -1,4 +1,108 @@
@layer theme, base, utilities;
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&display=swap');
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
/* Open Design Tailwind token vocabulary.
Colors intentionally clear Tailwind's default palette and expose only
project-approved tokens backed by the runtime CSS variables below. Use
native Tailwind utilities for spacing and standard type sizes; use the
`text-ui-*` aliases only when matching existing compact UI text exactly.
Examples:
- `bg-panel text-text border border-border rounded-card shadow-token-sm`
- `bg-accent text-accent-foreground hover:bg-accent-hover`
- `bg-selection-overlay border-selection-outline ring-selection-outline`
The local base-layer border reset below keeps examples like
`border border-border` rendering solid borders while Preflight is omitted. */
@theme {
--color-*: initial;
/* Surfaces */
--color-bg: var(--bg);
--color-app: var(--bg-app);
--color-panel: var(--bg-panel);
--color-subtle: var(--bg-subtle);
--color-muted-surface: var(--bg-muted);
--color-elevated: var(--bg-elevated);
/* Borders */
--color-border: var(--border);
--color-border-strong: var(--border-strong);
--color-border-soft: var(--border-soft);
/* Text */
--color-text: var(--text);
--color-strong: var(--text-strong);
--color-muted: var(--text-muted);
--color-soft: var(--text-soft);
--color-faint: var(--text-faint);
/* Accent */
--color-accent: var(--accent);
--color-accent-strong: var(--accent-strong);
--color-accent-soft: var(--accent-soft);
--color-accent-tint: var(--accent-tint);
--color-accent-hover: var(--accent-hover);
--color-accent-wash: var(--accent-wash);
--color-accent-foreground: var(--accent-foreground);
/* Semantic status */
--color-success: var(--green);
--color-success-surface: var(--green-bg);
--color-success-border: var(--green-border);
--color-info: var(--blue);
--color-info-surface: var(--blue-bg);
--color-info-border: var(--blue-border);
--color-discovery: var(--purple);
--color-discovery-surface: var(--purple-bg);
--color-discovery-border: var(--purple-border);
--color-danger: var(--red);
--color-danger-surface: var(--red-bg);
--color-danger-border: var(--red-border);
--color-danger-foreground: var(--bg-panel);
--color-warning: var(--amber);
--color-warning-surface: var(--amber-bg);
--color-warning-border: var(--warning-border);
/* Interaction and overlays */
--color-focus: var(--accent);
--color-focus-ring: var(--accent-soft);
--color-overlay: var(--overlay);
--color-selection-overlay: var(--selection-overlay);
--color-selection-outline: var(--selection-outline);
--color-inspect-overlay: var(--inspect-overlay);
--color-control-hover: var(--bg-subtle);
--color-control-active: var(--bg-muted);
/* Radius */
--radius-control: var(--radius-sm);
--radius-card: var(--radius);
--radius-panel: var(--radius-lg);
--radius-token-pill: var(--radius-pill);
/* Shadows */
--shadow-token-xs: var(--shadow-xs);
--shadow-token-sm: var(--shadow-sm);
--shadow-token-md: var(--shadow-md);
--shadow-token-lg: var(--shadow-lg);
/* Fonts */
--font-sans: var(--sans);
--font-serif: var(--serif);
--font-mono: var(--mono);
/* Exact existing UI type sizes */
--text-ui-9: 9px;
--text-ui-10: 10px;
--text-ui-10_5: 10.5px;
--text-ui-11: 11px;
--text-ui-11_5: 11.5px;
--text-ui-12_5: 12.5px;
--text-ui-13: 13px;
--text-ui-13_5: 13.5px;
}
/* ============================================================
Open Design neutral product workspace
@ -29,6 +133,8 @@
--accent-soft: #f5d8cb;
--accent-tint: #fbeee5;
--accent-hover: #b45a3b;
--accent-wash: color-mix(in srgb, var(--accent) 12%, transparent);
--accent-foreground: #fff;
/* Semantic accent tints used by tool / status pills. */
--green: #1f7a3a;
@ -45,6 +151,12 @@
--red-border: #f5c6c2;
--amber: #b26200;
--amber-bg: #fff3e0;
--warning-border: color-mix(in srgb, var(--amber) 35%, transparent);
--overlay: rgba(28, 27, 26, 0.42);
--selection-overlay: rgba(22, 119, 255, 0.18);
--selection-outline: rgba(22, 119, 255, 0.55);
--inspect-overlay: rgba(37, 99, 235, 0.12);
--shadow-xs: 0 1px 0 rgba(28, 27, 26, 0.04);
--shadow-sm: 0 1px 2px rgba(28, 27, 26, 0.05), 0 1px 3px rgba(28, 27, 26, 0.04);
@ -165,6 +277,20 @@
}
}
/* Tailwind foundation policy: Preflight stays out of this slice. Retained
element/reset rules that may conflict with migrated utilities must live in
@layer base, be constrained to non-migrated scopes, or be removed before the
affected elements migrate. */
@layer base {
*,
::before,
::after,
::backdrop,
::file-selector-button {
border: 0 solid;
}
}
* { box-sizing: border-box; }
html, body, #root { height: 100%; margin: 0; }

View file

@ -44,7 +44,7 @@ let
# `nix build .#daemon` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-KF3Mld72/iau+pJmA7HvnanRx8VLtDP0N624SKrtrrc=";
pnpmDepsHash = "sha256-HFLm+8hv3o5x3Xem4MXNsNclIgiVRc70+EBafL0rVn8=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {

View file

@ -30,7 +30,7 @@ let
# `nix build .#web` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-KF3Mld72/iau+pJmA7HvnanRx8VLtDP0N624SKrtrrc=";
pnpmDepsHash = "sha256-HFLm+8hv3o5x3Xem4MXNsNclIgiVRc70+EBafL0rVn8=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {

View file

@ -14,7 +14,7 @@
"tools-dev": "pnpm exec tools-dev",
"tools-pack": "pnpm exec tools-pack",
"tools-pr": "pnpm exec tools-pr",
"guard": "tsx ./scripts/guard.ts",
"guard": "tsx ./scripts/guard.ts && node --import tsx --test scripts/style-policy.test.ts",
"i18n:check": "tsx ./scripts/i18n-check.ts",
"sync:community-pets": "node --experimental-strip-types scripts/sync-community-pets.ts",
"bake:community-pets": "node --experimental-strip-types scripts/bake-community-pets.ts",

View file

@ -83,7 +83,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.1)
version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.1)(lightningcss@1.32.0)
apps/desktop:
dependencies:
@ -108,7 +108,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
apps/landing-page:
dependencies:
@ -117,7 +117,7 @@ importers:
version: 3.7.2
astro:
specifier: ^5.15.4
version: 5.18.1(@types/node@20.19.39)(jiti@2.6.1)(rollup@4.60.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4)
version: 5.18.1(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4)
react:
specifier: ^18.3.1
version: 18.3.1
@ -182,7 +182,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
apps/telemetry-worker:
devDependencies:
@ -191,7 +191,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
apps/web:
dependencies:
@ -223,6 +223,9 @@ importers:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
devDependencies:
'@tailwindcss/postcss':
specifier: ^4.1.17
version: 4.3.0
'@testing-library/react':
specifier: ^16.3.2
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -241,12 +244,18 @@ importers:
jsdom:
specifier: 29.1.0
version: 29.1.0
postcss:
specifier: ^8.5.6
version: 8.5.12
tailwindcss:
specifier: ^4.1.17
version: 4.3.0
typescript:
specifier: ^5.6.3
version: 5.9.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.0)
version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.0)(lightningcss@1.32.0)
e2e:
devDependencies:
@ -264,7 +273,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.1)
version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.1)(lightningcss@1.32.0)
packages/contracts:
dependencies:
@ -280,7 +289,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
packages/platform:
devDependencies:
@ -295,7 +304,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
packages/sidecar:
devDependencies:
@ -310,7 +319,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
packages/sidecar-proto:
devDependencies:
@ -325,7 +334,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
tools/dev:
dependencies:
@ -393,7 +402,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)
version: 2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0)
tools/pr:
dependencies:
@ -419,6 +428,10 @@ packages:
7zip-bin@5.2.0:
resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==}
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@anthropic-ai/sdk@0.32.1':
resolution: {integrity: sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==}
@ -1241,9 +1254,22 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@malept/cross-spawn-promise@2.0.0':
resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==}
engines: {node: '>= 12.13.0'}
@ -1504,6 +1530,98 @@ packages:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
'@tailwindcss/node@4.3.0':
resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
'@tailwindcss/oxide-android-arm64@4.3.0':
resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.3.0':
resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.3.0':
resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.3.0':
resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.3.0':
resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.3.0':
resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==}
engines: {node: '>= 20'}
'@tailwindcss/postcss@4.3.0':
resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==}
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@ -2356,6 +2474,10 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
enhanced-resolve@5.21.3:
resolution: {integrity: sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@ -2927,6 +3049,80 @@ packages:
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@ -3909,6 +4105,13 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tailwindcss@4.3.0:
resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==}
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
@ -4578,6 +4781,8 @@ snapshots:
7zip-bin@5.2.0: {}
'@alloc/quick-lru@5.2.0': {}
'@anthropic-ai/sdk@0.32.1':
dependencies:
'@types/node': 18.19.130
@ -5220,8 +5425,25 @@ snapshots:
dependencies:
minipass: 7.1.3
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@malept/cross-spawn-promise@2.0.0':
dependencies:
cross-spawn: 7.0.6
@ -5415,6 +5637,75 @@ snapshots:
dependencies:
defer-to-connect: 2.0.1
'@tailwindcss/node@4.3.0':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.21.3
jiti: 2.6.1
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.3.0
'@tailwindcss/oxide-android-arm64@4.3.0':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.3.0':
optional: true
'@tailwindcss/oxide-darwin-x64@4.3.0':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.3.0':
optional: true
'@tailwindcss/oxide@4.3.0':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.3.0
'@tailwindcss/oxide-darwin-arm64': 4.3.0
'@tailwindcss/oxide-darwin-x64': 4.3.0
'@tailwindcss/oxide-freebsd-x64': 4.3.0
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0
'@tailwindcss/oxide-linux-arm64-gnu': 4.3.0
'@tailwindcss/oxide-linux-arm64-musl': 4.3.0
'@tailwindcss/oxide-linux-x64-gnu': 4.3.0
'@tailwindcss/oxide-linux-x64-musl': 4.3.0
'@tailwindcss/oxide-wasm32-wasi': 4.3.0
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
'@tailwindcss/postcss@4.3.0':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
postcss: 8.5.12
tailwindcss: 4.3.0
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.0
@ -5599,21 +5890,21 @@ snapshots:
chai: 5.3.3
tinyrainbow: 1.2.0
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.39))':
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 5.4.21(@types/node@20.19.39)
vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.12.2))':
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.12.2)(lightningcss@1.32.0))':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 5.4.21(@types/node@24.12.2)
vite: 5.4.21(@types/node@24.12.2)(lightningcss@1.32.0)
'@vitest/pretty-format@2.1.9':
dependencies:
@ -5835,7 +6126,7 @@ snapshots:
astral-regex@2.0.0:
optional: true
astro@5.18.1(@types/node@20.19.39)(jiti@2.6.1)(rollup@4.60.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4):
astro@5.18.1(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4):
dependencies:
'@astrojs/compiler': 2.13.1
'@astrojs/internal-helpers': 0.7.6
@ -5892,8 +6183,8 @@ snapshots:
unist-util-visit: 5.1.0
unstorage: 1.17.5
vfile: 6.0.3
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)
vitefu: 1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4))
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)
vitefu: 1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
@ -6501,6 +6792,11 @@ snapshots:
dependencies:
once: 1.4.0
enhanced-resolve@5.21.3:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
entities@4.5.0: {}
entities@6.0.1: {}
@ -7282,6 +7578,55 @@ snapshots:
dependencies:
immediate: 3.0.6
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-freebsd-x64: 1.32.0
lightningcss-linux-arm-gnueabihf: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
lodash@4.18.1: {}
longest-streak@3.1.0: {}
@ -8545,6 +8890,10 @@ snapshots:
symbol-tree@3.2.4: {}
tailwindcss@4.3.0: {}
tapable@2.3.3: {}
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
@ -8805,13 +9154,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-node@2.1.9(@types/node@20.19.39):
vite-node@2.1.9(@types/node@20.19.39)(lightningcss@1.32.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 5.4.21(@types/node@20.19.39)
vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -8823,13 +9172,13 @@ snapshots:
- supports-color
- terser
vite-node@2.1.9(@types/node@24.12.2):
vite-node@2.1.9(@types/node@24.12.2)(lightningcss@1.32.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 5.4.21(@types/node@24.12.2)
vite: 5.4.21(@types/node@24.12.2)(lightningcss@1.32.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -8841,7 +9190,7 @@ snapshots:
- supports-color
- terser
vite@5.4.21(@types/node@20.19.39):
vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0):
dependencies:
esbuild: 0.21.5
postcss: 8.5.12
@ -8849,8 +9198,9 @@ snapshots:
optionalDependencies:
'@types/node': 20.19.39
fsevents: 2.3.3
lightningcss: 1.32.0
vite@5.4.21(@types/node@24.12.2):
vite@5.4.21(@types/node@24.12.2)(lightningcss@1.32.0):
dependencies:
esbuild: 0.21.5
postcss: 8.5.12
@ -8858,8 +9208,9 @@ snapshots:
optionalDependencies:
'@types/node': 24.12.2
fsevents: 2.3.3
lightningcss: 1.32.0
vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4):
vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
@ -8871,17 +9222,18 @@ snapshots:
'@types/node': 20.19.39
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.32.0
tsx: 4.21.0
yaml: 2.8.4
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)):
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)):
optionalDependencies:
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.4)
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)
vitest@2.1.9(@types/node@20.19.39)(jsdom@29.1.0):
vitest@2.1.9(@types/node@20.19.39)(jsdom@29.1.0)(lightningcss@1.32.0):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.39))
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
@ -8897,8 +9249,8 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.1.1
tinyrainbow: 1.2.0
vite: 5.4.21(@types/node@20.19.39)
vite-node: 2.1.9(@types/node@20.19.39)
vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)
vite-node: 2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.19.39
@ -8914,10 +9266,10 @@ snapshots:
- supports-color
- terser
vitest@2.1.9(@types/node@20.19.39)(jsdom@29.1.1):
vitest@2.1.9(@types/node@20.19.39)(jsdom@29.1.1)(lightningcss@1.32.0):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.39))
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
@ -8933,8 +9285,8 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.1.1
tinyrainbow: 1.2.0
vite: 5.4.21(@types/node@20.19.39)
vite-node: 2.1.9(@types/node@20.19.39)
vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)
vite-node: 2.1.9(@types/node@20.19.39)(lightningcss@1.32.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.19.39
@ -8950,10 +9302,10 @@ snapshots:
- supports-color
- terser
vitest@2.1.9(@types/node@24.12.2)(jsdom@29.1.1):
vitest@2.1.9(@types/node@24.12.2)(jsdom@29.1.1)(lightningcss@1.32.0):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.12.2))
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.12.2)(lightningcss@1.32.0))
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
@ -8969,8 +9321,8 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.1.1
tinyrainbow: 1.2.0
vite: 5.4.21(@types/node@24.12.2)
vite-node: 2.1.9(@types/node@24.12.2)
vite: 5.4.21(@types/node@24.12.2)(lightningcss@1.32.0)
vite-node: 2.1.9(@types/node@24.12.2)(lightningcss@1.32.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.12.2

View file

@ -9,6 +9,7 @@ import {
checkDesignSystemTokenFixtureSync,
checkDesignSystemUnknownTokens,
} from "./check-tokens-fixture-sync.ts";
import { collectCssHardcodedColorMatches, cssWideAndSpecialColorKeywords, realNamedColors } from "./style-policy.ts";
const repoRoot = path.resolve(import.meta.dirname, "..");
const allowedE2eScripts = new Set([
@ -60,6 +61,8 @@ const residualAllowedExactPaths = new Set([
"apps/packaged/esbuild.config.mjs",
// Browser service workers must be served as JavaScript files.
"apps/web/public/od-notifications-sw.js",
// PostCSS loads Tailwind through a web-local .mjs compatibility config entry.
"apps/web/postcss.config.mjs",
"scripts/bake-html-ppt-examples.mjs",
"scripts/scaffold-html-ppt-skills.mjs",
"scripts/sync-hyperframes-skill.mjs",
@ -419,12 +422,278 @@ async function checkToolsLayout(): Promise<boolean> {
return true;
}
const stylePolicySkippedDirectories = new Set([
".next",
".od-data",
"dist",
"node_modules",
"out",
"reports",
"test-results",
]);
const stylePolicySourcePrefixes = ["apps/web/app/", "apps/web/src/"];
const stylePolicyHardcodedColorEnforcedPrefixes = ["scripts/guard-style-policy-fixtures/"];
const stylePolicyCheckedDirectoryPrefixes = [
...new Set([...stylePolicySourcePrefixes, ...stylePolicyHardcodedColorEnforcedPrefixes]),
];
const stylePolicyExtensions = new Set([".css", ".ts", ".tsx"]);
const tailwindDefaultColorNames = [
"slate",
"gray",
"zinc",
"neutral",
"stone",
"red",
"orange",
"amber",
"yellow",
"lime",
"green",
"emerald",
"teal",
"cyan",
"sky",
"blue",
"indigo",
"violet",
"purple",
"fuchsia",
"pink",
"rose",
"white",
"black",
].join("|");
const tailwindDefaultPaletteClassPrefixes = [
"bg",
"text",
"border(?:-(?:x|y|s|e|t|r|b|l))?",
"divide",
"placeholder",
"marker",
"from",
"via",
"to",
"ring(?:-offset)?",
"outline",
"decoration",
"(?:inset-|text-|drop-)?shadow",
"accent",
"caret",
"fill",
"stroke",
].join("|");
const defaultTailwindPaletteClassPattern = new RegExp(
`\\b(?:${tailwindDefaultPaletteClassPrefixes})-(?:${tailwindDefaultColorNames})(?:-\\d{2,3})?\\b`,
"g",
);
const hardcodedColorPattern = new RegExp(
`#[0-9a-fA-F]{3,8}\\b|rgba?\\([^)]*\\)|hsla?\\([^)]*\\)|(?<quote>['"])\\s*(?<named>${realNamedColors.join("|")}|transparent|currentColor|currentcolor|inherit|initial|unset|revert)\\s*\\k<quote>`,
"g",
);
type StylePolicyAllowlistEntry = {
pathPattern: RegExp;
valuePattern: RegExp;
reason: string;
};
const hardcodedColorAllowlist: StylePolicyAllowlistEntry[] = [
{
pathPattern: /^apps\/web\/src\/index\.css$/,
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
reason: "global token definitions, shadows, overlays, and retained migration inventory live in the CSS source of truth",
},
{
pathPattern: /^apps\/web\/src\/components\/(?:AgentIcon|PaletteTweaks|PetSettings|SettingsDialog)\.tsx$/,
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
reason: "brand accents, user accent choices, and legacy token fallbacks are classified as Phase 1 migration inventory",
},
{
pathPattern: /^apps\/web\/src\/components\/(?:SketchEditor|SketchPreview|NewProjectPanel)\.tsx$/,
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|['\"](?:none|currentColor|currentcolor|transparent)['\"])$/,
reason: "sketch/canvas data and SVG illustrations keep narrow hardcoded color exceptions until their migration slice",
},
{
pathPattern: /^apps\/web\/src\/components\/(?:FileViewer|ManualEditPanel)\.tsx$/,
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
reason: "user-authored file, inspect, and editable style colors are handled by the file/viewer migration slice",
},
{
pathPattern: /^apps\/web\/src\/components\/(?:MemorySection|MemoryModelInline|MemoryToast)\.tsx$/,
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
reason: "memory UI legacy color fallbacks are classified as Phase 1 migration inventory",
},
{
pathPattern: /^apps\/web\/tests\//,
valuePattern: /.*/,
reason: "tests and fixtures may assert rejected colors explicitly",
},
];
type StylePolicyViolation = {
filePath: string;
lineNumber: number;
match: string;
reason: string;
};
function lineNumberForIndex(source: string, index: number): number {
return source.slice(0, index).split("\n").length;
}
function isStylePolicySource(repositoryPath: string): boolean {
return stylePolicySourcePrefixes.some((prefix) => repositoryPath.startsWith(prefix));
}
function isHardcodedColorEnforcedPath(repositoryPath: string): boolean {
return stylePolicyHardcodedColorEnforcedPrefixes.some((prefix) => repositoryPath.startsWith(prefix));
}
function isHardcodedColorAllowlisted(repositoryPath: string, match: string): boolean {
const normalizedMatch = match.trim();
const unquotedMatch = normalizedMatch.replace(/^['"]|['"]$/g, "");
if (cssWideAndSpecialColorKeywords.has(unquotedMatch.toLowerCase())) return true;
return hardcodedColorAllowlist.some(
(entry) => entry.pathPattern.test(repositoryPath) && entry.valuePattern.test(normalizedMatch),
);
}
function addStylePolicyViolation(
violations: StylePolicyViolation[],
repositoryPath: string,
source: string,
index: number,
match: string,
reason: string,
): void {
violations.push({
filePath: repositoryPath,
lineNumber: lineNumberForIndex(source, index),
match,
reason,
});
}
function collectStylePolicyViolationsFromSource(repositoryPath: string, source: string): StylePolicyViolation[] {
const violations: StylePolicyViolation[] = [];
if (isStylePolicySource(repositoryPath)) {
for (const match of source.matchAll(defaultTailwindPaletteClassPattern)) {
violations.push({
filePath: repositoryPath,
lineNumber: lineNumberForIndex(source, match.index ?? 0),
match: match[0],
reason: "default Tailwind palette classes must use Open Design token utilities instead",
});
}
}
if (isStylePolicySource(repositoryPath) || isHardcodedColorEnforcedPath(repositoryPath)) {
if (repositoryPath.endsWith(".css") && isHardcodedColorEnforcedPath(repositoryPath)) {
for (const match of collectCssHardcodedColorMatches(source)) {
const value = match.value;
if (value === undefined || isHardcodedColorAllowlisted(repositoryPath, value)) continue;
addStylePolicyViolation(
violations,
repositoryPath,
source,
match.index,
value,
"unregistered hardcoded UI colors must use Open Design tokens or an explicit allowlist entry",
);
}
} else {
for (const match of source.matchAll(hardcodedColorPattern)) {
const value = match[0];
if (isHardcodedColorAllowlisted(repositoryPath, value)) continue;
if (!isHardcodedColorEnforcedPath(repositoryPath)) continue;
addStylePolicyViolation(
violations,
repositoryPath,
source,
match.index ?? 0,
value,
"unregistered hardcoded UI colors must use Open Design tokens or an explicit allowlist entry",
);
}
}
}
return violations;
}
async function collectStylePolicyViolations(directory: string): Promise<StylePolicyViolation[]> {
const entries = await readdir(directory, { withFileTypes: true });
const violations: StylePolicyViolation[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (stylePolicySkippedDirectories.has(entry.name)) continue;
violations.push(...(await collectStylePolicyViolations(fullPath)));
continue;
}
if (!entry.isFile() || !stylePolicyExtensions.has(path.extname(entry.name))) continue;
const repositoryPath = toRepositoryPath(fullPath);
if (!isStylePolicySource(repositoryPath) && !isHardcodedColorEnforcedPath(repositoryPath)) continue;
violations.push(...collectStylePolicyViolationsFromSource(repositoryPath, await readFile(fullPath, "utf8")));
}
return violations;
}
async function repositoryDirectoryExists(repositoryPath: string): Promise<boolean> {
const parentPath = path.join(repoRoot, path.dirname(repositoryPath));
const directoryName = path.basename(repositoryPath);
const entries = await readdir(parentPath, { withFileTypes: true });
return entries.some((entry) => entry.name === directoryName && entry.isDirectory());
}
async function collectStylePolicyViolationsFromCheckedPaths(): Promise<StylePolicyViolation[]> {
const violations: StylePolicyViolation[] = [];
for (const repositoryPrefix of stylePolicyCheckedDirectoryPrefixes) {
const repositoryDirectory = repositoryPrefix.replace(/\/$/, "");
if (!(await repositoryDirectoryExists(repositoryDirectory))) continue;
violations.push(...(await collectStylePolicyViolations(path.join(repoRoot, repositoryDirectory))));
}
return violations;
}
async function checkStylePolicy(): Promise<boolean> {
const violations = await collectStylePolicyViolationsFromCheckedPaths();
if (violations.length > 0) {
console.error("Style policy violations found:");
for (const violation of violations) {
console.error(`- ${violation.filePath}:${violation.lineNumber} \`${violation.match}\` -> ${violation.reason}`);
}
console.error("Use Open Design token utilities/CSS variables or add a narrow allowlist entry with a reason.");
return false;
}
console.log("Style policy check passed: Tailwind palette classes and enforced hardcoded UI colors stay token-first.");
return true;
}
const checks: GuardCheck[] = [
{ name: "residual JavaScript", run: checkResidualJavaScript },
{ name: "test layout", run: checkTestLayout },
{ name: "e2e layout", run: checkE2eLayout },
{ name: "web test layout", run: checkWebTestLayout },
{ name: "tools layout", run: checkToolsLayout },
{ name: "style policy", run: checkStylePolicy },
{ name: "design system token-fixture sync", run: checkDesignSystemTokenFixtureSync },
{ name: "design system A1 required tokens", run: checkDesignSystemA1RequiredTokens },
{ name: "design system A2 required tokens", run: checkDesignSystemA2RequiredTokens },

View file

@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import test from "node:test";
import { collectCssHardcodedColorMatches, collectCssNamedColorMatches } from "./style-policy.ts";
test("collectCssNamedColorMatches finds named colors inside CSS shorthands and functions", () => {
const source = [
".example { border: 1px solid red; }",
".gradient { background: linear-gradient(red, blue); }",
].join("\n");
assert.deepEqual(
collectCssNamedColorMatches(source).map((match) => match.value.toLowerCase()),
["red", "red", "blue"],
);
});
test("collectCssNamedColorMatches covers mixed-case and full CSS named colors", () => {
const source = ".example { border-color: RebeccaPurple; outline-color: tomato; }";
assert.deepEqual(
collectCssNamedColorMatches(source).map((match) => match.value),
["RebeccaPurple", "tomato"],
);
});
test("collectCssNamedColorMatches keeps CSS-wide special keywords exempt", () => {
const source = ".example { color: transparent; fill: currentColor; border-color: inherit; }";
assert.deepEqual(collectCssNamedColorMatches(source), []);
});
test("collectCssNamedColorMatches skips strings, comments, urls, and var references", () => {
const source = [
"/* .ignored { color: red; } */",
'.content { content: "green"; }',
'.content-declaration { content: "{ color: red; }"; }',
".comment { color: /* red */ var(--blue); }",
".asset { background: url('/icons/blue.svg'); }",
].join("\n");
assert.deepEqual(collectCssNamedColorMatches(source), []);
});
test("collectCssHardcodedColorMatches scans CSS var fallbacks", () => {
const source = ".example { color: var(--missing-red, red); background: var(--x, rgb(1 2 3)); }";
assert.deepEqual(
collectCssHardcodedColorMatches(source).map((match) => match.value),
["red", "rgb(1 2 3)"],
);
});
test("collectCssHardcodedColorMatches finds CSS colors in declaration values", () => {
const source = ".example { color: #ff0000; background: rgb(255 0 0); border-color: hsl(0 100% 50%); }";
assert.deepEqual(
collectCssHardcodedColorMatches(source).map((match) => match.value),
["#ff0000", "rgb(255 0 0)", "hsl(0 100% 50%)"],
);
});

400
scripts/style-policy.ts Normal file
View file

@ -0,0 +1,400 @@
export const cssWideAndSpecialColorKeywords = new Set([
"transparent",
"currentcolor",
"inherit",
"initial",
"unset",
"revert",
]);
export const realNamedColors = [
"aliceblue",
"antiquewhite",
"aqua",
"aquamarine",
"azure",
"beige",
"bisque",
"black",
"blanchedalmond",
"blue",
"blueviolet",
"brown",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cornsilk",
"crimson",
"cyan",
"darkblue",
"darkcyan",
"darkgoldenrod",
"darkgray",
"darkgreen",
"darkgrey",
"darkkhaki",
"darkmagenta",
"darkolivegreen",
"darkorange",
"darkorchid",
"darkred",
"darksalmon",
"darkseagreen",
"darkslateblue",
"darkslategray",
"darkslategrey",
"darkturquoise",
"darkviolet",
"deeppink",
"deepskyblue",
"dimgray",
"dimgrey",
"dodgerblue",
"firebrick",
"floralwhite",
"forestgreen",
"fuchsia",
"gainsboro",
"ghostwhite",
"gold",
"goldenrod",
"gray",
"green",
"greenyellow",
"grey",
"honeydew",
"hotpink",
"indianred",
"indigo",
"ivory",
"khaki",
"lavender",
"lavenderblush",
"lawngreen",
"lemonchiffon",
"lightblue",
"lightcoral",
"lightcyan",
"lightgoldenrodyellow",
"lightgray",
"lightgreen",
"lightgrey",
"lightpink",
"lightsalmon",
"lightseagreen",
"lightskyblue",
"lightslategray",
"lightslategrey",
"lightsteelblue",
"lightyellow",
"lime",
"limegreen",
"linen",
"magenta",
"maroon",
"mediumaquamarine",
"mediumblue",
"mediumorchid",
"mediumpurple",
"mediumseagreen",
"mediumslateblue",
"mediumspringgreen",
"mediumturquoise",
"mediumvioletred",
"midnightblue",
"mintcream",
"mistyrose",
"moccasin",
"navajowhite",
"navy",
"oldlace",
"olive",
"olivedrab",
"orange",
"orangered",
"orchid",
"palegoldenrod",
"palegreen",
"paleturquoise",
"palevioletred",
"papayawhip",
"peachpuff",
"peru",
"pink",
"plum",
"powderblue",
"purple",
"rebeccapurple",
"red",
"rosybrown",
"royalblue",
"saddlebrown",
"salmon",
"sandybrown",
"seagreen",
"seashell",
"sienna",
"silver",
"skyblue",
"slateblue",
"slategray",
"slategrey",
"snow",
"springgreen",
"steelblue",
"tan",
"teal",
"thistle",
"tomato",
"turquoise",
"violet",
"wheat",
"white",
"whitesmoke",
"yellow",
"yellowgreen",
];
const cssDeclarationPattern = /(?:^|[;{])\s*[-_a-zA-Z][-_a-zA-Z0-9]*\s*:\s*(?<value>[^;{}]+)/g;
const cssNamedColors = new Set(realNamedColors);
export type CssNamedColorMatch = {
index: number;
value: string;
};
const cssHexColorPattern = /^#[0-9a-fA-F]{3,8}\b/;
export function collectCssNamedColorMatches(source: string): CssNamedColorMatch[] {
return collectCssHardcodedColorMatches(source).filter((match) => cssNamedColors.has(match.value.toLowerCase()));
}
export function collectCssHardcodedColorMatches(source: string): CssNamedColorMatch[] {
const matches: CssNamedColorMatch[] = [];
const scannableSource = maskCssCommentsAndStrings(source);
for (const declaration of scannableSource.matchAll(cssDeclarationPattern)) {
const declarationValue = declaration.groups?.value;
if (declarationValue === undefined) continue;
const valueOffset = (declaration.index ?? 0) + declaration[0].lastIndexOf(declarationValue);
matches.push(...collectCssHardcodedColorMatchesFromDeclarationValue(declarationValue, valueOffset));
}
return matches;
}
function maskCssCommentsAndStrings(source: string): string {
const characters = source.split("");
let index = 0;
while (index < characters.length) {
const current = characters[index];
const next = characters[index + 1];
if (current === "/" && next === "*") {
const endIndex = source.indexOf("*/", index + 2);
const exclusiveEnd = endIndex === -1 ? characters.length : endIndex + 2;
maskRange(characters, index, exclusiveEnd);
index = exclusiveEnd;
continue;
}
if (current === '"' || current === "'") {
const exclusiveEnd = skipCssString(source, index, current);
maskRange(characters, index, exclusiveEnd);
index = exclusiveEnd;
continue;
}
index += 1;
}
return characters.join("");
}
function maskRange(characters: string[], startIndex: number, exclusiveEnd: number): void {
for (let index = startIndex; index < exclusiveEnd; index += 1) {
if (characters[index] !== "\n") characters[index] = " ";
}
}
function collectCssHardcodedColorMatchesFromDeclarationValue(
declarationValue: string,
sourceOffset: number,
): CssNamedColorMatch[] {
const matches: CssNamedColorMatch[] = [];
let index = 0;
while (index < declarationValue.length) {
const current = declarationValue[index];
const next = declarationValue[index + 1];
if (current === "/" && next === "*") {
const commentEnd = declarationValue.indexOf("*/", index + 2);
index = commentEnd === -1 ? declarationValue.length : commentEnd + 2;
continue;
}
if (current === '"' || current === "'") {
index = skipCssString(declarationValue, index, current);
continue;
}
const hexColor = declarationValue.slice(index).match(cssHexColorPattern)?.[0];
if (hexColor !== undefined) {
matches.push({ index: sourceOffset + index, value: hexColor });
index += hexColor.length;
continue;
}
const functionName = readCssIdentifier(declarationValue, index);
if (functionName !== undefined && functionName.value.toLowerCase() === "url") {
const functionStart = skipCssWhitespace(declarationValue, functionName.endIndex);
if (declarationValue[functionStart] === "(") {
index = skipCssFunction(declarationValue, functionStart);
continue;
}
}
if (functionName !== undefined && functionName.value.toLowerCase() === "var") {
const functionStart = skipCssWhitespace(declarationValue, functionName.endIndex);
if (declarationValue[functionStart] === "(") {
const functionEnd = skipCssFunction(declarationValue, functionStart);
const fallbackStart = cssVarFallbackStartIndex(declarationValue, functionStart, functionEnd);
if (fallbackStart !== undefined) {
matches.push(
...collectCssHardcodedColorMatchesFromDeclarationValue(
declarationValue.slice(fallbackStart, functionEnd - 1),
sourceOffset + fallbackStart,
),
);
}
index = functionEnd;
continue;
}
}
if (functionName !== undefined && ["rgb", "rgba", "hsl", "hsla"].includes(functionName.value.toLowerCase())) {
const functionStart = skipCssWhitespace(declarationValue, functionName.endIndex);
if (declarationValue[functionStart] === "(") {
const functionEnd = skipCssFunction(declarationValue, functionStart);
matches.push({ index: sourceOffset + index, value: declarationValue.slice(index, functionEnd) });
index = functionEnd;
continue;
}
}
const identifier = readCssIdentifier(declarationValue, index);
if (identifier === undefined) {
index += 1;
continue;
}
const normalizedValue = identifier.value.toLowerCase();
if (cssNamedColors.has(normalizedValue) && !cssWideAndSpecialColorKeywords.has(normalizedValue)) {
matches.push({ index: sourceOffset + index, value: identifier.value });
}
index = identifier.endIndex;
}
return matches;
}
function readCssIdentifier(source: string, startIndex: number): { value: string; endIndex: number } | undefined {
const start = source[startIndex];
if (start === undefined || !/[A-Za-z_]/.test(start)) return undefined;
let endIndex = startIndex + 1;
while (endIndex < source.length && /[-_A-Za-z0-9]/.test(source[endIndex] ?? "")) {
endIndex += 1;
}
return { value: source.slice(startIndex, endIndex), endIndex };
}
function skipCssString(source: string, startIndex: number, quote: string): number {
let index = startIndex + 1;
while (index < source.length) {
const current = source[index];
if (current === "\\") {
index += 2;
continue;
}
if (current === quote) return index + 1;
index += 1;
}
return source.length;
}
function skipCssWhitespace(source: string, startIndex: number): number {
let index = startIndex;
while (index < source.length && /\s/.test(source[index] ?? "")) index += 1;
return index;
}
function skipCssFunction(source: string, openParenIndex: number): number {
let depth = 1;
let index = openParenIndex + 1;
while (index < source.length) {
const current = source[index];
const next = source[index + 1];
if (current === "/" && next === "*") {
const commentEnd = source.indexOf("*/", index + 2);
index = commentEnd === -1 ? source.length : commentEnd + 2;
continue;
}
if (current === '"' || current === "'") {
index = skipCssString(source, index, current);
continue;
}
if (current === "(") depth += 1;
if (current === ")") {
depth -= 1;
if (depth === 0) return index + 1;
}
index += 1;
}
return source.length;
}
function cssVarFallbackStartIndex(source: string, openParenIndex: number, functionEndIndex: number): number | undefined {
let depth = 0;
let index = openParenIndex + 1;
while (index < functionEndIndex - 1) {
const current = source[index];
const next = source[index + 1];
if (current === "/" && next === "*") {
const commentEnd = source.indexOf("*/", index + 2);
index = commentEnd === -1 ? functionEndIndex - 1 : Math.min(commentEnd + 2, functionEndIndex - 1);
continue;
}
if (current === '"' || current === "'") {
index = skipCssString(source, index, current);
continue;
}
if (current === "(") depth += 1;
if (current === ")") depth -= 1;
if (current === "," && depth === 0) return index + 1;
index += 1;
}
return undefined;
}

View file

@ -4,6 +4,131 @@
<!-- Files created/modified; implementation decisions; migration inventory/classification; retained/deferred rationale; problems encountered; deviations from design -->
### Step 1: Tailwind foundations
- `apps/web/package.json` / `pnpm-lock.yaml` - added Tailwind v4 foundation dependencies: `tailwindcss`, `@tailwindcss/postcss`, and `postcss`.
- `apps/web/postcss.config.mjs` - added the web-local PostCSS config that loads `@tailwindcss/postcss`.
- `scripts/guard.ts` - allowlisted the exact PostCSS config path with a compatibility-format comment so the residual JavaScript guard continues to fail on unplanned project-owned JavaScript.
- `apps/web/src/index.css` - added Tailwind theme/utilities layered imports, kept Preflight excluded, added the local base-layer border-style reset, and recorded the cascade policy for retained element/reset rules before component migration.
### Step 2: Open Design Tailwind tokens
- `apps/web/src/index.css` - added the CSS-first `@theme` block that clears Tailwind default colors and exposes the project-approved color namespace for surfaces, borders, text, accent, semantic status, interaction overlays, radius, shadows, fonts, and exact compact UI text-size aliases.
- `apps/web/src/index.css` - added missing runtime source variables for `--accent-wash`, `--accent-foreground`, `--warning-border`, modal overlay, selection overlays, and inspect overlays so Tailwind utilities resolve through the same CSS-variable token path as existing styles.
- `apps/web/src/index.css` - documented the token utility vocabulary next to the `@theme` block, including representative border/radius/shadow examples and the no-Preflight border reset expectation for `border border-border`.
- Token resolution remains CSS-variable-first: light, dark, and system modes update the existing token variables through `:root`, `[data-theme="dark"]`, and `html:not([data-theme])`; custom accent continues to update `--accent*` variables through the pre-hydration script and `applyAppearanceToDocument()`.
### Step 3: Base style guardrails
- `scripts/guard.ts` - added the `style policy` guard check for app UI files under `apps/web/app/` and `apps/web/src/`.
- `scripts/guard.ts` - added default Tailwind palette utility rejection for classes such as `text-red-500`, `bg-white`, `border-zinc-200`, `from-orange-500`, and related color utility namespaces so app UI uses Open Design token utilities.
- `scripts/guard.ts` - added hardcoded UI color detection scaffolding for hex, `rgb()` / `rgba()`, `hsl()` / `hsla()`, and named colors. Phase 1 enforcement is wired to scoped guard fixtures while existing app hardcoded colors stay classified as migration inventory or explicit exceptions until the relevant migration slices tighten enforcement.
- `scripts/guard.ts` - added a structured hardcoded-color allowlist with path pattern, value pattern, and reason fields for global token CSS, brand/accent choices, SVG illustrations, sketch/canvas data, file/inspect/user-authored colors, legacy UI fallback colors, and tests/fixtures.
- `scripts/guard.ts` - exempted CSS-wide and special color keywords `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` by semantics.
### Step 4: Migration inventory and visual comparison prep
- Migration inventory method: compared literal class tokens referenced from `apps/web/src/**/*.tsx` against class selectors defined in `apps/web/src/index.css`. Current scan covers 59 TSX files, 1,492 literal class tokens, and 1,334 literal TSX class tokens with matching `index.css` definitions. The remaining 158 literal tokens are Tailwind-ready local values, data/status words, generated strings, or classes defined outside `index.css`; migration slices must re-run the scan after rebase and classify any changed class at implementation time.
- High-volume TSX sources for global class migration:
| Area / file | Matching `index.css` class count | Representative classes |
| --- | ---: | --- |
| Settings dialog and settings sections: `apps/web/src/components/SettingsDialog.tsx`, `McpClientSection.tsx`, `SkillsSection.tsx`, `MemorySection.tsx`, `DesignSystemsSection.tsx`, `PrivacySection.tsx` | 430+ | `modal-backdrop`, `modal`, `modal-settings`, `settings-chrome`, `settings-section`, `section-head`, `hint`, `agent-card`, `library-toolbar`, `filter-pill` |
| File viewer / inspect / edit surfaces: `apps/web/src/components/FileViewer.tsx`, `ManualEditPanel.tsx`, `FileWorkspace.tsx` | 208+ | `viewer`, `viewer-toolbar`, `viewer-action`, `viewer-body`, `viewer-tab`, `manual-edit-panel-head`, `ws-tabs-shell` |
| Project creation / project panels / examples / designs: `NewProjectPanel.tsx`, `DesignFilesPanel.tsx`, `ConnectorsBrowser.tsx`, `DesignsTab.tsx`, `ExamplesTab.tsx`, `DesignSystemsTab.tsx`, `PromptTemplatesTab.tsx` | 330+ | `newproj`, `entry`, `df-file-row`, `connector-logo`, `tab-panel-toolbar`, `design-card`, `example-card`, `ds-card` |
| Shell, chat, composer, common controls: `ProjectView.tsx`, `AppChromeHeader.tsx`, `ChatPane.tsx`, `ChatComposer.tsx`, `AssistantMessage.tsx`, `EntryView.tsx`, `ConversationsMenu.tsx`, `AvatarMenu.tsx`, `QuickSwitcher.tsx` | 230+ | `app`, `app-chrome-header`, `pane`, `chat-log`, `composer-shell`, `msg`, `assistant`, `entry-shell`, `conv-menu`, `avatar-popover`, `qs-palette` |
| Sketch, runtime, pet, loading, feedback, modals: `SketchEditor.tsx`, `PetRail.tsx`, `PetSettings.tsx`, `PetOverlay.tsx`, `Loading.tsx`, `ToolCard.tsx`, `MessageFeedback.tsx`, `PromptTemplatePreviewModal.tsx`, `QuestionForm.tsx`, `runtime/markdown.tsx` | 190+ | `sketch-editor`, `pet-rail`, `pet-codex-thumb`, `loading-spinner`, `skeleton-shimmer`, `op-card`, `message-feedback`, `prompt-template-modal`, `md-p` |
- Class classification:
| Classification | Current inventory | Migration action |
| --- | --- | --- |
| Component-level migratable styles | Most referenced global classes, including app shell, settings, project panels, viewer chrome, chat/composer, cards, buttons, popovers, modals, pet UI, sketch editor chrome, live artifact cards, and status surfaces. | Replace with static token-first Tailwind utility strings during the owning phase, then delete the migrated selector from `index.css`. Use `token.md` aliases such as `bg-panel`, `text-muted`, `border-border`, `rounded-card`, `rounded-panel`, `rounded-token-pill`, `shadow-token-sm`, `shadow-token-md`, `font-mono`, and exact `text-ui-*` sizes when needed for parity. |
| Global base styles | `:root` tokens, theme overrides, `*`, `html, body, #root`, `body`, shared `input` / `textarea` / `select`, and content-neutral `code`. Sources: `apps/web/src/index.css:1-306,367-428`. | Retain as global/token source or move into `@layer base` before affected utilities migrate. Component slices may remove/constrain base selectors when they would override migrated utility properties. |
| Loading shell | `od-loading-shell`, used by the client-only loading path. Sources: `apps/web/app/[[...slug]]/client-app.tsx:5-13`; `apps/web/src/index.css:308-315`. | Retain global because it renders before the SPA tree is available. Token utility migration happens inside loaded TSX components. |
| Keyframes / animation | `skeleton-shimmer`, modal/settings transitions, spinner rules, and keyframes. Representative source: `apps/web/src/components/Loading.tsx:42`; `apps/web/src/index.css:1121-1143,12049`. | Retain keyframes globally. Migrate the element box/color/layout class around an animation to Tailwind while keeping the animation name global until a later animation utility strategy exists. |
| Content-level / third-party boundary styles | Markdown/runtime/viewer content classes such as `markdown-rendered`, `markdown-status`, `prose-block`, `viewer-source`, `viewer-body`, `viewer-empty`, `md-p`, `md-code`, and file/rendering boundaries. Sources: `apps/web/src/components/FileViewer.tsx:6407-6412`; `apps/web/src/runtime/markdown.tsx:112-196`; `apps/web/src/index.css:9754-9779,10149-10153`. | Retain or migrate only the app chrome around the boundary. User-authored content, generated HTML, syntax/file previews, iframes, and external documents stay in retained exception scopes. |
| Retained exceptions | Brand icons, SVG illustrations, sketch/canvas user data, user accent controls, file/user-authored colors, test fixtures, and token definitions. Sources: `scripts/guard.ts:503-534`. | Keep a narrow allowlist entry with a path pattern, value pattern, and reason. Revisit each exception in its owning migration phase. |
- Unlayered selector resolution before migration:
| Selector | Source | Resolution |
| --- | --- | --- |
| `button`, `button.primary`, `button.primary-ghost`, `button.ghost`, `button.subtle`, `button.icon-btn`, `button:disabled` | `apps/web/src/index.css:317-365` | Phase 2 migrates button variants to token utilities. Move shared reset-only behavior such as `font: inherit`, focus outline, disabled opacity/cursor into `@layer base` or a constrained primitive scope; remove variant visuals from global CSS after TSX callers migrate. |
| `input`, `textarea`, `select`, placeholder/focus/select chevron rules | `apps/web/src/index.css:367-419` | Phase 2 migrates controls where they are component chrome. Retain native select chevron/base reset in `@layer base` or constrain by form scope before utility-driven inputs rely on their own borders, radius, padding, and focus rings. |
| `code` | `apps/web/src/index.css:421-428` | Treat as content/base code styling; migrate component-local code chips to Tailwind utilities and retain global `code` only for rich-text/content boundaries. |
- Token-first migration note patterns for component classes:
| Existing visual pattern | Utility combination to prefer |
| --- | --- |
| Panel/card surfaces with border, radius, shadow | `bg-panel border border-border rounded-card shadow-token-sm text-text` or `bg-elevated rounded-panel shadow-token-md` for overlays |
| Muted copy, labels, metadata, hints | `text-muted`, `text-soft`, `text-faint`, plus `text-ui-11`, `text-ui-12_5`, or `text-xs` based on measured parity |
| Primary/accent actions | `bg-accent text-accent-foreground border border-accent hover:bg-accent-hover rounded-control` |
| Ghost/subtle controls | `bg-transparent text-text border border-border hover:bg-control-hover hover:border-border-strong rounded-control` or `bg-subtle hover:bg-control-active` |
| Status pills and banners | `bg-success-surface text-success border-success-border`, `bg-info-surface text-info border-info-border`, `bg-danger-surface text-danger border-danger-border`, `bg-warning-surface text-warning border-warning-border` |
| Inspect/comment overlays | `bg-selection-overlay border-selection-outline ring-selection-outline` and `bg-inspect-overlay` |
| Popovers/modals | `bg-elevated text-text border border-border rounded-panel shadow-token-lg` plus `bg-overlay` for scrims |
| Code/file path text | `font-mono text-ui-12_5 bg-subtle text-text rounded-control` unless the content boundary needs retained global rich-text CSS |
- Style guard allowlist and promotion policy:
| Scope | Pattern | Reason / follow-up |
| --- | --- | --- |
| `apps/web/src/index.css` | Hex, RGB(A), HSL(A) | CSS variable token definitions, shadows, overlays, and retained migration inventory remain the source of truth until cleanup. |
| `AgentIcon`, `PaletteTweaks`, `PetSettings`, `SettingsDialog` | Hex, RGB(A), HSL(A) | Brand accents, user accent choices, and legacy token fallbacks; each owning phase must either migrate to tokens or keep a narrower exception. |
| `SketchEditor`, `SketchPreview`, `NewProjectPanel` | Hex, RGB(A), HSL(A), `none`, `currentColor`, `transparent` | Sketch/canvas user data and SVG illustrations; migrate app chrome colors, retain user/illustration data colors. |
| `FileViewer`, `ManualEditPanel` | Hex, RGB(A), HSL(A) | User-authored file, inspect, editable style, and runtime content colors; app chrome migrates in Phase 4. |
| `MemorySection`, `MemoryModelInline`, `MemoryToast` | Hex, RGB(A), HSL(A) | Legacy memory UI fallback colors; migrate or narrow in the settings/project phases. |
| `apps/web/tests/` | Any | Tests and fixtures may assert rejected colors explicitly. |
| Repeated arbitrary app UI colors | Any unregistered hardcoded color after the second real app UI use | Promote to a named Open Design token before migration. One-off brand/user-content/illustration data keeps a reasoned allowlist entry. |
- Inventory verification: the Step 4 scan produced complete coverage for literal class tokens by cross-checking TSX references against `index.css` class selector definitions. Migration slices remain responsible for dynamic class maps and classes changed after rebase.
### Step 5: Dual-worktree agent visual comparison workflow
- Baseline worktree: `/Users/william/projects/open-design` on `main`; candidate worktree: `/Users/william/projects/open-design-wt-tailwind-phase-1` on `tailwind-phase-1`.
- Fixed service assignments for Phase 1 and follow-up migration slices:
| Role | Namespace | Daemon port | Web port | URL |
| --- | --- | ---: | ---: | --- |
| Baseline | `tailwind-baseline` | `18110` | `18111` | `http://127.0.0.1:18111` |
| Candidate | `tailwind-candidate` | `18120` | `18121` | `http://127.0.0.1:18121` |
- Startup commands:
```bash
# In /Users/william/projects/open-design
pnpm tools-dev run web --namespace tailwind-baseline --daemon-port 18110 --web-port 18111
# In /Users/william/projects/open-design-wt-tailwind-phase-1
pnpm tools-dev run web --namespace tailwind-candidate --daemon-port 18120 --web-port 18121
```
- Scenario list for the agent comparison record:
| Scenario | Route / state | Required evidence |
| --- | --- | --- |
| Dashboard / app shell | `/` with the same local data state in both worktrees | Full-viewport screenshots and notes on shell spacing, page background, nav/header controls, text color, and accent buttons. |
| Project detail | Open the same project in both services, seeded or selected through the production HTTP/UI flow | Screenshots of the project header, chat pane, composer, side panels, status surfaces, and shared controls. |
| Settings dialog | Open settings from the app UI in both services | Screenshots of modal scrim, modal radius/shadow, settings chrome, sections, buttons, form controls, pills, and disabled states. |
| File viewer / inspect overlay | Open the same file or artifact in both services and enable inspect/edit overlay state where available | Screenshots of viewer toolbar/body, tabs, overlays, selection/comment affordances, code/file text, and any source preview boundaries. |
| Sketch editor | Open the same sketch surface or fixture project in both services | Screenshots of editor chrome, canvas-adjacent controls, toolbar buttons, popovers, and user-content color boundaries. |
| Live artifact card | Open the same artifact/runtime surface in both services | Screenshots of card shell, iframe/runtime boundary, refreshing/failed/success status badges, and action controls. |
| Modal / popover / control states | Trigger representative menus, quick switcher/avatar/conversation menus, confirmation/question modals, hover/focus/disabled states where practical | Paired screenshots and drift notes for overlay elevation, radius, border color, focus ring, and hover state. |
- Viewport and browser state: use `1440x1000` viewport at device scale factor `1`; compare the same scroll position and the same opened dialogs/popovers; avoid mixed zoom or retained browser state between baseline and candidate sessions.
- Theme/accent matrix:
| Mode | Required state |
| --- | --- |
| Light | `data-theme="light"`, default accent. |
| Dark | `data-theme="dark"`, default accent. |
| System | Clear explicit `data-theme`; run with a recorded OS/browser color-scheme setting. |
| Custom accent | Apply the same custom accent in both services through the real settings UI or the same documented localStorage/document-token setup before capture. |
- Fixture data policy: seed through product UI or production HTTP APIs only; record project/artifact/file IDs or fixture creation steps in the phase note. Do not use source-level test backdoors for visual acceptance fixtures.
- Screenshot artifact requirements: store captures under `.tmp/visual-comparison/<phase>/<scenario>/<theme>/` in the candidate worktree, with paired names like `baseline-dashboard-light.png` and `candidate-dashboard-light.png`; include any annotated screenshots only as supporting artifacts. The phase note must list artifact paths, service URLs, viewport, theme/accent state, fixture identifiers, drift decisions, and approved deviations.
- Component source inspection guidance: use screenshots as the primary acceptance artifact. Inspect component source only when a screenshot shows drift or a migrated global class needs traceability to its token-first utility replacement; cite the component path/class or selector and record whether the fix was made or deviation approved.
- Drift handling: layout offsets, token color changes, radius/shadow changes, focus/hover changes, or theme/accent state mismatches must be fixed in the migration slice or listed as an approved deviation with owner/reason. Missing fixture data is a failed comparison setup, so stop and seed the shared data rather than accepting empty screenshots.
- Phase notes format for each visual slice:
```markdown
### Agent visual comparison
- Baseline: <worktree>, namespace `<name>`, URL `<url>`, command `<command>`
- Candidate: <worktree>, namespace `<name>`, URL `<url>`, command `<command>`
- Viewport: 1440x1000 @1x
- Fixture data: <project/artifact/file IDs or setup steps>
- Matrix covered: <scenarios × themes/accent states>
- Screenshot artifacts: <paths>
- Component source inspected: <paths/selectors and reason>
- Drift found: <items>
- Fixes made / approved deviations: <items>
```
### Implementation requirements
- Tailwind no-Preflight setup must use the official layered CSS imports in `apps/web/src/index.css`:
```css
@layer theme, base, utilities;
@ -22,3 +147,33 @@
## Verification
<!-- Commands run and results; screenshot artifact links/paths; exact baseline/development startup parameters or full commands; baseline/development service URLs; baseline/development namespace names; agent comparison scenario coverage; theme/accent matrix covered; observed drift; approved deviations -->
- `pnpm install` - passed; pnpm emitted existing workspace bin/link warnings for missing daemon dist CLI during install.
- `pnpm guard` - passed; residual JavaScript allowlist accepts `apps/web/postcss.config.mjs`.
- `pnpm --filter @open-design/web build` - passed with Next.js 16/Turbopack.
- `pnpm --filter @open-design/web build` - passed after adding the Open Design `@theme` token aliases and source variables.
- `pnpm guard` - passed after adding the style policy check; known Phase 1 hardcoded color migration inventory remains classified and does not fail the guard.
- Temporary sample `apps/web/src/__guard_tailwind_palette_sample.tsx` with `className="text-red-500"` plus temporary sample `scripts/guard-style-policy-fixtures/hardcoded-color-sample.tsx` with `color: "#ff0000"` made `pnpm guard` fail with both expected style policy violations; the sample also included `transparent`, `currentColor`, `currentcolor`, `inherit`, `initial`, `unset`, and `revert`, which stayed exempt. Temporary samples were removed.
- Temporary sample `scripts/guard-style-policy-fixtures/named-color-sample.tsx` with `color: "red"` made `pnpm guard` fail with the expected unregistered named color violation while CSS-wide/special keywords in the same fixture stayed exempt. Temporary sample was removed.
- `pnpm guard` - passed after removing the temporary style policy samples.
- `pnpm --filter @open-design/web test` - passed; 99 files / 925 tests.
- `pnpm typecheck` - passed.
- `pnpm guard` - passed after adding the Step 4 migration inventory and guard allowlist policy notes.
- `pnpm typecheck` - passed after adding the Step 4 documentation.
- `pnpm --filter @open-design/web test` - passed; 99 files / 925 tests.
- `pnpm --filter @open-design/web build` - passed with Next.js 16/Turbopack.
- Dual-worktree services started successfully for the Phase 1 workflow smoke run:
- Baseline: `/Users/william/projects/open-design`, namespace `tailwind-baseline`, daemon `http://127.0.0.1:18110`, web `http://127.0.0.1:18111`, command `pnpm tools-dev run web --namespace tailwind-baseline --daemon-port 18110 --web-port 18111`.
- Candidate: `/Users/william/projects/open-design-wt-tailwind-phase-1`, namespace `tailwind-candidate`, daemon `http://127.0.0.1:18120`, web `http://127.0.0.1:18121`, command `pnpm tools-dev run web --namespace tailwind-candidate --daemon-port 18120 --web-port 18121`.
- `pnpm tools-dev status --namespace tailwind-baseline --json` and `pnpm tools-dev status --namespace tailwind-candidate --json` - both reported daemon/web running; desktop remained idle, which is expected for web-only comparison.
- Agent visual comparison smoke run:
- Tooling: agent-browser CLI opened both web URLs in isolated sessions and captured paired Dashboard/app shell screenshots; Chrome DevTools MCP captured a candidate accessibility snapshot and paired Dashboard/app shell screenshots at a 1440x1000 viewport.
- Scenario/theme covered in Phase 1 smoke run: Dashboard/app shell, light/default accent, empty local data state in both worktrees.
- agent-browser artifacts: `.tmp/visual-comparison/phase1/dashboard/light/baseline-dashboard-light.png` and `.tmp/visual-comparison/phase1/dashboard/light/candidate-dashboard-light.png`.
- Chrome DevTools MCP artifacts: `.tmp/visual-comparison/phase1/dashboard/light/baseline-dashboard-light-devtools-1440x1000.png`, `.tmp/visual-comparison/phase1/dashboard/light/candidate-dashboard-light-devtools-1440x1000.png`, and `.tmp/visual-comparison/phase1/dashboard/light/candidate-devtools-snapshot.txt`.
- `sips -g pixelWidth -g pixelHeight ... && cmp -s ...` - passed for the agent-browser screenshots and the Chrome DevTools MCP screenshots; dimensions matched and the captured Dashboard/app shell images were byte-identical for the Phase 1 smoke run.
- Drift found: none in the covered smoke scenario.
- Approved deviations: none.
- Full scenario/theme/accent coverage is now defined as the required gate for migration slices. Phase 1 recorded the startup parameters, artifact contract, fixture policy, scenario matrix, theme/accent matrix, and notes format; later phases must seed the same project/artifact/file data in both services before checking project detail, settings, file viewer/inspect, sketch editor, live artifact, modal/popover, dark/system, and custom-accent states.
- `pnpm guard` - passed after recording the dual-worktree visual comparison workflow.
- `pnpm typecheck` - passed after recording the dual-worktree visual comparison workflow.

View file

@ -109,8 +109,8 @@ created: '2026-05-09'
- Scope: `apps/web` style toolchain. Impact: add Tailwind v4/PostCSS dependencies and config at the web package boundary because `@open-design/web` owns `dev/build/typecheck/test` scripts and currently declares no Tailwind/PostCSS dependencies. Source: `apps/web/package.json:23-50`; `https://tailwindcss.com/docs/guides/nextjs`
- Scope: `apps/web/src/index.css`. Impact: keep CSS variables, dark/system theme overrides, reset, body styles, loading shell, keyframes, and truly global content styles; add Tailwind import/theme/base layers in the same entry so the existing `layout.tsx` import remains the only global CSS entry, move retained conflicting element/reset rules into `@layer base` or constrain them before the affected TSX migration, and remove component-level global classes that have moved to TSX. Source: `apps/web/app/layout.tsx:1-4`; `apps/web/src/index.css:6-181,1121-1143,6219-6299`
- Scope: existing `apps/web/src/**/*.tsx`. Impact: migrate replaceable global CSS classes to token-first Tailwind `className` values by page/component area while keeping DOM structure and component responsibilities stable. Source: `apps/web/src/index.css:183-219`; `apps/web/src/**/*.tsx`
- Scope: token mapping. Impact: expose existing color, radius, shadow, and font CSS variables as Tailwind theme variables while preserving runtime custom accent behavior that writes to the same `--accent*` variables; use native Tailwind utilities for spacing and font size. Source: `apps/web/src/index.css:6-63`; `apps/web/src/state/appearance.ts:17-52`; `apps/web/app/layout.tsx:21-29`; `specs/change/20260509-token-first-tailwind/token.md`; `https://tailwindcss.com/docs/theme`
- Scope: constraints. Impact: extend the repository guard to explicitly check default Tailwind palette classes and uncontrolled hardcoded colors, with allowlists for brand/user-content scenarios. Source: `scripts/guard.ts:138-151,205-221`; `apps/web/src/components/AgentIcon.tsx:46-99`; `apps/web/src/components/SketchEditor.tsx:72,144-149`; `apps/web/src/components/FileViewer.tsx:1448-1474`
- Scope: token mapping. Impact: expose existing color, radius, shadow, and font CSS variables as Tailwind theme variables while preserving runtime custom accent behavior that writes to the same `--accent*` variables; use native Tailwind utilities for spacing and standard typography sizes, with exact `text-ui-*` aliases for existing non-standard UI sizes where visual parity requires them. Source: `apps/web/src/index.css:6-63`; `apps/web/src/state/appearance.ts:17-52`; `apps/web/app/layout.tsx:21-29`; `specs/change/20260509-token-first-tailwind/token.md`; `https://tailwindcss.com/docs/theme`
- Scope: constraints. Impact: extend the repository guard to reject default Tailwind palette classes, add hardcoded color detection with staged enforcement, and maintain allowlists for brand/user-content scenarios. Source: `scripts/guard.ts:138-151,205-221`; `apps/web/src/components/AgentIcon.tsx:46-99`; `apps/web/src/components/SketchEditor.tsx:72,144-149`; `apps/web/src/components/FileViewer.tsx:1448-1474`
- Scope: testing and validation. Impact: web-owned tests live in `apps/web/tests/`; validate through `pnpm guard`, `pnpm typecheck`, `pnpm --filter @open-design/web test`, and `pnpm --filter @open-design/web build`. Source: `apps/AGENTS.md:19-24,39-51`; `AGENTS.md#Validation strategy`
- Scope: agent visual consistency validation. Impact: each development slice uses a baseline worktree and development worktree, each running its own web/daemon pair; the agent compares the same scenarios across both services through the agent-browser CLI and Chrome DevTools MCP, using screenshots as the primary comparison record and component source inspection as auxiliary evidence, to validate consistent frontend display before and after the refactor. Source: `AGENTS.md:40-45,82-89,91-104`; `apps/web/src/index.css:65-157`; `apps/web/src/state/appearance.ts:28-52`
@ -140,7 +140,7 @@ created: '2026-05-09'
- Toolchain: run `pnpm install`, then `pnpm --filter @open-design/web build`, proving the Next/Tailwind/PostCSS integration compiles. Source: `apps/web/package.json:23-29`; `AGENTS.md#Validation strategy`
- Type safety: after config and TS guard changes, run `pnpm typecheck` and `pnpm --filter @open-design/web typecheck`. Source: `AGENTS.md#Validation strategy`; `apps/AGENTS.md:39-51`
- Constraint mechanism: add/extend guard coverage for disallowed default palette classes, and stage hardcoded UI color enforcement so Phase 1 only fails files or patterns already cleaned in that PR; later migration phases tighten the scope until Phase 6 runs the strict whole-app check. Source: `scripts/guard.ts:138-151,205-221,401-422`
- Constraint mechanism: add/extend guard coverage for disallowed default palette classes, and add hardcoded UI color detection plus allowlist scaffolding in Phase 1. Existing hardcoded UI colors stay classified as migration inventory or explicit exceptions until the component migration phases tighten enforcement by area; Phase 6 runs the strict app UI check. Source: `scripts/guard.ts:138-151,205-221,401-422`
- Web tests: when adding style-policy helper logic, add focused Vitest coverage under `apps/web/tests/`. Source: `apps/AGENTS.md:19-24`; `apps/web/package.json:23-29`
- Agent visual consistency validation: run `pnpm tools-dev run web --namespace baseline --daemon-port <port> --web-port <port>` in the baseline worktree and `pnpm tools-dev run web --namespace candidate --daemon-port <port> --web-port <port>` in the development worktree; the agent uses agent-browser CLI and Chrome DevTools MCP to compare screenshots for major pages/component areas, fixed viewport, light/dark/system themes, and custom accent across the two services, with component source inspection used to explain differences and confirm class migration traceability. Source: `AGENTS.md:40-45,82-89,91-104`; `apps/web/src/index.css:65-157`; `apps/web/src/state/appearance.ts:28-52`
- Manual visual review: use the agent comparison record to check Dashboard/app shell, project detail, settings dialog, file viewer/inspect overlay, sketch editor, live artifact card, and modal/popover/control states; approved deviations must be included in implementation notes.
@ -169,14 +169,15 @@ created: '2026-05-09'
Guard needs to cover three rule categories and record file scope, match pattern, and reason for every exception.
1. Default Tailwind palette class check: reject default palette utilities such as `text-red-500`, `bg-white`, `border-zinc-200`, `from-orange-500`, and `ring-blue-400` in app UI files. Allowed color utilities come from project tokens exposed through `@theme` in `token.md`.
2. Hardcoded UI color check: reject unregistered `#hex`, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and real named colors in app UI chrome and component styles within the cleaned scope for each phase; Phase 6 tightens this to the remaining app UI surface after migrated colors are gone. CSS-wide/special keywords such as `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` express transparency, inheritance, or reset semantics, so guard should explicitly exempt them or handle them by property semantics. When real unregistered colors are found, prefer migrating them to Tailwind token classes or CSS variables; any arbitrary color that appears repeatedly should be promoted to a named token.
2. Hardcoded UI color check: Phase 1 adds detection, keyword exemptions, allowlist structure, and fixture/temp-sample validation while existing hardcoded UI colors remain classified as migration inventory or explicit exceptions. Component migration phases then reject unregistered `#hex`, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and real named colors in the areas being migrated. Phase 6 tightens this to the remaining app UI surface after migrated colors are gone. CSS-wide/special keywords such as `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` express transparency, inheritance, or reset semantics, so guard should explicitly exempt them or handle them by property semantics. When real unregistered colors are found, prefer migrating them to Tailwind token classes or CSS variables; any arbitrary color that appears repeatedly should be promoted to a named token.
3. Explicit allowlist check: allow brand assets, SVG illustrations, user accent input, canvas/sketch user colors, user-authored file/inspect color conversion, external document/iframe/popup runtime HTML, and test fixtures. The allowlist should be as narrow as possible, with reasons annotated by file, function, or pattern, so path-level exemptions do not cover normal UI chrome.
### Open Questions
| Question | Resolution point |
| --- | --- |
| Exact style guard allowlist entries and path/pattern scopes | Phase 1 before strict guard lands |
| Initial style guard allowlist structure and fixture/temp-sample scopes | Phase 1 before guard scaffolding lands |
| Final strict hardcoded-color allowlist and app UI path/pattern scopes | Phase 6 before strict enforcement lands |
| Exact dual-worktree port assignments, scenario matrix, and agent comparison note format | Phase 1 before Phase 2 merges |
| Final retained global CSS inventory | Phase 6 |
| Dynamic class handling policy for each migrated component | During each phase before replacing dynamic class composition |
@ -191,50 +192,50 @@ Each phase maps to one PR. Every PR must be reviewable on its own, keep business
Goal: add Tailwind v4 infrastructure, expose Open Design tokens as Tailwind utilities, and land the first style guard scaffolding.
- [ ] Step 1: Install Tailwind foundations
- [ ] Substep 1.1 Implement: Add Tailwind v4/PostCSS dependencies to `apps/web/package.json`.
- [ ] Substep 1.2 Implement: Add a web-local PostCSS config for `@tailwindcss/postcss`.
- [ ] Substep 1.3 Implement: Add `apps/web/postcss.config.mjs` to the exact residual JavaScript allowlist in `scripts/guard.ts`, with a comment explaining that the PostCSS/Tailwind config entry needs the `.mjs` compatibility format, keeping `pnpm guard` coverage for planned config files.
- [ ] Substep 1.4 Implement: Import Tailwind theme and utilities layers in `apps/web/src/index.css` with `@layer theme, base, utilities;`, `@import "tailwindcss/theme.css" layer(theme);`, and `@import "tailwindcss/utilities.css" layer(utilities);`, while preserving the existing global entry behavior and excluding Preflight from the foundation slice. Add the narrow local border-style reset in the base layer with `@layer base { *, ::before, ::after, ::backdrop, ::file-selector-button { border: 0 solid; } }` so Tailwind `border` width utilities in the later utilities layer combine with project `border-*` color utilities without requiring `border-solid` on every migrated element. Record the cascade policy that retained element/reset rules which may conflict with migrated utilities must also live in `@layer base`, be constrained to non-migrated scopes, or be removed before the affected elements migrate.
- [ ] Substep 1.5 Verify: Run `pnpm install`.
- [ ] Substep 1.6 Verify: Run `pnpm guard` and confirm the PostCSS config allowlist works.
- [ ] Substep 1.7 Verify: Run `pnpm --filter @open-design/web build`.
- [ ] Step 2: Expose Open Design tokens as Tailwind utilities
- [ ] Substep 2.1 Implement: Add CSS-first `@theme` aliases for colors, core semantic status, selection/inspect overlays, radius, shadow, font tokens, and exact existing UI text-size aliases; use native Tailwind utilities for spacing and standard typography scale. Confirm token border examples such as `border border-border` render against the local border-style reset when Preflight is omitted.
- [ ] Substep 2.2 Implement: Clear default Tailwind colors and declare the project-approved color namespace.
- [ ] Substep 2.3 Implement: Document the token class vocabulary near the theme block.
- [ ] Substep 2.4 Verify: Confirm light, dark, system, and custom accent modes all resolve through the same CSS variables.
- [ ] Substep 2.5 Verify: Run `pnpm --filter @open-design/web build`.
- [ ] Step 3: Add base style guardrails
- [ ] Substep 3.1 Implement: Add a default Tailwind palette class check for app UI code in `scripts/guard.ts`.
- [ ] Substep 3.2 Implement: Add hardcoded UI color check scaffolding covering `#hex`, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and named colors, with enforcement initially scoped to files, classes, or patterns already cleaned in Phase 1 so known later-phase migrations keep `pnpm guard` passing.
- [ ] Substep 3.2a Implement: Exempt CSS-wide/special keywords such as `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` in the named-color check so ghost buttons, SVG current-color, and inherit/reset states pass by semantics.
- [ ] Substep 3.3 Implement: Add an explicit allowlist mechanism covering brand assets, SVG illustrations, user accent input, canvas/sketch user colors, user-authored file/inspect colors, external runtime documents, and tests/fixtures.
- [ ] Substep 3.4 Implement: If helpers need extraction, add focused tests under `apps/web/tests/`; test fixtures must cover `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` passing, and real unregistered named colors failing.
- [ ] Substep 3.5 Verify: Run `pnpm guard` and confirm the scoped hardcoded-color enforcement does not fail known migration inventory items such as legacy `SettingsDialog` fallbacks or component colors still scheduled for later phases.
- [ ] Substep 3.6 Verify: Temporarily write a default Tailwind native color class in a TSX file, such as `text-red-500`, confirm `pnpm guard` detects it and fails, then remove the temporary code.
- [ ] Substep 3.7 Verify: Temporarily write an unallowlisted hardcoded color inside the Phase 1 enforced scope, such as `style={{ color: '#ff0000' }}`, confirm `pnpm guard` detects it and fails, then remove the temporary code.
- [ ] Substep 3.8 Verify: Run `pnpm --filter @open-design/web test`.
- [ ] Step 4: Build migration inventory and agent visual comparison prep
- [ ] Substep 4.1 Implement: Generate an inventory of global CSS classes referenced in `apps/web/src/**/*.tsx` and map them to definitions in `apps/web/src/index.css`.
- [ ] Substep 4.2 Implement: Classify classes as component-level migratable styles, global base styles, loading shell, keyframes/animation, content-level/third-party boundary styles, and retained exceptions; identify unlayered element selectors that set properties planned for Tailwind utilities, such as global `button` base rules, and assign each one a remove, constrain, or move-to-`@layer base` resolution before its affected TSX migration slice.
- [ ] Substep 4.3 Implement: Record the corresponding token-first Tailwind utility combination or migration note for each component-level class.
- [ ] Substep 4.4 Implement: Define style guard allowlist entries, path/pattern scopes, and the repeated arbitrary color promotion threshold.
- [ ] Substep 4.5 Verify: Confirm the migration inventory covers all global classes referenced by TSX; the migration inventory is an implementation reference, while actual migration scope and classification follow the current code at implementation time, with on-the-spot judgment for classes added or changed after rebase.
- [ ] Substep 4.6 Verify: Run `pnpm guard`, `pnpm typecheck`, `pnpm --filter @open-design/web test`, and `pnpm --filter @open-design/web build`.
- [ ] Step 5: Establish the dual-worktree agent visual comparison workflow
- [ ] Substep 5.1 Implement: Define the agent comparison scenario list, viewport, theme/accent matrix, fixture data, dual-worktree namespace values and port assignments, screenshot artifact requirements, component source inspection guidance, and phase notes format.
- [ ] Substep 5.2 Implement: Prepare startup instructions for the baseline and development worktrees: run `pnpm tools-dev run web --namespace baseline --daemon-port <baseline-daemon-port> --web-port <baseline-web-port>` in the baseline worktree and `pnpm tools-dev run web --namespace candidate --daemon-port <candidate-daemon-port> --web-port <candidate-web-port>` in the development worktree so each web/daemon pair has independent runtime files and IPC sockets.
- [ ] Substep 5.3 Verify: Have an agent equipped with agent-browser CLI and Chrome DevTools MCP compare screenshots for Dashboard/app shell, project detail, settings dialog, file viewer/inspect overlay, sketch editor, live artifact card, and modal/popover/control states between the baseline and development services.
- [ ] Substep 5.4 Verify: Agent comparison must cover light, dark, system, and custom accent. When layout offset, token color drift, radius/shadow differences, or theme-state differences appear, fix the styles or record an approved deviation.
- [ ] Substep 5.5 Implement: In `phase1-notes.md`, record foundation changes, migration inventory, dual-worktree service URLs, agent comparison coverage scenarios, discovered issues, and approved deviations.
- [x] Step 1: Install Tailwind foundations
- [x] Substep 1.1 Implement: Add Tailwind v4/PostCSS dependencies to `apps/web/package.json`.
- [x] Substep 1.2 Implement: Add a web-local PostCSS config for `@tailwindcss/postcss`.
- [x] Substep 1.3 Implement: Add `apps/web/postcss.config.mjs` to the exact residual JavaScript allowlist in `scripts/guard.ts`, with a comment explaining that the PostCSS/Tailwind config entry needs the `.mjs` compatibility format, keeping `pnpm guard` coverage for planned config files.
- [x] Substep 1.4 Implement: Import Tailwind theme and utilities layers in `apps/web/src/index.css` with `@layer theme, base, utilities;`, `@import "tailwindcss/theme.css" layer(theme);`, and `@import "tailwindcss/utilities.css" layer(utilities);`, while preserving the existing global entry behavior and excluding Preflight from the foundation slice. Add the narrow local border-style reset in the base layer with `@layer base { *, ::before, ::after, ::backdrop, ::file-selector-button { border: 0 solid; } }` so Tailwind `border` width utilities in the later utilities layer combine with project `border-*` color utilities without requiring `border-solid` on every migrated element. Record the cascade policy that retained element/reset rules which may conflict with migrated utilities must also live in `@layer base`, be constrained to non-migrated scopes, or be removed before the affected elements migrate.
- [x] Substep 1.5 Verify: Run `pnpm install`.
- [x] Substep 1.6 Verify: Run `pnpm guard` and confirm the PostCSS config allowlist works.
- [x] Substep 1.7 Verify: Run `pnpm --filter @open-design/web build`.
- [x] Step 2: Expose Open Design tokens as Tailwind utilities
- [x] Substep 2.1 Implement: Add CSS-first `@theme` aliases for colors, core semantic status, selection/inspect overlays, radius, shadow, font tokens, and exact existing UI text-size aliases; use native Tailwind utilities for spacing and standard typography scale. Confirm token border examples such as `border border-border` render against the local border-style reset when Preflight is omitted.
- [x] Substep 2.2 Implement: Clear default Tailwind colors and declare the project-approved color namespace.
- [x] Substep 2.3 Implement: Document the token class vocabulary near the theme block.
- [x] Substep 2.4 Verify: Confirm light, dark, system, and custom accent modes all resolve through the same CSS variables.
- [x] Substep 2.5 Verify: Run `pnpm --filter @open-design/web build`.
- [x] Step 3: Add base style guardrails
- [x] Substep 3.1 Implement: Add a default Tailwind palette class check for app UI code in `scripts/guard.ts`.
- [x] Substep 3.2 Implement: Add hardcoded UI color check scaffolding covering `#hex`, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and named colors. In Phase 1, keep existing hardcoded UI colors classified as migration inventory or explicit exceptions, and validate the checker through focused fixtures or temporary scoped samples so `pnpm guard` stays green until component migration phases tighten enforcement.
- [x] Substep 3.2a Implement: Exempt CSS-wide/special keywords such as `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` in the named-color check so ghost buttons, SVG current-color, and inherit/reset states pass by semantics.
- [x] Substep 3.3 Implement: Add an explicit allowlist mechanism covering brand assets, SVG illustrations, user accent input, canvas/sketch user colors, user-authored file/inspect colors, external runtime documents, and tests/fixtures.
- [x] Substep 3.4 Implement: If helpers need extraction, add focused tests under `apps/web/tests/`; test fixtures must cover `transparent`, `currentColor` / `currentcolor`, `inherit`, `initial`, `unset`, and `revert` passing, and real unregistered named colors failing.
- [x] Substep 3.5 Verify: Run `pnpm guard` and confirm the hardcoded-color scaffolding does not fail known migration inventory items such as legacy `SettingsDialog` fallbacks or component colors still scheduled for later phases.
- [x] Substep 3.6 Verify: Temporarily write a default Tailwind native color class in a TSX file, such as `text-red-500`, confirm `pnpm guard` detects it and fails, then remove the temporary code.
- [x] Substep 3.7 Verify: Temporarily write an unallowlisted hardcoded color in a guard fixture or temporary scoped sample, such as `style={{ color: '#ff0000' }}`, confirm `pnpm guard` detects it and fails, then remove the temporary code.
- [x] Substep 3.8 Verify: Run `pnpm --filter @open-design/web test`.
- [x] Step 4: Build migration inventory and agent visual comparison prep
- [x] Substep 4.1 Implement: Generate an inventory of global CSS classes referenced in `apps/web/src/**/*.tsx` and map them to definitions in `apps/web/src/index.css`.
- [x] Substep 4.2 Implement: Classify classes as component-level migratable styles, global base styles, loading shell, keyframes/animation, content-level/third-party boundary styles, and retained exceptions; identify unlayered element selectors that set properties planned for Tailwind utilities, such as global `button` base rules, and assign each one a remove, constrain, or move-to-`@layer base` resolution before its affected TSX migration slice.
- [x] Substep 4.3 Implement: Record the corresponding token-first Tailwind utility combination or migration note for each component-level class.
- [x] Substep 4.4 Implement: Define style guard allowlist entries, path/pattern scopes, and the repeated arbitrary color promotion threshold.
- [x] Substep 4.5 Verify: Confirm the migration inventory covers all global classes referenced by TSX; the migration inventory is an implementation reference, while actual migration scope and classification follow the current code at implementation time, with on-the-spot judgment for classes added or changed after rebase.
- [x] Substep 4.6 Verify: Run `pnpm guard`, `pnpm typecheck`, `pnpm --filter @open-design/web test`, and `pnpm --filter @open-design/web build`.
- [x] Step 5: Establish the dual-worktree agent visual comparison workflow
- [x] Substep 5.1 Implement: Define the agent comparison scenario list, viewport, theme/accent matrix, fixture data, dual-worktree namespace values and port assignments, screenshot artifact requirements, component source inspection guidance, and phase notes format.
- [x] Substep 5.2 Implement: Prepare startup instructions for the baseline and development worktrees: run `pnpm tools-dev run web --namespace baseline --daemon-port <baseline-daemon-port> --web-port <baseline-web-port>` in the baseline worktree and `pnpm tools-dev run web --namespace candidate --daemon-port <candidate-daemon-port> --web-port <candidate-web-port>` in the development worktree so each web/daemon pair has independent runtime files and IPC sockets.
- [x] Substep 5.3 Verify: In Phase 1, have an agent equipped with agent-browser CLI and Chrome DevTools MCP smoke-compare the workflow definition against a Dashboard/app shell baseline-vs-development run so the dual-worktree process, screenshot artifact shape, and comparison notes format are proven end to end.
- [ ] Substep 5.4 Verify: The full migration gate for follow-up slices remains the complete scenario and theme matrix: Dashboard/app shell, project detail, settings dialog, file viewer/inspect overlay, sketch editor, live artifact card, and modal/popover/control states under light, dark, system, and custom accent. When layout offset, token color drift, radius/shadow differences, or theme-state differences appear, fix the styles or record an approved deviation.
- [x] Substep 5.5 Implement: In `phase1-notes.md`, record foundation changes, migration inventory, dual-worktree service URLs, agent comparison coverage scenarios, discovered issues, and approved deviations.
### Phase 2: Shell and common controls PR
Goal: migrate app shell, buttons, inputs, cards, popovers, and modals using token-backed colors/radius/shadows.
- [ ] Step 6: Migrate shell and common controls
- [ ] Substep 6.1 Implement: Replace component-level global classes for app shell, buttons, inputs, cards, popovers, and modals with token-first Tailwind classes according to the migration inventory.
- [ ] Substep 6.1 Implement: Replace component-level global classes for app shell, buttons, inputs, cards, popovers, and modals with token-first Tailwind classes according to the migration inventory and `token.md` migration rules, including font aliases, exact text-size aliases, and retained element/reset selector resolution before applying utilities to affected elements.
- [ ] Substep 6.2 Implement: For common UI that depends on `--radius*` and `--shadow*`, use variable-backed Tailwind utilities such as `rounded-card`, `rounded-panel`, `rounded-token-pill`, `shadow-token-sm`, and `shadow-token-md`.
- [ ] Substep 6.3 Implement: When retaining necessary dynamic class composition, use a complete static class map; cases that need runtime-generated classes must have explicit safelist and guard/test coverage.
- [ ] Substep 6.4 Implement: Remove component-level class definitions migrated in this phase from `index.css`, while retaining styles still used by global boundaries.
@ -249,7 +250,7 @@ Goal: migrate app shell, buttons, inputs, cards, popovers, and modals using toke
Goal: migrate settings dialogs, project creation, project detail panels, and status surfaces.
- [ ] Step 8: Migrate settings and project panel areas
- [ ] Substep 8.1 Implement: Replace component-level global classes for settings dialog, project creation, project detail panels, and status surfaces with token-first Tailwind classes.
- [ ] Substep 8.1 Implement: Replace component-level global classes for settings dialog, project creation, project detail panels, and status surfaces with token-first Tailwind classes according to `token.md` migration rules, including font aliases, exact text-size aliases, and retained element/reset selector resolution before applying utilities to affected elements.
- [ ] Substep 8.2 Implement: Migrate legacy token fallbacks in `SettingsDialog` and governable hardcoded colors in project panels to current tokens or Tailwind utilities.
- [ ] Substep 8.3 Implement: Use semantic token utilities such as `success`, `info`, `discovery`, `danger`, and `warning` for status surfaces.
- [ ] Substep 8.4 Implement: Remove component-level class definitions migrated in this phase from `index.css`, while retaining explicitly documented retained styles.
@ -264,7 +265,7 @@ Goal: migrate settings dialogs, project creation, project detail panels, and sta
Goal: migrate file viewer chrome, inspect/comment overlays, and edit-mode integration to token-first utilities while keeping user-authored file color conversion helpers allowlisted.
- [ ] Step 10: Migrate file viewer and inspect/edit-mode overlays
- [ ] Substep 10.1 Implement: Replace component-level global classes for file viewer app chrome with token-first Tailwind classes.
- [ ] Substep 10.1 Implement: Replace component-level global classes for file viewer app chrome with token-first Tailwind classes according to `token.md` migration rules, including font aliases, exact text-size aliases, and retained element/reset selector resolution before applying utilities to affected elements.
- [ ] Substep 10.2 Implement: Migrate inspect/comment overlays to `selection`/`inspect` tokens, such as `bg-selection-overlay`, `border-selection-outline`, `ring-selection-outline`, and `bg-inspect-overlay`.
- [ ] Substep 10.3 Implement: Keep file color conversion helpers and user-authored content colors in a narrow allowlist, annotated with a runtime/user-content reason.
- [ ] Substep 10.4 Implement: Remove component-level class definitions migrated in this phase from `index.css`, while retaining file/user-content boundary styles.
@ -279,7 +280,7 @@ Goal: migrate file viewer chrome, inspect/comment overlays, and edit-mode integr
Goal: migrate app chrome around sketch canvases and runtime surfaces while retaining user content, iframe, popup, generated runtime HTML, and fixtures under explicit exceptions.
- [ ] Step 12: Migrate sketch and runtime content boundaries
- [ ] Substep 12.1 Implement: Replace component-level global classes for sketch editor app chrome, runtime surfaces, live artifact card, and related controls with token-first Tailwind classes.
- [ ] Substep 12.1 Implement: Replace component-level global classes for sketch editor app chrome, runtime surfaces, live artifact card, and related controls with token-first Tailwind classes according to `token.md` migration rules, including font aliases, exact text-size aliases, and retained element/reset selector resolution before applying utilities to affected elements.
- [ ] Substep 12.2 Implement: Keep sketch/canvas user drawing colors, external document, iframe, popup, generated runtime HTML, and fixtures in the explicit allowlist, annotated with boundary reasons.
- [ ] Substep 12.3 Implement: Use app token utilities for canvas-adjacent UI; canvas data and user content keep data semantics.
- [ ] Substep 12.4 Implement: Remove component-level class definitions migrated in this phase from `index.css`, while retaining content-wide, iframe/runtime, and fixture boundary styles.

View file

@ -315,6 +315,8 @@ When a new color appears, classify it before adding an allowlist entry:
The style guard should reject default Tailwind palette utilities in app UI files after this token set lands. Examples to reject include `text-red-500`, `bg-white`, `border-zinc-200`, `from-orange-500`, `ring-blue-400`, and similar default palette classes.
Hardcoded UI color detection lands as Phase 1 scaffolding with keyword exemptions, allowlist structure, and fixture/temp-sample validation. Existing hardcoded UI colors stay classified as migration inventory or explicit exceptions until the component migration phases tighten enforcement by area. Phase 6 enables strict enforcement for the remaining app UI surface.
Allowed color sources are:
- Tailwind utilities generated by the `@theme` tokens in this file.