refactor(desktop): migrate voice key-binding editor onto the design system

Merged
CH
9d ago
+316-110

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 SettingsSection titled "Keys".
  • Each key (Talk to Luke, Stop Luke) is a SettingsRow, drawn through MenuRow'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's row gains an error slot. This changes only the desktop adapter: no @luke/ui API 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-state and .settings-card remain 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 SettingsRow contract (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) renders ShortcutSection:
    • 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+T and ends the recording.
  • Existing settings-panel, voice-page and desktop-settings tests pass unchanged.
  • pnpm check:affected exits 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.

LightDark
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


Open workspace in Conductor