feat(desktop): every menu and popover draws from one set of menu tokens

Merged
DA
7d ago
+171-70

What

Menus and popovers were drawn several ways. They now share one set of tokens, in desktop.css's :root:

TokenValuePicked because
ground--surface-popoveralready "what floats over the window"
edge1px solid var(--hairline-strong)base.css already names it "the edge of what floats over the window"
--menu-inset4px (existing token)the menu padding already named
--menu-row-height32pxthe searchable menu, every picker, and the command palette use it; only ActionMenu used 28
--menu-row-radius6pxevery menu row already used it
--menu-glyph16pxthe pickers and the palette use 16; only ActionMenu used 15
--menu-radiusrow radius + inset = 10pxconcentric, so a highlighted row follows the menu's curve
--menu-shadow0 12px 32px var(--shadow)the shadow drawn with the 10px corner

Surfaces drawing the set: ActionMenu (.plan-menu, its submenus, the account menu), the searchable menu and every picker built on it (.plan-compose-menu, .plan-compose-submenu), the crumb tree, the find bar, the tooltip card, and the agent context popover. The command palette's rows use the row tokens too.

--surface-overlay is gone. Its four uses, plus Tailwind's --color-background, now read --surface-popover.

What changes on screen:

  • ActionMenu rows grow from 28 to 32px, and their glyphs from 15 to 16px.
  • The pickers and the context popover go from a 12px corner to 10px, from a 0 16px 40px shadow to 0 12px 32px, and from --hairline to --hairline-strong.
  • The searchable menu's list, its foot, and its submenu are padded 4px instead of 6px.
  • In dark mode, everything that read --surface-overlay (those surfaces, plus the summary card) goes from #111214 to the sidebar's #111114. Light mode was already #fff for both.

Choices

  • The tokens live in desktop.css, not base.css. --surface-popover, --menu-inset, the --control-* set, and the spacing scale are all already there, and docs/DESIGN.md puts the window's grounds and geometry in that file. Splitting the set across two files would be worse.
  • The command palette keeps its 14px corner and its 0 24px 64px deep shadow. It is a centred modal over the window, like the confirm dialog and the attachment viewer, which share those values. It is not a menu hung from a control. Its rows are a menu's rows, so they take the row tokens.
  • The plan summary card keeps its 12px corner, shadow, and edge. Its rows are the docked summary's, with an 8px --control-radius corner inside a 4px inset, so the card's corner is concentric only at 12px. It is a panel of those rows, not a menu, so it takes only the --surface-popover ground.
  • Not converted: the plain .tooltip (a 7px one-line hint with its own fill), toasts, the call card, the voice caption, and the confirm and attachment dialogs. None of them is menu-shaped.

The audit's claims, checked against the code

  • Corners of 10, 12, and 14px, two shadows among the menus, rows of 28 and 32, glyphs of 15 and 16: all correct.
  • "--surface-popover and --surface-overlay are the same value in both themes": almost. Light is #fff for both, but dark is #111114 against #111214.
  • "Only one menu animates in": wrong, the other way round. The pickers, the submenu, the crumb tree, the find bar, the context popover, the summary card, and the palette all animate in. ActionMenu is the one that doesn't. Either way, animation is left to the exit-motion PR that stacks on this one, and nothing here touches it.

Tests

  • design/check-design-contract.mjs now refuses a menu or popover class that sets its own border-radius, box-shadow, or background instead of the menu tokens. It also refuses a menu row (.plan-menu-item, .plan-compose-menu-row, .command-palette-row) whose height or border-radius is not the row token. On main's stylesheets it fails with 23 findings; on this branch it passes. docs/DESIGN.md gains one paragraph describing the set and the check.
  • searchable-menu.test.ts reads computed sizes from the real desktop.css. jsdom leaves var(--…) unresolved, so those reads now resolve a token through :root. The assertions are unchanged: rows are one height, the end slot and its check are 16px, and every icon is square with a size from the sheet.
  • ./scripts/check.sh exits 0 on the branch rebased onto main.

Screenshots

These come from the renderer's real markup: ActionMenu, a searchable picker with its Effort submenu open, and the context popover. The markup was mounted in jsdom, wrapped in the built styles.css, and rendered in headless Chrome on Linux, with the enter animation held still. Before is main's stylesheet; after is this branch's. The keycaps show as boxes because Linux Chrome has no font for ⌘ and ⇧.

Dark

BeforeAfter
before darkafter dark

Light

BeforeAfter
before lightafter light

Mac validation

CI could not verify the Mac UI: no CI job builds the Mac app. ./scripts/verify.sh was not run, because this is a Linux cloud VM; LUKE-159 is the manual Mac pass.

šŸ¤– Generated with Claude Code