refactor(ui): extract size/spacing/radius/shadow values to tokens (Phase 2 batch 3/4)

Batch 3/4 of the design-token extraction (design/token-extraction). Replaces
hardcoded Tailwind arbitrary-bracket values for width/height/min/max,
padding/margin/gap/inset/translate, radius (incl. directional), ring/outline
width, and box-shadow in ui/src/components/** and ui/src/pages/** (including
*.test.tsx companions) with references to new CSS custom-property tokens in
ui/src/index.css. Zero visual change, proven by the Phase 0 Storybook
snapshot baseline.

Counts:
- 407 sites rewritten across 140 files (127 component/page + 13 test files
  updated in lockstep with the components they assert against).
- 163 new tokens minted: 127 --sz-* (one shared family across
  width/height/spacing/gap/inset/translate lengths, including 42
  --sz-calc-N compound calc()/min()/max()/clamp() expressions and 2
  --sz-safe-* env(safe-area-inset-*) wrappers), 8 --rad-* (shared across
  radius/ring-width/outline-width), 5 --pct-* (incl. --pct-neg-50 kept
  distinct per the source bracket's own negative sign), 23
  --shadow-extract-* (38 sites, deduped by exact value).
- 11 var()-only passthrough sites (--radix-*-trigger-width/height,
  --new-issue-dialog-height) rewritten to the bare paren form with no new
  token minted, per DESIGN.md's runtime-library-variable special case.
- 3 sites resolved Tailwind's build-time calc(theme(spacing.N)-Mpx) into a
  byte-equivalent runtime calc() (components/IssueRow.tsx), verified via
  headless-browser computed-style comparison against the pre-change built
  CSS.
- 2 sites skipped/allowlisted: rounded-[inherit] (CSS keyword, not a
  literal) and drop-shadow-[...] (different utility than this batch's
  shadow-[...] mandate) — logged as a Needs-human-decision item.

Three gotchas found and fixed during verification (all documented in the
codemod header and TOKEN-AUDIT.md's Batch 3 log): a boundary regex bug that
false-positived inside Tailwind's own slide-out-to-top-[1%] compound
utility, a calc()-operator-spacing bug that could corrupt
env(safe-area-inset-*) identifiers, and a generated-CSS-comment bug where a
literal "*/" substring in prose silently dropped the entire token block
from the compiled CSS (root cause of an initial 222-test snapshot failure,
fully resolved to 510/510 passing).

Verify: rg gates clean, pnpm build-storybook exit 0, Storybook visual
snapshot suite 510/510 (509 clean + 1 known-benign flaky retry), pnpm
typecheck exit 0, codemod re-run confirmed idempotent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
scotttong 2026-07-06 15:15:08 -07:00
parent 4b3f087d7f
commit be63424987
143 changed files with 1079 additions and 330 deletions

View File

