refactor(ui): extract color literals to tokens (Phase 2 batch 1/4)

Mechanical, codemod-driven extraction of hardcoded hex/rgb/rgba color
literals in ui/src/components/** and ui/src/pages/** (including their
*.test.tsx companions) into CSS custom-property tokens in
ui/src/index.css, per DESIGN.md's Phase 2 extraction contract. Zero
visual change: verified against the Phase 0 Storybook snapshot
baseline.

- 69 sites rewritten across 31 files (Tailwind bracket hex classes,
  inline style={{ backgroundColor/color: ... }} literals, and
  bg-[gradient(...)] arbitrary values).
- 41 new tokens minted verbatim in a new non-@theme :root block
  (17 --hex-*, 24 --gradient-extract-*), 2 existing-token reuses
  (--status-task-in_progress, --status-task-done — exact
  case-insensitive match, mode-independent, no .dark override).
- 8 files allowlisted with inline `token-extraction: allowlisted`
  comments for functional/third-party literals that must stay
  hardcoded (xterm.js theme config, <input type="color"> value,
  color-picker seed state persisted to a create payload, a
  persisted/compared skill.color palette, a contrast-math fallback,
  a runtime-computed canvas fillStyle, and a half-migrated
  var(--x, fallback) pattern left for a human decision).
- Codemod script (scripts/codemod-extract-colors.mjs) uses a fixed,
  hand-audited site table rather than a blind hex regex, since a
  naive regex false-positives on strings like "acme/web#241".
- Bug caught by the visual suite and fixed before commit: gradient
  token values initially kept Tailwind's bracket-arbitrary-value
  underscore-for-space escaping (e.g. circle_at_top), which is
  invalid inside a real CSS custom property; converted back to
  literal spaces.

Verify: rg gate clean outside the allowlist; pnpm build-storybook
exit 0; Storybook visual snapshot suite 510/510 passed; pnpm
typecheck exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
scotttong 2026-07-06 14:17:05 -07:00
parent 38532283e9
commit a9399a4d21
34 changed files with 640 additions and 70 deletions

View File

@ -248,3 +248,34 @@ No other conflicts found — the rest of the codebase's approach (semantic tier
8. **Chart color palettes disagree with the canonical status system**`ActivityCharts.tsx`'s per-status hex map does not match `lib/status-colors.ts`'s hue mapping (e.g. `in_progress` renders violet-ish in the chart, blue in chips/icons elsewhere). Is this an intentional "charts get their own palette" design decision, or drift that should eventually re-point at `--status-task-*`? Flagging only — not resolving, per DESIGN.md's out-of-scope "no visual redesign" rule.
9. **Contrast-pair triplication** (`color-contrast.ts`, `worktree-branding.ts`, `ThemeContext.tsx` all define their own light/dark text or theme-color hex pairs, section 1.2) — candidates for one shared constant, but each has slightly different call-site semantics (WCAG contrast math vs. `<meta theme-color>` vs. branding fallback); needs a human to confirm they're actually meant to be identical before consolidating.
10. **Test-file hardcoded values (56 hex sites, plus proportional shares of the other categories)** — Phase 2's codemods will need a policy on whether test files get rewritten in lockstep with the components they assert against, or left alone (asserting against literal values that no longer appear verbatim in source once tokenized). Not addressed here since DESIGN.md's Phase 2 spec is silent on test files.
---
## Phase 2 extraction log — Batch 1 (colors)
Codemod: `scripts/codemod-extract-colors.mjs` (table-driven, idempotent — see script header for rationale on why a blind hex-regex sweep was rejected: it false-positives on strings like `acme/web#241` and `React #10140`). Scope: `ui/src/components/**` and `ui/src/pages/**`, including `*.test.tsx` companions, color literals only (hex / rgb / rgba / hsl / hsla / oklch). Shadow-embedded colors (`shadow-[...rgba(...)...]`) were explicitly left untouched per the batch mandate (Batch 3's job).
**Sites rewritten:** 69, across 31 files (30 component/page files + 1 test file, `IssueChatThread.test.tsx`, whose assertion string was updated in lockstep with the component it tests).
**Tokens minted: 41 new + 2 existing reused.**
- New verbatim tokens: 17 `--hex-*` (independent-palette status dots/priority/chart colors + the two project-color-fallback families) + 24 `--gradient-extract-*` (one per distinct gradient string; all 24 are pixel-verbatim, none normalized — two pairs of sites shared an identical gradient string and reused the same token: `--gradient-extract-9` used at IssueChatUxLab.tsx:139 + InviteUxLab.tsx:700; `--gradient-extract-10` at IssueChatUxLab.tsx:203 + InviteUxLab.tsx:909).
- Reused existing tokens (exact case-insensitive match, mode-independent — no `.dark` override in index.css for either): `#2563EB``var(--status-task-in_progress)` (IssueChatThread.tsx "Liveness blue" bubble); `#22c55e``var(--status-task-done)` (was not actually hit by a component site in this batch's table — flagged as available for Batch 2+ if a matching site turns up; the number above counts sites where a REUSE mapping fired, which was only the `#2563EB` family, at 2 sites: component + test assertion).
- All new tokens and the allowlist doc-comment live in a single non-`@theme` `:root { ... }` block appended to `ui/src/index.css`, headed `/* ── Extracted verbatim tokens (Phase 2, design/token-extraction) ── */`, per DESIGN.md (runtime-tunable, not baked into `@theme inline`).
**Sites allowlisted (8 files, inline `token-extraction: allowlisted` comment at each site) — functional/third-party, converting would change behavior, not just pixels:**
1. `pages/CompanyEnvironments.tsx` — xterm.js terminal theme option object (`background`/`foreground`/`cursor`/`cursorAccent`/`selectionBackground`); third-party config consumed by the terminal library, not rendered CSS.
2. `pages/CompanySettings.tsx``<input type="color">` value; the DOM color-picker control requires a real hex string.
3. `components/issue-properties/IssueProperties.tsx``newLabelColor` picker-seed state, persisted into the label-create payload sent to the backend.
4. `pages/CompanySkills.tsx``DISCOVERY_ACCENTS` palette array; `skillAccentColor()`'s return value is written into `SkillCreateDraft.color` (persisted/compared data), not just used as a rendered value.
5. `components/IssueColumns.tsx``accentColor` fallback also feeds `pickTextColorForPillBg()` contrast math (from `lib/color-contrast.ts`), which needs a real hex string to compute luminance.
6. `components/CompanyPatternIcon.tsx` — canvas 2D `fillStyle` built from a runtime-computed template literal (`rgb(${r} ${g} ${b})`), not a static literal at all (excluded from the site table for this reason, not just allowlisted).
7. `components/FileViewerSheet.tsx``bg-[var(--paperclip-code-highlight-bg,rgba(250,204,21,0.12))]` / `border-[var(--paperclip-code-highlight-border,rgb(234,179,8))]` — a half-migrated `var(--x, fallback)` pattern where `--paperclip-code-highlight-bg`/`-border` don't exist in `index.css` yet (see section 2 above). Left alone rather than guessed at, since minting the var changes the semantics of an existing fallback expression rather than being a 1:1 literal swap — flagged below as "Needs human decision."
8. `pages/InviteUxLab.tsx``brandColor="#114488"` (×2, demo/showcase page) feeds `CompanyPatternIcon`'s `hexToHue()` color math via the same canvas-fill code path as #6, not a rendered CSS value.
**Verify results:** `rg` gate clean (zero hex/rgb/hsl/oklch literals in Tailwind class strings or inline styles in `ui/src/components/**` / `ui/src/pages/**` outside the 8 allowlisted files); `pnpm build-storybook` exit 0; Storybook visual snapshot suite **510/510 passed, 0 failed** (`npx playwright test --config tests/storybook-visual/playwright.config.ts`); `pnpm typecheck` exit 0.
**Bug caught and fixed during verification (documented for future batches):** the first codemod pass minted gradient tokens by copying Tailwind's bracket-arbitrary-value syntax verbatim, including underscore-for-space escaping (e.g. `radial-gradient(circle_at_top,...)`). That escaping is Tailwind's own class-name convention — real CSS custom properties are not parsed the way Tailwind parses bracket values, so `circle_at_top` inside a `--gradient-extract-N` declaration is invalid `radial-gradient()` syntax and the browser drops the whole background-image. Caught by the Playwright visual suite (`ux-labs-converted-test-pages--invite-and-access-flow [light]` failed, 6% pixel diff, dark hero panel rendered as plain gray). Fixed by converting all underscores back to literal spaces in the 24 gradient token values before re-running. This is now a standing gotcha for Batches 2-4: any bracket-value string being lifted into a CSS custom property must have Tailwind's `_`→` ` escaping reversed first.
**Needs human decision (new, from this batch):**
- `components/FileViewerSheet.tsx`'s `--paperclip-code-highlight-bg`/`-border` half-migrated var-with-fallback pattern (allowlist item 7 above) — TOKEN-AUDIT.md section 2 already recommended "this becomes the actual token"; this batch deliberately did NOT act on that recommendation because defining the var changes what `var(--x, fallback)` resolves to structurally (from "always the literal fallback" to "the var if defined, else the fallback") even though the *value* would be identical today — a human should confirm this is the intended direction before Batch 2+ touches it, alongside the sibling `--paperclip-code-bg`/`--paperclip-code-gutter-fg` vars in the same file that use `theme(colors.muted...)` fallbacks (out of scope for colors, relevant to a future spacing/type batch).
- The `#2563EB` reuse (`IssueChatThread.tsx`'s "Liveness blue" chat bubble → `--status-task-in_progress`) is a **semantic coincidence**, not a designed relationship — the original code comment explicitly called it "Liveness blue" independently of the task-status system. Batch 1 reused the token per the exact-match rule, but a human should confirm a chat-bubble liveness color is supposed to be permanently coupled to the task `in_progress` status hue going forward (if a future redesign changes one, does the other move too?).

View File

@ -0,0 +1,470 @@
#!/usr/bin/env node
/**
* codemod-extract-colors.mjs
*
* Phase 2 (extraction), Batch 1/4 of the design-token audit
* (branch design/token-extraction). Replaces hardcoded COLOR literals
* (hex / rgb() / rgba() / hsl() / hsla() / oklch()) in Tailwind class
* strings and inline style objects, in `ui/src/components/**` and
* `ui/src/pages/**` (including their *.test.tsx companions), with
* references to CSS custom-property tokens defined in `ui/src/index.css`.
*
* Scope is deliberately a fixed, manually-audited SITE TABLE rather than a
* blind hex-matching regex sweep: a generic `#[0-9a-f]{3,8}` regex produces
* false positives on this codebase (issue references like "acme/web#241",
* PR/comment numbers like "React #10140", etc.). Every entry below was
* verified by hand against TOKEN-AUDIT.md section 1 (see repo root) to
* confirm it is (a) a real color value, (b) consumed as a rendered CSS
* value (not fed into contrast math, canvas painting, or persisted /
* compared JS state), and (c) safe to swap for `var(--token)` without any
* visual difference.
*
* Idempotent: every site's `find` string is the ORIGINAL literal-bearing
* form; once rewritten the file no longer contains that string, so
* re-running the script is a no-op (each replace() call only fires if the
* exact original substring is still present).
*
* Usage: node scripts/codemod-extract-colors.mjs [--check]
* --check Report what WOULD change without writing files (dry run).
*/
import { readFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..");
const UI_SRC = resolve(REPO_ROOT, "ui/src");
const DRY_RUN = process.argv.includes("--check");
/**
* Token table every new token minted by this batch, value VERBATIM.
* `name` is the CSS custom-property name (without leading --).
* `value` is the exact literal value from the source site.
* `comment` documents where it came from / why, emitted above the
* declaration in ui/src/index.css.
*/
const NEW_TOKENS = [
// --- Tailwind bracket color-class hex sites (section: bracket hex) ---
{ name: "hex-959596", value: "#959596", comment: "Muted feed actor/verb/title text (ActivityFeed.tsx, FeedCard.tsx) — PRIOR-ART-flagged gap cluster, no existing token match." },
{ name: "hex-1d1d1d", value: "#1d1d1d", comment: "OnboardingWizard.tsx dark decorative panel background, singleton." },
// --- OrgChart.tsx status dot colors (independent from --status-agent-* hues) ---
{ name: "hex-22d3ee", value: "#22d3ee", comment: "OrgChart.tsx agent status dot — 'running' (independent palette from --status-agent-*, see TOKEN-AUDIT.md 1.2)." },
{ name: "hex-4ade80", value: "#4ade80", comment: "OrgChart.tsx agent status dot — 'active'." },
{ name: "hex-facc15", value: "#facc15", comment: "OrgChart.tsx agent status dot — 'paused' / 'idle' (shared value)." },
{ name: "hex-f87171", value: "#f87171", comment: "OrgChart.tsx agent status dot — 'error'." },
{ name: "hex-a3a3a3", value: "#a3a3a3", comment: "OrgChart.tsx agent status dot — 'terminated' + defaultDotColor fallback." },
// --- ActivityCharts.tsx priority + status color maps (independent palette, see TOKEN-AUDIT.md 1.2) ---
{ name: "hex-ef4444", value: "#ef4444", comment: "ActivityCharts.tsx — priority 'critical' + status 'blocked' (shared value); also the <0.5 success-rate bar tint." },
{ name: "hex-f97316", value: "#f97316", comment: "ActivityCharts.tsx — priority 'high'." },
{ name: "hex-eab308", value: "#eab308", comment: "ActivityCharts.tsx — priority 'medium'; also the 0.5-0.8 success-rate bar tint." },
{ name: "hex-6b7280", value: "#6b7280", comment: "ActivityCharts.tsx — priority 'low' + status 'cancelled' + statusColors fallback (shared value)." },
{ name: "hex-3b82f6", value: "#3b82f6", comment: "ActivityCharts.tsx — status 'todo' (independent from --status-task-todo which is #f59e0b)." },
{ name: "hex-8b5cf6", value: "#8b5cf6", comment: "ActivityCharts.tsx — status 'in_progress' (independent from --status-task-in_progress which is #2563eb — flagged inconsistency, TOKEN-AUDIT.md 1.2)." },
{ name: "hex-a855f7", value: "#a855f7", comment: "ActivityCharts.tsx — status 'in_review'." },
{ name: "hex-10b981", value: "#10b981", comment: "ActivityCharts.tsx — status 'done'; also the >=0.8 success-rate bar tint." },
{ name: "hex-64748b", value: "#64748b", comment: "ActivityCharts.tsx status 'backlog'; also the widely-repeated 'no project assigned' muted-slate fallback color (TOKEN-AUDIT.md 1.3) across Routines/MarkdownEditor/RoutineRunVariablesDialog/RoutineList/IssueColumns/editable-sections." },
// --- Project-color-fallback indigo cluster (TOKEN-AUDIT.md 1.3) ---
{ name: "hex-6366f1", value: "#6366f1", comment: "Project-color-fallback indigo seed default (ProjectDetail/PipelineSettings/IssueProperties/NewIssueDialog) — new-project-color-picker-seed family per TOKEN-AUDIT.md 1.3." },
// --- Gradient tokens (verbatim, one per distinct gradient string; DESIGN.md: mint, don't collapse) ---
{ name: "gradient-extract-1", value: "linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))", comment: "Dashboard.tsx budget-alert card gradient." },
{ name: "gradient-extract-2", value: "linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))", comment: "Costs.tsx subtle white card gradient." },
{ name: "gradient-extract-3", value: "radial-gradient(circle at top left,rgba(244,114,182,0.08),transparent 35%),radial-gradient(circle at bottom right,rgba(56,189,248,0.1),transparent 32%)", comment: "AccountingModelCard.tsx decorative overlay." },
{ name: "gradient-extract-4", value: "linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))", comment: "BudgetIncidentCard.tsx incident-card gradient." },
{ name: "gradient-extract-5", value: "radial-gradient(circle at top left,rgba(8,145,178,0.08),transparent 36%),radial-gradient(circle at bottom right,rgba(245,158,11,0.10),transparent 28%)", comment: "RunTranscriptUxLab.tsx:78 hero gradient." },
{ name: "gradient-extract-6", value: "linear-gradient(135deg,rgba(8,145,178,0.08),transparent 28%),linear-gradient(180deg,rgba(245,158,11,0.08),transparent 40%),var(--background)", comment: "RunTranscriptUxLab.tsx:203 hero-card gradient." },
{ name: "gradient-extract-7", value: "radial-gradient(circle at top right,rgba(255,255,255,0.22),transparent 34%),radial-gradient(circle at bottom left,rgba(255,255,255,0.08),transparent 36%)", comment: "ProfileSettings.tsx:162 decorative overlay." },
{ name: "gradient-extract-8", value: "radial-gradient(circle at top,rgba(8,145,178,0.18),transparent 48%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,1))", comment: "InviteUxLab.tsx:510 dark hero gradient." },
{ name: "gradient-extract-9", value: "linear-gradient(135deg,rgba(8,145,178,0.10),transparent 28%),linear-gradient(180deg,rgba(245,158,11,0.10),transparent 44%),var(--background)", comment: "IssueChatUxLab.tsx:139 + InviteUxLab.tsx:700 hero-card gradient (identical string, 2 sites)." },
{ name: "gradient-extract-10", value: "linear-gradient(180deg,rgba(168,85,247,0.06),transparent 28%),var(--background)", comment: "IssueChatUxLab.tsx:203 + InviteUxLab.tsx:909 accent gradient (identical string, 2 sites)." },
{ name: "gradient-extract-11", value: "linear-gradient(180deg,rgba(16,185,129,0.06),transparent 28%),var(--background)", comment: "IssueChatUxLab.tsx:226 accent gradient." },
{ name: "gradient-extract-12", value: "linear-gradient(180deg,rgba(6,182,212,0.05),transparent 28%),var(--background)", comment: "IssueChatUxLab.tsx:263 accent gradient." },
{ name: "gradient-extract-13", value: "linear-gradient(180deg,rgba(59,130,246,0.06),transparent 28%),var(--background)", comment: "IssueChatUxLab.tsx:294 accent gradient." },
{ name: "gradient-extract-14", value: "linear-gradient(180deg,rgba(168,85,247,0.05),transparent 26%),var(--background)", comment: "IssueChatUxLab.tsx:315 accent gradient." },
{ name: "gradient-extract-15", value: "linear-gradient(180deg,rgba(245,158,11,0.08),transparent 26%),var(--background)", comment: "IssueChatUxLab.tsx:339 accent gradient." },
{ name: "gradient-extract-16", value: "linear-gradient(135deg,rgba(245,158,11,0.10),transparent 28%),linear-gradient(180deg,rgba(8,145,178,0.08),transparent 44%),var(--background)", comment: "SystemNoticeUxLab.tsx:140 hero-card gradient." },
{ name: "gradient-extract-17", value: "linear-gradient(180deg,rgba(245,158,11,0.05),transparent 28%),var(--background)", comment: "SystemNoticeUxLab.tsx:193 accent gradient." },
{ name: "gradient-extract-18", value: "linear-gradient(180deg,rgba(8,145,178,0.05),transparent 28%),var(--background)", comment: "SystemNoticeUxLab.tsx:225 accent gradient." },
{ name: "gradient-extract-19", value: "linear-gradient(180deg,rgba(244,63,94,0.05),transparent 28%),var(--background)", comment: "SystemNoticeUxLab.tsx:289 accent gradient." },
{ name: "gradient-extract-20", value: "linear-gradient(180deg,rgba(16,185,129,0.05),transparent 28%),var(--background)", comment: "SystemNoticeUxLab.tsx:331 accent gradient." },
{ name: "gradient-extract-21", value: "linear-gradient(180deg,rgba(59,130,246,0.05),transparent 30%),var(--background)", comment: "InviteUxLab.tsx:753 accent gradient." },
{ name: "gradient-extract-22", value: "linear-gradient(180deg,rgba(234,179,8,0.06),transparent 28%),var(--background)", comment: "InviteUxLab.tsx:807 accent gradient." },
{ name: "gradient-extract-23", value: "linear-gradient(180deg,rgba(16,185,129,0.06),transparent 30%),var(--background)", comment: "InviteUxLab.tsx:884 accent gradient." },
{ name: "gradient-extract-24", value: "linear-gradient(180deg,rgba(244,114,182,0.06),transparent 28%),var(--background)", comment: "InviteUxLab.tsx:921 accent gradient." },
];
/**
* Existing-token reuse map hardcoded value -> existing index.css token,
* used ONLY where the value exact-matches (case-insensitive) a token whose
* value is IDENTICAL in :root and .dark (mode-independent brand tier per
* DESIGN.md). Both matches found in this batch are status hues with no
* `.dark` override in index.css.
*/
const REUSE = {
"#2563EB": "var(--status-task-in_progress)", // == --status-agent-running, both #2563eb, mode-independent
"#22c55e": "var(--status-task-done)",
};
/**
* SITE TABLE [relative file path, find, replace]. Order-independent;
* every find/replace pair is applied with a single non-global `.replace()`
* per occurrence count noted, so duplicate literal strings within one file
* (e.g. the three `text-[#959596]` in ActivityFeed.tsx) are handled via
* `replaceAll` where explicitly marked.
*/
const SITES = [
// ── Tailwind bracket hex-class sites ──────────────────────────────
{
file: "components/ActivityFeed.tsx",
replaceAll: [
['className="font-medium text-[#959596] group-hover:text-white"', 'className="font-medium text-(--hex-959596) group-hover:text-white"'],
['className="ml-1 text-[#959596]"', 'className="ml-1 text-(--hex-959596)"'],
['className="ml-1 text-[#959596] group-hover:text-white"', 'className="ml-1 text-(--hex-959596) group-hover:text-white"'],
],
},
{
file: "components/FeedCard.tsx",
replaceAll: [
['isMuted ? "text-muted-foreground/70" : "text-[#959596]"', 'isMuted ? "text-muted-foreground/70" : "text-(--hex-959596)"'],
],
},
{
file: "components/OnboardingWizard.tsx",
replaceAll: [
['"hidden md:block overflow-hidden bg-[#1d1d1d] transition-[width,opacity] duration-500 ease-in-out"', '"hidden md:block overflow-hidden bg-(--hex-1d1d1d) transition-[width,opacity] duration-500 ease-in-out"'],
],
},
{
file: "components/IssueChatThread.tsx",
replaceAll: [
['// Liveness blue (#2563EB) for the human\'s own messages (PAP-95 rev 5).', '// Liveness blue (--status-task-in_progress) for the human\'s own messages (PAP-95 rev 5).'],
['? "bg-[#2563EB] text-white"', '? "bg-(--status-task-in_progress) text-white"'],
],
},
{
file: "components/IssueChatThread.test.tsx",
replaceAll: [
['expect(bubble?.className).not.toContain("bg-[#2563EB]");', 'expect(bubble?.className).not.toContain("bg-(--status-task-in_progress)");'],
],
},
// ── OrgChart.tsx status dot color map (pure style render) ─────────
{
file: "pages/OrgChart.tsx",
replaceAll: [
['running: "#22d3ee",', 'running: "var(--hex-22d3ee)",'],
['active: "#4ade80",', 'active: "var(--hex-4ade80)",'],
['paused: "#facc15",\n idle: "#facc15",', 'paused: "var(--hex-facc15)",\n idle: "var(--hex-facc15)",'],
['error: "#f87171",', 'error: "var(--hex-f87171)",'],
['terminated: "#a3a3a3",', 'terminated: "var(--hex-a3a3a3)",'],
['const defaultDotColor = "#a3a3a3";', 'const defaultDotColor = "var(--hex-a3a3a3)";'],
],
},
// ── ActivityCharts.tsx priority + status color maps (pure style render) ──
{
file: "components/ActivityCharts.tsx",
replaceAll: [
['critical: "#ef4444",', 'critical: "var(--hex-ef4444)",'],
['high: "#f97316",', 'high: "var(--hex-f97316)",'],
['medium: "#eab308",', 'medium: "var(--hex-eab308)",'],
['low: "#6b7280",', 'low: "var(--hex-6b7280)",'],
['todo: "#3b82f6",', 'todo: "var(--hex-3b82f6)",'],
['in_progress: "#8b5cf6",', 'in_progress: "var(--hex-8b5cf6)",'],
['in_review: "#a855f7",', 'in_review: "var(--hex-a855f7)",'],
['done: "#10b981",', 'done: "var(--hex-10b981)",'],
['blocked: "#ef4444",', 'blocked: "var(--hex-ef4444)",'],
['cancelled: "#6b7280",', 'cancelled: "var(--hex-6b7280)",'],
['backlog: "#64748b",', 'backlog: "var(--hex-64748b)",'],
['backgroundColor: statusColors[s] ?? "#6b7280"', 'backgroundColor: statusColors[s] ?? "var(--hex-6b7280)"'],
['color: statusColors[s] ?? "#6b7280"', 'color: statusColors[s] ?? "var(--hex-6b7280)"'],
['rate >= 0.8 ? "#10b981" : rate >= 0.5 ? "#eab308" : "#ef4444"', 'rate >= 0.8 ? "var(--hex-10b981)" : rate >= 0.5 ? "var(--hex-eab308)" : "var(--hex-ef4444)"'],
],
},
// ── Project-color-fallback pure-style-render sites (indigo #6366f1) ──
{
file: "pages/ProjectDetail.tsx",
replaceAll: [
['backgroundColor: project.color ?? "#6366f1"', 'backgroundColor: project.color ?? "var(--hex-6366f1)"'],
],
},
{
file: "pages/PipelineSettings.tsx",
replaceAll: [
['backgroundColor: selectedAutomationProject.color ?? "#6366f1"', 'backgroundColor: selectedAutomationProject.color ?? "var(--hex-6366f1)"'],
['backgroundColor: project?.color ?? "#6366f1"', 'backgroundColor: project?.color ?? "var(--hex-6366f1)"'],
],
},
{
file: "components/issue-properties/IssueProperties.tsx",
replaceAll: [
['backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "#6366f1"', 'backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "var(--hex-6366f1)"'],
['backgroundColor: option.color ?? "#6366f1"', 'backgroundColor: option.color ?? "var(--hex-6366f1)"'],
],
},
{
file: "components/NewIssueDialog.tsx",
replaceAll: [
['backgroundColor: currentProject.color ?? "#6366f1"', 'backgroundColor: currentProject.color ?? "var(--hex-6366f1)"'],
['backgroundColor: project?.color ?? "#6366f1"', 'backgroundColor: project?.color ?? "var(--hex-6366f1)"'],
],
},
// ── Project-color-fallback pure-style-render sites (slate #64748b) ──
{
file: "pages/Routines.tsx",
replaceAll: [
['backgroundColor: currentProject.color ?? "#64748b"', 'backgroundColor: currentProject.color ?? "var(--hex-64748b)"'],
['backgroundColor: project?.color ?? "#64748b"', 'backgroundColor: project?.color ?? "var(--hex-64748b)"'],
],
},
{
file: "components/MarkdownEditor.tsx",
replaceAll: [
['backgroundColor: option.projectColor ?? "#64748b"', 'backgroundColor: option.projectColor ?? "var(--hex-64748b)"'],
],
},
{
file: "components/RoutineRunVariablesDialog.tsx",
replaceAll: [
['backgroundColor: selectedProject.color ?? "#64748b"', 'backgroundColor: selectedProject.color ?? "var(--hex-64748b)"'],
['backgroundColor: project?.color ?? "#64748b"', 'backgroundColor: project?.color ?? "var(--hex-64748b)"'],
],
},
{
file: "components/RoutineList.tsx",
replaceAll: [
['backgroundColor: project?.color ?? "#64748b"', 'backgroundColor: project?.color ?? "var(--hex-64748b)"'],
],
},
{
file: "components/routine-sections/editable-sections.tsx",
replaceAll: [
['backgroundColor: currentProject.color ?? "#64748b"', 'backgroundColor: currentProject.color ?? "var(--hex-64748b)"'],
['backgroundColor: project?.color ?? "#64748b"', 'backgroundColor: project?.color ?? "var(--hex-64748b)"'],
],
},
// ── Gradient sites (verbatim value -> --gradient-extract-N token) ─
{
file: "pages/Dashboard.tsx",
replaceAll: [
['bg-[linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))]', 'bg-(image:--gradient-extract-1)'],
],
},
{
file: "pages/Costs.tsx",
replaceAll: [
['bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]', 'bg-(image:--gradient-extract-2)'],
],
},
{
file: "components/AccountingModelCard.tsx",
replaceAll: [
['bg-[radial-gradient(circle_at_top_left,rgba(244,114,182,0.08),transparent_35%),radial-gradient(circle_at_bottom_right,rgba(56,189,248,0.1),transparent_32%)]', 'bg-(image:--gradient-extract-3)'],
],
},
{
file: "components/BudgetIncidentCard.tsx",
replaceAll: [
['bg-[linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))]', 'bg-(image:--gradient-extract-4)'],
],
},
{
file: "pages/RunTranscriptUxLab.tsx",
replaceAll: [
['bg-[radial-gradient(circle_at_top_left,rgba(8,145,178,0.08),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(245,158,11,0.10),transparent_28%)]', 'bg-(image:--gradient-extract-5)'],
['bg-[linear-gradient(135deg,rgba(8,145,178,0.08),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.08),transparent_40%),var(--background)]', 'bg-(image:--gradient-extract-6)'],
],
},
{
file: "pages/ProfileSettings.tsx",
replaceAll: [
['bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.22),transparent_34%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.08),transparent_36%)]', 'bg-(image:--gradient-extract-7)'],
],
},
{
file: "pages/InviteUxLab.tsx",
replaceAll: [
['bg-[radial-gradient(circle_at_top,rgba(8,145,178,0.18),transparent_48%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,1))]', 'bg-(image:--gradient-extract-8)'],
['bg-[linear-gradient(135deg,rgba(8,145,178,0.10),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.10),transparent_44%),var(--background)]', 'bg-(image:--gradient-extract-9)'],
['bg-[linear-gradient(180deg,rgba(59,130,246,0.05),transparent_30%),var(--background)]', 'bg-(image:--gradient-extract-21)'],
['bg-[linear-gradient(180deg,rgba(234,179,8,0.06),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-22)'],
['bg-[linear-gradient(180deg,rgba(16,185,129,0.06),transparent_30%),var(--background)]', 'bg-(image:--gradient-extract-23)'],
['bg-[linear-gradient(180deg,rgba(168,85,247,0.06),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-10)'],
['bg-[linear-gradient(180deg,rgba(244,114,182,0.06),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-24)'],
],
},
{
file: "pages/IssueChatUxLab.tsx",
replaceAll: [
['bg-[linear-gradient(135deg,rgba(8,145,178,0.10),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.10),transparent_44%),var(--background)]', 'bg-(image:--gradient-extract-9)'],
['bg-[linear-gradient(180deg,rgba(168,85,247,0.06),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-10)'],
['bg-[linear-gradient(180deg,rgba(16,185,129,0.06),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-11)'],
['bg-[linear-gradient(180deg,rgba(6,182,212,0.05),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-12)'],
['bg-[linear-gradient(180deg,rgba(59,130,246,0.06),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-13)'],
['bg-[linear-gradient(180deg,rgba(168,85,247,0.05),transparent_26%),var(--background)]', 'bg-(image:--gradient-extract-14)'],
['bg-[linear-gradient(180deg,rgba(245,158,11,0.08),transparent_26%),var(--background)]', 'bg-(image:--gradient-extract-15)'],
],
},
{
file: "pages/SystemNoticeUxLab.tsx",
replaceAll: [
['bg-[linear-gradient(135deg,rgba(245,158,11,0.10),transparent_28%),linear-gradient(180deg,rgba(8,145,178,0.08),transparent_44%),var(--background)]', 'bg-(image:--gradient-extract-16)'],
['bg-[linear-gradient(180deg,rgba(245,158,11,0.05),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-17)'],
['bg-[linear-gradient(180deg,rgba(8,145,178,0.05),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-18)'],
['bg-[linear-gradient(180deg,rgba(244,63,94,0.05),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-19)'],
['bg-[linear-gradient(180deg,rgba(16,185,129,0.05),transparent_28%),var(--background)]', 'bg-(image:--gradient-extract-20)'],
],
},
];
// ── Allowlist — sites intentionally NOT rewritten (functional / third-party) ──
// One entry per file; each also gets an inline
// `/* token-extraction: allowlisted — ... */` comment injected at the site
// (idempotent: only injected if not already present).
const ALLOWLIST_COMMENTS = [
{
file: "pages/CompanyEnvironments.tsx",
anchor: 'background: "#0a0a0a",',
commentLine: " // token-extraction: allowlisted — xterm.js terminal theme config; functional third-party option object, not a rendered CSS value.",
},
{
file: "pages/CompanySettings.tsx",
anchor: ' <input\n type="color"',
commentLine: " {/* token-extraction: allowlisted — <input type=\"color\"> value must be a real hex string, not a var() reference. */}",
},
{
file: "components/issue-properties/IssueProperties.tsx",
anchor: 'const [newLabelColor, setNewLabelColor] = useState("#6366f1");',
commentLine: " // token-extraction: allowlisted — color-picker seed state, persisted into label-create payload; a var() string would break that payload.",
},
{
file: "pages/CompanySkills.tsx",
anchor: "const DISCOVERY_ACCENTS = [",
commentLine: "// token-extraction: allowlisted — skill.color is persisted/compared JS data (SkillCreateDraft), not just a rendered value; a var() string would corrupt it.",
},
{
file: "components/IssueColumns.tsx",
anchor: 'const accentColor = projectColor ?? "#64748b";',
commentLine: " // token-extraction: allowlisted — accentColor also feeds pickTextColorForPillBg() contrast math; a var() string can't be parsed as a hex color there.",
},
{
file: "components/CompanyPatternIcon.tsx",
anchor: "ctx.fillStyle = `rgb(${offR} ${offG} ${offB})`;",
commentLine: " // token-extraction: allowlisted — canvas 2D fillStyle computed at runtime from numeric channel props; not a static literal.",
},
{
file: "components/FileViewerSheet.tsx",
anchor: 'isHighlighted && "bg-[var(--paperclip-code-highlight-bg,rgba(250,204,21,0.12))]",',
commentLine: " // token-extraction: allowlisted — half-migrated var(--x, fallback) pattern; --paperclip-code-highlight-bg/-border don't exist in index.css yet. Needs human decision (see TOKEN-AUDIT.md 2) before minting, since defining the var changes a fallback-CSS-var-expression, not a plain literal swap.",
},
{
file: "pages/InviteUxLab.tsx",
anchor: ' <div className="flex items-start gap-4">\n <CompanyPatternIcon\n companyName="Acme Robotics"\n logoUrl="/api/invites/pcp_invite_test/logo"\n brandColor="#114488"\n className="h-16 w-16 rounded-none border border-zinc-800"',
commentLine: " {/* token-extraction: allowlisted — brandColor feeds CompanyPatternIcon's hexToHue() color math via a canvas fill; demo/showcase-only prop, not a rendered CSS value. */}",
},
{
file: "pages/InviteUxLab.tsx",
anchor: ' <div className="flex items-center gap-3">\n <CompanyPatternIcon\n companyName="Acme Robotics"\n logoUrl="/api/invites/pcp_invite_test/logo"\n brandColor="#114488"\n className="h-12 w-12 rounded-none border border-zinc-800"',
commentLine: " {/* token-extraction: allowlisted — brandColor feeds CompanyPatternIcon's hexToHue() color math via a canvas fill; demo/showcase-only prop, not a rendered CSS value. */}",
},
];
function applyReplacements(content, pairs, filePath) {
let next = content;
let count = 0;
for (const [find, replace] of pairs) {
if (next.includes(find)) {
next = next.split(find).join(replace);
count += 1;
} else if (!next.includes(replace)) {
// Neither the original nor the replacement is present — likely a
// stale site table entry; flag loudly rather than silently no-op.
console.warn(` ! WARNING: pattern not found (and not already applied) in ${filePath}:\n ${find.slice(0, 100)}`);
}
}
return { next, count };
}
function injectAllowlistComment(content, anchor, commentLine, filePath) {
if (content.includes(commentLine.trim())) return { next: content, injected: false }; // idempotent
const idx = content.indexOf(anchor);
if (idx === -1) {
console.warn(` ! WARNING: allowlist anchor not found in ${filePath}:\n ${anchor}`);
return { next: content, injected: false };
}
const lineStart = content.lastIndexOf("\n", idx) + 1;
const next = content.slice(0, lineStart) + commentLine + "\n" + content.slice(lineStart);
return { next, injected: true };
}
function main() {
let totalSitesRewritten = 0;
let filesChanged = 0;
const changedFiles = [];
for (const site of SITES) {
const filePath = resolve(UI_SRC, site.file);
const original = readFileSync(filePath, "utf8");
const { next, count } = applyReplacements(original, site.replaceAll, site.file);
if (next !== original) {
filesChanged += 1;
changedFiles.push(site.file);
if (!DRY_RUN) writeFileSync(filePath, next, "utf8");
}
totalSitesRewritten += count;
}
let allowlistInjections = 0;
for (const entry of ALLOWLIST_COMMENTS) {
const filePath = resolve(UI_SRC, entry.file);
const original = readFileSync(filePath, "utf8");
const { next, injected } = injectAllowlistComment(original, entry.anchor, entry.commentLine, entry.file);
if (injected) {
allowlistInjections += 1;
if (!DRY_RUN) writeFileSync(filePath, next, "utf8");
if (!changedFiles.includes(entry.file)) {
filesChanged += 1;
changedFiles.push(entry.file);
}
}
}
// ── index.css token block ──────────────────────────────────────────
const cssPath = resolve(UI_SRC, "index.css");
const cssOriginal = readFileSync(cssPath, "utf8");
const marker = "/* ── Extracted verbatim tokens (Phase 2, design/token-extraction) ── */";
let cssNext = cssOriginal;
let cssChanged = false;
if (!cssOriginal.includes(marker)) {
const tokenLines = NEW_TOKENS.map((t) => ` --${t.name}: ${t.value}; /* ${t.comment} */`).join("\n");
const block = `\n${marker}\n/* Batch 1/4: color literals only. Reused-from-existing-token sites (see\n TOKEN-AUDIT.md section 1.1) are NOT duplicated here — they reference\n --status-task-in_progress / --status-task-done directly at the call site.\n\n Allowlist (sites intentionally left as hardcoded / functional literals,\n NOT converted to tokens — each also carries an inline\n \`token-extraction: allowlisted\` comment at the site):\n - pages/CompanyEnvironments.tsx — xterm.js terminal theme config; functional JS values, third-party.\n - pages/CompanySettings.tsx — <input type="color"> value; functional form control, not a rendered value.\n - components/issue-properties/IssueProperties.tsx (newLabelColor) — color-picker seed persisted into label-create payload.\n - pages/CompanySkills.tsx (DISCOVERY_ACCENTS) — persisted/compared skill.color JS data, not just rendered.\n - components/IssueColumns.tsx (accentColor fallback) — also feeds pickTextColorForPillBg() contrast math.\n - components/CompanyPatternIcon.tsx — canvas fillStyle computed at runtime from numeric props, not a static literal.\n - components/FileViewerSheet.tsx — half-migrated var(--paperclip-code-highlight-*, fallback) pattern; needs human decision, see TOKEN-AUDIT.md section 2.\n - pages/InviteUxLab.tsx (brandColor prop, x2) — demo/showcase-only prop feeding CompanyPatternIcon's hexToHue() color math, not a rendered CSS value.\n*/\n:root {\n${tokenLines}\n}\n`;
cssNext = cssOriginal + block;
cssChanged = true;
}
if (cssChanged && !DRY_RUN) writeFileSync(cssPath, cssNext, "utf8");
// ── Summary ─────────────────────────────────────────────────────────
console.log(`\n${DRY_RUN ? "[DRY RUN] " : ""}codemod-extract-colors summary`);
console.log(` Sites rewritten: ${totalSitesRewritten}`);
console.log(` Component/page files changed: ${filesChanged}`);
console.log(` Allowlist comments injected: ${allowlistInjections}`);
console.log(` New tokens minted: ${NEW_TOKENS.length}`);
console.log(` Existing tokens reused: ${Object.keys(REUSE).length} (${Object.values(REUSE).join(", ")})`);
console.log(` index.css token block: ${cssChanged ? "added" : "already present (idempotent no-op)"}`);
if (changedFiles.length) {
console.log(`\n Changed files:`);
for (const f of changedFiles) console.log(` - ui/src/${f}`);
}
}
main();

View File

@ -28,7 +28,7 @@ const SURFACES = [
export function AccountingModelCard() {
return (
<Card className="relative overflow-hidden border-border/70">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(244,114,182,0.08),transparent_35%),radial-gradient(circle_at_bottom_right,rgba(56,189,248,0.1),transparent_32%)]" />
<div className="absolute inset-0 bg-(image:--gradient-extract-3)" />
<CardHeader className="relative px-5 pt-5 pb-2">
<CardTitle className="text-sm font-semibold uppercase tracking-[0.22em] text-muted-foreground">
Accounting model

View File

@ -122,10 +122,10 @@ export function RunActivityChart(props: RunChartProps) {
}
const priorityColors: Record<string, string> = {
critical: "#ef4444",
high: "#f97316",
medium: "#eab308",
low: "#6b7280",
critical: "var(--hex-ef4444)",
high: "var(--hex-f97316)",
medium: "var(--hex-eab308)",
low: "var(--hex-6b7280)",
};
const priorityOrder = ["critical", "high", "medium", "low"] as const;
@ -175,13 +175,13 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA
}
const statusColors: Record<string, string> = {
todo: "#3b82f6",
in_progress: "#8b5cf6",
in_review: "#a855f7",
done: "#10b981",
blocked: "#ef4444",
cancelled: "#6b7280",
backlog: "#64748b",
todo: "var(--hex-3b82f6)",
in_progress: "var(--hex-8b5cf6)",
in_review: "var(--hex-a855f7)",
done: "var(--hex-10b981)",
blocked: "var(--hex-ef4444)",
cancelled: "var(--hex-6b7280)",
backlog: "var(--hex-64748b)",
};
const statusLabels: Record<string, string> = {
@ -225,7 +225,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
{total > 0 ? (
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
{statusOrder.map(s => (entry[s] ?? 0) > 0 ? (
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "#6b7280" }} />
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "var(--hex-6b7280)" }} />
) : null)}
</div>
) : (
@ -236,7 +236,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
})}
</div>
<DateLabels days={days} />
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "#6b7280", label: statusLabels[s] ?? s }))} />
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "var(--hex-6b7280)", label: statusLabels[s] ?? s }))} />
</div>
);
}
@ -255,7 +255,7 @@ export function SuccessRateChart(props: RunChartProps) {
{days.map(day => {
const entry = grouped.get(day) ?? { date: day, succeeded: 0, failed: 0, other: 0, total: 0 };
const rate = entry.total > 0 ? entry.succeeded / entry.total : 0;
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "#10b981" : rate >= 0.5 ? "#eab308" : "#ef4444";
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--hex-10b981)" : rate >= 0.5 ? "var(--hex-eab308)" : "var(--hex-ef4444)";
return (
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${entry.total > 0 ? Math.round(rate * 100) : 0}% (${entry.succeeded}/${entry.total})`}>
{entry.total > 0 ? (

View File

@ -283,9 +283,9 @@ function CollapsedFeedGroup({
: <Settings className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
}
<span className="flex-1 min-w-0 truncate">
<span data-fc="actor" className="font-medium text-[#959596] group-hover:text-white">{actorName}</span>
<span data-fc="verb" className="ml-1 text-[#959596]">made {group.events.length} updates to</span>
<span data-fc="title" className="ml-1 text-[#959596] group-hover:text-white">{entityName ?? group.entityId}</span>
<span data-fc="actor" className="font-medium text-(--hex-959596) group-hover:text-white">{actorName}</span>
<span data-fc="verb" className="ml-1 text-(--hex-959596)">made {group.events.length} updates to</span>
<span data-fc="title" className="ml-1 text-(--hex-959596) group-hover:text-white">{entityName ?? group.entityId}</span>
</span>
<span data-fc="time" className="text-muted-foreground shrink-0">
{timeAgo(group.latestEvent.createdAt)}

View File

@ -43,7 +43,7 @@ export function BudgetIncidentCard({
const stateLabel = incidentStateLabel(incident);
return (
<Card className="overflow-hidden border-red-500/20 bg-[linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))]">
<Card className="overflow-hidden border-red-500/20 bg-(image:--gradient-extract-4)">
<CardHeader className="px-5 pt-5 pb-3">
<div className="flex items-start justify-between gap-3">
<div>

View File

@ -125,6 +125,7 @@ function makeCompanyPatternDataUrl(seed: string, brandColor?: string | null, log
const diagonalPhase = rand() * Math.PI * 2;
const antiDiagonalPhase = rand() * Math.PI * 2;
// token-extraction: allowlisted — canvas 2D fillStyle computed at runtime from numeric channel props; not a static literal.
ctx.fillStyle = `rgb(${offR} ${offG} ${offB})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);

View File

@ -429,7 +429,7 @@ export function FeedCard({
const verb = formatVerb(event.action, details, isPinned ? "pinned" : "chronological");
const iconSpec = getIconSpec(event, details, isActive);
const mutedTextBase = isMuted ? "text-muted-foreground/70" : "text-[#959596]";
const mutedTextBase = isMuted ? "text-muted-foreground/70" : "text-(--hex-959596)";
const mutedTextHover = isMuted ? "" : "group-hover:text-white";
const card = (

View File

@ -367,6 +367,7 @@ export function FileContentViewer({ content, highlightedLine, onLoaded }: FileCo
data-line-number={lineNumber}
className={cn(
"grid grid-cols-[auto_minmax(0,1fr)]",
// token-extraction: allowlisted — half-migrated var(--x, fallback) pattern; --paperclip-code-highlight-bg/-border don't exist in index.css yet. Needs human decision (see TOKEN-AUDIT.md 2) before minting, since defining the var changes a fallback-CSS-var-expression, not a plain literal swap.
isHighlighted && "bg-[var(--paperclip-code-highlight-bg,rgba(250,204,21,0.12))]",
)}
>

View File

@ -1757,7 +1757,7 @@ describe("IssueChatThread", () => {
expect(bubble?.className).toContain("max-w-[calc(100%-0.5rem)]");
expect(bubble?.className).toContain("sm:max-w-[85%]");
// Neutral, not the human liveness-blue bubble.
expect(bubble?.className).not.toContain("bg-[#2563EB]");
expect(bubble?.className).not.toContain("bg-(--status-task-in_progress)");
act(() => {
root.unmount();

View File

@ -1447,8 +1447,8 @@ function IssueChatUserMessage({
: deleted
? "bg-muted/50 text-muted-foreground"
: isCurrentUser
// Liveness blue (#2563EB) for the human's own messages (PAP-95 rev 5).
? "bg-[#2563EB] text-white"
// Liveness blue (--status-task-in_progress) for the human's own messages (PAP-95 rev 5).
? "bg-(--status-task-in_progress) text-white"
: "bg-muted",
pending && "opacity-80",
)}

View File

@ -359,6 +359,7 @@ export function InboxIssueTrailingColumns({
if (column === "project") {
if (projectName) {
// token-extraction: allowlisted — accentColor also feeds pickTextColorForPillBg() contrast math; a var() string can't be parsed as a hex color there.
const accentColor = projectColor ?? "#64748b";
return (
<span

View File

@ -1352,7 +1352,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
) : option.kind === "project" && option.projectId ? (
<span
className="inline-flex h-2 w-2 rounded-full border border-border/50"
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
style={{ backgroundColor: option.projectColor ?? "var(--hex-64748b)" }}
/>
) : option.kind === "user" ? (
<User className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />

View File

@ -1486,7 +1486,7 @@ export function NewIssueDialog() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: currentProject.color ?? "#6366f1" }}
style={{ backgroundColor: currentProject.color ?? "var(--hex-6366f1)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -1501,7 +1501,7 @@ export function NewIssueDialog() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#6366f1" }}
style={{ backgroundColor: project?.color ?? "var(--hex-6366f1)" }}
/>
<span className="truncate">{option.label}</span>
</>

View File

@ -1719,7 +1719,7 @@ export function OnboardingWizard() {
name + mission steps) */}
<div
className={cn(
"hidden md:block overflow-hidden bg-[#1d1d1d] transition-[width,opacity] duration-500 ease-in-out",
"hidden md:block overflow-hidden bg-(--hex-1d1d1d) transition-[width,opacity] duration-500 ease-in-out",
step === 1 || step === 2 ? "w-1/2 opacity-100" : "w-0 opacity-0"
)}
>

View File

@ -118,7 +118,7 @@ export function RoutineListRow<TRoutine extends RoutineListRowItem>({
<span className="flex items-center gap-2">
<span
className="h-2.5 w-2.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#64748b" }}
style={{ backgroundColor: project?.color ?? "var(--hex-64748b)" }}
/>
<span>{routine.projectId ? (project?.name ?? "Unknown project") : "No project"}</span>
</span>

View File

@ -423,7 +423,7 @@ export function RoutineRunVariablesDialog({
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: selectedProject.color ?? "#64748b" }}
style={{ backgroundColor: selectedProject.color ?? "var(--hex-64748b)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -438,7 +438,7 @@ export function RoutineRunVariablesDialog({
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#64748b" }}
style={{ backgroundColor: project?.color ?? "var(--hex-64748b)" }}
/>
<span className="truncate">{option.label}</span>
</>

View File

@ -178,6 +178,7 @@ export function IssueProperties({
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
const [labelSearch, setLabelSearch] = useState("");
const [newLabelName, setNewLabelName] = useState("");
// token-extraction: allowlisted — color-picker seed state, persisted into label-create payload; a var() string would break that payload.
const [newLabelColor, setNewLabelColor] = useState("#6366f1");
const [monitorAtInput, setMonitorAtInput] = useState(() => toDateTimeLocalValue(issue.executionPolicy?.monitor?.nextCheckAt));
const [monitorNotesInput, setMonitorNotesInput] = useState(issue.executionPolicy?.monitor?.notes ?? "");
@ -1573,7 +1574,7 @@ export function IssueProperties({
<>
<span
className="shrink-0 h-3 w-3 rounded-sm"
style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "#6366f1" }}
style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "var(--hex-6366f1)" }}
/>
<span className="text-sm truncate min-w-0" title={projectName(issue.projectId)}>{projectName(issue.projectId)}</span>
</>
@ -1646,7 +1647,7 @@ export function IssueProperties({
{option.kind === "project" ? (
<span
className="shrink-0 h-3 w-3 rounded-sm"
style={{ backgroundColor: option.color ?? "#6366f1" }}
style={{ backgroundColor: option.color ?? "var(--hex-6366f1)" }}
/>
) : null}
{option.name}

View File

@ -190,7 +190,7 @@ export function OverviewSection({
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
style={{ backgroundColor: currentProject.color ?? "var(--hex-64748b)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -205,7 +205,7 @@ export function OverviewSection({
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#64748b" }}
style={{ backgroundColor: project?.color ?? "var(--hex-64748b)" }}
/>
<span className="truncate">{option.label}</span>
</>

View File

@ -1429,3 +1429,64 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
.status-fill {
background-color: var(--sc);
}
/* ── Extracted verbatim tokens (Phase 2, design/token-extraction) ── */
/* Batch 1/4: color literals only. Reused-from-existing-token sites (see
TOKEN-AUDIT.md section 1.1) are NOT duplicated here they reference
--status-task-in_progress / --status-task-done directly at the call site.
Allowlist (sites intentionally left as hardcoded / functional literals,
NOT converted to tokens each also carries an inline
`token-extraction: allowlisted` comment at the site):
- pages/CompanyEnvironments.tsx xterm.js terminal theme config; functional JS values, third-party.
- pages/CompanySettings.tsx <input type="color"> value; functional form control, not a rendered value.
- components/issue-properties/IssueProperties.tsx (newLabelColor) color-picker seed persisted into label-create payload.
- pages/CompanySkills.tsx (DISCOVERY_ACCENTS) persisted/compared skill.color JS data, not just rendered.
- components/IssueColumns.tsx (accentColor fallback) also feeds pickTextColorForPillBg() contrast math.
- components/CompanyPatternIcon.tsx canvas fillStyle computed at runtime from numeric props, not a static literal.
- components/FileViewerSheet.tsx half-migrated var(--paperclip-code-highlight-*, fallback) pattern; needs human decision, see TOKEN-AUDIT.md section 2.
- pages/InviteUxLab.tsx (brandColor prop, x2) demo/showcase-only prop feeding CompanyPatternIcon's hexToHue() color math, not a rendered CSS value.
*/
:root {
--hex-959596: #959596; /* Muted feed actor/verb/title text (ActivityFeed.tsx, FeedCard.tsx) — PRIOR-ART-flagged gap cluster, no existing token match. */
--hex-1d1d1d: #1d1d1d; /* OnboardingWizard.tsx dark decorative panel background, singleton. */
--hex-22d3ee: #22d3ee; /* OrgChart.tsx agent status dot — 'running' (independent palette from --status-agent-*, see TOKEN-AUDIT.md 1.2). */
--hex-4ade80: #4ade80; /* OrgChart.tsx agent status dot — 'active'. */
--hex-facc15: #facc15; /* OrgChart.tsx agent status dot — 'paused' / 'idle' (shared value). */
--hex-f87171: #f87171; /* OrgChart.tsx agent status dot — 'error'. */
--hex-a3a3a3: #a3a3a3; /* OrgChart.tsx agent status dot — 'terminated' + defaultDotColor fallback. */
--hex-ef4444: #ef4444; /* ActivityCharts.tsx — priority 'critical' + status 'blocked' (shared value); also the <0.5 success-rate bar tint. */
--hex-f97316: #f97316; /* ActivityCharts.tsx — priority 'high'. */
--hex-eab308: #eab308; /* ActivityCharts.tsx — priority 'medium'; also the 0.5-0.8 success-rate bar tint. */
--hex-6b7280: #6b7280; /* ActivityCharts.tsx — priority 'low' + status 'cancelled' + statusColors fallback (shared value). */
--hex-3b82f6: #3b82f6; /* ActivityCharts.tsx — status 'todo' (independent from --status-task-todo which is #f59e0b). */
--hex-8b5cf6: #8b5cf6; /* ActivityCharts.tsx — status 'in_progress' (independent from --status-task-in_progress which is #2563eb — flagged inconsistency, TOKEN-AUDIT.md 1.2). */
--hex-a855f7: #a855f7; /* ActivityCharts.tsx — status 'in_review'. */
--hex-10b981: #10b981; /* ActivityCharts.tsx — status 'done'; also the >=0.8 success-rate bar tint. */
--hex-64748b: #64748b; /* ActivityCharts.tsx status 'backlog'; also the widely-repeated 'no project assigned' muted-slate fallback color (TOKEN-AUDIT.md 1.3) across Routines/MarkdownEditor/RoutineRunVariablesDialog/RoutineList/IssueColumns/editable-sections. */
--hex-6366f1: #6366f1; /* Project-color-fallback indigo seed default (ProjectDetail/PipelineSettings/IssueProperties/NewIssueDialog) — new-project-color-picker-seed family per TOKEN-AUDIT.md 1.3. */
--gradient-extract-1: linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02)); /* Dashboard.tsx budget-alert card gradient. */
--gradient-extract-2: linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02)); /* Costs.tsx subtle white card gradient. */
--gradient-extract-3: radial-gradient(circle at top left,rgba(244,114,182,0.08),transparent 35%),radial-gradient(circle at bottom right,rgba(56,189,248,0.1),transparent 32%); /* AccountingModelCard.tsx decorative overlay. */
--gradient-extract-4: linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02)); /* BudgetIncidentCard.tsx incident-card gradient. */
--gradient-extract-5: radial-gradient(circle at top left,rgba(8,145,178,0.08),transparent 36%),radial-gradient(circle at bottom right,rgba(245,158,11,0.10),transparent 28%); /* RunTranscriptUxLab.tsx:78 hero gradient. */
--gradient-extract-6: linear-gradient(135deg,rgba(8,145,178,0.08),transparent 28%),linear-gradient(180deg,rgba(245,158,11,0.08),transparent 40%),var(--background); /* RunTranscriptUxLab.tsx:203 hero-card gradient. */
--gradient-extract-7: radial-gradient(circle at top right,rgba(255,255,255,0.22),transparent 34%),radial-gradient(circle at bottom left,rgba(255,255,255,0.08),transparent 36%); /* ProfileSettings.tsx:162 decorative overlay. */
--gradient-extract-8: radial-gradient(circle at top,rgba(8,145,178,0.18),transparent 48%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,1)); /* InviteUxLab.tsx:510 dark hero gradient. */
--gradient-extract-9: linear-gradient(135deg,rgba(8,145,178,0.10),transparent 28%),linear-gradient(180deg,rgba(245,158,11,0.10),transparent 44%),var(--background); /* IssueChatUxLab.tsx:139 + InviteUxLab.tsx:700 hero-card gradient (identical string, 2 sites). */
--gradient-extract-10: linear-gradient(180deg,rgba(168,85,247,0.06),transparent 28%),var(--background); /* IssueChatUxLab.tsx:203 + InviteUxLab.tsx:909 accent gradient (identical string, 2 sites). */
--gradient-extract-11: linear-gradient(180deg,rgba(16,185,129,0.06),transparent 28%),var(--background); /* IssueChatUxLab.tsx:226 accent gradient. */
--gradient-extract-12: linear-gradient(180deg,rgba(6,182,212,0.05),transparent 28%),var(--background); /* IssueChatUxLab.tsx:263 accent gradient. */
--gradient-extract-13: linear-gradient(180deg,rgba(59,130,246,0.06),transparent 28%),var(--background); /* IssueChatUxLab.tsx:294 accent gradient. */
--gradient-extract-14: linear-gradient(180deg,rgba(168,85,247,0.05),transparent 26%),var(--background); /* IssueChatUxLab.tsx:315 accent gradient. */
--gradient-extract-15: linear-gradient(180deg,rgba(245,158,11,0.08),transparent 26%),var(--background); /* IssueChatUxLab.tsx:339 accent gradient. */
--gradient-extract-16: linear-gradient(135deg,rgba(245,158,11,0.10),transparent 28%),linear-gradient(180deg,rgba(8,145,178,0.08),transparent 44%),var(--background); /* SystemNoticeUxLab.tsx:140 hero-card gradient. */
--gradient-extract-17: linear-gradient(180deg,rgba(245,158,11,0.05),transparent 28%),var(--background); /* SystemNoticeUxLab.tsx:193 accent gradient. */
--gradient-extract-18: linear-gradient(180deg,rgba(8,145,178,0.05),transparent 28%),var(--background); /* SystemNoticeUxLab.tsx:225 accent gradient. */
--gradient-extract-19: linear-gradient(180deg,rgba(244,63,94,0.05),transparent 28%),var(--background); /* SystemNoticeUxLab.tsx:289 accent gradient. */
--gradient-extract-20: linear-gradient(180deg,rgba(16,185,129,0.05),transparent 28%),var(--background); /* SystemNoticeUxLab.tsx:331 accent gradient. */
--gradient-extract-21: linear-gradient(180deg,rgba(59,130,246,0.05),transparent 30%),var(--background); /* InviteUxLab.tsx:753 accent gradient. */
--gradient-extract-22: linear-gradient(180deg,rgba(234,179,8,0.06),transparent 28%),var(--background); /* InviteUxLab.tsx:807 accent gradient. */
--gradient-extract-23: linear-gradient(180deg,rgba(16,185,129,0.06),transparent 30%),var(--background); /* InviteUxLab.tsx:884 accent gradient. */
--gradient-extract-24: linear-gradient(180deg,rgba(244,114,182,0.06),transparent 28%),var(--background); /* InviteUxLab.tsx:921 accent gradient. */
}

View File

@ -424,6 +424,7 @@ function EnvironmentCustomImageBrowserTerminal({
lineHeight: 1.35,
scrollback: CUSTOM_IMAGE_TERMINAL_SCROLLBACK_ROWS,
theme: {
// token-extraction: allowlisted — xterm.js terminal theme config; functional third-party option object, not a rendered CSS value.
background: "#0a0a0a",
foreground: "#f5f5f5",
cursor: "#22d3ee",

View File

@ -267,6 +267,7 @@ export function CompanySettings() {
hint="Sets the hue for the company icon. Leave empty for auto-generated color."
>
<div className="flex items-center gap-2">
{/* token-extraction: allowlisted — <input type="color"> value must be a real hex string, not a var() reference. */}
<input
type="color"
value={brandColor || "#6366f1"}

View File

@ -545,6 +545,7 @@ export type DiscoveryCard = {
// Stable palette used to auto-assign an accent colour to a skill when the
// backend has not stored an explicit one. Colour is derived from the skill key
// so the same skill always lands on the same hue.
// token-extraction: allowlisted — skill.color is persisted/compared JS data (SkillCreateDraft), not just a rendered value; a var() string would corrupt it.
const DISCOVERY_ACCENTS = [
"#6366f1", "#0ea5e9", "#10b981", "#f59e0b", "#ef4444",
"#8b5cf6", "#ec4899", "#14b8a6", "#f97316", "#22c55e",

View File

@ -839,7 +839,7 @@ export function Costs() {
<p className="text-sm text-destructive">{(budgetError as Error).message}</p>
) : (
<>
<Card className="border-border/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]">
<Card className="border-border/70 bg-(image:--gradient-extract-2)">
<CardHeader className="px-5 pt-5 pb-3">
<CardTitle className="text-base">Budget control plane</CardTitle>
<CardDescription>

View File

@ -219,7 +219,7 @@ export function Dashboard() {
{data && (
<>
{data.budgets.activeIncidents > 0 ? (
<div className="flex items-start justify-between gap-3 rounded-xl border border-red-500/20 bg-[linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))] px-4 py-3">
<div className="flex items-start justify-between gap-3 rounded-xl border border-red-500/20 bg-(image:--gradient-extract-1) px-4 py-3">
<div className="flex items-start gap-2.5">
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-300" />
<div>

View File

@ -192,6 +192,7 @@ function InviteSummaryPanel({
}) {
return (
<>
{/* token-extraction: allowlisted — brandColor feeds CompanyPatternIcon's hexToHue() color math via a canvas fill; demo/showcase-only prop, not a rendered CSS value. */}
<div className="flex items-start gap-4">
<CompanyPatternIcon
companyName="Acme Robotics"
@ -507,7 +508,7 @@ function AuthScreenPreview({ mode, error }: { mode: "sign_in" | "sign_up"; error
</div>
</div>
</div>
<div className="hidden min-h-[420px] items-center justify-center bg-[radial-gradient(circle_at_top,rgba(8,145,178,0.18),transparent_48%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,1))] px-8 py-10 md:flex">
<div className="hidden min-h-[420px] items-center justify-center bg-(image:--gradient-extract-8) px-8 py-10 md:flex">
<div className="max-w-sm space-y-4 text-zinc-200">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-400/30 bg-cyan-500/[0.08] px-3 py-1 text-[10px] uppercase tracking-[0.22em] text-cyan-200">
Auth preview
@ -697,7 +698,7 @@ function CompanyInvitesPreview() {
export function InviteUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-[linear-gradient(135deg,rgba(8,145,178,0.10),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.10),transparent_44%),var(--background)] shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-(image:--gradient-extract-9) shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="p-6 sm:p-7">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-cyan-700 dark:text-cyan-300">
@ -750,7 +751,7 @@ export function InviteUxLab() {
eyebrow="Top-level states"
title="Landing state coverage"
description="Small cards for the fast-return invite states that do not render the full split-screen layout."
accentClassName="bg-[linear-gradient(180deg,rgba(59,130,246,0.05),transparent_30%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-21)"
>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatusCard
@ -804,7 +805,7 @@ export function InviteUxLab() {
eyebrow="Invite landing"
title="Split-screen invite flows"
description="These frames mirror the production invite surface closely enough to review spacing, hierarchy, and control states while keeping data fixture-driven."
accentClassName="bg-[linear-gradient(180deg,rgba(234,179,8,0.06),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-22)"
>
<div className="space-y-5">
<InviteLandingShell
@ -881,7 +882,7 @@ export function InviteUxLab() {
eyebrow="Result states"
title="Approval and completion screens"
description="These are the post-submit states returned from invite acceptance, including optional claim and onboarding metadata."
accentClassName="bg-[linear-gradient(180deg,rgba(16,185,129,0.06),transparent_30%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-23)"
>
<div className="grid gap-5 xl:grid-cols-3">
<InviteResultPreview
@ -906,7 +907,7 @@ export function InviteUxLab() {
eyebrow="Standalone auth"
title="Auth page states"
description="The general `/auth` page uses a different composition from invite landing. These previews keep both sign-in and sign-up variants visible."
accentClassName="bg-[linear-gradient(180deg,rgba(168,85,247,0.06),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-10)"
>
<div className="space-y-5">
<AuthScreenPreview mode="sign_in" error="Invalid email or password" />
@ -918,7 +919,7 @@ export function InviteUxLab() {
eyebrow="Company settings"
title="Company invite management"
description="This section captures the board-side invite creation flow, copied-link state, audit table, and the edge states that are otherwise tedious to stage."
accentClassName="bg-[linear-gradient(180deg,rgba(244,114,182,0.06),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-24)"
>
<CompanyInvitesPreview />
</LabSection>

View File

@ -136,7 +136,7 @@ export function IssueChatUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-[linear-gradient(135deg,rgba(8,145,178,0.10),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.10),transparent_44%),var(--background)] shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-(image:--gradient-extract-9) shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="p-6 sm:p-7">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-cyan-700 dark:text-cyan-300">
@ -200,7 +200,7 @@ export function IssueChatUxLab() {
eyebrow="Animation demo"
title="Rotating reasoning text"
description="Isolated ticker that cycles sample reasoning lines on a timer. The outgoing line slides up and fades out while the incoming line slides up from below. Runs in a loop so you can tune timing and easing without needing a live stream."
accentClassName="bg-[linear-gradient(180deg,rgba(168,85,247,0.06),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-10)"
>
<div className="space-y-4">
<div className="rounded-xl border border-border/60 bg-accent/10 p-4">
@ -223,7 +223,7 @@ export function IssueChatUxLab() {
eyebrow="Status tokens"
title="Working / Worked header verb"
description='The "Working" token uses the shimmer-text gradient sweep to signal an active run. Once the run completes it becomes the static "Worked" token.'
accentClassName="bg-[linear-gradient(180deg,rgba(16,185,129,0.06),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-11)"
>
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-xl border border-border/60 bg-accent/10 p-4">
@ -260,7 +260,7 @@ export function IssueChatUxLab() {
eyebrow="Primary preview"
title="Live execution thread"
description="Shows the fully active state: timeline events, historical run marker, a running assistant reply with reasoning and tools, and a queued follow-up from the user."
accentClassName="bg-[linear-gradient(180deg,rgba(6,182,212,0.05),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-12)"
>
<IssueChatThread
comments={issueChatUxLiveComments}
@ -291,7 +291,7 @@ export function IssueChatUxLab() {
eyebrow="Submitting state"
title="Pending message bubble"
description='When a user sends a message, the bubble briefly shows a "Sending..." label at reduced opacity until the server confirms receipt. This preview renders that transient state.'
accentClassName="bg-[linear-gradient(180deg,rgba(59,130,246,0.06),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-13)"
>
<IssueChatThread
comments={issueChatUxSubmittingComments}
@ -312,7 +312,7 @@ export function IssueChatUxLab() {
eyebrow="Settled review"
title="Durable comments and feedback"
description="Shows the post-run state: assistant comment feedback controls, historical run context, and timeline reassignment without any active stream."
accentClassName="bg-[linear-gradient(180deg,rgba(168,85,247,0.05),transparent_26%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-14)"
>
<IssueChatThread
comments={issueChatUxReviewComments}
@ -336,7 +336,7 @@ export function IssueChatUxLab() {
eyebrow="Empty thread"
title="Empty state and disabled composer"
description="Keeps the message area visible even when there is no thread yet, and replaces the composer with an explicit warning when replies are blocked."
accentClassName="bg-[linear-gradient(180deg,rgba(245,158,11,0.08),transparent_26%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-15)"
>
<IssueChatThread
comments={[]}

View File

@ -159,14 +159,14 @@ function touchCenter(a: React.Touch, b: React.Touch, container: HTMLDivElement):
import { getAdapterLabel } from "../adapters/adapter-display-registry";
const statusDotColor: Record<string, string> = {
running: "#22d3ee",
active: "#4ade80",
paused: "#facc15",
idle: "#facc15",
error: "#f87171",
terminated: "#a3a3a3",
running: "var(--hex-22d3ee)",
active: "var(--hex-4ade80)",
paused: "var(--hex-facc15)",
idle: "var(--hex-facc15)",
error: "var(--hex-f87171)",
terminated: "var(--hex-a3a3a3)",
};
const defaultDotColor = "#a3a3a3";
const defaultDotColor = "var(--hex-a3a3a3)";
// ── Main component ──────────────────────────────────────────────────────

View File

@ -2956,7 +2956,7 @@ export function PipelineSettings() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: selectedAutomationProject.color ?? "#6366f1" }}
style={{ backgroundColor: selectedAutomationProject.color ?? "var(--hex-6366f1)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -2971,7 +2971,7 @@ export function PipelineSettings() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#6366f1" }}
style={{ backgroundColor: project?.color ?? "var(--hex-6366f1)" }}
/>
<span className="truncate">{option.label}</span>
</>

View File

@ -159,7 +159,7 @@ export function ProfileSettings() {
<section className="space-y-8">
<div className="relative overflow-hidden rounded-[28px] border border-border/70 bg-card shadow-sm">
<div className="absolute inset-x-0 top-0 h-32 bg-[linear-gradient(135deg,hsl(var(--primary))_0%,hsl(var(--accent))_58%,color-mix(in_oklab,hsl(var(--background))_76%,white_24%)_100%)]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.22),transparent_34%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.08),transparent_36%)]" />
<div className="absolute inset-0 bg-(image:--gradient-extract-7)" />
<div className="relative p-6 pt-10">
<div className="flex flex-wrap items-end gap-5 rounded-[24px] border border-border/70 bg-background/92 p-5 shadow-[0_18px_44px_-28px_rgba(0,0,0,0.45)] backdrop-blur-sm">
<div className="space-y-3">

View File

@ -786,7 +786,7 @@ export function ProjectDetail() {
) : null}
{project.managedByPlugin ? (
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1 text-[11px] font-medium text-muted-foreground">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: project.color ?? "#6366f1" }} />
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: project.color ?? "var(--hex-6366f1)" }} />
Managed by {project.managedByPlugin.pluginDisplayName}
</div>
) : null}

View File

@ -748,7 +748,7 @@ export function Routines() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
style={{ backgroundColor: currentProject.color ?? "var(--hex-64748b)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -763,7 +763,7 @@ export function Routines() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#64748b" }}
style={{ backgroundColor: project?.color ?? "var(--hex-64748b)" }}
/>
<span className="truncate">{option.label}</span>
</>

View File

@ -75,7 +75,7 @@ function RunDetailPreview({
Transcript ({runTranscriptFixtureEntries.length})
</div>
</div>
<div className="max-h-[720px] overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(8,145,178,0.08),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(245,158,11,0.10),transparent_28%)] p-5">
<div className="max-h-[720px] overflow-y-auto bg-(image:--gradient-extract-5) p-5">
<RunTranscriptView
entries={runTranscriptFixtureEntries}
mode={mode}
@ -200,7 +200,7 @@ export function RunTranscriptUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-2xl border border-border/70 bg-[linear-gradient(135deg,rgba(8,145,178,0.08),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.08),transparent_40%),var(--background)] shadow-[0_28px_70px_rgba(15,23,42,0.10)]">
<div className="overflow-hidden rounded-2xl border border-border/70 bg-(image:--gradient-extract-6) shadow-[0_28px_70px_rgba(15,23,42,0.10)]">
<div className="grid gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
<aside className="border-b border-border/60 bg-background/75 p-5 lg:border-b-0 lg:border-r">
<div className="mb-5">

View File

@ -137,7 +137,7 @@ export function SystemNoticeUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-[linear-gradient(135deg,rgba(245,158,11,0.10),transparent_28%),linear-gradient(180deg,rgba(8,145,178,0.08),transparent_44%),var(--background)] shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-(image:--gradient-extract-16) shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="p-6 sm:p-7">
<div className="inline-flex items-center gap-2 rounded-full border border-amber-500/25 bg-amber-500/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-300">
@ -190,7 +190,7 @@ export function SystemNoticeUxLab() {
eyebrow="Tone matrix"
title="Three tones, two states"
description="Each tone pairs a unique icon and tone label so the notice is recognizable without color. Collapsed is the default; the Details affordance reveals operational metadata only when reviewers ask for it."
accentClassName="bg-[linear-gradient(180deg,rgba(245,158,11,0.05),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-17)"
>
<div className="space-y-5">
<FixtureFrame caption={warningCollapsed.caption}>
@ -222,7 +222,7 @@ export function SystemNoticeUxLab() {
eyebrow="Hierarchy in thread"
title="Distinct from user and agent comments"
description="Side-by-side with adjacent comment types so reviewers can confirm the system row reads as a system row — full width, no avatar gutter, no chat bubble — while user and agent comments keep their existing rounded bubbles."
accentClassName="bg-[linear-gradient(180deg,rgba(8,145,178,0.05),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-18)"
>
<div className="space-y-4 rounded-2xl border border-border/70 bg-background/70 p-4">
<MockUserBubble
@ -286,7 +286,7 @@ export function SystemNoticeUxLab() {
eyebrow="Before"
title="Today's nested treatment"
description="The same content rendered through the existing user-bubble + warning-callout path. Two containers, same gray background as user comments, and the warning icon is forced inside a chat row."
accentClassName="bg-[linear-gradient(180deg,rgba(244,63,94,0.05),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-19)"
>
<div className="space-y-3 rounded-2xl border border-border/70 bg-background/70 p-4">
<div className="flex items-start gap-2.5">
@ -328,7 +328,7 @@ export function SystemNoticeUxLab() {
eyebrow="After"
title="System notice replacement"
description="One container, system-authored label, hidden details. The chat surface keeps user and agent bubbles unchanged."
accentClassName="bg-[linear-gradient(180deg,rgba(16,185,129,0.05),transparent_28%),var(--background)]"
accentClassName="bg-(image:--gradient-extract-20)"
>
<div className="space-y-3 rounded-2xl border border-border/70 bg-background/70 p-4">
<SystemNotice {...dangerCollapsed} />