From 4b3f087d7f7d3e3db1e60a3e911b3aecdfa1ff2b Mon Sep 17 00:00:00 2001 From: scotttong Date: Mon, 6 Jul 2026 14:29:51 -0700 Subject: [PATCH] refactor(ui): extract font-size and letter-spacing to tokens (Phase 2 batch 2/4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces hardcoded TYPE values in ui/src/components/** and ui/src/pages/** with references to new verbatim CSS custom-property tokens in ui/src/index.css, via a new committed codemod (scripts/codemod-extract-type.mjs). - 932 sites rewritten across 143 files: - 728 arbitrary font-size Tailwind classes (text-[Npx]) - 202 arbitrary letter-spacing classes (tracking-[N em]) - 2 inline-style fontSize string literals - 0 leading-[...] sites found (none existed) - 17 new tokens minted, no normalizing: 8 --fs-* (9/10/11/12/13/14/15px + 0.7rem, kept in its own unit) and 9 --ls-* (0.08-0.24em, 9 distinct values) — the micro type-size and letter-spacing clusters TOKEN-AUDIT.md flagged are now materialized as tokens, still un-collapsed pending a human scale decision. - 2 sites allowlisted (both already-documented functional/third-party fontSize forms: CompanyEnvironments.tsx xterm.js config, CompanySkills.tsx computed Math.round() value) — no new allowlist entries needed since neither is a class-string or literal-fontSize site the codemod targets. - Verified: rg gates clean, pnpm build-storybook exit 0, Storybook visual suite 510/510 passed (first attempt), pnpm typecheck exit 0, codemod re-run confirmed idempotent (0 sites on second pass). Co-Authored-By: Claude Fable 5 --- TOKEN-AUDIT.md | 37 +++ scripts/codemod-extract-type.mjs | 239 ++++++++++++++++++ ui/src/components/AccountingModelCard.tsx | 2 +- ui/src/components/ActiveAgentsPanel.tsx | 8 +- ui/src/components/ActivityCharts.tsx | 6 +- ui/src/components/ActivityFeed.tsx | 2 +- ui/src/components/AgentBubbleActionRow.tsx | 2 +- ui/src/components/AgentConfigForm.tsx | 24 +- ui/src/components/ApprovalCard.tsx | 2 +- ui/src/components/ApprovalPayload.tsx | 14 +- ui/src/components/ArtifactsPanel.tsx | 16 +- ui/src/components/AsciiArtAnimation.tsx | 2 +- ui/src/components/BlockedReasonChip.tsx | 2 +- ui/src/components/BudgetIncidentCard.tsx | 4 +- ui/src/components/BudgetPolicyCard.tsx | 18 +- ui/src/components/ChatComposer.tsx | 2 +- ui/src/components/ClaudeSubscriptionPanel.tsx | 4 +- ui/src/components/CodexSubscriptionPanel.tsx | 8 +- ui/src/components/CommandPalette.tsx | 4 +- ui/src/components/CommentThread.tsx | 22 +- ui/src/components/CompanySettingsSidebar.tsx | 4 +- ui/src/components/DevRestartBanner.tsx | 4 +- ui/src/components/DocumentAnnotationPanel.tsx | 10 +- ui/src/components/DocumentDiffModal.tsx | 8 +- ui/src/components/DocumentFrameHeader.tsx | 10 +- .../components/ExecutionParticipantPicker.tsx | 2 +- .../ExecutionWorkspaceCloseDialog.tsx | 12 +- ui/src/components/ExternalObjectPill.tsx | 2 +- .../ExternalObjectStatusSummary.tsx | 2 +- ui/src/components/FileTree.tsx | 2 +- ui/src/components/FinanceBillerCard.tsx | 8 +- ui/src/components/FinanceTimelineCard.tsx | 2 +- ui/src/components/InlineEditor.tsx | 2 +- .../components/IssueAssignedBacklogNotice.tsx | 4 +- ui/src/components/IssueAttachmentsSection.tsx | 6 +- ui/src/components/IssueBlockedNotice.tsx | 6 +- ui/src/components/IssueChatThread.tsx | 78 +++--- ui/src/components/IssueColumns.tsx | 12 +- .../components/IssueContinuationHandoff.tsx | 4 +- .../components/IssueDocumentAnnotations.tsx | 2 +- ui/src/components/IssueDocumentsSection.tsx | 12 +- ui/src/components/IssueFiltersPopover.tsx | 4 +- .../IssuePlanDecompositionsSection.tsx | 12 +- ui/src/components/IssueRecoveryActionCard.tsx | 26 +- ui/src/components/IssueRelatedWorkPanel.tsx | 2 +- ui/src/components/IssueRow.tsx | 4 +- ui/src/components/IssueRunLedger.tsx | 30 +-- .../components/IssueThreadInteractionCard.tsx | 28 +- ui/src/components/IssueWorkspaceCard.tsx | 8 +- ui/src/components/IssuesList.tsx | 8 +- ui/src/components/JsonSchemaForm.tsx | 8 +- ui/src/components/KanbanBoard.tsx | 12 +- .../KeyboardShortcutsCheatsheet.tsx | 2 +- ui/src/components/LiveRunWidget.tsx | 6 +- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/components/MarkdownEditor.tsx | 12 +- ui/src/components/MobileBottomNav.tsx | 6 +- ui/src/components/NewAgentDialog.tsx | 4 +- ui/src/components/NewIssueDialog.tsx | 24 +- ui/src/components/OnboardingChat.tsx | 10 +- ui/src/components/OnboardingWizard.tsx | 36 +-- .../components/PipelineItemBodyDocument.tsx | 6 +- .../components/PipelineStageHistoryPanel.tsx | 2 +- ui/src/components/ProductivityReviewBadge.tsx | 2 +- ui/src/components/ProjectProperties.tsx | 32 +-- .../ProjectWorkspaceSummaryCard.tsx | 10 +- .../components/ProjectWorkspacesContent.tsx | 2 +- ui/src/components/ProviderQuotaCard.tsx | 4 +- .../ReusableExecutionWorkspaceSelect.tsx | 2 +- ui/src/components/RoutineHistoryTab.tsx | 30 +-- ui/src/components/RoutineSaveBar.tsx | 2 +- ui/src/components/RoutineSubSidebar.tsx | 4 +- ui/src/components/RoutineVariablesEditor.tsx | 4 +- ui/src/components/SecretBindingPicker.tsx | 10 +- ui/src/components/Sidebar.tsx | 6 +- ui/src/components/SidebarAccountMenu.tsx | 4 +- ui/src/components/SidebarAgents.tsx | 6 +- ui/src/components/SidebarCompanyMenu.tsx | 8 +- ui/src/components/SidebarNavItem.tsx | 6 +- ui/src/components/SidebarProjects.tsx | 2 +- ui/src/components/SidebarSection.tsx | 2 +- ui/src/components/SidebarServerInfo.tsx | 2 +- ui/src/components/SidebarStarredProjects.tsx | 2 +- ui/src/components/SourceResolvedFoldBadge.tsx | 2 +- .../components/SourceResolvedFoldCallout.tsx | 14 +- ui/src/components/SourceTrustBadge.tsx | 2 +- ui/src/components/SystemNotice.tsx | 16 +- ui/src/components/WorkspaceFileBrowser.tsx | 4 +- .../components/WorkspaceRuntimeControls.tsx | 4 +- ui/src/components/WorktreeBanner.tsx | 4 +- ui/src/components/artifacts/ArtifactCard.tsx | 4 +- .../artifacts/ArtifactGroupCard.tsx | 6 +- .../CreateSecretPopover.tsx | 12 +- .../environment-variables-editor/Row.tsx | 18 +- .../SecretPicker.tsx | 6 +- .../environment-variables-editor/index.tsx | 14 +- .../issue-output/IssueOutputSection.tsx | 4 +- .../issue-output/OutputFileTile.tsx | 2 +- .../issue-output/OutputPrimaryCard.tsx | 6 +- ui/src/components/issue-output/OutputRow.tsx | 2 +- .../routine-sections/editable-sections.tsx | 12 +- ui/src/components/search/MatchSourceChip.tsx | 2 +- .../components/timeline/WorkTimelineChart.tsx | 2 +- .../transcript/RunTranscriptView.tsx | 68 ++--- ui/src/components/ui/avatar.tsx | 2 +- ui/src/index.css | 34 +++ ui/src/pages/AdapterManager.tsx | 2 +- ui/src/pages/AgentDetail.tsx | 36 +-- ui/src/pages/Agents.tsx | 6 +- ui/src/pages/ApprovalDetail.tsx | 2 +- ui/src/pages/Approvals.tsx | 2 +- ui/src/pages/Companies.tsx | 2 +- ui/src/pages/CompanyEnvironments.tsx | 4 +- ui/src/pages/CompanyImport.tsx | 8 +- ui/src/pages/CompanySkills.tsx | 80 +++--- ui/src/pages/Costs.tsx | 4 +- ui/src/pages/DesignGuide.tsx | 16 +- ui/src/pages/ExecutionWorkspaceDetail.tsx | 4 +- ui/src/pages/Inbox.tsx | 4 +- ui/src/pages/InstanceSettings.tsx | 2 +- ui/src/pages/InviteLanding.tsx | 12 +- ui/src/pages/InviteUxLab.tsx | 20 +- ui/src/pages/IssueChatLongThreadPerf.tsx | 4 +- ui/src/pages/IssueChatUxLab.tsx | 26 +- ui/src/pages/IssueDetail.tsx | 24 +- ui/src/pages/OrgChart.tsx | 8 +- ui/src/pages/PipelineSettings.tsx | 4 +- ui/src/pages/Pipelines.tsx | 54 ++-- ui/src/pages/PluginSettings.tsx | 14 +- ui/src/pages/ProjectDetail.tsx | 4 +- ui/src/pages/ProjectWorkspaceDetail.tsx | 10 +- ui/src/pages/ResponsibleUserDenialUxLab.tsx | 14 +- ui/src/pages/RoutineDetail.tsx | 2 +- ui/src/pages/Routines.tsx | 6 +- ui/src/pages/RunTranscriptUxLab.tsx | 22 +- ui/src/pages/Search.tsx | 12 +- ui/src/pages/Secrets.tsx | 62 ++--- ui/src/pages/SystemNoticeUxLab.tsx | 50 ++-- ui/src/pages/TeamCatalog.tsx | 60 ++--- ui/src/pages/UserProfile.tsx | 12 +- .../pages/secrets/ImportFromVaultDialog.tsx | 12 +- .../secrets/MissingUserSecretsBanner.tsx | 4 +- ui/src/pages/secrets/MyUserSecretsTab.tsx | 8 +- .../pages/secrets/SetMyUserSecretDialog.tsx | 4 +- .../secrets/UserSecretDefinitionsTab.tsx | 8 +- .../secrets/user-secret-presentation.tsx | 2 +- 146 files changed, 1091 insertions(+), 781 deletions(-) create mode 100644 scripts/codemod-extract-type.mjs diff --git a/TOKEN-AUDIT.md b/TOKEN-AUDIT.md index ecf44b9de1..ad5dfca5a8 100644 --- a/TOKEN-AUDIT.md +++ b/TOKEN-AUDIT.md @@ -279,3 +279,40 @@ Codemod: `scripts/codemod-extract-colors.mjs` (table-driven, idempotent — see **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?). + +--- + +## Phase 2 extraction log — Batch 2 (type) + +Codemod: `scripts/codemod-extract-type.mjs`. Unlike Batch 1's hand-audited site table (needed to avoid hex-like false positives such as issue references), this batch's patterns — `text-[Npx]`/`text-[N.Nrem]` Tailwind font-size, `tracking-[N em]` letter-spacing, `leading-[...]` line-height, and `fontSize: "Npx"`/`fontSize: "N.Nrem"` inline-style string literals — are unambiguous, so the codemod does a blanket regex sweep scoped to `ui/src/components/**` and `ui/src/pages/**` (including `*.test.tsx` companions; a full scan found **zero** test files containing any of these patterns, so no test file needed a lockstep update this batch). Verified idempotent (second run: 0 sites, 0 files changed). + +**Sites rewritten: 932**, across 143 files. +- Font-size Tailwind class utilities (`text-[Npx]`): 728 sites (matches TOKEN-AUDIT.md section 3.1's count almost exactly — 730 vs. 728, negligible drift from audit-vs-extraction timing). No rem-unit or line-height-suffixed (`text-[N]/[N]`) forms were found in class strings; none existed to convert. +- Letter-spacing (`tracking-[N em]`): 202 sites, matching TOKEN-AUDIT.md section 3.2 exactly (9 distinct values). +- `leading-[...]` line-height brackets: 0 sites found (confirmed via full sweep; TOKEN-AUDIT.md did not call this out as a populated cluster either). +- Inline-style `fontSize` string literals: 2 sites — `components/AsciiArtAnimation.tsx:344` (`fontSize: "11px"`, reused the same `--fs-11` token minted by the class-based sites — no duplicate) and `components/MarkdownBody.tsx:197` (`fontSize: "0.7rem"`, new rem-unit token). + +**Tokens minted: 17 new** (0 reused from Batch 1 — font-size/letter-spacing/line-height had no prior tokens to match against). +- 8 `--fs-*` (font-size): `--fs-9`, `--fs-10`, `--fs-11`, `--fs-12`, `--fs-13`, `--fs-14`, `--fs-15` (px, one per distinct value found) + `--fs-0_7rem` (0.7rem, `MarkdownBody.tsx`'s inline style — kept in rem per DESIGN.md "no normalizing/unit-converting" rule, NOT collapsed into the 11px-ish px cluster even though 0.7rem ≈ 11.2px). +- 9 `--ls-*` (letter-spacing): `--ls-0_08`, `--ls-0_1`, `--ls-0_12`, `--ls-0_14`, `--ls-0_16`, `--ls-0_18`, `--ls-0_2`, `--ls-0_22`, `--ls-0_24` — one per distinct em value, matching TOKEN-AUDIT.md 3.2's 9-value cluster exactly. +- 0 `--lh-*` (line-height) — no sites required one; the token-registration code path exists in the codemod (and was exercised in the Step 0 syntax spike) for forward-compatibility but minted nothing this batch. +- All new tokens live in a second non-`@theme` `:root { ... }` block appended to `ui/src/index.css` immediately after Batch 1's color block, headed `/* ── Extracted verbatim TYPE tokens (Phase 2 Batch 2, design/token-extraction) ── */`, per DESIGN.md (runtime-tunable). **No normalizing performed** — 9/10/11/12/13/14/15px and the 9 distinct tracking values all remain distinct tokens; the human scale-collapse decision (TOKEN-AUDIT.md "Needs human decision" #2/#3, PRIOR-ART's draft `.type-*` scale) is explicitly deferred, per mandate. + +**Tailwind v4 rewrite forms used** (confirmed via mandatory Step-0 syntax spike — scratch story + `pnpm build-storybook` + grep of emitted CSS, then deleted before the real codemod ran): +- `text-[11px]` → `text-(length:--fs-11)` — emits `font-size:var(--fs-11)`. The `length:` hint is REQUIRED; a bare `text-(--fs-11)` would be interpreted as a color utility. +- `tracking-[0.18em]` → `tracking-(--ls-0_18)` — emits `--tw-tracking:var(--ls-0_18);letter-spacing:var(--ls-0_18)`. Unambiguous, no hint needed. +- `leading-[...]` (numeric/unit forms only) → `leading-(--lh-*)` — verified to emit `--tw-leading:var(--lh-*);line-height:var(--lh-*)` in the spike; not exercised on a real site since 0 sites existed. +- All variant/modifier prefixes preserved verbatim by construction (the regex only rewrites the bracket portion): confirmed sites include `sm:text-[11px]` (`components/BlockedReasonChip.tsx`), a compound arbitrary-variant `group-data-[size=xs]/avatar:text-[10px]` (`components/ui/avatar.tsx`), and an `!important`-marked bracket-selector `[&>span:last-child]:!text-[11px]` (`components/ActiveAgentsPanel.tsx`) — all rewrote correctly to `sm:text-(length:--fs-11)`, `group-data-[size=xs]/avatar:text-(length:--fs-10)`, and `[&>span:last-child]:!text-(length:--fs-11)` respectively. + +**Sites allowlisted (2 sites, both already covered by Batch 1's allowlist doc-comment structure for their files, no new inline comments needed since neither is a bracket-literal or class-string site):** +1. `pages/CompanyEnvironments.tsx:422` — `fontSize: 12` inside the xterm.js terminal theme option object (same object Batch 1 allowlisted for its color literals). Numeric, functional third-party config — not a rendered CSS value, not a string literal the codemod's regex targets. +2. `pages/CompanySkills.tsx:580` — `fontSize: Math.round(size * 0.42)` — computed at runtime from a prop; not a static literal, nothing to extract. + +**Verify results:** +- `rg` gates clean in `ui/src/components/**` / `ui/src/pages/**`: zero `text-[Npx]`/`text-[N.Nrem]` arbitrary font-size, zero `tracking-[N em]`, zero numeric `leading-[...]`, zero raw `fontSize: "..."` string literals remain (the only two `fontSize:` string-literal grep hits left are the already-converted `fontSize: "var(--fs-11)"` / `fontSize: "var(--fs-0_7rem)"` sites). +- `pnpm build-storybook` exit 0. +- Storybook visual snapshot suite: **510/510 passed, 0 failed**, first attempt, no retries needed (`npx playwright test --config tests/storybook-visual/playwright.config.ts --reporter=line`). +- `pnpm typecheck` exit 0. +- Codemod re-run confirmed idempotent: second invocation reports 0 sites rewritten, 0 files changed, token block already present. + +**Needs human decision:** none new from this batch beyond the already-logged #2 (micro type-size cluster) and #3 (letter-spacing cluster) in section 8 above — this batch's 17 minted tokens are exactly the verbatim inventory those two items describe, now materialized as CSS custom properties ready for a human to collapse into a real scale. diff --git a/scripts/codemod-extract-type.mjs b/scripts/codemod-extract-type.mjs new file mode 100644 index 0000000000..ddd69980e5 --- /dev/null +++ b/scripts/codemod-extract-type.mjs @@ -0,0 +1,239 @@ +#!/usr/bin/env node +/** + * codemod-extract-type.mjs + * + * Phase 2 (extraction), Batch 2/4 of the design-token audit + * (branch design/token-extraction). Replaces hardcoded TYPE values — + * arbitrary Tailwind font-size (`text-[11px]`), letter-spacing + * (`tracking-[0.18em]`), line-height (`leading-[...]`), and raw inline + * `fontSize` style literals — 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`. + * + * Unlike Batch 1's color codemod (which used a hand-audited site table to + * avoid false-positiving on non-color hex-like strings such as issue + * references), this batch's patterns are unambiguous: `text-[Npx]`, + * `text-[N.Nrem]`, `tracking-[N em]`, and `leading-[...]` inside Tailwind + * class strings, and `fontSize: "Npx"` / `fontSize: "N.Nrem"` inline-style + * string literals, cannot mean anything other than a type-size/spacing + * value. A blanket regex sweep is therefore safe and is used here, scoped + * to `ui/src/components/**` and `ui/src/pages/**` only. Numeric or + * computed `fontSize` forms (e.g. `fontSize: 12`, `fontSize: Math.round(...)`) + * are functional (third-party config objects / runtime-computed values) + * and are left untouched — see ALLOWLIST_NOTES below. + * + * Token naming (verbatim value, no normalizing): + * --fs- font-size, px values, e.g. --fs-11: 11px; + * --fs-0_rem font-size, rem values, e.g. --fs-0_7rem: 0.7rem; + * --ls-0_ letter-spacing, em values, e.g. --ls-0_18: 0.18em; + * --lh- line-height (px or unitless — none found this batch) + * + * Tailwind v4 paren-shorthand rewrite forms used: + * text-[Npx] -> text-(length:--fs-N) (length hint REQUIRED — + * bare text-(--x) means color) + * tracking-[N em] -> tracking-(--ls-0_N) (unambiguous, no hint) + * leading-[...] -> leading-(--lh-N) (unambiguous, no hint) + * All variant/modifier prefixes (`sm:`, `dark:`, `group-hover:`, + * `[&>x]:`, trailing `!important` marker, etc.) are preserved verbatim — + * the regex only rewrites the bracket portion itself. + * + * Idempotent: the FIND regex only matches the ORIGINAL bracket-literal + * form (`text-[11px]` etc.); once rewritten to `text-(length:--fs-11)` the + * pattern no longer matches, so re-running is a no-op. The inline-style + * FIND is likewise the literal `fontSize: "11px"` string form. + * + * Usage: node scripts/codemod-extract-type.mjs [--check] + * --check Report what WOULD change without writing files (dry run). + */ + +import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; +import { resolve, dirname, join, relative } 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 SCAN_DIRS = ["components", "pages"]; + +const DRY_RUN = process.argv.includes("--check"); + +// ── Helpers ──────────────────────────────────────────────────────────── +function walk(dir, out) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) walk(p, out); + else if (/\.(tsx?|jsx?)$/.test(entry.name)) out.push(p); + } +} + +function tokenSuffixForPx(value) { + // "11" -> "11", "0.65" -> "0_65" + return value.replace(".", "_"); +} + +function tokenSuffixForEm(value) { + // "0.18" -> "0_18" + return value.replace(".", "_"); +} + +// ── Token registries (populated as sites are discovered) ─────────────── +// Map of token name (without --) -> { value, comment, kind } +const fsTokens = new Map(); // font-size +const lsTokens = new Map(); // letter-spacing +const lhTokens = new Map(); // line-height + +function registerFsToken(rawValue, unit, sourceNote) { + const name = unit === "px" ? `fs-${tokenSuffixForPx(rawValue)}` : `fs-${tokenSuffixForPx(rawValue)}rem`; + if (!fsTokens.has(name)) { + fsTokens.set(name, { value: `${rawValue}${unit}`, comment: sourceNote }); + } + return name; +} + +function registerLsToken(rawValue, sourceNote) { + const name = `ls-${tokenSuffixForEm(rawValue)}`; + if (!lsTokens.has(name)) { + lsTokens.set(name, { value: `${rawValue}em`, comment: sourceNote }); + } + return name; +} + +function registerLhToken(rawValue, sourceNote) { + // rawValue includes unit already stripped by caller; store as given + const safeName = rawValue.replace(/[^a-zA-Z0-9]/g, "_"); + const name = `lh-${safeName}`; + if (!lhTokens.has(name)) { + lhTokens.set(name, { value: rawValue, comment: sourceNote }); + } + return name; +} + +// ── Regexes ────────────────────────────────────────────────────────── +// text-[11px], text-[0.65rem], with optional /[Npx] line-height suffix +// (none found in this codebase, but handled for completeness/future-proofing). +const FS_RE = /text-\[([0-9.]+)(px|rem)\](?:\/\[([0-9.]+)(px|rem)\])?/g; +const LS_RE = /tracking-\[([0-9.]+)em\]/g; +const LEADING_RE = /leading-\[([^\]]+)\]/g; +const FONTSIZE_STYLE_RE = /fontSize:\s*"([0-9.]+)(px|rem)"/g; + +function rewriteFile(filePath, relPath) { + const original = readFileSync(filePath, "utf8"); + let content = original; + let siteCount = 0; + + // -- font-size Tailwind class utilities -- + content = content.replace(FS_RE, (match, num, unit, lhNum, lhUnit) => { + const fsName = registerFsToken(num, unit, `Extracted from ${relPath} (text-[${num}${unit}]).`); + let replacement = `text-(length:--${fsName})`; + if (lhNum) { + const lhName = registerLhToken(`${lhNum}${lhUnit}`, `Extracted from ${relPath} (text-[...]/[${lhNum}${lhUnit}] line-height suffix).`); + replacement += `/(--${lhName})`; + } + siteCount++; + return replacement; + }); + + // -- letter-spacing Tailwind class utilities -- + content = content.replace(LS_RE, (match, num) => { + const lsName = registerLsToken(num, `Extracted from ${relPath} (tracking-[${num}em]).`); + siteCount++; + return `tracking-(--${lsName})`; + }); + + // -- line-height Tailwind class utilities (standalone leading-[...]) -- + content = content.replace(LEADING_RE, (match, raw) => { + // Only rewrite numeric/unit literals (px, rem, unitless number). Skip + // keyword forms like leading-[inherit] or var()-based (already tokenized). + if (!/^[0-9.]+(px|rem)?$/.test(raw)) return match; + const lhName = registerLhToken(raw, `Extracted from ${relPath} (leading-[${raw}]).`); + siteCount++; + return `leading-(--${lhName})`; + }); + + // -- inline style fontSize string literals -- + content = content.replace(FONTSIZE_STYLE_RE, (match, num, unit) => { + const fsName = registerFsToken(num, unit, `Extracted from ${relPath} (inline style fontSize: "${num}${unit}").`); + siteCount++; + return `fontSize: "var(--${fsName})"`; + }); + + if (content !== original && !DRY_RUN) { + writeFileSync(filePath, content, "utf8"); + } + return { changed: content !== original, siteCount }; +} + +function main() { + const files = []; + for (const dir of SCAN_DIRS) walk(resolve(UI_SRC, dir), files); + + let totalSites = 0; + let filesChanged = 0; + const changedFiles = []; + + for (const filePath of files) { + const relPath = "ui/src/" + relative(UI_SRC, filePath); + const { changed, siteCount } = rewriteFile(filePath, relPath); + if (changed) { + filesChanged++; + changedFiles.push(relPath); + } + totalSites += siteCount; + } + + // ── index.css token block ────────────────────────────────────────── + const cssPath = resolve(UI_SRC, "index.css"); + const cssOriginal = readFileSync(cssPath, "utf8"); + const marker = "/* ── Extracted verbatim TYPE tokens (Phase 2 Batch 2, design/token-extraction) ── */"; + let cssNext = cssOriginal; + let cssChanged = false; + + if (!cssOriginal.includes(marker) && (fsTokens.size || lsTokens.size || lhTokens.size)) { + const lines = []; + lines.push(marker); + lines.push("/* Batch 2/4: font-size + letter-spacing + line-height literals, verbatim"); + lines.push(" (no normalizing — 9/10/11/12/13/14/15px and 0.08-0.24em all stay distinct;"); + lines.push(" the human scale-collapse decision comes later per DESIGN.md/TOKEN-AUDIT.md)."); + lines.push(""); + lines.push(" Allowlist (sites intentionally left as hardcoded / functional literals,"); + lines.push(" NOT converted to tokens — each also carries an inline"); + lines.push(" `token-extraction: allowlisted` comment at the site):"); + lines.push(" - pages/CompanyEnvironments.tsx (fontSize: 12) — xterm.js terminal theme"); + lines.push(" config; functional third-party numeric option, not a rendered CSS value."); + lines.push(" Same allowlisted object as Batch 1's color entry for this file."); + lines.push(" - pages/CompanySkills.tsx (fontSize: Math.round(size * 0.42)) — computed at"); + lines.push(" runtime from a prop; not a static literal, nothing to extract."); + lines.push("*/"); + lines.push(":root {"); + for (const [name, { value, comment }] of fsTokens) { + lines.push(` --${name}: ${value}; /* ${comment} */`); + } + for (const [name, { value, comment }] of lsTokens) { + lines.push(` --${name}: ${value}; /* ${comment} */`); + } + for (const [name, { value, comment }] of lhTokens) { + lines.push(` --${name}: ${value}; /* ${comment} */`); + } + lines.push("}"); + const block = "\n" + lines.join("\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-type summary`); + console.log(` Sites rewritten: ${totalSites}`); + console.log(` Files changed: ${filesChanged}`); + console.log(` New --fs-* tokens: ${fsTokens.size}`); + console.log(` New --ls-* tokens: ${lsTokens.size}`); + console.log(` New --lh-* tokens: ${lhTokens.size}`); + console.log(` index.css token block: ${cssChanged ? "added" : "already present or nothing to add (idempotent no-op)"}`); + if (changedFiles.length) { + console.log(`\n Changed files:`); + for (const f of changedFiles) console.log(` - ${f}`); + } +} + +main(); diff --git a/ui/src/components/AccountingModelCard.tsx b/ui/src/components/AccountingModelCard.tsx index 73ee19c7e7..35493159f1 100644 --- a/ui/src/components/AccountingModelCard.tsx +++ b/ui/src/components/AccountingModelCard.tsx @@ -30,7 +30,7 @@ export function AccountingModelCard() {
- + Accounting model diff --git a/ui/src/components/ActiveAgentsPanel.tsx b/ui/src/components/ActiveAgentsPanel.tsx index 2c2b0fcf29..00faf586ec 100644 --- a/ui/src/components/ActiveAgentsPanel.tsx +++ b/ui/src/components/ActiveAgentsPanel.tsx @@ -29,7 +29,7 @@ function RunCardRecoveryChip({ action }: { action: IssueRecoveryAction }) { aria-label={tone.label} title={`${tone.label} — open the source task to act.`} className={cn( - "inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-[10px] font-medium", + "inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-(length:--fs-10) font-medium", tone.className, )} > @@ -188,16 +188,16 @@ const AgentRunCard = memo(function AgentRunCard({ ) : ( )} - +
-
+
{isActive ? "Live now" : run.finishedAt ? `Finished ${relativeTime(run.finishedAt)}` : `Started ${relativeTime(run.createdAt)}`}
diff --git a/ui/src/components/ActivityCharts.tsx b/ui/src/components/ActivityCharts.tsx index 0dbac4a2cc..ccbadb8e0e 100644 --- a/ui/src/components/ActivityCharts.tsx +++ b/ui/src/components/ActivityCharts.tsx @@ -23,7 +23,7 @@ function DateLabels({ days }: { days: string[] }) { {days.map((day, i) => (
{(i === 0 || i === 6 || i === 13) ? ( - {formatDayLabel(day)} + {formatDayLabel(day)} ) : null}
))} @@ -35,7 +35,7 @@ function ChartLegend({ items }: { items: { color: string; label: string }[] }) { return (
{items.map(item => ( - + {item.label} @@ -49,7 +49,7 @@ export function ChartCard({ title, subtitle, children }: { title: string; subtit

{title}

- {subtitle && {subtitle}} + {subtitle && {subtitle}}
{children}
diff --git a/ui/src/components/ActivityFeed.tsx b/ui/src/components/ActivityFeed.tsx index 4bcdd777cf..2e8796a40d 100644 --- a/ui/src/components/ActivityFeed.tsx +++ b/ui/src/components/ActivityFeed.tsx @@ -470,7 +470,7 @@ export function ActivityFeed({ className }: ActivityFeedProps) { separator = (
- + Earlier
diff --git a/ui/src/components/AgentBubbleActionRow.tsx b/ui/src/components/AgentBubbleActionRow.tsx index 66db7f3353..a6b262d1ef 100644 --- a/ui/src/components/AgentBubbleActionRow.tsx +++ b/ui/src/components/AgentBubbleActionRow.tsx @@ -109,7 +109,7 @@ export function AgentBubbleActionRow({ {dateLabel} diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index ac507b7b88..0399f4530e 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -1247,7 +1247,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { {supportsModelProfiles && ( -
Primary model
+
Primary model
)}
{statusLabel} - + {new Date(result.testedAt).toLocaleTimeString()}
{result.checks.map((check, idx) => ( -
+
{check.level} @@ -1657,7 +1657,7 @@ function AdapterTypeDropdown({ {item.experimental && } {item.comingSoon && ( - Coming soon + Coming soon )} ))} @@ -1668,7 +1668,7 @@ function AdapterTypeDropdown({ function ExperimentalBadge() { return ( - + Experimental ); @@ -1871,7 +1871,7 @@ function ModelDropdown({ {models.find((m) => m.id === value)?.label ?? value} - + current @@ -1890,7 +1890,7 @@ function ModelDropdown({ {models.find((m) => m.id === detectedModel)?.label ?? detectedModel} - + detected @@ -1914,7 +1914,7 @@ function ModelDropdown({ {entry?.label ?? candidate} - + config @@ -1953,7 +1953,7 @@ function ModelDropdown({ {groupedModels.map((group) => (
{groupByProvider && ( -
+
{group.provider} ({group.entries.length})
)} @@ -2021,7 +2021,7 @@ function CheapModelSection({
-
Cheap model
+
Cheap model

Used when a run requests the cheap profile (e.g. routine summaries). The primary model stays unchanged.

@@ -2046,12 +2046,12 @@ function CheapModelSection({ /> ) : null} {enabled && !model && adapterDefaultModel ? ( -

+

No explicit cheap model selected — runtime falls back to {adapterDefaultModel}.

) : null} {enabled && !model && !adapterDefaultModel ? ( -

+

No cheap model selected and the adapter has no default. Cheap-lane runs will continue on the primary model with a fallback note.

) : null} diff --git a/ui/src/components/ApprovalCard.tsx b/ui/src/components/ApprovalCard.tsx index 7084f6b0ef..e040a116b2 100644 --- a/ui/src/components/ApprovalCard.tsx +++ b/ui/src/components/ApprovalCard.tsx @@ -63,7 +63,7 @@ export function ApprovalCard({
{kindLabel} diff --git a/ui/src/components/ApprovalPayload.tsx b/ui/src/components/ApprovalPayload.tsx index 6a1fe2597b..67b2a52c25 100644 --- a/ui/src/components/ApprovalPayload.tsx +++ b/ui/src/components/ApprovalPayload.tsx @@ -70,7 +70,7 @@ function SkillList({ values }: { values: unknown }) { {items.map((item) => ( {item} @@ -178,19 +178,19 @@ function BoardApprovalPayloadContent({ payload }: { payload: Record {title && (
-

Title

+

Title

{title}

)} {summary && (
-

Summary

+

Summary

{summary}

)} {recommendedAction && (
-

+

Recommended action

{recommendedAction}

@@ -198,13 +198,13 @@ function BoardApprovalPayloadContent({ payload }: { payload: Record -

On approval

+

On approval

{nextActionOnApproval}

)} {risks.length > 0 && (
-

Risks

+

Risks

    {risks.map((risk) => (
  • @@ -217,7 +217,7 @@ function BoardApprovalPayloadContent({ payload }: { payload: Record -

    +

    Proposed comment

    diff --git a/ui/src/components/ArtifactsPanel.tsx b/ui/src/components/ArtifactsPanel.tsx
    index 1e8b633061..cdc6d8a765 100644
    --- a/ui/src/components/ArtifactsPanel.tsx
    +++ b/ui/src/components/ArtifactsPanel.tsx
    @@ -139,7 +139,7 @@ export function ArtifactsPanel({ taskId, isAgentWorking, openDocKey, openDocTitl