Skip to content

fix(design-system): stop the radius contract from mandating invalid CSS - #1514

Merged
jackwener merged 2 commits into
mainfrom
fix/radius-calc-spacing
Jul 27, 2026
Merged

fix(design-system): stop the radius contract from mandating invalid CSS#1514
jackwener merged 2 commits into
mainfrom
fix/radius-calc-spacing

Conversation

@jackwener

Copy link
Copy Markdown
Member

search-modal.css renders square corners where the concentric formula asks for 4px — and the governance test is why.

CSS requires whitespace around +/- inside calc(). Without it the declaration is a parse error, the browser drops it, and the box falls back to radius 0. Measured in the renderer:

calc(var(--radius-modal)-8px) -> 0px (dropped)
calc(var(--radius-modal) - 8px) -> 4px (intended)

The contract accepted only the broken spelling. Two rules combined to force it:

  • findCssOffenders split corner values on /\s+/, tearing the valid form into three nonsense corners — calc(var(--radius-modal), -, 8px) — none of which is an allowed corner, so correct CSS was rejected.
  • CALC_ALLOW_RE used \s*-\s*, so the unspaced form passed.

The only spelling satisfying both was the one the browser throws away.

Fix

  • splitCorners() splits on whitespace at paren depth 0, so a calc() stays one corner while genuine multi-corner values still split.
  • CALC_ALLOW_RE now requires the whitespace CSS requires, so the invalid spelling is a contract failure rather than the mandated form.
  • search-modal.css:32 moves to the valid form. Verified live — the .maka-search-modal-input-row rule now computes 4px.

Tailwind is deliberately exempt

Arbitrary values keep the unspaced spelling: a literal space there would have to be written _, and Tailwind emits valid CSS either way. Confirmed against the built bundle:

source rounded-[calc(var(--radius-md)-1px)]
built calc(var(--radius-md) - 1px) normalized
source border-radius: calc(var(--radius-modal)-8px)
built calc(var(--radius-modal)-8px) verbatim

So the TSX scanner keeps a lenient isWhitelistedTailwindCalc (which also accepts _-escaped spaces) while the CSS scanner is strict. Same rule, two languages. The token allowlist and shrink-only constraint apply to both.

New cases cover the unspaced CSS rejection, the Tailwind acceptance, and splitCorners end-to-end through the scanner.

Found while landing #1511, which had to route around the contract by spelling a shrink as its tier token.

Gates

2858 desktop tests green · biome clean · 0 dead CSS.

