refactor(ui): extract remaining bracket values, add token gate check (Phase 2 batch 4/4)

Final batch of the design-token extraction: mints tokens for every
remaining value-bearing arbitrary Tailwind bracket in
ui/src/components/** and ui/src/pages/**, then adds a gate script so
the "zero hardcoded value" rule is enforced going forward instead of
re-audited by hand each time.

Codemod: scripts/codemod-extract-misc.mjs (198 sites, 84 files, 96 new
tokens: 58 --gtc-*/3 --gtr-* grid track lists, 19 --tp-*
transition-property lists, 6 --z-* z-index values, --s-*/--e-*/--va-*/
--sw-* scale/ease/vertical-align/stroke-width, 2 --blur-*, 1
--drop-shadow-extract-*, 4 *-resolved wrapper tokens resolving
FileViewerSheet.tsx's half-migrated var(x, fallback) sites Batch 1
deferred, and 2 --gradient-extract-* continuing Batch 1's counter for
2 hsl(var())/color-mix() gradient sites its color-literal audit didn't
catch). Mandatory Step 0 syntax spike confirmed every paren form
byte-equivalent via scratch stories + built-CSS grep, and confirmed
the tw-animate-css plugin utilities (zoom-in-[0.97] etc.) are
currently dead/no-op classes (plugin not installed) — allowlisted,
not rewritten, since there's no visual behavior to preserve or change.