@ -316,3 +316,49 @@ Codemod: `scripts/codemod-extract-type.mjs`. Unlike Batch 1's hand-audited site
- Codemod re-run confirmed idempotent: second invocation reports 0 sites rewritten, 0 files changed, token block already present.
**Needs human decision:** none new from this batch beyond the already-logged #2 (micro type-size cluster) and #3 (letter-spacing cluster) in section 8 above — this batch's 17 minted tokens are exactly the verbatim inventory those two items describe, now materialized as CSS custom properties ready for a human to collapse into a real scale.
---
## Phase 2 extraction log — Batch 3 (sizes/spacing/radius/shadows)
Codemod: `scripts/codemod-extract-sizes.mjs`. Blanket regex sweep (like Batch 2 — these bracket-utility families are unambiguous), scoped to `ui/src/components/**` and `ui/src/pages/**` including `*.test.tsx` companions. Covers `w-[...] h-[...] size-[...] min-w/max-w/min-h/max-h-[...]`, `p*/m*-[...]`, `gap-[...]`/`gap-x/y-[...]`, `inset/inset-x/inset-y/top/left/right/bottom-[...]`, `translate-x/y-[...]`, `rounded-[...]` (incl. directional `rounded-t/r/b/l/tl/tr/bl/br-[...]`), `shadow-[...]`, `ring-[...]`, `outline-[...]`. Verified idempotent (second run: 0 sites, 0 files changed).
**Sites rewritten: 407**, across 140 files (127 component/page files + 13 test files rewritten in lockstep: `ChatComposer.test.tsx`, `FileViewerSheet.test.tsx`, `IssueChatThread.test.tsx`, `IssueFiltersPopover.test.tsx`, `IssueSiblingNavigation.test.tsx`, `MarkdownBody.test.tsx`, `MarkdownEditor.test.tsx`, `NewIssueDialog.test.tsx`, `RoutineRunVariablesDialog.test.tsx`, `SidebarAccountMenu.test.tsx`, `artifacts/ArtifactCard.test.tsx`, `Agents.test.tsx`, `IssueDetail.test.tsx`).
**Tokens minted: 163 new** (127 `--sz-*` + 8 `--rad-*` + 5 `--pct-*` + 23 `--shadow-extract-*`; 0 reused from Batches 1-2 — no existing size/radius/shadow tokens existed to match against, and DESIGN.md/TOKEN-AUDIT.md section 6.3 already noted zero `--shadow-*` tokens existed pre-Phase-2).
- **`--sz-*` (127 tokens)** — ONE shared family across width/height/min/max, padding/margin, gap, inset/top/left/right/bottom, and translate, so identical literal values dedupe regardless of which property used them (e.g. a `220px` used as both `h-[220px]` and `w-[220px]` in different files collapses to one `--sz-220px`). Includes:
- 85 simple numeric length tokens (`--sz-320px`, `--sz-18rem`, `--sz-85vh`, `--sz-24ch`, etc. — px/rem/em/vh/vw/dvh/dvw/ch units, verbatim, no rounding).
- 42 `--sz-calc-N` tokens for `calc()`/`min()`/`max()`/`clamp()` compound expressions (sequentially numbered since content isn't safely nameable); 2 sites reused an existing `--sz-calc-N` where the exact string recurred (`min(calc(100dvh - 2rem),42rem)`-style forms did not recur verbatim, but `theme(spacing.N)` resolution below did feed distinct calcs — no cross-file duplicate calc strings were found this batch).
- 2 `--sz-safe-*` tokens (`--sz-safe-top`, `--sz-safe-bottom`) for bare `env(safe-area-inset-*)` forms with no arithmetic — `Layout.tsx`, `MobileBottomNav.tsx`. `env()` forms MIXED into a `calc()`/`min()`/`max()` (e.g. `calc(5rem+env(safe-area-inset-bottom))`, `max(1rem,env(safe-area-inset-top))`) were folded into the `--sz-calc-N` family instead, verbatim (`env()` is valid inside a runtime custom property — confirmed in the Step 0 spike).
- **`--rad-*` (8 tokens)** — shared across radius, ring-width, and outline-width (`--rad-2` through `--rad-32`, bare-number naming to match Batch 1/2's `--fs-11`-style convention since every site here used px). Two directional-radius sites (`IssueChatThread.tsx`'s `rounded-br-[4px]`/`rounded-bl-[4px]` speech-tail corners) reuse the same `--rad-4` token minted from `StatusBadge.tsx`'s bare `rounded-[4px]`.
- **`--pct-*` (5 tokens)** — `--pct-50`, `--pct-72`, `--pct-85`, `--pct-90` (bare percentages) + `--pct-neg-50` (kept as a DISTINCT token from `--pct-50`, not a negated reference, because the source bracket already carries the minus sign inside the value itself — `translate-x-[-50%]`, not `-translate-x-[50%]` — confirmed via the Step 0 spike that both the `-utility-(--x)` prefix-negation form AND a literal negative-value token both compile correctly, and the codebase's actual sites are all the latter form).
- **`--shadow-extract-*` (23 tokens)** for 38 sites (dedup: `--shadow-extract-15` = `0 24px 60px rgba(15,23,42,0.08)` reused at 5 sites; `--shadow-extract-17` = `0 30px 80px rgba(15,23,42,0.10)` reused at 3 sites — matching the two clusters TOKEN-AUDIT.md section 3.6 flagged). Underscore-to-space reversal applied to every shadow value per Batch 1's gradient gotcha, generalized (`shadow-[0_0_0_2px_hsl(var(--background))]` → `0 0 0 2px hsl(var(--background))`). Two sites are `dark:` variant pairs sharing the same base shadow token family but pointing at different tokens (`ChatComposer.tsx`/`IssueChatThread.tsx`/`ChatComposer.test.tsx`: light `shadow-(--shadow-extract-4)`, `dark:shadow-(--shadow-extract-5)`).
- All new tokens live in a third non-`@theme` `:root { ... }` block appended to `ui/src/index.css` immediately after Batch 2's type block, headed `/* ── Extracted verbatim SIZE/SPACING/RADIUS/SHADOW tokens (Phase 2 Batch 3, design/token-extraction) ── */`, per DESIGN.md (runtime-tunable).
**var()-only passthrough (11 sites, 0 new tokens minted per DESIGN.md's special case)** — bracket values that only wrap a runtime library/component variable are rewritten straight to the bare paren form with no token mint: `w-[var(--radix-popover-trigger-width)]` → `w-(--radix-popover-trigger-width)` (`SearchableSelect.tsx` ×1, `OnboardingWizard.tsx` ×1, `AgentConfigForm.tsx` ×3), `h-[var(--radix-select-trigger-height)]` / `min-w-[var(--radix-select-trigger-width)]` → same form (`ui/select.tsx` ×2), `h-[var(--new-issue-dialog-height)]` / `max-h-[var(--new-issue-dialog-height)]` → same form (`NewIssueDialog.tsx` ×2, `NewIssueDialog.test.tsx` ×2 in lockstep).
**`theme(spacing.N)` resolution (`components/IssueRow.tsx`, 3 sites)** — `theme()` is a Tailwind build-time function and does not work inside a runtime CSS custom property. Resolved using Tailwind v4's default `--spacing: 0.25rem` base (confirmed no `--spacing` override exists in `index.css`, and the BUILT CSS for these exact classes was inspected before the codemod ran: `padding-left:calc(.5rem - 2px)` / `calc(.25rem - 2px)` for `theme(spacing.2)`/`theme(spacing.1)` respectively, and a plain `margin-left:1.25rem` for the fully-constant `theme(spacing.3)+theme(spacing.2)` expression). Minted as `--sz-calc-11: calc(0.5rem - 2px)`, `--sz-calc-12: calc(0.25rem - 2px)`, `--sz-calc-13: calc(0.75rem + 0.5rem)` — byte-equivalent computed output verified via headless-browser spike (`6px`/`4px`/`1.25rem` respectively, matching the pre-codemod built CSS exactly).
**Sites allowlisted / intentionally skipped (2 items):**
1. `components/ui/scroll-area.tsx` — `rounded-[inherit]` is a CSS **keyword**, not a literal value; the codemod explicitly detects and skips it (no rewrite, no token). Documented in the `index.css` block comment.
2. `components/CompanyPatternIcon.tsx` — `drop-shadow-[0_1px_2px_rgba(0,0,0,0.65)]` is a **different Tailwind utility** (`drop-shadow`, a CSS `filter` function) than the batch mandate's `shadow-[...]` (`box-shadow`) pattern; left untouched as out of the batch's explicit scope (TOKEN-AUDIT.md section 3.6 also only inventoried `shadow-[...]`, not `drop-shadow-[...]`) — flagged here for a future batch or human decision on whether `drop-shadow` should be folded into the same `--shadow-extract-*` family or get its own.
**BOUNDARY BUG caught and fixed during this batch (documented for future batches):** the first codemod pass used a bare `\b` word-boundary regex for `top/left/right/bottom-[...]`, which false-positived on Tailwind's own compound animation utilities `slide-out-to-top-[1%]` / `slide-in-from-top-[1%]` (`components/ui/dialog.tsx`, `components/ui/alert-dialog.tsx`) — `\b` matches at the `-to-top` boundary since `-` is not a word character, so the regex silently corrupted `slide-out-to-top-[1%]` into `slide-out-to-top-(--pct-1)`, which Tailwind does not recognize as the `slide-out-to-top-*` utility at all (a real, silent visual/behavioral regression, not just a naming nit — caught by manual inspection before the Playwright run, not by the snapshot suite itself, since the corrupted form simply drops the animation rather than changing a static rendered pixel in a way the frozen-time/reduced-motion snapshot harness would catch). Fixed by requiring the utility name to start at a genuine class-token boundary (preceded by whitespace, a quote/backtick, template-literal `${`, colon `:` for variant prefixes, or start-of-string) rather than a bare `\b`.
**CALC-SPACING BUG caught and fixed during this batch (second gotcha, generalizing Batch 1's underscore lesson):** two distinct spacing problems in calc() expressions, both caught via the Step 0 spike and a subsequent live headless-browser check:
1. `calc(100%-2rem)`-style brackets (NO space around the operator at all, not even underscore-escaped) are syntactically INVALID once lifted verbatim into a CSS custom property — a headless-browser test confirmed `calc(100%-2rem)` silently drops (computed value falls back to the containing block's own size) while `calc(100% - 2rem)` computes correctly. Fixed with a `normalizeCalcSpacing()` helper that inserts spacing around top-level `+`/`-` operators.
2. The first version of that helper was too broad (matched any letter-hyphen-letter sequence) and corrupted `env(safe-area-inset-bottom)` into `env(safe - area - inset - bottom)` inside `calc(1.5rem+5rem+env(safe-area-inset-bottom))` — caught via manual `grep`/inspection of the generated token block before the Playwright run (the malformed token would have made the whole value invalid, again a silent drop rather than a pixel diff the snapshot suite would flag directly, though it likely would have surfaced as a layout diff on `ScrollToBottom.tsx`'s stories). Fixed by narrowing the operator-spacing regex to only fire when the LEFT side is a number/number-with-unit/closing-paren and the RIGHT side is a number or the start of a known CSS value-function call (`env(`, `var(`, `calc(`, `min(`, `max(`, `clamp(`) — this excludes any hyphen sitting between two bare identifier characters.
**COMMENT-SYNTAX BUG caught and fixed during this batch (third gotcha, new category — a lesson for future codemods' generated CSS comments, not the token values themselves):** the codemod's own generated `index.css` doc-comment originally read "`--rad-* is likewise shared across rounded-*/ring/outline widths.`" — the literal two-character sequence `*/` inside "rounded-\*/ring" prematurely closed the enclosing CSS block comment, and everything from that point to the real intended `*/` several lines later was parsed as raw (garbled, non-declaration) CSS. This didn't just corrupt the comment — it desynced the CSS parser badly enough that the ENTIRE Batch 3 `:root { ... }` token block (all 163 tokens) was silently dropped from the compiled Storybook CSS, while the hundreds of `var(--sz-*)`/`var(--rad-*)` REFERENCES in component class names still compiled fine (Tailwind generates utility classes independent of whether the referenced custom property is actually declared anywhere) — so every `--sz-*`/`--rad-*`/`--pct-*`/`--shadow-extract-*` reference resolved to nothing (`unset`, i.e. `0` for lengths), which is what produced the initial 222-test Playwright failure (all length/radius/shadow values silently collapsing to their initial/zero value). Caught by: (a) the Playwright run failing far more broadly than a mechanical rename should cause, (b) isolating a single-file change that passed in isolation but failed in the full batch, (c) directly querying `getComputedStyle(...).marginTop` in a live headless-browser session against the built Storybook static output, which showed `0px` instead of the expected `7px`, and (d) a manual `/*`/`*/` balance count over the generated CSS block, which found the block was one `*/` short of balanced. Fixed by rephrasing the comment to avoid a bare `*/`-forming substring ("rounded, ring, and outline widths" instead of "rounded-\*/ring/outline widths") and adding an inline guard-comment in the codemod script itself warning future editors never to let generated CSS-comment prose contain a literal `*/` sequence.
**Verify results:**
- `rg` gates clean in `ui/src/components/**` / `ui/src/pages/**`: zero remaining `w-[...]/h-[...]/size-[...]/min-w-[...]/max-w-[...]/min-h-[...]/max-h-[...]`, zero `p*/m*-[...]`, zero `gap-[...]`/`gap-x/y-[...]`, zero `inset/inset-x/inset-y/top/left/right/bottom-[...]` (except Tailwind's own `slide-out-to-top-[1%]`/`slide-in-from-top-[1%]` compound utilities, which are NOT this batch's target pattern), zero `translate-x/y-[...]`, zero `rounded*-[...]` (except the documented `rounded-[inherit]` keyword skip), zero `shadow-[...]` (except the out-of-scope `drop-shadow-[...]` noted above), zero `ring-[...]`, zero `outline-[...]`.
- `pnpm build-storybook` exit 0.
- Storybook visual snapshot suite: **510/510 passed** on the final verification run (509 passed clean + 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, exit code 0). An earlier run with the COMMENT-SYNTAX BUG present failed 222/510 (all attributable to the single root cause above, not 222 independent regressions); after the fix, first clean re-run passed 510/510 with zero retries needed, and the final verification run (rebuilt from scratch a second time) reproduced the single known-benign flaky retry only.
- `pnpm typecheck` exit 0.
- Codemod re-run confirmed idempotent: second invocation reports 0 sites rewritten, 0 files changed, token block already present.
**Needs human decision (new, from this batch):**
- **`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.

View File

@ -0,0 +1,517 @@
#!/usr/bin/env node
/**
* codemod-extract-sizes.mjs
*
* Phase 2 (extraction), Batch 3/4 of the design-token audit
* (branch design/token-extraction). Replaces hardcoded SIZE / SPACING /
* RADIUS / SHADOW arbitrary Tailwind bracket values 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 (all unambiguous bracket-literal Tailwind utilities —
* like Batch 2's font-size/tracking sweep, a blanket regex is safe here
* because none of these utility prefixes have a non-dimensional meaning):
* w-[...] h-[...] size-[...] min-w-[...] max-w-[...] min-h-[...] max-h-[...]
* p-[...] pt/pb/pl/pr/px/py-[...] m-[...] mt/mb/ml/mr/mx/my-[...]
* gap-[...] gap-x-[...] gap-y-[...]
* inset-[...] inset-x/y-[...] top/left/right/bottom-[...]
* translate-x-[...] translate-y-[...]
* rounded-[...] and directional rounded-t/r/b/l/tl/tr/br/bl-[...]
* shadow-[...] ring-[...] outline-[...]
* `!`-important-prefixed utilities (`!max-w-[90%]`) and variant/arbitrary-
* variant prefixes (`sm:`, `dark:`, `[&>x]:`, etc.) are preserved verbatim —
* the regex only rewrites the bracket portion itself.
*
* BOUNDARY GOTCHA (found + fixed during this batch's dry run): a naive
* `\b(top|left|right|bottom)-\[...\]` regex false-positives on Tailwind's
* OWN compound animation utilities `slide-out-to-top-[1%]` /
* `slide-in-from-top-[1%]` (`components/ui/dialog.tsx`,
* `components/ui/alert-dialog.tsx`) — `\b` matches at the `-to-top`
* boundary because `-` is not a word character, so the regex would rewrite
* only the `top-[1%]` tail of a longer utility name and silently corrupt
* `slide-out-to-top-[1%]` into `slide-out-to-top-(--pct-1)` (which Tailwind
* would not recognize as the `slide-out-to-top-*` animation-direction
* utility at all — a real visual regression, not just a naming nit). Fixed
* by requiring the utility name to start at a genuine class-token boundary
* (preceded by whitespace, a quote character, backtick, template-literal
* `${`, or the start of the string) rather than a bare `\b`.
*
* Token naming (verbatim value, no normalizing/rounding/unit-conversion):
* --sz-<value><unit> width/height/spacing lengths — ONE shared family so
* identical values dedupe across w/h/p/m/gap/inset/etc.
* e.g. --sz-320: 320px; --sz-0_7rem: 0.7rem;
* --sz-85vh: 85vh; --sz-24ch: 24ch;
* --sz-calc-<n> calc(...)/min()/max()/clamp() forms, sequentially
* numbered (values are not safely nameable by content
* without ambiguity), each with an inline comment.
* --sz-safe-<edge> bare env(safe-area-inset-<edge>) forms, verbatim.
* --pct-<N> bare percentage values, e.g. --pct-50: 50%;
* --pct-neg-<N> negative percentage values as they literally
* appear in the bracket (e.g. translate-x-[-50%]),
* e.g. --pct-neg-50: -50%; (kept as a DISTINCT token
* from --pct-50 rather than negated at the utility
* level, since the source bracket already carries the
* minus sign inside the value, not as a utility
* prefix — see Step 0 spike notes in TOKEN-AUDIT.md).
* --rad-<value> radius/ring/outline width values (unitless numeric
* suffix, unit implied px unless the source used a
* different unit, in which case the unit is appended
* to the name for disambiguation), e.g. --rad-8: 8px;
* --shadow-extract-<n> shadow values verbatim (multi-stop shadows are not
* nameable by content), each with an inline comment.
* Underscore-to-space reversal applied (Tailwind's
* own bracket-escaping convention; see Batch 1's
* gradient gotcha, generalized here to shadows).
*
* CALC-SPACING GOTCHA (Batch 1's underscore lesson, generalized): Tailwind's
* bracket escaping uses `_` for literal spaces (`calc(-50%_-_2px)`), but
* `calc(100%-2rem)` (WITHOUT underscores, i.e. no space around the operator
* AT ALL in the original bracket) is *also* present in this codebase and is
* INVALID CSS once lifted verbatim into a custom property — verified via a
* headless-browser Step 0 spike: `calc(100%-2rem)` silently drops (falls
* back to the containing block's width) while `calc(100% - 2rem)` (spaces
* added) computes correctly. Every calc() lifted into a token by this
* codemod therefore has spaces normalized around its top-level `+`/`-`
* operators (percent/length arithmetic), in addition to underscore reversal.
*
* Tailwind v4 paren-shorthand rewrite forms used (confirmed via mandatory
* Step-0 syntax spike — scratch story + `pnpm build-storybook` + grep of
* emitted CSS, then deleted before this codemod ran):
* w-[320px] -> w-(--sz-320) width:var(--sz-320)
* max-h-[85vh] -> max-h-(--sz-85vh) max-height:var(...)
* p-[18px] -> p-(--sz-18) padding:var(...)
* gap-[3px] -> gap-(--sz-3) gap:var(...)
* rounded-[8px] -> rounded-(--rad-8) border-radius
* rounded-br-[4px] -> rounded-br-(--rad-4) border-bottom-right-radius
* shadow-[...] -> shadow-(--shadow-extract-N) box-shadow (bare
* form works — no `shadow:` hint needed; verified
* identical emitted rule to the hinted form).
* top-[50%] -> top-(--pct-50) top:var(...)
* translate-x-[-50%] -> translate-x-(--pct-neg-50) --tw-translate-x:var(...)
* pb-[env(safe-area-inset-bottom)] -> pb-(--sz-safe-bottom) padding-bottom
* w-[var(--radix-popover-trigger-width)] -> w-(--radix-popover-trigger-width)
* (brackets that only wrap a var() reference a runtime library variable
* directly — no new token minted, per DESIGN.md special-case guidance).
*
* ring-[Npx] / outline-[Npx] note: `ring`/`outline` are box-shadow/outline-
* width utilities whose bracket form sets a WIDTH, not a generic length or
* shadow value — and `ring`/`outline` ALSO have color-bracket forms
* (`ring-[#hex]`). The correct paren-shorthand hint is `ring-(length:--x)` /
* `outline-(length:--x)` (spiked and confirmed correct — a bare `ring-(--x)`
* would be ambiguous the same way `text-(--x)` is). No ring/outline color-
* bracket sites exist in this codebase (verified during Step 0 inventory),
* so only the width form is handled.
*
* `calc(theme(spacing.N)±Mpx)` forms (components/IssueRow.tsx): theme() is a
* Tailwind BUILD-TIME function and does not work inside a runtime CSS custom
* property. This codemod resolves theme(spacing.N) using Tailwind v4's
* default `--spacing: 0.25rem` base (confirmed via Step 0: no `--spacing`
* override exists in index.css, and the ACTUAL BUILT CSS for these exact
* classes was inspected before this codemod ran and shows
* `padding-left:calc(.5rem - 2px)` / `calc(.25rem - 2px)` for
* theme(spacing.2)/theme(spacing.1) respectively, and a plain
* `margin-left:1.25rem` for the fully-constant
* theme(spacing.3)+theme(spacing.2) expression, i.e. Tailwind pre-resolves
* it when both operands are compile-time constants). The resolved-equivalent
* calc() is minted as the token value (byte-equivalent computed output
* verified via headless-browser spike, see TOKEN-AUDIT.md Batch 3 log).
*
* Idempotent: the FIND regex only matches the ORIGINAL bracket-literal form;
* once rewritten the pattern no longer matches, so re-running is a no-op.
*
* Usage: node scripts/codemod-extract-sizes.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. No site in this batch uses
// `\_` (escaped underscore meaning a LITERAL underscore) — verified by
// inspection of every match — so a plain global replace is safe.
function unescapeSpaces(value) {
return value.replace(/_/g, " ");
}
// Ensures whitespace around top-level +/- operators inside calc()/min()/
// max()/clamp() so the expression is valid once it's the value of a runtime
// custom property (see CALC-SPACING GOTCHA in the header comment).
//
// DELIBERATELY NARROW: only rewrites a +/- whose LEFT side is a number,
// number+unit, or a closing paren `)`, and whose RIGHT side is a number or
// the start of a known CSS value-function call (`env(`, `var(`, `calc(`,
// `min(`, `max(`, `clamp(`). This excludes any hyphen that sits between two
// bare identifier characters — critical because a naive "any letter-hyphen-
// letter" rule corrupts `env(safe-area-inset-bottom)` into
// `env(safe - area - inset - bottom)` (caught during this batch's dry run;
// see CALC-SPACING GOTCHA). A leading unary minus right after `(`/`,`/start
// (e.g. `min(-40px, ...)`) is also left untouched since nothing precedes it
// on the left to match.
const CALC_UNIT = "(?:px|rem|em|vh|vw|dvh|dvw|svh|svw|ch|%)";
const CALC_NUM = "[0-9]*\\.?[0-9]+";
const CALC_FUNC = "(?:env|var|calc|min|max|clamp)\\(";
const CALC_SPACING_RE = new RegExp(
`(${CALC_NUM}${CALC_UNIT}|${CALC_NUM}|\\))\\s*([+-])\\s*(?=${CALC_NUM}|${CALC_FUNC})`,
"g",
);
function normalizeCalcSpacing(value) {
return value.replace(CALC_SPACING_RE, (m, left, op) => `${left} ${op} `);
}
// "320" "0.7rem" "-50" etc -> safe token-name suffix. Dots become
// underscores (matches Batch 2's `--fs-0_7rem` convention); a leading minus
// is spelled out as `neg-` for readability.
function safeSuffix(value) {
let v = value;
const negative = v.startsWith("-");
if (negative) v = v.slice(1);
v = v.replace(/\./g, "_");
return negative ? `neg-${v}` : v;
}
// ── Token registries ────────────────────────────────────────────────────
const szTokens = new Map(); // --sz-* (width/height/spacing/gap/inset lengths, shared family)
const radTokens = new Map(); // --rad-* (radius/ring-width/outline-width, shared family)
const pctTokens = new Map(); // --pct-* / --pct-neg-*
const shadowTokens = new Map(); // --shadow-extract-*
let calcCounter = 0;
let shadowCounter = 0;
function registerSzToken(rawValue, unit, sourceNote) {
const value = `${rawValue}${unit}`;
const name = `sz-${safeSuffix(rawValue)}${unit}`;
if (!szTokens.has(name)) szTokens.set(name, { value, comment: sourceNote });
return name;
}
// Percentages: --pct-50 for "50", --pct-neg-50 for "-50" (kept distinct per
// DESIGN.md Step-0 spike guidance — the minus sign is IN the bracket value).
function registerPctToken(rawNum, sourceNote) {
const name = `pct-${safeSuffix(rawNum)}`;
const value = `${rawNum}%`;
if (!pctTokens.has(name)) pctTokens.set(name, { value, comment: sourceNote });
return name;
}
// Radius / ring-width / outline-width family, shared. Unit is only appended
// to the name when it's not a bare px number (px is the overwhelming
// majority and matches Batch 1/2's bare-number convention, e.g. --fs-11).
function registerRadToken(rawValue, unit, sourceNote) {
const value = `${rawValue}${unit}`;
const name = unit === "px" ? `rad-${safeSuffix(rawValue)}` : `rad-${safeSuffix(rawValue)}${unit}`;
if (!radTokens.has(name)) radTokens.set(name, { value, comment: sourceNote });
return name;
}
// calc()/env()/min()/max()/clamp() forms and any other compound expression:
// minted 1:1 per distinct (already-unescaped, space-normalized) string into
// the --sz-* family, named sequentially (content isn't safely nameable) with
// a comment noting origin. Reused across sites if the exact string recurs.
const calcByValue = new Map(); // normalized value -> token name
function registerCalcToken(normalizedValue, sourceNote) {
if (calcByValue.has(normalizedValue)) return calcByValue.get(normalizedValue);
calcCounter += 1;
const name = `sz-calc-${calcCounter}`;
szTokens.set(name, { value: normalizedValue, comment: sourceNote });
calcByValue.set(normalizedValue, name);
return name;
}
function registerSafeAreaToken(edge, unescapedValue, sourceNote) {
const name = `sz-safe-${edge}`;
if (!szTokens.has(name)) szTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
const shadowByValue = new Map(); // unescaped shadow value -> token name
function registerShadowToken(unescapedValue, sourceNote) {
if (shadowByValue.has(unescapedValue)) return shadowByValue.get(unescapedValue);
shadowCounter += 1;
const name = `shadow-extract-${shadowCounter}`;
shadowTokens.set(name, { value: unescapedValue, comment: sourceNote });
shadowByValue.set(unescapedValue, name);
return name;
}
// ── Classification of a raw bracket value ───────────────────────────────
// Returns { tokenRef, isVarOnly } where tokenRef is the FULL replacement
// content to place inside `(...)`, e.g. "--sz-320" or
// "--radix-popover-trigger-width" (var-only passthrough, no new token).
const SIMPLE_LENGTH_RE = /^(-?[0-9.]+)(px|rem|em|vh|vw|dvh|dvw|svh|svw|ch|%)$/;
const VAR_ONLY_RE = /^var\((--[a-zA-Z0-9-]+)\)$/;
const ENV_ONLY_RE = /^env\((safe-area-inset-[a-z]+)\)$/;
const THEME_SPACING_RE = /theme\(spacing\.([0-9.]+)\)/g;
// Tailwind v4 default spacing scale: --spacing: 0.25rem (confirmed via Step 0:
// no --spacing override exists in ui/src/index.css).
const TAILWIND_SPACING_BASE_REM = 0.25;
function resolveThemeSpacing(raw) {
// Resolves theme(spacing.N) tokens to their rem equivalents so the
// expression can live inside a runtime CSS custom property (theme() is a
// Tailwind build-time function and does not work at runtime).
return raw.replace(THEME_SPACING_RE, (_m, n) => {
const rem = Number(n) * TAILWIND_SPACING_BASE_REM;
// Keep a leading zero (0.5rem, not .5rem) for readability; verified
// byte-equivalent computed output vs Tailwind's own `.5rem` form.
return `${rem}rem`;
});
}
function classifyBracketValue(raw, { kind, sourceNote }) {
// 1) Bare var() passthrough — no new token, per DESIGN.md special case.
const varOnly = raw.match(VAR_ONLY_RE);
if (varOnly) return { tokenRef: varOnly[1], isVarOnly: true };
// 2) Bare env(safe-area-inset-*) passthrough — mint a --sz-safe-<edge> token.
const envOnly = raw.match(ENV_ONLY_RE);
if (envOnly) {
const edge = envOnly[1].replace("safe-area-inset-", "");
const name = registerSafeAreaToken(edge, `env(${envOnly[1]})`, sourceNote);
return { tokenRef: `--${name}`, isVarOnly: false };
}
// 3) Simple numeric length / percentage.
const simple = raw.match(SIMPLE_LENGTH_RE);
if (simple) {
const [, num, unit] = simple;
if (unit === "%") {
const name = registerPctToken(num, sourceNote);
return { tokenRef: `--${name}`, isVarOnly: false };
}
if (kind === "radius") {
const name = registerRadToken(num, unit, sourceNote);
return { tokenRef: `--${name}`, isVarOnly: false };
}
const name = registerSzToken(num, unit, sourceNote);
return { tokenRef: `--${name}`, isVarOnly: false };
}
// 4) Everything else: calc()/min()/max()/clamp()/env()-mixed compound
// expressions. Reverse Tailwind's underscore-space escaping, normalize
// calc operator spacing, resolve any theme(spacing.N) build-time calls,
// and mint a sequential --sz-calc-N.
let normalized = unescapeSpaces(raw);
normalized = resolveThemeSpacing(normalized);
normalized = normalizeCalcSpacing(normalized);
const name = registerCalcToken(normalized, sourceNote);
return { tokenRef: `--${name}`, isVarOnly: false };
}
// ── Regexes for each utility family ──────────────────────────────────────
// Every regex requires the utility to start at a genuine class-token
// boundary — preceded by whitespace, a quote/backtick, template-literal
// `${`, or the start of the string — NOT a bare `\b`, which would
// false-positive inside compound utility names like
// `slide-out-to-top-[1%]` (see BOUNDARY GOTCHA in the header comment).
// Each captures: (1) optional `!important` prefix, (2) the utility name
// (with any directional/axis suffix), (3) the raw bracket contents.
// `:` is included because Tailwind variant prefixes (`sm:`, `dark:`,
// `focus-visible:`, `data-[state=open]:`, etc.) always precede the utility
// name with a colon — a safe class-token boundary, never part of a longer
// utility's own name.
const BOUNDARY = String.raw`(?<=^|[\s"'\`{:])`;
const UTILITIES = [
// width/height/size family
{ re: new RegExp(`${BOUNDARY}(!?)(w|h|size|min-w|max-w|min-h|max-h)-\\[([^\\]]+)\\]`, "g"), kind: "length" },
// padding/margin family
{ re: new RegExp(`${BOUNDARY}(!?)(p|pt|pb|pl|pr|px|py|m|mt|mb|ml|mr|mx|my)-\\[([^\\]]+)\\]`, "g"), kind: "length" },
// gap family
{ re: new RegExp(`${BOUNDARY}(!?)(gap|gap-x|gap-y)-\\[([^\\]]+)\\]`, "g"), kind: "length" },
// inset / top / left / right / bottom family
{ re: new RegExp(`${BOUNDARY}(!?)(inset-x|inset-y|inset|top|left|right|bottom)-\\[([^\\]]+)\\]`, "g"), kind: "length" },
// translate family
{ re: new RegExp(`${BOUNDARY}(!?)(translate-x|translate-y)-\\[([^\\]]+)\\]`, "g"), kind: "length" },
];
// Radius: bare `rounded-[...]` and directional `rounded-t/r/b/l/tl/tr/bl/br-[...]`.
// `rounded-[inherit]` is a KEYWORD (not a numeric literal) — per the batch
// mandate, skip it (documented in the extraction log / allowlist below).
const ROUNDED_RE = new RegExp(`${BOUNDARY}(!?)rounded(-(?:tl|tr|bl|br|t|r|b|l))?-\\[([^\\]]+)\\]`, "g");
// Shadow: bare `shadow-[...]`.
const SHADOW_RE = new RegExp(`${BOUNDARY}(!?)shadow-\\[([^\\]]+)\\]`, "g");
// Ring width: bare `ring-[Npx]` (color-bracket forms like `ring-[#hex]` are
// NOT touched here — this batch is size/spacing/radius/shadow only, and no
// ring color-bracket sites exist in this codebase; verified during Step 0
// inventory). Requires the `length:` hint per the Step-0 spike (see header).
const RING_RE = new RegExp(`${BOUNDARY}(!?)ring-\\[([^\\]]+)\\]`, "g");
// Outline width (numeric only; no outline-[...] sites exist in this
// codebase per Step 0 inventory, kept for forward-compatibility/completeness).
const OUTLINE_RE = new RegExp(`${BOUNDARY}(!?)outline-\\[([^\\]]+)\\]`, "g");
function rewriteFile(filePath, relPath) {
const original = readFileSync(filePath, "utf8");
let content = original;
let siteCount = 0;
for (const { re, kind } of UTILITIES) {
content = content.replace(re, (match, bang, util, raw) => {
const sourceNote = `Extracted from ${relPath} (${util}-[${raw}]).`;
const { tokenRef } = classifyBracketValue(raw, { kind, sourceNote });
siteCount++;
return `${bang}${util}-(${tokenRef})`;
});
}
// Radius (needs its own hint-free paren form + inherit skip).
content = content.replace(ROUNDED_RE, (match, bang, dir, raw) => {
if (raw === "inherit") return match; // keyword, not a literal — skip (see log)
const util = `rounded${dir || ""}`;
const sourceNote = `Extracted from ${relPath} (${util}-[${raw}]).`;
const { tokenRef } = classifyBracketValue(raw, { kind: "radius", sourceNote });
siteCount++;
return `${bang}${util}-(${tokenRef})`;
});
// Shadow (always unescape underscores + normalize calc spacing inside any
// embedded rgba()/hsl() var() args; bare paren form confirmed correct via
// Step 0 spike — no `shadow:` hint needed).
content = content.replace(SHADOW_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (shadow-[${raw}]).`;
const name = registerShadowToken(unescaped, sourceNote);
siteCount++;
return `${bang}shadow-(--${name})`;
});
// Ring width (length hint required — bare ring-(--x) is ambiguous with
// the color-bracket form per the Step-0 spike).
content = content.replace(RING_RE, (match, bang, raw) => {
const sourceNote = `Extracted from ${relPath} (ring-[${raw}]).`;
const { tokenRef, isVarOnly } = classifyBracketValue(raw, { kind: "radius", sourceNote });
siteCount++;
const hint = isVarOnly ? "" : "length:";
return `${bang}ring-(${hint}${tokenRef})`;
});
// Outline width (same length-hint treatment; 0 sites exist today).
content = content.replace(OUTLINE_RE, (match, bang, raw) => {
const sourceNote = `Extracted from ${relPath} (outline-[${raw}]).`;
const { tokenRef, isVarOnly } = classifyBracketValue(raw, { kind: "radius", sourceNote });
siteCount++;
const hint = isVarOnly ? "" : "length:";
return `${bang}outline-(${hint}${tokenRef})`;
});
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 SIZE/SPACING/RADIUS/SHADOW tokens (Phase 2 Batch 3, design/token-extraction) ── */";
let cssNext = cssOriginal;
let cssChanged = false;
const anyTokens = szTokens.size || radTokens.size || pctTokens.size || shadowTokens.size;
if (!cssOriginal.includes(marker) && anyTokens) {
const lines = [];
lines.push(marker);
lines.push("/* Batch 3/4: width/height/min/max, padding/margin/gap/inset/translate,");
lines.push(" radius, ring/outline-width, and shadow literals, verbatim (no");
lines.push(" normalizing — the human scale-collapse decision comes later per");
lines.push(" DESIGN.md/TOKEN-AUDIT.md). --sz-* is ONE shared family across");
lines.push(" w/h/p/m/gap/inset/translate so identical values dedupe regardless of");
lines.push(" which property used them. --rad-* is likewise shared across");
// NOTE: never write a literal "*/" sequence in this prose (e.g. from a
// "rounded-*" + "/ring" join) — it prematurely closes this CSS block
// comment and silently corrupts everything after it (caught during this
// batch's verification: the entire --sz-*/--rad-*/--pct-*/--shadow-*
// :root block was being dropped from the built CSS because of exactly
// this). Always phrase such utility-family lists with "and" instead of
// a bare slash-adjacent asterisk.
lines.push(" rounded, ring, and outline widths.");
lines.push("");
lines.push(" Allowlist (sites intentionally left as hardcoded / functional literals");
lines.push(" or var()-only passthrough with no new token minted):");
lines.push(" - components/ui/scroll-area.tsx (rounded-[inherit]) — CSS keyword, not a");
lines.push(" literal value; nothing to extract.");
lines.push(" - Bracket values that only wrap var(--radix-*-trigger-width/height) or");
lines.push(" var(--new-issue-dialog-height) etc. are rewritten to the bare paren");
lines.push(" form directly (e.g. w-(--radix-popover-trigger-width)) — these are");
lines.push(" runtime library/component variables, not design values, so no new");
lines.push(" --sz-* token is minted for them (see TOKEN-AUDIT.md extraction log");
lines.push(" for the full site list).");
lines.push("*/");
lines.push(":root {");
for (const [name, { value, comment }] of szTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of radTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of pctTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of shadowTokens) {
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-sizes summary`);
console.log(` Sites rewritten: ${totalSites}`);
console.log(` Files changed: ${filesChanged}`);
console.log(` New --sz-* tokens: ${szTokens.size}`);
console.log(` New --rad-* tokens: ${radTokens.size}`);
console.log(` New --pct-* tokens: ${pctTokens.size}`);
console.log(` New --shadow-extract-*: ${shadowTokens.size}`);
console.log(` index.css token block: ${cssChanged ? "added" : "already present or nothing to add (idempotent no-op)"}`);
if (changedFiles.length) {
console.log(`\n Changed files:`);
for (const f of changedFiles) console.log(` - ${f}`);
}
}
main();

View File

@ -170,9 +170,9 @@ const AgentRunCard = memo(function AgentRunCard({
}) {
return (
<div className={cn(
"flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-sm",
"flex h-(--sz-320px) flex-col overflow-hidden rounded-xl border shadow-sm",
isActive
? "border-cyan-500/25 bg-cyan-500/[0.04] shadow-[0_16px_40px_rgba(6,182,212,0.08)]"
? "border-cyan-500/25 bg-cyan-500/[0.04] shadow-(--shadow-extract-1)"
: "border-border bg-background/70",
className,
)}>

View File

@ -19,7 +19,7 @@ function formatDayLabel(dateStr: string): string {
function DateLabels({ days }: { days: string[] }) {
return (
<div className="flex gap-[3px] mt-1.5">
<div className="flex gap-(--sz-3px) mt-1.5">
{days.map((day, i) => (
<div key={day} className="flex-1 text-center">
{(i === 0 || i === 6 || i === 13) ? (
@ -96,7 +96,7 @@ export function RunActivityChart(props: RunChartProps) {
return (
<div>
<div className="flex items-end gap-[3px] h-20">
<div className="flex items-end gap-(--sz-3px) h-20">
{days.map(day => {
const entry = grouped.get(day) ?? { date: day, succeeded: 0, failed: 0, other: 0, total: 0 };
const total = entry.total;
@ -148,7 +148,7 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA
return (
<div>
<div className="flex items-end gap-[3px] h-20">
<div className="flex items-end gap-(--sz-3px) h-20">
{days.map(day => {
const entry = grouped.get(day)!;
const total = Object.values(entry).reduce((a, b) => a + b, 0);
@ -215,7 +215,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
return (
<div>
<div className="flex items-end gap-[3px] h-20">
<div className="flex items-end gap-(--sz-3px) h-20">
{days.map(day => {
const entry = grouped.get(day)!;
const total = Object.values(entry).reduce((a, b) => a + b, 0);
@ -251,7 +251,7 @@ export function SuccessRateChart(props: RunChartProps) {
return (
<div>
<div className="flex items-end gap-[3px] h-20">
<div className="flex items-end gap-(--sz-3px) h-20">
{days.map(day => {
const entry = grouped.get(day) ?? { date: day, succeeded: 0, failed: 0, other: 0, total: 0 };
const rate = entry.total > 0 ? entry.succeeded / entry.total : 0;

View File

@ -268,7 +268,7 @@ function CollapsedFeedGroup({
onClick={() => setExpanded((e) => !e)}
data-fc="card"
className={cn(
"group ml-3 mr-3 md:ml-0 my-2 flex w-[calc(100%-1.5rem)] md:w-[calc(100%-0.75rem)] items-center gap-2 rounded-lg border bg-card p-[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-[background-color,border-color] duration-150",
"cursor-pointer hover:bg-accent hover:border-muted-foreground/30",
)}
>
@ -680,7 +680,7 @@ export function ActivityFeed({ className }: ActivityFeedProps) {
>
{isEmpty ? (
<div className="flex min-h-0 flex-1 items-center justify-center p-6 h-full">
<div className="flex flex-col items-center gap-2 max-w-[16rem]">
<div className="flex flex-col items-center gap-2 max-w-(--sz-16rem)">
{emptyMessage?.showPulse && (
<span className="relative flex h-2.5 w-2.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary/40" />

View File

@ -983,7 +983,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
value={eff("identity", "capabilities", props.agent.capabilities ?? "") ?? ""}
onChange={(v) => mark("identity", "capabilities", v || null)}
placeholder="Describe what this agent can do..."
contentClassName="min-h-[44px] text-sm font-mono"
contentClassName="min-h-(--sz-44px) text-sm font-mono"
imageUploadHandler={async (file) => {
const asset = await uploadMarkdownImage.mutateAsync({
file,
@ -1004,7 +1004,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
)}
onChange={(v) => mark("adapterConfig", "promptTemplate", v ?? "")}
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
contentClassName="min-h-[88px] text-sm font-mono"
contentClassName="min-h-(--sz-88px) text-sm font-mono"
imageUploadHandler={async (file) => {
const namespace = `agents/${props.agent.id}/prompt-template`;
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
@ -1345,7 +1345,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
mark("adapterConfig", "bootstrapPromptTemplate", v || undefined)
}
placeholder="Optional initial setup prompt for the first run"
contentClassName="min-h-[44px] text-sm font-mono"
contentClassName="min-h-(--sz-44px) text-sm font-mono"
imageUploadHandler={async (file) => {
const namespace = `agents/${props.agent.id}/bootstrap-prompt`;
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
@ -1632,7 +1632,7 @@ function AdapterTypeDropdown({
<ChevronDown className="h-3 w-3 text-muted-foreground" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
<PopoverContent className="w-(--radix-popover-trigger-width) p-1" align="start">
{adapterList.map((item) => (
<button
key={item.value}
@ -1802,7 +1802,7 @@ function ModelDropdown({
<ChevronDown className="h-3 w-3 text-muted-foreground" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
<PopoverContent className="w-(--radix-popover-trigger-width) p-1" align="start">
<div className="relative mb-1">
<input
className="w-full px-2 py-1.5 pr-6 text-xs bg-transparent outline-none border-b border-border placeholder:text-muted-foreground/50"
@ -1920,7 +1920,7 @@ function ModelDropdown({
</button>
);
})}
<div className="max-h-[240px] overflow-y-auto">
<div className="max-h-(--sz-240px) overflow-y-auto">
{allowDefault && (
<button
type="button"
@ -2083,7 +2083,7 @@ function ThinkingEffortDropdown({
<ChevronDown className="h-3 w-3 text-muted-foreground" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
<PopoverContent className="w-(--radix-popover-trigger-width) p-1" align="start">
{options.map((option) => (
<button
key={option.id || "auto"}

View File

@ -309,7 +309,7 @@ function BlockedInboxRow({
const desktopTrailing = (
<span className="flex shrink-0 items-center gap-3 text-xs">
<span
className="hidden w-[10.5rem] shrink-0 justify-start sm:inline-flex"
className="hidden w-(--sz-10_5rem) shrink-0 justify-start sm:inline-flex"
data-testid="blocked-row-reason-column"
>
<BlockedReasonChip
@ -319,7 +319,7 @@ function BlockedInboxRow({
/>
</span>
{ownerName ? (
<span className="hidden w-[150px] min-w-0 items-center text-muted-foreground sm:inline-flex">
<span className="hidden w-(--sz-150px) min-w-0 items-center text-muted-foreground sm:inline-flex">
<Identity
name={ownerName}
size="xs"
@ -327,10 +327,10 @@ function BlockedInboxRow({
/>
</span>
) : (
<span className="hidden w-[150px] shrink-0 sm:inline-flex" aria-hidden="true" />
<span className="hidden w-(--sz-150px) shrink-0 sm:inline-flex" aria-hidden="true" />
)}
{showUpdatedColumn ? (
<span className="hidden w-[5.75rem] text-right text-muted-foreground sm:inline" data-testid="blocked-row-age">
<span className="hidden w-(--sz-5_75rem) text-right text-muted-foreground sm:inline" data-testid="blocked-row-age">
{stoppedAge}
</span>
) : null}
@ -375,7 +375,7 @@ function BlockedInboxRow({
<BlockedReasonChip
reason={row.attention.reason}
severity={row.attention.severity}
className="ml-2 max-w-[12rem] align-middle sm:hidden"
className="ml-2 max-w-(--sz-12rem) align-middle sm:hidden"
/>
}
mobileMeta={mobileMeta}

View File

@ -189,7 +189,7 @@ export function BudgetPolicyCard({
}
return (
<Card className={cn("overflow-hidden border-border/70 bg-card/80", compact ? "" : "shadow-[0_20px_80px_-40px_rgba(0,0,0,0.55)]")}>
<Card className={cn("overflow-hidden border-border/70 bg-card/80", compact ? "" : "shadow-(--shadow-extract-2)")}>
<CardHeader className={cn("gap-3", compact ? "px-4 pt-4 pb-2" : "px-5 pt-5 pb-3")}>
<div className="flex items-start justify-between gap-3">
<div>

View File

@ -27,7 +27,7 @@ export function BudgetSidebarMarker({
<span
title={accessibleTitle}
aria-label={accessibleTitle}
className={`ml-auto inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full shadow-[0_0_0_1px_rgba(255,255,255,0.08)] ${levelClasses[level]}`}
className={`ml-auto inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full shadow-(--shadow-extract-3) ${levelClasses[level]}`}
>
<DollarSign className="h-3 w-3" />
</span>

View File

@ -160,7 +160,7 @@ describe("ChatComposer", () => {
const el = input();
expect(el.getAttribute("wrap")).toBe("soft");
expect(el.className).toContain("overflow-y-auto");
expect(el.className).toContain("max-h-[200px]");
expect(el.className).toContain("max-h-(--sz-200px)");
expect(el.className).not.toContain("whitespace-nowrap");
expect(el.className).not.toContain("overflow-x-auto");
act(() => root.unmount());
@ -174,7 +174,7 @@ describe("ChatComposer", () => {
const el = input();
expect(el.getAttribute("wrap")).toBe("off");
expect(el.className).toContain("whitespace-nowrap");
expect(el.className).toContain("max-h-[22px]");
expect(el.className).toContain("max-h-(--sz-22px)");
act(() => root.unmount());
});
@ -225,8 +225,8 @@ describe("ChatComposer", () => {
expect(box.className).toContain("bg-background/95");
expect(box.className).toContain("supports-[backdrop-filter]:bg-background/85");
expect(box.className).toContain("backdrop-blur");
expect(box.className).toContain("shadow-[0_-12px_28px_rgba(15,23,42,0.08)]");
expect(box.className).toContain("dark:shadow-[0_-12px_28px_rgba(0,0,0,0.28)]");
expect(box.className).toContain("shadow-(--shadow-extract-4)");
expect(box.className).toContain("dark:shadow-(--shadow-extract-5)");
expect(box.className).not.toContain("bg-card");
// Shared chrome survives: rounded-xl corners + neutral focus darkening.
expect(box.className).toContain("rounded-xl");

View File

@ -231,7 +231,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(fu
"relative rounded-xl border px-3 pt-2.5 pb-2 transition-colors duration-150 focus-within:border-muted-foreground/40",
// Surface: opaque card vs the task glass recipe (IssueChatThread.tsx shell).
surface === "translucent"
? "border-border/70 bg-background/95 shadow-[0_-12px_28px_rgba(15,23,42,0.08)] backdrop-blur supports-[backdrop-filter]:bg-background/85 dark:shadow-[0_-12px_28px_rgba(0,0,0,0.28)]"
? "border-border/70 bg-background/95 shadow-(--shadow-extract-4) backdrop-blur supports-[backdrop-filter]:bg-background/85 dark:shadow-(--shadow-extract-5)"
: "border-border bg-card",
// No blue focus ring — neutral border darkening only.
isAsk &&
@ -271,10 +271,10 @@ export const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(fu
rows={1}
wrap={singleLine ? "off" : "soft"}
className={cn(
"block min-h-[22px] w-full resize-none border-0 bg-transparent p-0 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground focus:outline-none focus:ring-0",
"block min-h-(--sz-22px) w-full resize-none border-0 bg-transparent p-0 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground focus:outline-none focus:ring-0",
singleLine
? "max-h-[22px] overflow-x-auto whitespace-nowrap"
: "max-h-[200px] overflow-y-auto",
? "max-h-(--sz-22px) overflow-x-auto whitespace-nowrap"
: "max-h-(--sz-200px) overflow-y-auto",
textareaClassName,
)}
/>

View File

@ -1035,7 +1035,7 @@ export function CommentThread({
mentions={mentions}
onSubmit={handleSubmit}
imageUploadHandler={imageUploadHandler}
contentClassName="min-h-[60px] text-sm"
contentClassName="min-h-(--sz-60px) text-sm"
/>
<div className="flex items-center justify-end gap-3">
{(imageUploadHandler || onAttachImage) && (

View File

@ -55,7 +55,7 @@ export function CompanySwitcher({ open: controlledOpen, onOpenChange }: CompanyS
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[220px]">
<DropdownMenuContent align="start" className="w-(--sz-220px)">
<DropdownMenuLabel>Companies</DropdownMenuLabel>
<DropdownMenuSeparator />
{sidebarCompanies.map((company) => (

View File

@ -454,7 +454,7 @@ export function DocumentAnnotationLayer({
: isStale
? "bg-yellow-200 outline outline-2 outline-dashed outline-offset-0 outline-yellow-700/65 dark:bg-yellow-600 dark:outline-yellow-200/70"
: isFocused
? "bg-yellow-300 outline outline-2 outline-offset-0 outline-yellow-700/85 shadow-[0_0_0_1px_var(--color-background)] dark:bg-yellow-500 dark:outline-yellow-200/85"
? "bg-yellow-300 outline outline-2 outline-offset-0 outline-yellow-700/85 shadow-(--shadow-extract-6) dark:bg-yellow-500 dark:outline-yellow-200/85"
: "bg-yellow-200 dark:bg-yellow-600",
)}
style={{

View File

@ -71,7 +71,7 @@ export function DocumentAnnotationPanel(props: AnnotationPanelProps) {
<SheetContent
side="bottom"
showCloseButton={false}
className="paperclip-doc-annotation-sheet z-[60] flex max-h-[88vh] flex-col rounded-none border-t border-border bg-popover p-0 text-popover-foreground shadow-2xl"
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"
>
<SheetTitle className="sr-only">
Comments on {props.documentKey} revision {props.documentRevisionNumber}
@ -91,7 +91,7 @@ export function DocumentAnnotationPanel(props: AnnotationPanelProps) {
aria-label={`Annotations for ${props.documentKey.toUpperCase()}, revision ${props.documentRevisionNumber}`}
data-testid="document-annotation-panel"
className={cn(
"isolate flex h-full max-h-[80vh] w-[360px] shrink-0 flex-col overflow-hidden rounded-none border border-border bg-popover text-popover-foreground shadow-xl",
"isolate flex h-full max-h-(--sz-80vh) w-(--sz-360px) shrink-0 flex-col overflow-hidden rounded-none border border-border bg-popover text-popover-foreground shadow-xl",
props.className,
)}
style={props.desktopWidth ? { width: props.desktopWidth, maxWidth: props.desktopWidth } : undefined}

View File

@ -85,7 +85,7 @@ export function DocumentDiffModal({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="!max-w-[90%] w-full max-h-[85vh] overflow-hidden flex flex-col">
<DialogContent className="!max-w-(--pct-90) w-full max-h-(--sz-85vh) overflow-hidden flex flex-col">
<div className="flex items-center justify-between gap-4">
<DialogHeader className="shrink-0">
<DialogTitle>

View File

@ -50,7 +50,7 @@ export function EntityRow({
<div className={cn("min-w-0", !meta && "flex-1", titleClassName)}>
<div className="flex items-center gap-2">
{identifier && (
<span className="text-xs text-muted-foreground font-mono shrink-0 relative top-[1px]">
<span className="text-xs text-muted-foreground font-mono shrink-0 relative top-(--sz-1px)">
{identifier}
</span>
)}

View File

@ -105,7 +105,7 @@ export function ExecutionParticipantPicker({
>
<Icon className="h-3 w-3" />
{values.length > 0 ? (
<span className="truncate max-w-[100px]">
<span className="truncate max-w-(--sz-100px)">
{values.map(participantLabel).join(", ")}
</span>
) : (

View File

@ -89,7 +89,7 @@ export function ExecutionWorkspaceCloseDialog({
<Dialog open={open} onOpenChange={(nextOpen) => {
if (!closeWorkspace.isPending) onOpenChange(nextOpen);
}}>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-(--sz-85vh) overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{actionLabel}</DialogTitle>
<DialogDescription className="break-words">

View File

@ -134,7 +134,7 @@ export function ExternalObjectPill({
tone.split(" ").filter((c) => c.startsWith("text-")).join(" "),
overlay,
interactive
&& "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring",
&& "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring",
className,
);
const titleAttr = sourceSummary
@ -149,7 +149,7 @@ export function ExternalObjectPill({
sizeClassName="h-3 w-3"
label={`${providerLabel}: ${statusLabel}`}
/>
<span className="max-w-[16rem] truncate font-medium">{valueLabel}</span>
<span className="max-w-(--sz-16rem) truncate font-medium">{valueLabel}</span>
</>
);
const countSuffix = typeof sourceCount === "number" && sourceCount > 1 ? (

View File

@ -436,7 +436,7 @@ export function FeedCard({
<div
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-[18px] text-xs",
"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",
content.link && "cursor-pointer hover:bg-accent hover:border-muted-foreground/30",
className,

View File

@ -164,7 +164,7 @@ describe("FileViewerMetadataRow", () => {
it("reserves metadata row height while file details load", () => {
const markup = renderToStaticMarkup(<FileViewerMetadataRow state={state} />);
expect(markup).toContain("min-h-[18px]");
expect(markup).toContain("min-h-(--sz-18px)");
expect(markup).toContain("Loading file details");
});
});

View File

@ -223,7 +223,7 @@ export function FileViewerMetadataRow({
state: FileViewerUrlState | null;
}) {
return (
<div className="flex min-h-[18px] flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
<div className="flex min-h-(--sz-18px) flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
{resolvedResource ? (
<>
{resolvedResource.previewKind ? <span className="capitalize">{resolvedResource.previewKind}</span> : null}
@ -734,7 +734,7 @@ export function FileViewerSheet({
return (
<Dialog open={computedOpen} onOpenChange={handleOpenChange}>
<DialogContent
className="flex h-[min(840px,calc(100dvh-2rem))] w-[calc(100vw-2rem)] max-w-[min(1280px,calc(100vw-2rem))] flex-col gap-0 overflow-hidden p-0 sm:w-[94vw] sm:max-w-[1280px]"
className="flex h-(--sz-calc-3) w-(--sz-calc-4) max-w-(--sz-calc-5) flex-col gap-0 overflow-hidden p-0 sm:w-(--sz-94vw) sm:max-w-(--sz-1280px)"
aria-labelledby={FILE_VIEWER_LABELLED_BY_ID}
aria-describedby={FILE_VIEWER_DESCRIBED_BY_ID}
showCloseButton={false}

View File

@ -7,7 +7,7 @@ interface FrontDoorProps {
export function FrontDoor({ onChoose }: FrontDoorProps) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] px-8">
<div className="flex flex-col items-center justify-center min-h-(--sz-60vh) px-8">
<div className="text-center mb-10">
<h2 className="text-2xl font-bold tracking-tight">
Welcome to Paperclip

View File

@ -93,7 +93,7 @@ export function ImageGalleryModal({
>
{/* Top bar */}
<div className="flex items-center justify-between px-5 py-3 text-white/80 text-sm shrink-0">
<span className="truncate max-w-[50%] font-medium" title={filename}>
<span className="truncate max-w-(--pct-50) font-medium" title={filename}>
{filename}
</span>
<div className="flex items-center gap-4">

View File

@ -131,7 +131,7 @@ export const InlineEntitySelector = forwardRef<HTMLButtonElement, InlineEntitySe
align="start"
side="bottom"
collisionPadding={16}
className="w-[min(20rem,calc(100vw-2rem))] p-1"
className="w-(--sz-calc-6) p-1"
disablePortal={disablePortal}
onOpenAutoFocus={(event) => {
event.preventDefault();

View File

@ -134,7 +134,7 @@ function MarkdownAttachmentCard({
<p className="px-1 py-2 text-xs text-destructive">Could not load markdown preview.</p>
) : (
<FoldCurtain>
<MarkdownBody className="paperclip-edit-in-place-content min-h-[220px] text-(length:--fs-15) leading-7" softBreaks={false}>
<MarkdownBody className="paperclip-edit-in-place-content min-h-(--sz-220px) text-(length:--fs-15) leading-7" softBreaks={false}>
{data ?? ""}
</MarkdownBody>
</FoldCurtain>

View File

@ -189,7 +189,7 @@ export function IssueBlockedNotice({
className="inline-flex max-w-full items-center gap-1 rounded-md border border-amber-300/70 bg-background/80 px-2 py-1 font-mono text-xs text-amber-950 transition-colors hover:border-amber-500 hover:bg-amber-100 hover:underline dark:border-amber-500/40 dark:bg-background/40 dark:text-amber-100 dark:hover:bg-amber-500/15"
>
<span>{blocker.identifier ?? blocker.id.slice(0, 8)}</span>
<span className="max-w-[18rem] truncate font-sans text-(length:--fs-11) text-amber-800 dark:text-amber-200">
<span className="max-w-(--sz-18rem) truncate font-sans text-(length:--fs-11) text-amber-800 dark:text-amber-200">
{blocker.title}
</span>
{recoveryAction ? <BlockerRecoveryIndicator action={recoveryAction} /> : null}

View File

@ -347,7 +347,7 @@ describe("IssueChatThread", () => {
const viewport = container.querySelector('[data-testid="thread-viewport"]') as HTMLDivElement | null;
expect(viewport).not.toBeNull();
expect(viewport?.className).not.toContain("overflow-y-auto");
expect(viewport?.className).not.toContain("max-h-[70vh]");
expect(viewport?.className).not.toContain("max-h-(--sz-70vh)");
act(() => {
root.unmount();
@ -1754,8 +1754,8 @@ describe("IssueChatThread", () => {
);
expect(bubble).toBeDefined();
expect(bubble?.textContent).toContain("Here is my agent reply.");
expect(bubble?.className).toContain("max-w-[calc(100%-0.5rem)]");
expect(bubble?.className).toContain("sm:max-w-[85%]");
expect(bubble?.className).toContain("max-w-(--sz-calc-7)");
expect(bubble?.className).toContain("sm:max-w-(--pct-85)");
// Neutral, not the human liveness-blue bubble.
expect(bubble?.className).not.toContain("bg-(--status-task-in_progress)");
@ -2647,19 +2647,19 @@ describe("IssueChatThread", () => {
const dock = container.querySelector('[data-testid="issue-chat-composer-dock"]') as HTMLDivElement | null;
expect(dock).not.toBeNull();
expect(dock?.className).toContain("sticky");
expect(dock?.className).toContain("bottom-[calc(env(safe-area-inset-bottom)+20px)]");
expect(dock?.className).toContain("bottom-(--sz-calc-8)");
expect(dock?.className).toContain("z-20");
const composer = container.querySelector('[data-testid="issue-chat-composer"]') as HTMLDivElement | null;
expect(composer).not.toBeNull();
expect(composer?.className).toContain("rounded-md");
expect(composer?.className).not.toContain("rounded-lg");
expect(composer?.className).toContain("p-[15px]");
expect(composer?.className).toContain("p-(--sz-15px)");
const editor = container.querySelector('textarea[aria-label="Issue chat editor"]') as HTMLTextAreaElement | null;
expect(editor?.dataset.contentClassName).toContain("max-h-[28dvh]");
expect(editor?.dataset.contentClassName).toContain("max-h-(--sz-28dvh)");
expect(editor?.dataset.contentClassName).toContain("overflow-y-auto");
expect(editor?.dataset.contentClassName).not.toContain("min-h-[72px]");
expect(editor?.dataset.contentClassName).not.toContain("min-h-(--sz-72px)");
expect(editor?.dataset.fileDropTarget).toBe("parent");
act(() => {

View File

@ -228,7 +228,7 @@ const IssueChatCtx = createContext<IssueChatMessageContext>({
successfulRunHandoff: null,
});
const AGENT_COMMENT_BUBBLE_WIDTH_CLASS = "max-w-[calc(100%-0.5rem)] sm:max-w-[85%]";
const AGENT_COMMENT_BUBBLE_WIDTH_CLASS = "max-w-(--sz-calc-7) sm:max-w-(--pct-85)";
export type IssueChatRunFinalizationAction = {
id: "cancel" | "done";
@ -1426,7 +1426,7 @@ function IssueChatUserMessage({
void onDeleteComment?.(commentId);
};
const messageBody = (
<div className={cn("flex min-w-0 max-w-[85%] flex-col", isCurrentUser && "items-end")}>
<div className={cn("flex min-w-0 max-w-(--pct-85) flex-col", isCurrentUser && "items-end")}>
<div className={cn("mb-1 flex items-center gap-2 px-1", isCurrentUser ? "justify-end" : "justify-start")}>
<span className="text-sm font-medium text-foreground">{resolvedAuthorName}</span>
<SourceTrustBadge sourceTrust={sourceTrust} artifactLabel="comment" />
@ -1441,7 +1441,7 @@ function IssueChatUserMessage({
"min-w-0 max-w-full overflow-hidden break-all rounded-2xl px-4 py-2.5",
// Tail-hugging corner: flatten the bottom corner nearest the avatar so
// the bubble points at it (bottom-right for the right-aligned human).
isCurrentUser ? "rounded-br-[4px]" : "rounded-bl-[4px]",
isCurrentUser ? "rounded-br-(--rad-4)" : "rounded-bl-(--rad-4)",
queued
? "bg-amber-50/80 dark:bg-amber-500/10"
: deleted
@ -2676,7 +2676,7 @@ function IssueChatMetadataRow({
return (
<div id={anchorId} data-testid={testid}>
<div className="ml-3 flex items-start gap-2.5 border-l-2 border-border/50 py-0.5 pl-3">
<span className="mt-px flex size-[18px] shrink-0 items-center justify-center rounded-full border border-border/70 bg-muted/30 text-muted-foreground/60">
<span className="mt-px flex size-(--sz-18px) shrink-0 items-center justify-center rounded-full border border-border/70 bg-muted/30 text-muted-foreground/60">
{icon}
</span>
<div className="min-w-0 flex-1 space-y-1">{children}</div>
@ -3852,9 +3852,9 @@ 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-[15px] shadow-[0_-12px_28px_rgba(15,23,42,0.08)] backdrop-blur transition-[border-color,background-color,box-shadow] duration-150 supports-[backdrop-filter]:bg-background/85 dark:shadow-[0_-12px_28px_rgba(0,0,0,0.28)]",
"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)",
pendingWorkModeMeta.classes.container,
isDragOver && "border-primary/45 bg-background shadow-[0_-12px_28px_rgba(15,23,42,0.08),0_0_0_1px_hsl(var(--primary)/0.16)]",
isDragOver && "border-primary/45 bg-background shadow-(--shadow-extract-7)",
)}
onKeyDownCapture={handleComposerKeyDown}
onDragEnterCapture={handleFileDragEnter}
@ -3891,7 +3891,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
imageUploadHandler={onImageUpload}
fileDropTarget="parent"
bordered={false}
contentClassName="max-h-[28dvh] overflow-y-auto pr-1 pb-2 text-sm scrollbar-auto-hide"
contentClassName="max-h-(--sz-28dvh) overflow-y-auto pr-1 pb-2 text-sm scrollbar-auto-hide"
/>
{coachVisible && plainNameCandidate ? (
@ -4928,7 +4928,7 @@ export function IssueChatThread({
<div
ref={composerViewportAnchorRef}
data-testid="issue-chat-composer-dock"
className="sticky bottom-[calc(env(safe-area-inset-bottom)+20px)] z-20 space-y-2 bg-gradient-to-t from-background via-background/95 to-background/0 pt-6"
className="sticky bottom-(--sz-calc-8) z-20 space-y-2 bg-gradient-to-t from-background via-background/95 to-background/0 pt-6"
>
<IssueChatComposer
ref={composerRef}

View File

@ -93,7 +93,7 @@ export function IssueColumnPicker({
{!iconOnly && "Columns"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[300px] rounded-xl border-border/70 p-1.5 shadow-xl shadow-black/10">
<DropdownMenuContent align="end" className="w-(--sz-300px) rounded-xl border-border/70 p-1.5 shadow-xl shadow-black/10">
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">
<div className="space-y-1">
<div className="text-(length:--fs-10) font-semibold uppercase tracking-(--ls-0_22) text-muted-foreground">

View File

@ -762,7 +762,7 @@ export function IssueDocumentsSection({
const documentBodyShellClassName = "mt-3";
const documentBodyPaddingClassName = "";
const documentBodyContentClassName = "paperclip-edit-in-place-content min-h-[220px] text-(length:--fs-15) leading-7";
const documentBodyContentClassName = "paperclip-edit-in-place-content min-h-(--sz-220px) text-(length:--fs-15) leading-7";
const toggleFoldedDocument = (key: string) => {
setFoldedDocumentKeys((current) =>
current.includes(key)
@ -850,7 +850,7 @@ export function IssueDocumentsSection({
placeholder="Markdown body"
bordered={false}
className="bg-transparent"
contentClassName="min-h-[220px] text-(length:--fs-15) leading-7"
contentClassName="min-h-(--sz-220px) text-(length:--fs-15) leading-7"
mentions={mentions}
imageUploadHandler={imageUploadHandler}
onSubmit={() => void commitDraft(draft, { clearAfterSave: false, trackAutosave: false })}

View File

@ -73,7 +73,7 @@ describe("IssueFiltersPopover", () => {
const popoverContent = container.querySelector("[data-testid='popover-content']");
expect(popoverContent).not.toBeNull();
expect(popoverContent?.className).toContain("overflow-y-auto");
expect(popoverContent?.className).toContain("max-h-[min(80vh,42rem)]");
expect(popoverContent?.className).toContain("max-h-(--sz-calc-9)");
const layoutGrid = Array.from(popoverContent?.querySelectorAll("div") ?? []).find((element) =>
element.className.includes("md:grid-cols-3"),

View File

@ -132,7 +132,7 @@ export function IssueFiltersPopover({
</PopoverTrigger>
<PopoverContent
align="end"
className="w-[min(780px,calc(100vw-2rem))] max-h-[min(80vh,42rem)] overflow-y-auto overscroll-contain p-0"
className="w-(--sz-calc-10) max-h-(--sz-calc-9) overflow-y-auto overscroll-contain p-0"
>
<div className="space-y-3 p-3">
<div className="flex items-center justify-between">

View File

@ -142,7 +142,7 @@ export function IssuePlanDecompositionsSection({
<span className="font-medium">
{child.identifier ?? child.id.slice(0, 8)}
</span>
<span className="truncate max-w-[24ch] text-muted-foreground">
<span className="truncate max-w-(--sz-24ch) text-muted-foreground">
{child.title}
</span>
<ChevronRight className="h-3 w-3 text-muted-foreground" />

View File

@ -388,7 +388,7 @@ export function IssueRecoveryActionCard({
data-recovery-state={cardState}
data-recovery-kind={action.kind}
className={cn(
"relative w-full overflow-hidden rounded-lg border text-sm shadow-[0_1px_0_rgba(15,23,42,0.02)]",
"relative w-full overflow-hidden rounded-lg border text-sm shadow-(--shadow-extract-8)",
tone.containerClass,
className,
)}

View File

@ -20,7 +20,7 @@ export function IssueReferencePill({
const classNames = cn(
"paperclip-mention-chip paperclip-mention-chip--issue",
"inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-xs no-underline",
issue.identifier && "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring",
issue.identifier && "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring",
strikethrough && "opacity-60 line-through decoration-muted-foreground",
className,
);

View File

@ -122,7 +122,7 @@ export function IssueRow({
className={cn(
"group flex items-start gap-2 border-b border-border py-2.5 pl-2 pr-3 text-sm no-underline text-inherit transition-colors last:border-b-0 sm:items-center sm:py-2 sm:pl-1",
selected ? "hover:bg-transparent" : "hover:bg-accent/50",
checklistCurrentStep ? "border-l-2 border-l-primary bg-primary/5 pl-[calc(theme(spacing.2)-2px)] sm:pl-[calc(theme(spacing.1)-2px)]" : null,
checklistCurrentStep ? "border-l-2 border-l-primary bg-primary/5 pl-(--sz-calc-11) sm:pl-(--sz-calc-12)" : null,
className,
)}
>
@ -137,7 +137,7 @@ export function IssueRow({
{issue.title}{titleSuffix}
</span>
{checklistDependencyChips ? (
<span className="flex flex-wrap gap-1 sm:order-3 sm:ml-[calc(theme(spacing.3)+theme(spacing.2))]">
<span className="flex flex-wrap gap-1 sm:order-3 sm:ml-(--sz-calc-13)">
{checklistDependencyChips}
</span>
) : null}

View File

@ -145,7 +145,7 @@ export function IssueScheduledRetryCard({
</span>
)}
</Button>
<span className="text-right text-xs text-muted-foreground sm:max-w-[12rem]">
<span className="text-right text-xs text-muted-foreground sm:max-w-(--sz-12rem)">
{retryNow.isPending
? "Promoting scheduled retry"
: isSuccessTransient

View File

@ -105,7 +105,7 @@ describe("IssueSiblingNavigation", () => {
expect(links[0].className).toContain("rounded-lg");
expect(links[0].className).toContain("hover:bg-accent/50");
expect(links[0].className).toContain("focus-visible:ring-[3px]");
expect(links[0].className).toContain("focus-visible:ring-(length:--rad-3)");
expect(node.querySelector(".truncate")?.textContent).toBe("Previous sibling title");
});

View File

@ -60,7 +60,7 @@ function SiblingLink({
issueQuicklookAlign={direction === "previous" ? "start" : "end"}
aria-label={`${ariaDirection}: ${identifier} - ${issue.title}`}
className={cn(
"group min-w-0 rounded-lg border border-border bg-card px-3 py-2.5 text-left no-underline transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring",
"group min-w-0 rounded-lg border border-border bg-card px-3 py-2.5 text-left no-underline transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring",
direction === "next" && "sm:text-right",
className,
)}

View File

@ -671,7 +671,7 @@ function QuestionOptionButton({
role={selectionMode === "single" ? "radio" : "checkbox"}
aria-checked={selected}
className={cn(
"w-full rounded-sm border px-4 py-3 text-left transition-colors outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"w-full rounded-sm border px-4 py-3 text-left transition-colors outline-none focus-visible:border-ring focus-visible:ring-(length:--rad-3) focus-visible:ring-ring/50",
selected
? "border-sky-500/80 bg-sky-500/10 text-sky-950 dark:border-sky-400/80 dark:bg-sky-400/15 dark:text-sky-50"
: "border-border/70 bg-transparent text-foreground hover:border-sky-500/70 hover:bg-sky-500/10 dark:hover:border-sky-400/70 dark:hover:bg-sky-400/10",
@ -858,7 +858,7 @@ function AskUserQuestionsCard({
{questions.map((question, index) => (
<div
key={question.id}
className="rounded-2xl border border-border/70 bg-background/82 p-4 shadow-[0_18px_42px_rgba(15,23,42,0.06)]"
className="rounded-2xl border border-border/70 bg-background/82 p-4 shadow-(--shadow-extract-9)"
>
<div className="flex items-start justify-between gap-3">
<div>
@ -908,7 +908,7 @@ function AskUserQuestionsCard({
id={`${interaction.id}-${question.id}-other`}
aria-expanded={otherActiveQuestions[question.id] === true}
className={cn(
"text-sm font-medium underline underline-offset-4 transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
"text-sm font-medium underline underline-offset-4 transition-colors outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring/50",
otherActiveQuestions[question.id]
? "text-sky-700 hover:text-sky-800 dark:text-sky-300 dark:hover:text-sky-200"
: "text-muted-foreground hover:text-foreground",

View File

@ -597,7 +597,7 @@ const SecretField = React.memo(({
onChange={(e) => onChange(e.target.value)}
placeholder={String(defaultValue ?? "")}
disabled={disabled}
className="min-h-[140px] pr-10 font-mono text-xs"
className="min-h-(--sz-140px) pr-10 font-mono text-xs"
aria-invalid={!!error}
/>
) : (
@ -614,7 +614,7 @@ const SecretField = React.memo(({
readOnly
placeholder={String(defaultValue ?? "")}
disabled={disabled}
className="min-h-[140px] pr-10 font-mono text-xs italic text-muted-foreground"
className="min-h-(--sz-140px) pr-10 font-mono text-xs italic text-muted-foreground"
aria-invalid={!!error}
/>
)}
@ -837,7 +837,7 @@ const StringField = React.memo(({
onChange={(e) => onChange(e.target.value)}
placeholder={String(defaultValue ?? "")}
disabled={disabled}
className="min-h-[100px]"
className="min-h-(--sz-100px)"
aria-invalid={!!error}
/>
) : (

View File

@ -153,7 +153,7 @@ function KanbanColumn({
<div
ref={setNodeRef}
className={cn(
"flex min-h-[220px] w-[52px] shrink-0 flex-col items-center rounded-md border px-1.5 py-2 transition-colors",
"flex min-h-(--sz-220px) w-(--sz-52px) shrink-0 flex-col items-center rounded-md border px-1.5 py-2 transition-colors",
tone.rail,
isOver && tone.railOver,
)}
@ -171,7 +171,7 @@ function KanbanColumn({
}
return (
<div className={`flex flex-col shrink-0 transition-[width,min-width] ${isEmpty && !isOver ? "min-w-[48px] w-[48px]" : "min-w-[260px] w-[260px]"}`}>
<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 items-center gap-2 px-2 py-2 mb-1 ${isEmpty && !isOver ? "justify-center" : ""}`}>
<StatusIcon status={status} />
{(!isEmpty || isOver) && (
@ -188,7 +188,7 @@ function KanbanColumn({
<div
ref={setNodeRef}
className={cn(
"flex-1 min-h-[120px] rounded-md p-1 space-y-1 transition-colors",
"flex-1 min-h-(--sz-120px) rounded-md p-1 space-y-1 transition-colors",
isOver ? tone.bodyOver : tone.body,
)}
>

View File

@ -64,7 +64,7 @@ const sections: ShortcutSection[] = [
function KeyCap({ children }: { children: string }) {
return (
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-foreground shadow-[0_1px_0_1px_hsl(var(--border))]">
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-foreground shadow-(--shadow-extract-10)">
{children}
</kbd>
);

View File

@ -498,7 +498,7 @@ export function Layout() {
<GeneralSettingsProvider value={{ keyboardShortcutsEnabled }}>
<div
className={cn(
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
"bg-background text-foreground pt-(--sz-safe-top)",
// overflow-x-clip on mobile keeps a stray wide descendant from making the
// whole viewport scroll horizontally. clip (not hidden) leaves overflow-y
// computed as visible, so native body scroll + the sticky breadcrumb keep
@ -527,7 +527,7 @@ export function Layout() {
{isMobile ? (
<div
className={cn(
"fixed inset-y-0 left-0 z-50 flex flex-col overflow-hidden pt-[env(safe-area-inset-top)] transition-transform duration-100 ease-out",
"fixed inset-y-0 left-0 z-50 flex flex-col overflow-hidden pt-(--sz-safe-top) transition-transform duration-100 ease-out",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}
>
@ -591,7 +591,7 @@ export function Layout() {
// changes (e.g. switching skill-detail tabs) don't widen/shift
// when the vertical scrollbar appears or disappears (PAP-10907).
isMobile
? "overflow-visible pb-[calc(5rem+env(safe-area-inset-bottom))]"
? "overflow-visible pb-(--sz-calc-14)"
: "overflow-auto [scrollbar-gutter:stable]",
)}
>

View File

@ -89,7 +89,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
if (runs.length === 0) return null;
return (
<div className="overflow-hidden rounded-xl border border-cyan-500/25 bg-background/80 shadow-[0_18px_50px_rgba(6,182,212,0.08)]">
<div className="overflow-hidden rounded-xl border border-cyan-500/25 bg-background/80 shadow-(--shadow-extract-11)">
<div className="border-b border-border/60 bg-cyan-500/[0.04] px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-(--ls-0_18) text-cyan-700 dark:text-cyan-300">
Live Runs
@ -143,7 +143,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
</div>
</div>
<div className="max-h-[320px] overflow-y-auto pr-1">
<div className="max-h-(--sz-320px) overflow-y-auto pr-1">
<RunChatSurface
run={run}
transcript={transcript}

View File

@ -283,7 +283,7 @@ describe("MarkdownBody", () => {
expect(html).toContain('data-workspace-file-link="true"');
expect(html).toContain('data-workspace-file-path="videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4"');
expect(html).toContain("videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4");
expect(html).not.toContain("max-w-[38ch]");
expect(html).not.toContain("max-w-(--sz-38ch)");
expect(html).not.toContain("paperclip-markdown-issue-ref");
expect(html).not.toContain('href="/issues/PAP-10306"');
});

View File

@ -890,7 +890,7 @@ describe("MarkdownEditor", () => {
const options = Array.from(menu.querySelectorAll('button[type="button"]'));
expect(options).toHaveLength(12);
expect(menu.className).toContain("max-h-[208px]");
expect(menu.className).toContain("max-h-(--sz-208px)");
expect(menu.className).toContain("overflow-y-auto");
expect(menu.style.touchAction).toBe("pan-y");

View File

@ -1140,7 +1140,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
}
}}
className={cn(
"min-h-[12rem] w-full resize-none bg-transparent px-3 pb-3 pt-2 font-mono text-sm leading-6 outline-none",
"min-h-(--sz-12rem) w-full resize-none bg-transparent px-3 pb-3 pt-2 font-mono text-sm leading-6 outline-none",
contentClassName,
)}
/>
@ -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-[180px] max-w-[calc(100vw-16px)] max-h-[208px] overflow-y-auto rounded-md border border-border bg-popover shadow-md"
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"
style={{
top: mentionMenuPosition.top,
left: mentionMenuPosition.left,

View File

@ -41,7 +41,7 @@ export function MembershipAction({
return (
<span
className={cn(
"flex w-[66px] shrink-0 justify-end",
"flex w-(--sz-66px) shrink-0 justify-end",
!isLeft && !compact
? "opacity-100 sm:opacity-0 sm:transition-opacity sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
: "opacity-100",
@ -55,7 +55,7 @@ export function MembershipAction({
aria-busy={pending ? "true" : undefined}
disabled={pending}
onClick={handleClick}
className="w-[66px]"
className="w-(--sz-66px)"
>
<Icon className={cn("h-3 w-3", pending && "motion-safe:animate-spin")} />
<span>{label}</span>

View File

@ -60,7 +60,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
return (
<nav
className={cn(
"fixed bottom-0 left-0 right-0 z-30 border-t border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-transform duration-200 ease-out md:hidden pb-[env(safe-area-inset-bottom)]",
"fixed bottom-0 left-0 right-0 z-30 border-t border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-transform duration-200 ease-out md:hidden pb-(--sz-safe-bottom)",
visible ? "translate-y-0" : "translate-y-full",
)}
aria-label="Mobile navigation"
@ -82,7 +82,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
: "text-muted-foreground hover:text-foreground",
)}
>
<Icon className="h-[18px] w-[18px]" />
<Icon className="h-(--sz-18px) w-(--sz-18px)" />
<span className="truncate">{item.label}</span>
</button>
);
@ -106,7 +106,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
{({ isActive }) => (
<>
<span className="relative">
<Icon className={cn("h-[18px] w-[18px]", isActive && "stroke-[2.3]")} />
<Icon className={cn("h-(--sz-18px) w-(--sz-18px)", isActive && "stroke-[2.3]")} />
{item.badge != null && item.badge > 0 && (
<span className="absolute -right-2 -top-2 rounded-full bg-primary px-1.5 py-0.5 text-(length:--fs-10) leading-none text-primary-foreground">
{item.badge > 99 ? "99+" : item.badge}

View File

@ -226,7 +226,7 @@ export function NewAgentDialog() {
<DialogContent
showCloseButton={false}
className={cn(
"max-h-[min(calc(100dvh-2rem),46rem)] p-0 gap-0 overflow-hidden flex flex-col",
"max-h-(--sz-calc-16) p-0 gap-0 overflow-hidden flex flex-col",
mode === "invite" || mode === "prompt" ? "sm:max-w-2xl" : "sm:max-w-md",
)}
>
@ -395,7 +395,7 @@ export function NewAgentDialog() {
<Textarea
readOnly
value={latestAgentPrompt ?? ""}
className="h-[24rem] resize-y font-mono text-xs"
className="h-(--sz-24rem) resize-y font-mono text-xs"
/>
<Button
size="sm"

View File

@ -168,14 +168,14 @@ export function NewGoalDialog() {
</div>
{/* Description */}
<div className="px-4 pb-2 overflow-y-auto max-h-[50vh]">
<div className="px-4 pb-2 overflow-y-auto max-h-(--sz-50vh)">
<MarkdownEditor
ref={descriptionEditorRef}
value={description}
onChange={setDescription}
placeholder="Add description..."
bordered={false}
contentClassName={cn("text-sm text-muted-foreground", expanded ? "min-h-[220px]" : "min-h-[120px]")}
contentClassName={cn("text-sm text-muted-foreground", expanded ? "min-h-(--sz-220px)" : "min-h-(--sz-120px)")}
imageUploadHandler={async (file) => {
const asset = await uploadDescriptionImage.mutateAsync(file);
return asset.contentPath;

View File

@ -995,9 +995,9 @@ describe("NewIssueDialog", () => {
await flush();
const dialogContent = Array.from(container.querySelectorAll("div")).find((element) =>
typeof element.className === "string" && element.className.includes("max-h-[var(--new-issue-dialog-height)]"),
typeof element.className === "string" && element.className.includes("max-h-(--new-issue-dialog-height)"),
);
expect(dialogContent?.className).toContain("h-[var(--new-issue-dialog-height)]");
expect(dialogContent?.className).toContain("h-(--new-issue-dialog-height)");
expect(dialogContent?.className).toContain("overflow-hidden");
expect(dialogContent?.getAttribute("style")).toContain("env(safe-area-inset-top)");
expect(dialogContent?.getAttribute("style")).toContain("env(safe-area-inset-bottom)");

View File

@ -396,7 +396,7 @@ const IssueDescriptionEditor = memo(function IssueDescriptionEditor({
placeholder="Add description..."
bordered={false}
mentions={mentions}
contentClassName={cn("text-sm text-muted-foreground pb-12", expanded ? "min-h-[220px]" : "min-h-[120px]")}
contentClassName={cn("text-sm text-muted-foreground pb-12", expanded ? "min-h-(--sz-220px)" : "min-h-(--sz-120px)")}
imageUploadHandler={imageUploadHandler}
/>
);
@ -1268,9 +1268,9 @@ export function NewIssueDialog() {
aria-describedby={undefined}
style={{ "--new-issue-dialog-height": MOBILE_DIALOG_HEIGHT } as CSSProperties}
className={cn(
"flex h-[var(--new-issue-dialog-height)] max-h-[var(--new-issue-dialog-height)] flex-col gap-0 overflow-hidden p-0 sm:h-auto",
"flex h-(--new-issue-dialog-height) max-h-(--new-issue-dialog-height) flex-col gap-0 overflow-hidden p-0 sm:h-auto",
expanded
? "sm:max-w-2xl sm:h-[var(--new-issue-dialog-height)]"
? "sm:max-w-2xl sm:h-(--new-issue-dialog-height)"
: "sm:max-w-lg"
)}
onKeyDown={handleKeyDown}
@ -2252,7 +2252,7 @@ export function NewIssueDialog() {
</div>
<Button
size="sm"
className="min-w-[8.5rem] disabled:opacity-100"
className="min-w-(--sz-8_5rem) disabled:opacity-100"
disabled={!titleHasText || createIssue.isPending}
onClick={handleSubmit}
aria-busy={createIssue.isPending}

View File

@ -271,7 +271,7 @@ export function NewProjectDialog() {
placeholder="Add description..."
bordered={false}
mentions={mentionOptions}
contentClassName={cn("text-sm text-muted-foreground", expanded ? "min-h-[220px]" : "min-h-[120px]")}
contentClassName={cn("text-sm text-muted-foreground", expanded ? "min-h-(--sz-220px)" : "min-h-(--sz-120px)")}
imageUploadHandler={async (file) => {
const asset = await uploadDescriptionImage.mutateAsync(file);
return asset.contentPath;
@ -288,7 +288,7 @@ export function NewProjectDialog() {
<TooltipTrigger asChild>
<HelpCircle className="h-3 w-3 text-muted-foreground/50 cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[240px] text-xs">
<TooltipContent side="top" className="max-w-(--sz-240px) text-xs">
Link a GitHub repository so agents can clone, read, and push code for this project.
</TooltipContent>
</Tooltip>
@ -309,7 +309,7 @@ export function NewProjectDialog() {
<TooltipTrigger asChild>
<HelpCircle className="h-3 w-3 text-muted-foreground/50 cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[240px] text-xs">
<TooltipContent side="top" className="max-w-(--sz-240px) text-xs">
Set an absolute path on this machine where local agents will read and write files for this project.
</TooltipContent>
</Tooltip>
@ -361,7 +361,7 @@ export function NewProjectDialog() {
className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-xs"
>
<Target className="h-3 w-3 text-muted-foreground" />
<span className="max-w-[160px] truncate">{goal.title}</span>
<span className="max-w-(--sz-160px) truncate">{goal.title}</span>
<button
className="text-muted-foreground hover:text-foreground"
onClick={() => setGoalIds((prev) => prev.filter((id) => id !== goal.id))}

View File

@ -259,7 +259,7 @@ export function OnboardingChat({
{/* Messages */}
<div
ref={scrollRef}
className="flex-1 overflow-y-auto space-y-3 mb-3 min-h-[180px] max-h-[320px] pr-1"
className="flex-1 overflow-y-auto space-y-3 mb-3 min-h-(--sz-180px) max-h-(--sz-320px) pr-1"
>
{/* CEO welcome message + chips — delayed reveal */}
<WelcomeMessage

View File

@ -901,7 +901,7 @@ export function OnboardingWizard() {
<div className="group">
<label className="text-xs text-muted-foreground mb-1 block">What are your current workflows?</label>
<textarea
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-(--sz-60px)"
placeholder="e.g. Manual content creation, spreadsheet tracking, email outreach"
value={growWorkflows}
onChange={(e) => setGrowWorkflows(e.target.value)}
@ -910,7 +910,7 @@ export function OnboardingWizard() {
<div className="group">
<label className="text-xs text-muted-foreground mb-1 block">What pain points would you solve with AI?</label>
<textarea
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-(--sz-60px)"
placeholder="e.g. Can't produce content fast enough, no time for social media"
value={growPainPoints}
onChange={(e) => setGrowPainPoints(e.target.value)}
@ -945,7 +945,7 @@ export function OnboardingWizard() {
<div className="group">
<label className="text-xs text-foreground mb-1 block">Generated mission — edit however you like:</label>
<textarea
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-(--sz-60px)"
value={companyGoal}
onChange={(e) => setCompanyGoal(e.target.value)}
/>
@ -1080,7 +1080,7 @@ export function OnboardingWizard() {
Mission
</label>
<textarea
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-(--sz-60px)"
placeholder="What is your team trying to achieve?"
value={companyGoal}
onChange={(e) => setCompanyGoal(e.target.value)}
@ -1178,7 +1178,7 @@ export function OnboardingWizard() {
Here's your draft mission — edit it however you like:
</label>
<textarea
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[80px]"
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-(--sz-80px)"
value={companyGoal}
onChange={(e) => setCompanyGoal(e.target.value)}
autoFocus
@ -1369,7 +1369,7 @@ export function OnboardingWizard() {
</button>
</PopoverTrigger>
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-1"
className="w-(--radix-popover-trigger-width) p-1"
align="start"
>
<input
@ -1393,7 +1393,7 @@ export function OnboardingWizard() {
Default
</button>
)}
<div className="max-h-[240px] overflow-y-auto">
<div className="max-h-(--sz-240px) overflow-y-auto">
{groupedModels.map((group) => (
<div
key={group.provider}

View File

@ -99,7 +99,7 @@ export function PageSkeleton({ variant = "list" }: PageSkeletonProps) {
if (variant === "org-chart") {
return (
<div className="space-y-4">
<Skeleton className="h-[calc(100vh-4rem)] w-full rounded-lg border border-border" />
<Skeleton className="h-(--sz-calc-17) w-full rounded-lg border border-border" />
</div>
);
}

View File

@ -242,7 +242,7 @@ export function PipelineItemBodyDocument({
[conversationIssueId, doc?.latestRevisionId, latestBody, onStartConversation, pushToast, saveMutation],
);
const bodyContentClassName = "paperclip-edit-in-place-content min-h-[220px] text-(length:--fs-15) leading-7";
const bodyContentClassName = "paperclip-edit-in-place-content min-h-(--sz-220px) text-(length:--fs-15) leading-7";
const renderReadOnlyBody = (body: string) => (
<FoldCurtain className="max-w-3xl">
@ -268,7 +268,7 @@ export function PipelineItemBodyDocument({
onChange={setDraftBody}
placeholder="Write the item body in Markdown…"
bordered={false}
className="min-h-[220px] bg-transparent"
className="min-h-(--sz-220px) bg-transparent"
contentClassName={bodyContentClassName}
mentions={mentions}
imageUploadHandler={imageUploadHandler}

View File

@ -13,7 +13,7 @@ export function PropertiesPanel() {
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"
style={{ width: panelVisible ? 320 : 0, opacity: panelVisible ? 1 : 0 }}
>
<div className="w-80 flex-1 flex flex-col min-w-[320px] min-h-0">
<div className="w-80 flex-1 flex flex-col min-w-(--sz-320px) min-h-0">
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
<span className="text-sm font-medium">Properties</span>
<Button variant="ghost" size="icon-xs" onClick={() => setPanelVisible(false)}>

View File

@ -55,7 +55,7 @@ export function QuotaBar({
{/* deficit notch — 2px wide, sits at the fill tip */}
{showDeficitNotch && clampedPct > 0 && (
<div
className="absolute inset-y-0 w-[2px] bg-destructive z-10"
className="absolute inset-y-0 w-(--sz-2px) bg-destructive z-10"
style={{ left: `${notchLeft}%` }}
/>
)}

View File

@ -818,7 +818,7 @@ function RoutineRevisionDiffModal({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="!max-w-[90%] w-full max-h-[85vh] overflow-hidden flex flex-col">
<DialogContent className="!max-w-(--pct-90) w-full max-h-(--sz-85vh) overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Compare routine revisions</DialogTitle>
</DialogHeader>
@ -919,7 +919,7 @@ function RevisionPicker({
<select
value={value}
onChange={(event) => onChange(event.target.value)}
className="h-8 min-w-[12rem] rounded-md border border-border/60 bg-background px-2 text-xs"
className="h-8 min-w-(--sz-12rem) rounded-md border border-border/60 bg-background px-2 text-xs"
>
{revisions.map((revision) => (
<option key={revision.id} value={revision.id}>

View File

@ -322,9 +322,9 @@ describe("RoutineRunVariablesDialog", () => {
});
const dialogContent = Array.from(document.body.querySelectorAll("div")).find((element) =>
typeof element.className === "string" && element.className.includes("max-h-[calc(100dvh-2rem)]"),
typeof element.className === "string" && element.className.includes("max-h-(--sz-calc-18)"),
);
expect(dialogContent?.className).toContain("h-[calc(100dvh-2rem)]");
expect(dialogContent?.className).toContain("h-(--sz-calc-18)");
expect(dialogContent?.className).toContain("overflow-hidden");
const notesInput = document.querySelector("textarea");
@ -337,7 +337,7 @@ describe("RoutineRunVariablesDialog", () => {
expect(formScrollRegion?.contains(notesInput)).toBe(true);
const footer = Array.from(document.body.querySelectorAll("div")).find((element) =>
typeof element.className === "string" && element.className.includes("pb-[calc(1rem+env(safe-area-inset-bottom))]"),
typeof element.className === "string" && element.className.includes("pb-(--sz-calc-19)"),
);
expect(footer?.className).toContain("shrink-0");
expect(footer?.contains(formScrollRegion ?? null)).toBe(false);

View File

@ -339,7 +339,7 @@ export function RoutineRunVariablesDialog({
return (
<Dialog open={open} onOpenChange={(next) => !isPending && onOpenChange(next)}>
<DialogContent className="flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] max-w-xl flex-col gap-0 overflow-hidden p-0 sm:h-auto sm:max-h-[min(calc(100dvh-2rem),42rem)]">
<DialogContent className="flex h-(--sz-calc-18) max-h-(--sz-calc-18) max-w-xl flex-col gap-0 overflow-hidden p-0 sm:h-auto sm:max-h-(--sz-calc-20)">
<DialogHeader className="shrink-0 border-b border-border/60 px-6 pb-4 pr-12 pt-6">
{routineName && (
<p className="text-muted-foreground text-sm">{routineName}</p>
@ -532,7 +532,7 @@ export function RoutineRunVariablesDialog({
<DialogFooter
showCloseButton={false}
className="shrink-0 border-t border-border/60 bg-background px-6 pb-[calc(1rem+env(safe-area-inset-bottom))] pt-4"
className="shrink-0 border-t border-border/60 bg-background px-6 pb-(--sz-calc-19) pt-4"
>
{!selection.assigneeAgentId ? (
<p className="mr-auto text-xs text-amber-600">Default agent required for this run.</p>

View File

@ -110,7 +110,7 @@ export function RoutineSubSidebar({
return (
<nav
aria-label="Routine sections"
className="sticky top-0 hidden max-h-[100dvh] w-52 shrink-0 flex-col gap-4 self-start overflow-y-auto border-r border-border bg-sidebar/30 px-3 py-4 md:flex"
className="sticky top-0 hidden max-h-(--sz-100dvh) w-52 shrink-0 flex-col gap-4 self-start overflow-y-auto border-r border-border bg-sidebar/30 px-3 py-4 md:flex"
>
{NAV_GROUPS.map((group) => (
<div key={group.label} className="flex flex-col gap-0.5">

View File

@ -300,7 +300,7 @@ export function ScheduleEditor({
emitChange(preset, h, minute, dayOfWeek, dayOfMonth, customCron);
}}
>
<SelectTrigger className="w-[120px]">
<SelectTrigger className="w-(--sz-120px)">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -319,7 +319,7 @@ export function ScheduleEditor({
emitChange(preset, hour, m, dayOfWeek, dayOfMonth, customCron);
}}
>
<SelectTrigger className="w-[80px]">
<SelectTrigger className="w-(--sz-80px)">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -343,7 +343,7 @@ export function ScheduleEditor({
emitChange(preset, hour, m, dayOfWeek, dayOfMonth, customCron);
}}
>
<SelectTrigger className="w-[80px]">
<SelectTrigger className="w-(--sz-80px)">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -391,7 +391,7 @@ export function ScheduleEditor({
emitChange(preset, hour, minute, dayOfWeek, dom, customCron);
}}
>
<SelectTrigger className="w-[80px]">
<SelectTrigger className="w-(--sz-80px)">
<SelectValue />
</SelectTrigger>
<SelectContent>

View File

@ -74,8 +74,8 @@ export function ScrollToBottom() {
<button
onClick={scroll}
className={cn(
"fixed bottom-[calc(1.5rem+5rem+env(safe-area-inset-bottom))] 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",
panelVisible && panelContent && "md:right-[calc(320px+1.5rem)]",
"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",
panelVisible && panelContent && "md:right-(--sz-calc-22)",
)}
aria-label="Scroll to bottom"
>

View File

@ -231,8 +231,8 @@ export function SearchableSelect<
className={cn(
"p-0",
contentWidth === "trigger"
? "w-[var(--radix-popover-trigger-width)] min-w-56 max-w-[min(32rem,calc(100vw-2rem))]"
: "w-72 max-w-[min(32rem,calc(100vw-2rem))]",
? "w-(--radix-popover-trigger-width) min-w-56 max-w-(--sz-calc-23)"
: "w-72 max-w-(--sz-calc-23)",
contentClassName,
)}
onKeyDownCapture={(event) => {

View File

@ -130,7 +130,7 @@ describe("SidebarAccountMenu", () => {
expect(document.body.textContent).toContain("Paperclip v1.2.3");
expect(document.body.textContent).toContain("jane@example.com");
expect(document.body.querySelector('[data-slot="popover-content"]')?.className)
.toContain("w-[277px]");
.toContain("w-(--sz-277px)");
expect(document.body.querySelector('a[href="/company/settings/instance/profile"]')).not.toBeNull();
await act(async () => {

View File

@ -158,7 +158,7 @@ export function SidebarAccountMenu({
side="top"
align="start"
sideOffset={10}
className="w-[277px] max-w-[calc(100vw-1rem)] overflow-hidden rounded-t-2xl rounded-b-none border-border p-0 shadow-2xl"
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="-mt-8 px-4 pb-4">

View File

@ -106,7 +106,7 @@ function SortableCompanyItem({
type="button"
ref={setActivatorNodeRef}
aria-label={`Reorder ${company.name}`}
className="inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring"
className="inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-(length:--rad-2) focus-visible:ring-ring"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();

View File

@ -108,20 +108,20 @@ export function SidebarNavItem({
<span className="relative shrink-0">
<Icon className="h-4 w-4" />
{alert && (
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-red-500 shadow-[0_0_0_2px_hsl(var(--background))]" aria-hidden="true" />
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-red-500 shadow-(--shadow-extract-12)" aria-hidden="true" />
)}
{/* Collapsed rail: numeric badge / live count collapse to a dot on the
icon. The icon markup is untouched so it stays pixel-aligned. */}
{rail && !alert && hasLive && (
<span className="absolute -right-0.5 -top-0.5 flex h-2 w-2" aria-hidden="true">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500 shadow-[0_0_0_2px_hsl(var(--background))]" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500 shadow-(--shadow-extract-12)" />
</span>
)}
{rail && !alert && !hasLive && hasBadge && (
<span
className={cn(
"absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full shadow-[0_0_0_2px_hsl(var(--background))]",
"absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full shadow-(--shadow-extract-12)",
badgeTone === "danger" ? "bg-red-600" : "bg-primary",
)}
aria-hidden="true"

View File

@ -63,7 +63,7 @@ export function SourceResolvedFoldCallout({
aria-label="Source-resolved watchdog fold"
data-source-resolved-fold
className={cn(
"relative w-full overflow-hidden rounded-lg border text-sm shadow-[0_1px_0_rgba(15,23,42,0.02)]",
"relative w-full overflow-hidden rounded-lg border text-sm shadow-(--shadow-extract-8)",
"border-emerald-300/70 bg-emerald-50/80 text-emerald-950",
"dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-100",
className,

View File

@ -67,7 +67,7 @@ export function AgentStatusCapsule({ status }: { status: string }) {
return (
<span
aria-hidden
className={cn("status-fill inline-block h-4 w-2 rounded-[4px] shrink-0", motion)}
className={cn("status-fill inline-block h-4 w-2 rounded-(--rad-4) shrink-0", motion)}
style={scStyle(cssVar)}
/>
);

View File

@ -236,7 +236,7 @@ export function SystemNotice({
role="status"
aria-label={resolvedLabel}
className={cn(
"relative w-full overflow-hidden rounded-lg border text-sm shadow-[0_1px_0_rgba(15,23,42,0.02)]",
"relative w-full overflow-hidden rounded-lg border text-sm shadow-(--shadow-extract-8)",
tokens.container,
className,
)}

View File

@ -185,7 +185,7 @@ function WorkspaceFileRow({ item, treeItemId, selected, highlighted, depth, onOp
onMouseEnter={onHover}
title={item.displayPath}
className={cn(
"flex min-h-[36px] cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 sm:min-h-0",
"flex min-h-(--sz-36px) cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 sm:min-h-0",
selected ? "bg-accent text-foreground" : highlighted ? "bg-accent/50" : "hover:bg-accent/60",
)}
style={{ paddingLeft: `${0.5 + depth * 0.875}rem` }}
@ -384,7 +384,7 @@ function WorkspaceFileTree({
aria-expanded={expanded}
title={node.key}
onClick={() => onToggleFolder(node.key)}
className="flex min-h-[32px] w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-accent/60"
className="flex min-h-(--sz-32px) w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-accent/60"
style={{ paddingLeft: `${0.25 + node.depth * 0.875}rem` }}
>
{expanded ? (
@ -400,7 +400,7 @@ function WorkspaceFileTree({
{children.map(renderNode)}
{loading ? (
<div
className="flex min-h-[30px] items-center gap-2 py-1 pr-2 text-xs text-muted-foreground"
className="flex min-h-(--sz-30px) items-center gap-2 py-1 pr-2 text-xs text-muted-foreground"
style={{ paddingLeft: `${1 + (node.depth + 1) * 0.875}rem` }}
>
<Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
@ -411,7 +411,7 @@ function WorkspaceFileTree({
<button
type="button"
onClick={() => onLoadMoreFolder?.(node.key)}
className="flex min-h-[30px] w-full items-center gap-2 rounded-md py-1 pr-2 text-left text-xs text-muted-foreground hover:bg-accent/60 hover:text-foreground"
className="flex min-h-(--sz-30px) w-full items-center gap-2 rounded-md py-1 pr-2 text-left text-xs text-muted-foreground hover:bg-accent/60 hover:text-foreground"
style={{ paddingLeft: `${1 + (node.depth + 1) * 0.875}rem` }}
>
<span className="h-3.5 w-3.5 shrink-0" />

View File

@ -52,7 +52,7 @@ describe("ArtifactCard", () => {
expect(markup).toContain('href="/issues/PAP-10306#attachment-art-1"');
expect(markup).toContain('data-disable-issue-quicklook="true"');
expect(markup).toContain('data-media-kind="image"');
expect(markup).toContain("rounded-[8px]");
expect(markup).toContain("rounded-(--rad-8)");
expect(markup).toContain('src="/files/art-1.png"');
expect(markup).toContain("object-cover");
expect(markup).toContain("Hero shot");

View File

@ -197,7 +197,7 @@ export function ArtifactCard({ artifact }: ArtifactCardProps) {
disableIssueQuicklook
data-testid="artifact-card"
data-media-kind={artifact.mediaKind}
className="group flex flex-col overflow-hidden rounded-[8px] border border-border bg-card transition-colors hover:border-foreground/20"
className="group flex flex-col overflow-hidden rounded-(--rad-8) border border-border bg-card transition-colors hover:border-foreground/20"
>
<ArtifactPreview artifact={artifact} />

View File

@ -29,12 +29,12 @@ export function ArtifactGroupCard({ group, to }: ArtifactGroupCardProps) {
<div
aria-hidden="true"
data-testid="artifact-stack-layer"
className="pointer-events-none absolute inset-0 translate-x-[8px] translate-y-[8px] rounded-[8px] border border-border bg-muted/40 shadow-sm"
className="pointer-events-none absolute inset-0 translate-x-(--sz-8px) translate-y-(--sz-8px) rounded-(--rad-8) border border-border bg-muted/40 shadow-sm"
/>
<div
aria-hidden="true"
data-testid="artifact-stack-layer"
className="pointer-events-none absolute inset-0 translate-x-[4px] translate-y-[4px] rounded-[8px] border border-border bg-muted/70 shadow-sm"
className="pointer-events-none absolute inset-0 translate-x-(--sz-4px) translate-y-(--sz-4px) rounded-(--rad-8) border border-border bg-muted/70 shadow-sm"
/>
</>
) : null}
@ -46,7 +46,7 @@ export function ArtifactGroupCard({ group, to }: ArtifactGroupCardProps) {
data-group-id={group.id}
data-count={group.count}
data-stacked={stacked ? "true" : "false"}
className="group relative flex flex-col overflow-hidden rounded-[8px] border border-border bg-card transition-colors hover:border-foreground/20"
className="group relative flex flex-col overflow-hidden rounded-(--rad-8) border border-border bg-card transition-colors hover:border-foreground/20"
>
<div className="relative">
{preview ? (

View File

@ -257,7 +257,7 @@ export function SecretPicker({
searchPlaceholder="Search secrets…"
emptyMessage="No matching secrets"
triggerClassName={cn(
"h-[34px] min-h-[34px] font-mono text-sm",
"h-(--sz-34px) min-h-(--sz-34px) font-mono text-sm",
boundMissing && "border-destructive text-destructive",
boundSecret && boundSecret.status !== "active" && "border-amber-500/60",
triggerClassName,

View File

@ -464,7 +464,7 @@ export const EnvironmentVariablesEditor = forwardRef<EnvironmentVariablesEditorH
className="mt-3 flex w-full flex-col gap-3 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-3 text-amber-950 shadow-sm dark:bg-amber-500/15 dark:text-amber-100 @[34rem]/env:flex-row @[34rem]/env:items-center @[34rem]/env:justify-between"
>
<div className="flex min-w-0 items-center gap-2 text-sm font-medium">
<span className="size-2 rounded-full bg-amber-500 shadow-[0_0_0_3px_rgba(245,158,11,0.18)]" />
<span className="size-2 rounded-full bg-amber-500 shadow-(--shadow-extract-13)" />
<span>Unsaved changes</span>
</div>
<div className="flex items-center gap-2">

View File

@ -61,7 +61,7 @@ export function AssigneeChip({
<span className={cn(CHIP_CLASS, className)} data-testid="handoff-assignee-chip" data-kind="agent">
<span className="sr-only">Agent </span>
<AgentIcon icon={agentIcon(assignee.agentId, resolvers)} className="h-3 w-3 shrink-0 text-muted-foreground" />
<span className="max-w-[12rem] truncate">{agentName(assignee.agentId, resolvers)}</span>
<span className="max-w-(--sz-12rem) truncate">{agentName(assignee.agentId, resolvers)}</span>
</span>
);
}
@ -70,7 +70,7 @@ export function AssigneeChip({
<span className={cn(CHIP_CLASS, className)} data-testid="handoff-assignee-chip" data-kind="user">
<span className="sr-only">User </span>
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
<span className="max-w-[12rem] truncate">{userLabel(assignee.userId, resolvers)}</span>
<span className="max-w-(--sz-12rem) truncate">{userLabel(assignee.userId, resolvers)}</span>
</span>
);
}

View File

@ -1947,7 +1947,7 @@ export function IssueProperties({
onOpenChange={(open) => { setProjectOpen(open); if (!open) setProjectSearch(""); }}
triggerContent={projectTrigger}
triggerClassName="min-w-0 max-w-full"
popoverClassName="w-fit min-w-[11rem]"
popoverClassName="w-fit min-w-(--sz-11rem)"
extra={issue.projectId ? (
<Link
to={projectLink(issue.projectId)!}
@ -2143,7 +2143,7 @@ export function IssueProperties({
onOpenChange={setScheduledRetryOpen}
triggerContent={scheduledRetryTrigger}
triggerClassName="min-w-0 max-w-full"
popoverClassName={cn("max-w-full", inline ? "w-full" : "w-80 sm:w-[32rem]")}
popoverClassName={cn("max-w-full", inline ? "w-full" : "w-80 sm:w-(--sz-32rem)")}
extra={scheduledRetryAttemptBadge}
>
{scheduledRetryContent}
@ -2157,7 +2157,7 @@ export function IssueProperties({
onOpenChange={setMonitorOpen}
triggerContent={monitorTrigger}
triggerClassName="min-w-0 max-w-full"
popoverClassName={cn("max-w-full", inline ? "w-full" : "w-80 sm:w-[32rem]")}
popoverClassName={cn("max-w-full", inline ? "w-full" : "w-80 sm:w-(--sz-32rem)")}
extra={monitorAttemptBadge}
>
{monitorContent}

View File

@ -124,7 +124,7 @@ function ExternalObjectPropertyValue({ group }: { group: IssueExternalObjectGrou
const className = cn(
"inline-flex min-w-0 max-w-full items-center gap-1.5 text-sm no-underline",
externalObjectPropertyTone(group),
pill.url ? "hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring" : "",
pill.url ? "hover:underline focus-visible:outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring" : "",
);
if (pill.url) {

View File

@ -28,7 +28,7 @@ export function RemovableIssueReferencePill({
const chipClassName = cn(
"paperclip-mention-chip paperclip-mention-chip--issue",
"inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-xs no-underline",
issue.identifier && "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring",
issue.identifier && "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring",
);
const content = (
<>
@ -52,7 +52,7 @@ export function RemovableIssueReferencePill({
<span className="group relative inline-flex">
<button
type="button"
className="absolute -right-1 -top-1 z-10 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border bg-background text-muted-foreground opacity-0 shadow-sm transition-colors transition-opacity hover:bg-destructive/10 hover:text-destructive focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring group-hover:opacity-100"
className="absolute -right-1 -top-1 z-10 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border bg-background text-muted-foreground opacity-0 shadow-sm transition-colors transition-opacity hover:bg-destructive/10 hover:text-destructive focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-(length:--rad-2) focus-visible:ring-ring group-hover:opacity-100"
aria-label={removeLabel}
title={removeLabel}
onClick={handleRemove}

View File

@ -254,7 +254,7 @@ export function OverviewSection({
onChange={(description) => setEditDraft((current) => ({ ...current, description }))}
placeholder="Add instructions..."
bordered={false}
contentClassName="min-h-[120px] text-(length:--fs-15) leading-7"
contentClassName="min-h-(--sz-120px) text-(length:--fs-15) leading-7"
mentions={mentionOptions}
onSubmit={() => {
if (!saveRoutine.isPending && editDraft.title.trim()) {
@ -270,7 +270,7 @@ export function OverviewSection({
onChange={(description) => setEditDraft((current) => ({ ...current, description }))}
placeholder="Add instructions..."
bordered={false}
contentClassName="min-h-[120px] text-(length:--fs-15) leading-7"
contentClassName="min-h-(--sz-120px) text-(length:--fs-15) leading-7"
mentions={mentionOptions}
onSubmit={() => {
if (!saveRoutine.isPending && editDraft.title.trim()) {

View File

@ -151,7 +151,7 @@ function SearchResultRowImpl({
alt=""
loading="lazy"
decoding="async"
className="h-[88px] w-[88px] shrink-0 rounded-md border border-border bg-muted object-cover"
className="h-(--sz-88px) w-(--sz-88px) shrink-0 rounded-md border border-border bg-muted object-cover"
/>
) : null}
</div>
@ -224,7 +224,7 @@ function SearchResultRowImpl({
alt=""
loading="lazy"
decoding="async"
className="h-[88px] w-[88px] shrink-0 rounded-md border border-border bg-muted object-cover"
className="h-(--sz-88px) w-(--sz-88px) shrink-0 rounded-md border border-border bg-muted object-cover"
/>
) : null}
</div>

View File

@ -388,7 +388,7 @@ export function WorkTimelineChart({
<div className="relative">
<div
ref={scrollRef}
className="max-h-[70vh] overflow-auto"
className="max-h-(--sz-70vh) overflow-auto"
data-testid="work-timeline-scroll"
onScroll={(e) => {
setScrollLeft(e.currentTarget.scrollLeft);
@ -721,7 +721,7 @@ function Tooltip({ tooltip, now }: { tooltip: TooltipState; now: number }) {
const left = Math.min(tooltip.x + 14, (typeof window !== "undefined" ? window.innerWidth : 1200) - 300);
return (
<div
className="pointer-events-none fixed z-50 max-w-[280px] rounded-md border border-foreground bg-card px-2.5 py-2 text-xs shadow-md"
className="pointer-events-none fixed z-50 max-w-(--sz-280px) rounded-md border border-foreground bg-card px-2.5 py-2 text-xs shadow-md"
style={{ left, top: tooltip.y + 14 }}
>
<div className="text-(length:--fs-13) font-medium text-foreground">{truncate(title)}</div>

View File

@ -1145,7 +1145,7 @@ function TranscriptEventRow({
) : block.tone === "warn" ? (
<TerminalSquare className="mt-0.5 h-3.5 w-3.5 shrink-0" />
) : (
<span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-current/50" />
<span className="mt-(--sz-7px) h-1.5 w-1.5 shrink-0 rounded-full bg-current/50" />
)}
<div className="min-w-0 flex-1">
{block.label === "result" && block.tone !== "error" ? (

View File

@ -48,7 +48,7 @@ function AlertDialogContent({
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] fixed top-[max(1rem,env(safe-area-inset-top))] md:top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-0 md:translate-y-[-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-[cubic-bezier(0.16,1,0.3,1)] outline-none sm:max-w-lg [&>*]:min-w-0",
className
)}
{...props}

View File

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

View File

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

View File

@ -14,7 +14,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-(--rad-4) border shadow-xs transition-shadow outline-none focus-visible:ring-(length:--rad-3) disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}

View File

@ -100,7 +100,7 @@ function CommandList({
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
"max-h-(--sz-300px) scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}

View File

@ -59,7 +59,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] data-[state=closed]:slide-out-to-top-[1%] data-[state=open]:slide-in-from-top-[1%] fixed top-[max(1rem,env(safe-area-inset-top))] md:top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-0 md:translate-y-[-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-[cubic-bezier(0.16,1,0.3,1)] outline-none sm:max-w-lg [&>*]:min-w-0",
className
)}
{...props}

View File

@ -42,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"bg-popover text-popover-foreground 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-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-(--sz-8rem) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
@ -230,7 +230,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
"bg-popover text-popover-foreground 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-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-(--sz-8rem) origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}

View File

@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
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",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3)",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}

View File

@ -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-[3px] focus-visible:outline-1 [&>div]:!block"
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"
>
{children}
</ScrollAreaPrimitive.Viewport>

Some files were not shown because too many files have changed in this diff Show More