@joewinke/jatui 0.22.0 → 0.24.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 (71) hide show
  1. package/CLAUDE.md +72 -2
  2. package/dist/actions/reveal.d.ts +37 -0
  3. package/dist/actions/reveal.js +42 -0
  4. package/dist/components/AudioRecorder.svelte +529 -0
  5. package/dist/components/AudioRecorder.svelte.d.ts +30 -0
  6. package/dist/components/BulkActionBar.svelte +40 -0
  7. package/dist/components/BulkActionBar.svelte.d.ts +9 -0
  8. package/dist/components/ConfirmDialog.svelte +39 -35
  9. package/dist/components/CopyButton.svelte +5 -2
  10. package/dist/components/FloatingActionBar.svelte +55 -1
  11. package/dist/components/FloatingActionBar.svelte.d.ts +8 -1
  12. package/dist/components/IconSelector.svelte +417 -0
  13. package/dist/components/IconSelector.svelte.d.ts +55 -0
  14. package/dist/components/InfiniteGrid.svelte +305 -0
  15. package/dist/components/InfiniteGrid.svelte.d.ts +71 -0
  16. package/dist/components/InfiniteSearchGrid.svelte +131 -0
  17. package/dist/components/InfiniteSearchGrid.svelte.d.ts +67 -0
  18. package/dist/components/InputDialog.svelte +49 -38
  19. package/dist/components/KeyboardShortcutsOverlay.svelte +94 -3
  20. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +61 -16
  21. package/dist/components/MediaCaptureIcon.svelte +23 -0
  22. package/dist/components/MediaCaptureIcon.svelte.d.ts +1 -1
  23. package/dist/components/ThumbnailGrid.svelte +137 -0
  24. package/dist/components/ThumbnailGrid.svelte.d.ts +42 -0
  25. package/dist/components/batch/BatchJobTable.svelte +549 -0
  26. package/dist/components/batch/BatchJobTable.svelte.d.ts +114 -0
  27. package/dist/components/batch/QuotaMeter.svelte +78 -0
  28. package/dist/components/batch/QuotaMeter.svelte.d.ts +16 -0
  29. package/dist/components/batch/batchQueue.d.ts +56 -0
  30. package/dist/components/batch/batchQueue.js +81 -0
  31. package/dist/components/batch/elapsedEstimate.d.ts +24 -0
  32. package/dist/components/batch/elapsedEstimate.js +25 -0
  33. package/dist/components/batch/pollOperation.d.ts +45 -0
  34. package/dist/components/batch/pollOperation.js +80 -0
  35. package/dist/components/batch/useElapsedEstimate.d.ts +10 -0
  36. package/dist/components/batch/useElapsedEstimate.js +38 -0
  37. package/dist/components/bulkActionBarKeys.d.ts +40 -0
  38. package/dist/components/bulkActionBarKeys.js +38 -0
  39. package/dist/components/infiniteGridLoader.d.ts +70 -0
  40. package/dist/components/infiniteGridLoader.js +141 -0
  41. package/dist/components/linked-columns/LinkedColumns.svelte +10 -22
  42. package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +1 -9
  43. package/dist/components/linked-columns/types.d.ts +9 -0
  44. package/dist/components/linked-columns/types.js +1 -0
  45. package/dist/components/replay/ChapterTimeline.svelte +43 -25
  46. package/dist/components/replay/ChapterTimeline.svelte.d.ts +26 -1
  47. package/dist/components/table/columnResize.d.ts +38 -0
  48. package/dist/components/table/columnResize.js +117 -0
  49. package/dist/components/table/index.d.ts +2 -0
  50. package/dist/components/table/index.js +1 -0
  51. package/dist/components/toast/ToastItem.svelte +47 -2
  52. package/dist/components/variant/VariantMatrixPicker.svelte +181 -0
  53. package/dist/components/variant/VariantMatrixPicker.svelte.d.ts +37 -0
  54. package/dist/components/variant/variantCombinations.d.ts +75 -0
  55. package/dist/components/variant/variantCombinations.js +100 -0
  56. package/dist/index.d.ts +34 -3
  57. package/dist/index.js +30 -1
  58. package/dist/stores/toast.d.ts +8 -0
  59. package/dist/stores/toast.js +2 -0
  60. package/dist/styles/motion.css +100 -0
  61. package/dist/utils/audioRecorder.d.ts +28 -0
  62. package/dist/utils/audioRecorder.js +58 -0
  63. package/dist/utils/audioRecorderCore.d.ts +132 -0
  64. package/dist/utils/audioRecorderCore.js +447 -0
  65. package/dist/utils/clipboard.d.ts +25 -0
  66. package/dist/utils/clipboard.js +59 -0
  67. package/dist/utils/dateFormatters.d.ts +11 -2
  68. package/dist/utils/dateFormatters.js +22 -2
  69. package/dist/utils/keyboardShortcutRegistry.d.ts +152 -0
  70. package/dist/utils/keyboardShortcutRegistry.js +365 -0
  71. package/package.json +1 -1
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Keyboard shortcut registry — the data source `KeyboardShortcutsOverlay`
3
+ * was always missing (jatui-m4uz3.6, harvested from chimaro's
4
+ * keyboardShortcutManager with its app-specific default registrations
5
+ * stripped).
6
+ *
7
+ * One registry per app (or use the lazy `getShortcutRegistry()` singleton).
8
+ * Components register shortcuts against a CONTEXT and get back an
9
+ * unregister function, which is exactly what a Svelte `$effect` wants to
10
+ * return:
11
+ *
12
+ * $effect(() => registry.register([
13
+ * { key: 'j', description: 'Next item', context: 'list', action: next },
14
+ * { key: 'mod+k', description: 'Search', action: openPalette, allowInInput: true },
15
+ * ]));
16
+ *
17
+ * Contexts form a STACK. `global` is always the bottom. `pushContext()`
18
+ * returns a pop function. When a key is pressed the stack is walked from
19
+ * the top down and the first matching, enabled shortcut wins — so a
20
+ * context higher up shadows the same combo lower down. A context pushed
21
+ * with `{ modal: true }` stops the walk: everything beneath it (including
22
+ * `global`) is suppressed while it is on the stack — that is how a dialog
23
+ * keeps page shortcuts from firing behind it.
24
+ *
25
+ * Sharp edges handled here so every app doesn't re-learn them:
26
+ * - Input focus: shortcuts don't fire while typing in a text input,
27
+ * textarea, select or contenteditable (checkboxes/buttons/ranges don't
28
+ * count as typing). Opt back in per shortcut with `allowInInput`.
29
+ * - IME composition and already-handled (`defaultPrevented`) events are
30
+ * ignored.
31
+ * - Modifiers: `mod` is ⌘ on macOS and Ctrl elsewhere. Matching is exact —
32
+ * `mod+k` does not fire on `mod+shift+k`. Shift is implied by printable
33
+ * symbols (`?` is Shift+/ on most layouts), so `key: '?'` matches without
34
+ * listing `shift`. macOS Option rewrites `event.key` (Alt+K → "˚"), so
35
+ * alt combos fall back to `event.code`.
36
+ * - Browser-reserved combos: Ctrl+B / Ctrl+T / Ctrl+W / Ctrl+N belong to the
37
+ * browser (Chrome won't hand some of them to the page at all). The sidebar
38
+ * convention across JAT apps is `mod+\` — see `SIDEBAR_TOGGLE_KEY`.
39
+ *
40
+ * Pure TypeScript, no Svelte runes and no DOM globals at import time, so it
41
+ * is SSR-safe and unit-testable under plain node.
42
+ */
43
+ export type ShortcutModifier = 'mod' | 'meta' | 'ctrl' | 'shift' | 'alt' | 'cmd';
44
+ export type ShortcutPlatform = 'mac' | 'other';
45
+ export interface ShortcutDefinition {
46
+ /**
47
+ * `KeyboardEvent.key` value (case-insensitive for letters), optionally
48
+ * prefixed with modifiers: `'k'`, `'mod+k'`, `'shift+ArrowDown'`, `'?'`,
49
+ * `'mod+\\'`. Use `'+'` / `'mod++'` for the plus key itself.
50
+ */
51
+ key: string;
52
+ /** Extra modifiers, merged with any parsed from `key`. `cmd` is an alias of `mod`. */
53
+ modifiers?: ShortcutModifier | ShortcutModifier[];
54
+ /** Shown in the overlay. */
55
+ description: string;
56
+ action: (event: KeyboardEvent) => void | Promise<void>;
57
+ /** Context this shortcut belongs to. Default `'global'`. */
58
+ context?: string;
59
+ /** Live predicate — a disabled shortcut is skipped and the walk continues. */
60
+ enabled?: () => boolean;
61
+ /** Fire even while focus is in a text field. Default false. */
62
+ allowInInput?: boolean;
63
+ /** Fire on auto-repeat keydowns (held key). Default true. */
64
+ allowRepeat?: boolean;
65
+ /** Call `preventDefault()` when this shortcut fires. Default true. */
66
+ preventDefault?: boolean;
67
+ /** Overlay section title. Defaults to the context's label. */
68
+ group?: string;
69
+ /** Register but leave out of the overlay. */
70
+ hidden?: boolean;
71
+ /** Overrides the computed keycap label in the overlay (e.g. `'j / ↓'`). */
72
+ label?: string;
73
+ }
74
+ export interface PushContextOptions {
75
+ /** Suppress every context beneath this one (including `global`) while pushed. */
76
+ modal?: boolean;
77
+ /** Overlay section title for this context. Default: the context name. */
78
+ label?: string;
79
+ }
80
+ /** Same shape as `KeyboardShortcutsOverlay`'s `ShortcutSection`. */
81
+ export interface RegistrySection {
82
+ title: string;
83
+ shortcuts: {
84
+ key: string;
85
+ description: string;
86
+ }[];
87
+ }
88
+ export interface ToSectionsOptions {
89
+ /**
90
+ * `'active'` (default) lists only contexts that can fire right now —
91
+ * the live part of the stack, top first. `'all'` lists every context
92
+ * with registrations.
93
+ */
94
+ scope?: 'active' | 'all';
95
+ /** Drop shortcuts whose `enabled()` currently returns false. Default false. */
96
+ onlyEnabled?: boolean;
97
+ }
98
+ export interface ShortcutRegistryOptions {
99
+ /** Force the platform (tests, SSR). Default: sniffed from `navigator`. */
100
+ platform?: ShortcutPlatform;
101
+ /** Overlay labels for contexts. `global` defaults to `'General'`. */
102
+ contextLabels?: Record<string, string>;
103
+ /** Called when an action throws or rejects. Default: `console.error`. */
104
+ onError?: (error: unknown, shortcut: ShortcutDefinition) => void;
105
+ }
106
+ /** Minimal event-target shape `attach()` needs (window, document, an element). */
107
+ export interface ShortcutEventTarget {
108
+ addEventListener(type: 'keydown', listener: (e: KeyboardEvent) => void): void;
109
+ removeEventListener(type: 'keydown', listener: (e: KeyboardEvent) => void): void;
110
+ }
111
+ export interface ShortcutRegistry {
112
+ /** Register one or many shortcuts. Returns an unregister function. */
113
+ register(shortcuts: ShortcutDefinition | ShortcutDefinition[]): () => void;
114
+ /** Push a context onto the stack. Returns a pop function (idempotent). */
115
+ pushContext(name: string, options?: PushContextOptions): () => void;
116
+ /** Context names that can currently fire, top of stack first. */
117
+ activeContexts(): string[];
118
+ /** Listen for keydown on `target` (default `window`). Returns a detach function. */
119
+ attach(target?: ShortcutEventTarget): () => void;
120
+ /** Dispatch an event by hand. Returns true when a shortcut fired. */
121
+ handleKeydown(event: KeyboardEvent): boolean;
122
+ /** Shortcuts grouped for `KeyboardShortcutsOverlay`'s `sections` prop. */
123
+ toSections(options?: ToSectionsOptions): RegistrySection[];
124
+ /** Every registration, in registration order. */
125
+ list(): ShortcutDefinition[];
126
+ /** Keycap label for a shortcut on this registry's platform (`⌘K`, `Ctrl+K`). */
127
+ format(shortcut: ShortcutDefinition | string): string;
128
+ /** Notified after every register/unregister/push/pop. Returns unsubscribe. */
129
+ subscribe(listener: () => void): () => void;
130
+ readonly platform: ShortcutPlatform;
131
+ }
132
+ /** The sidebar-toggle convention. Ctrl+B is reserved by Chrome (bookmarks bar). */
133
+ export declare const SIDEBAR_TOGGLE_KEY = "mod+\\";
134
+ interface ParsedCombo {
135
+ key: string;
136
+ mod: boolean;
137
+ meta: boolean;
138
+ ctrl: boolean;
139
+ shift: boolean;
140
+ alt: boolean;
141
+ }
142
+ export declare function detectPlatform(): ShortcutPlatform;
143
+ /** Parse `'mod+shift+k'` (plus any extra modifiers) into a normalized combo. */
144
+ export declare function parseCombo(key: string, extra?: ShortcutModifier | ShortcutModifier[]): ParsedCombo;
145
+ /** True when keystrokes on `target` are text entry (so bare-key shortcuts must not fire). */
146
+ export declare function isTypingTarget(target: EventTarget | null | undefined): boolean;
147
+ /** Keycap label: `⌘⇧K` on macOS, `Ctrl+Shift+K` elsewhere. */
148
+ export declare function formatShortcut(shortcut: ShortcutDefinition | string, platform?: ShortcutPlatform): string;
149
+ export declare function createShortcutRegistry(options?: ShortcutRegistryOptions): ShortcutRegistry;
150
+ /** Lazily-created app-wide registry, for apps that want one without threading it through props. */
151
+ export declare function getShortcutRegistry(): ShortcutRegistry;
152
+ export {};
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Keyboard shortcut registry — the data source `KeyboardShortcutsOverlay`
3
+ * was always missing (jatui-m4uz3.6, harvested from chimaro's
4
+ * keyboardShortcutManager with its app-specific default registrations
5
+ * stripped).
6
+ *
7
+ * One registry per app (or use the lazy `getShortcutRegistry()` singleton).
8
+ * Components register shortcuts against a CONTEXT and get back an
9
+ * unregister function, which is exactly what a Svelte `$effect` wants to
10
+ * return:
11
+ *
12
+ * $effect(() => registry.register([
13
+ * { key: 'j', description: 'Next item', context: 'list', action: next },
14
+ * { key: 'mod+k', description: 'Search', action: openPalette, allowInInput: true },
15
+ * ]));
16
+ *
17
+ * Contexts form a STACK. `global` is always the bottom. `pushContext()`
18
+ * returns a pop function. When a key is pressed the stack is walked from
19
+ * the top down and the first matching, enabled shortcut wins — so a
20
+ * context higher up shadows the same combo lower down. A context pushed
21
+ * with `{ modal: true }` stops the walk: everything beneath it (including
22
+ * `global`) is suppressed while it is on the stack — that is how a dialog
23
+ * keeps page shortcuts from firing behind it.
24
+ *
25
+ * Sharp edges handled here so every app doesn't re-learn them:
26
+ * - Input focus: shortcuts don't fire while typing in a text input,
27
+ * textarea, select or contenteditable (checkboxes/buttons/ranges don't
28
+ * count as typing). Opt back in per shortcut with `allowInInput`.
29
+ * - IME composition and already-handled (`defaultPrevented`) events are
30
+ * ignored.
31
+ * - Modifiers: `mod` is ⌘ on macOS and Ctrl elsewhere. Matching is exact —
32
+ * `mod+k` does not fire on `mod+shift+k`. Shift is implied by printable
33
+ * symbols (`?` is Shift+/ on most layouts), so `key: '?'` matches without
34
+ * listing `shift`. macOS Option rewrites `event.key` (Alt+K → "˚"), so
35
+ * alt combos fall back to `event.code`.
36
+ * - Browser-reserved combos: Ctrl+B / Ctrl+T / Ctrl+W / Ctrl+N belong to the
37
+ * browser (Chrome won't hand some of them to the page at all). The sidebar
38
+ * convention across JAT apps is `mod+\` — see `SIDEBAR_TOGGLE_KEY`.
39
+ *
40
+ * Pure TypeScript, no Svelte runes and no DOM globals at import time, so it
41
+ * is SSR-safe and unit-testable under plain node.
42
+ */
43
+ /** The sidebar-toggle convention. Ctrl+B is reserved by Chrome (bookmarks bar). */
44
+ export const SIDEBAR_TOGGLE_KEY = 'mod+\\';
45
+ const GLOBAL = 'global';
46
+ export function detectPlatform() {
47
+ if (typeof navigator === 'undefined')
48
+ return 'other';
49
+ const nav = navigator;
50
+ const p = nav.userAgentData?.platform || nav.platform || nav.userAgent || '';
51
+ return /mac|iphone|ipad|ipod/i.test(p) ? 'mac' : 'other';
52
+ }
53
+ /** Parse `'mod+shift+k'` (plus any extra modifiers) into a normalized combo. */
54
+ export function parseCombo(key, extra) {
55
+ const combo = { key: '', mod: false, meta: false, ctrl: false, shift: false, alt: false };
56
+ const mods = extra === undefined ? [] : Array.isArray(extra) ? [...extra] : [extra];
57
+ let base = key;
58
+ if (key.length > 1) {
59
+ // `mod++` → modifiers 'mod', key '+'. A lone '+' never splits.
60
+ const parts = key.endsWith('++') ? [...key.slice(0, -2).split('+'), '+'] : key.split('+');
61
+ base = parts.pop() ?? '';
62
+ for (const p of parts) {
63
+ const m = p.trim().toLowerCase();
64
+ if (m === 'mod' || m === 'cmd' || m === 'meta' || m === 'ctrl' || m === 'shift' || m === 'alt') {
65
+ mods.push(m);
66
+ }
67
+ else if (m === 'control')
68
+ mods.push('ctrl');
69
+ else if (m === 'option')
70
+ mods.push('alt');
71
+ else if (m)
72
+ throw new Error(`keyboardShortcutRegistry: unknown modifier "${p}" in "${key}"`);
73
+ }
74
+ }
75
+ for (const m of mods) {
76
+ if (m === 'mod' || m === 'cmd')
77
+ combo.mod = true;
78
+ else
79
+ combo[m] = true;
80
+ }
81
+ combo.key = normalizeKey(base);
82
+ return combo;
83
+ }
84
+ function normalizeKey(key) {
85
+ if (key === 'Esc')
86
+ return 'escape';
87
+ if (key === 'Space' || key === 'Spacebar')
88
+ return ' ';
89
+ if (key === 'Del')
90
+ return 'delete';
91
+ return key.toLowerCase();
92
+ }
93
+ /** A single printable non-letter (`?`, `/`, `\`, `1`): its shift state is baked into the char. */
94
+ function isSymbolKey(key) {
95
+ return key.length === 1 && key.toLowerCase() === key.toUpperCase() && key !== ' ';
96
+ }
97
+ function codeToKey(code) {
98
+ if (!code)
99
+ return null;
100
+ if (/^Key[A-Z]$/.test(code))
101
+ return code.slice(3).toLowerCase();
102
+ if (/^Digit\d$/.test(code))
103
+ return code.slice(5);
104
+ if (code === 'Backslash')
105
+ return '\\';
106
+ if (code === 'Slash')
107
+ return '/';
108
+ return null;
109
+ }
110
+ function matches(event, combo, platform) {
111
+ const evKey = normalizeKey(event.key ?? '');
112
+ let keyHit = evKey === combo.key;
113
+ // macOS Option (and some layouts) rewrite event.key for alt combos — fall
114
+ // back to the physical key when alt is part of the shortcut.
115
+ if (!keyHit && combo.alt)
116
+ keyHit = codeToKey(event.code) === combo.key;
117
+ if (!keyHit)
118
+ return false;
119
+ const wantMeta = combo.meta || (combo.mod && platform === 'mac');
120
+ const wantCtrl = combo.ctrl || (combo.mod && platform !== 'mac');
121
+ if (!!event.metaKey !== wantMeta)
122
+ return false;
123
+ if (!!event.ctrlKey !== wantCtrl)
124
+ return false;
125
+ if (!!event.altKey !== combo.alt)
126
+ return false;
127
+ // `?` arrives as Shift+/ — don't demand the shortcut spell that out.
128
+ if (!combo.shift && isSymbolKey(combo.key))
129
+ return true;
130
+ return !!event.shiftKey === combo.shift;
131
+ }
132
+ const NON_TEXT_INPUT_TYPES = new Set([
133
+ 'button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'
134
+ ]);
135
+ /** True when keystrokes on `target` are text entry (so bare-key shortcuts must not fire). */
136
+ export function isTypingTarget(target) {
137
+ const el = target;
138
+ if (!el || typeof el !== 'object')
139
+ return false;
140
+ if (el.isContentEditable)
141
+ return true;
142
+ const tag = typeof el.tagName === 'string' ? el.tagName.toUpperCase() : '';
143
+ if (tag === 'TEXTAREA' || tag === 'SELECT')
144
+ return true;
145
+ if (tag === 'INPUT')
146
+ return !NON_TEXT_INPUT_TYPES.has(String(el.type ?? 'text').toLowerCase());
147
+ return false;
148
+ }
149
+ const KEY_GLYPHS = {
150
+ escape: 'Esc',
151
+ ' ': 'Space',
152
+ delete: 'Del',
153
+ backspace: '⌫',
154
+ enter: 'Enter',
155
+ tab: 'Tab',
156
+ arrowup: '↑',
157
+ arrowdown: '↓',
158
+ arrowleft: '←',
159
+ arrowright: '→',
160
+ pageup: 'PgUp',
161
+ pagedown: 'PgDn',
162
+ home: 'Home',
163
+ end: 'End'
164
+ };
165
+ /** Keycap label: `⌘⇧K` on macOS, `Ctrl+Shift+K` elsewhere. */
166
+ export function formatShortcut(shortcut, platform = detectPlatform()) {
167
+ const def = typeof shortcut === 'string' ? null : shortcut;
168
+ if (def?.label)
169
+ return def.label;
170
+ const combo = def ? parseCombo(def.key, def.modifiers) : parseCombo(shortcut);
171
+ const k = combo.key;
172
+ const keyLabel = KEY_GLYPHS[k] ?? (/^f\d{1,2}$/.test(k) ? k.toUpperCase() : k.length === 1 ? k.toUpperCase() : k);
173
+ if (platform === 'mac') {
174
+ let s = '';
175
+ if (combo.ctrl)
176
+ s += '⌃';
177
+ if (combo.alt)
178
+ s += '⌥';
179
+ if (combo.shift)
180
+ s += '⇧';
181
+ if (combo.mod || combo.meta)
182
+ s += '⌘';
183
+ return s + keyLabel;
184
+ }
185
+ const parts = [];
186
+ if (combo.ctrl || combo.mod)
187
+ parts.push('Ctrl');
188
+ if (combo.meta)
189
+ parts.push('Win');
190
+ if (combo.alt)
191
+ parts.push('Alt');
192
+ if (combo.shift)
193
+ parts.push('Shift');
194
+ parts.push(keyLabel);
195
+ return parts.join('+');
196
+ }
197
+ export function createShortcutRegistry(options = {}) {
198
+ const platform = options.platform ?? detectPlatform();
199
+ const labels = { [GLOBAL]: 'General', ...options.contextLabels };
200
+ const onError = options.onError ??
201
+ ((error) => console.error('keyboardShortcutRegistry: shortcut action failed', error));
202
+ const entries = [];
203
+ const stack = [];
204
+ const listeners = new Set();
205
+ let seq = 0;
206
+ function notify() {
207
+ for (const l of [...listeners])
208
+ l();
209
+ }
210
+ function activeContexts() {
211
+ const live = [];
212
+ for (let i = stack.length - 1; i >= 0; i--) {
213
+ const f = stack[i];
214
+ if (!live.includes(f.name))
215
+ live.push(f.name);
216
+ if (f.modal)
217
+ return live;
218
+ }
219
+ if (!live.includes(GLOBAL))
220
+ live.push(GLOBAL);
221
+ return live;
222
+ }
223
+ function register(shortcuts) {
224
+ const defs = Array.isArray(shortcuts) ? shortcuts : [shortcuts];
225
+ const added = defs.map((def) => {
226
+ const entry = {
227
+ def,
228
+ combo: parseCombo(def.key, def.modifiers),
229
+ context: def.context ?? GLOBAL,
230
+ seq: seq++
231
+ };
232
+ entries.push(entry);
233
+ return entry;
234
+ });
235
+ notify();
236
+ let done = false;
237
+ return () => {
238
+ if (done)
239
+ return;
240
+ done = true;
241
+ for (const e of added) {
242
+ const i = entries.indexOf(e);
243
+ if (i !== -1)
244
+ entries.splice(i, 1);
245
+ }
246
+ notify();
247
+ };
248
+ }
249
+ function pushContext(name, opts = {}) {
250
+ const frame = { name, modal: !!opts.modal, token: {} };
251
+ if (opts.label)
252
+ labels[name] = opts.label;
253
+ stack.push(frame);
254
+ notify();
255
+ return () => {
256
+ // Pop THIS frame even if it is no longer on top (out-of-order
257
+ // teardown is normal when sibling components unmount together).
258
+ const i = stack.indexOf(frame);
259
+ if (i === -1)
260
+ return;
261
+ stack.splice(i, 1);
262
+ notify();
263
+ };
264
+ }
265
+ function handleKeydown(event) {
266
+ if (event.defaultPrevented || event.isComposing || event.key === 'Process')
267
+ return false;
268
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
269
+ const typing = isTypingTarget(path[0] ?? event.target);
270
+ for (const ctx of activeContexts()) {
271
+ // Later registrations shadow earlier ones within a context.
272
+ for (let i = entries.length - 1; i >= 0; i--) {
273
+ const e = entries[i];
274
+ if (e.context !== ctx)
275
+ continue;
276
+ if (!matches(event, e.combo, platform))
277
+ continue;
278
+ if (typing && !e.def.allowInInput)
279
+ continue;
280
+ if (event.repeat && e.def.allowRepeat === false)
281
+ continue;
282
+ if (e.def.enabled && !e.def.enabled())
283
+ continue;
284
+ if (e.def.preventDefault !== false)
285
+ event.preventDefault();
286
+ try {
287
+ const result = e.def.action(event);
288
+ if (result && typeof result.catch === 'function') {
289
+ result.catch((err) => onError(err, e.def));
290
+ }
291
+ }
292
+ catch (err) {
293
+ onError(err, e.def);
294
+ }
295
+ return true;
296
+ }
297
+ }
298
+ return false;
299
+ }
300
+ function attach(target) {
301
+ const t = target ?? (typeof window !== 'undefined' ? window : null);
302
+ if (!t)
303
+ return () => { };
304
+ const listener = (e) => {
305
+ handleKeydown(e);
306
+ };
307
+ t.addEventListener('keydown', listener);
308
+ return () => t.removeEventListener('keydown', listener);
309
+ }
310
+ function toSections(opts = {}) {
311
+ const contexts = opts.scope === 'all'
312
+ ? [...new Set(entries.map((e) => e.context))].sort((a, b) => a === GLOBAL ? 1 : b === GLOBAL ? -1 : 0)
313
+ : activeContexts();
314
+ const sections = new Map();
315
+ const seen = new Set();
316
+ for (const ctx of contexts) {
317
+ // Walk newest-first so a shadowed combo (same context, or a lower
318
+ // context) is the one dropped, then restore registration order.
319
+ const kept = [];
320
+ for (let i = entries.length - 1; i >= 0; i--) {
321
+ const e = entries[i];
322
+ if (e.context !== ctx || e.def.hidden)
323
+ continue;
324
+ if (opts.onlyEnabled && e.def.enabled && !e.def.enabled())
325
+ continue;
326
+ const comboId = JSON.stringify(e.combo);
327
+ if (opts.scope !== 'all' && seen.has(comboId))
328
+ continue;
329
+ seen.add(comboId);
330
+ kept.unshift(e);
331
+ }
332
+ for (const e of kept) {
333
+ const title = e.def.group ?? labels[ctx] ?? ctx;
334
+ let section = sections.get(title);
335
+ if (!section) {
336
+ section = { title, shortcuts: [] };
337
+ sections.set(title, section);
338
+ }
339
+ section.shortcuts.push({ key: formatShortcut(e.def, platform), description: e.def.description });
340
+ }
341
+ }
342
+ return [...sections.values()];
343
+ }
344
+ return {
345
+ register,
346
+ pushContext,
347
+ activeContexts,
348
+ attach,
349
+ handleKeydown,
350
+ toSections,
351
+ list: () => [...entries].sort((a, b) => a.seq - b.seq).map((e) => e.def),
352
+ format: (s) => formatShortcut(s, platform),
353
+ subscribe(listener) {
354
+ listeners.add(listener);
355
+ return () => listeners.delete(listener);
356
+ },
357
+ platform
358
+ };
359
+ }
360
+ let defaultRegistry = null;
361
+ /** Lazily-created app-wide registry, for apps that want one without threading it through props. */
362
+ export function getShortcutRegistry() {
363
+ defaultRegistry ??= createShortcutRegistry();
364
+ return defaultRegistry;
365
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",