refactor(desktop): migrate voice key-binding editor onto the design system
D28 of the design-system migration (Desktop stack, base feature/desktop-d27-voice-settings, #3406).
What
Settings › Voice's Keys group (apps/desktop/src/renderer/settings/voice-keys.tsx) moves off the old settings-group and settings-copy markup onto @luke/ui:
- The group is a
SettingsSectiontitled "Keys". - Each key (Talk to Luke, Stop Luke) is a
SettingsRow, drawn throughMenuRow's existing shared-row form (row={…}), like the API-key and integration rows.- The title and its Modified and attention marks are the row's label.
- The key's detail is the description. While recording, the "Hold ⌃, ⌥, or ⌘…" hint appears on a second line under it.
- A refused chord (one that can't be a talk key, or one the window already uses) shows in the row's error line.
MenuRow'srowgains anerrorslot. This changes only the desktop adapter: no@luke/uiAPI changes.- The chord field keeps its own recorder button. No barrel control records a keystroke or shows a ring while recording, and the plan says to ask before adding a variant. Its colours, height, corner radius and type now use
luke-*tokens instead of the legacy aliases. - Removed:
.shortcut-name .settings-attention, which only this row used. It becomes.luke-settings-row-label > .settings-attention, so the attention mark keeps its spacing inside the shared label..shortcut-name,.shortcut-stateand.settings-cardremain because the Keyboard shortcuts page (D34) still uses them.
No caller here imported @luke/ui/compat.
What stays the same: clicking the field or choosing Change shortcut in the ⋯ menu starts recording. Escape, another click, or moving focus away cancels and keeps the old key. A modifier pressed on its own waits for the rest of the chord. A chord without ⌃/⌥/⌘, or one Luke's window already uses, is refused and recording stays on. A complete chord is saved and registered. Reset and Remove appear under the same conditions as before. onCapture still holds the Luke keys while a field is recording.
Visible changes
- Row titles use the contract's label weight, 500. They were 400.
- Each row is at least 52px tall, the
SettingsRowcontract (was about 58px with a description and 54px without). The description uses caption leading. - The gap between the chord field and the ⋯ is now the control gap, 8px. It was 12px.
- A refusal is now announced by the row's polite live region instead of a
role="alert"paragraph. It is red caption text under the row, as on every other shared row. - The "Keys" heading uses
SettingsSection's section heading. - The chord inside the field stays at 13px. Without
.settings-card's scope it would have fallen to the 12px default, so the field now sets the size itself.
Tests
- New
voice-keys.test.ts(jsdom) rendersShortcutSection:- Pressing the field starts recording and shows the hint. Escape keeps the old key and saves nothing.
- A chord that can't be a talk key (
A) is refused in the row's live region, and recording stays on. - A chord the window already uses (⌘N) is refused with "…is Luke's own shortcut for New task. Choose another."
- A modifier pressed alone, then ⌃⌥T, saves
Control+Alt+Tand ends the recording.
- Existing
settings-panel,voice-pageanddesktop-settingstests pass unchanged. pnpm check:affectedexits 0.
Evidence
Captured on Linux with headless Chrome at 1280×840, as #3405 and #3406 did. A throwaway, uncommitted test rendered the real ShortcutSection in four synthetic states, framed in SettingsPage markup with the built desktop stylesheet. From top to bottom: at rest (Stop shows Modified), recording, ⌘N refused, and voice unavailable with Stop removed. Linux has no font for ⌘/⌥/⌃, so those glyphs show as boxes.
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Not verified on a Mac
- SF Pro rendering and the ⌘/⌥/⌃ glyphs.
- Registering the real global hotkey, and holding the Luke keys during capture through
onCapture(only the jsdom tests cover capture). - The focus ring with keyboard versus pointer focus in Electron.
🤖 Generated with Claude Code



