@joewinke/jatui 0.34.2 → 0.36.1

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 (41) hide show
  1. package/CLAUDE.md +9 -3
  2. package/dist/actions/reveal.d.ts +72 -11
  3. package/dist/actions/reveal.js +103 -14
  4. package/dist/components/AudioRecorder.svelte +23 -1
  5. package/dist/components/AudioRecorder.svelte.d.ts +13 -1
  6. package/dist/components/BulkActionBar.svelte +17 -2
  7. package/dist/components/BulkActionBar.svelte.d.ts +5 -0
  8. package/dist/components/Drawer.svelte +20 -1
  9. package/dist/components/Drawer.svelte.d.ts +11 -0
  10. package/dist/components/FloatingActionBar.svelte +26 -11
  11. package/dist/components/FloatingActionBar.svelte.d.ts +5 -0
  12. package/dist/components/KeyboardShortcutsOverlay.svelte +180 -7
  13. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +21 -0
  14. package/dist/components/ListenControl.svelte +1064 -0
  15. package/dist/components/ListenControl.svelte.d.ts +87 -0
  16. package/dist/components/ThumbnailGrid.svelte +34 -7
  17. package/dist/components/ThumbnailGrid.svelte.d.ts +17 -1
  18. package/dist/components/table/DataTable.svelte.d.ts +2 -2
  19. package/dist/index.d.ts +9 -3
  20. package/dist/index.js +9 -2
  21. package/dist/listen/keys.d.ts +36 -0
  22. package/dist/listen/keys.js +68 -0
  23. package/dist/listen/mediaSession.d.ts +90 -0
  24. package/dist/listen/mediaSession.js +108 -0
  25. package/dist/listen/prefs.d.ts +49 -0
  26. package/dist/listen/prefs.js +97 -0
  27. package/dist/listen/segments.d.ts +101 -0
  28. package/dist/listen/segments.js +224 -0
  29. package/dist/listen/types.d.ts +81 -0
  30. package/dist/listen/types.js +7 -0
  31. package/dist/motion/index.d.ts +1 -1
  32. package/dist/motion/index.js +1 -1
  33. package/dist/utils/audioRecorder.d.ts +6 -1
  34. package/dist/utils/audioRecorder.js +5 -0
  35. package/dist/utils/audioRecorderCore.d.ts +34 -0
  36. package/dist/utils/audioRecorderCore.js +43 -6
  37. package/dist/utils/keyboardShortcutHub.d.ts +73 -0
  38. package/dist/utils/keyboardShortcutHub.js +89 -0
  39. package/dist/utils/mentionParser.d.ts +44 -2
  40. package/dist/utils/mentionParser.js +84 -4
  41. package/package.json +2 -1
@@ -11,6 +11,11 @@ export interface ActionButton {
11
11
  key?: string;
12
12
  /** Called when clicked */
13
13
  onclick: () => void;
14
+ /** Dimmed, non-interactive button (mardukos). */
15
+ disabled?: boolean;
16
+ /** When set, the button is disabled and shows a spinner + this text
17
+ * instead of the label (mardukos per-action loading). */
18
+ loadingText?: string | null;
14
19
  }
