Skip to content

Commit f1fb136

Browse files
committed
docs(openspec): archive isolate-hotkeys
- move change to openspec/changes/archive/2026-05-28-isolate-hotkeys/ - update ui-hotkeys spec with scope stack, per-scope hotkey isolation, global keymap, typing guard, and keymap-driven keyboard help
1 parent a74c0b0 commit f1fb136

6 files changed

Lines changed: 167 additions & 12 deletions

File tree

openspec/changes/isolate-hotkeys/.openspec.yaml renamed to openspec/changes/archive/2026-05-28-isolate-hotkeys/.openspec.yaml

File renamed without changes.
File renamed without changes.

openspec/changes/isolate-hotkeys/proposal.md renamed to openspec/changes/archive/2026-05-28-isolate-hotkeys/proposal.md

File renamed without changes.

openspec/changes/isolate-hotkeys/specs/ui-hotkeys/spec.md renamed to openspec/changes/archive/2026-05-28-isolate-hotkeys/specs/ui-hotkeys/spec.md

File renamed without changes.
File renamed without changes.

openspec/specs/ui-hotkeys/spec.md

Lines changed: 167 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,131 @@ Keyboard shortcut handling for the mailbrus SvelteKit frontend. Covers global sh
66

77
---
88
## Requirements
9+
10+
### Requirement: Active scope and scope stack
11+
The frontend SHALL maintain a single ordered stack of *active scopes* where each scope is one of
12+
`list`, `reader`, `compose`, `palette`, `modal`, or `hint`. The top of the stack is the active scope.
13+
Every view SHALL push its scope when it mounts and pop the same scope when it unmounts. The stack SHALL
14+
never be empty; `list` is the base scope at app boot.
15+
16+
#### Scenario: Boot stack
17+
- **WHEN** the app first mounts at the message list
18+
- **THEN** the scope stack is `['list']` and the active scope is `list`
19+
20+
#### Scenario: Opening the reader pushes
21+
- **WHEN** the user opens a message from the list
22+
- **THEN** the scope stack becomes `['list', 'reader']` and the active scope is `reader`
23+
24+
#### Scenario: Closing the reader pops
25+
- **WHEN** the reader is open and the user closes it
26+
- **THEN** the scope stack returns to `['list']` and the active scope is `list`
27+
28+
#### Scenario: Modal layers over reader
29+
- **WHEN** the reader is open and the user opens keyboard help
30+
- **THEN** the scope stack becomes `['list', 'reader', 'modal']` and the active scope is `modal`
31+
32+
#### Scenario: Hint mode layers over its host scope
33+
- **WHEN** the user presses `f` to enter hint mode in the reader
34+
- **THEN** the scope stack becomes `['list', 'reader', 'hint']` and the active scope is `hint`
35+
36+
#### Scenario: Mismatched pop fails loudly in development
37+
- **WHEN** a view attempts to pop a scope that is not the current top of the stack
38+
- **THEN** the pop SHALL throw in development builds and SHALL be logged as an error in release builds
39+
40+
---
41+
42+
### Requirement: Per-scope hotkey isolation
43+
A keyboard binding declared for one scope SHALL NOT fire while a different scope is active. Only two
44+
kinds of bindings fire at any time: Global bindings (always eligible) and bindings of the active scope
45+
(the top of the stack). The dispatcher SHALL NOT fall through to scopes lower in the stack.
46+
47+
#### Scenario: List key does not fire in reader
48+
- **WHEN** the reader is open and the user presses `/` (the list-scope search shortcut)
49+
- **THEN** nothing happens — the inline search bar does not open
50+
51+
#### Scenario: Reader key does not fire on list
52+
- **WHEN** the message list is the active scope and the user presses `J` (the reader-scope scroll-down shortcut)
53+
- **THEN** the list selection does not move and no reader-body scroll occurs
54+
55+
#### Scenario: Compose key does not fire on list
56+
- **WHEN** the message list is the active scope and the user presses `Ctrl+S`
57+
- **THEN** no save-draft action is triggered
58+
59+
#### Scenario: Exclusive scope swallows underlying view's keys
60+
- **WHEN** a `palette`, `modal`, or `hint` scope is active and the user presses any key that is bound
61+
only in the underlying view (e.g. `j` for list navigation while the command palette is open)
62+
- **THEN** the binding does not fire; the key is consumed by the active scope only
63+
64+
---
65+
66+
### Requirement: Global keymap
67+
A small set of bindings SHALL be declared as Global and SHALL be eligible to fire regardless of the
68+
active scope, subject only to the typing guard (see *Typing guard applies to plain-key bindings*). The
69+
Global keymap SHALL include at minimum:
70+
71+
| Binding | Action |
72+
|---------|--------|
73+
| `Ctrl+K` | Toggle command palette (when an account and folder are active) |
74+
| `Ctrl+,` | Open settings |
75+
| `?` | Toggle keyboard help (per the *Keyboard help toggle* requirement) |
76+
| `Escape` | Walk back through the navigation stack (per the existing *Escape back-navigation* requirement) |
77+
78+
#### Scenario: Ctrl+K opens palette from any non-exclusive scope
79+
- **WHEN** the active scope is `list`, `reader`, or `compose` and the user presses `Ctrl+K`
80+
- **THEN** the command palette opens (provided an account and folder are active)
81+
82+
#### Scenario: ? opens help from any non-exclusive scope
83+
- **WHEN** the active scope is `list`, `reader`, or `compose`, focus is not in a text input, and the
84+
user presses `?`
85+
- **THEN** the keyboard help overlay opens
86+
87+
#### Scenario: Global keys still suppressed in inputs (plain keys)
88+
- **WHEN** focus is in an `input` or `textarea` and the user presses `?`
89+
- **THEN** `?` is typed normally and the keyboard help overlay does not open
90+
91+
#### Scenario: Global modifier combos work in inputs
92+
- **WHEN** focus is in an `input` or `textarea` and the user presses `Ctrl+K`
93+
- **THEN** the command palette toggles (modifier combos bypass the typing guard)
94+
95+
---
96+
97+
### Requirement: Typing guard applies to plain-key bindings
98+
When focus is in an `input`, `textarea`, or `contenteditable` element, the dispatcher SHALL skip any
99+
binding whose `keys` consist solely of plain characters or non-modifier keys (e.g. `j`, `?`, `g`, `/`,
100+
`Enter` alone). Bindings whose `keys` include `Ctrl`, `Alt`, `Meta`, or `Shift+<letter>` combinations
101+
SHALL still be eligible.
102+
103+
#### Scenario: j typed in compose body
104+
- **WHEN** focus is in the compose body field and the user presses `j`
105+
- **THEN** the character is typed and no list/reader navigation occurs
106+
107+
#### Scenario: ? typed in palette query
108+
- **WHEN** focus is in the command palette search input and the user presses `?`
109+
- **THEN** the character is typed and keyboard help does not open
110+
111+
#### Scenario: Ctrl+Enter still sends from compose
112+
- **WHEN** focus is in the compose body field and the user presses `Ctrl+Enter`
113+
- **THEN** the send action is triggered
114+
115+
---
116+
117+
### Requirement: Keymaps are the single source of help content
118+
Each view's keymap SHALL declare, for every binding, the displayed `keys`, a `group` label, and a
119+
human-readable `description`. The Global keymap SHALL declare the same. The keyboard help overlay SHALL
120+
render its content by reading these declarations directly; there SHALL NOT be a parallel hard-coded help
121+
list. Removing a binding from a keymap SHALL remove it from help; adding a binding SHALL add it to help.
122+
123+
#### Scenario: Help rows match registered bindings
124+
- **WHEN** the keyboard help overlay is open in any scope
125+
- **THEN** every row displayed corresponds to a binding currently registered in either the Global keymap
126+
or the active scope's keymap
127+
128+
#### Scenario: No orphan help rows
129+
- **WHEN** the keyboard help overlay is open
130+
- **THEN** there SHALL NOT be any displayed row that does not correspond to a registered binding
131+
132+
---
133+
9134
### Requirement: Global command palette shortcut
10135
The app SHALL open the command palette when `⌘K` or `Ctrl+K` is pressed, provided an account and folder are active. If the palette is already open it SHALL close.
11136

@@ -24,16 +149,38 @@ The app SHALL open the command palette when `⌘K` or `Ctrl+K` is pressed, provi
24149
---
25150

26151
### Requirement: Keyboard help toggle
27-
The app SHALL open the keyboard help overlay when `?` is pressed on the message list with no modal open. Pressing `?` again or `Escape` SHALL close it.
152+
The app SHALL open the keyboard help overlay when `?` is pressed in any non-exclusive scope (`list`,
153+
`reader`, `compose`), provided focus is not in a text input. Pressing `?` again or `Escape` SHALL close
154+
it. The overlay SHALL render exactly two sections: a **Global** section listing the Global keymap, and
155+
one section named for the active scope listing that scope's bindings (grouped by their `group` field).
156+
The overlay SHALL NOT render bindings from inactive scopes; the previous "All hotkeys" union view is
157+
removed.
28158

29159
#### Scenario: Open help from list
30-
- **WHEN** phase is list, no modal is open, and the user presses `?`
31-
- **THEN** the keyboard help overlay opens
160+
- **WHEN** the active scope is `list`, no modal is open, and the user presses `?`
161+
- **THEN** the keyboard help overlay opens showing the Global section and the List section only
162+
163+
#### Scenario: Open help from reader
164+
- **WHEN** the active scope is `reader`, no modal is open, and the user presses `?`
165+
- **THEN** the keyboard help overlay opens showing the Global section and the Reader section only;
166+
list-scope bindings (such as `/`, `c`, `g i`) are not rendered
167+
168+
#### Scenario: Open help from compose
169+
- **WHEN** the active scope is `compose` and the user presses `?` while focus is not in a text input
170+
- **THEN** the keyboard help overlay opens showing the Global section and the Compose section only
32171

33172
#### Scenario: Escape closes help
34173
- **WHEN** the keyboard help overlay is open and the user presses `Escape`
174+
- **THEN** the keyboard help overlay closes and the underlying scope becomes active again
175+
176+
#### Scenario: ? toggles help closed
177+
- **WHEN** the keyboard help overlay is open and the user presses `?`
35178
- **THEN** the keyboard help overlay closes
36179

180+
#### Scenario: ? suppressed in text inputs
181+
- **WHEN** focus is in an `input` or `textarea` and the user presses `?`
182+
- **THEN** the character is typed and the keyboard help overlay does not open
183+
37184
---
38185

39186
### Requirement: List navigation keys
@@ -299,13 +446,21 @@ When the reader is the active phase, `f` SHALL enter hint mode as specified in t
299446
---
300447

301448
### Requirement: Keyboard help documents f hint mode
302-
The keyboard help overlay (`KeyboardHelp.svelte`) SHALL include entries for the `f` hint-mode shortcut in both the list and reader sections.
303-
304-
#### Scenario: f shown in list shortcuts
305-
- **WHEN** the user opens keyboard help while on the list
306-
- **THEN** an entry for `f` is visible describing "open message by hint"
307-
308-
#### Scenario: f shown in reader shortcuts
309-
- **WHEN** the user opens keyboard help while in the reader
310-
- **THEN** an entry for `f` is visible describing "follow link / attachment by hint"
449+
The keyboard help overlay SHALL include an `f` entry in the active scope's section whenever that scope
450+
declares an `f` binding for hint mode. Specifically, the List scope SHALL declare `f` with description
451+
"open message by hint" and the Reader scope SHALL declare `f` with description "follow link / attachment
452+
by hint". Because help is rendered per active scope (see *Keyboard help toggle*), only the entry for the
453+
active scope SHALL appear at any one time.
454+
455+
#### Scenario: f shown in list help
456+
- **WHEN** the user opens keyboard help while the active scope is `list`
457+
- **THEN** the List section contains an entry for `f` describing "open message by hint"
458+
459+
#### Scenario: f shown in reader help
460+
- **WHEN** the user opens keyboard help while the active scope is `reader`
461+
- **THEN** the Reader section contains an entry for `f` describing "follow link / attachment by hint"
462+
463+
#### Scenario: List f entry not shown in reader help
464+
- **WHEN** the user opens keyboard help while the active scope is `reader`
465+
- **THEN** the List section's `f` entry is not displayed (the List section is not rendered)
311466

0 commit comments

Comments
 (0)