@densedash/core 0.1.0 → 0.2.0

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.
Files changed (62) hide show
  1. package/README.md +59 -0
  2. package/dist/bare.d.ts +3 -3
  3. package/dist/bare.js +2 -2
  4. package/dist/collections/types.d.ts +0 -46
  5. package/dist/commands/index.d.ts +3 -3
  6. package/dist/commands/index.js +152 -39
  7. package/dist/commands/index.js.map +1 -1
  8. package/dist/commands/keys.js +4 -3
  9. package/dist/commands/keys.js.map +1 -1
  10. package/dist/commands/scopes.js +30 -3
  11. package/dist/commands/scopes.js.map +1 -1
  12. package/dist/commands/trading-preset.d.ts +5 -1
  13. package/dist/commands/trading-preset.js +22 -43
  14. package/dist/commands/trading-preset.js.map +1 -1
  15. package/dist/commands/types.d.ts +32 -5
  16. package/dist/defaults/keyboard.js +55 -10
  17. package/dist/defaults/keyboard.js.map +1 -1
  18. package/dist/index.d.ts +7 -10
  19. package/dist/index.js +3 -7
  20. package/dist/index.js.map +1 -1
  21. package/dist/kernel.d.ts +4 -4
  22. package/dist/kernel.js.map +1 -1
  23. package/dist/navigation/commands.js +200 -0
  24. package/dist/navigation/commands.js.map +1 -0
  25. package/dist/navigation/index.d.ts +31 -0
  26. package/dist/navigation/index.js +148 -0
  27. package/dist/navigation/index.js.map +1 -0
  28. package/dist/{collections → navigation}/move.js +1 -1
  29. package/dist/navigation/move.js.map +1 -0
  30. package/dist/{tree → navigation}/props.js +25 -9
  31. package/dist/navigation/props.js.map +1 -0
  32. package/dist/{collections → navigation}/reconcile.js +1 -1
  33. package/dist/navigation/reconcile.js.map +1 -0
  34. package/dist/navigation/selection.js +21 -0
  35. package/dist/navigation/selection.js.map +1 -0
  36. package/dist/navigation/state.d.ts +3 -0
  37. package/dist/navigation/state.js +265 -0
  38. package/dist/navigation/state.js.map +1 -0
  39. package/dist/navigation/types.d.ts +86 -0
  40. package/dist/ports/keyboard.d.ts +3 -2
  41. package/dist/ports/keyboard.js +9 -0
  42. package/dist/ports/keyboard.js.map +1 -0
  43. package/dist/tree/types.d.ts +7 -36
  44. package/package.json +4 -3
  45. package/dist/collections/flatten.d.ts +0 -8
  46. package/dist/collections/flatten.js +0 -59
  47. package/dist/collections/flatten.js.map +0 -1
  48. package/dist/collections/move.d.ts +0 -7
  49. package/dist/collections/move.js.map +0 -1
  50. package/dist/collections/reconcile.d.ts +0 -9
  51. package/dist/collections/reconcile.js.map +0 -1
  52. package/dist/collections/selection.d.ts +0 -12
  53. package/dist/collections/selection.js +0 -39
  54. package/dist/collections/selection.js.map +0 -1
  55. package/dist/tree/commands.js +0 -119
  56. package/dist/tree/commands.js.map +0 -1
  57. package/dist/tree/index.d.ts +0 -6
  58. package/dist/tree/index.js +0 -71
  59. package/dist/tree/index.js.map +0 -1
  60. package/dist/tree/props.js.map +0 -1
  61. package/dist/tree/state.js +0 -126
  62. package/dist/tree/state.js.map +0 -1
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Commands
2
+
3
+ Commands define actions; presets and overrides choose their keys. Each registered scope owns
4
+ its keyboard bindings and focus listeners until its unregister function runs.
5
+
6
+ ```ts
7
+ const unregister = kernel.commands.register("orders", [
8
+ { id: "approve", label: "Approve order", run: approveOrder },
9
+ ]);
10
+
11
+ kernel.commands.update("orders", [
12
+ { id: "approve", label: "Approve selected order", run: approveSelectedOrder },
13
+ ]);
14
+
15
+ unregister();
16
+ ```
17
+
18
+ `update(scope, commands)` replaces definitions in place, including labels, groups, `hidden`,
19
+ confirmation prompts, `enabled` predicates and handlers. It requires the same command ids in
20
+ the same order; changing membership, order or scope options requires unregistering first.
21
+ Updates leave the scope's bindings and sequence progress intact. A pending confirmation keeps
22
+ its original keyboard event, uses the latest handler and enabled predicate, and refreshes its
23
+ label and prompt. Removing `confirm` from the definition leaves an existing prompt pending
24
+ until accepted or canceled. Updating an unknown scope throws; updates after disposal do nothing.
25
+
26
+ `commands.keymap` exposes formatted keys and the evaluated `enabled` flag. Predicate failures
27
+ are reported as command errors and make that entry disabled. Call `update` when app state
28
+ changes to refresh enabled state; the React `useCommands` hook does this after each render.
29
+ Disabled commands release their keys to other scopes and the browser default. Duplicate keys
30
+ within one scope still belong to their first registered command, even when it is disabled.
31
+ Equal snapshots retain their array identity across focus and definition updates. Override
32
+ replacement still notifies subscribers so consumers of `commands.keys(id)` can refresh keys
33
+ for unfocused scopes.
34
+
35
+ `Footer.Legend` and `Keymap.Legend` join commands by `layer`, then `group`, and show their keys
36
+ side by side. `groupLabel` supplies the group's short label; a group without one uses its name.
37
+ Ungrouped commands keep their own labels. Footer shows the first key per command and compresses
38
+ three or more consecutive digits into a range. Keymap does the same for grouped commands;
39
+ ungrouped commands retain all their alternative keys.
40
+
41
+ Built-in primary hints cover moving, opening/closing, sorting, picking a tab and
42
+ stepping between tabs. Activating, jumping, paging, going to a parent, selecting/extending selection and
43
+ toggling expansion are secondary (`legend: "secondary"`). Both legends omit secondary hints
44
+ by default so a focused tree fits a narrow footer. Pass `all` to include them; `Keymap.Legend all`
45
+ also lists alternative keys. Disabled, hidden, shadowed and unbound commands remain omitted.
46
+ `tree.toggle` has no default key; bind it and use `all` to include it.
47
+
48
+ Pass `labels={{ "tree.move": "navigate", "tabs.pick": "choose a view" }}` to either legend to
49
+ override the default labels by group id, as with row label overrides. The command metadata is
50
+ the shared source for both legends; it can also be replaced through `commands.update`.
51
+ An instance's `keys: { label }` takes precedence and retains all its hints. An explicit `groupBy`
52
+ returns both the item's name and its label; it takes precedence over default labels. An explicit
53
+ `filter` or `groupBy` selects from all available entries, including secondary hints. `order`
54
+ uses group ids or layer names, independently of translated labels. `AppShell.Footer` forwards
55
+ these same options to its legend. None of these presentation options change bindings.
56
+
57
+ ChordHint omits disabled sequences.
58
+ The default preset retains component commands, confirmation controls and the generic/navigation
59
+ ids used by examples (`next`, `previous`, `open`, `approve`, `p-home`).
package/dist/bare.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BasePorts, CheckedKernelOptions, Kernel, KernelOptions, Plugin, PortProviders, Ports, ResolvedPorts } from "./kernel.js";
2
2
  import { createCommands } from "./commands/index.js";
3
- import { tradingPreset } from "./commands/trading-preset.js";
4
- import { Command, CommandContext, CommandId, CommandScopeOptions, Commands, KeyOverrideId, KeyOverrides, KeymapEntry, KeysOptions, PendingConfirm, Preset, Register, ValidatedKeyOverrides } from "./commands/types.js";
3
+ import { shiftedTreePreset, tradingPreset, treePreset } from "./commands/trading-preset.js";
4
+ import { Command, CommandContext, CommandId, CommandScopeOptions, Commands, KeyLayer, KeyOverrideId, KeyOverrides, KeymapEntry, KeysOptions, PendingConfirm, Preset, Register, ValidatedKeyOverrides } from "./commands/types.js";
5
5
  import { ClockPort } from "./ports/clock.js";
6
6
  import { KeyboardBinding, KeyboardBindingOptions, KeyboardHandler, KeyboardHotkey, KeyboardHotkeyChord, KeyboardParseResult, KeyboardPendingStore, KeyboardPlatform, KeyboardPort, KeyboardScopeOptions, KeyboardTarget, PendingKeyboardSequence } from "./ports/keyboard.js";
7
7
  import { ReporterPort } from "./ports/reporter.js";
@@ -10,5 +10,5 @@ import { Task } from "./task.js";
10
10
  import { ChangeDetails, Controllable } from "./controllable.js";
11
11
  /** No defaults: every port must be supplied by plugins or explicit ports. */
12
12
  export declare function createKernel<const Options extends KernelOptions = KernelOptions>(options?: Options & CheckedKernelOptions<Options>): Kernel<ResolvedPorts<Options>>;
13
- export { type BasePorts, type ChangeDetails, type ClockPort, type Command, type CommandContext, type CommandId, type CommandScopeOptions, type Commands, type Controllable, type Kernel, type KernelOptions, type KeyOverrideId, type KeyOverrides, type KeyboardBinding, type KeyboardBindingOptions, type KeyboardHandler, type KeyboardHotkey, type KeyboardHotkeyChord, type KeyboardParseResult, type KeyboardPendingStore, type KeyboardPlatform, type KeyboardPort, type KeyboardScopeOptions, type KeyboardTarget, type KeymapEntry, type KeysOptions, type PendingConfirm, type PendingKeyboardSequence, type Plugin, type PortProviders, type Ports, type Preset, type Register, type ReporterPort, type ResolvedPorts, type Store, type StorePort, type Task, type ValidatedKeyOverrides, createCommands, tradingPreset };
13
+ export { type BasePorts, type ChangeDetails, type ClockPort, type Command, type CommandContext, type CommandId, type CommandScopeOptions, type Commands, type Controllable, type Kernel, type KernelOptions, type KeyLayer, type KeyOverrideId, type KeyOverrides, type KeyboardBinding, type KeyboardBindingOptions, type KeyboardHandler, type KeyboardHotkey, type KeyboardHotkeyChord, type KeyboardParseResult, type KeyboardPendingStore, type KeyboardPlatform, type KeyboardPort, type KeyboardScopeOptions, type KeyboardTarget, type KeymapEntry, type KeysOptions, type PendingConfirm, type PendingKeyboardSequence, type Plugin, type PortProviders, type Ports, type Preset, type Register, type ReporterPort, type ResolvedPorts, type Store, type StorePort, type Task, type ValidatedKeyOverrides, createCommands, shiftedTreePreset, tradingPreset, treePreset };
14
14
  //# sourceMappingURL=bare.d.ts.map
package/dist/bare.js CHANGED
@@ -1,4 +1,4 @@
1
- import { tradingPreset } from "./commands/trading-preset.js";
1
+ import { shiftedTreePreset, tradingPreset, treePreset } from "./commands/trading-preset.js";
2
2
  import { resolveKernel } from "./kernel.js";
3
3
  import { createCommands } from "./commands/index.js";
4
4
  // src/bare.ts
@@ -6,6 +6,6 @@ import { createCommands } from "./commands/index.js";
6
6
  function createKernel(options) {
7
7
  return resolveKernel({ options: options ?? {} });
8
8
  }
9
- export { createCommands, createKernel, tradingPreset };
9
+ export { createCommands, createKernel, shiftedTreePreset, tradingPreset, treePreset };
10
10
 
11
11
  //# sourceMappingURL=bare.js.map
@@ -1,48 +1,2 @@
1
1
  export type Key = string;
2
- /** Callbacks must be synchronous and stable for the duration of a call. */
3
- export interface TreeSource<T> {
4
- roots: readonly T[];
5
- getKey(item: NoInfer<T>): Key;
6
- getChildren?(item: NoInfer<T>): readonly NoInfer<T>[] | undefined;
7
- isDisabled?(item: NoInfer<T>): boolean;
8
- getTextValue?(item: NoInfer<T>): string;
9
- }
10
- export interface TreeRow<T> {
11
- readonly key: Key;
12
- readonly item: T;
13
- readonly index: number;
14
- readonly depth: number;
15
- readonly parentKey: Key | null;
16
- readonly hasChildren: boolean;
17
- readonly expanded: boolean;
18
- readonly posInSet: number;
19
- readonly setSize: number;
20
- readonly disabled: boolean;
21
- readonly textValue: string;
22
- }
23
- export interface TreeState {
24
- highlighted: Key | null;
25
- selected: ReadonlySet<Key>;
26
- anchor: Key | null;
27
- expanded: ReadonlySet<Key>;
28
- }
29
- export type Move = {
30
- type: "next" | "prev";
31
- count?: number;
32
- } | {
33
- type: "first" | "last";
34
- } | {
35
- type: "parent" | "firstChild";
36
- } | {
37
- type: "page";
38
- direction: 1 | -1;
39
- size: number;
40
- } | {
41
- type: "typeahead";
42
- text: string;
43
- };
44
- export interface MoveOptions {
45
- /** Wrap next, prev, page and typeahead at the ends. Defaults to false. */
46
- loop?: boolean;
47
- }
48
2
  //# sourceMappingURL=types.d.ts.map
@@ -1,10 +1,10 @@
1
- import { tradingPreset } from "./trading-preset.js";
1
+ import { shiftedTreePreset, tradingPreset, treePreset } from "./trading-preset.js";
2
2
  import { Ports } from "../kernel.js";
3
- import { Command, CommandContext, CommandId, CommandScopeOptions, Commands, KeyOverrideId, KeyOverrides, KeymapEntry, KeysOptions, PendingConfirm, Preset, Register, ValidatedKeyOverrides } from "./types.js";
3
+ import { Command, CommandContext, CommandId, CommandScopeOptions, Commands, KeyLayer, KeyOverrideId, KeyOverrides, KeymapEntry, KeysOptions, PendingConfirm, Preset, Register, ValidatedKeyOverrides } from "./types.js";
4
4
  /** Only port interfaces enter this owner; bare and default kernels share the same layer. */
5
5
  export declare function createCommands({ ports: { keyboard, store, reporter }, keys: options }: {
6
6
  ports: Pick<Ports, "keyboard" | "store" | "reporter">;
7
7
  keys?: KeysOptions;
8
8
  }): Commands;
9
- export { type Command, type CommandContext, type CommandId, type CommandScopeOptions, type Commands, type KeyOverrideId, type KeyOverrides, type KeymapEntry, type KeysOptions, type PendingConfirm, type Preset, type Register, type ValidatedKeyOverrides, tradingPreset };
9
+ export { type Command, type CommandContext, type CommandId, type CommandScopeOptions, type Commands, type KeyLayer, type KeyOverrideId, type KeyOverrides, type KeymapEntry, type KeysOptions, type PendingConfirm, type Preset, type Register, type ValidatedKeyOverrides, shiftedTreePreset, tradingPreset, treePreset };
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,6 @@
1
- import { tradingPreset } from "./trading-preset.js";
1
+ import { shiftedTreePreset, tradingPreset, treePreset } from "./trading-preset.js";
2
2
  import { createKeys, keyIdentity } from "./keys.js";
3
- import { activeScopes, scopeHasFocus } from "./scopes.js";
3
+ import { activeScopes, keymapScopes, scopeHasFocus } from "./scopes.js";
4
4
  // src/commands/index.ts
5
5
  let nextConfirmScope = 0;
6
6
  /** Only port interfaces enter this owner; bare and default kernels share the same layer. */
@@ -16,40 +16,95 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
16
16
  const confirmScope = `densedash:confirm:${nextConfirmScope++}`;
17
17
  let pending = null;
18
18
  let disposed = false;
19
- function publishKeymap() {
20
- const entries = [];
21
- const claimed = /* @__PURE__ */ new Set();
22
- for (const scope of pending ? [] : activeScopes(scopes).filter(scopeHasFocus)) for (const command of scope.commands) {
23
- const available = keys.get(command.id).filter((key) => {
24
- const identity = keyIdentity(key);
25
- if (claimed.has(identity)) return false;
26
- claimed.add(identity);
27
- return true;
28
- });
29
- if (command.hidden || keys.get(command.id).length > 0 && available.length === 0) continue;
30
- entries.push({
31
- command: command.id,
32
- label: command.label,
33
- scope: scope.name,
34
- ...command.group === void 0 ? {} : { group: command.group },
35
- keys: available.map((key) => keyboard.format(key))
19
+ function commandEnabled(command) {
20
+ try {
21
+ return command.enabled?.() !== false;
22
+ } catch (cause) {
23
+ reporter.error(cause, {
24
+ source: "command",
25
+ id: command.id
36
26
  });
27
+ return false;
28
+ }
29
+ }
30
+ function validateLayers() {
31
+ const owners = /* @__PURE__ */ new Map();
32
+ for (const scope of scopes.values()) {
33
+ if (!scope.options.keys) continue;
34
+ for (const command of scope.commands) for (const key of scope.keys.get(command.id)) {
35
+ const identity = keyIdentity(key);
36
+ const owner = owners.get(identity);
37
+ if (owner && owner !== scope) throw new Error(`Key layers "${owner.options.keys?.label}" (${owner.name}) and "${scope.options.keys.label}" (${scope.name}) share ${keyboard.format(key)}.`);
38
+ owners.set(identity, scope);
39
+ }
40
+ }
41
+ }
42
+ function publishKeymap(keysChanged = false) {
43
+ const layered = /* @__PURE__ */ new Set();
44
+ for (const scope of activeScopes(scopes)) {
45
+ if (!scope.options.keys) continue;
46
+ for (const owned of scope.bindings) if (commandEnabled(scope.commands[owned.commandIndex])) layered.add(owned.key);
47
+ }
48
+ for (const scope of scopes.values()) for (const owned of scope.bindings) {
49
+ const enabled = commandEnabled(scope.commands[owned.commandIndex]) && (scope.options.keys !== void 0 || !layered.has(owned.key));
50
+ if (enabled === owned.enabled) continue;
51
+ owned.enabled = enabled;
52
+ owned.binding.update({ enabled });
53
+ }
54
+ const entries = [];
55
+ const claimed = /* @__PURE__ */ new Map();
56
+ for (const scope of pending ? [] : keymapScopes(scopes)) {
57
+ const reserved = /* @__PURE__ */ new Set();
58
+ for (const command of scope.commands) {
59
+ const enabled = commandEnabled(command);
60
+ const available = scope.keys.get(command.id).filter((key) => {
61
+ const identity = keyIdentity(key);
62
+ if (reserved.has(identity)) return false;
63
+ reserved.add(identity);
64
+ if (claimed.has(identity)) return false;
65
+ const first = typeof key === "string" ? void 0 : key[0];
66
+ const prefixOwner = first === void 0 ? void 0 : claimed.get(keyIdentity(first));
67
+ const prefixTarget = prefixOwner?.options.keys ? void 0 : prefixOwner?.options.target;
68
+ if (prefixTarget && "ownerDocument" in prefixTarget && prefixTarget.ownerDocument && prefixTarget !== scope.options.target) return false;
69
+ if (enabled) claimed.set(identity, scope);
70
+ return true;
71
+ });
72
+ if (command.hidden || scope.keys.get(command.id).length > 0 && available.length === 0) continue;
73
+ entries.push({
74
+ command: command.id,
75
+ label: command.label,
76
+ scope: scope.name,
77
+ ...scope.options.keys ? { layer: scope.options.keys.label } : {},
78
+ ...command.group === void 0 ? {} : { group: command.group },
79
+ ...command.groupLabel === void 0 ? {} : { groupLabel: command.groupLabel },
80
+ ...command.legend === void 0 ? {} : { legend: command.legend },
81
+ keys: available.map((key) => keyboard.format(key)),
82
+ enabled
83
+ });
84
+ }
37
85
  }
38
86
  if (pending) {
39
87
  entries.push({
40
88
  command: "confirm",
41
89
  label: "Confirm",
42
90
  scope: confirmScope,
43
- keys: keys.get("confirm").map((key) => keyboard.format(key))
91
+ keys: keys.get("confirm").map((key) => keyboard.format(key)),
92
+ enabled: true
44
93
  });
45
94
  entries.push({
46
95
  command: "cancel",
47
96
  label: "Cancel",
48
97
  scope: confirmScope,
49
- keys: keys.get("cancel").map((key) => keyboard.format(key))
98
+ keys: keys.get("cancel").map((key) => keyboard.format(key)),
99
+ enabled: true
50
100
  });
51
101
  }
52
- keymap.set(entries);
102
+ const previous = keymap.get();
103
+ const equal = previous.length === entries.length && entries.every((entry, index) => {
104
+ const before = previous[index];
105
+ return before !== void 0 && before.command === entry.command && before.label === entry.label && before.group === entry.group && before.groupLabel === entry.groupLabel && before.legend === entry.legend && before.scope === entry.scope && before.layer === entry.layer && before.enabled === entry.enabled && before.keys.length === entry.keys.length && before.keys.every((key, keyIndex) => key === entry.keys[keyIndex]);
106
+ });
107
+ if (keysChanged || !equal) keymap.set(entries);
53
108
  }
54
109
  function cancel() {
55
110
  if (!pending) return;
@@ -63,7 +118,7 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
63
118
  }
64
119
  function run(command, context) {
65
120
  try {
66
- if (command.enabled?.() === false) return;
121
+ if (!commandEnabled(command)) return;
67
122
  const result = reporter.trace ? reporter.trace(command.id, () => command.run(context)) : command.run(context);
68
123
  if (result !== void 0) Promise.resolve(result).catch((cause) => reporter.error(cause, {
69
124
  source: "command",
@@ -89,7 +144,7 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
89
144
  run(command, context);
90
145
  return;
91
146
  }
92
- if (command.enabled?.() === false) return;
147
+ if (!commandEnabled(command)) return;
93
148
  const prompt = command.confirm.prompt;
94
149
  const bindings = [];
95
150
  const release = keyboard.activateScope(confirmScope, { exclusive: true });
@@ -135,10 +190,10 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
135
190
  }
136
191
  }
137
192
  function bindScope(scope) {
138
- for (const binding of scope.bindings) binding.dispose();
193
+ for (const { binding } of scope.bindings) binding.dispose();
139
194
  scope.bindings.length = 0;
140
195
  const owners = /* @__PURE__ */ new Map();
141
- for (const command of scope.commands) for (const key of keys.get(command.id)) {
196
+ for (const [index, command] of scope.commands.entries()) for (const key of scope.keys.get(command.id)) {
142
197
  const identity = keyIdentity(key);
143
198
  const owner = owners.get(identity);
144
199
  if (owner) {
@@ -154,13 +209,28 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
154
209
  continue;
155
210
  }
156
211
  owners.set(identity, command);
157
- const { parent: _parent, only: _only, ...bindingOptions } = scope.options;
158
- scope.bindings.push(keyboard.bind(key, (event) => {
159
- if (!event.repeat || !command.confirm) invokeOwned(scope, command, { event });
160
- }, {
161
- ...bindingOptions,
162
- scope: scope.name
163
- }));
212
+ const { parent: _parent, only: _only, keys: layer, ...bindingOptions } = scope.options;
213
+ const target = bindingOptions.target;
214
+ const ambientTarget = target && "ownerDocument" in target ? target.ownerDocument : target;
215
+ const enabled = commandEnabled(command);
216
+ scope.bindings.push({
217
+ key: identity,
218
+ commandIndex: index,
219
+ enabled,
220
+ binding: keyboard.bind(key, (event) => {
221
+ const current = scope.commands[index];
222
+ if (current && (!event.repeat || !current.confirm)) invokeOwned(scope, current, { event });
223
+ }, {
224
+ ...bindingOptions,
225
+ ignoreInputs: bindingOptions.ignoreInputs ?? "editable",
226
+ ...layer ? {
227
+ ...ambientTarget ? { target: ambientTarget } : {},
228
+ ignoreInputs: "editable"
229
+ } : {},
230
+ scope: scope.name,
231
+ enabled
232
+ })
233
+ });
164
234
  }
165
235
  }
166
236
  return {
@@ -168,17 +238,29 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
168
238
  if (disposed) throw new Error("Commands have been disposed.");
169
239
  if (name === "global" && scopeOptions.only) throw new Error("The global scope cannot be exclusive. Use a named layer.");
170
240
  if (scopes.has(name) || name === confirmScope) throw new Error(`Command scope "${name}" already has an owner.`);
241
+ const scopeKeys = scopeOptions.keys ? createKeys({
242
+ keyboard,
243
+ reporter,
244
+ options: scopeOptions.keys,
245
+ fallback: keys.get
246
+ }) : keys;
171
247
  const release = keyboard.activateScope(name, {
172
248
  ...scopeOptions.parent === void 0 ? {} : { parent: scopeOptions.parent },
173
249
  exclusive: scopeOptions.only ?? false
174
250
  });
175
251
  const target = scopeOptions.target;
176
- const focusDocument = target && "ownerDocument" in target ? target.ownerDocument : void 0;
177
- const onFocus = () => publishKeymap();
252
+ const focusDocument = target && "document" in target ? target.document : target && "defaultView" in target ? target : target?.ownerDocument ?? globalThis.document;
253
+ const onFocus = () => {
254
+ publishKeymap();
255
+ queueMicrotask(() => {
256
+ if (scopes.get(name) === scope) publishKeymap();
257
+ });
258
+ };
178
259
  focusDocument?.addEventListener("focusin", onFocus, true);
179
260
  focusDocument?.addEventListener("focusout", onFocus, true);
180
261
  const scope = {
181
262
  name,
263
+ keys: scopeKeys,
182
264
  commands: [...commands],
183
265
  options: { ...scopeOptions },
184
266
  release() {
@@ -191,7 +273,7 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
191
273
  unregister() {
192
274
  if (scopes.get(name) !== scope) return;
193
275
  scopes.delete(name);
194
- for (const binding of scope.bindings) binding.dispose();
276
+ for (const { binding } of scope.bindings) binding.dispose();
195
277
  scope.release();
196
278
  if (pending?.scope === scope) cancel();
197
279
  else publishKeymap();
@@ -199,6 +281,7 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
199
281
  };
200
282
  scopes.set(name, scope);
201
283
  try {
284
+ validateLayers();
202
285
  bindScope(scope);
203
286
  publishKeymap();
204
287
  } catch (cause) {
@@ -207,6 +290,29 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
207
290
  }
208
291
  return scope.unregister;
209
292
  },
293
+ update(name, commands) {
294
+ if (disposed) return;
295
+ const scope = scopes.get(name);
296
+ if (!scope) throw new Error(`Command scope "${name}" has no owner.`);
297
+ if (commands.length !== scope.commands.length || commands.some((command, index) => command.id !== scope.commands[index]?.id)) throw new Error(`Command scope "${name}" updates must preserve ordered command ids.`);
298
+ scope.commands = [...commands];
299
+ store.batch(() => {
300
+ if (pending?.scope === scope) {
301
+ const command = commands.find((candidate) => candidate.id === pending?.command.id);
302
+ const current = confirm.get();
303
+ if (command && current) {
304
+ pending.command = command;
305
+ const prompt = command.confirm?.prompt ?? current.prompt;
306
+ if (current.label !== command.label || current.prompt !== prompt) confirm.set({
307
+ ...current,
308
+ label: command.label,
309
+ prompt
310
+ });
311
+ }
312
+ }
313
+ publishKeymap();
314
+ });
315
+ },
210
316
  invoke(id, context = {}) {
211
317
  if (disposed || pending) return;
212
318
  for (const scope of activeScopes(scopes)) {
@@ -227,16 +333,23 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
227
333
  get: () => confirm.get(),
228
334
  subscribe: (listener) => confirm.subscribe(listener)
229
335
  },
230
- keys: (id) => keys.get(id),
336
+ keys: (id, scope) => scope === void 0 ? keys.get(id) : scopes.get(scope)?.keys.get(id) ?? [],
231
337
  accept,
232
338
  cancel,
233
339
  setOverrides(overrides) {
234
340
  if (disposed) return;
341
+ const previous = keys.overrides();
235
342
  const validated = keys.replace(overrides);
343
+ try {
344
+ validateLayers();
345
+ } catch (cause) {
346
+ keys.replace(previous);
347
+ throw cause;
348
+ }
236
349
  store.batch(() => {
237
350
  cancel();
238
351
  for (const scope of scopes.values()) bindScope(scope);
239
- publishKeymap();
352
+ publishKeymap(true);
240
353
  });
241
354
  options.onChange?.(validated);
242
355
  },
@@ -250,6 +363,6 @@ function createCommands({ ports: { keyboard, store, reporter }, keys: options =
250
363
  }
251
364
  };
252
365
  }
253
- export { createCommands, tradingPreset };
366
+ export { createCommands, shiftedTreePreset, tradingPreset, treePreset };
254
367
 
255
368
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/commands/index.ts"],"sourcesContent":["import type { Ports } from \"../kernel.js\";\nimport type { KeyboardBinding } from \"../ports/keyboard.js\";\nimport { createKeys, keyIdentity } from \"./keys.js\";\nimport { activeScopes, scopeHasFocus } from \"./scopes.js\";\nimport type { Scope } from \"./scopes.js\";\nimport type {\n Command,\n CommandContext,\n Commands,\n KeymapEntry,\n KeysOptions,\n PendingConfirm,\n} from \"./types.js\";\n\nexport type {\n Command,\n CommandContext,\n CommandId,\n Commands,\n CommandScopeOptions,\n KeymapEntry,\n KeyOverrideId,\n KeyOverrides,\n KeysOptions,\n PendingConfirm,\n Preset,\n Register,\n ValidatedKeyOverrides,\n} from \"./types.js\";\nexport { tradingPreset } from \"./trading-preset.js\";\n\nlet nextConfirmScope = 0;\n\n/** Only port interfaces enter this owner; bare and default kernels share the same layer. */\nexport function createCommands({\n ports: { keyboard, store, reporter },\n keys: options = {},\n}: {\n ports: Pick<Ports, \"keyboard\" | \"store\" | \"reporter\">;\n keys?: KeysOptions;\n}): Commands {\n const keys = createKeys({ keyboard, reporter, options });\n const keymap = store.create<readonly KeymapEntry[]>([]);\n const confirm = store.create<PendingConfirm | null>(null);\n const scopes = new Map<string, Scope>();\n const confirmScope = `densedash:confirm:${nextConfirmScope++}`;\n let pending: {\n scope: Scope;\n command: Command;\n context: CommandContext;\n release: () => void;\n } | null = null;\n let disposed = false;\n\n function publishKeymap(): void {\n const entries: KeymapEntry[] = [];\n const claimed = new Set<string>();\n for (const scope of pending ? [] : activeScopes(scopes).filter(scopeHasFocus)) {\n for (const command of scope.commands) {\n const available = keys.get(command.id).filter((key) => {\n const identity = keyIdentity(key);\n if (claimed.has(identity)) return false;\n claimed.add(identity);\n return true;\n });\n if (command.hidden || (keys.get(command.id).length > 0 && available.length === 0)) continue;\n entries.push({\n command: command.id,\n label: command.label,\n scope: scope.name,\n ...(command.group === undefined ? {} : { group: command.group }),\n keys: available.map((key) => keyboard.format(key)),\n });\n }\n }\n if (pending) {\n entries.push({\n command: \"confirm\",\n label: \"Confirm\",\n scope: confirmScope,\n keys: keys.get(\"confirm\").map((key) => keyboard.format(key)),\n });\n entries.push({\n command: \"cancel\",\n label: \"Cancel\",\n scope: confirmScope,\n keys: keys.get(\"cancel\").map((key) => keyboard.format(key)),\n });\n }\n keymap.set(entries);\n }\n\n function cancel(): void {\n if (!pending) return;\n const captured = pending;\n pending = null;\n captured.release();\n store.batch(() => {\n confirm.set(null);\n publishKeymap();\n });\n }\n\n function run(command: Command, context: CommandContext): void {\n try {\n if (command.enabled?.() === false) return;\n const result = reporter.trace\n ? reporter.trace(command.id, () => command.run(context))\n : command.run(context);\n if (result !== undefined) {\n void Promise.resolve(result).catch((cause: unknown) =>\n reporter.error(cause, { source: \"command\", id: command.id }),\n );\n }\n } catch (cause) {\n reporter.error(cause, { source: \"command\", id: command.id });\n }\n }\n\n function accept(): void {\n const captured = pending;\n if (!captured) return;\n cancel();\n if (scopes.get(captured.scope.name) === captured.scope && !disposed) {\n run(captured.command, captured.context);\n }\n }\n\n function invokeOwned(scope: Scope, command: Command, context: CommandContext): void {\n if (disposed || pending || scopes.get(scope.name) !== scope) return;\n try {\n if (!command.confirm) {\n run(command, context);\n return;\n }\n if (command.enabled?.() === false) return;\n const prompt = command.confirm.prompt;\n const bindings: KeyboardBinding[] = [];\n const release = keyboard.activateScope(confirmScope, { exclusive: true });\n const releaseConfirm = () => {\n for (const binding of bindings) binding.dispose();\n release();\n };\n try {\n for (const key of keys.get(\"confirm\")) {\n bindings.push(\n keyboard.bind(\n key,\n (event) => {\n if (!event.repeat) accept();\n },\n { scope: confirmScope, ignoreInputs: false },\n ),\n );\n }\n for (const key of keys.get(\"cancel\")) {\n bindings.push(keyboard.bind(key, cancel, { scope: confirmScope, ignoreInputs: false }));\n }\n } catch (cause) {\n releaseConfirm();\n throw cause;\n }\n pending = {\n scope,\n command,\n context,\n release: releaseConfirm,\n };\n store.batch(() => {\n confirm.set({ command: command.id, label: command.label, prompt, scope: scope.name });\n publishKeymap();\n });\n } catch (cause) {\n reporter.error(cause, { source: \"command\", id: command.id });\n }\n }\n\n function bindScope(scope: Scope): void {\n for (const binding of scope.bindings) binding.dispose();\n scope.bindings.length = 0;\n const owners = new Map<string, Command>();\n for (const command of scope.commands) {\n for (const key of keys.get(command.id)) {\n const identity = keyIdentity(key);\n const owner = owners.get(identity);\n if (owner) {\n const warning = JSON.stringify([identity, owner.id, command.id]);\n if (owner.id !== command.id && !scope.warned.has(warning)) {\n scope.warned.add(warning);\n reporter.warn(\n `Commands \"${owner.id}\" and \"${command.id}\" share ${keyboard.format(key)} in scope \"${scope.name}\"; \"${owner.id}\" keeps the key.`,\n );\n }\n continue;\n }\n owners.set(identity, command);\n const { parent: _parent, only: _only, ...bindingOptions } = scope.options;\n scope.bindings.push(\n keyboard.bind(\n key,\n (event) => {\n if (!event.repeat || !command.confirm) invokeOwned(scope, command, { event });\n },\n { ...bindingOptions, scope: scope.name },\n ),\n );\n }\n }\n }\n\n return {\n register(name, commands, scopeOptions = {}) {\n if (disposed) throw new Error(\"Commands have been disposed.\");\n if (name === \"global\" && scopeOptions.only)\n throw new Error(\"The global scope cannot be exclusive. Use a named layer.\");\n if (scopes.has(name) || name === confirmScope)\n throw new Error(`Command scope \"${name}\" already has an owner.`);\n const release = keyboard.activateScope(name, {\n ...(scopeOptions.parent === undefined ? {} : { parent: scopeOptions.parent }),\n exclusive: scopeOptions.only ?? false,\n });\n const target = scopeOptions.target;\n const focusDocument = target && \"ownerDocument\" in target ? target.ownerDocument : undefined;\n const onFocus = () => publishKeymap();\n focusDocument?.addEventListener(\"focusin\", onFocus, true);\n focusDocument?.addEventListener(\"focusout\", onFocus, true);\n const scope: Scope = {\n name,\n commands: [...commands],\n options: { ...scopeOptions },\n release() {\n focusDocument?.removeEventListener(\"focusin\", onFocus, true);\n focusDocument?.removeEventListener(\"focusout\", onFocus, true);\n release();\n },\n bindings: [],\n warned: new Set(),\n unregister() {\n if (scopes.get(name) !== scope) return;\n scopes.delete(name);\n for (const binding of scope.bindings) binding.dispose();\n scope.release();\n if (pending?.scope === scope) cancel();\n else publishKeymap();\n },\n };\n scopes.set(name, scope);\n try {\n bindScope(scope);\n publishKeymap();\n } catch (cause) {\n scope.unregister();\n throw cause;\n }\n return scope.unregister;\n },\n invoke(id, context = {}) {\n if (disposed || pending) return;\n for (const scope of activeScopes(scopes)) {\n if (context.scope !== undefined && context.scope !== scope.name) continue;\n if (!scopeHasFocus(scope)) continue;\n const command = scope.commands.find((candidate) => candidate.id === id);\n if (command) {\n invokeOwned(scope, command, context);\n return;\n }\n }\n },\n keymap: { get: () => keymap.get(), subscribe: (listener) => keymap.subscribe(listener) },\n confirm: { get: () => confirm.get(), subscribe: (listener) => confirm.subscribe(listener) },\n keys: (id) => keys.get(id),\n accept,\n cancel,\n setOverrides(overrides) {\n if (disposed) return;\n const validated = keys.replace(overrides);\n store.batch(() => {\n cancel();\n for (const scope of scopes.values()) bindScope(scope);\n publishKeymap();\n });\n options.onChange?.(validated);\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n store.batch(() => {\n cancel();\n for (const scope of scopes.values()) scope.unregister();\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/commands/index.ts"],"sourcesContent":["import type { Ports } from \"../kernel.js\";\nimport type { KeyboardBinding } from \"../ports/keyboard.js\";\nimport { createKeys, keyIdentity } from \"./keys.js\";\nimport { activeScopes, keymapScopes, scopeHasFocus } from \"./scopes.js\";\nimport type { Scope } from \"./scopes.js\";\nimport type {\n Command,\n CommandContext,\n Commands,\n KeymapEntry,\n KeysOptions,\n PendingConfirm,\n} from \"./types.js\";\n\nexport type {\n Command,\n CommandContext,\n CommandId,\n Commands,\n CommandScopeOptions,\n KeymapEntry,\n KeyLayer,\n KeyOverrideId,\n KeyOverrides,\n KeysOptions,\n PendingConfirm,\n Preset,\n Register,\n ValidatedKeyOverrides,\n} from \"./types.js\";\nexport { shiftedTreePreset, tradingPreset, treePreset } from \"./trading-preset.js\";\n\nlet nextConfirmScope = 0;\n\n/** Only port interfaces enter this owner; bare and default kernels share the same layer. */\nexport function createCommands({\n ports: { keyboard, store, reporter },\n keys: options = {},\n}: {\n ports: Pick<Ports, \"keyboard\" | \"store\" | \"reporter\">;\n keys?: KeysOptions;\n}): Commands {\n const keys = createKeys({ keyboard, reporter, options });\n const keymap = store.create<readonly KeymapEntry[]>([]);\n const confirm = store.create<PendingConfirm | null>(null);\n const scopes = new Map<string, Scope>();\n const confirmScope = `densedash:confirm:${nextConfirmScope++}`;\n let pending: {\n scope: Scope;\n command: Command;\n context: CommandContext;\n release: () => void;\n } | null = null;\n let disposed = false;\n\n function commandEnabled(command: Command): boolean {\n try {\n return command.enabled?.() !== false;\n } catch (cause) {\n reporter.error(cause, { source: \"command\", id: command.id });\n return false;\n }\n }\n\n function validateLayers(): void {\n const owners = new Map<string, Scope>();\n for (const scope of scopes.values()) {\n if (!scope.options.keys) continue;\n for (const command of scope.commands) {\n for (const key of scope.keys.get(command.id)) {\n const identity = keyIdentity(key);\n const owner = owners.get(identity);\n if (owner && owner !== scope) {\n throw new Error(\n `Key layers \"${owner.options.keys?.label}\" (${owner.name}) and \"${scope.options.keys.label}\" (${scope.name}) share ${keyboard.format(key)}.`,\n );\n }\n owners.set(identity, scope);\n }\n }\n }\n }\n\n function publishKeymap(keysChanged = false): void {\n // Reserve layer keys before element-target handlers can consume them on the way up.\n // Only the keyboard port matches events and filters editable targets.\n const layered = new Set<string>();\n for (const scope of activeScopes(scopes)) {\n if (!scope.options.keys) continue;\n for (const owned of scope.bindings) {\n if (commandEnabled(scope.commands[owned.commandIndex]!)) layered.add(owned.key);\n }\n }\n for (const scope of scopes.values()) {\n for (const owned of scope.bindings) {\n const enabled =\n commandEnabled(scope.commands[owned.commandIndex]!) &&\n (scope.options.keys !== undefined || !layered.has(owned.key));\n if (enabled === owned.enabled) continue;\n owned.enabled = enabled;\n owned.binding.update({ enabled });\n }\n }\n const entries: KeymapEntry[] = [];\n const claimed = new Map<string, Scope>();\n for (const scope of pending ? [] : keymapScopes(scopes)) {\n // Duplicate keys keep their first owner within a scope, even when disabled.\n const reserved = new Set<string>();\n for (const command of scope.commands) {\n const enabled = commandEnabled(command);\n const available = scope.keys.get(command.id).filter((key) => {\n const identity = keyIdentity(key);\n if (reserved.has(identity)) return false;\n reserved.add(identity);\n if (claimed.has(identity)) return false;\n const first = typeof key === \"string\" ? undefined : key[0];\n const prefixOwner = first === undefined ? undefined : claimed.get(keyIdentity(first));\n const prefixTarget = prefixOwner?.options.keys ? undefined : prefixOwner?.options.target;\n // A widget chord stops the sequence prefix before it reaches an outer target.\n if (\n prefixTarget &&\n \"ownerDocument\" in prefixTarget &&\n prefixTarget.ownerDocument &&\n prefixTarget !== scope.options.target\n )\n return false;\n if (enabled) claimed.set(identity, scope);\n return true;\n });\n if (command.hidden || (scope.keys.get(command.id).length > 0 && available.length === 0))\n continue;\n entries.push({\n command: command.id,\n label: command.label,\n scope: scope.name,\n ...(scope.options.keys ? { layer: scope.options.keys.label } : {}),\n ...(command.group === undefined ? {} : { group: command.group }),\n ...(command.groupLabel === undefined ? {} : { groupLabel: command.groupLabel }),\n ...(command.legend === undefined ? {} : { legend: command.legend }),\n keys: available.map((key) => keyboard.format(key)),\n enabled,\n });\n }\n }\n if (pending) {\n entries.push({\n command: \"confirm\",\n label: \"Confirm\",\n scope: confirmScope,\n keys: keys.get(\"confirm\").map((key) => keyboard.format(key)),\n enabled: true,\n });\n entries.push({\n command: \"cancel\",\n label: \"Cancel\",\n scope: confirmScope,\n keys: keys.get(\"cancel\").map((key) => keyboard.format(key)),\n enabled: true,\n });\n }\n const previous = keymap.get();\n const equal =\n previous.length === entries.length &&\n entries.every((entry, index) => {\n const before = previous[index];\n return (\n before !== undefined &&\n before.command === entry.command &&\n before.label === entry.label &&\n before.group === entry.group &&\n before.groupLabel === entry.groupLabel &&\n before.legend === entry.legend &&\n before.scope === entry.scope &&\n before.layer === entry.layer &&\n before.enabled === entry.enabled &&\n before.keys.length === entry.keys.length &&\n before.keys.every((key, keyIndex) => key === entry.keys[keyIndex])\n );\n });\n // keys() consumers also use this store to follow overrides outside focused scopes.\n if (keysChanged || !equal) keymap.set(entries);\n }\n\n function cancel(): void {\n if (!pending) return;\n const captured = pending;\n pending = null;\n captured.release();\n store.batch(() => {\n confirm.set(null);\n publishKeymap();\n });\n }\n\n function run(command: Command, context: CommandContext): void {\n try {\n if (!commandEnabled(command)) return;\n const result = reporter.trace\n ? reporter.trace(command.id, () => command.run(context))\n : command.run(context);\n if (result !== undefined) {\n void Promise.resolve(result).catch((cause: unknown) =>\n reporter.error(cause, { source: \"command\", id: command.id }),\n );\n }\n } catch (cause) {\n reporter.error(cause, { source: \"command\", id: command.id });\n }\n }\n\n function accept(): void {\n const captured = pending;\n if (!captured) return;\n cancel();\n if (scopes.get(captured.scope.name) === captured.scope && !disposed) {\n run(captured.command, captured.context);\n }\n }\n\n function invokeOwned(scope: Scope, command: Command, context: CommandContext): void {\n if (disposed || pending || scopes.get(scope.name) !== scope) return;\n try {\n if (!command.confirm) {\n run(command, context);\n return;\n }\n if (!commandEnabled(command)) return;\n const prompt = command.confirm.prompt;\n const bindings: KeyboardBinding[] = [];\n const release = keyboard.activateScope(confirmScope, { exclusive: true });\n const releaseConfirm = () => {\n for (const binding of bindings) binding.dispose();\n release();\n };\n try {\n for (const key of keys.get(\"confirm\")) {\n bindings.push(\n keyboard.bind(\n key,\n (event) => {\n if (!event.repeat) accept();\n },\n { scope: confirmScope, ignoreInputs: false },\n ),\n );\n }\n for (const key of keys.get(\"cancel\")) {\n bindings.push(keyboard.bind(key, cancel, { scope: confirmScope, ignoreInputs: false }));\n }\n } catch (cause) {\n releaseConfirm();\n throw cause;\n }\n pending = {\n scope,\n command,\n context,\n release: releaseConfirm,\n };\n store.batch(() => {\n confirm.set({ command: command.id, label: command.label, prompt, scope: scope.name });\n publishKeymap();\n });\n } catch (cause) {\n reporter.error(cause, { source: \"command\", id: command.id });\n }\n }\n\n function bindScope(scope: Scope): void {\n for (const { binding } of scope.bindings) binding.dispose();\n scope.bindings.length = 0;\n const owners = new Map<string, Command>();\n for (const [index, command] of scope.commands.entries()) {\n for (const key of scope.keys.get(command.id)) {\n const identity = keyIdentity(key);\n const owner = owners.get(identity);\n if (owner) {\n const warning = JSON.stringify([identity, owner.id, command.id]);\n if (owner.id !== command.id && !scope.warned.has(warning)) {\n scope.warned.add(warning);\n reporter.warn(\n `Commands \"${owner.id}\" and \"${command.id}\" share ${keyboard.format(key)} in scope \"${scope.name}\"; \"${owner.id}\" keeps the key.`,\n );\n }\n continue;\n }\n owners.set(identity, command);\n const { parent: _parent, only: _only, keys: layer, ...bindingOptions } = scope.options;\n const target = bindingOptions.target;\n const ambientTarget = target && \"ownerDocument\" in target ? target.ownerDocument : target;\n const enabled = commandEnabled(command);\n scope.bindings.push({\n key: identity,\n commandIndex: index,\n enabled,\n binding: keyboard.bind(\n key,\n (event) => {\n const current = scope.commands[index];\n if (current && (!event.repeat || !current.confirm))\n invokeOwned(scope, current, { event });\n },\n {\n ...bindingOptions,\n // Commands own their declared keys across non-editable widgets. The port's\n // automatic ARIA filtering otherwise reserves every printable tree key.\n ignoreInputs: bindingOptions.ignoreInputs ?? \"editable\",\n ...(layer\n ? {\n ...(ambientTarget ? { target: ambientTarget } : {}),\n ignoreInputs: \"editable\" as const,\n }\n : {}),\n scope: scope.name,\n enabled,\n },\n ),\n });\n }\n }\n }\n\n return {\n register(name, commands, scopeOptions = {}) {\n if (disposed) throw new Error(\"Commands have been disposed.\");\n if (name === \"global\" && scopeOptions.only)\n throw new Error(\"The global scope cannot be exclusive. Use a named layer.\");\n if (scopes.has(name) || name === confirmScope)\n throw new Error(`Command scope \"${name}\" already has an owner.`);\n const scopeKeys = scopeOptions.keys\n ? createKeys({ keyboard, reporter, options: scopeOptions.keys, fallback: keys.get })\n : keys;\n const release = keyboard.activateScope(name, {\n ...(scopeOptions.parent === undefined ? {} : { parent: scopeOptions.parent }),\n exclusive: scopeOptions.only ?? false,\n });\n const target = scopeOptions.target;\n const focusDocument =\n target && \"document\" in target\n ? target.document\n : target && \"defaultView\" in target\n ? target\n : (target?.ownerDocument ?? globalThis.document);\n const onFocus = () => {\n publishKeymap();\n // During focusout activeElement still names the departing control.\n queueMicrotask(() => {\n if (scopes.get(name) === scope) publishKeymap();\n });\n };\n focusDocument?.addEventListener(\"focusin\", onFocus, true);\n focusDocument?.addEventListener(\"focusout\", onFocus, true);\n const scope: Scope = {\n name,\n keys: scopeKeys,\n commands: [...commands],\n options: { ...scopeOptions },\n release() {\n focusDocument?.removeEventListener(\"focusin\", onFocus, true);\n focusDocument?.removeEventListener(\"focusout\", onFocus, true);\n release();\n },\n bindings: [],\n warned: new Set(),\n unregister() {\n if (scopes.get(name) !== scope) return;\n scopes.delete(name);\n for (const { binding } of scope.bindings) binding.dispose();\n scope.release();\n if (pending?.scope === scope) cancel();\n else publishKeymap();\n },\n };\n scopes.set(name, scope);\n try {\n validateLayers();\n bindScope(scope);\n publishKeymap();\n } catch (cause) {\n scope.unregister();\n throw cause;\n }\n return scope.unregister;\n },\n update(name, commands) {\n if (disposed) return;\n const scope = scopes.get(name);\n if (!scope) throw new Error(`Command scope \"${name}\" has no owner.`);\n if (\n commands.length !== scope.commands.length ||\n commands.some((command, index) => command.id !== scope.commands[index]?.id)\n ) {\n throw new Error(`Command scope \"${name}\" updates must preserve ordered command ids.`);\n }\n scope.commands = [...commands];\n store.batch(() => {\n if (pending?.scope === scope) {\n const command = commands.find((candidate) => candidate.id === pending?.command.id);\n const current = confirm.get();\n if (command && current) {\n pending.command = command;\n const prompt = command.confirm?.prompt ?? current.prompt;\n if (current.label !== command.label || current.prompt !== prompt) {\n confirm.set({ ...current, label: command.label, prompt });\n }\n }\n }\n publishKeymap();\n });\n },\n invoke(id, context = {}) {\n if (disposed || pending) return;\n for (const scope of activeScopes(scopes)) {\n if (context.scope !== undefined && context.scope !== scope.name) continue;\n if (!scopeHasFocus(scope)) continue;\n const command = scope.commands.find((candidate) => candidate.id === id);\n if (command) {\n invokeOwned(scope, command, context);\n return;\n }\n }\n },\n keymap: { get: () => keymap.get(), subscribe: (listener) => keymap.subscribe(listener) },\n confirm: { get: () => confirm.get(), subscribe: (listener) => confirm.subscribe(listener) },\n keys: (id, scope) =>\n scope === undefined ? keys.get(id) : (scopes.get(scope)?.keys.get(id) ?? []),\n accept,\n cancel,\n setOverrides(overrides) {\n if (disposed) return;\n const previous = keys.overrides();\n const validated = keys.replace(overrides);\n try {\n validateLayers();\n } catch (cause) {\n keys.replace(previous);\n throw cause;\n }\n store.batch(() => {\n cancel();\n for (const scope of scopes.values()) bindScope(scope);\n publishKeymap(true);\n });\n options.onChange?.(validated);\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n store.batch(() => {\n cancel();\n for (const scope of scopes.values()) scope.unregister();\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -4,13 +4,13 @@ import { tradingPreset } from "./trading-preset.js";
4
4
  function keyIdentity(hotkey) {
5
5
  return JSON.stringify(typeof hotkey === "string" ? [hotkey] : hotkey);
6
6
  }
7
- function createKeys({ keyboard, reporter, options }) {
7
+ function createKeys({ keyboard, reporter, options, fallback }) {
8
8
  const preset = /* @__PURE__ */ new Map();
9
9
  const configured = options.preset ? {
10
10
  confirm: tradingPreset.confirm,
11
11
  cancel: tradingPreset.cancel,
12
12
  ...options.preset
13
- } : tradingPreset;
13
+ } : fallback ? {} : tradingPreset;
14
14
  for (const [id, keys] of Object.entries(configured)) {
15
15
  const normalized = [];
16
16
  for (const key of keys) {
@@ -43,7 +43,8 @@ function createKeys({ keyboard, reporter, options }) {
43
43
  }
44
44
  replace(options.overrides ?? {});
45
45
  return {
46
- get: (id) => overrides.get(id) ?? preset.get(id) ?? [],
46
+ get: (id) => overrides.get(id) ?? preset.get(id) ?? (options.preset ? void 0 : fallback?.(id)) ?? [],
47
+ overrides: () => Object.fromEntries(overrides),
47
48
  replace
48
49
  };
49
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"keys.js","names":[],"sources":["../../src/commands/keys.ts"],"sourcesContent":["import type { KeyboardHotkey, KeyboardPort } from \"../ports/keyboard.js\";\nimport type { ReporterPort } from \"../ports/reporter.js\";\nimport { tradingPreset } from \"./trading-preset.js\";\nimport type { KeyOverrides, KeysOptions, Preset, ValidatedKeyOverrides } from \"./types.js\";\n\n/** parse() supplies canonical identities for aliases as well as validating saved keys. */\nexport function keyIdentity(hotkey: KeyboardHotkey): string {\n return JSON.stringify(typeof hotkey === \"string\" ? [hotkey] : hotkey);\n}\n\nexport function createKeys({\n keyboard,\n reporter,\n options,\n}: {\n keyboard: KeyboardPort;\n reporter: ReporterPort;\n options: KeysOptions;\n}) {\n const preset = new Map<string, readonly KeyboardHotkey[]>();\n const configured: Preset = options.preset\n ? { confirm: tradingPreset.confirm, cancel: tradingPreset.cancel, ...options.preset }\n : tradingPreset;\n for (const [id, keys] of Object.entries(configured)) {\n const normalized: KeyboardHotkey[] = [];\n for (const key of keys) {\n const parsed = keyboard.parse(key);\n if (!parsed.ok) throw new Error(`Invalid preset key for \"${id}\": ${parsed.reason}`);\n normalized.push(parsed.hotkey);\n }\n preset.set(id, normalized);\n }\n let overrides = new Map<string, readonly KeyboardHotkey[]>();\n\n function replace(input: KeyOverrides): ValidatedKeyOverrides {\n const next = new Map<string, readonly KeyboardHotkey[]>();\n for (const [id, value] of Object.entries(input)) {\n const normalized: KeyboardHotkey[] = [];\n let reason: string | undefined;\n if (!Array.isArray(value)) {\n reason = \"Expected an array of hotkeys.\";\n } else {\n for (const key of value) {\n const parsed = keyboard.parse(key);\n if (!parsed.ok) {\n reason = parsed.reason;\n break;\n }\n normalized.push(parsed.hotkey);\n }\n }\n if (reason !== undefined) {\n reporter.warn(`Invalid key override for \"${id}\": ${reason} Keeping preset keys.`);\n } else {\n next.set(id, normalized);\n }\n }\n overrides = next;\n return Object.fromEntries(next);\n }\n\n replace(options.overrides ?? {});\n return {\n get: (id: string): readonly KeyboardHotkey[] => overrides.get(id) ?? preset.get(id) ?? [],\n replace,\n };\n}\n"],"mappings":";;;AAMA,SAAgB,YAAY,QAAgC;CAC1D,OAAO,KAAK,UAAU,OAAO,WAAW,WAAW,CAAC,MAAM,IAAI,MAAM;AACtE;AAEA,SAAgB,WAAW,EACzB,UACA,UACA,WAKC;CACD,MAAM,yBAAS,IAAI,IAAuC;CAC1D,MAAM,aAAqB,QAAQ,SAC/B;EAAE,SAAS,cAAc;EAAS,QAAQ,cAAc;EAAQ,GAAG,QAAQ;CAAO,IAClF;CACJ,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,UAAU,GAAG;EACnD,MAAM,aAA+B,CAAC;EACtC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,SAAS,SAAS,MAAM,GAAG;GACjC,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,MAAM,2BAA2B,GAAG,KAAK,OAAO,QAAQ;GAClF,WAAW,KAAK,OAAO,MAAM;EAC/B;EACA,OAAO,IAAI,IAAI,UAAU;CAC3B;CACA,IAAI,4BAAY,IAAI,IAAuC;CAE3D,SAAS,QAAQ,OAA4C;EAC3D,MAAM,uBAAO,IAAI,IAAuC;EACxD,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,KAAK,GAAG;GAC/C,MAAM,aAA+B,CAAC;GACtC,IAAI;GACJ,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,SAAS;QAET,KAAK,MAAM,OAAO,OAAO;IACvB,MAAM,SAAS,SAAS,MAAM,GAAG;IACjC,IAAI,CAAC,OAAO,IAAI;KACd,SAAS,OAAO;KAChB;IACF;IACA,WAAW,KAAK,OAAO,MAAM;GAC/B;GAEF,IAAI,WAAW,KAAA,GACb,SAAS,KAAK,6BAA6B,GAAG,KAAK,OAAO,sBAAsB;QAEhF,KAAK,IAAI,IAAI,UAAU;EAE3B;EACA,YAAY;EACZ,OAAO,OAAO,YAAY,IAAI;CAChC;CAEA,QAAQ,QAAQ,aAAa,CAAC,CAAC;CAC/B,OAAO;EACL,MAAM,OAA0C,UAAU,IAAI,EAAE,KAAK,OAAO,IAAI,EAAE,KAAK,CAAC;EACxF;CACF;AACF"}
1
+ {"version":3,"file":"keys.js","names":[],"sources":["../../src/commands/keys.ts"],"sourcesContent":["import type { KeyboardHotkey, KeyboardPort } from \"../ports/keyboard.js\";\nimport type { ReporterPort } from \"../ports/reporter.js\";\nimport { tradingPreset } from \"./trading-preset.js\";\nimport type { KeyOverrides, KeysOptions, Preset, ValidatedKeyOverrides } from \"./types.js\";\n\n/** parse() supplies canonical identities for aliases as well as validating saved keys. */\nexport function keyIdentity(hotkey: KeyboardHotkey): string {\n return JSON.stringify(typeof hotkey === \"string\" ? [hotkey] : hotkey);\n}\n\nexport function createKeys({\n keyboard,\n reporter,\n options,\n fallback,\n}: {\n keyboard: KeyboardPort;\n reporter: ReporterPort;\n options: KeysOptions;\n fallback?: (id: string) => readonly KeyboardHotkey[];\n}) {\n const preset = new Map<string, readonly KeyboardHotkey[]>();\n const configured: Preset = options.preset\n ? { confirm: tradingPreset.confirm, cancel: tradingPreset.cancel, ...options.preset }\n : fallback\n ? {}\n : tradingPreset;\n for (const [id, keys] of Object.entries(configured)) {\n const normalized: KeyboardHotkey[] = [];\n for (const key of keys) {\n const parsed = keyboard.parse(key);\n if (!parsed.ok) throw new Error(`Invalid preset key for \"${id}\": ${parsed.reason}`);\n normalized.push(parsed.hotkey);\n }\n preset.set(id, normalized);\n }\n let overrides = new Map<string, readonly KeyboardHotkey[]>();\n\n function replace(input: KeyOverrides): ValidatedKeyOverrides {\n const next = new Map<string, readonly KeyboardHotkey[]>();\n for (const [id, value] of Object.entries(input)) {\n const normalized: KeyboardHotkey[] = [];\n let reason: string | undefined;\n if (!Array.isArray(value)) {\n reason = \"Expected an array of hotkeys.\";\n } else {\n for (const key of value) {\n const parsed = keyboard.parse(key);\n if (!parsed.ok) {\n reason = parsed.reason;\n break;\n }\n normalized.push(parsed.hotkey);\n }\n }\n if (reason !== undefined) {\n reporter.warn(`Invalid key override for \"${id}\": ${reason} Keeping preset keys.`);\n } else {\n next.set(id, normalized);\n }\n }\n overrides = next;\n return Object.fromEntries(next);\n }\n\n replace(options.overrides ?? {});\n return {\n get: (id: string): readonly KeyboardHotkey[] =>\n overrides.get(id) ?? preset.get(id) ?? (options.preset ? undefined : fallback?.(id)) ?? [],\n overrides: (): ValidatedKeyOverrides => Object.fromEntries(overrides),\n replace,\n };\n}\n"],"mappings":";;;AAMA,SAAgB,YAAY,QAAgC;CAC1D,OAAO,KAAK,UAAU,OAAO,WAAW,WAAW,CAAC,MAAM,IAAI,MAAM;AACtE;AAEA,SAAgB,WAAW,EACzB,UACA,UACA,SACA,YAMC;CACD,MAAM,yBAAS,IAAI,IAAuC;CAC1D,MAAM,aAAqB,QAAQ,SAC/B;EAAE,SAAS,cAAc;EAAS,QAAQ,cAAc;EAAQ,GAAG,QAAQ;CAAO,IAClF,WACE,CAAC,IACD;CACN,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,UAAU,GAAG;EACnD,MAAM,aAA+B,CAAC;EACtC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,SAAS,SAAS,MAAM,GAAG;GACjC,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,MAAM,2BAA2B,GAAG,KAAK,OAAO,QAAQ;GAClF,WAAW,KAAK,OAAO,MAAM;EAC/B;EACA,OAAO,IAAI,IAAI,UAAU;CAC3B;CACA,IAAI,4BAAY,IAAI,IAAuC;CAE3D,SAAS,QAAQ,OAA4C;EAC3D,MAAM,uBAAO,IAAI,IAAuC;EACxD,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,KAAK,GAAG;GAC/C,MAAM,aAA+B,CAAC;GACtC,IAAI;GACJ,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,SAAS;QAET,KAAK,MAAM,OAAO,OAAO;IACvB,MAAM,SAAS,SAAS,MAAM,GAAG;IACjC,IAAI,CAAC,OAAO,IAAI;KACd,SAAS,OAAO;KAChB;IACF;IACA,WAAW,KAAK,OAAO,MAAM;GAC/B;GAEF,IAAI,WAAW,KAAA,GACb,SAAS,KAAK,6BAA6B,GAAG,KAAK,OAAO,sBAAsB;QAEhF,KAAK,IAAI,IAAI,UAAU;EAE3B;EACA,YAAY;EACZ,OAAO,OAAO,YAAY,IAAI;CAChC;CAEA,QAAQ,QAAQ,aAAa,CAAC,CAAC;CAC/B,OAAO;EACL,MAAM,OACJ,UAAU,IAAI,EAAE,KAAK,OAAO,IAAI,EAAE,MAAM,QAAQ,SAAS,KAAA,IAAY,WAAW,EAAE,MAAM,CAAC;EAC3F,iBAAwC,OAAO,YAAY,SAAS;EACpE;CACF;AACF"}