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: '