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:
parent
4b3f087d7f
commit
be63424987
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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,
|
||||
)}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) && (
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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))}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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}%` }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" ? (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue