#!/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();