Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) by Astro-Han · Pull Request #527 · apache/maka · GitHub
Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) by Astro-Han · Pull Request #527 · apache/maka · GitHub
Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) by Astro-Han · Pull Request #527 · apache/maka · GitHub
Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) by Astro-Han · Pull Request #527 · apache/maka · GitHub
Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) by Astro-Han · Pull Request #527 · apache/maka · GitHub
Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) by Astro-Han · Pull Request #527 · apache/maka · GitHub
Skip to content

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527

Merged
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift
Jul 4, 2026
Merged

refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3)#527
Astro-Han merged 8 commits into
mainfrom
refactor/anti-layout-shift

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Converge the three anti-layout-shift dimensions in #520 PR3 — tabular-nums on numeric surfaces, min-w-0 on truncation surfaces, min-width on state-swap surfaces. Each dimension adds a presence contract so its class of bug can't regress. Same converge-contract pattern as #430 / #448 / #499.

Why

Refs #520 (PR3). The roadmap calls out all three as fill + contract governance sweeps: §1.6 (tabular-nums fill per numeric surface), §4.1 (text-swap min-width is the chat layout-shift root cause), and missing min-w-0 is a historic truncation-bug source.

Scope

Three dimensions, one contract each:

tabular-nums — 12 numeric surfaces found missing tabular-nums (10 self-refreshing timestamps fixed in one shot on RelativeTime, plus usage stats table, metric cards, health/skill/search/provider/plan counts). Contract: whitelist presence guard locking 31 known numeric surfaces + RelativeTime.

