fix(desktop): read the --luke-* roles where CSS still named retired tokens

Merged
DA
4d ago
+132-45

Summary

#3524 (and #3569 after it) deleted the desktop's compatibility token aliases. PRs that were already open then merged CSS that still reads them. Nothing failed, because a var() naming an undeclared property just makes its declaration invalid, so borders, gaps, padding and colours silently dropped.

  • Fix. Every one of the 41 reads on main when this PR opened, and 10 more that merged since, now reads the --luke-* role its alias pointed at, using #3524's deleted alias block (--space-N→--luke-space-N, --hairline(-strong)→--luke-color-border(-strong), --text-primary/secondary/tertiary→--luke-color-text/-muted/-subtle, --raised→--luke-color-fill-hover, --control-height/gap/radius→--luke-control-*, --switch-on→--luke-color-switch-on, --focus-ring→--luke-color-focus-ring) and #3569's (--focus-ring-width/offset→--luke-focus-ring-width/offset):
    • settings.css: the video avatar's picture choice (#3257, Settings › Experimental › Avatar), not a Diffs group.
    • tasks.css: the Pull request tab header's branch row, stack button and labels (#3268). These use the same replacements #3309 makes to the same lines.
    • tasks.css (merged from main after this PR opened): the inline review threads in Files changed (#3245), 10 reads more. This PR's own check caught them in CI, with --section-fill→--luke-color-surface and a local --focus-ring-offset: var(--focus-ring-inset) changed to the --luke-focus-ring-* override its neighbours use.
    • desktop.css: the offline mark's size and the sidebar toggle's clearance in the title bar (#3523), which the first regex missed.
    • breadcrumbs.css: this one read --focus-ring-width/-offset with literal fallbacks equal to today's values. It now reads the roles with no fallback, the way its sibling components do, so the guard can see it.
  • Guard. A new step in scripts/repository-checks.sh takes every tracked .css, .html, .ts and .tsx file under apps/ and packages/ (tests excluded). It treats a property as declared if it appears as a sheet's --name: or @property --name, or as a script's "--name" literal (setProperty, a style object's key, a constant). A --name-${…} template, such as ChartStyle's --color-${key}, declares the whole family. The step then fails on any var(--name) with no fallback that nothing declares, naming the file and line. The only name on the hand-kept list is --default-button-size, which comes from Excalidraw's own sheet. The step takes about 0.5s. Declarations are pooled across apps, so it catches a name that is gone everywhere, which is what a retired token is.
    • Why not Biome or stylelint: Biome's noUndeclaredCustomProperties is a nursery rule that needs Biome ≥ 2.5.11 (we pin 2.5.8). It runs the project scanner, follows CSS imports only, and cannot see properties set from TypeScript. Stylelint is not in the repo, and its plugin for this has the same blind spot.

Evidence

  • The guard on main's CSS, before the fix: fails with 41 reads (desktop.css ×4, settings.css ×19, tasks.css ×18), each named by file and line. After the fix: passes.
  • ./scripts/repository-checks.sh: passed. ./scripts/oxlint.sh: 0 warnings. pnpm knip: clean. biome check on the touched sheets: clean.
  • pnpm check:affected: ran out of memory in discarded-effect (a TypeScript-only check; this PR changes no TypeScript), so I ran the individual checks above instead.
  • Screenshots: see Screenshots below. They come from a throwaway esbuild harness with fakeAppBridge that renders the real components under the real built stylesheet, before and after, in dark and light. The harness is not committed.

Screenshots

Before, on main, and after, with the built desktop stylesheet. Dark first, then light.

Settings › Experimental › Avatar (picture choice). Before this fix, the faces have no gaps, the chosen face has no ring, the names are the wrong colour, and the stage has no inset or radius.

BeforeAfter
beforeafter
beforeafter

Pull request tab header (labels, branch row, stack). Before this fix, the label pills have no padding or gap, and the Stack button has no edge, gap or colour.

BeforeAfter
beforeafter
beforeafter

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run, or diff explained line by line). (CI)
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. The new repository check fails on main without the CSS fix.
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-raw-async-primitives)
  • No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via pnpm discarded-effect)
  • Test count equals the pre-PR count or the delta is listed. Unchanged: the guard is a repository check, not a test.
  • Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root AGENTS.md's "The permanent adaptors" with the reason it stays.
  • Every CLAUDE.md/AGENTS.md sentence naming a changed part is edited; root pair stays byte-identical.
  • PRIVACY.md unchanged.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI)
  • Barrel/door rule: no Node-reaching Effect module behind a barrel the renderer or a web function opens. (CI)

Notes

  • Blockers or follow-up verification: None. Open PRs #3245, #3294, #3305, #3309 and #3313 fix their own retired tokens. Once this lands, any of them that misses one will fail this check.

Made with Luke


Devin Review


Summary by cubic

Restores the desktop UI's borders, gaps, padding, and colors that silently dropped after #3524 and #3569 retired the CSS token aliases. All var() reads still naming a retired token now read the --luke-* role they pointed at, and a new repository check fails on any var(--name) with no fallback that nothing declares, so the regression can't repeat.

Bug Fixes

  • settings.css: the video avatar picture choice in Settings › Experimental › Avatar regains its spacing, borders, focus ring, and colors.
  • tasks.css: the Pull request tab's branch row, stack button, and labels regain their gaps, edges, and text colors, and the inline review threads in Files changed (#3245) regain their padding, borders, and surface fill — the new check caught those reads on the merge with main.
  • desktop.css and breadcrumbs.css: the offline mark's size, the sidebar toggle's title-bar clearance, and the breadcrumb focus ring are restored; breadcrumbs now read the roles with no fallback, like sibling components, so the check can validate it.

New check

  • scripts/repository-checks.sh fails when a var(--name) has no fallback and no tracked .css, .html, .ts, or .tsx file under apps/ and packages/ declares --name (tests excluded), naming the file and line.
  • Sheets declare by --name: or @property --name; scripts only by writing the name (a style string, a style object key, a ["--name"] =, or setProperty), so reads like getPropertyValue don't count.
  • A --name-${…} template declares the whole family, and declarations are pooled across apps so the check catches a token gone everywhere.
  • The only hand-listed exception is --default-button-size, which Excalidraw's own stylesheet sets; the check adds about 0.5s to the run.