refactor(desktop): stop app CSS restyling the shared components' insides

Merged
CH
11d ago
+146-150

Summary

Phase 3, P8: the desktop's own CSS stops reaching into @luke/ui's parts. I re-scanned rg '\.luke-' apps/desktop/src/renderer/styles apps/web/src: the web has no hits. Every desktop rule that restyled a shared component's inside is now a component option, points at the app's own element, or is deleted.

Into the components (approved by the orchestrator):

  • SplitButton answers a new --luke-fold: bare value: its main part's glyph alone, at the control's corner, with no chevron. The plan toolbar at ≤320px sets it instead of hiding .luke-split-button > .luke-button:last-child and re-rounding the main part. folded still folds the words.
  • SearchField answers --luke-fold: folded: its tally is read out to screen readers but not drawn. The find bar at ≤300px sets it instead of hiding .luke-search-field-tally.
  • InlineEdit's existing input look now fills the label's line and overhangs it (−3px above and below, minus its inset and border at the start). Before, Settings set those negative margins on .luke-inline-edit. No new API.
  • packages/ui/README.md documents the --luke-fold values. The catalog shows SplitButton bare and SearchField folded.

Pointed at the app's own element instead of a shared part:

  • To-dos dock: the rise animation now targets the dock's own body class (.todos-dock-body) instead of .luke-disclosure-body. fold.tsx is untouched (it's P4's).
  • Hover cards: .card-code, .actor-words:has(> .card-when), and a card-corner-mark class on the pull request mark replace the .luke-hover-card-note/-corner descendant selectors.
  • Chords: the chord in the tooltip and the chord on a filled button (dialog Send/Create issue, home New task) take a class on the app's Shortcut (tooltip-chord, button-chord). Before, they were selected through .luke-tooltip, .luke-dialog-actions .luke-button and .home .luke-button.
  • Capture runs: the tooltip, the menu and palette, and the toast stack each had a "hold motion still" rule. They are now one body-level line in base.css's capture rule.

Deleted:

  • .sidebar-task > .luke-inline-edit and .luke-hover-card .integration-tool-list were dead: no element matches them anymore.
  • The menu's chevron and check were restated at menu size only because the app's global glyph rules (.settings-chevron, .credential-check) beat the shared layer. Those global rules now leave .luke-menu-item-end alone, as they already left chips and button faces, so the menu draws its own.
  • .luke-combobox-option-end .credential-check, added by a sibling PR to undo the same leak in a picker's option. The global .credential-check rule now leaves .luke-combobox-option-end alone too, so the combobox draws its own check (16px, text colour when chosen). It is pixel-identical to the sibling's fix; screenshots below.
  • The negative margins on the Issues column head's + button.
  • overflow: visible on the automations empty state.

Why exclusions rather than scoping the glyph rules: .settings-chevron, .credential-check and .link-icon are @luke/ui glyph classes that the app styles globally. Scoping them to the app's own containers, or moving them into the base layer, would also resize the chevrons in several buttons, nav items and list rows that rely on the app size today. So the rules keep their :where(:not(…)) list of the shared slots that draw their own glyph, which has zero specificity and restyles nothing of those slots.

Kept, as placement or app content (not internals): flex, width or grid placement on a component's root (.luke-text, .luke-badge, .luke-empty-state, .luke-search-field, .luke-text-field, .luke-hover-card-head, .tab-button > .luke-inline-edit width). Also kept:

  • the drag-region opt-outs and menu layers;
  • :has() conditions on the app's own containers;
  • the app's own content in a slot (a repository picture in a menu glyph, and --state-glyph-size on hover-card slots, which sizes the app's own StateGlyph and nothing of the card's);
  • base.css's .luke-face rules and the face-size rules. These are the motion wiring that packages/brand's generator writes into the renderer (--face-motion, --voice-*), so they stay with the generated face-motion.css.

Visible changes

  1. Issues column head 24 → 30px. The + is a full control again instead of being squeezed into a 24px head with negative margins. Every head, placeholder ones included, now stands the control's height, so nothing jumps when the board loads. The + glyph still ends on the cards' time edge.
DarkLight
Before
After
  1. The shared Menu's check, 12 → 16px. It is now the menu's own glyph size, as the web and the catalog already draw it, instead of the size the app's settings-row rule leaked in.
DarkLight
Before
After

Nothing else should change visibly. The Start split button at 600/400/300px, the find bar's tally at 420/280px, and the Settings rename field are pixel-identical between main and this branch:

Start: drawn, folded, bare
Find bar: tally drawn, then folded

  1. A picker's submenu chevron, 11 → 16px. The global .settings-chevron rule leaked into a combobox option's end slot as it did into the menu's. It now leaves .luke-combobox-option-end alone, so the chevron is the picker's own glyph size and matches the check beside it.
DarkLight
Before
After

The picker's check after this change (identical to main):

DarkLight

Evidence

  • pnpm check:affected: green (typecheck, lint, knip, discarded-effect, tests). One run hit a failure in list-row.test.ts's swipe test (from #3653, already on main). It passes alone 3/3 and on rerun; this PR touches no list-row code.
  • Screenshots: Linux, headless Chrome, synthetic data. Markup from the real components, rendered in jsdom by a throwaway test that is not committed, drawn with the desktop renderer's built styles.css from main (before) and from this branch (after).
  • No new test: the PR changes only styling, and the rules forbid CSS/class assertions. The --luke-fold states are shown in the catalog instead.
  • Not verified on a Mac.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (styling only)
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.runPromise/runSync/runFork added.
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch.
  • No Effect, Stream, or Layer dropped.
  • Test count unchanged.
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names the changed part.
  • PRIVACY.md unchanged.
  • No dependency changes.
  • No barrel changes.

Notes

  • Blockers or follow-up verification: not verified on a Mac.

🤖 Generated with Claude Code