min-w-0 — 12 flex/grid truncation children missing min-width: 0 (daily-review session/archive/preview, plan template/run, model-table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without it the default min-width: auto stops ellipsis from firing. Contract: heuristic scan of every ellipsis + nowrap rule (grid-minmax + dead-ellipsis exceptions).

text-swap min-width — 30 state-swap surfaces across memory/open-gateway/daily-review/error-boundary (复制↔已复制, 保存↔保存中…, token counts 9→100, 归档↔归档到草稿, etc.). Contract is three layers:

  1. scoped discovery — scan the PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keep min-w-[Nrem] AND be value-pinned (or in EXCEPTIONS). This catches omissions the whitelist hasn't listed (reload, backup-candidate, instruction-file, memory-entry actions all slipped through earlier review rounds). Inline-ternary omissions in these files are auto-caught.
  2. exact value pin — each known button keeps a SPECIFIC min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock. Chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings.
  3. computed-label boundary — buttons whose children is a variable holding a ternary result ({copyLabel} / {statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned in TEXT_SWAP_BUTTONS. The contract has a COMPUTED-LABEL note + a test asserting the scan does NOT discover these, so the "scan covers it" claim can't overstate its reach.

The scan is scoped to the PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / stand-alone retry buttons with no right sibling. Those are a separate, broader text-swap convergence effort, not this contract.

Changed files: packages/ui/src/relative-time.tsx, primitives/chat.tsx, daily-review-panel.tsx; apps/desktop/src/renderer/styles/*.css; apps/desktop/src/renderer/{error-boundary,settings/memory-settings-page,settings/open-gateway-settings-page}.tsx; 3 new contract tests.

Not included: scroll-area (locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts; #520 explicitly out of scope); other settings pages' state-swap buttons (about/bot-chat/data/general/permission — separate broader text-swap effort, not PR3's 5-file scope); screenshot / manual verification (value-changing, needs app run).

Verification

  • @maka/ui typecheck + @maka/desktop typecheck (main + renderer + storybook): pass
  • desktop build:main: pass
  • 3 new contracts: pass (tabular-nums 2/2, min-w-0 2/2, text-swap 4/4)
  • renderer-css-parse-contract + chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts)
  • Negative tests: text-swap value-lock (openBackupCandidate 4rem→1rem fails the exact-value pin); scan notPinned (openBackupCandidate anchor not matched fails the scan value-pin); scan computed-label boundary (a {label} child is NOT flagged as discovered). All restore green.
  • Not verified: screenshot / manual state-swap visual (value-changing; run npm run screenshots + trigger copy/save/token-count/归档 state swaps before merge).

User-facing impact

None beyond reduced layout jitter: digits stay aligned as they change, truncated text actually truncates, state-swap buttons stop pushing right-hand siblings. No CHANGELOG / docs / breaking changes / migrations.

Reviewer notes

  • text-swap contract reached its stable three-layer form over several review rounds; the commit history shows the progression (whitelist-only → +scoped discovery → +notPinned value-pin → +computed-label boundary). The final contract is what's described above; the intermediate "coarse count guard" framing in early commit messages is superseded — the contract is now scoped discovery + exact value pin + chat variant literal assertion, with a computed-label boundary test.
  • min-w-0 contract heuristic scan: three exceptions (grid-minmax parents + one dead-ellipsis), each commented in the contract.
  • text-swapmin-w-[Nrem] values are estimates sized to the widest state (偏宽防抖). Screenshot verification may want to tune them.

…PR3)
Audit found 12 numeric surfaces missing tabular-nums (self-refreshing timestamps via RelativeTime, usage stats table, metric cards, health/skill/search/provider/plan counts). Add tabular-nums to each, and lock all 31 known numeric surfaces plus RelativeTime with a presence contract so digit columns stay aligned and numbers can't jitter layout as they change.
Part of #520 PR3 (anti-layout-shift contracts).
…ontract (#520 PR3)
Audit found 12 flex/grid truncation children missing min-width:0 (daily-review session/archive/preview, plan template/run, model table id, search result title/meta, theme label, skill id, settingsNavItem strong). Without min-width:0 the default min-width:auto stops ellipsis from firing and text overflows the layout. Add min-width:0 to each, and lock every ellipsis+nowrap rule with a presence contract (grid-minmax + dead-ellipsis exceptions) so the historic truncation-bug source can't regress.
Part of #520 PR3 (anti-layout-shift contracts).
…PR3)
State-swap buttons/labels (复制↔已复制, 保存↔保存中…, token counts 9→100) shrink on state change and push right-hand siblings in left-aligned multi-button rows (settingsActionRow, maka-error-actions, maka-daily-review-actions) and summary chips. Add min-w-[Nrem] sized to the widest state on 19 surfaces, and lock each known surface file with a min-w-[Nrem] count contract so a refactor can't silently drop the width locks.
Part of #520 PR3 (anti-layout-shift contracts).
…act (#520 PR3)
- restore the missing } on the memory 复制上下文 button's disabled prop that broke renderer TSX parsing (caught by @maka/desktop typecheck, which build:main skips because tsconfig.main.json doesn't compile src/renderer/).
- replace the coarse file-count text-swap contract with a per-element pin: each state-swap button is located by its onClick handler and its className is asserted to keep min-w-[Nrem]; chat summary-chip / stream-count variant locks are pinned by their literal declaration substrings. Dropping a lock from a real button fails; moving a lock to an unrelated element also fails (verified by a negative test that flips 保存's 3.5rem to 3.4rem and sees the contract fail).
Part of #520 PR3.
- open-gateway 复制地址 (复制中…↔复制地址): min-w-[4rem] + contract pin. Guards the half-width ellipsis case where 复制中… can be narrower than 复制地址.
- memory 复制路径 (复制中…↔复制路径): same 4-CJK-but-font-dependent case, min-w-[4rem] + contract pin (同类主动补).
- daily-review 粘到输入框 (追加中…↔粘到输入框, 4↔5 chars): min-w-[5rem] + contract pin. This was a real miscount in the original audit — treated as 4↔4 equal, but 粘到输入框 is 5 chars so it grows and pushes 保存.
- open-gateway 生成 token / 清空 token: confirmed fixed text (no state swap), no min-w needed.
Part of #520 PR3.
#520 PR3)
Upgrade text-swap contract from whitelist-only to two-layer: (1) heuristic scan (DISCOVERY) — scan PR3 scope files for every <Button>/<UiButton> whose children contain a string-ternary (? 'A' : 'B', the state-swap signal) and require min-w-[Nrem]; catches buttons the whitelist hasn't listed, so 'did we forget one in these files?' is a question the test answers, not one a reviewer keeps answering. (2) whitelist per-element pin (VALUE LOCK) — each known state-swap button keeps a specific min-w-[Nrem] sized to its widest state, located by its onClick handler; stops a refactor from shrinking a real lock.
Scan is scoped to PR3 files, not the whole renderer — ? 'A' : 'B' is only the state-swap signal; it can't tell whether the button sits in a multi-element row where width change pushes siblings (the real bug needs layout context, unlike min-w-0's mechanical ellipsis+nowrap signal). A repo-wide scan flags ~77 buttons, most toggles / accordion headers / stand-alone retry buttons with no right-hand sibling to push — those are a separate broader text-swap convergence effort, not this contract.
Lock 8 surfaces the scan discovered in PR3 scope: memory settingsActionRow 重新载入; memory backup-candidate row 打开/恢复/复制引用; memory instruction-file row 打开/创建; memory entry actions 复制引用/归档切换. Each gets min-w-[4rem] + a TEXT_SWAP_BUTTONS entry. Verified: scan fails closed (8 before, 0 after).
Part of #520 PR3.
…520 PR3)
- onStatusChange (归档切换): min-w-[4rem] → [5rem]. statusActionLabel widest state is 归档到草稿/恢复到草稿 (5 CJK, draftDirty), not the 4-char 归档/恢复 idle state — 4rem could still jitter in draftDirty. Whitelist minW updated to 5rem.
- scan discovery test: add a second failure mode — a state-swap button with SOME min-w-[Nrem] but no TEXT_SWAP_BUTTONS value pin (and not in EXCEPTIONS) now fails. Without this, a too-small min-w-[1rem] on a scanned button bypassed the value lock (scan only checked has-any min-w; the whitelist only pinned hand-listed anchors; a new button with min-w-[1rem] and no whitelist entry passed both). Now the scan requires every discovered button to be value-pinned (or explicitly excepted).
Verified: P2.1 negative (openBackupCandidate 4rem→1rem) fails the value lock; P2.2 negative (openBackupCandidate anchor not matched) fails the scan notPinned assertion. Both restore green.
Part of #520 PR3.
#520 PR3)
Address review: the 'no reviewer needed' claim was over-full. The scan only discovers INLINE string ternaries (? 'A' : 'B' in children); computed-label buttons (children is a variable holding a ternary result, e.g. {copyLabel}/{statusActionLabel}) are NOT discovered and must be hand-pinned.
- file header: replace the over-full 'did we forget one? the test answers, not a reviewer' with the accurate boundary — inline-ternary omissions in the PR3 scope files are auto-caught; computed-label omissions are still on the whitelist author.
- add a COMPUTED-LABEL note above TEXT_SWAP_BUTTONS listing the known computed-label state-swap buttons (error-boundary copyLabel, memory onStatusChange statusActionLabel) + why the scan can't see them (and why onStatusChange's scan flag is an onClick-leak regex artifact, not a real discovery).
- add a boundary test asserting STRING_TERNARY_RE does NOT match a computed-label child ({label}) — so the 'scan covers it' claim can't overstate its reach.
- layer 2 note: update 'scan only checks has-any min-w' to reflect the notPinned value-pin assertion.
Part of #520 PR3.
@Astro-Han
Astro-Hanforce-pushed the refactor/anti-layout-shift branch from 4ccc2e9 to 3a33d2eCompareJuly 4, 2026 19:46
@Astro-Han
Astro-Han merged commit e6501e2 into mainJul 4, 2026
@Astro-Han
Astro-Han deleted the refactor/anti-layout-shift branch July 4, 2026 19:47
Astro-Han added a commit that referenced this pull request Jul 5, 2026
* test(desktop): relax exact className match for PR3 min-width additions
PR3 (#527) added min-w-[Nrem] utilities to the error-boundary copy button
and the daily-review copy/append/save buttons (text-swap width lock for
复制中…/已复制 feedback). Two contract tests asserted the semantic class
with an exact className="X" regex that required the closing quote right
after the class, so the extra utility class broke the match and the suites
have been red on main since PR3 merged.
Match each semantic class as a whole word in the class list
(className="[^"]*\bX\b[^"]*"), the same form the daily-review test
already used for its negative maka-button check. This keeps the intent
(button uses the semantic class, not a legacy maka-button) while
tolerating the utility classes PR3 deliberately added.
* refactor(ui): converge control heights onto --h-control-* scale (#520 PR4 item 15)
The sidebar / 会话 / 设置 control heights had drifted onto off-ruler bare
px (22 / 26 / 30 / 34 / 38) while the TSX side used the 4px spacing ruler
via Tailwind h-N. The two scales never aligned, so a sidebar nav row at
34px next to a session row at 30px next to a settings nav at 38px read as
three different systems — the user's 碍眼.
Add a six-tier --h-control-* scale (xs/sm/md/lg/xl/2xl = 20/24/28/32/36/40)
on the 4px spacing ruler (var(--space-N)) so CSS var(--h-control-*) and
Tailwind h-N share one scale. Snap the off-ruler control heights to the
nearest tier and tokenize the on-ruler ones:
.maka-list-row 30 → 32 (--h-control-lg) 会话 row
.maka-search-modal-close 22 → 24 (--h-control-sm)
.maka-search-modal-clear 22 → 24 (--h-control-sm)
.settingsBackButton 38 → 36 (--h-control-xl)
.settingsNavItem 38 → 36 (--h-control-xl)
.settingsSelectTrigger 34 → 32 (--h-control-lg)
.settingsSelectMenuPopup [role=option] 32 → 32 (--h-control-lg)
.maka-model-switcher-trigger 22 → 24 (--h-control-sm)
.maka-chat-jump-bottom 26 → 28 (--h-control-md)
.maka-palette-input-wrap 32 → 32 (--h-control-lg)
.maka-first-run-checklist-error-action 24 → 24 (--h-control-sm)
.maka-first-run-checklist-row > button 38 → 36 (--h-control-xl)
.maka-composer-send-button 30 → 32 (--h-control-lg)
.maka-composer-workspace-picker 24 → 24 (--h-control-sm)
On the TSX side, replace arbitrary h-[Npx] / min-h-[Npx] / max-h-[Npx] with
the Tailwind ruler scale (min-h-8, min-h-11, h-7, max-h-55, max-h-80,
min-h-21, etc.) so TSX and CSS share the 4px ruler. The #332 chat-marker
and chat-preview cascade contracts deliberately pin min-h-[28px] and
max-h-[180px] as arbitrary literals (a "literalize vehicle" immune to
scale re-tuning); those two stay arbitrary and are whitelisted here, with
#332 retaining governance of them.
control-height-converge-contract.test.ts pins the six tokens exactly-once
to their spacing tier, maps a curated set of control selectors to their
expected tier (the radius-contract SELECTOR_TIER pattern — height has no
single anchor the way border-radius does, so the contract scopes to
control selectors, not every height), and bans bare-numeric arbitrary
h-[Npx] / min-h-[Npx] / max-h-[Npx] in TSX with a small whitelist for the
decorator dots, the count badge, the off-ruler 110px scroll cap, and the
two #332-pinned literals. App-chrome bars (--h-titlebar/--h-toolbar/
--h-composer-min/--h-list-header) and content min/max heights stay bare —
they are structure / content, not controls.
Verification: 1941/1941 desktop tests pass. Screenshots vs main:
first-run 0% diff (untouched), turn-narrative 0.6%, settings-appearance
1.6-1.8% — all consistent with the deliberate 2px control-height snap.
* refactor(ui): converge border-width onto --border-width-* tokens (#520 PR4 item 14)
Border COLOR was already tokenized (--border / --border-strong); the WIDTH
was bare px in every `border: 1px solid var(--border)` shorthand plus a
handful of `border-left: 3px solid …` status strips. Add three semantic
weights on a small scale:
--border-width-hairline 1px the universal divider (210+ sites)
--border-width-thick 2px a heavier divider / selected outline
--border-width-accent 3px a status / decorative strip (toast
variant color bars, avatar rings)
Snap the rare 1.5px hairlines to hairline (3 sites, -0.5px) and the one 4px
avatar ring to accent (1 site, -1px). Border-STYLE (solid / dashed) stays a
literal keyword — it is a named value, not a magic number, so tokenizing it
adds indirection with no governance benefit. CSS-triangle carets
(`border-width: 4px 0 4px 5px`) are multi-value geometry, not border
strokes, so the contract only flags a SINGLE bare-px width.
Replace 221 `border:` / `border-{side}:` shorthand widths across 25 CSS
files with `var(--border-width-*)` (perl with `(?<![-\w])` lookbehind so the
`--border-width-hairline: 1px` token definition is not turned into a
self-reference).
border-width-converge-contract.test.ts pins the three tokens exactly-once;
scans `border:` / `border-{side}:` shorthand for bare px (stripping
var()/calc()/oklch() first so color px is not mistaken for the width);
flags a SINGLE bare-px `border-width:` longhand while sparing multi-value
triangle geometry; enforces `border-style:` keyword literals; and bans
arbitrary `border-[Npx]` / `border-{side}-[Npx]` in TSX (Tailwind `border`
defaults to 1px = hairline, so TSX and CSS agree on the value).
Update four existing contracts that hardcoded `border: 1px solid …` to
match the new token form (artifact-pane, command-palette, permissions-
unified-card, startup-loading-shell).
Verification: 1950/1950 desktop tests pass. Screenshots vs main: turn-
narrative / settings-appearance 0% incremental over item 15, first-run
+375px (0.00009%, the onboarding 1.5→1 hairline). No regression.
* refactor(ui): systematize concentric radius nesting rule (#520 PR4 item 12)
Roadmap §1.3 / P-RADIUS: when a rounded surface sits inside another rounded
surface with padding between them, the inner radius = outer radius −
padding so the two curves share a center and read as one machined shell.
Document the convention on the radius tokens in maka-tokens.css with the
two forms it takes:
1. outer − padding lands on a tier → pick that tier directly (e.g. an
8px surface card inside a 12px modal with 4px padding: 12 − 4 = 8).
The radius-converge contract SELECTOR_TIER already pins this.
2. outer − padding does NOT land on a tier → use
`calc(var(--radius-*) - Npx)` (the radius-converge calc allowlist
permits only this shrink form). Two sites use it today — an input
inside a 12px modal shell with an 8px inset (12 − 8 = 4px, not a
tier): .maka-search-modal-input-row and .maka-palette-input-wrap.
Audit conclusion: the settings-modal inner cards use the surface/control
tier inside the modal shell (form 1, governed by SELECTOR_TIER), and the
seven settings inner surfaces that keep --radius-modal are peer sub-modals
(login modal, scan modal, select popup), not nested cards — so no nesting
violations and no CSS value changes are needed.
radius-nesting-contract.test.ts pins the two calc-nested input sites so a
later cleanup can't drop the calc and revert to a hardcoded --radius-control
(6px) that would read too round against the 12px shell corners, and
restates the shrink-only rule (no calc addition) for the nesting sites.
Verification: 1952/1952 desktop tests pass. No CSS values changed → no
visual regression (screenshots unchanged from item 14).
* refactor(ui): converge responsive breakpoints + chat content measure (#520 PR4 item 16)
Two responsive seams had drifted:
1. @media breakpoints. CSS @media queries evaluate at parse time, before
custom properties resolve, so `@media (max-width: var(--bp))` is INVALID
— breakpoints cannot be tokenized with var(). Instead a contract
whitelists the eight max/min-width pixel values the app actually uses
(620 / 720 / 760 / 820 / 900 / 980 / 990 / 1100) and bans any other bare
`@media (max-width: Npx)`. A new breakpoint must be added to the
whitelist, which forces a conscious decision instead of a silent drift.
prefers-reduced-motion / prefers-color-scheme are not width breakpoints
and stay out of scope.
2. The chat content measure (--maka-chat-measure: 680px). This IS a regular
property value, so it can be tokenized. It was a LOCAL token on
.mainColumn with a `680px` fallback at every call site; promote it to
:root in maka-tokens.css so it is canonical, drop the redundant
`, 680px` fallbacks at six call sites, and adopt it in the onboarding
hero (was a bare `width: min(680px, 100%)`). The chat column, tool
output, composer, and onboarding hero now share one measure with no
fallback. A 680px HEIGHT cap on the settings form modal is a different
semantic and stays bare — the contract scopes the ban to width.
responsive-breakpoint-contract.test.ts whitelists the breakpoints, pins
--maka-chat-measure to 680px exactly-once in maka-tokens.css, bans a local
re-declaration in styles/, and bans a bare 680px in width / max-width /
min-width declarations.
Update project-context-badge.test.ts to match the no-fallback
composer-workspace-row width formula.
Verification: 1959/1959 desktop tests pass. No CSS values changed (the
chat-measure move is functionally a no-op — every site already resolved to
680px via the local def or the fallback) → first-run screenshot unchanged
from item 14 (375px / 0.00009%, the onboarding 1.5→1 hairline).
* refactor(ui): converge box-shadow color onto foreground-derived (P-SHADOW) (#520 PR4 item 13)
P-SHADOW (roadmap §1.2 / maka-tokens.css): blur layers in box-shadow must
derive from --foreground, not pure black — a pure-black rgba()/oklch()
shadow on maka's warm shell reads as a dirty smudge, while a foreground-
derived shadow shares one light source with the border ring and ink.
Thirteen bare pure-black box-shadow usages (rgba(0,0,0,A) and
oklch(0 0 0 / A)) survived across settings, plan-reminders, and
reference-shell. Swap each pure-black color for
oklch(from var(--foreground) l c h / A), KEEPING the shadow geometry
(offset / blur / spread) intact so only the color warms — the safe
P-SHADOW win. The dark-mode shadow recipe overrides in maka-tokens.css
(--shadow-medium / --shadow-modal for dark mode) intentionally keep
pure-black oklch(0 0 0 / 0.5|0.6) — on a dark canvas a pure-black shadow is
correct ("dark mode shadows collapse to a single ring; modal keeps one
deep drop") — those are token definitions, not box-shadow usages, so they
are out of scope.
The broader recipe-converge (mapping the ~20 already-foreground-derived
elevation shadows onto var(--shadow-*) recipes) is deferred: the recipes
add a 1px border ring + standardize the blur, which changes the visual
weight of compliant surfaces — a design-review change, separate from the
P-SHADOW color fix here. The themed colored skill-card shadow
(oklch(0.2 0.02 224 / 0.4)) and the hard 1px blur-0 hover edge
(0 1px 0 0 ...) are not pure-black and are not flagged.
box-shadow-converge-contract.test.ts bans a pure-black color
(oklch(0 0 0 / A) / rgba(0,0,0,A) / #000 / black with non-zero alpha) in
any box-shadow declaration (multi-line values captured), and confirms the
shadow recipes are defined. It does not scan --shadow-*: token
definitions, so the intentional dark-mode pure-black recipes are not
flagged.
Update settings-form-a11y-contract.test.ts: the three connection / auth-
contract / model-table near-flat shadow assertions pinned the pure-black
rgba form as the "reference implementation" shadow; P-SHADOW supersedes
that — the geometry (0 1px 3px) and alpha (0.03) are unchanged, only the
color form moves to foreground-derived.
Verification: 1965/1965 desktop tests pass. Screenshots vs main: 0%
incremental over item 14/15/16 (the color warm-up is within the 2% fuzz on
light theme — near-black and pure-black at low alpha are visually
identical there; the fix matters on warm/dark shells where pure-black
reads as a smudge).
* fix(ui): close --h-control-* reference chain + tighten mapped-selector tier check (#520 PR4 review)
P1: --h-control-md / --h-control-xl referenced --space-7 / --space-9, which
are NOT in maka's discrete spacing scale (it defines --space-0/1/2/3/4/5/6/
8/10/12/16 — 7 and 9 are skipped). A var() with no fallback resolving to an
undefined custom prop is invalid at computed-value time, so the jump-bottom
FAB (md=28px) and settings nav/back rows (xl=36px) collapsed to width/height
auto / min-height 0 — masked by their padding/content, but fragile and wrong.
Fix with calc(var(--spacing) * 7) / calc(var(--spacing) * 9) (the --spacing
base step IS defined, = 4px), so 28/36px resolve explicitly without expanding
the general --space-* scale (which would invite p-7 / gap-7 drift).
Add assertCustomPropRefsDefined to css-test-helpers: walk the var(--xxx)
reference chain in a token's value and assert every referenced custom prop is
defined somewhere in the tokens file (token :root, @theme inline, or bridge
alias). A pin-only check that --h-control-md is declared with var(--space-7)
passes while the token is broken — this helper catches the bug class. Add a
contract test that runs it on every --h-control-* tier, plus a negative case
that feeds a --space-7 reference and asserts it throws.
P3: isAllowedControlHeight previously let a MAPPED selector pass with a
direct var(--space-N), calc(var(--spacing) * N), or a layout-chrome token
(--h-titlebar / --maka-sidebar-topbar-button-size / …) instead of its
expected --h-control-* tier — bypassing the semantic scale the mapping table
exists to enforce. Tighten: a mapped selector's height / min-height / width
must be exactly var(--h-control-<expected>) or a neutral literal (0 / auto /
100%). Unmapped controls are added to CONTROL_HEIGHT rather than allowed to
slip via a space token. No current mapped selector uses a bypass value, so
nothing real is flagged; the negative cases now assert the bypass forms fail.
Verification: 1967/1967 desktop tests pass (was 1965; +1 ref-chain test, +1
ref-chain negative case). Screenshots vs main: settings-appearance diff drops
0.0159 → 0.0136 (the settings nav rows restore to explicit 36px instead of
the padding-masked auto height); turn-narrative unchanged (the FAB collapse
was already masked by its icon + padding). typecheck clean.
* fix(ui): ban multi-value bare-px border-width, allowlist triangle carets by selector (#520 PR4 review)
P2: the border-width contract only flagged a SINGLE bare-px value in a
border-width: longhand (SINGLE_BARE_PX_RE). A multi-value form like
border-width: 1px 2px (a non-uniform stroke, NOT a triangle) was spared — a
false negative a future drift could hide behind. The triangle-carets-are-
multi-value heuristic spared ALL multi-value, which is the hole.
Fix: flag ANY bare px in a border-width: longhand (single OR multi-value),
and allowlist the three known triangle/caret SELECTORS (.maka-turn-thinking
summary::before, .maka-bubble-assistant …checkbox:checked::after,
.maka-permission-raw > summary::before) whose multi-value is geometry, not a
stroke. Selector tracking walks the line before { to keep the current
selector; a new caret with a new value on a new selector is flagged until
explicitly added to the allowlist (correct — review new carets consciously).
Add negative cases: border-width: 1px 2px on a non-allowlisted selector must
fail; the three allowlisted caret selectors must pass; single bare-px still
fails; token / 0 still pass.
Verification: 1967/1967 desktop tests pass. The real renderer CSS scan stays
clean (the only three border-width: longhands are the allowlisted carets).
* fix(test): require every mapped control-height prop to appear, not any one (#520 PR4 review)
checkSelectorTier used a single checkedAny flag — if a selector was mapped
with multiple REQUIRED props (e.g. .maka-chat-jump-bottom [width, height],
.settingsNavItem [height, min-height]), the contract only checked that AT
LEAST ONE appeared. Deleting the width declaration but keeping height passed
silently, dropping the square-control width requirement.
Replace checkedAny with seenProps: Set<string>, accumulated across all
matched blocks (base + @media / :state variants), and require every prop in
check.props to appear at least once — a missing required prop is flagged as
'is missing required <prop> declaration'. Each declaration that does appear
is still value-checked against the expected tier. No current mapped selector
is missing a prop, so nothing real is flagged.
Add a negative case: a .maka-chat-jump-bottom fixture with only height must
flag the missing width; a .settingsNavItem fixture missing height OR
min-height must flag; a complete fixture passes.
Verification: 1968/1968 desktop tests pass.
* docs(test): fix stale SINGLE-only border-width comment to match selector-allowlist impl (#520 PR4 review)
The previous review fix changed border-width longhand scanning from 'only
flag a SINGLE bare-px value' to 'flag ANY bare px (single OR multi-value)
unless the selector is an allowlisted triangle/caret', but the top summary
paragraph still described the old single-only rule — comment, impl, and
negative cases disagreed.
Update the top summary to match: triangle carets are multi-value geometry
allowed only on allowlisted caret selectors (TRIANGLE_CARET_SELECTORS), and
any bare px — single OR multi-value — is flagged elsewhere. The item-2
invariant and inline comment already said this; only the summary was stale.
Verification: 1968/1968 desktop tests pass; non-allowlisted border-width:
1px 2px continues to fail.
* fix(test): make assertCustomPropRefsDefined a recursive DFS with cycle detection (#520 PR4 review)
The helper only checked the FIRST hop: it collected var(--xxx) refs in the
target prop's value and checked each was defined, but did not recurse into a
ref's own value. A chain like --h-control-xs → --space-5 → --missing
(--missing undefined two hops out) passed because --space-5 was defined and
the helper stopped there. It also had no cycle detection (--a → --b → --a
would infinite-loop).
Rewrite as a small DFS from the target prop through every var() ref,
maintaining visiting/visited sets: an undefined ref at any depth throws
'references undefined <name> (via <path>)', and a back-edge to a node on the
current path throws 'circular custom-prop reference: a → b → a'. Each node
must still be declared exactly once. Function name kept; the doc comment now
says 'recursively, with cycle detection' and means it.
Add negative cases: --h-control-xs → --space-5 → --missing must fail (the
2-hop case the old helper missed); --a → --b → --a must fail (cycle). The
direct --space-7 case and the valid closed chain still behave as before.
Verification: 1968/1968 desktop tests pass; typecheck clean. The real
--h-control-* chain (xs/sm/lg/2xl → --space-N → calc(var(--spacing) * N) →
--spacing: 4px; md/xl → calc(var(--spacing) * N) → --spacing) still passes.
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

@Astro-Han