`search-modal.css` rendered square corners where the concentric formula
asks for 4px, and the governance test was the reason.
CSS requires whitespace around `+`/`-` inside `calc()`. Without it the
declaration is a parse error and the browser drops it, so the box falls
back to radius 0. Measured in the renderer:
calc(var(--radius-modal)-8px) -> 0px (dropped)
calc(var(--radius-modal) - 8px) -> 4px (intended)
The contract accepted only the broken spelling. Two rules combined to
force it: `findCssOffenders` split corner values on `/\s+/`, which tore
the valid form into three nonsense corners (`calc(var(--radius-modal)`,
`-`, `8px)`) and rejected it; and `CALC_ALLOW_RE` used `\s*-\s*`, so the
unspaced form passed. The only spelling that satisfied both was the one
the browser throws away.
- `splitCorners()` splits on whitespace at paren depth 0, so a calc()
stays one corner and genuine multi-corner values still split.
- `CALC_ALLOW_RE` now requires the whitespace CSS requires, making the
invalid spelling a contract failure rather than the mandated one.
- `search-modal.css:32` moves to the valid form. Verified live: the
`.maka-search-modal-input-row` rule now computes 4px.
Tailwind arbitrary values keep the unspaced spelling, deliberately — a
literal space there would have to be written `_`, and Tailwind emits
valid CSS either way. Confirmed against the built bundle:
source rounded-[calc(var(--radius-md)-1px)]
-> built calc(var(--radius-md) - 1px) normalized
source border-radius: calc(var(--radius-modal)-8px)
-> built calc(var(--radius-modal)-8px) verbatim
So the TSX scanner keeps a lenient `isWhitelistedTailwindCalc` (which
also accepts `_`-escaped spaces) while the CSS scanner is strict. Same
rule, two languages; the token allowlist and shrink-only constraint
apply to both.
Found while landing #1511, which had to route around the contract by
spelling a shrink as its tier token.
Gates: 2858 desktop tests green, biome clean, 0 dead CSS.
Review of this PR found that `radius-nesting-contract.test.ts:63` is a
SECOND gate on the same declaration `search-modal.css` shipped broken,
and it still matched `\s*-\s*` — so after tightening the converge
contract it became the only remaining place that would accept the
unspaced spelling CSS drops as a parse error. Two gates on one site are
only worth having if they agree.
Also corrects that entry's label: the selector lives in
`styles/search-modal.css`, not `sidebar.css`. The label is what the
failure message points a future reader at, so a stale one sends them to
the wrong file.
Gates: 2874 desktop tests green, biome clean.
@jackwener
jackwener merged commit fb4724a into mainJul 27, 2026
3 checks passed
jackwener added a commit that referenced this pull request Jul 27, 2026
The CSS governance scanner matched `border-radius` values with the class
`[^;}\n]+`. Excluding `\n` meant a declaration whose VALUE wrapped onto a
second line matched nothing at all — the scanner skipped it in silence
rather than reporting it, so any radius could escape the token contract
just by being formatted across two lines:
border-radius: 10px
12px; /* never scanned */
Neither prettier nor biome reflows such a value back onto one line, so a
long four-corner shorthand or calc() reaches this state on its own.
(A newline directly after the colon was always fine — the `\s*` there
already spans it. Only a newline *inside* the value was affected, which
is narrower than it first looks.)
Widening the class to `[^;}]+` cannot make a match outrun its own
declaration: `;` and `}` are still excluded, so it stops at the first
declaration terminator or the end of the rule body either way. The
regression test covering that is asserted alongside the fix.
Found while reviewing #1514, which fixed a separate bug in the same
contract (it accepted only invalid unspaced calc()). This is the
remaining scanner gap.
jackwener added a commit that referenced this pull request Jul 27, 2026
…ues (#1520)
* fix(design-system): stop the radius scanner from skipping wrapped values
The CSS governance scanner matched `border-radius` values with the class
`[^;}\n]+`. Excluding `\n` meant a declaration whose VALUE wrapped onto a
second line matched nothing at all — the scanner skipped it in silence
rather than reporting it, so any radius could escape the token contract
just by being formatted across two lines:
border-radius: 10px
12px; /* never scanned */
Neither prettier nor biome reflows such a value back onto one line, so a
long four-corner shorthand or calc() reaches this state on its own.
(A newline directly after the colon was always fine — the `\s*` there
already spans it. Only a newline *inside* the value was affected, which
is narrower than it first looks.)
Widening the class to `[^;}]+` cannot make a match outrun its own
declaration: `;` and `}` are still excluded, so it stops at the first
declaration terminator or the end of the rule body either way. The
regression test covering that is asserted alongside the fix.
Found while reviewing #1514, which fixed a separate bug in the same
contract (it accepted only invalid unspaced calc()). This is the
remaining scanner gap.
* fix(ci): restore green main after #1515
`main` has been red since 17362dc (#1515); 0e80fe1 (#1359) inherited it.
All three jobs failed, none of it visible in that PR's own checks because
the earliest failure short-circuits the rest of the job.
typecheck job
- `format:check`: scripts/build-cursor-overlay.mjs was committed
unformatted. Reapplied `biome format`; purely mechanical.
- `knip`: #1515 added a second overlay entry point pair
(permission-overlay{,-preload}.ts) but registered only the cursor
overlay in knip.json, so both files read as unused. Registered them
beside their cursor-overlay twins — they are loaded by path at
runtime (loadFile / preload), never imported.
- `knip`: asOsPermissionId was exported and referenced nowhere in the
repo. It is an identity function (`return id`), so nothing is lost by
dropping it — git history holds it if Stage 2 wants it back. Its sole
import (OsPermissionId) went with it.
test job
- `check-console`: three console.warn sites in
permission-overlay-main.ts (locale fallback, missing .app bundle, the
controller log sink). These are main-process diagnostics that expose
paths, not secrets — the same category as the daily-review and
settings-runtime entries — so they are allow-listed with a reason
per the script's own documented convention.
e2e job
- `settings.spec.ts:141` asserted a permission row draws exactly two
grant buttons. #1515 added a third (拖拽授权) to precisely the row
the fixture aims at — screen_recording is requestable, openable AND
drag-grantable — so the `=== 2` filter matched nothing and the guard
silently measured no rows. Relaxed to `>= 2`, which is what the
contract was always about: a row whose actions track competes with
the body. A wider actions track only makes the squeeze worse, so the
three-button row is strictly the better subject.
Verified locally: lint, format:check, build, typecheck, knip (both
workspaces), check-console, and the full test:dist suite.
jackwener added a commit that referenced this pull request Jul 27, 2026
…rns (#1535)
An audit of the radius system found the same failure repeated: the rules
were not derived from the design language, they were retrofitted to
whatever the CSS already said. The suite was 18/18 green — but green
because the non-conforming parts were either absent from the tables or
had the rule rewritten around them. Two contracts had gone further and
were actively holding defects in place.
Contracts that were protecting defects
--------------------------------------
.providerLogo — PR-UI-13 had pinned a single ~27-28% radius ratio across
plate sizes after an audit found the 44px and 32px plates reading as
different shapes. The tier convergence rewrote both to --radius-surface,
which is 18.2% at 44px and 25% at 32px: the exact drift that PR removed.
Both inline comments still claimed 27.3% / 28.1%, now arithmetically
false, and SELECTOR_TIER pinned the regression, so restoring the ratio
failed CI.
Square icon plates are governed by RATIO, not by tier — a fixed px
cannot read the same at 32px and 48px. Added --radius-plate: 27%, which
is scale-invariant, restores the PR-UI-13 anchor (12px at 44px), and is
correct at sizes not yet added. Applied to all six plates, which had
drifted to four different ratios (18.2 / 25 / 16.7 / 22.2%). The compact
override and the catalog-row override no longer restate a radius at all
— with a ratio the size change alone is enough.
.maka-search-modal-input-row — pinned by radius-nesting-contract to
calc(var(--radius-modal) - 8px) = 4px, described as concentric nesting
inside the 12px shell, with a warning that a tier "would read as too
round". Measured live: that row is the middle of a 3-row grid, 57px
below the shell's top edge, 12px in from its sides, adjacent to no shell
corner. The 8px was the vertical margin to the header band — an axis
with nothing to do with the corner. Its twin .maka-palette-input-wrap
(same primitive, same position, same shell) used the control tier all
along, so the two searches differed by 50% in roundness. Now control,
and pinned in SELECTOR_TIER so they stay together.
The rule itself was the root cause: it said only "with padding between
them" and never mentioned adjacency, so the search modal satisfied it
literally. Rewritten in maka-tokens.css and the nesting contract to
state the precondition — the inner corner must sit in the outer corner,
with an equal inset on both edges meeting there. Its worked example was
this very bug; it now cites the one genuinely concentric site instead.
Rules rewritten to fit the code
-------------------------------
TabsList / TabsTrigger were dropped from COMPONENT_RADIUS when #499
moved them to primitives/tabs.tsx, on the stated grounds that they were
"governed by primitives-design-contract escape hatches". That file
contains no radius assertion at all, leaving the vocabulary's own "tab
trigger" and "tab list" as the only named roles governed nowhere. The
components had merely changed file; re-pointed.
The vocabulary said `checkbox` belongs to pill/999px while every
checkbox renders rounded-[var(--radius-control)]. The code is right (a
fully round checkbox is a radio); the vocabulary was wrong, and because
Checkbox is absent from COMPONENT_RADIUS nothing ever caught it.
.enabledEmptyChip was pinned to control by reading its class name — it
renders as a full-width two-line card with 16px padding. Surface now,
matching its twin .settingsWechatQrState.
Dead by construction
--------------------
input-group's `**:[textarea_button]:rounded-[calc(var(--radius-sm)-1px)]`
compiles to `:is(... *):is(textarea button)` — a <button> inside a
<textarea>, which the HTML content model forbids. Not merely unused;
unmatchable. Removed.
Also refreshed the segmented.css comment, which justified using the tier
over the calc form on the grounds that the calc was unspellable — true
until #1514 fixed the value splitter. The tier is still right, for the
better reason that it survives a token revalue.
Not changed, deliberately
-------------------------
Toggle / ToggleGroup look dead from the app's side (no JSX call sites)
but are design-system primitives with a dedicated story, pinned by
storybook-baseline-contract. Their entries stay.
The tier VALUES themselves (6/8/12/999) are untouched. The audit raised
real questions there — one control token spans 16px Kbd to 36px Input,
a 2.25x swing in roundness, and 6-vs-8 is below the perceptual
threshold as a global semantic distinction — but changing them moves
the whole app's surface and is a design decision, not a defect fix.
Verified: lint, format:check, build, typecheck, knip (both workspaces),
check-console, check-dead-css, and 2879 main-process tests. Plate
geometry was compared visually at all four sizes before and after.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener