@rokkit/cli 1.1.13 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/cli",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "description": "Command line utilities for icon bundling and conversion.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,256 @@
1
+ ---
2
+ name: command-system-rokkit
3
+ description: Use when adding keyboard shortcuts, a command palette, or app-level commands to a Rokkit (Svelte 5) app — the commands registry (@rokkit/states), the shortcuts action (@rokkit/actions), the CommandPalette component (@rokkit/ui), the 'mod+k' shortcut grammar, command scope/lifecycle, conflict detection, and i18n.
4
+ ---
5
+
6
+ # Rokkit Command System
7
+
8
+ Three packages, one coherent system — commands live in a registry, shortcuts fire them from the keyboard,
9
+ the palette lets users search and run them by name.
10
+
11
+ ```
12
+ @rokkit/states → commands registry (register, execute, resolve, commands.all)
13
+ @rokkit/actions → shortcuts action (use:shortcuts={commands})
14
+ @rokkit/ui → CommandPalette component (<CommandPalette bind:open />)
15
+ ```
16
+
17
+ Parallel to the theme system: `vibe` (state) + `themable` (action) + `ThemeSwitcherToggle` (component).
18
+ Commands are the same shape — registry singleton + action wiring + a ready-made UI.
19
+
20
+ ---
21
+
22
+ ## The three pieces
23
+
24
+ | Package | Export | Role |
25
+ |---------|--------|------|
26
+ | `@rokkit/states` | `commands` | Singleton registry — register, resolve, execute |
27
+ | `@rokkit/actions` | `shortcuts` | Svelte action — intercepts `keydown`, delegates to `commands` |
28
+ | `@rokkit/ui` | `CommandPalette` | Searchable dialog — reads `commands.all`, opens on `mod+k` |
29
+
30
+ ---
31
+
32
+ ## Defining commands
33
+
34
+ A `Command` object has one required field (`run`) plus metadata:
35
+
36
+ ```ts
37
+ type Command = {
38
+ id: string // unique — re-registering the same id replaces and warns
39
+ label: string // display text in the palette (consumer-localized)
40
+ run: () => void // executed on shortcut match or palette selection
41
+ shortcut?: string // e.g. 'mod+k', 'mod+shift+l', '?'
42
+ group?: string // palette category header
43
+ global?: boolean // fire even while a text input is focused (default false)
44
+ enabled?: () => boolean // dynamic gate — command is skipped when returns false
45
+ keywords?: string[] // extra palette search terms
46
+ }
47
+ ```
48
+
49
+ Register a batch with `registerMany` (returns a single unregister callback):
50
+
51
+ ```js
52
+ import { commands } from '@rokkit/states'
53
+
54
+ const unregister = commands.registerMany([
55
+ {
56
+ id: 'palette.open',
57
+ label: 'Open command palette',
58
+ shortcut: 'mod+k',
59
+ global: true, // fires while typing — required for Cmd/Ctrl+K
60
+ group: 'general',
61
+ run: () => { paletteOpen = true }
62
+ },
63
+ {
64
+ id: 'theme.toggle',
65
+ label: 'Toggle light / dark',
66
+ shortcut: 'mod+shift+l',
67
+ group: 'theme',
68
+ run: () => { vibe.mode = vibe.mode === 'dark' ? 'light' : 'dark' }
69
+ },
70
+ {
71
+ id: 'conversation.new',
72
+ label: 'New conversation',
73
+ shortcut: 'mod+shift+n',
74
+ group: 'general',
75
+ run: startNewConversation
76
+ }
77
+ ])
78
+ ```
79
+
80
+ Register one command with `register` (also returns an unregister callback):
81
+
82
+ ```js
83
+ const unregister = commands.register({
84
+ id: 'search.focus',
85
+ label: 'Focus search',
86
+ shortcut: '/',
87
+ run: () => searchInput?.focus()
88
+ })
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Wiring it up
94
+
95
+ Wire the root element once (e.g. `+layout.svelte`). Register commands in `onMount`
96
+ and return the unregister fn — Svelte calls it on destroy to clean up:
97
+
98
+ ```svelte
99
+ <script>
100
+ import { onMount } from 'svelte'
101
+ import { commands, vibe } from '@rokkit/states'
102
+ import { shortcuts } from '@rokkit/actions'
103
+ import { CommandPalette } from '@rokkit/ui'
104
+
105
+ let paletteOpen = $state(false)
106
+
107
+ onMount(() => {
108
+ return commands.registerMany([
109
+ {
110
+ id: 'palette.open',
111
+ label: 'Open command palette',
112
+ shortcut: 'mod+k',
113
+ global: true,
114
+ group: 'general',
115
+ run: () => { paletteOpen = true }
116
+ },
117
+ {
118
+ id: 'theme.toggle',
119
+ label: 'Toggle light / dark',
120
+ shortcut: 'mod+shift+l',
121
+ group: 'theme',
122
+ run: () => { vibe.mode = vibe.mode === 'dark' ? 'light' : 'dark' }
123
+ }
124
+ ])
125
+ })
126
+ </script>
127
+
128
+ <!-- shortcuts action on the root element intercepts keydown globally -->
129
+ <div use:shortcuts={commands}>
130
+ {@render children()}
131
+ </div>
132
+
133
+ <!-- palette renders as an overlay; it reads commands.all directly -->
134
+ <CommandPalette bind:open={paletteOpen} />
135
+ ```
136
+
137
+ The `shortcuts` action listens on `window` for `keydown` (via Svelte's `on()`), so
138
+ placing it on any element in the tree works — root element or `<svelte:body>` are both fine.
139
+
140
+ ---
141
+
142
+ ## Scope & lifecycle
143
+
144
+ **App-root commands** — register once in the root layout's `onMount`. They live for
145
+ the entire session. The `palette.open` command belongs here; it must always be reachable.
146
+
147
+ **Route or component-scoped commands** — register in the component's `onMount` and
148
+ return the unregister fn. They are removed when the component unmounts:
149
+
150
+ ```svelte
151
+ <script>
152
+ import { onMount } from 'svelte'
153
+ import { commands } from '@rokkit/states'
154
+
155
+ onMount(() => {
156
+ // These commands are only active while this component is mounted
157
+ return commands.registerMany([
158
+ {
159
+ id: 'editor.save',
160
+ label: 'Save document',
161
+ shortcut: 'mod+s',
162
+ group: 'editor',
163
+ run: saveDocument
164
+ },
165
+ {
166
+ id: 'editor.format',
167
+ label: 'Format code',
168
+ shortcut: 'mod+shift+f',
169
+ group: 'editor',
170
+ run: formatCode
171
+ }
172
+ ])
173
+ })
174
+ </script>
175
+ ```
176
+
177
+ `commands.all` is reactive (`$state` inside the registry), so `CommandPalette` and
178
+ any list reading `commands.all` automatically re-renders when commands are added or removed.
179
+
180
+ ---
181
+
182
+ ## Shortcut grammar
183
+
184
+ `mod` resolves to `Cmd` on macOS, `Ctrl` on Linux/Windows — use it for all
185
+ primary shortcuts so they are cross-platform by default.
186
+
187
+ | Shortcut string | Fires on |
188
+ |-----------------|----------|
189
+ | `'mod+k'` | Cmd+K (macOS) / Ctrl+K (other) |
190
+ | `'mod+shift+l'` | Cmd+Shift+L / Ctrl+Shift+L |
191
+ | `'mod+shift+n'` | Cmd+Shift+N / Ctrl+Shift+N |
192
+ | `'?'` | `?` key (no modifier) |
193
+ | `'alt+t'` | Option+T (macOS) / Alt+T (other) |
194
+
195
+ **`global: true` is required for any shortcut that must fire while a text input is focused.**
196
+ Without it the `shortcuts` action skips the command when `document.activeElement` is an
197
+ `<input>`, `<textarea>`, `<select>`, or `contenteditable`. The `palette.open` command
198
+ (`mod+k`) must always be `global: true` — the palette is most useful when the composer
199
+ or a form field is focused.
200
+
201
+ **Conflict detection:** the registry normalizes shortcuts on registration (modifiers
202
+ sorted by canonical order, key lowercased, `mod` resolved). If a second command registers
203
+ the same normalized shortcut it is warned and skipped — the first registrant wins.
204
+
205
+ **Single chords only:** the grammar is one key + modifiers. Multi-chord sequences
206
+ (e.g. Vim-style `g g`) are not supported.
207
+
208
+ ---
209
+
210
+ ## i18n
211
+
212
+ The palette chrome comes from `messages.command.*` — override once to localize globally:
213
+
214
+ ```js
215
+ import { messages } from '@rokkit/states'
216
+
217
+ messages.set({
218
+ command: {
219
+ placeholder: 'Befehl ausführen…',
220
+ noResults: 'Keine Befehle gefunden',
221
+ label: 'Befehlspalette'
222
+ }
223
+ })
224
+ ```
225
+
226
+ Or register a named locale and switch:
227
+
228
+ ```js
229
+ messages.register('de', {
230
+ command: {
231
+ placeholder: 'Befehl ausführen…',
232
+ noResults: 'Keine Befehle gefunden',
233
+ label: 'Befehlspalette'
234
+ }
235
+ })
236
+ messages.setLocale('de')
237
+ ```
238
+
239
+ Command `label` and `keywords` are consumer-supplied strings — localize them
240
+ directly in the object passed to `register` / `registerMany`.
241
+
242
+ Default English values: `placeholder: 'Run a command…'`, `noResults: 'No commands found'`,
243
+ `label: 'Command palette'`.
244
+
245
+ ---
246
+
247
+ ## Common mistakes
248
+
249
+ | Mistake | Why it fails | Fix |
250
+ |---------|-------------|-----|
251
+ | Registering at module top-level (not in `onMount`) | The registry is a singleton; commands pile up across hot-reloads in dev and are never cleaned up on unmount | Always register in `onMount`, return the unregister fn |
252
+ | Forgetting `global: true` on `mod+k` (or any Cmd-shortcut) | `shortcuts` skips non-global commands while a text input is focused — Cmd+K fired in the composer silently no-ops | Add `global: true` to any command that must fire during text input |
253
+ | Hand-rolling `window.addEventListener('keydown', ...)` | No conflict detection, no `enabled()` gate, no `commands.all` integration, no cleanup | Use `use:shortcuts={commands}` — it handles all of these |
254
+ | Using `<svelte:body use:shortcuts />` without noting that `shortcuts` uses `window` | The action attaches a `window` listener regardless of which element it's placed on — placing it on body is fine but redundant | Place on any element in the tree; root element or body both work |
255
+ | Putting `run: () => paletteOpen = true` at module scope | `paletteOpen` is `$state` in the component — the closure captures the right reference only when created inside the component function | Define `run` functions after `$state` declarations, inside the component |
256
+ | Expecting `commands.all` to include disabled commands without `enabled` | `enabled` is a dynamic gate called at dispatch time, not a filter on the list | `commands.all` always contains all registered commands; `enabled` only gates execution |