15
20
  export interface DropdownItem {
16
21
  /** Display label */
@@ -39,9 +39,32 @@
39
39
  * <KeyboardShortcutsOverlay {registry}>
40
40
  * {#snippet extra()}<div>…custom footer…</div>{/snippet}
41
41
  * </KeyboardShortcutsOverlay>
42
+ *
43
+ * ── Host / contributor mode ───────────────────────────────────────────
44
+ *
45
+ * An app shell can mount ONE global host overlay; every other instance
46
+ * on the page then contributes its sections to the host instead of
47
+ * self-rendering, so exactly one `?` overlay exists no matter how many
48
+ * surfaces mount the component:
49
+ *
50
+ * // App shell (layout.svelte):
51
+ * <KeyboardShortcutsOverlay mode="host" />
52
+ *
53
+ * // A page with its own shortcut set:
54
+ * <KeyboardShortcutsOverlay {sections} sectionTitle="Inbox" />
55
+ *
56
+ * `mode` defaults to `'auto'`: the instance contributes to the host when
57
+ * one is mounted, and self-renders when there isn't (the portal /
58
+ * contract-page fallback). Force self-rendering with
59
+ * `mode="standalone"`.
42
60
  */
43
61
  import type { Snippet } from 'svelte';
44
62
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
63
+ import {
64
+ keyboardShortcutHub,
65
+ type ShortcutContribution,
66
+ type ShortcutSectionLike
67
+ } from '../utils/keyboardShortcutHub';
45
68
 
46
69
  export interface KeyboardShortcut {
47
70
  key: string;
@@ -67,7 +90,27 @@
67
90
  extraSections,
68
91
  extra,
69
92
  title = 'Keyboard Shortcuts',
70
- open = $bindable(false)
93
+ open = $bindable(false),
94
+ /**
95
+ * Host / contributor mode.
96
+ * - `'host'`: this instance IS the global overlay — it registers
97
+ * with the hub and aggregates contributions from other instances.
98
+ * - `'auto'` (default): contribute to the host when one exists,
99
+ * otherwise self-render (the portal / contract-page fallback).
100
+ * - `'standalone'`: never defer, always self-render.
101
+ */
102
+ mode = 'auto',
103
+ /**
104
+ * Label under which this instance's sections appear in the host
105
+ * panel when contributing. Ignored when self-rendering.
106
+ */
107
+ sectionTitle = 'Page shortcuts',
108
+ /**
109
+ * Show the closed-state "press ?" hint pill (bottom-right).
110
+ * `showHint={false}` opts out — e.g. on contributor instances that
111
+ * defer to the host's own hint.
112
+ */
113
+ showHint = true
71
114
  }: {
72
115
  shortcuts?: KeyboardShortcut[];
73
116
  /**
@@ -97,6 +140,9 @@
97
140
  extra?: Snippet;
98
141
  title?: string;
99
142
  open?: boolean;
143
+ mode?: 'auto' | 'host' | 'standalone';
144
+ sectionTitle?: string;
145
+ showHint?: boolean;
100
146
  } = $props();
101
147
 
102
148
  let isOpen = $state(open);
@@ -108,19 +154,103 @@
108
154
  open = isOpen;
109
155
  });
110
156
 
111
- // Registry sections are snapshotted on open — BEFORE pushing the overlay's
112
- // own modal context, which would otherwise hide every page context.
157
+ // ── Registry → sections ─────────────────────────────────────────────
158
+ // Registry sections are kept live via `registry.subscribe()` so the host
159
+ // panel always reflects the current context stack. The overlay pushes its
160
+ // own modal context while open, which hides the page contexts underneath —
161
+ // that push is what suppresses page shortcuts behind the modal.
113
162
  let registrySections = $state<ShortcutSection[] | null>(null);
114
163
  $effect(() => {
115
- if (!isOpen || !registry) return;
164
+ if (!registry) return;
116
165
  registrySections = registry.toSections();
166
+ return registry.subscribe(() => {
167
+ registrySections = registry.toSections();
168
+ });
169
+ });
170
+ $effect(() => {
171
+ if (!isOpen || !registry) return;
117
172
  return registry.pushContext('keyboard-shortcuts-overlay', { modal: true });
118
173
  });
119
174
 
120
- const shownSections = $derived(
175
+ // ── Hub wiring ──────────────────────────────────────────────────────
176
+ let hubHasHost = $state(keyboardShortcutHub.hasHost);
177
+ let hubContributions = $state<ShortcutContribution[]>(keyboardShortcutHub.getContributions());
178
+ $effect(() => {
179
+ return keyboardShortcutHub.subscribe(() => {
180
+ hubHasHost = keyboardShortcutHub.hasHost;
181
+ hubContributions = keyboardShortcutHub.getContributions();
182
+ });
183
+ });
184
+
185
+ // A 'host' instance registers itself with the hub while mounted.
186
+ $effect(() => {
187
+ if (mode !== 'host') return;
188
+ return keyboardShortcutHub.registerHost({
189
+ open: () => {
190
+ isOpen = true;
191
+ },
192
+ toggle: () => {
193
+ isOpen = !isOpen;
194
+ }
195
+ });
196
+ });
197
+
198
+ // Whether this instance self-renders or defers to the host.
199
+ const isHost = $derived(mode === 'host');
200
+ const isContributor = $derived(mode !== 'host' && mode !== 'standalone' && hubHasHost);
201
+
202
+ // This instance's own sections (precedence: sections > registry > null).
203
+ const ownSections = $derived(
121
204
  sections && sections.length > 0 ? sections : registry ? registrySections : null
122
205
  );
123
206
 
207
+ // Contributing instances hand their sections to the hub. `extraSections`
208
+ // ride along too — they belong to the page, not the overlay itself.
209
+ const contributionId = `kso-${Math.random().toString(36).slice(2, 10)}`;
210
+ $effect(() => {
211
+ if (!isContributor) return;
212
+ const primary = ownSections ?? [];
213
+ if (primary.length === 0 && (!extraSections || extraSections.length === 0)) return;
214
+ const contribution: ShortcutContribution = {
215
+ id: contributionId,
216
+ sectionTitle,
217
+ sections: primary,
218
+ registrySections: extraSections ?? []
219
+ };
220
+ return keyboardShortcutHub.contribute(contribution);
221
+ });
222
+
223
+ // Contributing instances forward external `open` requests to the host —
224
+ // they can't open themselves.
225
+ $effect(() => {
226
+ if (!isContributor) return;
227
+ if (!open) return;
228
+ keyboardShortcutHub.requestOpen();
229
+ isOpen = false;
230
+ });
231
+
232
+ function flattenSections(secs: ShortcutSectionLike[]): SectionEntry[] {
233
+ return secs.flatMap((s) => s.shortcuts);
234
+ }
235
+
236
+ // Host panel content: own sections plus every contributor's sections,
237
+ // each grouped under its sectionTitle.
238
+ const hostSections = $derived.by(() => {
239
+ if (!isHost) return null;
240
+ const primary = ownSections ?? (shortcuts.length > 0 ? [{ title, shortcuts }] : []);
241
+ const contributed: ShortcutSection[] = hubContributions.flatMap((c) => [
242
+ { title: c.sectionTitle, shortcuts: flattenSections(c.sections) },
243
+ ...(c.registrySections ?? []).map((s) => ({ title: s.title, shortcuts: s.shortcuts }))
244
+ ]);
245
+ return [...primary, ...contributed];
246
+ });
247
+
248
+ // What actually renders in the panel.
249
+ const shownSections = $derived(isHost ? hostSections : ownSections);
250
+ const shownFlatShortcuts = $derived(
251
+ isHost || (shownSections && shownSections.length > 0) ? [] : shortcuts
252
+ );
253
+
124
254
  // Track which key label is currently depressed
125
255
  let depressedKey = $state<string | null>(null);
126
256
  let depressTimer: ReturnType<typeof setTimeout> | null = null;
@@ -143,6 +273,8 @@
143
273
  if (e.key === '?') {
144
274
  // `?` is always Shift+/. Don't trigger while typing — would swallow the char.
145
275
  if (isTypingTarget(e.target)) return;
276
+ // A contributor doesn't own `?` — the host's own listener handles it.
277
+ if (isContributor) return;
146
278
  e.preventDefault();
147
279
  isOpen = !isOpen;
148
280
  return;
@@ -236,7 +368,7 @@
236
368
  {/each}
237
369
  {/each}
238
370
  {:else}
239
- {#each shortcuts as { key, description }, i (key + '::' + i)}
371
+ {#each shownFlatShortcuts as { key, description }, i (key + '::' + i)}
240
372
  <div class="kso-row">
241
373
  <kbd
242
374
  class="kso-key"
@@ -279,6 +411,13 @@
279
411
  </div>
280
412
  </div>
281
413
  </div>
414
+ {:else if showHint && !isContributor}
415
+ <!-- Closed-state hint — press ? to open. Contributors that defer to a
416
+ host stay silent: the host renders its own hint. -->
417
+ <div class="kso-hint" aria-hidden="true">
418
+ <kbd class="kso-hint-key">?</kbd>
419
+ <span class="kso-hint-text">for shortcuts</span>
420
+ </div>
282
421
  {/if}
283
422
 
284
423
  <style>
@@ -419,6 +558,39 @@
419
558
  border-color: oklch(0.45 0.06 240);
420
559
  }
421
560
 
561
+ .kso-hint {
562
+ position: fixed;
563
+ right: 1rem;
564
+ bottom: 1rem;
565
+ z-index: 50;
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 0.45rem;
569
+ padding: 0.3rem 0.65rem;
570
+ background: oklch(0.18 0.02 250 / 0.85);
571
+ border: 1px solid oklch(0.30 0.03 250);
572
+ border-radius: 0.5rem;
573
+ color: oklch(0.85 0.02 250);
574
+ font-size: 0.78rem;
575
+ pointer-events: none;
576
+ animation: kso-fade 0.15s ease-out;
577
+ }
578
+
579
+ .kso-hint-key {
580
+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
581
+ font-size: 0.72rem;
582
+ background: oklch(0.25 0.02 250);
583
+ border: 1px solid oklch(0.35 0.02 250);
584
+ border-bottom-width: 2px;
585
+ border-radius: 0.3rem;
586
+ padding: 0.05rem 0.45rem;
587
+ color: oklch(0.93 0.02 250);
588
+ }
589
+
590
+ .kso-hint-text {
591
+ color: oklch(0.72 0.02 250);
592
+ }
593
+
422
594
  @keyframes kso-fade {
423
595
  from {
424
596
  opacity: 0;
@@ -430,7 +602,8 @@
430
602
 
431
603
  @media (prefers-reduced-motion: reduce) {
432
604
  .kso-backdrop,
433
- .kso-panel {
605
+ .kso-panel,
606
+ .kso-hint {
434
607
  animation: none !important;
435
608
  }
436
609
  .kso-key {
@@ -38,6 +38,24 @@
38
38
  * <KeyboardShortcutsOverlay {registry}>
39
39
  * {#snippet extra()}<div>…custom footer…</div>{/snippet}
40
40
  * </KeyboardShortcutsOverlay>
41
+ *
42
+ * ── Host / contributor mode ───────────────────────────────────────────
43
+ *
44
+ * An app shell can mount ONE global host overlay; every other instance
45
+ * on the page then contributes its sections to the host instead of
46
+ * self-rendering, so exactly one `?` overlay exists no matter how many
47
+ * surfaces mount the component:
48
+ *
49
+ * // App shell (layout.svelte):
50
+ * <KeyboardShortcutsOverlay mode="host" />
51
+ *
52
+ * // A page with its own shortcut set:
53
+ * <KeyboardShortcutsOverlay {sections} sectionTitle="Inbox" />
54
+ *
55
+ * `mode` defaults to `'auto'`: the instance contributes to the host when
56
+ * one is mounted, and self-renders when there isn't (the portal /
57
+ * contract-page fallback). Force self-rendering with
58
+ * `mode="standalone"`.
41
59
  */
42
60
  import type { Snippet } from 'svelte';
43
61
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
@@ -84,6 +102,9 @@ type $$ComponentProps = {
84
102
  extra?: Snippet;
85
103
  title?: string;
86
104
  open?: boolean;
105
+ mode?: 'auto' | 'host' | 'standalone';
106
+ sectionTitle?: string;
107
+ showHint?: boolean;
87
108
  };
88
109
  declare const KeyboardShortcutsOverlay: import("svelte").Component<$$ComponentProps, {}, "open">;
89
110
  type KeyboardShortcutsOverlay = ReturnType<typeof KeyboardShortcutsOverlay>;