Uh oh!
There was an error while loading. Please reload this page.
refactor(ui): converge anti-layout-shift specs — tabular-nums, min-w-0, text-swap (#520 PR3) - #527
Merged
Merged
Conversation
…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-Hanforce-pushed
the
refactor/anti-layout-shift
branch
from
July 4, 2026 19:46
4ccc2e9 to
3a33d2eCompare
This was referenced Jul 5, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 defaultmin-width: autostops ellipsis from firing. Contract: heuristic scan of everyellipsis + nowraprule (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:
<Button>/<UiButton>whose children contain a string-ternary (? 'A' : 'B', the state-swap signal); each must keepmin-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.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.{copyLabel}/{statusActionLabel}) have no inline ternary for the scan to find, so they MUST be hand-pinned inTEXT_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/uitypecheck +@maka/desktoptypecheck (main + renderer + storybook): passbuild:main: passrenderer-css-parse-contract+chat-primitive-cascade-contract: pass (CSS still parseable, chat variant changes don't break chat contracts){label}child is NOT flagged as discovered). All restore green.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
min-w-0contract 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.