feat(desktop): every menu and popover draws from one set of menu tokens
What
Menus and popovers were drawn several ways. They now share one set of tokens, in desktop.css's :root:
| Token | Value | Picked because |
|---|---|---|
| ground | --surface-popover | already "what floats over the window" |
| edge | 1px solid var(--hairline-strong) | base.css already names it "the edge of what floats over the window" |
--menu-inset | 4px (existing token) | the menu padding already named |
--menu-row-height | 32px | the searchable menu, every picker, and the command palette use it; only ActionMenu used 28 |
--menu-row-radius | 6px | every menu row already used it |
--menu-glyph | 16px | the pickers and the palette use 16; only ActionMenu used 15 |
--menu-radius | row radius + inset = 10px | concentric, so a highlighted row follows the menu's curve |
--menu-shadow | 0 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 40pxshadow to0 12px 32px, and from--hairlineto--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#111214to the sidebar's#111114. Light mode was already#ffffor both.
Choices
- The tokens live in
desktop.css, notbase.css.--surface-popover,--menu-inset, the--control-*set, and the spacing scale are all already there, anddocs/DESIGN.mdputs 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 64pxdeep 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-radiuscorner 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-popoverground. - 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-popoverand--surface-overlayare the same value in both themes": almost. Light is#ffffor both, but dark is#111114against#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.mjsnow refuses a menu or popover class that sets its ownborder-radius,box-shadow, orbackgroundinstead of the menu tokens. It also refuses a menu row (.plan-menu-item,.plan-compose-menu-row,.command-palette-row) whoseheightorborder-radiusis not the row token. Onmain's stylesheets it fails with 23 findings; on this branch it passes.docs/DESIGN.mdgains one paragraph describing the set and the check.searchable-menu.test.tsreads computed sizes from the realdesktop.css. jsdom leavesvar(--ā¦)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.shexits 0 on the branch rebased ontomain.
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
| Before | After |
|---|---|
![]() | ![]() |
Light
| Before | After |
|---|---|
![]() | ![]() |
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