Gate script: scripts/check-token-gates.mjs (new, wired as
`pnpm check:token-gates`) implements the three DONE-WHEN gates
(hex/color-literal, arbitrary-bracket-value, raw-font-size) over
ui/src/components/** and ui/src/pages/**, parsing a new
machine-readable ALLOWLIST block appended to ui/src/index.css that
consolidates Batches 1-3's free-form allowlist prose into
`* allow <path> — <reason>` lines. Resolved TOKEN-AUDIT.md's open
"test file policy" question by allowlisting 43 test-fixture hex-literal
sites across 18 *.test.tsx files (mock prop/API-response data, not
rendered CSS). Gate exits 0 clean.

AGENTS.md's existing Design system section now names the gate command.
TOKEN-AUDIT.md gets a Batch 4 log appendix with counts, allowlist
rationale, and the remaining out-of-scope debt (36 hex sites in
ui/src/lib|context|plugins; the ~3,115-site Tailwind-palette-class
scope question is still unresolved and flagged for a human decision).

Verify: node scripts/check-token-gates.mjs exit 0 (all 3 gates clean);
pnpm build-storybook exit 0; Storybook visual suite 510/510 (Playwright,
tests/storybook-visual); pnpm typecheck (ui/) exit 0; codemod idempotent
on re-run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
scotttong 2026-07-06 15:50:09 -07:00
parent be63424987
commit e0bbf3c911
87 changed files with 1365 additions and 185 deletions

View File

@ -223,4 +223,4 @@ PR #2218 (`feat/external-adapter-phase1`) adds external adapter support. See roo
## Design system
`DESIGN.md` at the repo root is the source of truth for UI design decisions. The token-only rule applies to all `ui/` changes: every color, spacing, radius, type, shadow, and motion value comes from the token layer in `ui/src/index.css` — no hex, raw px, or arbitrary Tailwind bracket values in components.
`DESIGN.md` at the repo root is the source of truth for UI design decisions. The token-only rule applies to all `ui/` changes: every color, spacing, radius, type, shadow, and motion value in `ui/src/components/**` and `ui/src/pages/**` comes from the token layer in `ui/src/index.css` — no hex, raw px, arbitrary Tailwind bracket values, or raw `font-size`/`fontSize` declarations in components, outside the documented allowlist in `ui/src/index.css`. Run `pnpm check:token-gates` (`scripts/check-token-gates.mjs`) before committing UI changes — it fails on any violation not covered by that allowlist.

View File

@ -362,3 +362,67 @@ Codemod: `scripts/codemod-extract-sizes.mjs`. Blanket regex sweep (like Batch 2
- **`drop-shadow-[...]` vs `shadow-[...]` token family** (`components/CompanyPatternIcon.tsx`, 1 site) — should `drop-shadow` values share the `--shadow-extract-*` family this batch minted, or get their own `--drop-shadow-extract-*` family? Left untouched this batch since TOKEN-AUDIT.md section 3.6 only inventoried `box-shadow` (`shadow-[...]`) sites, and the mission mandate for this batch was explicitly `shadow-[...]` only.
- **`theme(spacing.N)` resolution direction** (`components/IssueRow.tsx`) — this batch resolved `calc(theme(spacing.N)±Mpx)` to its Tailwind-v4-default-scale rem equivalent (verbatim, byte-equivalent verified) rather than leaving it as a build-time-only expression. If a future change to Tailwind's `--spacing` base ever happens, these 3 tokens will silently stop tracking that base (since they're now baked rem literals, not `theme()` calls) — a human should confirm this is the intended tradeoff, or decide these 3 sites should instead reference the Tailwind spacing scale by a different mechanism.
- **`--pct-neg-50` vs prefix-negation form** — this batch mints distinct positive/negative percentage tokens (`--pct-50` / `--pct-neg-50`) rather than using Tailwind's `-utility-(--x)` prefix-negation shorthand, because every negative-percentage site in this codebase already carries the minus sign inside the bracket value itself (`translate-x-[-50%]`), not as a separate utility-level negation (`-translate-x-[50%]`). A human doing the eventual scale-collapse pass should be aware both forms exist in Tailwind v4 and this codebase consistently uses the former.
---
## Phase 2 extraction log — Batch 4 (final sweep + gates)
Codemod: `scripts/codemod-extract-misc.mjs`. Blanket regex sweep (same style as Batches 2-3 — these remaining bracket-utility families are unambiguous), scoped to `ui/src/components/**` and `ui/src/pages/**` including `*.test.tsx` companions. Covers `grid-cols-[...]`/`grid-rows-[...]`, `transition-[...]`, `z-[...]`, `scale-[...]`, `ease-[...]`, `align-[...]`, `stroke-[...]`, `blur-[...]`/`backdrop-blur-[...]`, `drop-shadow-[...]`, `bg|text|border-[var(--x)]` (with and without a `,fallback`), and — discovered during this batch's own bracket sweep, not in the original task list — 2 `bg-[linear-gradient(...)]` sites that Batch 1's hand-audited color-literal table did not catch (see "scope correction" below). Verified idempotent (second run: 0 sites, 0 files changed).
**MANDATORY STEP 0 SYNTAX SPIKE (scratch stories + `pnpm build-storybook` + grep of emitted CSS, deleted before the real codemod ran):** every paren form in the batch mandate was confirmed byte-equivalent before any component was touched:
- `grid-cols-(--gtc-N)` with a multi-part track list containing `minmax()` → `grid-template-columns:var(--gtc-N)`. Confirmed.
- `transition-(--tp-N)` → `transition-property:var(--tp-N)` (plus the timing-function/duration vars Tailwind always emits alongside). Confirmed it sets `transition-property`, not some other longhand.
- `z-(--z-N)` → `z-index:var(--z-N)`. Confirmed.
- `scale-(--s-N)` → `scale:var(--s-N)`. Confirmed.
- `ease-(--e-N)` → `transition-timing-function:var(--e-N)` (and `--tw-ease`). Confirmed.
- `align-(--va-N)` → `vertical-align:var(--va-N)`. Confirmed.
- `stroke-(length:--sw-N)` → `stroke-width:var(--sw-N)`. Confirmed the `length:` hint is REQUIRED — a bare `stroke-(--x)` would be ambiguous with the `stroke` COLOR utility, same reasoning as Batch 3's `ring`/`outline` hint requirement.
- `backdrop-blur-(--blur-N)` → `--tw-backdrop-blur:blur(var(--blur-N))`. Confirmed (spiked separately after discovering the batch's 2 real sites are `backdrop-blur-[...]`, not bare `blur-[...]` — see gotcha below).
- Fallback-comma paren form (`bg-(--x,fallback)`): **NOT SUPPORTED.** Tailwind v4's paren shorthand only accepts a single custom-property reference (optionally with a `type:` hint); a literal comma-separated fallback inside the parens does not parse as a fallback expression. Per the mission's fallback plan, a wrapper token (`--code-highlight-bg-resolved: var(--paperclip-code-highlight-bg, rgba(250, 204, 21, 0.12));`) was minted instead and referenced via the plain `bg-(--code-highlight-bg-resolved)` form — confirmed this compiles to `background-color:var(--code-highlight-bg-resolved)`.
- Plugin utilities (`zoom-in-[0.97]`, `zoom-out-[0.97]`, `slide-in-from-top-[1%]`, `slide-out-to-top-[1%]`, plus their siblings `animate-in`/`animate-out`/`fade-in-0`/`fade-out-0`): **DEAD CLASSES, zero rendered CSS.** Grepping the built `storybook-static` CSS for any of these class names (in escaped or unescaped form) returns nothing — not even the base `animate-in` machinery they depend on. Confirmed via `package.json`/`node_modules` inspection that the `tw-animate-css` plugin is not installed anywhere in this repo, and `ui/src/index.css` defines no matching `@utility` overrides. These utilities have had zero visual effect since before this run started; nothing to tokenize without changing (from nothing to something) a currently inert class, which is out of this batch's zero-visual-change mandate. Left untouched, allowlisted with the reasoning above.
- `theme(colors.muted.DEFAULT)` / `theme(colors.muted.foreground)` fallback resolution: inspected the ALREADY-BUILT `ui/storybook-static/assets/*.css` before writing the codemod (per the mission's instruction to check build output first, since `theme()` is a build-time function) — confirmed byte-for-byte that Tailwind compiles `bg-[var(--paperclip-code-bg,theme(colors.muted.DEFAULT))]` to `background-color:var(--paperclip-code-bg,var(--muted))` and the gutter-fg sibling to `color:var(--paperclip-code-gutter-fg,var(--muted-foreground))`. Minted `--code-bg-resolved: var(--paperclip-code-bg, var(--muted));` and `--code-gutter-fg-resolved: var(--paperclip-code-gutter-fg, var(--muted-foreground));` reproducing that exact resolved form.
**Sites rewritten: 198**, across 84 files (79 component/page files + 3 test files rewritten in lockstep — `IssueDocumentAnnotations.test.tsx` (`z-[60]` → `z-(--z-60)`, 2 assertion strings), `MarkdownBody.test.tsx` (`align-[-0.125em]` → `align-(--va-0_125em)`, 3 assertion strings), `SidebarShell.test.tsx` (`transition-[width]` → `transition-(--tp-width)`, 1 negative assertion) — plus `AGENTS.md`/`package.json`/`ui/src/index.css` for the gate script wiring and doc updates).
**Tokens minted: 96 new** (58 `--gtc-*` + 3 `--gtr-*` + 19 `--tp-*` + 6 `--z-*` + 1 `--s-*` + 1 `--e-*` + 1 `--va-*` + 1 `--sw-*` + 2 `--blur-*` + 1 `--drop-shadow-extract-*` + 4 `*-resolved` wrapper tokens + 2 `--gradient-extract-*` continuing Batch 1's counter as `-25`/`-26`; 0 reused from Batches 1-3 — no existing token in any prior family matched these new value shapes).
- **`--gtc-*` (58) / `--gtr-*` (3)** — grid-template-columns/rows track lists, verbatim (underscore-to-space reversal applied per Batch 1's gotcha), sequentially numbered (content like `minmax(0,1fr)_auto` isn't safely nameable). Deduped on exact string match: e.g. `--gtr-2`/`--gtr-3` (`1fr`/`0fr`) each reused across both `CompanySkills.tsx` `expanded`/collapsed sites. One pre-existing source oddity preserved verbatim, not "fixed": `pages/Costs.tsx`'s `grid-cols-[1.3fr,1fr]`/`[1.25fr,0.95fr]`/`[1.2fr,0.95fr]` use a literal COMMA between track values (not the standard space-separated `grid-template-columns` syntax) — confirmed via the built CSS that this is exactly what ships today (`grid-template-columns:1.3fr,1fr`), so the token values (`--gtc-31`/`-32`/`-33`) preserve the comma byte-for-byte rather than "correcting" it to a space, per DESIGN.md's no-normalizing rule.
- **`--tp-*` (19)** — `transition-property` lists, slugged from the comma-joined property names (e.g. `--tp-width-background-color: width,background-color;`), one token per distinct property-list string.
- **`--z-*` (6)** — `--z-1`, `--z-2`, `--z-60`, `--z-120`, `--z-200`, `--z-9999`, bare numeric values matching TOKEN-AUDIT.md section 3.7's ad hoc z-index inventory exactly. Still no z-index TIER/scale decision made (per that section's original flag) — these are 6 independent verbatim values, not a scale.
- **`--s-0_98`, `--e-cubic-bezier-0_16-1-0_3-1`, `--va-0_125em`, `--sw-2_3`** — one token each (scale/ease/vertical-align/stroke-width all had exactly one distinct value in scope).
- **`--blur-1px` / `--blur-2px`** — 2 sites, 2 distinct values (`ChatComposer.tsx`'s `backdrop-blur-[1px]`, `IssueChatThread.tsx`'s `backdrop-blur-[2px]`); confirmed both are `backdrop-blur`, not bare `blur` (see gotcha below).
- **`--drop-shadow-extract-1`** — `CompanyPatternIcon.tsx`'s `drop-shadow-[0_1px_2px_rgba(0,0,0,0.65)]`, its own family per Batch 3's logged "needs human decision" (kept separate from `--shadow-extract-*`, which is `box-shadow`; `drop-shadow` is a `filter` function, a different CSS property).
- **4 `*-resolved` wrapper tokens** (`--code-bg-resolved`, `--code-highlight-bg-resolved`, `--code-gutter-fg-resolved`, `--code-highlight-border-resolved`) — all four `FileViewerSheet.tsx` half-migrated `var(--paperclip-code-*, fallback)` sites Batch 1 explicitly deferred as "needs human decision" (see Batch 1's log above) are now resolved: the `--paperclip-code-highlight-bg`/`-border` sites keep their literal rgba/rgb fallback verbatim; the `--paperclip-code-bg`/`-gutter-fg` sites' `theme(colors.muted...)` fallbacks are resolved to the build's own `var(--muted)`/`var(--muted-foreground)` equivalent (see Step 0 spike above). The inline `token-extraction: allowlisted` comment at the site and Batch 1's allowlist doc-comment entry for this file were both updated to note the resolution rather than silently deleting the historical note.
- **`--gradient-extract-25` / `-26`** — SCOPE CORRECTION found during this batch's own bracket sweep, not called out in the original task list: `components/SidebarAccountMenu.tsx` and `pages/ProfileSettings.tsx` each had one `bg-[linear-gradient(...)]` site using `hsl(var(--primary))`/`color-mix(in_oklab,...)` CSS-native color functions rather than raw hex/rgb literals — which is exactly why Batch 1's hand-audited color-literal table (built to avoid false-positiving on issue references like `acme/web#241`) did not catch them; they were never scanned as "color literals" because they contain no hex/rgb/hsl numeric literal, only `var()`/`color-mix()` references. They ARE still value-bearing gradient brackets by gate 2's definition (a CSS value function), so this batch mints 2 more tokens CONTINUING Batch 1's `--gradient-extract-*` counter (picking up at 25, not restarting at 1) rather than creating a parallel family.
- All new tokens live in a fourth non-`@theme` `:root { ... }` block appended to `ui/src/index.css` immediately after Batch 3's block, headed `/* ── Extracted verbatim MISC tokens (Phase 2 Batch 4, design/token-extraction) ── */`, per DESIGN.md (runtime-tunable).
**GOTCHA — `backdrop-blur-[...]` vs bare `blur-[...]` boundary:** the task's "any `blur-[...]`" language and TOKEN-AUDIT.md section 3.7's inventory both describe the 2 sites as `blur-[Npx]`, but the actual class names are `backdrop-blur-[1px]`/`backdrop-blur-[2px]` (a `filter: backdrop-filter` utility, not the plain `filter: blur()` utility). A bare `blur-\[...\]` regex anchored at Batch 3's class-token BOUNDARY does not match `backdrop-blur-[...]` at all, since `blur` there doesn't start at a boundary (it's preceded by `backdrop-`, not whitespace/quote/colon) — caught immediately in this batch's dry run (0 blur sites found on the first pass despite 2 being expected from the inventory) rather than silently mis-tokenizing. Fixed by matching `(backdrop-blur|blur)-\[...\]` as two alternatives sharing one token family, confirmed both compile correctly in the Step 0 spike.
**Sites allowlisted (added to the canonical machine-readable ALLOWLIST block in `ui/src/index.css`, consolidating and reformatting the prior per-batch prose lists into `* allow <path> — <reason>` lines for `scripts/check-token-gates.mjs` to parse):**
1. `components/ui/dialog.tsx` / `components/ui/alert-dialog.tsx` — `zoom-in-[0.97]`/`zoom-out-[0.97]`/`slide-in-from-top-[1%]`/`slide-out-to-top-[1%]` (plus `animate-in`/`animate-out`/`fade-in-0`/`fade-out-0`): dead/no-op classes, no `tw-animate-css` plugin installed, confirmed via built-CSS grep (see Step 0 spike above).
2. `components/ProjectWorkspaceSummaryCard.tsx` (`min-[420px]:`) / `components/FileTree.tsx` (`max-[480px]:`, 2 sites) — arbitrary BREAKPOINT VARIANTS, not values; the variant position cannot reference a CSS custom property (Tailwind resolves variants at build time), so there is nothing to tokenize without changing Tailwind's own variant syntax. Distinct from `rounded-[inherit]` (Batch 3, a keyword) but the same category of "syntactically cannot hold a token reference."
3. **18 `*.test.tsx` files, 43 hex-literal sites (new policy ruling this batch, resolving TOKEN-AUDIT.md section 8 item 10):** `IssueFiltersPopover.test.tsx`, `IssueProperties.test.tsx`, `MarkdownBody.test.tsx`, `MarkdownEditor.test.tsx`, `NewIssueDialog.test.tsx`, `ProjectTile.test.tsx`, `RoutineRunVariablesDialog.test.tsx`, `SidebarCompanyMenu.test.tsx`, `SidebarProjects.test.tsx`, `SidebarStarredProjects.test.tsx`, `CompanyEnvironments.test.tsx`, `ExecutionWorkspaceDetail.test.tsx`, `InviteLanding.test.tsx`, `IssueDetail.test.tsx`, `ProjectDetail.test.tsx`, `ProjectWorkspaceDetail.test.tsx`, `Projects.test.tsx`, `Routines.test.tsx`. Every site is MOCK PROP / MOCK API-RESPONSE DATA (`color: "#hex"` label/project/routine fixtures, `brandColor`/`companyBrandColor` company fixtures, an xterm.js theme-mock assertion mirroring the already-allowlisted `CompanyEnvironments.tsx` production config) — none are Tailwind class strings or CSS-in-JS style declarations. This is the test-file counterpart of the exact category Batch 1 already allowlisted in production source (persisted/functional data, not a rendered CSS value); rewriting them to `var()` strings would make mock fixtures look unlike real API responses without touching a single rendered pixel. Ruling: test-fixture prop/mock-data hex literals are allowlisted as a standing category, one line per file for reviewability.
4. **3 gate-1 false positives found and fixed in the gate script itself, not allowlisted (no real hex color involved):** `ExternalObjectPill.test.tsx`/`IssueProperties.test.tsx`'s `"acme/web#241"`-style issue-reference strings were initially flagged by a naive hex regex (`#241` is 3 valid hex digits) — the exact false-positive shape Batch 1's own color codemod header already documented. Fixed with a negative lookbehind requiring a hex color's `#` not be immediately preceded by an identifier character or `/` (a real CSS color is always preceded by a delimiter — quote, colon, paren, comma, whitespace, backtick, or string start — never glued directly to a slash-path or word).
**Gate script: `scripts/check-token-gates.mjs`** (new, Part C of this batch). Implements the three DONE-WHEN gates over `ui/src/components/**` and `ui/src/pages/**`:
- **Gate 1 (color literals):** hex colors (`#fff`/`#ffffff`/`#ffffffff`) and `rgb()`/`rgba()`/`hsl()`/`hsla()`/`oklch()` calls with a LITERAL first argument (not `var(...)`) — so `hsl(var(--primary)/0.16)` passes, `rgba(0,0,0,0.5)` fails.
- **Gate 2 (arbitrary bracket values):** flags `word-[content]` where `content` carries a rendered value (digits+CSS-unit, bare hex, or a CSS value function — `calc`/`min`/`max`/`clamp`/`var`/`env`/`linear-gradient`/`radial-gradient`/`conic-gradient`/`cubic-bezier`/`rgba?`/`hsla?`/`oklch`/`color-mix`). **Selector/variant brackets are excluded BY DEFINITION** (documented in the script's header, matching the mission's Part B instruction), recognized structurally two ways: (a) known variant-keyword prefixes (`data`, `has`, `aria`, `supports`, `group-data`, `group-has-data`, `group-aria`, `peer-data`, `peer-aria`, `in`, `not`), and (b) any bracket immediately followed by `:` (the structural signature of a breakpoint/arbitrary-variant prefix like `max-[480px]:hidden`, since a value-bearing utility bracket is never itself followed by another `:`-prefixed segment). `rounded-[inherit]` is not flagged (a bare keyword, not a unit/hex/function shape) and is separately allowlisted per Batch 3's precedent for defense-in-depth.
- **Gate 3 (raw font-size):** `text-[Npx]`/`text-[N.Nrem]` class brackets, plus `fontSize: "N..."` / `font-size: "N..."` string-literal declarations that BEGIN with a digit — deliberately excluding `fontSize: "var(--fs-11)"`-style token references (the desired post-extraction form) from matching as a violation.
- **Allowlist parsing:** reads `* allow <repo-relative-path> — <reason>` lines from the canonical ALLOWLIST block appended to the end of `ui/src/index.css` (added this batch, consolidating the Batches 1-3 free-prose lists into this one machine-readable format while leaving the original prose blocks in place as historical narrative). A path suppresses a violation if the violating file's path CONTAINS the allowlisted path as a substring.
- Exit code 0 with a per-gate summary when clean; exit code 1 listing every violation (file:line + snippet), grouped by gate, otherwise.
- Wired as `pnpm check:token-gates` (`"check:token-gates": "node scripts/check-token-gates.mjs"` in the root `package.json`).
**Verify results:**
- Own full bracket sweep (`rg`-based, matching the gate script's boundary logic) over `ui/src/components/**`/`ui/src/pages/**`: only selector/variant brackets (`data-[...]`, `group-data-[...]`, `has-[...]`, `supports-[...]`, etc.) and the documented allowlisted exceptions (`rounded-[inherit]`, `max-[480px]`/`min-[420px]`, the 4 dead tw-animate-css classes) remain.
- `node scripts/check-token-gates.mjs` → **exit 0**, "All gates clean" (Gate 1/2/3 all CLEAN; 468 files scanned; 31 allowlist entries loaded; 71 allowlisted issues correctly skipped rather than flagged).
- `pnpm build-storybook` → exit 0.
- Storybook visual snapshot suite: **510/510 passed** (`npx playwright test --config tests/storybook-visual/playwright.config.ts --reporter=line`) — 509 passed clean on the first attempt + 1 known-benign flaky retry (`product-documents-annotations--integrated-desktop-open [dark]`, the exact flake called out in the batch mandate as expected-benign) passed on Playwright's automatic retry.
- `pnpm typecheck` (from `ui/`) → exit 0.
- Codemod re-run confirmed idempotent: second invocation reports 0 sites rewritten, 0 files changed, token block already present.
- Spot-checked 3 lockstep test-file rewrites directly with `npx vitest run` (`IssueDocumentAnnotations.test.tsx`, `MarkdownBody.test.tsx`, `SidebarShell.test.tsx`): 67/67 tests passed.
**AGENTS.md:** the existing "Design system" section (added before Phase 2 began) was extended with one sentence naming `pnpm check:token-gates` as the required pre-commit check and clarifying the rule's exact scope (`ui/src/components/**`/`ui/src/pages/**`, allowlist-exempted) — kept to the section's existing brief tone, no new section added.
**Remaining out-of-scope debt (explicitly NOT addressed by this run, flagged for whoever scopes the next one):**
- **`ui/src/lib/`, `ui/src/context/`, `ui/src/plugins/`** were never in scope for any of the 4 batches (DESIGN.md/GOAL-PROMPT.md's Phase 2 spec and every batch mandate scoped strictly to `ui/src/components/**` and `ui/src/pages/**`). A quick count at the end of this batch: **36 hex-literal sites** remain in `lib`/`context`/`plugins` combined (`lib/color-contrast.ts`, `lib/worktree-branding.ts`, `context/ThemeContext.tsx` and others — the same "contrast-pair triplication" TOKEN-AUDIT.md section 8 item 9 already flagged). These are helper/logic modules, not component render code, so they were correctly out of this run's gate — but they are real remaining hardcoded-value debt in `ui/src/` overall, and DESIGN.md's "single token source" principle would eventually want them re-pointed at the same tokens too.
- **The Tailwind-palette-class scope question** (TOKEN-AUDIT.md section 8 item 1: `bg-red-500`-style classes, ~3,115 sites / 145 files) was never resolved by any batch and remains exactly as open as it was after Phase 1 — none of Batches 1-4 touched Tailwind's own built-in palette utilities (only literal hex/rgb/bracket-arbitrary values were extracted). This is still the single largest scope decision a human needs to make before any further token-extraction work: whether Tailwind's own oklch palette counts as "hardcoded" under DESIGN.md principle 2, or is considered part of the token system already (Tailwind's palette is itself a fixed design-token set, just not `index.css`'s).
- The micro type-size cluster (9-15px, TOKEN-AUDIT.md section 8 item 2), letter-spacing cluster (item 3), and radius 0px/lg/xl conflict (item 4) are all still open — this run intentionally minted verbatim tokens for every distinct value without collapsing any of them into a real scale, per DESIGN.md's explicit "ugly values stay ugly" instruction. The human scale-collapse decision (GOAL-PROMPT.md "after the run" step 2) is unchanged by Batch 4.

View File

@ -36,6 +36,7 @@
"release:rollback": "./scripts/rollback-latest.sh",
"release:bootstrap-package": "node scripts/bootstrap-npm-package.mjs",
"check:tokens": "node scripts/check-forbidden-tokens.mjs",
"check:token-gates": "node scripts/check-token-gates.mjs",
"check:no-git-push": "node scripts/check-no-git-push.mjs",
"test:check-no-git-push": "node --test scripts/check-no-git-push.test.mjs",
"test:hermes-gateway-smoke": "node --test scripts/smoke/hermes-gateway-smoke.test.mjs",

View File

@ -0,0 +1,315 @@
#!/usr/bin/env node
/**
* check-token-gates.mjs
*
* Phase 2 (extraction) DONE-WHEN gate check for the design-token-extraction
* run (branch design/token-extraction; see DESIGN.md, GOAL-PROMPT.md,
* TOKEN-AUDIT.md). Scans `ui/src/components/**` and `ui/src/pages/**`
* (excluding `ui/src/lib|context|plugins`, which are explicitly out of
* scope for this run per TOKEN-AUDIT.md's Batch 4 log) for three gates:
*
* Gate 1 — zero hardcoded COLOR LITERALS: hex colors (#fff, #ffffff,
* #ffffffff) and rgb()/rgba()/hsl()/hsla()/oklch() value literals
* (i.e. NOT a var() reference, and not merely referencing a CSS
* variable inside one of those functions, e.g. hsl(var(--primary)) is
* fine — only a literal numeric color argument fails the gate).
*
* Gate 2 — zero VALUE-BEARING arbitrary Tailwind bracket utilities:
* bracket contents (`utility-[...]`) that carry a rendered CSS value
* (digits with CSS units, bare numbers, color literals, or CSS value
* functions like calc()/min()/max()/clamp()/var()/linear-gradient()/
* cubic-bezier()/rgba()/env()). This is checked on the UTILITY
* position, i.e. `word-[...]` where `word` is not itself a selector/
* variant keyword.
*
* SELECTOR/VARIANT BRACKETS ARE EXCLUDED BY DEFINITION, not by
* omission: `data-[...]`, `group-data-[...]`, `has-[...]`,
* `group-has-data-[...]`, `aria-[...]`, `supports-[...]`, and
* `max-[...]`/`min-[...]` used as a BREAKPOINT VARIANT PREFIX (i.e.
* immediately followed by `:`, such as `max-[480px]:hidden`) are CSS
* SELECTOR CONDITIONS or responsive variant prefixes, not visual
* values applied to a property — they describe WHEN a rule applies,
* not WHAT value it sets. A variant's bracket cannot reference a CSS
* custom property (Tailwind resolves variants at build time, before
* any `var()` could be evaluated), so there is nothing to tokenize;
* tokenizing would require changing Tailwind's own variant syntax,
* which is out of scope. These are recognized structurally: a
* bracket immediately followed by `:` (not part of a class string's
* trailing utility) is a variant, not a utility value.
*
* True exceptions that DO carry a value but cannot be tokenized are
* ALLOWLISTED, not silently excluded (see ALLOWLIST parsing below):
* `max-[480px]`/`min-[420px]` breakpoint variants (variant position
* cannot reference a var), and `rounded-[inherit]` (a CSS-wide
* keyword, not a literal value, cannot come from a custom property).
*
* Gate 3 — zero raw FONT-SIZE declarations: `text-[Npx]`/`text-[N.Nrem]`
* Tailwind arbitrary font-size utilities (a subset of gate 2, checked
* explicitly since font-size is its own DESIGN.md-named category) and
* `fontSize: "..."` / `font-size:` string-literal declarations in
* inline styles or css-in-js.
*
* The ALLOWLIST is parsed from the machine-readable block in
* ui/src/index.css (search for "── ALLOWLIST" below it), one entry per
* line in the form:
* * allow <repo-relative-path> — <reason>
* A violation at a path is suppressed if the path CONTAINS (substring
* match) any allowlisted path. This intentionally allowlists the whole
* file for simplicity/reviewability, matching how Batches 1-3 allowlisted
* entire sites' surrounding functional code rather than individual
* characters.
*
* Exit code: 0 if all three gates are clean (prints a per-gate summary).
* Exit code: 1 if any gate has violations (lists them, grouped by gate).
*
* Usage: node scripts/check-token-gates.mjs
*/
import { readFileSync, readdirSync } 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 CSS_PATH = resolve(UI_SRC, "index.css");
// ── Allowlist parsing ────────────────────────────────────────────────────
// Reads the machine-readable "* allow <path> — <reason>" lines from the
// ALLOWLIST block in ui/src/index.css. Tolerant of either em-dash (—) or
// a plain hyphen-minus as the path/reason separator, and of the historical
// per-batch prose blocks NOT being in this format (they are not parsed;
// only lines starting with "* allow " are).
function loadAllowlist(cssPath) {
const css = readFileSync(cssPath, "utf8");
const entries = [];
const lineRe = /^\s*\*\s*allow\s+(\S+)\s+(?:—|-{1,2})\s*(.*)$/;
for (const rawLine of css.split("\n")) {
const m = rawLine.match(lineRe);
if (m) {
entries.push({ path: m[1], reason: m[2].trim() });
}
}
return entries;
}
function isAllowlisted(relPath, allowlist) {
return allowlist.some((entry) => relPath.includes(entry.path));
}
// ── File walking ─────────────────────────────────────────────────────────
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 listFiles() {
const files = [];
for (const dir of SCAN_DIRS) walk(resolve(UI_SRC, dir), files);
files.sort();
return files;
}
// ── Gate 1: color literals ───────────────────────────────────────────────
// Hex colors: #abc, #aabbcc, #aabbccdd — word-boundary guarded so it
// doesn't match inside identifiers, and NOT preceded by another hex digit
// (avoids over-matching truncated substrings of longer non-color tokens,
// though `#` itself is a strong enough anchor in practice).
// A genuine CSS hex color is never glued directly to an identifier
// character (letter/digit/underscore) or `/` immediately before the `#` —
// that shape is an issue/PR reference like "acme/web#241" or "acme/web#12"
// (Batch 1's codemod header documented this exact false-positive risk for
// its own hex-literal sweep; the same guard applies here). A real color
// literal is preceded by a delimiter (quote, colon, paren, comma,
// whitespace, backtick, template `${`) or sits at the start of the string.
const HEX_COLOR_RE = /(?<![a-zA-Z0-9_/])#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
// rgb()/rgba()/hsl()/hsla()/oklch() with a LITERAL first argument (a digit,
// a `.` decimal, or a `%` — i.e. not `var(` or `calc(` immediately inside).
// `hsl(var(--x)/0.16)` must NOT match (var() reference); `rgba(0,0,0,0.5)`
// MUST match (literal numeric channels).
const COLOR_FN_LITERAL_RE = /\b(?:rgb|rgba|hsl|hsla|oklch)\(\s*(?!var\()[0-9.%-]/g;
function findColorLiteralIssues(content) {
const issues = [];
for (const m of content.matchAll(HEX_COLOR_RE)) {
issues.push({ index: m.index, snippet: m[0] });
}
for (const m of content.matchAll(COLOR_FN_LITERAL_RE)) {
issues.push({ index: m.index, snippet: m[0] });
}
return issues;
}
// ── Gate 2: value-bearing arbitrary bracket utilities ───────────────────
// Matches `word-[content]` (optionally prefixed by `!`, and optionally
// preceded by a Tailwind variant chain like `sm:` / `dark:` / `hover:` /
// `data-[state=open]:` etc. — the regex only needs to find the utility's
// OWN bracket, not parse the whole variant chain, since VARIANT_KEYWORDS
// below excludes variant-shaped words directly at the match site).
//
// A bracket is a VARIANT (excluded by definition, see header) if:
// (a) the word immediately before `-[` is one of the known variant
// keywords (data, group-data, has, group-has-data, aria, supports,
// group-aria, peer-data, peer-aria, in, not), OR
// (b) the bracket is immediately followed by `:` (a breakpoint-style
// variant prefix, e.g. `max-[480px]:hidden` — the `:` right after
/// `]` is the structural signal that this bracket is a CONDITION,
// not a value).
const BRACKET_RE = /(!?)([a-zA-Z][a-zA-Z0-9-]*)-\[([^\[\]]*)\]/g;
const VARIANT_WORD_RE =
/(?:^|[\s"'`{])(?:group-|peer-)?(?:data|has|aria|supports|in|not)(?:-[a-zA-Z0-9]+)*$/;
// A bracket carries a VALUE (not just a keyword/selector fragment) if its
// content looks like: a number (optionally with a CSS unit or %), a CSS
// color literal (# hex or a color function), OR a known CSS value function
// call (calc/min/max/clamp/var/env/linear-gradient/radial-gradient/
// conic-gradient/cubic-bezier/rgba/rgb/hsl/hsla/oklch). Pure CSS KEYWORDS
// (e.g. `inherit`, `auto`, `pointer`) do NOT match and are not gated here
// (they're a separate, allowlisted concern — see `rounded-[inherit]`).
const VALUE_UNIT_RE = /^-?[0-9.]+(?:px|rem|em|vh|vw|dvh|dvw|svh|svw|ch|%|deg|s|ms|fr)?$/;
const VALUE_FUNC_RE =
/^(?:calc|min|max|clamp|var|env|linear-gradient|radial-gradient|conic-gradient|cubic-bezier|rgba?|hsla?|oklch|color-mix)\(/;
const HEX_ONLY_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
function bracketCarriesValue(raw) {
const trimmed = raw.trim();
if (VALUE_UNIT_RE.test(trimmed)) return true;
if (HEX_ONLY_RE.test(trimmed)) return true;
if (VALUE_FUNC_RE.test(trimmed)) return true;
// A bracket containing an embedded CSS value function anywhere (e.g. a
// grid track list `56px_56px_24px_minmax(0,1fr)` that doesn't itself
// start with one of the above, or `translate-y-[-50%]`-style negative
// percentages already covered by VALUE_UNIT_RE) also counts.
if (/[0-9](?:px|rem|em|vh|vw|dvh|dvw|svh|svw|ch|%|deg|fr)\b/.test(trimmed)) return true;
if (/\b(?:calc|min|max|clamp|var|env|linear-gradient|radial-gradient|conic-gradient|cubic-bezier|rgba?|hsla?|oklch|color-mix)\(/.test(trimmed)) return true;
if (HEX_COLOR_RE.test(trimmed)) return true;
return false;
}
function findArbitraryBracketIssues(content) {
const issues = [];
for (const m of content.matchAll(BRACKET_RE)) {
const [full, , word, raw] = m;
const matchEnd = m.index + full.length;
const followedByColon = content[matchEnd] === ":";
if (followedByColon) continue; // breakpoint/arbitrary-variant prefix, not a utility value
// Reject if `word` itself IS (or ends in) a variant keyword shape, e.g.
// a match that accidentally captured "...data" as the utility name for
// some malformed/edge case. In practice BRACKET_RE's utility-name
// capture group only ever contains real utility names (data-[...] etc.
// are matched with `word` = "data", "group-data", "has", etc.).
const precedingContext = content.slice(Math.max(0, m.index - 1), m.index + word.length + 1);
if (VARIANT_WORD_RE.test(precedingContext)) continue;
if (/^(?:data|has|aria|supports|group-data|group-has-data|group-aria|peer-data|peer-aria|group-has-data-slot|in|not)$/.test(word)) {
continue;
}
if (!raw.includes("[") && bracketCarriesValue(raw)) {
issues.push({ index: m.index, snippet: `${word}-[${raw}]` });
}
}
return issues;
}
// ── Gate 3: raw font-size declarations ──────────────────────────────────
const FONT_SIZE_CLASS_RE = /\btext-\[(?:[0-9.]+(?:px|rem|em)|[0-9.]+\/[0-9.]+)\]/g;
// A raw literal font-size value: starts with a digit (px/rem/em number) —
// EXCLUDES `fontSize: "var(--fs-11)"`-style token references, which start
// with `var(` and are the desired post-extraction form, not a violation.
const FONT_SIZE_INLINE_RE = /\bfontSize\s*:\s*["'][0-9][^"']*["']/g;
const FONT_SIZE_CSS_PROP_RE = /(?<!-)\bfont-size\s*:\s*["'`][0-9][^"'`]*["'`]/g;
function findFontSizeIssues(content) {
const issues = [];
for (const m of content.matchAll(FONT_SIZE_CLASS_RE)) {
issues.push({ index: m.index, snippet: m[0] });
}
for (const m of content.matchAll(FONT_SIZE_INLINE_RE)) {
issues.push({ index: m.index, snippet: m[0] });
}
for (const m of content.matchAll(FONT_SIZE_CSS_PROP_RE)) {
issues.push({ index: m.index, snippet: m[0] });
}
return issues;
}
function lineNumberAt(content, index) {
return content.slice(0, index).split("\n").length;
}
function main() {
const allowlist = loadAllowlist(CSS_PATH);
const files = listFiles();
const violations = { gate1: [], gate2: [], gate3: [] };
let allowlistedSkips = 0;
for (const filePath of files) {
const content = readFileSync(filePath, "utf8");
const relPathPosix = relPathToPosix(filePath);
const allowed = isAllowlisted(relPathPosix, allowlist);
const g1 = findColorLiteralIssues(content);
const g2 = findArbitraryBracketIssues(content);
const g3 = findFontSizeIssues(content);
if (allowed) {
allowlistedSkips += g1.length + g2.length + g3.length;
continue;
}
for (const issue of g1) {
violations.gate1.push({ file: relPathPosix, line: lineNumberAt(content, issue.index), snippet: issue.snippet });
}
for (const issue of g2) {
violations.gate2.push({ file: relPathPosix, line: lineNumberAt(content, issue.index), snippet: issue.snippet });
}
for (const issue of g3) {
violations.gate3.push({ file: relPathPosix, line: lineNumberAt(content, issue.index), snippet: issue.snippet });
}
}
const totalViolations = violations.gate1.length + violations.gate2.length + violations.gate3.length;
console.log("check-token-gates summary");
console.log(` Files scanned: ${files.length}`);
console.log(` Allowlist entries loaded: ${allowlist.length}`);
console.log(` Allowlisted issues skipped: ${allowlistedSkips}`);
console.log("");
console.log(` Gate 1 (color literals): ${violations.gate1.length === 0 ? "CLEAN" : `${violations.gate1.length} violation(s)`}`);
console.log(` Gate 2 (arbitrary bracket vals): ${violations.gate2.length === 0 ? "CLEAN" : `${violations.gate2.length} violation(s)`}`);
console.log(` Gate 3 (raw font-size): ${violations.gate3.length === 0 ? "CLEAN" : `${violations.gate3.length} violation(s)`}`);
if (totalViolations > 0) {
console.log("\nViolations:\n");
for (const [gateName, list] of Object.entries(violations)) {
if (list.length === 0) continue;
console.log(`── ${gateName} ──`);
for (const v of list) {
console.log(` ${v.file}:${v.line} ${v.snippet}`);
}
console.log("");
}
process.exitCode = 1;
return;
}
console.log("\nAll gates clean.");
process.exitCode = 0;
}
// Windows path separators never appear in this repo's CI, but keep relative
// paths POSIX-style for allowlist substring matching regardless of platform.
function relPathToPosix(filePath) {
return ("ui/src/" + relative(UI_SRC, filePath)).split("\\").join("/");
}
main();

View File

@ -0,0 +1,599 @@
#!/usr/bin/env node
/**
* codemod-extract-misc.mjs
*
* Phase 2 (extraction), Batch 4/4 (final sweep) of the design-token audit
* (branch design/token-extraction). Replaces the remaining value-bearing
* arbitrary Tailwind bracket utilities 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`.
*
* Patterns covered this batch:
* grid-cols-[...] / grid-rows-[...] -> --gtc-<n> / --gtr-<n> (track lists,
* deduped on exact string match)
* transition-[...] -> --tp-<slug> (property lists,
* deduped on exact string match)
* z-[...] -> --z-<n> (bare z-index scale values)
* scale-[...] -> --s-<slug>
* ease-[cubic-bezier(...)] -> --e-<slug>
* align-[...] -> --va-<slug> (vertical-align)
* stroke-[...] -> --sw-<slug> (SVG stroke-WIDTH,
* requires the `length:` hint)
* blur-[...] -> --blur-<slug>
* drop-shadow-[...] -> --drop-shadow-extract-<n> (its own
* family, separate from Batch 3's
* --shadow-extract-* box-shadow
* family — filter vs. box-shadow are
* different CSS properties, see
* TOKEN-AUDIT.md Batch 3 "Needs human
* decision")
* bg-[linear|radial|conic-gradient(...)] -> --gradient-extract-<n>,
* CONTINUING Batch 1's counter (not
* restarting it) - 2 sites that use
* hsl(var(...))/color-mix(...)
* CSS-native color functions rather
* than raw hex/rgb literals, so
* Batch 1's hand-audited color table
* did not catch them even though
* they are the same gradient-bracket
* shape.
* bg|text|border-[var(--x)] -> bare paren passthrough, no new
* (no fallback) token minted (same rule as Batch
* 3's var()-only case)
* bg|text|border-[var(--x,fallback)] -> mints a --*-resolved wrapper token
* (fallback form) whose value is `var(--x, fallback)`
* verbatim (paren-with-fallback-comma
* does not parse in Tailwind v4 —
* confirmed in the Step 0 spike --
* so the fallback expression itself
* must live in the CSS token, not in
* the utility). theme(colors.a.b)
* fallbacks are resolved to their
* BUILT-CSS equivalent var(...) form
* first (inspected from
* storybook-static output before
* writing this codemod - theme() is
* a Tailwind build-time function).
*
* NOT rewritten (documented, not a bug):
* - Selector/variant brackets (data-[...], group-data-[...], has-[...],
* aria-[...], supports-[...], etc.) are CSS selector conditions, not
* visual values - out of scope for this codemod BY DEFINITION (see
* check-token-gates.mjs header for the same distinction, gate 2).
* - tw-animate/animate-plugin arbitrary utilities (zoom-in-[0.97],
* zoom-out-[0.97], slide-in-from-top-[1%], slide-out-to-top-[1%]) plus
* their siblings (animate-in, animate-out, fade-in-0, fade-out-0):
* confirmed via the Step 0 spike that NONE of these compile to any CSS
* at all in this repo's build (no tw-animate-css plugin is installed,
* and no @utility overrides exist in index.css) - grep of the built
* storybook-static CSS shows zero occurrences of "animate-in", "zoom",
* "slide-in-from-top", etc. These are dead, no-op class names with zero
* rendered visual value today, so there is nothing to tokenize; touching
* them would not preserve OR change any pixel. Left untouched and
* allowlisted (third-party plugin syntax, retained for whenever
* tw-animate-css is actually installed) rather than silently deleted,
* since deleting dead classes is itself a (no-op but non-mechanical)
* edit outside this batch's mandate.
* - max-[480px] / min-[420px] breakpoint variants and rounded-[inherit]:
* documented allowlist entries, not code the codemod touches (variant
* position cannot reference a CSS custom property; `inherit` is a
* keyword). See ALLOWLIST block in index.css.
*
* Idempotent: the FIND regexes only match the ORIGINAL bracket-literal form;
* once rewritten the pattern no longer matches, so re-running is a no-op.
*
* Usage: node scripts/codemod-extract-misc.mjs [--check]
* --check Report what WOULD change without writing files (dry run).
*/
import { readFileSync, writeFileSync, readdirSync } 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);
}
}
// Tailwind bracket-escaping uses `_` for literal spaces; reverse it before
// writing into a real CSS custom property value (Batch 1's gradient gotcha,
// generalized in Batch 3, applies again here for grid track lists / calc
// expressions embedded in transition/ease values).
function unescapeSpaces(value) {
return value.replace(/_/g, " ");
}
// "320" "0.7rem" "-50" "auto_minmax(0,1fr)" etc -> safe token-name suffix.
// Dots become underscores, spaces/commas/parens/percent become hyphens,
// collapsed and trimmed (matches Batch 1-3's `--fs-0_7rem`-style convention
// for numeric suffixes; grid/transition/ease values use a slug instead since
// their content isn't a single number).
function slugify(value) {
return value
.trim()
.replace(/_/g, " ")
.replace(/\./g, "_")
.replace(/[^a-zA-Z0-9_]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
}
// ── Token registries ────────────────────────────────────────────────────
const gtcTokens = new Map(); // --gtc-<n> grid-template-columns
const gtrTokens = new Map(); // --gtr-<n> grid-template-rows
const tpTokens = new Map(); // --tp-<slug> transition-property lists
const zTokens = new Map(); // --z-<n>
const sTokens = new Map(); // --s-<slug> scale
const eTokens = new Map(); // --e-<slug> ease / timing-function
const vaTokens = new Map(); // --va-<slug> vertical-align
const swTokens = new Map(); // --sw-<slug> stroke-width
const blurTokens = new Map(); // --blur-<slug>
const dropShadowTokens = new Map(); // --drop-shadow-extract-<n>
const resolvedVarTokens = new Map(); // --*-resolved wrapper tokens (fallback var() forms)
// --gradient-extract-<n> continuing Batch 1's family/counter (these 2 sites
// use hsl(var(...))/color-mix(...) CSS-native color functions rather than
// raw hex/rgb literals, which is why Batch 1's hand-audited color-literal
// table did not catch them - they're still a value-bearing gradient bracket,
// in scope for this batch's "remaining bracket utilities" sweep).
const gradientTokens = new Map();
let gtcCounter = 0;
let gtrCounter = 0;
let dropShadowCounter = 0;
// Batch 1 minted --gradient-extract-1 through --gradient-extract-24; this
// batch's new gradient sites continue that numbering, not restart it.
let gradientCounter = 24;
const gtcByValue = new Map();
function registerGtcToken(unescapedValue, sourceNote) {
if (gtcByValue.has(unescapedValue)) return gtcByValue.get(unescapedValue);
gtcCounter += 1;
const name = `gtc-${gtcCounter}`;
gtcTokens.set(name, { value: unescapedValue, comment: sourceNote });
gtcByValue.set(unescapedValue, name);
return name;
}
const gtrByValue = new Map();
function registerGtrToken(unescapedValue, sourceNote) {
if (gtrByValue.has(unescapedValue)) return gtrByValue.get(unescapedValue);
gtrCounter += 1;
const name = `gtr-${gtrCounter}`;
gtrTokens.set(name, { value: unescapedValue, comment: sourceNote });
gtrByValue.set(unescapedValue, name);
return name;
}
function registerTpToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue.replace(/,/g, " "));
const name = `tp-${slug}`;
if (!tpTokens.has(name)) tpTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerZToken(rawNum, sourceNote) {
const name = `z-${rawNum}`;
if (!zTokens.has(name)) zTokens.set(name, { value: rawNum, comment: sourceNote });
return name;
}
function registerSToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `s-${slug}`;
if (!sTokens.has(name)) sTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerEToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `e-${slug}`;
if (!eTokens.has(name)) eTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerVaToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `va-${slug}`;
if (!vaTokens.has(name)) vaTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerSwToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `sw-${slug}`;
if (!swTokens.has(name)) swTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerBlurToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `blur-${slug}`;
if (!blurTokens.has(name)) blurTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
const gradientByValue = new Map();
function registerGradientToken(unescapedValue, sourceNote) {
if (gradientByValue.has(unescapedValue)) return gradientByValue.get(unescapedValue);
gradientCounter += 1;
const name = `gradient-extract-${gradientCounter}`;
gradientTokens.set(name, { value: unescapedValue, comment: sourceNote });
gradientByValue.set(unescapedValue, name);
return name;
}
const dropShadowByValue = new Map();
function registerDropShadowToken(unescapedValue, sourceNote) {
if (dropShadowByValue.has(unescapedValue)) return dropShadowByValue.get(unescapedValue);
dropShadowCounter += 1;
const name = `drop-shadow-extract-${dropShadowCounter}`;
dropShadowTokens.set(name, { value: unescapedValue, comment: sourceNote });
dropShadowByValue.set(unescapedValue, name);
return name;
}
// Wrapper tokens for the var(--x, fallback) forms that can't use the
// paren-with-fallback-comma shorthand (confirmed unsupported in the Step 0
// spike). theme(colors.a.b) fallbacks are resolved to the equivalent
// var(--token) form the Tailwind build already resolves them to today
// (inspected in ui/storybook-static/assets/*.css before writing this
// codemod - theme() is a Tailwind build-time function and cannot appear
// inside a runtime custom property).
const THEME_COLOR_MAP = {
"theme(colors.muted.DEFAULT)": "var(--muted)",
"theme(colors.muted.foreground)": "var(--muted-foreground)",
};
function resolveThemeColor(raw) {
let out = raw;
for (const [from, to] of Object.entries(THEME_COLOR_MAP)) {
out = out.split(from).join(to);
}
return out;
}
function registerResolvedVarToken(varName, fallbackRaw, sourceNote) {
// varName like "--paperclip-code-highlight-bg" -> token name
// "code-highlight-bg-resolved" (strip the leading "--paperclip-" prefix
// for readability, matching the mission's suggested name for the first
// site; other vars in the same family follow the same convention).
const bare = varName.replace(/^--paperclip-/, "").replace(/^--/, "");
const name = `${bare}-resolved`;
const fallback = resolveThemeColor(fallbackRaw);
const value = `var(${varName}, ${fallback})`;
if (!resolvedVarTokens.has(name)) resolvedVarTokens.set(name, { value, comment: sourceNote });
return name;
}
// ── Regexes ──────────────────────────────────────────────────────────────
// Every regex requires the utility to start at a genuine class-token
// boundary (preceded by whitespace, a quote/backtick, template-literal `${`,
// or start-of-string) - see Batch 3's BOUNDARY GOTCHA. `:` is included for
// variant prefixes (`data-[state=open]:`, `sm:`, etc.).
const BOUNDARY = String.raw`(?<=^|[\s"'\`{:])`;
const GRID_COLS_RE = new RegExp(`${BOUNDARY}(!?)grid-cols-\\[([^\\]]+)\\]`, "g");
const GRID_ROWS_RE = new RegExp(`${BOUNDARY}(!?)grid-rows-\\[([^\\]]+)\\]`, "g");
const TRANSITION_RE = new RegExp(`${BOUNDARY}(!?)transition-\\[([^\\]]+)\\]`, "g");
const Z_RE = new RegExp(`${BOUNDARY}(!?)z-\\[([0-9]+)\\]`, "g");
const SCALE_RE = new RegExp(`${BOUNDARY}(!?)scale-\\[([^\\]]+)\\]`, "g");
const EASE_RE = new RegExp(`${BOUNDARY}(!?)ease-\\[([^\\]]+)\\]`, "g");
const ALIGN_RE = new RegExp(`${BOUNDARY}(!?)align-\\[([^\\]]+)\\]`, "g");
const STROKE_RE = new RegExp(`${BOUNDARY}(!?)stroke-\\[([^\\]]+)\\]`, "g");
// NOTE: negative lookbehind isn't reliably portable across regex engines at
// the boundary position used elsewhere, so drop-shadow is matched with its
// own explicit prefix (drop-shadow-) which never collides with bare blur-.
// Matches both bare `blur-[...]` and `backdrop-blur-[...]` (the latter's
// `backdrop-` prefix means `blur` doesn't start at a class-token BOUNDARY,
// so it needs its own alternative rather than relying on the shared
// boundary-anchored pattern).
const BLUR_RE = new RegExp(`${BOUNDARY}(!?)(backdrop-blur|blur)-\\[([^\\]]+)\\]`, "g");
const DROP_SHADOW_RE = new RegExp(`${BOUNDARY}(!?)drop-shadow-\\[([^\\]]+)\\]`, "g");
// bg-[linear-gradient(...)] / bg-[radial-gradient(...)] / bg-[conic-gradient(...)]
// - continuing Batch 1's --gradient-extract-* family (see registerGradientToken).
// Matched separately from the generic VAR_* regexes below since these are not
// var() passthrough; they need the `image:` paren hint (Batch 1 convention).
const GRADIENT_RE = new RegExp(
`${BOUNDARY}(!?)bg-\\[((?:linear|radial|conic)-gradient\\([^\\]]*\\))\\]`,
"g",
);
// bg|text|border-[var(--x)] (no fallback) and bg|text|border-[var(--x,fallback)]
// (with fallback). The fallback variant's raw capture includes everything up
// to the matching `)]` - since none of these fallback values contain nested
// brackets, a simple `[^\]]+` capture is safe (verified by inspection of all
// matching sites in this batch).
const VAR_NOFALLBACK_RE = new RegExp(
`${BOUNDARY}(!?)(bg|text|border)-\\[var\\((--[a-zA-Z0-9-]+)\\)\\]`,
"g",
);
const VAR_FALLBACK_RE = new RegExp(
`${BOUNDARY}(!?)(bg|text|border)-\\[var\\((--[a-zA-Z0-9-]+),([^\\]]+)\\)\\]`,
"g",
);
function rewriteFile(filePath, relPath) {
const original = readFileSync(filePath, "utf8");
let content = original;
let siteCount = 0;
content = content.replace(GRID_COLS_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (grid-cols-[${raw}]).`;
const name = registerGtcToken(unescaped, sourceNote);
siteCount++;
return `${bang}grid-cols-(--${name})`;
});
content = content.replace(GRID_ROWS_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (grid-rows-[${raw}]).`;
const name = registerGtrToken(unescaped, sourceNote);
siteCount++;
return `${bang}grid-rows-(--${name})`;
});
content = content.replace(TRANSITION_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (transition-[${raw}]).`;
const name = registerTpToken(unescaped, sourceNote);
siteCount++;
return `${bang}transition-(--${name})`;
});
content = content.replace(Z_RE, (match, bang, raw) => {
const sourceNote = `Extracted from ${relPath} (z-[${raw}]).`;
const name = registerZToken(raw, sourceNote);
siteCount++;
return `${bang}z-(--${name})`;
});
content = content.replace(SCALE_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (scale-[${raw}]).`;
const name = registerSToken(unescaped, sourceNote);
siteCount++;
return `${bang}scale-(--${name})`;
});
content = content.replace(EASE_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (ease-[${raw}]).`;
const name = registerEToken(unescaped, sourceNote);
siteCount++;
return `${bang}ease-(--${name})`;
});
content = content.replace(ALIGN_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (align-[${raw}]).`;
const name = registerVaToken(unescaped, sourceNote);
siteCount++;
return `${bang}align-(--${name})`;
});
content = content.replace(STROKE_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (stroke-[${raw}]).`;
const name = registerSwToken(unescaped, sourceNote);
siteCount++;
// stroke-WIDTH requires the `length:` hint (bare stroke-(--x) is
// ambiguous with the stroke-COLOR utility) - confirmed in Step 0 spike.
return `${bang}stroke-(length:--${name})`;
});
content = content.replace(DROP_SHADOW_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (drop-shadow-[${raw}]).`;
const name = registerDropShadowToken(unescaped, sourceNote);
siteCount++;
return `${bang}drop-shadow-(--${name})`;
});
// Gradient bg-[...] brackets must be rewritten BEFORE the generic var()
// passthrough regexes below (a gradient value can itself contain
// `var(--x)` sub-expressions, e.g. hsl(var(--primary)), which the generic
// VAR_NOFALLBACK_RE must not also try to match against the outer bg-[...]).
content = content.replace(GRADIENT_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (bg-[${raw}]).`;
const name = registerGradientToken(unescaped, sourceNote);
siteCount++;
return `${bang}bg-(image:--${name})`;
});
content = content.replace(BLUR_RE, (match, bang, util, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (${util}-[${raw}]).`;
const name = registerBlurToken(unescaped, sourceNote);
siteCount++;
return `${bang}${util}-(--${name})`;
});
// var(--x, fallback) forms MUST be rewritten before the no-fallback form
// (which would otherwise partially match the `var(--x` prefix of a
// fallback expression and corrupt it - the fallback regex requires a
// literal comma so there's no real ambiguity, but ordering fallback-first
// keeps the intent explicit and avoids relying on regex engine match order).
content = content.replace(VAR_FALLBACK_RE, (match, bang, util, varName, fallbackRaw) => {
const sourceNote = `Extracted from ${relPath} (${util}-[var(${varName},${fallbackRaw})]).`;
const name = registerResolvedVarToken(varName, fallbackRaw, sourceNote);
siteCount++;
return `${bang}${util}-(--${name})`;
});
content = content.replace(VAR_NOFALLBACK_RE, (match, bang, util, varName) => {
siteCount++;
// Bare var() passthrough - no new token minted, per DESIGN.md/Batch 3's
// special case for runtime library/component variables. Here the vars
// already exist as first-class design tokens in index.css (--chip-match-*),
// so this is a pure syntax modernization, not a token mint.
return `${bang}${util}-(${varName})`;
});
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);
files.sort();
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 MISC tokens (Phase 2 Batch 4, design/token-extraction) ── */";
let cssNext = cssOriginal;
let cssChanged = false;
const anyTokens =
gtcTokens.size ||
gtrTokens.size ||
tpTokens.size ||
zTokens.size ||
sTokens.size ||
eTokens.size ||
vaTokens.size ||
swTokens.size ||
blurTokens.size ||
dropShadowTokens.size ||
resolvedVarTokens.size ||
gradientTokens.size;
if (!cssOriginal.includes(marker) && anyTokens) {
const lines = [];
lines.push(marker);
lines.push("/* Batch 4/4 (final sweep): grid track lists, transition-property");
lines.push(" lists, z-index, scale, easing, vertical-align, stroke-width, blur,");
lines.push(" drop-shadow, and half-migrated var(x, fallback) color forms,");
lines.push(" verbatim (no normalizing - the human scale-collapse decision comes");
lines.push(" later per DESIGN.md/TOKEN-AUDIT.md). --gtc-* and --gtr-* and --tp-* are");
lines.push(" sequentially numbered / slugged since their content (track lists,");
lines.push(" property lists) is not safely nameable by value alone.");
lines.push("");
lines.push(" drop-shadow (a CSS filter function) gets its own");
lines.push(" --drop-shadow-extract-* family, kept separate from Batch 3's");
lines.push(" --shadow-extract-* box-shadow family per that batch's logged");
lines.push(" human-decision note.");
lines.push("");
lines.push(" *-resolved wrapper tokens hold a verbatim var(--x, fallback)");
lines.push(" expression for sites where Tailwind v4's paren-with-fallback-comma");
lines.push(" shorthand does not parse (confirmed unsupported in this batch's");
lines.push(" Step 0 syntax spike). theme(colors.a.b) fallbacks were resolved to");
lines.push(" the equivalent var(--token) form the Tailwind build already");
lines.push(" compiles them to today (inspected byte-for-byte from the built");
lines.push(" storybook-static CSS before this codemod ran: theme(colors.muted.DEFAULT)");
lines.push(" -> var(--muted), theme(colors.muted.foreground) -> var(--muted-foreground)).");
lines.push("");
lines.push(" --gradient-extract-* here CONTINUES Batch 1's counter (Batch 1 minted");
lines.push(" 1 through 24) rather than restarting it - these 2 sites use");
lines.push(" hsl(var(...))/color-mix(...) CSS-native color functions rather than raw");
lines.push(" hex/rgb literals, which is why Batch 1's hand-audited color-literal");
lines.push(" table did not catch them, but they are still value-bearing gradient");
lines.push(" brackets in scope for this batch's final sweep.");
lines.push("");
lines.push(" Allowlist (sites intentionally left as-is - see ALLOWLIST doc-comment");
lines.push(" at the end of this file for the machine-readable list consumed by");
lines.push(" scripts/check-token-gates.mjs):");
lines.push(" allow ui/src/components/ui/dialog.tsx — tw-animate-css plugin utilities (zoom-in-[0.97] etc.) are dead/no-op classes today (plugin not installed, verified via built-CSS grep); nothing to tokenize without visually changing a currently-inert class");
lines.push(" allow ui/src/components/ui/alert-dialog.tsx — same tw-animate-css dead-class situation as dialog.tsx");
lines.push("*/");
lines.push(":root {");
for (const [name, { value, comment }] of gtcTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of gtrTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of tpTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of zTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of sTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of eTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of vaTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of swTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of blurTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of dropShadowTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of resolvedVarTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of gradientTokens) {
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-misc summary`);
console.log(` Sites rewritten: ${totalSites}`);
console.log(` Files changed: ${filesChanged}`);
console.log(` New --gtc-*: ${gtcTokens.size}`);
console.log(` New --gtr-*: ${gtrTokens.size}`);
console.log(` New --tp-*: ${tpTokens.size}`);
console.log(` New --z-*: ${zTokens.size}`);
console.log(` New --s-*: ${sTokens.size}`);
console.log(` New --e-*: ${eTokens.size}`);
console.log(` New --va-*: ${vaTokens.size}`);
console.log(` New --sw-*: ${swTokens.size}`);
console.log(` New --blur-*: ${blurTokens.size}`);
console.log(` New --drop-shadow-extract-*: ${dropShadowTokens.size}`);
console.log(` New *-resolved wrapper: ${resolvedVarTokens.size}`);
console.log(` New --gradient-extract-* (continuing Batch 1): ${gradientTokens.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();

View File

@ -268,7 +268,7 @@ function CollapsedFeedGroup({
onClick={() => setExpanded((e) => !e)}
data-fc="card"
className={cn(
"group ml-3 mr-3 md:ml-0 my-2 flex w-(--sz-calc-1) md:w-(--sz-calc-2) items-center gap-2 rounded-lg border bg-card p-(--sz-18px) text-left text-xs transition-[background-color,border-color] duration-150",
"group ml-3 mr-3 md:ml-0 my-2 flex w-(--sz-calc-1) md:w-(--sz-calc-2) items-center gap-2 rounded-lg border bg-card p-(--sz-18px) text-left text-xs transition-(--tp-background-color-border-color) duration-150",
"cursor-pointer hover:bg-accent hover:border-muted-foreground/30",
)}
>

View File

@ -102,7 +102,7 @@ export function BudgetPolicyCard({
<div className={cn("h-2 overflow-hidden rounded-full", isPlain ? "bg-border/70" : "bg-muted/70")}>
<div
className={cn(
"h-full rounded-full transition-[width,background-color] duration-200",
"h-full rounded-full transition-(--tp-width-background-color) duration-200",
summary.status === "hard_stop"
? "bg-red-400"
: summary.status === "warning"

View File

@ -250,7 +250,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(fu
{isDragOver && canAttach ? (
<div
data-testid="chat-composer-drop-overlay"
className="pointer-events-none absolute inset-1.5 z-20 flex items-center justify-center rounded-lg border border-dashed border-muted-foreground/50 bg-background/80 text-xs text-muted-foreground backdrop-blur-[1px]"
className="pointer-events-none absolute inset-1.5 z-20 flex items-center justify-center rounded-lg border border-dashed border-muted-foreground/50 bg-background/80 text-xs text-muted-foreground backdrop-blur-(--blur-1px)"
>
<span className="inline-flex items-center gap-2">
<Paperclip className="h-3.5 w-3.5" />

View File

@ -127,7 +127,7 @@ export function ClaudeSubscriptionPanel({
<div className="mt-3 h-2 overflow-hidden bg-muted">
<div
className={cn("h-full transition-[width] duration-200", fillClass(window.usedPercent))}
className={cn("h-full transition-(--tp-width) duration-200", fillClass(window.usedPercent))}
style={{ width: `${width}%` }}
/>
</div>

View File

@ -148,7 +148,7 @@ function QuotaWindowRow({ window }: { window: QuotaWindow }) {
<div className="mt-3 h-2 overflow-hidden bg-muted">
<div
className={cn("h-full transition-[width] duration-200", fillClass(window.usedPercent))}
className={cn("h-full transition-(--tp-width) duration-200", fillClass(window.usedPercent))}
style={{ width: `${Math.max(0, Math.min(100, window.usedPercent))}%` }}
/>
</div>

View File

@ -209,7 +209,7 @@ export function CompanyPatternIcon({
<div className="absolute inset-0 bg-muted" />
)}
{!logo && (
<span className="relative z-10 drop-shadow-[0_1px_2px_rgba(0,0,0,0.65)]">
<span className="relative z-10 drop-shadow-(--drop-shadow-extract-1)">
{initial}
</span>
)}

View File

@ -470,7 +470,7 @@ export function DocumentAnnotationLayer({
</div>
) : null}
<div
className="paperclip-doc-annotation-layer pointer-events-none absolute inset-0 z-[2]"
className="paperclip-doc-annotation-layer pointer-events-none absolute inset-0 z-(--z-2)"
aria-hidden="true"
>
<div ref={overlayRef} className="relative h-full w-full">

View File

@ -71,7 +71,7 @@ export function DocumentAnnotationPanel(props: AnnotationPanelProps) {
<SheetContent
side="bottom"
showCloseButton={false}
className="paperclip-doc-annotation-sheet z-[60] flex max-h-(--sz-88vh) flex-col rounded-none border-t border-border bg-popover p-0 text-popover-foreground shadow-2xl"
className="paperclip-doc-annotation-sheet z-(--z-60) flex max-h-(--sz-88vh) flex-col rounded-none border-t border-border bg-popover p-0 text-popover-foreground shadow-2xl"
>
<SheetTitle className="sr-only">
Comments on {props.documentKey} revision {props.documentRevisionNumber}

View File

@ -142,7 +142,7 @@ export function DocumentDiffModal({
<div className="p-6 text-center text-muted-foreground text-sm">Both sides are the same revision.</div>
) : (
<div className="font-mono text-(length:--fs-12) leading-6">
<div className="grid grid-cols-[56px_56px_24px_minmax(0,1fr)] border-b border-border/60 bg-muted/30 px-3 py-2 text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">
<div className="grid grid-cols-(--gtc-1) border-b border-border/60 bg-muted/30 px-3 py-2 text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">
<span>Old</span>
<span>New</span>
<span />
@ -151,7 +151,7 @@ export function DocumentDiffModal({
{diffRows.map((row, index) => (
<div
key={`${row.kind}-${index}-${row.oldLineNumber ?? "x"}-${row.newLineNumber ?? "x"}`}
className={`grid grid-cols-[56px_56px_24px_minmax(0,1fr)] gap-0 border-b border-border/30 px-3 ${lineClassesByKind[row.kind]}`}
className={`grid grid-cols-(--gtc-1) gap-0 border-b border-border/30 px-3 ${lineClassesByKind[row.kind]}`}
>
<span className="select-none border-r border-border/30 pr-3 text-right text-muted-foreground">
{row.oldLineNumber ?? ""}

View File

@ -64,7 +64,7 @@ export function ExternalObjectStatusIcon({
// The clock overlay needs a positioned wrapper. Inline mode keeps the icon
// tight to the surrounding text; pill mode expects to size by sizeClassName.
const wrapperBase = inline
? "relative mr-1 inline-flex shrink-0 align-[-0.125em]"
? "relative mr-1 inline-flex shrink-0 align-(--va-0_125em)"
: "relative inline-flex shrink-0";
const iconSize = inline ? "h-3 w-3" : sizeClassName;
const isSpinner = category === "running";

View File

@ -437,7 +437,7 @@ export function FeedCard({
data-fc="card"
className={cn(
"group ml-3 mr-3 md:ml-0 my-2 flex items-center gap-2 rounded-lg border bg-card p-(--sz-18px) text-xs",
"transition-[background-color,border-color] duration-150",
"transition-(--tp-background-color-border-color) duration-150",
content.link && "cursor-pointer hover:bg-accent hover:border-muted-foreground/30",
className,
)}

View File

@ -417,8 +417,8 @@ export function FileTree({
className={cn(
node.kind === "dir"
? showCheckboxes
? "group grid w-full grid-cols-[auto_minmax(0,1fr)_2.25rem] items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground"
: "group grid w-full grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground max-[480px]:grid-cols-[minmax(0,1fr)]"
? "group grid w-full grid-cols-(--gtc-2) items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground"
: "group grid w-full grid-cols-(--gtc-3) items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground max-[480px]:grid-cols-(--gtc-4)"
: "group flex w-full items-center gap-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground cursor-pointer",
TREE_ROW_HEIGHT_CLASS,
isSelected && "text-foreground bg-accent/20",
@ -478,7 +478,7 @@ export function FileTree({
{node.kind === "dir" && (
<button
type="button"
className="flex h-9 w-9 items-center justify-center self-center rounded-sm text-muted-foreground opacity-70 transition-[background-color,color,opacity] hover:bg-accent hover:text-foreground group-hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50 max-[480px]:hidden"
className="flex h-9 w-9 items-center justify-center self-center rounded-sm text-muted-foreground opacity-70 transition-(--tp-background-color-color-opacity) hover:bg-accent hover:text-foreground group-hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50 max-[480px]:hidden"
onClick={(event) => {
event.stopPropagation();
onToggleDir(node.path);

View File

@ -354,7 +354,7 @@ export function FileContentViewer({ content, highlightedLine, onLoaded }: FileCo
role="region"
aria-label={`${resource.title} source`}
tabIndex={0}
className="paperclip-file-viewer-code flex-1 overflow-auto bg-[var(--paperclip-code-bg,theme(colors.muted.DEFAULT))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
className="paperclip-file-viewer-code flex-1 overflow-auto bg-(--code-bg-resolved) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
>
<pre className="m-0 font-mono text-xs leading-5">
{lines.map((lineText, index) => {
@ -366,18 +366,20 @@ export function FileContentViewer({ content, highlightedLine, onLoaded }: FileCo
ref={isHighlighted ? highlightedLineRef : undefined}
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))]",
"grid grid-cols-(--gtc-5)",
// Batch 4 resolved the former half-migrated var(--x, fallback)
// pattern here into --code-highlight-bg-resolved (see
// ui/src/index.css MISC token block + TOKEN-AUDIT.md Batch 4 log).
isHighlighted && "bg-(--code-highlight-bg-resolved)",
)}
>
<span
aria-hidden="true"
className={cn(
"sticky left-0 z-10 shrink-0 select-none pl-3 pr-4 text-right text-[var(--paperclip-code-gutter-fg,theme(colors.muted.foreground))] opacity-70",
"bg-[var(--paperclip-code-bg,theme(colors.muted.DEFAULT))]",
"sticky left-0 z-10 shrink-0 select-none pl-3 pr-4 text-right text-(--code-gutter-fg-resolved) opacity-70",
"bg-(--code-bg-resolved)",
isHighlighted &&
"opacity-100 bg-[var(--paperclip-code-highlight-bg,rgba(250,204,21,0.12))] border-l-2 border-[var(--paperclip-code-highlight-border,rgb(234,179,8))]",
"opacity-100 bg-(--code-highlight-bg-resolved) border-l-2 border-(--code-highlight-border-resolved)",
)}
style={{ width: gutterWidth, minWidth: gutterWidth }}
>
@ -747,7 +749,7 @@ export function FileViewerSheet({
}}
>
<DialogHeader className="border-b border-border gap-1 p-3">
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-2">
<div className="grid min-w-0 grid-cols-(--gtc-6) items-start gap-2">
{browseMode ? (
<FolderSearch aria-hidden="true" className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
) : (

View File

@ -102,7 +102,7 @@ export function FoldCurtain({
ref={contentRef}
className={cn(
"fold-curtain__content relative overflow-hidden",
allowTransition && "motion-safe:transition-[max-height] motion-reduce:transition-none",
allowTransition && "motion-safe:transition-(--tp-max-height) motion-reduce:transition-none",
contentClassName,
)}
style={{

View File

@ -2397,7 +2397,7 @@ function StaleDispositionWarningMetadataRow({ row }: { row: SystemNoticeMetadata
})();
return (
<div className="grid grid-cols-[7.5rem_minmax(0,1fr)] gap-2 text-xs leading-5">
<div className="grid grid-cols-(--gtc-7) gap-2 text-xs leading-5">
{label}
<div className="min-w-0 break-words text-foreground/80">{value}</div>
</div>
@ -3852,7 +3852,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
data-testid="issue-chat-composer"
data-pending-work-mode={pendingWorkMode}
className={cn(
"relative rounded-md border border-border/70 bg-background/95 p-(--sz-15px) shadow-(--shadow-extract-4) backdrop-blur transition-[border-color,background-color,box-shadow] duration-150 supports-[backdrop-filter]:bg-background/85 dark:shadow-(--shadow-extract-5)",
"relative rounded-md border border-border/70 bg-background/95 p-(--sz-15px) shadow-(--shadow-extract-4) backdrop-blur transition-(--tp-border-color-background-color-box-shadow) duration-150 supports-[backdrop-filter]:bg-background/85 dark:shadow-(--shadow-extract-5)",
pendingWorkModeMeta.classes.container,
isDragOver && "border-primary/45 bg-background shadow-(--shadow-extract-7)",
)}
@ -3865,7 +3865,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
{isDragOver && canAcceptFiles ? (
<div
data-testid="issue-chat-composer-drop-overlay"
className="pointer-events-none absolute inset-2 z-30 flex items-center justify-center rounded-sm border border-dashed border-primary/55 bg-background/75 px-4 py-3 text-center shadow-sm backdrop-blur-[2px] dark:bg-background/65"
className="pointer-events-none absolute inset-2 z-30 flex items-center justify-center rounded-sm border border-dashed border-primary/55 bg-background/75 px-4 py-3 text-center shadow-sm backdrop-blur-(--blur-2px) dark:bg-background/65"
>
<div className="flex max-w-md items-center gap-3 rounded-md bg-background/80 px-3 py-2 text-left shadow-sm ring-1 ring-border/60">
<span className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">

View File

@ -306,7 +306,7 @@ describe("IssueDocumentAnnotations", () => {
const anchor = container.querySelector('[data-testid="document-annotation-panel-anchor"]');
expect(anchor).not.toBeNull();
expect(anchor?.className).toContain("fixed");
expect(anchor?.className).toContain("z-[60]");
expect(anchor?.className).toContain("z-(--z-60)");
});
it("keeps the desktop annotation panel inside the issue content area when properties are visible", async () => {
@ -982,7 +982,7 @@ describe("IssueDocumentAnnotations", () => {
expect(sheet).not.toBeNull();
expect(sheet?.getAttribute("data-side")).toBe("bottom");
expect(sheet?.className).toContain("paperclip-doc-annotation-sheet");
expect(sheet?.className).toContain("z-[60]");
expect(sheet?.className).toContain("z-(--z-60)");
expect(sheet?.className).toContain("bg-popover");
} finally {
Object.defineProperty(window, "matchMedia", {

View File

@ -344,7 +344,7 @@ export function IssueDocumentAnnotations({
className="relative min-w-0"
data-testid={`document-annotation-body-${doc.key}`}
>
<div className="relative z-[1]">
<div className="relative z-(--z-1)">
{children}
</div>
{!historicalPreview && doc.latestRevisionId ? (
@ -368,7 +368,7 @@ export function IssueDocumentAnnotations({
{panelOpen && !isMobile && renderedDesktopPanelFrame ? (
<div
data-testid="document-annotation-panel-anchor"
className="pointer-events-auto fixed z-[60] hidden lg:block"
className="pointer-events-auto fixed z-(--z-60) hidden lg:block"
style={{
left: renderedDesktopPanelFrame.left,
maxHeight: renderedDesktopPanelFrame.maxHeight,

View File

@ -219,7 +219,7 @@ function MetadataRow({
children: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[7.5rem_1fr] gap-x-3 gap-y-0 px-3 py-1.5 text-xs sm:px-4">
<div className="grid grid-cols-(--gtc-8) gap-x-3 gap-y-0 px-3 py-1.5 text-xs sm:px-4">
<dt className="truncate text-(length:--fs-11) font-medium uppercase tracking-(--ls-0_08) text-muted-foreground">
{label}
</dt>

View File

@ -171,7 +171,7 @@ function KanbanColumn({
}
return (
<div className={`flex flex-col shrink-0 transition-[width,min-width] ${isEmpty && !isOver ? "min-w-(--sz-48px) w-(--sz-48px)" : "min-w-(--sz-260px) w-(--sz-260px)"}`}>
<div className={`flex flex-col shrink-0 transition-(--tp-width-min-width) ${isEmpty && !isOver ? "min-w-(--sz-48px) w-(--sz-48px)" : "min-w-(--sz-260px) w-(--sz-260px)"}`}>
<div className={`flex items-center gap-2 px-2 py-2 mb-1 ${isEmpty && !isOver ? "justify-center" : ""}`}>
<StatusIcon status={status} />
{(!isEmpty || isOver) && (

View File

@ -508,7 +508,7 @@ export function Layout() {
>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-(--z-200) focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Skip to Main Content
</a>

View File

@ -433,7 +433,7 @@ describe("MarkdownBody", () => {
const html = renderMarkdown("[https://github.com/paperclipai/paperclip/pull/4099](https://github.com/paperclipai/paperclip/pull/4099)");
expect(html).toContain('<a href="https://github.com/paperclipai/paperclip/pull/4099"');
expect(html).toContain('class="lucide lucide-github mr-1 inline h-3.5 w-3.5 align-[-0.125em]"');
expect(html).toContain('class="lucide lucide-github mr-1 inline h-3.5 w-3.5 align-(--va-0_125em)"');
// The icon and first character "h" must sit in a no-wrap span so the
// icon can never be orphaned on the previous line from the URL text.
expect(html).toMatch(/<span style="white-space:nowrap">.*lucide-github.*?<\/svg>h<\/span>/);
@ -445,7 +445,7 @@ describe("MarkdownBody", () => {
const html = renderMarkdown("See https://github.com/paperclipai/paperclip/issues/1778");
expect(html).toContain('<a href="https://github.com/paperclipai/paperclip/issues/1778"');
expect(html).toContain('class="lucide lucide-github mr-1 inline h-3.5 w-3.5 align-[-0.125em]"');
expect(html).toContain('class="lucide lucide-github mr-1 inline h-3.5 w-3.5 align-(--va-0_125em)"');
});
it("does not prefix non-GitHub markdown links with the GitHub icon", () => {
@ -559,7 +559,7 @@ describe("MarkdownBody", () => {
// PAP-243b: the lg glyph is optically centered to the body text
// (vertical-align: middle + a 1px lift), not floating off the baseline.
expect(html).toContain("align-middle");
expect(html).not.toContain("align-[-0.125em]");
expect(html).not.toContain("align-(--va-0_125em)");
// Legacy h-3 w-3 sizing is gone.
expect(html).not.toContain("mr-1 h-3 w-3");
});

View File

@ -827,10 +827,10 @@ function MarkdownBodyImpl({
const isGitHubLink = isGitHubUrl(href);
const isExternal = isExternalHttpUrl(href);
const leadingIcon = isGitHubLink ? (
<Github aria-hidden="true" className="mr-1 inline h-3.5 w-3.5 align-[-0.125em]" />
<Github aria-hidden="true" className="mr-1 inline h-3.5 w-3.5 align-(--va-0_125em)" />
) : null;
const trailingIcon = isExternal && !isGitHubLink ? (
<ExternalLink aria-hidden="true" className="ml-1 inline h-3 w-3 align-[-0.125em]" />
<ExternalLink aria-hidden="true" className="ml-1 inline h-3 w-3 align-(--va-0_125em)" />
) : null;
return (
<a

View File

@ -1306,7 +1306,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
<div
data-paperclip-floating-ui=""
data-testid="mention-autocomplete-menu"
className="pointer-events-auto fixed z-[9999] min-w-(--sz-180px) max-w-(--sz-calc-15) max-h-(--sz-208px) overflow-y-auto rounded-md border border-border bg-popover shadow-md"
className="pointer-events-auto fixed z-(--z-9999) min-w-(--sz-180px) max-w-(--sz-calc-15) max-h-(--sz-208px) overflow-y-auto rounded-md border border-border bg-popover shadow-md"
style={{
top: mentionMenuPosition.top,
left: mentionMenuPosition.left,

View File

@ -106,7 +106,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
{({ isActive }) => (
<>
<span className="relative">
<Icon className={cn("h-(--sz-18px) w-(--sz-18px)", isActive && "stroke-[2.3]")} />
<Icon className={cn("h-(--sz-18px) w-(--sz-18px)", isActive && "stroke-(length:--sw-2_3)")} />
{item.badge != null && item.badge > 0 && (
<span className="absolute -right-2 -top-2 rounded-full bg-primary px-1.5 py-0.5 text-(length:--fs-10) leading-none text-primary-foreground">
{item.badge > 99 ? "99+" : item.badge}

View File

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

View File

@ -360,7 +360,7 @@ export function PipelineItemBodyDocument({
className="relative min-w-0"
data-testid="pipeline-item-body-unlinked"
>
<div className="relative z-[1]">{renderReadOnlyBody(displayedBody)}</div>
<div className="relative z-(--z-1)">{renderReadOnlyBody(displayedBody)}</div>
<DocumentAnnotationLayer
containerRef={containerRef}
markdown={displayedBody}

View File

@ -10,7 +10,7 @@ export function PropertiesPanel() {
return (
<aside
className="hidden md:flex border-l border-border bg-card flex-col shrink-0 overflow-hidden transition-[width,opacity] duration-200 ease-in-out h-full"
className="hidden md:flex border-l border-border bg-card flex-col shrink-0 overflow-hidden transition-(--tp-width-opacity) duration-200 ease-in-out h-full"
style={{ width: panelVisible ? 320 : 0, opacity: panelVisible ? 1 : 0 }}
>
<div className="w-80 flex-1 flex flex-col min-w-(--sz-320px) min-h-0">

View File

@ -205,7 +205,7 @@ export function ProviderQuotaCard({
</div>
<div className="h-2 w-full border border-border overflow-hidden">
<div
className="h-full bg-primary/60 transition-[width] duration-150"
className="h-full bg-primary/60 transition-(--tp-width) duration-150"
style={{ width: `${barPct}%` }}
/>
</div>
@ -242,7 +242,7 @@ export function ProviderQuotaCard({
<>
<div className="h-1.5 w-full border border-border overflow-hidden">
<div
className="h-full bg-primary/60 transition-[width] duration-150"
className="h-full bg-primary/60 transition-(--tp-width) duration-150"
style={{ width: `${subSharePct}%` }}
/>
</div>
@ -286,13 +286,13 @@ export function ProviderQuotaCard({
{/* token share bar */}
<div className="relative h-2 w-full border border-border overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-primary/60 transition-[width] duration-150"
className="absolute inset-y-0 left-0 bg-primary/60 transition-(--tp-width) duration-150"
style={{ width: `${tokenPct}%` }}
title={`${Math.round(tokenPct)}% of provider tokens`}
/>
{/* cost share overlay — narrower, opaque, shows relative cost weight */}
<div
className="absolute inset-y-0 left-0 bg-primary/85 transition-[width] duration-150"
className="absolute inset-y-0 left-0 bg-primary/85 transition-(--tp-width) duration-150"
style={{ width: `${costPct}%` }}
title={`${Math.round(costPct)}% of provider cost`}
/>
@ -356,7 +356,7 @@ export function ProviderQuotaCard({
{qw.usedPercent != null && fillColor != null && (
<div className="h-2 w-full border border-border overflow-hidden">
<div
className={`h-full transition-[width] duration-150 ${fillColor}`}
className={`h-full transition-(--tp-width) duration-150 ${fillColor}`}
style={{ width: `${qw.usedPercent}%` }}
/>
</div>

View File

@ -47,7 +47,7 @@ export function QuotaBar({
{/* fill */}
<div
className={cn(
"absolute inset-y-0 left-0 transition-[width,background-color] duration-150",
"absolute inset-y-0 left-0 transition-(--tp-width-background-color) duration-150",
fillColor(clampedPct),
)}
style={{ width: `${clampedPct}%` }}

View File

@ -184,7 +184,7 @@ export function RoutineHistoryTab({
if (revisionsQuery.isLoading) {
return (
<div className="grid gap-5 md:grid-cols-[300px_minmax(0,1fr)]">
<div className="grid gap-5 md:grid-cols-(--gtc-9)">
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, idx) => (
<Skeleton key={idx} className="h-10 w-full" />
@ -216,7 +216,7 @@ export function RoutineHistoryTab({
const onlyBootstrapRevision = revisions.length <= 1;
return (
<div className="grid gap-5 md:grid-cols-[300px_minmax(0,1fr)]">
<div className="grid gap-5 md:grid-cols-(--gtc-9)">
<RevisionList
revisions={visibleRevisions}
latestRevisionId={routine.latestRevisionId}
@ -951,7 +951,7 @@ function DiffTable({ rows }: { rows: DiffRow[] }) {
};
return (
<div className="rounded-md border border-border text-xs font-mono leading-6 overflow-hidden">
<div className="grid grid-cols-[56px_56px_24px_minmax(0,1fr)] border-b border-border/60 bg-muted/30 px-3 py-2 text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">
<div className="grid grid-cols-(--gtc-1) border-b border-border/60 bg-muted/30 px-3 py-2 text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">
<span>Old</span>
<span>New</span>
<span />
@ -960,7 +960,7 @@ function DiffTable({ rows }: { rows: DiffRow[] }) {
{rows.map((row, index) => (
<div
key={`${row.kind}-${index}-${row.oldLineNumber ?? "x"}-${row.newLineNumber ?? "x"}`}
className={`grid grid-cols-[56px_56px_24px_minmax(0,1fr)] gap-0 border-b border-border/30 px-3 ${lineClassesByKind[row.kind]}`}
className={`grid grid-cols-(--gtc-1) gap-0 border-b border-border/30 px-3 ${lineClassesByKind[row.kind]}`}
>
<span className="select-none border-r border-border/30 pr-3 text-right text-muted-foreground">
{row.oldLineNumber ?? ""}

View File

@ -74,7 +74,7 @@ export function ScrollToBottom() {
<button
onClick={scroll}
className={cn(
"fixed bottom-(--sz-calc-21) right-6 z-40 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background shadow-md hover:bg-accent transition-[background-color,right] duration-200 md:bottom-6",
"fixed bottom-(--sz-calc-21) right-6 z-40 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background shadow-md hover:bg-accent transition-(--tp-background-color-right) duration-200 md:bottom-6",
panelVisible && panelContent && "md:right-(--sz-calc-22)",
)}
aria-label="Scroll to bottom"

View File

@ -160,7 +160,7 @@ export function SidebarAccountMenu({
sideOffset={10}
className="w-(--sz-277px) max-w-(--sz-calc-24) overflow-hidden rounded-t-2xl rounded-b-none border-border p-0 shadow-2xl"
>
<div className="h-24 bg-[linear-gradient(135deg,hsl(var(--primary))_0%,hsl(var(--accent))_55%,hsl(var(--muted))_100%)]" />
<div className="h-24 bg-(image:--gradient-extract-25)" />
<div className="-mt-8 px-4 pb-4">
<div className="flex items-start gap-3">
<div className="rounded-2xl border-4 border-popover bg-popover p-0.5 shadow-sm">

View File

@ -195,7 +195,7 @@ describe("SidebarShell", () => {
// Open/close must be instant: the panel never animates its width, so neither
// the transition nor its reduced-motion fallback should be present.
expect(panel().className).not.toContain("transition-[width]");
expect(panel().className).not.toContain("transition-(--tp-width)");
expect(panel().className).not.toContain("motion-reduce:transition-none");
});
});

View File

@ -34,7 +34,7 @@ function MetaRow({
children: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[10rem_1fr] gap-x-3 gap-y-0 py-1 text-xs sm:grid-cols-[12rem_1fr]">
<div className="grid grid-cols-(--gtc-10) gap-x-3 gap-y-0 py-1 text-xs sm:grid-cols-(--gtc-11)">
<dt className="truncate text-(length:--fs-11) font-medium uppercase tracking-(--ls-0_08) text-emerald-900/70 dark:text-emerald-200/70">
{label}
</dt>

View File

@ -113,7 +113,7 @@ function formatTimestamp(ts: string) {
function MetadataRow({ row, tone }: { row: SystemNoticeMetadataRow; tone: ToneTokens }) {
return (
<div className="grid grid-cols-[7.5rem_1fr] gap-x-3 gap-y-0.5 px-3 py-1.5 text-xs">
<div className="grid grid-cols-(--gtc-8) gap-x-3 gap-y-0.5 px-3 py-1.5 text-xs">
<div className="truncate text-(length:--fs-11) font-medium uppercase tracking-(--ls-0_08) text-muted-foreground">
{row.label}
</div>
@ -289,7 +289,7 @@ export function SystemNotice({
aria-expanded={open}
aria-controls={detailsId}
className={cn(
"ml-1 inline-flex h-7 shrink-0 items-center gap-1 rounded-md border border-transparent px-2 text-(length:--fs-11) font-medium uppercase tracking-(--ls-0_12) text-muted-foreground transition-[background-color,border-color,color]",
"ml-1 inline-flex h-7 shrink-0 items-center gap-1 rounded-md border border-transparent px-2 text-(length:--fs-11) font-medium uppercase tracking-(--ls-0_12) text-muted-foreground transition-(--tp-background-color-border-color-color)",
"hover:border-border/70 hover:bg-background/70 hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
)}

View File

@ -40,7 +40,7 @@ function AnimatedToast({
return (
<li
className={cn(
"pointer-events-auto rounded-sm border shadow-lg backdrop-blur-xl transition-[transform,opacity] duration-200 ease-out",
"pointer-events-auto rounded-sm border shadow-lg backdrop-blur-xl transition-(--tp-transform-opacity) duration-200 ease-out",
visible
? "translate-y-0 opacity-100"
: "translate-y-3 opacity-0",
@ -89,7 +89,7 @@ export function ToastViewport() {
<aside
aria-live="polite"
aria-atomic="false"
className="pointer-events-none fixed bottom-3 left-3 z-[120] w-full max-w-sm px-1"
className="pointer-events-none fixed bottom-3 left-3 z-(--z-120) w-full max-w-sm px-1"
>
<ol className="flex w-full flex-col-reverse gap-2">
{toasts.map((toast) => (

View File

@ -148,7 +148,7 @@ export function TrustPresetSection({
</div>
{boundaryEditable ? (
<div className="rounded-md border border-border/70 bg-background/70 p-3 text-foreground space-y-3">
<div className="grid gap-3 sm:grid-cols-[minmax(0,0.75fr)_minmax(0,1fr)]">
<div className="grid gap-3 sm:grid-cols-(--gtc-12)">
<Field label="Boundary type">
<select
className={inputClass}

View File

@ -199,8 +199,8 @@ export function EnvironmentVariableRow({
return (
<div
className={cn(
"group/row grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-1.5 gap-y-1 rounded-md px-1 py-1",
"@[40rem]/env:grid-cols-[minmax(160px,2fr)_minmax(240px,3fr)_32px] @[40rem]/env:items-center",
"group/row grid grid-cols-(--gtc-13) items-start gap-x-1.5 gap-y-1 rounded-md px-1 py-1",
"@[40rem]/env:grid-cols-(--gtc-14) @[40rem]/env:items-center",
isDirty && "bg-amber-500/[0.06] ring-1 ring-amber-500/20",
)}
>
@ -419,7 +419,7 @@ export function EnvironmentVariableRow({
) : null}
</div>
) : (
<div className="grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto]">
<div className="grid min-w-0 flex-1 grid-cols-(--gtc-13)">
{userSecretsEnabled ? (
<select
aria-label="User secret"

View File

@ -386,7 +386,7 @@ export const EnvironmentVariablesEditor = forwardRef<EnvironmentVariablesEditorH
{hasRows ? (
<>
{/* Header (desktop only) */}
<div className="hidden gap-x-1.5 @[40rem]/env:grid @[40rem]/env:grid-cols-[minmax(160px,2fr)_minmax(240px,3fr)_32px]">
<div className="hidden gap-x-1.5 @[40rem]/env:grid @[40rem]/env:grid-cols-(--gtc-14)">
<span className="text-(length:--fs-11) font-medium uppercase tracking-wide text-muted-foreground">Name</span>
<span className="text-(length:--fs-11) font-medium uppercase tracking-wide text-muted-foreground">Value</span>
<span />

View File

@ -1060,7 +1060,7 @@ export function IssueProperties({
</span>
) : null}
</div>
<dl className="grid grid-cols-[6rem_1fr] gap-y-1">
<dl className="grid grid-cols-(--gtc-15) gap-y-1">
{scheduledRetryReasonLabel ? (
<>
<dt className="text-muted-foreground">Reason</dt>

View File

@ -4,13 +4,13 @@ export type MatchSourceChipKind = "title" | "identifier" | "comment" | "document
const chipStyles: Record<MatchSourceChipKind, string> = {
title:
"bg-[var(--chip-match-title-bg)] text-[var(--chip-match-title-fg)] border-[var(--chip-match-title-border)]",
"bg-(--chip-match-title-bg) text-(--chip-match-title-fg) border-(--chip-match-title-border)",
identifier:
"bg-[var(--chip-match-identifier-bg)] text-[var(--chip-match-identifier-fg)] border-[var(--chip-match-identifier-border)]",
"bg-(--chip-match-identifier-bg) text-(--chip-match-identifier-fg) border-(--chip-match-identifier-border)",
comment:
"bg-[var(--chip-match-comment-bg)] text-[var(--chip-match-comment-fg)] border-[var(--chip-match-comment-border)]",
"bg-(--chip-match-comment-bg) text-(--chip-match-comment-fg) border-(--chip-match-comment-border)",
document:
"bg-[var(--chip-match-document-bg)] text-[var(--chip-match-document-fg)] border-[var(--chip-match-document-border)]",
"bg-(--chip-match-document-bg) text-(--chip-match-document-fg) border-(--chip-match-document-border)",
};
const chipLabels: Record<MatchSourceChipKind, string> = {

View File

@ -1481,7 +1481,7 @@ function RawTranscriptView({
key={`${entry.kind}-${entry.ts}-${range.start + idx}`}
className={cn(
"grid gap-x-3",
"grid-cols-[auto_1fr]",
"grid-cols-(--gtc-16)",
)}
>
<span className="text-(length:--fs-10) uppercase tracking-(--ls-0_18) text-muted-foreground">

View File

@ -48,7 +48,7 @@ function AlertDialogContent({
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] fixed top-(--sz-calc-26) md:top-(--pct-50) left-(--pct-50) z-50 grid w-full max-w-(--sz-calc-25) translate-x-(--pct-neg-50) translate-y-0 md:translate-y-(--pct-neg-50) gap-4 rounded-lg border p-6 shadow-lg duration-150 ease-[cubic-bezier(0.16,1,0.3,1)] outline-none sm:max-w-lg [&>*]:min-w-0",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] fixed top-(--sz-calc-26) md:top-(--pct-50) left-(--pct-50) z-50 grid w-full max-w-(--sz-calc-25) translate-x-(--pct-neg-50) translate-y-0 md:translate-y-(--pct-neg-50) gap-4 rounded-lg border p-6 shadow-lg duration-150 ease-(--e-cubic-bezier-0_16-1-0_3-1) outline-none sm:max-w-lg [&>*]:min-w-0",
className
)}
{...props}

View File

@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-(--tp-color-box-shadow) overflow-hidden",
{
variants: {
variant: {

View File

@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-(--tp-color-background-color-border-color-box-shadow-opacity) disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {

View File

@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
"@container/card-header grid auto-rows-min grid-rows-(--gtr-1) items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-(--gtc-17) [.border-b]:pb-6",
className
)}
{...props}

View File

@ -59,7 +59,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] data-[state=closed]:slide-out-to-top-[1%] data-[state=open]:slide-in-from-top-[1%] fixed top-(--sz-calc-26) md:top-(--pct-50) left-(--pct-50) z-50 grid w-full max-w-(--sz-calc-25) translate-x-(--pct-neg-50) translate-y-0 md:translate-y-(--pct-neg-50) gap-4 rounded-lg border p-6 shadow-lg duration-150 ease-[cubic-bezier(0.16,1,0.3,1)] outline-none sm:max-w-lg [&>*]:min-w-0",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] data-[state=closed]:slide-out-to-top-[1%] data-[state=open]:slide-in-from-top-[1%] fixed top-(--sz-calc-26) md:top-(--pct-50) left-(--pct-50) z-50 grid w-full max-w-(--sz-calc-25) translate-x-(--pct-neg-50) translate-y-0 md:translate-y-(--pct-neg-50) gap-4 rounded-lg border p-6 shadow-lg duration-150 ease-(--e-cubic-bezier-0_16-1-0_3-1) outline-none sm:max-w-lg [&>*]:min-w-0",
className
)}
{...props}

View File

@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-(--tp-color-box-shadow) outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3)",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className

View File

@ -33,7 +33,7 @@ export function RadioCard({
data-state={selected ? "checked" : "unchecked"}
className={cn(
"group relative flex w-full flex-col items-start gap-1 rounded-md border px-4 py-3 text-left transition-colors",
"motion-safe:transition-[border-color,background-color] motion-safe:duration-150",
"motion-safe:transition-(--tp-border-color-background-color) motion-safe:duration-150",
selected
? "border-primary bg-primary/5 ring-1 ring-primary"
: "border-border hover:border-border hover:bg-accent/40",

View File

@ -16,7 +16,7 @@ function ScrollArea({
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="flex-1 min-h-0 w-full focus-visible:ring-ring/50 rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-(length:--rad-3) focus-visible:outline-1 [&>div]:!block"
className="flex-1 min-h-0 w-full focus-visible:ring-ring/50 rounded-[inherit] transition-(--tp-color-box-shadow) outline-none focus-visible:ring-(length:--rad-3) focus-visible:outline-1 [&>div]:!block"
>
{children}
</ScrollAreaPrimitive.Viewport>

View File

@ -35,7 +35,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base md:text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-(length:--rad-3) disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base md:text-sm whitespace-nowrap shadow-xs transition-(--tp-color-box-shadow) outline-none focus-visible:ring-(length:--rad-3) disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@ -62,7 +62,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-(--sz-calc-27) flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow] group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-(length:--rad-3) focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-(--sz-calc-27) flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-(--tp-color-background-color-border-color-box-shadow) group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-(length:--rad-3) focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-(--sz-neg-5px) group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",

View File

@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 min-w-0 w-full max-w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-(length:--rad-3) disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 min-w-0 w-full max-w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-(--tp-color-box-shadow) outline-none focus-visible:ring-(length:--rad-3) disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}

View File

@ -1444,7 +1444,7 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
- 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.
- components/FileViewerSheet.tsx — half-migrated var(--paperclip-code-highlight-*, fallback) pattern at the time of Batch 1; RESOLVED in Batch 4 via *-resolved wrapper tokens (see the Batch 4 MISC token block below) — no longer allowlisted.
- pages/InviteUxLab.tsx (brandColor prop, x2) — demo/showcase-only prop feeding CompanyPatternIcon's hexToHue() color math, not a rendered CSS value.
*/
:root {
@ -1710,3 +1710,202 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
--shadow-extract-22: 0 28px 70px rgba(15,23,42,0.10); /* Extracted from ui/src/pages/RunTranscriptUxLab.tsx (shadow-[0_28px_70px_rgba(15,23,42,0.10)]). */
--shadow-extract-23: 0 12px 24px rgba(6,182,212,0.12); /* Extracted from ui/src/pages/RunTranscriptUxLab.tsx (shadow-[0_12px_24px_rgba(6,182,212,0.12)]). */
}
/* ── Extracted verbatim MISC tokens (Phase 2 Batch 4, design/token-extraction) ── */
/* Batch 4/4 (final sweep): grid track lists, transition-property
lists, z-index, scale, easing, vertical-align, stroke-width, blur,
drop-shadow, and half-migrated var(x, fallback) color forms,
verbatim (no normalizing - the human scale-collapse decision comes
later per DESIGN.md/TOKEN-AUDIT.md). --gtc-* and --gtr-* and --tp-* are
sequentially numbered / slugged since their content (track lists,
property lists) is not safely nameable by value alone.
drop-shadow (a CSS filter function) gets its own
--drop-shadow-extract-* family, kept separate from Batch 3's
--shadow-extract-* box-shadow family per that batch's logged
human-decision note.
*-resolved wrapper tokens hold a verbatim var(--x, fallback)
expression for sites where Tailwind v4's paren-with-fallback-comma
shorthand does not parse (confirmed unsupported in this batch's
Step 0 syntax spike). theme(colors.a.b) fallbacks were resolved to
the equivalent var(--token) form the Tailwind build already
compiles them to today (inspected byte-for-byte from the built
storybook-static CSS before this codemod ran: theme(colors.muted.DEFAULT)
-> var(--muted), theme(colors.muted.foreground) -> var(--muted-foreground)).
--gradient-extract-* here CONTINUES Batch 1's counter (Batch 1 minted
1 through 24) rather than restarting it - these 2 sites use
hsl(var(...))/color-mix(...) CSS-native color functions rather than raw
hex/rgb literals, which is why Batch 1's hand-audited color-literal
table did not catch them, but they are still value-bearing gradient
brackets in scope for this batch's final sweep.
Allowlist (sites intentionally left as-is - see ALLOWLIST doc-comment
at the end of this file for the machine-readable list consumed by
scripts/check-token-gates.mjs):
allow ui/src/components/ui/dialog.tsx — tw-animate-css plugin utilities (zoom-in-[0.97] etc.) are dead/no-op classes today (plugin not installed, verified via built-CSS grep); nothing to tokenize without visually changing a currently-inert class
allow ui/src/components/ui/alert-dialog.tsx — same tw-animate-css dead-class situation as dialog.tsx
*/
:root {
--gtc-1: 56px 56px 24px minmax(0,1fr); /* Extracted from ui/src/components/DocumentDiffModal.tsx (grid-cols-[56px_56px_24px_minmax(0,1fr)]). */
--gtc-2: auto minmax(0,1fr) 2.25rem; /* Extracted from ui/src/components/FileTree.tsx (grid-cols-[auto_minmax(0,1fr)_2.25rem]). */
--gtc-3: minmax(0,1fr) 2.25rem; /* Extracted from ui/src/components/FileTree.tsx (grid-cols-[minmax(0,1fr)_2.25rem]). */
--gtc-4: minmax(0,1fr); /* Extracted from ui/src/components/FileTree.tsx (grid-cols-[minmax(0,1fr)]). */
--gtc-5: auto minmax(0,1fr); /* Extracted from ui/src/components/FileViewerSheet.tsx (grid-cols-[auto_minmax(0,1fr)]). */
--gtc-6: auto minmax(0,1fr) auto; /* Extracted from ui/src/components/FileViewerSheet.tsx (grid-cols-[auto_minmax(0,1fr)_auto]). */
--gtc-7: 7.5rem minmax(0,1fr); /* Extracted from ui/src/components/IssueChatThread.tsx (grid-cols-[7.5rem_minmax(0,1fr)]). */
--gtc-8: 7.5rem 1fr; /* Extracted from ui/src/components/IssueRecoveryActionCard.tsx (grid-cols-[7.5rem_1fr]). */
--gtc-9: 300px minmax(0,1fr); /* Extracted from ui/src/components/RoutineHistoryTab.tsx (grid-cols-[300px_minmax(0,1fr)]). */
--gtc-10: 10rem 1fr; /* Extracted from ui/src/components/SourceResolvedFoldCallout.tsx (grid-cols-[10rem_1fr]). */
--gtc-11: 12rem 1fr; /* Extracted from ui/src/components/SourceResolvedFoldCallout.tsx (grid-cols-[12rem_1fr]). */
--gtc-12: minmax(0,0.75fr) minmax(0,1fr); /* Extracted from ui/src/components/TrustPresetSection.tsx (grid-cols-[minmax(0,0.75fr)_minmax(0,1fr)]). */
--gtc-13: minmax(0,1fr) auto; /* Extracted from ui/src/components/environment-variables-editor/Row.tsx (grid-cols-[minmax(0,1fr)_auto]). */
--gtc-14: minmax(160px,2fr) minmax(240px,3fr) 32px; /* Extracted from ui/src/components/environment-variables-editor/Row.tsx (grid-cols-[minmax(160px,2fr)_minmax(240px,3fr)_32px]). */
--gtc-15: 6rem 1fr; /* Extracted from ui/src/components/issue-properties/IssueProperties.tsx (grid-cols-[6rem_1fr]). */
--gtc-16: auto 1fr; /* Extracted from ui/src/components/transcript/RunTranscriptView.tsx (grid-cols-[auto_1fr]). */
--gtc-17: 1fr auto; /* Extracted from ui/src/components/ui/card.tsx (grid-cols-[1fr_auto]). */
--gtc-18: auto minmax(0,1fr) minmax(12rem,0.65fr); /* Extracted from ui/src/pages/AgentDetail.tsx (grid-cols-[auto_minmax(0,1fr)_minmax(12rem,0.65fr)]). */
--gtc-19: 260px minmax(0,1fr); /* Extracted from ui/src/pages/AgentDetail.tsx (grid-cols-[260px_minmax(0,1fr)]). */
--gtc-20: 7rem 8rem 1fr; /* Extracted from ui/src/pages/CloudUpstream.tsx (grid-cols-[7rem_8rem_1fr]). */
--gtc-21: 1.25rem 12rem 1fr; /* Extracted from ui/src/pages/CloudUpstream.tsx (grid-cols-[1.25rem_12rem_1fr]). */
--gtc-22: 8rem 1fr 1fr 8rem; /* Extracted from ui/src/pages/CloudUpstream.tsx (grid-cols-[8rem_1fr_1fr_8rem]). */
--gtc-23: 8rem 1fr auto; /* Extracted from ui/src/pages/CloudUpstream.tsx (grid-cols-[8rem_1fr_auto]). */
--gtc-24: minmax(0,1.5fr) 120px 120px 180px; /* Extracted from ui/src/pages/CompanyAccess.tsx (grid-cols-[minmax(0,1.5fr)_120px_120px_180px]). */
--gtc-25: 19rem minmax(0,1fr); /* Extracted from ui/src/pages/CompanyExport.tsx (grid-cols-[19rem_minmax(0,1fr)]). */
--gtc-26: 7rem minmax(0,1fr); /* Extracted from ui/src/pages/CompanySkills.tsx (grid-cols-[7rem_minmax(0,1fr)]). */
--gtc-27: minmax(0,1fr) auto auto; /* Extracted from ui/src/pages/CompanySkills.tsx (grid-cols-[minmax(0,1fr)_auto_auto]). */
--gtc-28: 13rem minmax(0,1fr); /* Extracted from ui/src/pages/CompanySkills.tsx (grid-cols-[13rem_minmax(0,1fr)]). */
--gtc-29: minmax(0,1fr) 18rem; /* Extracted from ui/src/pages/CompanySkills.tsx (grid-cols-[minmax(0,1fr)_18rem]). */
--gtc-30: 14rem minmax(0,1fr); /* Extracted from ui/src/pages/CompanySkills.tsx (grid-cols-[14rem_minmax(0,1fr)]). */
--gtc-31: 1.3fr,1fr; /* Extracted from ui/src/pages/Costs.tsx (grid-cols-[1.3fr,1fr]). */
--gtc-32: 1.25fr,0.95fr; /* Extracted from ui/src/pages/Costs.tsx (grid-cols-[1.25fr,0.95fr]). */
--gtc-33: 1.2fr,0.95fr; /* Extracted from ui/src/pages/Costs.tsx (grid-cols-[1.2fr,0.95fr]). */
--gtc-34: 320px minmax(0,1fr); /* Extracted from ui/src/pages/InstanceAccess.tsx (grid-cols-[320px_minmax(0,1fr)]). */
--gtc-35: minmax(10rem,14rem) 1fr; /* Extracted from ui/src/pages/InstanceExperimentalSettings.tsx (grid-cols-[minmax(10rem,14rem)_1fr]). */
--gtc-36: minmax(0,1.15fr) minmax(360px,0.85fr); /* Extracted from ui/src/pages/InviteLanding.tsx (grid-cols-[minmax(0,1.15fr)_minmax(360px,0.85fr)]). */
--gtc-37: minmax(0,1.15fr) minmax(320px,0.85fr); /* Extracted from ui/src/pages/InviteUxLab.tsx (grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]). */
--gtc-38: minmax(0,0.92fr) minmax(0,1.08fr); /* Extracted from ui/src/pages/InviteUxLab.tsx (grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]). */
--gtc-39: minmax(0,1.2fr) 320px; /* Extracted from ui/src/pages/InviteUxLab.tsx (grid-cols-[minmax(0,1.2fr)_320px]). */
--gtc-40: minmax(0,1fr) 320px; /* Extracted from ui/src/pages/IssueChatLongThreadPerf.tsx (grid-cols-[minmax(0,1fr)_320px]). */
--gtc-41: 10rem minmax(0,1fr); /* Extracted from ui/src/pages/PipelineSettings.tsx (grid-cols-[10rem_minmax(0,1fr)]). */
--gtc-42: minmax(0,1fr) 240px; /* Extracted from ui/src/pages/PipelineSettings.tsx (grid-cols-[minmax(0,1fr)_240px]). */
--gtc-43: minmax(0,1fr) minmax(0,1fr); /* Extracted from ui/src/pages/PipelineSettings.tsx (grid-cols-[minmax(0,1fr)_minmax(0,1fr)]). */
--gtc-44: 5rem 240px; /* Extracted from ui/src/pages/PipelineSettings.tsx (grid-cols-[5rem_240px]). */
--gtc-45: minmax(0,1fr) 340px; /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[minmax(0,1fr)_340px]). */
--gtc-46: 18px minmax(0,1fr); /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[18px_minmax(0,1fr)]). */
--gtc-47: 120px 1fr; /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[120px_1fr]). */
--gtc-48: 18px minmax(0,1fr) auto; /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[18px_minmax(0,1fr)_auto]). */
--gtc-49: 18px 1fr auto; /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[18px_1fr_auto]). */
--gtc-50: 1fr 280px; /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[1fr_280px]). */
--gtc-51: 160px 1fr; /* Extracted from ui/src/pages/Pipelines.tsx (grid-cols-[160px_1fr]). */
--gtc-52: minmax(0,1.4fr) minmax(220px,0.8fr); /* Extracted from ui/src/pages/PluginSettings.tsx (grid-cols-[minmax(0,1.4fr)_minmax(220px,0.8fr)]). */
--gtc-53: minmax(0,1.4fr) minmax(18rem,0.9fr); /* Extracted from ui/src/pages/ProjectWorkspaceDetail.tsx (grid-cols-[minmax(0,1.4fr)_minmax(18rem,0.9fr)]). */
--gtc-54: minmax(12rem,2.4fr) minmax(5.25rem,0.75fr) minmax(7rem,1fr) minmax(5rem,0.7fr) 2.75rem; /* Extracted from ui/src/pages/Secrets.tsx (grid-cols-[minmax(12rem,2.4fr)_minmax(5.25rem,0.75fr)_minmax(7rem,1fr)_minmax(5rem,0.7fr)_2.75rem]). */
--gtc-55: 8rem minmax(0,1fr); /* Extracted from ui/src/pages/Secrets.tsx (grid-cols-[8rem_minmax(0,1fr)]). */
--gtc-56: minmax(0,1fr) minmax(14rem,18rem); /* Extracted from ui/src/pages/TeamCatalog.tsx (grid-cols-[minmax(0,1fr)_minmax(14rem,18rem)]). */
--gtc-57: repeat(14,minmax(0,1fr)); /* Extracted from ui/src/pages/UserProfile.tsx (grid-cols-[repeat(14,minmax(0,1fr))]). */
--gtc-58: auto 1fr auto; /* Extracted from ui/src/pages/UserProfile.tsx (grid-cols-[auto_1fr_auto]). */
--gtr-1: auto auto; /* Extracted from ui/src/components/ui/card.tsx (grid-rows-[auto_auto]). */
--gtr-2: 1fr; /* Extracted from ui/src/pages/CompanySkills.tsx (grid-rows-[1fr]). */
--gtr-3: 0fr; /* Extracted from ui/src/pages/CompanySkills.tsx (grid-rows-[0fr]). */
--tp-background-color-border-color: background-color,border-color; /* Extracted from ui/src/components/ActivityFeed.tsx (transition-[background-color,border-color]). */
--tp-width-background-color: width,background-color; /* Extracted from ui/src/components/BudgetPolicyCard.tsx (transition-[width,background-color]). */
--tp-width: width; /* Extracted from ui/src/components/ClaudeSubscriptionPanel.tsx (transition-[width]). */
--tp-background-color-color-opacity: background-color,color,opacity; /* Extracted from ui/src/components/FileTree.tsx (transition-[background-color,color,opacity]). */
--tp-max-height: max-height; /* Extracted from ui/src/components/FoldCurtain.tsx (transition-[max-height]). */
--tp-border-color-background-color-box-shadow: border-color,background-color,box-shadow; /* Extracted from ui/src/components/IssueChatThread.tsx (transition-[border-color,background-color,box-shadow]). */
--tp-width-min-width: width,min-width; /* Extracted from ui/src/components/KanbanBoard.tsx (transition-[width,min-width]). */
--tp-width-opacity: width,opacity; /* Extracted from ui/src/components/OnboardingWizard.tsx (transition-[width,opacity]). */
--tp-background-color-right: background-color,right; /* Extracted from ui/src/components/ScrollToBottom.tsx (transition-[background-color,right]). */
--tp-background-color-border-color-color: background-color,border-color,color; /* Extracted from ui/src/components/SystemNotice.tsx (transition-[background-color,border-color,color]). */
--tp-transform-opacity: transform,opacity; /* Extracted from ui/src/components/ToastViewport.tsx (transition-[transform,opacity]). */
--tp-color-box-shadow: color,box-shadow; /* Extracted from ui/src/components/ui/badge.tsx (transition-[color,box-shadow]). */
--tp-color-background-color-border-color-box-shadow-opacity: color,background-color,border-color,box-shadow,opacity; /* Extracted from ui/src/components/ui/button.tsx (transition-[color,background-color,border-color,box-shadow,opacity]). */
--tp-border-color-background-color: border-color,background-color; /* Extracted from ui/src/components/ui/radio-card.tsx (transition-[border-color,background-color]). */
--tp-color-background-color-border-color-box-shadow: color,background-color,border-color,box-shadow; /* Extracted from ui/src/components/ui/tabs.tsx (transition-[color,background-color,border-color,box-shadow]). */
--tp-grid-template-rows-opacity: grid-template-rows,opacity; /* Extracted from ui/src/pages/CompanySkills.tsx (transition-[grid-template-rows,opacity]). */
--tp-box-shadow-border-color: box-shadow,border-color; /* Extracted from ui/src/pages/OrgChart.tsx (transition-[box-shadow,border-color]). */
--tp-box-shadow: box-shadow; /* Extracted from ui/src/pages/ProjectDetail.tsx (transition-[box-shadow]). */
--tp-transform-box-shadow: transform,box-shadow; /* Extracted from ui/src/pages/ProjectDetail.tsx (transition-[transform,box-shadow]). */
--z-2: 2; /* Extracted from ui/src/components/DocumentAnnotationLayer.tsx (z-[2]). */
--z-60: 60; /* Extracted from ui/src/components/DocumentAnnotationPanel.tsx (z-[60]). */
--z-1: 1; /* Extracted from ui/src/components/IssueDocumentAnnotations.tsx (z-[1]). */
--z-200: 200; /* Extracted from ui/src/components/Layout.tsx (z-[200]). */
--z-9999: 9999; /* Extracted from ui/src/components/MarkdownEditor.tsx (z-[9999]). */
--z-120: 120; /* Extracted from ui/src/components/ToastViewport.tsx (z-[120]). */
--s-0_98: 0.98; /* Extracted from ui/src/pages/Inbox.tsx (scale-[0.98]). */
--e-cubic-bezier-0_16-1-0_3-1: cubic-bezier(0.16,1,0.3,1); /* Extracted from ui/src/components/ui/alert-dialog.tsx (ease-[cubic-bezier(0.16,1,0.3,1)]). */
--va-0_125em: -0.125em; /* Extracted from ui/src/components/ExternalObjectStatusIcon.tsx (align-[-0.125em]). */
--sw-2_3: 2.3; /* Extracted from ui/src/components/MobileBottomNav.tsx (stroke-[2.3]). */
--blur-1px: 1px; /* Extracted from ui/src/components/ChatComposer.tsx (backdrop-blur-[1px]). */
--blur-2px: 2px; /* Extracted from ui/src/components/IssueChatThread.tsx (backdrop-blur-[2px]). */
--drop-shadow-extract-1: 0 1px 2px rgba(0,0,0,0.65); /* Extracted from ui/src/components/CompanyPatternIcon.tsx (drop-shadow-[0_1px_2px_rgba(0,0,0,0.65)]). */
--code-bg-resolved: var(--paperclip-code-bg, var(--muted)); /* Extracted from ui/src/components/FileViewerSheet.tsx (bg-[var(--paperclip-code-bg,theme(colors.muted.DEFAULT))]). */
--code-highlight-bg-resolved: var(--paperclip-code-highlight-bg, rgba(250,204,21,0.12)); /* Extracted from ui/src/components/FileViewerSheet.tsx (bg-[var(--paperclip-code-highlight-bg,rgba(250,204,21,0.12))]). */
--code-gutter-fg-resolved: var(--paperclip-code-gutter-fg, var(--muted-foreground)); /* Extracted from ui/src/components/FileViewerSheet.tsx (text-[var(--paperclip-code-gutter-fg,theme(colors.muted.foreground))]). */
--code-highlight-border-resolved: var(--paperclip-code-highlight-border, rgb(234,179,8)); /* Extracted from ui/src/components/FileViewerSheet.tsx (border-[var(--paperclip-code-highlight-border,rgb(234,179,8))]). */
--gradient-extract-25: linear-gradient(135deg,hsl(var(--primary)) 0%,hsl(var(--accent)) 55%,hsl(var(--muted)) 100%); /* Extracted from ui/src/components/SidebarAccountMenu.tsx (bg-[linear-gradient(135deg,hsl(var(--primary))_0%,hsl(var(--accent))_55%,hsl(var(--muted))_100%)]). */
--gradient-extract-26: linear-gradient(135deg,hsl(var(--primary)) 0%,hsl(var(--accent)) 58%,color-mix(in oklab,hsl(var(--background)) 76%,white 24%) 100%); /* Extracted from ui/src/pages/ProfileSettings.tsx (bg-[linear-gradient(135deg,hsl(var(--primary))_0%,hsl(var(--accent))_58%,color-mix(in_oklab,hsl(var(--background))_76%,white_24%)_100%)]). */
}
/* ── ALLOWLIST (Phase 2, design/token-extraction) ──────────────────────────
Canonical, MACHINE-READABLE allowlist consumed by
scripts/check-token-gates.mjs. This block consolidates every site the
Batch 1-4 codemods intentionally left un-tokenized (functional/third-party
values, CSS keywords, and currently-inert dead classes) into one list the
gate script parses directly, rather than re-deriving it from each batch's
prose log above. The per-batch prose blocks above are kept as historical
narrative (why each decision was made); this block is the source of truth
for WHICH sites are exempt.
Format (one line per entry, do not reflow):
* allow <repo-relative-path> — <reason>
A path may appear more than once if it has more than one distinct
allowlisted concern. Paths are matched by the gate script as a substring
of the file being checked (repo-relative, POSIX-style).
* allow ui/src/pages/CompanyEnvironments.tsx — xterm.js terminal theme config (background/foreground/cursor/cursorAccent/selectionBackground hex, fontSize: 12); functional third-party option object, not a rendered CSS value
* allow ui/src/pages/CompanySettings.tsx — <input type="color"> value; the DOM color-picker control requires a real hex string
* allow ui/src/components/issue-properties/IssueProperties.tsx — newLabelColor color-picker seed state persisted into the label-create payload sent to the backend; a var() string would corrupt that payload
* allow ui/src/pages/CompanySkills.tsx — DISCOVERY_ACCENTS palette array feeds skill.color, persisted/compared JS data (SkillCreateDraft), not just a rendered value; also fontSize: Math.round(size * 0.42), computed at runtime from a prop, not a static literal
* allow ui/src/components/IssueColumns.tsx — accentColor fallback also feeds pickTextColorForPillBg() contrast math (lib/color-contrast.ts), which needs a real hex string to compute luminance
* allow ui/src/components/CompanyPatternIcon.tsx — canvas 2D fillStyle built from a runtime-computed template literal, not a static literal at all
* allow ui/src/pages/InviteUxLab.tsx — brandColor prop (x2) feeds CompanyPatternIcon's hexToHue() color math via the same canvas-fill code path; demo/showcase-only prop, not a rendered CSS value
* allow ui/src/components/ui/scroll-area.tsx — rounded-[inherit] is a CSS keyword, not a literal value; nothing to extract
* allow ui/src/components/ui/dialog.tsx — tw-animate-css plugin utilities (zoom-in-[0.97], zoom-out-[0.97], slide-in-from-top-[1%], slide-out-to-top-[1%], animate-in, animate-out, fade-in-0, fade-out-0) are dead/no-op classes today (the tw-animate-css plugin is not installed and no matching @utility exists in this file; confirmed via grep of the built storybook-static CSS — none of these class names emit any rule); nothing to tokenize without visually changing a currently-inert class
* allow ui/src/components/ui/alert-dialog.tsx — same tw-animate-css dead-class situation as dialog.tsx (zoom-out-[0.97], zoom-in-[0.97])
* allow ui/src/components/ProjectWorkspaceSummaryCard.tsx — min-[420px]: is a Tailwind arbitrary BREAKPOINT variant (responsive container-query-style prefix), not a value applied to a property; the variant position cannot reference a CSS custom property, so it cannot be tokenized without changing Tailwind's own variant syntax
* allow ui/src/components/FileTree.tsx — max-[480px]: arbitrary breakpoint variant (x2), same reasoning as min-[420px] above
Test-fixture hex-literal policy (Batch 4, resolving TOKEN-AUDIT.md section
8 item 10 "Needs human decision" for these specific sites): the following
*.test.tsx files pass hex-literal strings as MOCK PROP / MOCK API-RESPONSE
DATA (label/project/company/brand `color` fields, xterm.js theme-mock
assertions) — not as Tailwind class strings or CSS-in-JS style
declarations. These are the test-file counterpart of the same
"functional/persisted data, not a rendered CSS value" category Batch 1
already allowlisted in production source (see CompanySkills.tsx,
IssueProperties.tsx, IssueColumns.tsx above); tokenizing them would
replace realistic-looking mock data with `var()` strings that no longer
resemble what a real API response contains, without touching any
rendered pixel. Each file is listed individually for reviewability.
* allow ui/src/components/IssueFiltersPopover.test.tsx — mock label color prop
* allow ui/src/components/IssueProperties.test.tsx — mock label/project color fixtures
* allow ui/src/components/MarkdownBody.test.tsx — mock project color fed through a mention-href builder
* allow ui/src/components/MarkdownEditor.test.tsx — mock projectColor mention fixture
* allow ui/src/components/NewIssueDialog.test.tsx — mock brandColor/label color fixtures
* allow ui/src/components/ProjectTile.test.tsx — mock project color prop
* allow ui/src/components/RoutineRunVariablesDialog.test.tsx — mock label color fixture
* allow ui/src/components/SidebarCompanyMenu.test.tsx — mock brandColor company fixtures
* allow ui/src/components/SidebarProjects.test.tsx — mock project color fixture
* allow ui/src/components/SidebarStarredProjects.test.tsx — mock project color fixture
* allow ui/src/pages/CompanyEnvironments.test.tsx — xterm.js theme-mock assertion values (mirrors the allowlisted xterm.js config in the CompanyEnvironments.tsx source above)
* allow ui/src/pages/ExecutionWorkspaceDetail.test.tsx — mock project color fixture
* allow ui/src/pages/InviteLanding.test.tsx — mock companyBrandColor fixtures
* allow ui/src/pages/IssueDetail.test.tsx — mock project color fixture
* allow ui/src/pages/ProjectDetail.test.tsx — mock project color fixture
* allow ui/src/pages/ProjectWorkspaceDetail.test.tsx — mock project color fixture
* allow ui/src/pages/Projects.test.tsx — mock project color fixture
* allow ui/src/pages/Routines.test.tsx — mock routine/label color fixtures
── end allowlist ── */

View File

@ -2173,7 +2173,7 @@ function PromptsTab({
</CollapsibleTrigger>
<CollapsibleContent className="pt-4 pb-6">
<TooltipProvider>
<div className="grid gap-x-6 gap-y-4 md:grid-cols-[auto_minmax(0,1fr)_minmax(12rem,0.65fr)]">
<div className="grid gap-x-6 gap-y-4 md:grid-cols-(--gtc-18)">
<label className="space-y-1.5 min-w-0">
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
Mode
@ -2573,7 +2573,7 @@ function PromptsTabSkeleton() {
))}
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[260px_minmax(0,1fr)]">
<div className="grid gap-4 lg:grid-cols-(--gtc-19)">
<div className="rounded-lg border border-border p-3 space-y-3">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-12" />

View File

@ -271,7 +271,7 @@ export function CloudUpstream() {
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Connection</div>
<div className="rounded-md border border-border px-4 py-4">
{connection ? (
<div className="grid gap-3 lg:grid-cols-[1fr_auto] lg:items-start">
<div className="grid gap-3 lg:grid-cols-(--gtc-17) lg:items-start">
<div>
<div className="text-sm font-medium">
{connection.target.stackDisplayName ?? connection.target.stackSlug ?? connection.target.stackId}
@ -297,7 +297,7 @@ export function CloudUpstream() {
</div>
</div>
) : (
<div className="grid gap-3 md:grid-cols-[1fr_auto]">
<div className="grid gap-3 md:grid-cols-(--gtc-17)">
<Input
value={remoteUrl}
onChange={(event) => setRemoteUrl(event.target.value)}
@ -382,7 +382,7 @@ export function CloudUpstream() {
</div>
<div className="mt-4 divide-y divide-border">
{latestRun.events.map((event) => (
<div key={event.id} className="grid gap-2 py-2 text-sm sm:grid-cols-[7rem_8rem_1fr]">
<div key={event.id} className="grid gap-2 py-2 text-sm sm:grid-cols-(--gtc-20)">
<span className="text-xs text-muted-foreground">{formatDate(event.at)}</span>
<span className="text-xs capitalize text-muted-foreground">{event.phase}</span>
<span>{event.message}</span>
@ -413,7 +413,7 @@ export function CloudUpstream() {
<button
key={run.id}
type="button"
className="grid w-full gap-1 px-4 py-3 text-left text-sm hover:bg-accent/40 sm:grid-cols-[1fr_auto]"
className="grid w-full gap-1 px-4 py-3 text-left text-sm hover:bg-accent/40 sm:grid-cols-(--gtc-17)"
onClick={() => setActiveRun(run)}
>
<span>Run {run.id.slice(0, 8)} · {run.status}</span>
@ -486,7 +486,7 @@ function WarningsPanel({ warnings }: { warnings: CloudUpstreamPreview["warnings"
</div>
<div className="divide-y divide-border">
{warnings.map((warning) => (
<div key={warning.code} className="grid gap-2 py-2 sm:grid-cols-[1.25rem_12rem_1fr]">
<div key={warning.code} className="grid gap-2 py-2 sm:grid-cols-(--gtc-21)">
<AlertTriangle className={warning.severity === "blocker" ? "h-4 w-4 text-destructive" : "h-4 w-4 text-amber-600"} />
<div className="text-sm font-medium">{warning.title}</div>
<div className="text-sm text-muted-foreground">{warning.detail}</div>
@ -506,7 +506,7 @@ function ConflictTable({ conflicts }: { conflicts: CloudUpstreamPreview["conflic
) : (
<div className="divide-y divide-border">
{conflicts.map((conflict) => (
<div key={conflict.id} className="grid gap-2 py-2 text-sm sm:grid-cols-[8rem_1fr_1fr_8rem]">
<div key={conflict.id} className="grid gap-2 py-2 text-sm sm:grid-cols-(--gtc-22)">
<span className="text-muted-foreground">{conflict.entityType}</span>
<span>{conflict.sourceLabel}</span>
<span>{conflict.targetLabel}</span>
@ -539,7 +539,7 @@ function ActivationChecklist({
const pending = isPending && pendingEntityType === row.key;
const activated = row.status === "activated";
return (
<div key={row.key} className="grid gap-2 py-2 text-sm sm:grid-cols-[8rem_1fr_auto] sm:items-center">
<div key={row.key} className="grid gap-2 py-2 text-sm sm:grid-cols-(--gtc-23) sm:items-center">
<div>
<div className="font-medium">{row.label}</div>
<div className="text-xs text-muted-foreground">{row.statusLabel}</div>

View File

@ -189,7 +189,7 @@ function CloudUpstreamRender({ fixture }: { fixture: Fixture }) {
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Connection</div>
<div className="rounded-md border border-border px-4 py-4">
{connection ? (
<div className="grid gap-3 lg:grid-cols-[1fr_auto] lg:items-start">
<div className="grid gap-3 lg:grid-cols-(--gtc-17) lg:items-start">
<div>
<div className="text-sm font-medium">
{connection.target.stackDisplayName ?? connection.target.stackSlug ?? connection.target.stackId}
@ -207,7 +207,7 @@ function CloudUpstreamRender({ fixture }: { fixture: Fixture }) {
</Button>
</div>
) : (
<div className="grid gap-3 md:grid-cols-[1fr_auto]">
<div className="grid gap-3 md:grid-cols-(--gtc-17)">
<Input
defaultValue="https://paperclip.paperclip.app/PC521D/dashboard"
placeholder="https://paperclip.paperclip.app/PC521D/dashboard"
@ -279,7 +279,7 @@ function CloudUpstreamRender({ fixture }: { fixture: Fixture }) {
</div>
<div className="mt-4 divide-y divide-border">
{latestRun.events.map((event) => (
<div key={event.id} className="grid gap-2 py-2 text-sm sm:grid-cols-[7rem_8rem_1fr]">
<div key={event.id} className="grid gap-2 py-2 text-sm sm:grid-cols-(--gtc-20)">
<span className="text-xs text-muted-foreground">{formatDate(event.at)}</span>
<span className="text-xs capitalize text-muted-foreground">{event.phase}</span>
<span>{event.message}</span>
@ -302,7 +302,7 @@ function CloudUpstreamRender({ fixture }: { fixture: Fixture }) {
{history.map((run) => (
<div
key={run.id}
className="grid w-full gap-1 px-4 py-3 text-left text-sm hover:bg-accent/40 sm:grid-cols-[1fr_auto]"
className="grid w-full gap-1 px-4 py-3 text-left text-sm hover:bg-accent/40 sm:grid-cols-(--gtc-17)"
>
<span>Run {run.id.slice(0, 8)} · {run.status}</span>
<span className="text-xs text-muted-foreground">{formatDate(run.createdAt)}</span>
@ -359,7 +359,7 @@ function WarningsPanel({ warnings }: { warnings: CloudUpstreamWarning[] }) {
</div>
<div className="divide-y divide-border">
{warnings.map((warning) => (
<div key={warning.code} className="grid gap-2 py-2 sm:grid-cols-[1.25rem_12rem_1fr]">
<div key={warning.code} className="grid gap-2 py-2 sm:grid-cols-(--gtc-21)">
<AlertTriangle className={warning.severity === "blocker" ? "h-4 w-4 text-destructive" : "h-4 w-4 text-amber-600"} />
<div className="text-sm font-medium">{warning.title}</div>
<div className="text-sm text-muted-foreground">{warning.detail}</div>
@ -379,7 +379,7 @@ function ConflictTable({ conflicts }: { conflicts: CloudUpstreamConflict[] }) {
) : (
<div className="divide-y divide-border">
{conflicts.map((conflict) => (
<div key={conflict.id} className="grid gap-2 py-2 text-sm sm:grid-cols-[8rem_1fr_1fr_8rem]">
<div key={conflict.id} className="grid gap-2 py-2 text-sm sm:grid-cols-(--gtc-22)">
<span className="text-muted-foreground">{conflict.entityType}</span>
<span>{conflict.sourceLabel}</span>
<span>{conflict.targetLabel}</span>
@ -401,7 +401,7 @@ function ActivationChecklist({ run }: { run: CloudUpstreamRun }) {
{rows.map((row) => {
const activated = row.status === "activated";
return (
<div key={row.key} className="grid gap-2 py-2 text-sm sm:grid-cols-[8rem_1fr_auto] sm:items-center">
<div key={row.key} className="grid gap-2 py-2 text-sm sm:grid-cols-(--gtc-23) sm:items-center">
<div>
<div className="font-medium">{row.label}</div>
<div className="text-xs text-muted-foreground">{row.statusLabel}</div>

View File

@ -310,7 +310,7 @@ export function CompanyAccess() {
) : null}
<div className="overflow-hidden rounded-xl border border-border">
<div className="grid grid-cols-[minmax(0,1.5fr)_120px_120px_180px] gap-3 border-b border-border px-4 py-3 text-xs font-medium uppercase tracking-wide text-muted-foreground">
<div className="grid grid-cols-(--gtc-24) gap-3 border-b border-border px-4 py-3 text-xs font-medium uppercase tracking-wide text-muted-foreground">
<div>User account</div>
<div>Role</div>
<div>Status</div>
@ -325,7 +325,7 @@ export function CompanyAccess() {
return (
<div
key={member.id}
className="grid grid-cols-[minmax(0,1.5fr)_120px_120px_180px] gap-3 border-b border-border px-4 py-3 last:border-b-0"
className="grid grid-cols-(--gtc-24) gap-3 border-b border-border px-4 py-3 last:border-b-0"
>
<div className="min-w-0">
<div className="truncate font-medium">{member.user?.name?.trim() || member.user?.email || member.principalId}</div>

View File

@ -353,7 +353,7 @@ function FrontmatterCard({
}) {
return (
<div className="rounded-md border border-border bg-accent/20 px-4 py-3 mb-4">
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-sm">
<dl className="grid grid-cols-(--gtc-5) gap-x-4 gap-y-1.5 text-sm">
{Object.entries(data).map(([key, value]) => (
<div key={key} className="contents">
<dt className="text-muted-foreground whitespace-nowrap py-0.5">
@ -969,7 +969,7 @@ export function CompanyExport() {
)}
{/* Two-column layout */}
<div className="grid gap-4 xl:h-(--sz-calc-30) xl:grid-cols-[19rem_minmax(0,1fr)] xl:gap-0">
<div className="grid gap-4 xl:h-(--sz-calc-30) xl:grid-cols-(--gtc-25) xl:gap-0">
<aside className="flex max-h-(--sz-24rem) flex-col overflow-hidden border-b border-border xl:max-h-none xl:border-b-0 xl:border-r">
<div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2>

View File

@ -115,7 +115,7 @@ const ACTION_COLORS: Record<string, string> = {
function FrontmatterCard({ data }: { data: FrontmatterData }) {
return (
<div className="rounded-md border border-border bg-accent/20 px-4 py-3 mb-4">
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-sm">
<dl className="grid grid-cols-(--gtc-5) gap-x-4 gap-y-1.5 text-sm">
{Object.entries(data).map(([key, value]) => (
<div key={key} className="contents">
<dt className="text-muted-foreground whitespace-nowrap py-0.5">
@ -1319,7 +1319,7 @@ export function CompanyImport() {
)}
{/* Two-column layout */}
<div className="grid gap-4 xl:h-(--sz-calc-31) xl:grid-cols-[19rem_minmax(0,1fr)] xl:gap-0">
<div className="grid gap-4 xl:h-(--sz-calc-31) xl:grid-cols-(--gtc-25) xl:gap-0">
<aside className="flex max-h-(--sz-24rem) flex-col overflow-hidden border-b border-border xl:max-h-none xl:border-b-0 xl:border-r">
<div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2>

View File

@ -1456,7 +1456,7 @@ function NewSkillWizard({
</div>
) : (
<div className="space-y-4 text-sm">
<div className="grid grid-cols-[7rem_minmax(0,1fr)] gap-y-2">
<div className="grid grid-cols-(--gtc-26) gap-y-2">
<span className="text-muted-foreground">Name</span>
<span>{draft.name || "Untitled"}</span>
<span className="text-muted-foreground">Slug</span>
@ -1589,7 +1589,7 @@ function CatalogList({
<div key={skill.id} className="border-b border-border">
<div
className={cn(
"group grid grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-x-1 px-3 py-1.5 hover:bg-accent/30",
"group grid grid-cols-(--gtc-3) items-center gap-x-1 px-3 py-1.5 hover:bg-accent/30",
isSelected && "text-foreground",
)}
>
@ -1609,7 +1609,7 @@ function CatalogList({
</Link>
<button
type="button"
className="flex h-9 w-9 shrink-0 items-center justify-center self-center rounded-sm text-muted-foreground opacity-80 transition-[background-color,color,opacity] hover:bg-accent hover:text-foreground group-hover:opacity-100"
className="flex h-9 w-9 shrink-0 items-center justify-center self-center rounded-sm text-muted-foreground opacity-80 transition-(--tp-background-color-color-opacity) hover:bg-accent hover:text-foreground group-hover:opacity-100"
onClick={() => onToggleSkill(skill.id)}
aria-label={expanded ? `Collapse ${skill.name}` : `Expand ${skill.name}`}
>
@ -1619,8 +1619,8 @@ function CatalogList({
<div
aria-hidden={!expanded}
className={cn(
"grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
expanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
"grid overflow-hidden transition-(--tp-grid-template-rows-opacity) duration-200 ease-(--e-cubic-bezier-0_16-1-0_3-1)",
expanded ? "grid-rows-(--gtr-2) opacity-100" : "grid-rows-(--gtr-3) opacity-0",
)}
>
<div className="min-h-0 overflow-hidden">
@ -1901,7 +1901,7 @@ function InstallPreviewDialog({
<div className="space-y-4 text-sm">
<div className="rounded-md border border-border p-3">
<div className="grid grid-cols-[7rem_minmax(0,1fr)] gap-y-2 text-xs">
<div className="grid grid-cols-(--gtc-26) gap-y-2 text-xs">
<div className="text-muted-foreground">Trust</div>
<div className="flex items-center gap-2">
<TrustChip level={skill.trustLevel} />
@ -1942,7 +1942,7 @@ function InstallPreviewDialog({
</div>
<div className="max-h-48 overflow-y-auto">
{skill.files.map((file) => (
<div key={file.path} className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-x-3 border-b border-border/50 px-3 py-1.5 text-xs last:border-b-0">
<div key={file.path} className="grid grid-cols-(--gtc-27) items-center gap-x-3 border-b border-border/50 px-3 py-1.5 text-xs last:border-b-0">
<span className="truncate font-mono text-muted-foreground">{file.path}</span>
<span className="rounded border border-border bg-muted/40 px-1 py-0.5 text-(length:--fs-10) uppercase text-muted-foreground">{file.kind}</span>
<span className="text-(length:--fs-11) text-muted-foreground">{formatBytes(file.sizeBytes)}</span>
@ -2195,7 +2195,7 @@ function SkillTree({
<div key={node.path ?? node.name}>
<div
className={cn(
"group grid w-full grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground",
"group grid w-full grid-cols-(--gtc-3) items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground",
SKILL_TREE_ROW_HEIGHT_CLASS,
)}
>
@ -2212,7 +2212,7 @@ function SkillTree({
</button>
<button
type="button"
className="flex h-9 w-9 items-center justify-center self-center rounded-sm text-muted-foreground opacity-70 transition-[background-color,color,opacity] hover:bg-accent hover:text-foreground group-hover:opacity-100"
className="flex h-9 w-9 items-center justify-center self-center rounded-sm text-muted-foreground opacity-70 transition-(--tp-background-color-color-opacity) hover:bg-accent hover:text-foreground group-hover:opacity-100"
onClick={() => node.path && onToggleDir(node.path)}
>
{expanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
@ -2323,7 +2323,7 @@ function SkillList({
<div key={skill.id} className="border-b border-border">
<div
className={cn(
"group grid grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-x-1 px-3 py-1.5 hover:bg-accent/30",
"group grid grid-cols-(--gtc-3) items-center gap-x-1 px-3 py-1.5 hover:bg-accent/30",
skill.id === selectedSkillId && "text-foreground",
)}
>
@ -2349,7 +2349,7 @@ function SkillList({
</Link>
<button
type="button"
className="flex h-9 w-9 shrink-0 items-center justify-center self-center rounded-sm text-muted-foreground opacity-80 transition-[background-color,color,opacity] hover:bg-accent hover:text-foreground group-hover:opacity-100"
className="flex h-9 w-9 shrink-0 items-center justify-center self-center rounded-sm text-muted-foreground opacity-80 transition-(--tp-background-color-color-opacity) hover:bg-accent hover:text-foreground group-hover:opacity-100"
onClick={() => onToggleSkill(skill.id)}
aria-label={expanded ? `Collapse ${skill.name}` : `Expand ${skill.name}`}
>
@ -2359,8 +2359,8 @@ function SkillList({
<div
aria-hidden={!expanded}
className={cn(
"grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
expanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
"grid overflow-hidden transition-(--tp-grid-template-rows-opacity) duration-200 ease-(--e-cubic-bezier-0_16-1-0_3-1)",
expanded ? "grid-rows-(--gtr-2) opacity-100" : "grid-rows-(--gtr-3) opacity-0",
)}
>
<div className="min-h-0 overflow-hidden">
@ -2530,7 +2530,7 @@ function SkillVersionDiffDialog({
<div className="p-6 text-center text-sm text-muted-foreground">Both sides are the same version.</div>
) : (
<div className="font-mono text-(length:--fs-12) leading-6">
<div className="grid grid-cols-[56px_56px_24px_minmax(0,1fr)] border-b border-border/60 bg-muted/30 px-3 py-2 text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">
<div className="grid grid-cols-(--gtc-1) border-b border-border/60 bg-muted/30 px-3 py-2 text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">
<span>Old</span>
<span>New</span>
<span />
@ -2539,7 +2539,7 @@ function SkillVersionDiffDialog({
{diffRows.map((row, index) => (
<div
key={`${row.kind}-${index}-${row.oldLineNumber ?? "x"}-${row.newLineNumber ?? "x"}`}
className={cn("grid grid-cols-[56px_56px_24px_minmax(0,1fr)] gap-0 border-b border-border/30 px-3", lineClassesByKind[row.kind])}
className={cn("grid grid-cols-(--gtc-1) gap-0 border-b border-border/30 px-3", lineClassesByKind[row.kind])}
>
<span className="select-none border-r border-border/30 pr-3 text-right text-muted-foreground">{row.oldLineNumber ?? ""}</span>
<span className="select-none border-r border-border/30 px-3 text-right text-muted-foreground">{row.newLineNumber ?? ""}</span>
@ -2732,7 +2732,7 @@ export function SkillDetailPage({
function renderFilesBody() {
return (
<div className="grid min-h-(--sz-560px) gap-0 lg:grid-cols-[13rem_minmax(0,1fr)]">
<div className="grid min-h-(--sz-560px) gap-0 lg:grid-cols-(--gtc-28)">
<aside className="border-b border-border pb-3 lg:border-b-0 lg:border-r lg:pb-0 lg:pr-3">
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">Files</div>
<SkillTree
@ -2867,7 +2867,7 @@ export function SkillDetailPage({
<div className="py-6 text-sm text-muted-foreground">No saved versions yet.</div>
) : (
sortedVersions.map((version) => (
<div key={version.id} className="grid gap-2 border-b border-border px-0 py-3 text-sm last:border-b-0 sm:grid-cols-[minmax(0,1fr)_auto]">
<div key={version.id} className="grid gap-2 border-b border-border px-0 py-3 text-sm last:border-b-0 sm:grid-cols-(--gtc-13)">
<div className="min-w-0">
<div className="font-medium">{versionLabel(version)}</div>
<div className="mt-1 text-xs text-muted-foreground">
@ -3089,7 +3089,7 @@ export function SkillDetailPage({
</div>
</div>
<div className="grid gap-6 px-4 py-4 xl:grid-cols-[minmax(0,1fr)_18rem]">
<div className="grid gap-6 px-4 py-4 xl:grid-cols-(--gtc-29)">
<main className="min-w-0">
<Tabs value={activeTab} onValueChange={(value) => onTabChange(value as SkillDetailTab)}>
{/* Underlined tab strip: the bottom padding keeps the active-tab
@ -4718,7 +4718,7 @@ export function CompanySkills() {
) : !selectedCatalogSkill ? (
<EmptyState icon={Boxes} message="Catalog skill not found." />
) : (
<div className="grid gap-0 xl:grid-cols-[14rem_minmax(0,1fr)]">
<div className="grid gap-0 xl:grid-cols-(--gtc-30)">
<aside className="border-b border-border px-3 py-4 xl:border-b-0 xl:border-r">
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">Files</div>
<SkillTree

View File

@ -655,7 +655,7 @@ export function Costs() {
</div>
) : null}
<div className="grid gap-4 xl:grid-cols-[1.3fr,1fr]">
<div className="grid gap-4 xl:grid-cols-(--gtc-31)">
<Card>
<CardHeader className="px-5 pt-5 pb-2">
<CardTitle className="text-base">Inference ledger</CardTitle>
@ -687,7 +687,7 @@ export function Costs() {
<div className="h-2 overflow-hidden bg-muted">
<div
className={cn(
"h-full transition-[width,background-color] duration-150",
"h-full transition-(--tp-width-background-color) duration-150",
spendData.summary.utilizationPercent > 90
? "bg-red-400"
: spendData.summary.utilizationPercent > 70
@ -714,7 +714,7 @@ export function Costs() {
/>
</div>
<div className="grid gap-4 xl:grid-cols-[1.25fr,0.95fr]">
<div className="grid gap-4 xl:grid-cols-(--gtc-32)">
<Card>
<CardHeader className="px-5 pt-5 pb-2">
<CardTitle className="text-base">By agent</CardTitle>
@ -1074,7 +1074,7 @@ export function Costs() {
eventCount={financeData?.summary.eventCount ?? 0}
/>
<div className="grid gap-4 xl:grid-cols-[1.2fr,0.95fr]">
<div className="grid gap-4 xl:grid-cols-(--gtc-33)">
<div className="space-y-4">
<Card>
<CardHeader className="px-5 pt-5 pb-2">

View File

@ -1379,7 +1379,7 @@ export function DesignGuide() {
</div>
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-[width,background-color] duration-150 ${color}`}
className={`h-full rounded-full transition-(--tp-width-background-color) duration-150 ${color}`}
style={{ width: `${pct}%` }}
/>
</div>

View File

@ -2411,7 +2411,7 @@ export function Inbox() {
selected={selected}
className={
isArchiving
? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
? "pointer-events-none -translate-x-4 scale-(--s-0_98) opacity-0 transition-all duration-200 ease-out"
: "transition-all duration-200 ease-out"
}
desktopMetaLeading={
@ -2624,7 +2624,7 @@ export function Inbox() {
archiveDisabled={isArchiving}
className={
isArchiving
? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
? "pointer-events-none -translate-x-4 scale-(--s-0_98) opacity-0 transition-all duration-200 ease-out"
: "transition-all duration-200 ease-out"
}
/>
@ -2662,7 +2662,7 @@ export function Inbox() {
archiveDisabled={isArchiving}
className={
isArchiving
? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
? "pointer-events-none -translate-x-4 scale-(--s-0_98) opacity-0 transition-all duration-200 ease-out"
: "transition-all duration-200 ease-out"
}
/>
@ -2697,7 +2697,7 @@ export function Inbox() {
archiveDisabled={isArchiving}
className={
isArchiving
? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
? "pointer-events-none -translate-x-4 scale-(--s-0_98) opacity-0 transition-all duration-200 ease-out"
: "transition-all duration-200 ease-out"
}
/>

View File

@ -110,7 +110,7 @@ export function InstanceAccess() {
</p>
</div>
<div className="grid gap-6 lg:grid-cols-[320px_minmax(0,1fr)]">
<div className="grid gap-6 lg:grid-cols-(--gtc-34)">
<section className="space-y-4 rounded-xl border border-border bg-card p-4">
<label className="block space-y-2 text-sm">
<span className="font-medium">Search users</span>

View File

@ -541,7 +541,7 @@ export function InstanceExperimentalSettings() {
/>
</div>
<div className="grid gap-3 sm:grid-cols-[minmax(10rem,14rem)_1fr] sm:items-end">
<div className="grid gap-3 sm:grid-cols-(--gtc-35) sm:items-end">
<label className="space-y-1.5">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<Clock className="h-3.5 w-3.5" />

View File

@ -537,7 +537,7 @@ export function InviteLandingPage() {
return (
<div className="min-h-screen bg-zinc-950 px-6 py-12 text-zinc-100">
<div className="mx-auto max-w-5xl">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.15fr)_minmax(360px,0.85fr)]">
<div className="grid gap-6 lg:grid-cols-(--gtc-36)">
<section className={`${panelClassName} space-y-6`}>
<div className="flex items-start gap-4">
<InviteCompanyLogo

View File

@ -169,7 +169,7 @@ function InviteLandingShell({
}) {
return (
<div className="overflow-hidden rounded-(--rad-28) border border-zinc-800 bg-zinc-950 shadow-(--shadow-extract-16)">
<div className="grid gap-px bg-zinc-800 lg:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]">
<div className="grid gap-px bg-zinc-800 lg:grid-cols-(--gtc-37)">
<section className={cn(panelClassName, "space-y-6 bg-zinc-950")}>{left}</section>
<section className={cn(panelClassName, "h-full bg-zinc-950")}>{right}</section>
</div>
@ -526,7 +526,7 @@ function AuthScreenPreview({ mode, error }: { mode: "sign_in" | "sign_up"; error
function CompanyInvitesPreview() {
return (
<div className="grid gap-5 xl:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
<div className="grid gap-5 xl:grid-cols-(--gtc-38)">
<Card className="rounded-(--rad-28) shadow-none">
<CardHeader className="space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@ -699,7 +699,7 @@ export function InviteUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-(--rad-32) border border-border/70 bg-(image:--gradient-extract-9) shadow-(--shadow-extract-17)">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="grid gap-6 lg:grid-cols-(--gtc-39)">
<div className="p-6 sm:p-7">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-(length:--fs-10) font-semibold uppercase tracking-(--ls-0_24) text-cyan-700 dark:text-cyan-300">
<FlaskConical className="h-3.5 w-3.5" />

View File

@ -128,7 +128,7 @@ export function IssueChatLongThreadPerf() {
</div>
</div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
<div className="grid gap-4 xl:grid-cols-(--gtc-40)">
<main className="min-w-0 space-y-4">
<Card className="border-border/70">
<CardHeader className="pb-2">

View File

@ -137,7 +137,7 @@ export function IssueChatUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-(--rad-32) border border-border/70 bg-(image:--gradient-extract-9) shadow-(--shadow-extract-17)">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="grid gap-6 lg:grid-cols-(--gtc-39)">
<div className="p-6 sm:p-7">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-(length:--fs-10) font-semibold uppercase tracking-(--ls-0_24) text-cyan-700 dark:text-cyan-300">
<FlaskConical className="h-3.5 w-3.5" />

View File

@ -565,7 +565,7 @@ export function OrgChart() {
<div
key={node.id}
data-org-card
className="absolute bg-card border border-border rounded-lg shadow-sm hover:shadow-md hover:border-foreground/20 transition-[box-shadow,border-color] duration-150 cursor-pointer select-none"
className="absolute bg-card border border-border rounded-lg shadow-sm hover:shadow-md hover:border-foreground/20 transition-(--tp-box-shadow-border-color) duration-150 cursor-pointer select-none"
style={{
left: node.x,
top: node.y,

View File

@ -1074,7 +1074,7 @@ function FieldRow({
children: ReactNode;
}) {
return (
<div className="grid gap-2 py-3 text-sm sm:grid-cols-[10rem_minmax(0,1fr)] sm:items-center">
<div className="grid gap-2 py-3 text-sm sm:grid-cols-(--gtc-41) sm:items-center">
<div className="font-medium text-muted-foreground">{label}</div>
<div className="min-w-0">{children}</div>
</div>
@ -1249,7 +1249,7 @@ function StageEventsList({
{events.map((event) => (
<div
key={event.id}
className="grid min-h-11 grid-cols-[6rem_1fr] items-center gap-3 border-b border-border/70 px-3 py-2 text-sm last:border-b-0"
className="grid min-h-11 grid-cols-(--gtc-15) items-center gap-3 border-b border-border/70 px-3 py-2 text-sm last:border-b-0"
>
<span className="text-xs text-muted-foreground" title={new Date(event.createdAt).toLocaleString()}>
{relativeTime(event.createdAt)}
@ -2585,7 +2585,7 @@ export function PipelineSettings() {
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">
<div className="grid gap-3 md:grid-cols-(--gtc-13) md:items-end">
<div className="space-y-3">
<label className="block space-y-1.5 text-sm font-medium">
<span className="sr-only">Pipeline name</span>
@ -2854,7 +2854,7 @@ export function PipelineSettings() {
] as const).map(([label, value, setValue, emptyLabel]) => (
<div
key={label}
className="grid grid-cols-1 items-center gap-2 sm:grid-cols-[minmax(0,1fr)_240px]"
className="grid grid-cols-1 items-center gap-2 sm:grid-cols-(--gtc-42)"
>
<span className="text-sm font-medium">{label}</span>
<select
@ -2870,13 +2870,13 @@ export function PipelineSettings() {
</select>
</div>
))}
<div className="grid grid-cols-1 items-center gap-2 sm:grid-cols-[minmax(0,1fr)_240px]">
<div className="grid grid-cols-1 items-center gap-2 sm:grid-cols-(--gtc-42)">
<span className="text-sm font-medium">Ask for a note when requesting changes</span>
<div className="sm:justify-self-start">
<ToggleSwitch checked={requireRequestChangesReason} onCheckedChange={setRequireRequestChangesReason} />
</div>
</div>
<div className="grid grid-cols-1 items-center gap-2 sm:grid-cols-[minmax(0,1fr)_240px]">
<div className="grid grid-cols-1 items-center gap-2 sm:grid-cols-(--gtc-42)">
<span className="text-sm font-medium">Ask for a note when declining</span>
<div className="sm:justify-self-start">
<ToggleSwitch checked={requireRejectReason} onCheckedChange={setRequireRejectReason} />
@ -2941,7 +2941,7 @@ export function PipelineSettings() {
<>
<div className="divide-y divide-border border-y border-border">
<FieldRow label="Project context">
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="grid gap-2 sm:grid-cols-(--gtc-43)">
<InlineEntitySelector
value={stageProjectId}
options={projectOptions}
@ -3007,7 +3007,7 @@ export function PipelineSettings() {
{selectedAutomationProject && selectedProjectSupportsExecutionWorkspace ? (
<FieldRow label="Execution workspace">
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="grid gap-2 sm:grid-cols-(--gtc-43)">
<select
aria-label="Execution workspace mode"
value={stageExecutionWorkspacePreference || "shared_workspace"}
@ -3228,7 +3228,7 @@ export function PipelineSettings() {
Advance when the last child is done
</span>
</div>
<div className="grid grid-cols-1 items-center gap-2 sm:grid-cols-[5rem_240px]">
<div className="grid grid-cols-1 items-center gap-2 sm:grid-cols-(--gtc-44)">
<span className="text-sm font-medium text-muted-foreground">Move to</span>
<select
aria-label="Move to stage when children finish"

View File

@ -2784,7 +2784,7 @@ export function PipelineItemDetailView({ pipelineId, caseId }: { pipelineId: str
return (
<div className="mx-auto max-w-6xl px-6 py-8">
<div className="mb-6 grid gap-5 lg:grid-cols-[minmax(0,1fr)_340px] lg:items-start lg:gap-8">
<div className="mb-6 grid gap-5 lg:grid-cols-(--gtc-45) lg:items-start lg:gap-8">
<div className="min-w-0">
<div className="mb-2 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Link to="/pipelines" className="hover:text-foreground">Pipelines</Link>
@ -3084,7 +3084,7 @@ export function PipelineItemDetailView({ pipelineId, caseId }: { pipelineId: str
<label
key={option.id}
className={cn(
"grid grid-cols-[18px_minmax(0,1fr)] gap-3 py-1.5 text-sm",
"grid grid-cols-(--gtc-46) gap-3 py-1.5 text-sm",
option.disabled && !option.required ? "text-muted-foreground" : "text-foreground",
)}
>
@ -3230,7 +3230,7 @@ export function PipelineItemDetailView({ pipelineId, caseId }: { pipelineId: str
</section>
) : null}
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_340px]">
<div className="grid gap-8 lg:grid-cols-(--gtc-45)">
<main className="min-w-0 space-y-8">
<PipelineItemBodyDocument
caseId={caseId}
@ -3385,7 +3385,7 @@ export function PipelineItemDetailView({ pipelineId, caseId }: { pipelineId: str
{itemFields.length > 0 ? (
<dl className="divide-y divide-border">
{itemFields.map((field) => (
<div key={field.key} className="grid grid-cols-[120px_1fr] gap-3 py-2 text-sm">
<div key={field.key} className="grid grid-cols-(--gtc-47) gap-3 py-2 text-sm">
<dt className="text-muted-foreground">{field.label}</dt>
<dd className="min-w-0 text-foreground [overflow-wrap:anywhere]">{field.value}</dd>
</div>
@ -3508,7 +3508,7 @@ function WaitingChildRow({
<li>
<Link
to={`/pipelines/${row.case.pipelineId}/items/${row.case.id}`}
className="grid grid-cols-[18px_minmax(0,1fr)_auto] items-start gap-3 py-2 text-sm"
className="grid grid-cols-(--gtc-48) items-start gap-3 py-2 text-sm"
>
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="min-w-0">
@ -4132,7 +4132,7 @@ function BuiltFromTree({
<li key={row.case.id}>
<Link
to={`/pipelines/${row.case.pipelineId}/items/${row.case.id}`}
className="grid grid-cols-[18px_1fr_auto] items-center gap-3 py-3 text-sm hover:bg-muted/40"
className="grid grid-cols-(--gtc-49) items-center gap-3 py-3 text-sm hover:bg-muted/40"
>
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="min-w-0">
@ -4323,7 +4323,7 @@ function DraftItemRow({
return (
<section className={cn("border border-border bg-background", row.expanded && "border-primary")}>
<div className="grid grid-cols-[1fr_auto] items-center gap-3 px-4 py-3">
<div className="grid grid-cols-(--gtc-17) items-center gap-3 px-4 py-3">
<button type="button" className="min-w-0 text-left" onClick={onToggle}>
<span className="block text-xs font-semibold uppercase tracking-(--ls-0_14) text-muted-foreground">Item {index + 1}</span>
<span className="block truncate text-sm font-semibold text-foreground">{title}</span>
@ -4341,7 +4341,7 @@ function DraftItemRow({
</div>
{row.expanded ? (
<div className="grid gap-5 border-t border-border px-4 py-4 lg:grid-cols-[1fr_280px]">
<div className="grid gap-5 border-t border-border px-4 py-4 lg:grid-cols-(--gtc-50)">
<div className="grid gap-4 md:grid-cols-2">
{fields.map((field) => (
<GeneratedField
@ -4622,7 +4622,7 @@ function ReviewQueueDetailDialog({
{fields.length > 0 ? (
<div className="divide-y divide-border rounded-md border border-border">
{fields.map(([key, value]) => (
<div key={key} className="grid grid-cols-[160px_1fr] gap-3 px-3 py-2 text-sm">
<div key={key} className="grid grid-cols-(--gtc-51) gap-3 px-3 py-2 text-sm">
<span className="text-muted-foreground">{humanizeFieldLabel(key)}</span>
<span className="text-foreground">{String(value)}</span>
</div>
@ -4736,7 +4736,7 @@ function ReviewQueueSection({
tabIndex={0}
aria-current={active ? "true" : undefined}
className={cn(
"grid min-h-10 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-2 py-2 text-sm outline-none transition-colors",
"grid min-h-10 grid-cols-(--gtc-13) items-center gap-3 px-2 py-2 text-sm outline-none transition-colors",
active ? "bg-accent/60" : "hover:bg-accent/40",
)}
onMouseEnter={() => onActivate(row.id)}
@ -5221,7 +5221,7 @@ export function Learnings() {
<div
key={event.id}
className={cn(
"grid min-h-11 grid-cols-[6rem_1fr] items-center gap-3 border-b border-border/70 px-3 py-2 text-sm last:border-b-0",
"grid min-h-11 grid-cols-(--gtc-15) items-center gap-3 border-b border-border/70 px-3 py-2 text-sm last:border-b-0",
forcedMove && "border-l-2 border-l-amber-400 bg-amber-50/50 dark:bg-amber-400/10",
)}
>

View File

@ -188,7 +188,7 @@ export function PluginSettings() {
<div className="space-y-8">
<section className="space-y-5">
<h2 className="text-base font-semibold">About</h2>
<div className="grid gap-8 lg:grid-cols-[minmax(0,1.4fr)_minmax(220px,0.8fr)]">
<div className="grid gap-8 lg:grid-cols-(--gtc-52)">
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">Description</h3>
<p className="text-sm leading-6 text-foreground/90">{pluginDescription}</p>
@ -275,7 +275,7 @@ export function PluginSettings() {
</TabsContent>
<TabsContent value="status" className="space-y-6">
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="grid gap-6 xl:grid-cols-(--gtc-39)">
<div className="space-y-6">
<Card>
<CardHeader>

View File

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

View File

@ -148,7 +148,7 @@ function ProjectTilePicker({
<PopoverTrigger asChild>
<button
type="button"
className="shrink-0 rounded-lg cursor-pointer hover:ring-2 hover:ring-foreground/20 transition-[box-shadow]"
className="shrink-0 rounded-lg cursor-pointer hover:ring-2 hover:ring-foreground/20 transition-(--tp-box-shadow)"
aria-label="Change project icon and color"
>
<ProjectTile color={color} icon={icon} size="md" />
@ -192,7 +192,7 @@ function ProjectTilePicker({
<button
type="button"
onClick={() => onSelectColor(null)}
className={`h-6 w-6 cursor-pointer transition-[transform,box-shadow] duration-150 hover:scale-110 ${
className={`h-6 w-6 cursor-pointer transition-(--tp-transform-box-shadow) duration-150 hover:scale-110 ${
color === null
? "ring-2 ring-foreground ring-offset-1 ring-offset-background rounded-md"
: ""
@ -207,7 +207,7 @@ function ProjectTilePicker({
key={swatch}
type="button"
onClick={() => onSelectColor(swatch)}
className={`h-6 w-6 rounded-md cursor-pointer transition-[transform,box-shadow] duration-150 hover:scale-110 ${
className={`h-6 w-6 rounded-md cursor-pointer transition-(--tp-transform-box-shadow) duration-150 hover:scale-110 ${
swatch === color
? "ring-2 ring-foreground ring-offset-1 ring-offset-background"
: "hover:ring-2 hover:ring-foreground/30"

View File

@ -480,7 +480,7 @@ export function ProjectWorkspaceDetail() {
</Tabs>
{activeTab === "configuration" ? (
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(18rem,0.9fr)]">
<div className="grid gap-6 lg:grid-cols-(--gtc-53)">
<div className="space-y-6">
<div className="rounded-2xl border border-border bg-card p-5">
<p className="max-w-2xl text-sm text-muted-foreground">
@ -531,7 +531,7 @@ export function ProjectWorkspaceDetail() {
</select>
</Field>
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
<div className="grid gap-4 md:grid-cols-(--gtc-13)">
<Field label="Local path">
<input
className="w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none"

View File

@ -201,7 +201,7 @@ export function RunTranscriptUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-2xl border border-border/70 bg-(image:--gradient-extract-6) shadow-(--shadow-extract-22)">
<div className="grid gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
<div className="grid gap-6 lg:grid-cols-(--gtc-19)">
<aside className="border-b border-border/60 bg-background/75 p-5 lg:border-b-0 lg:border-r">
<div className="mb-5">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-(length:--fs-10) font-semibold uppercase tracking-(--ls-0_24) text-cyan-700 dark:text-cyan-300">

View File

@ -1523,7 +1523,7 @@ export function Secrets() {
>
<div
role="row"
className="grid grid-cols-[minmax(12rem,2.4fr)_minmax(5.25rem,0.75fr)_minmax(7rem,1fr)_minmax(5rem,0.7fr)_2.75rem] items-center gap-3 bg-muted/40 px-3 py-2 text-xs uppercase tracking-wide text-muted-foreground"
className="grid grid-cols-(--gtc-54) items-center gap-3 bg-muted/40 px-3 py-2 text-xs uppercase tracking-wide text-muted-foreground"
>
<div role="columnheader" className="font-medium">Secret</div>
<div role="columnheader" className="font-medium">Status</div>
@ -1548,7 +1548,7 @@ export function Secrets() {
key={row.id}
role="row"
className={cn(
"grid cursor-pointer grid-cols-[minmax(12rem,2.4fr)_minmax(5.25rem,0.75fr)_minmax(7rem,1fr)_minmax(5rem,0.7fr)_2.75rem] items-center gap-3 border-b border-border/60 px-3 py-3 hover:bg-accent/40",
"grid cursor-pointer grid-cols-(--gtc-54) items-center gap-3 border-b border-border/60 px-3 py-3 hover:bg-accent/40",
row.kind === "company" && selectedSecretId === row.secret.id && "bg-accent/60",
row.kind === "user" && selectedDefinitionId === row.definition.id && "bg-accent/60",
)}
@ -3668,7 +3668,7 @@ function SecretDetailsTab({
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="grid grid-cols-[8rem_minmax(0,1fr)] gap-3 py-2">
<div className="grid grid-cols-(--gtc-55) gap-3 py-2">
<dt className="text-(length:--fs-11) uppercase tracking-wide text-muted-foreground">{label}</dt>
<dd className="min-w-0 text-foreground">{children}</dd>
</div>

View File

@ -138,7 +138,7 @@ export function SystemNoticeUxLab() {
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-(--rad-32) border border-border/70 bg-(image:--gradient-extract-16) shadow-(--shadow-extract-17)">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_320px]">
<div className="grid gap-6 lg:grid-cols-(--gtc-39)">
<div className="p-6 sm:p-7">
<div className="inline-flex items-center gap-2 rounded-full border border-amber-500/25 bg-amber-500/[0.08] px-3 py-1 text-(length:--fs-10) font-semibold uppercase tracking-(--ls-0_24) text-amber-700 dark:text-amber-300">
<FlaskConical className="h-3.5 w-3.5" />

View File

@ -1872,7 +1872,7 @@ export function StepPreview({
const visible = Boolean(visibleSecretKeys[formKey]);
const missingRequired = input.requirement === "required" && (secretValues[formKey] ?? "").trim().length === 0;
return (
<li key={formKey} className="grid gap-2 px-3 py-2 text-sm sm:grid-cols-[minmax(0,1fr)_minmax(14rem,18rem)] sm:items-center">
<li key={formKey} className="grid gap-2 px-3 py-2 text-sm sm:grid-cols-(--gtc-56) sm:items-center">
<div className="flex min-w-0 items-center gap-2">
<KeyRound className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="font-mono text-xs uppercase tracking-wide">{input.key}</span>

View File

@ -104,7 +104,7 @@ function UsageChart({ points }: { points: UserProfileDailyPoint[] }) {
<span>tokens total</span>
</div>
</div>
<div className="mt-6 grid grid-cols-[repeat(14,minmax(0,1fr))] items-end gap-1.5 sm:gap-2">
<div className="mt-6 grid grid-cols-(--gtc-57) items-end gap-1.5 sm:gap-2">
{points.map((point) => {
const tokens = totalTokens(point);
const heightPct = tokens === 0 ? 0 : Math.max(2, Math.round((tokens / maxTokens) * 100));
@ -128,7 +128,7 @@ function UsageChart({ points }: { points: UserProfileDailyPoint[] }) {
);
})}
</div>
<div className="mt-2 grid grid-cols-[repeat(14,minmax(0,1fr))] gap-1.5 text-(length:--fs-10) tabular-nums text-muted-foreground sm:gap-2">
<div className="mt-2 grid grid-cols-(--gtc-57) gap-1.5 text-(length:--fs-10) tabular-nums text-muted-foreground sm:gap-2">
{points.map((point, index) => (
<div key={point.date} className="text-center">
{index === 0 || index === 6 || index === 13 ? formatShortDate(point.date) : null}
@ -177,7 +177,7 @@ function UsageList({
) : (
<ul className="divide-y divide-border">
{rows.map((row) => (
<li key={row.key} className="grid gap-2 py-2.5 sm:grid-cols-[1fr_auto] sm:items-center">
<li key={row.key} className="grid gap-2 py-2.5 sm:grid-cols-(--gtc-17) sm:items-center">
<div className="min-w-0">
<div className="truncate text-sm font-medium">{row.label}</div>
<div className="truncate text-xs text-muted-foreground">{row.sublabel}</div>
@ -310,7 +310,7 @@ export function UserProfile() {
<li key={issue.id}>
<Link
to={issueUrl(issue)}
className="grid gap-2 py-2.5 transition-colors hover:bg-accent/40 sm:grid-cols-[auto_1fr_auto] sm:items-center"
className="grid gap-2 py-2.5 transition-colors hover:bg-accent/40 sm:grid-cols-(--gtc-58) sm:items-center"
>
<span className="font-mono text-xs text-muted-foreground">{issue.identifier ?? issue.id.slice(0, 8)}</span>
<span className="truncate text-sm">{issue.title}</span>
@ -335,7 +335,7 @@ export function UserProfile() {
) : (
<ul className="divide-y divide-border">
{data.recentActivity.map((event) => (
<li key={event.id} className="grid gap-2 py-2.5 sm:grid-cols-[1fr_auto] sm:items-center">
<li key={event.id} className="grid gap-2 py-2.5 sm:grid-cols-(--gtc-17) sm:items-center">
<div className="min-w-0">
<div className="truncate text-sm">{event.action.replaceAll("_", " ")}</div>
<div className="truncate text-xs text-muted-foreground">