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:
parent
be63424987
commit
e0bbf3c911
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 ?? ""}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}%` }}
|
||||
|
|
|
|||
|
|
@ -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 ?? ""}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
201
ui/src/index.css
201
ui/src/index.css
|
|
@ -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 ── */
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue