diff --git a/TOKEN-AUDIT.md b/TOKEN-AUDIT.md index 5d0051d057..ecf44b9de1 100644 --- a/TOKEN-AUDIT.md +++ b/TOKEN-AUDIT.md @@ -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. `` 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` — `` 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?). diff --git a/scripts/codemod-extract-colors.mjs b/scripts/codemod-extract-colors.mjs new file mode 100644 index 0000000000..49d595c3ed --- /dev/null +++ b/scripts/codemod-extract-colors.mjs @@ -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: ' 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: '
\n \n ` --${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 — 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(); diff --git a/ui/src/components/AccountingModelCard.tsx b/ui/src/components/AccountingModelCard.tsx index 987040615c..73ee19c7e7 100644 --- a/ui/src/components/AccountingModelCard.tsx +++ b/ui/src/components/AccountingModelCard.tsx @@ -28,7 +28,7 @@ const SURFACES = [ export function AccountingModelCard() { return ( -
+
Accounting model diff --git a/ui/src/components/ActivityCharts.tsx b/ui/src/components/ActivityCharts.tsx index bd3dd22adb..0dbac4a2cc 100644 --- a/ui/src/components/ActivityCharts.tsx +++ b/ui/src/components/ActivityCharts.tsx @@ -122,10 +122,10 @@ export function RunActivityChart(props: RunChartProps) { } const priorityColors: Record = { - 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 = { - 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 = { @@ -225,7 +225,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created {total > 0 ? (
{statusOrder.map(s => (entry[s] ?? 0) > 0 ? ( -
+
) : null)}
) : ( @@ -236,7 +236,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created })}
- ({ color: statusColors[s] ?? "#6b7280", label: statusLabels[s] ?? s }))} /> + ({ color: statusColors[s] ?? "var(--hex-6b7280)", label: statusLabels[s] ?? s }))} />
); } @@ -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 (
0 ? Math.round(rate * 100) : 0}% (${entry.succeeded}/${entry.total})`}> {entry.total > 0 ? ( diff --git a/ui/src/components/ActivityFeed.tsx b/ui/src/components/ActivityFeed.tsx index 2df82a27d5..4bcdd777cf 100644 --- a/ui/src/components/ActivityFeed.tsx +++ b/ui/src/components/ActivityFeed.tsx @@ -283,9 +283,9 @@ function CollapsedFeedGroup({ : } - {actorName} - made {group.events.length} updates to - {entityName ?? group.entityId} + {actorName} + made {group.events.length} updates to + {entityName ?? group.entityId} {timeAgo(group.latestEvent.createdAt)} diff --git a/ui/src/components/BudgetIncidentCard.tsx b/ui/src/components/BudgetIncidentCard.tsx index 49836819cf..90de791764 100644 --- a/ui/src/components/BudgetIncidentCard.tsx +++ b/ui/src/components/BudgetIncidentCard.tsx @@ -43,7 +43,7 @@ export function BudgetIncidentCard({ const stateLabel = incidentStateLabel(incident); return ( - +
diff --git a/ui/src/components/CompanyPatternIcon.tsx b/ui/src/components/CompanyPatternIcon.tsx index 6591f7d13a..c680becb31 100644 --- a/ui/src/components/CompanyPatternIcon.tsx +++ b/ui/src/components/CompanyPatternIcon.tsx @@ -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); diff --git a/ui/src/components/FeedCard.tsx b/ui/src/components/FeedCard.tsx index 09b8471058..7806fe85c6 100644 --- a/ui/src/components/FeedCard.tsx +++ b/ui/src/components/FeedCard.tsx @@ -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 = ( diff --git a/ui/src/components/FileViewerSheet.tsx b/ui/src/components/FileViewerSheet.tsx index 5706bda1ec..0fb572fb28 100644 --- a/ui/src/components/FileViewerSheet.tsx +++ b/ui/src/components/FileViewerSheet.tsx @@ -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))]", )} > diff --git a/ui/src/components/IssueChatThread.test.tsx b/ui/src/components/IssueChatThread.test.tsx index 2ede85effd..2f8e6e4357 100644 --- a/ui/src/components/IssueChatThread.test.tsx +++ b/ui/src/components/IssueChatThread.test.tsx @@ -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(); diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index 6aa61cf760..efdbd0dc31 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -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", )} diff --git a/ui/src/components/IssueColumns.tsx b/ui/src/components/IssueColumns.tsx index 2a921237e7..f116794fe1 100644 --- a/ui/src/components/IssueColumns.tsx +++ b/ui/src/components/IssueColumns.tsx @@ -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 ( ) : option.kind === "project" && option.projectId ? ( ) : option.kind === "user" ? ( diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 60cbd4cfed..330daf50b3 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -1486,7 +1486,7 @@ export function NewIssueDialog() { <> {option.label} @@ -1501,7 +1501,7 @@ export function NewIssueDialog() { <> {option.label} diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index b68a7de9bb..46fefd5507 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -1719,7 +1719,7 @@ export function OnboardingWizard() { name + mission steps) */}
diff --git a/ui/src/components/RoutineList.tsx b/ui/src/components/RoutineList.tsx index 5c19bf524e..bd19200b80 100644 --- a/ui/src/components/RoutineList.tsx +++ b/ui/src/components/RoutineList.tsx @@ -118,7 +118,7 @@ export function RoutineListRow({ {routine.projectId ? (project?.name ?? "Unknown project") : "No project"} diff --git a/ui/src/components/RoutineRunVariablesDialog.tsx b/ui/src/components/RoutineRunVariablesDialog.tsx index 25449ec732..6bf5dc2f52 100644 --- a/ui/src/components/RoutineRunVariablesDialog.tsx +++ b/ui/src/components/RoutineRunVariablesDialog.tsx @@ -423,7 +423,7 @@ export function RoutineRunVariablesDialog({ <> {option.label} @@ -438,7 +438,7 @@ export function RoutineRunVariablesDialog({ <> {option.label} diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx index 135e1932e7..7b8b65a5ac 100644 --- a/ui/src/components/issue-properties/IssueProperties.tsx +++ b/ui/src/components/issue-properties/IssueProperties.tsx @@ -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({ <> p.id === issue.projectId)?.color ?? "#6366f1" }} + style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "var(--hex-6366f1)" }} /> {projectName(issue.projectId)} @@ -1646,7 +1647,7 @@ export function IssueProperties({ {option.kind === "project" ? ( ) : null} {option.name} diff --git a/ui/src/components/routine-sections/editable-sections.tsx b/ui/src/components/routine-sections/editable-sections.tsx index 36fedc6688..ba9b22b005 100644 --- a/ui/src/components/routine-sections/editable-sections.tsx +++ b/ui/src/components/routine-sections/editable-sections.tsx @@ -190,7 +190,7 @@ export function OverviewSection({ <> {option.label} @@ -205,7 +205,7 @@ export function OverviewSection({ <> {option.label} diff --git a/ui/src/index.css b/ui/src/index.css index 0e5e2cf6ff..5fe28aa87e 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -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 — 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. */ +} diff --git a/ui/src/pages/CompanyEnvironments.tsx b/ui/src/pages/CompanyEnvironments.tsx index c19881491b..896bd02029 100644 --- a/ui/src/pages/CompanyEnvironments.tsx +++ b/ui/src/pages/CompanyEnvironments.tsx @@ -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", diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index 5fce4d0587..fe504892d3 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -267,6 +267,7 @@ export function CompanySettings() { hint="Sets the hue for the company icon. Leave empty for auto-generated color." >
+ {/* token-extraction: allowlisted — value must be a real hex string, not a var() reference. */} {(budgetError as Error).message}

) : ( <> - + Budget control plane diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx index 63d5b20e0c..4c78a1e32c 100644 --- a/ui/src/pages/Dashboard.tsx +++ b/ui/src/pages/Dashboard.tsx @@ -219,7 +219,7 @@ export function Dashboard() { {data && ( <> {data.budgets.activeIncidents > 0 ? ( -
+
diff --git a/ui/src/pages/InviteUxLab.tsx b/ui/src/pages/InviteUxLab.tsx index 38bcd24c2f..db3c0313fa 100644 --- a/ui/src/pages/InviteUxLab.tsx +++ b/ui/src/pages/InviteUxLab.tsx @@ -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. */}
-
+
Auth preview @@ -697,7 +698,7 @@ function CompanyInvitesPreview() { export function InviteUxLab() { return (
-
+
@@ -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)" >
@@ -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)" > diff --git a/ui/src/pages/IssueChatUxLab.tsx b/ui/src/pages/IssueChatUxLab.tsx index 1027ff4c35..6e8047bf8b 100644 --- a/ui/src/pages/IssueChatUxLab.tsx +++ b/ui/src/pages/IssueChatUxLab.tsx @@ -136,7 +136,7 @@ export function IssueChatUxLab() { return (
-
+
@@ -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)" >
@@ -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)" >
@@ -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)" > = { - 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 ────────────────────────────────────────────────────── diff --git a/ui/src/pages/PipelineSettings.tsx b/ui/src/pages/PipelineSettings.tsx index aa7bc2a28a..bee7f1d35a 100644 --- a/ui/src/pages/PipelineSettings.tsx +++ b/ui/src/pages/PipelineSettings.tsx @@ -2956,7 +2956,7 @@ export function PipelineSettings() { <> {option.label} @@ -2971,7 +2971,7 @@ export function PipelineSettings() { <> {option.label} diff --git a/ui/src/pages/ProfileSettings.tsx b/ui/src/pages/ProfileSettings.tsx index 674ff78029..b63a30afbe 100644 --- a/ui/src/pages/ProfileSettings.tsx +++ b/ui/src/pages/ProfileSettings.tsx @@ -159,7 +159,7 @@ export function ProfileSettings() {
-
+
diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index f0e7990213..c31695355b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -786,7 +786,7 @@ export function ProjectDetail() { ) : null} {project.managedByPlugin ? (
- + Managed by {project.managedByPlugin.pluginDisplayName}
) : null} diff --git a/ui/src/pages/Routines.tsx b/ui/src/pages/Routines.tsx index edac74328e..9020973ce8 100644 --- a/ui/src/pages/Routines.tsx +++ b/ui/src/pages/Routines.tsx @@ -748,7 +748,7 @@ export function Routines() { <> {option.label} @@ -763,7 +763,7 @@ export function Routines() { <> {option.label} diff --git a/ui/src/pages/RunTranscriptUxLab.tsx b/ui/src/pages/RunTranscriptUxLab.tsx index 80759cf9da..c4f3af405d 100644 --- a/ui/src/pages/RunTranscriptUxLab.tsx +++ b/ui/src/pages/RunTranscriptUxLab.tsx @@ -75,7 +75,7 @@ function RunDetailPreview({ Transcript ({runTranscriptFixtureEntries.length})
-
+
-
+