@joewinke/jatui 0.25.0 → 0.27.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 (74) hide show
  1. package/CLAUDE.md +115 -2
  2. package/dist/actions/keyboardScope.d.ts +58 -0
  3. package/dist/actions/keyboardScope.js +110 -0
  4. package/dist/actions/listNav.d.ts +48 -3
  5. package/dist/actions/listNav.js +267 -77
  6. package/dist/actions/longpress.d.ts +37 -0
  7. package/dist/actions/longpress.js +105 -0
  8. package/dist/actions/stickToBottom.d.ts +66 -0
  9. package/dist/actions/stickToBottom.js +155 -0
  10. package/dist/actions/swipe.d.ts +64 -0
  11. package/dist/actions/swipe.js +179 -0
  12. package/dist/components/BottomSheet.svelte +280 -0
  13. package/dist/components/BottomSheet.svelte.d.ts +27 -0
  14. package/dist/components/ConfirmDialog.svelte +21 -0
  15. package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
  16. package/dist/components/ConfirmModal.svelte +15 -0
  17. package/dist/components/ConfirmModal.svelte.d.ts +5 -0
  18. package/dist/components/Drawer.svelte +18 -1
  19. package/dist/components/Drawer.svelte.d.ts +9 -1
  20. package/dist/components/InputDialog.svelte +21 -0
  21. package/dist/components/InputDialog.svelte.d.ts +11 -1
  22. package/dist/components/KeyboardShortcutsOverlay.svelte +5 -0
  23. package/dist/components/Modal.svelte +50 -1
  24. package/dist/components/Modal.svelte.d.ts +8 -0
  25. package/dist/components/basic/Empty.svelte +173 -9
  26. package/dist/components/basic/Empty.svelte.d.ts +50 -1
  27. package/dist/components/basic/ErrorState.svelte +128 -0
  28. package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
  29. package/dist/components/basic/JumpToLatest.svelte +60 -0
  30. package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
  31. package/dist/components/basic/PageHeader.svelte +216 -0
  32. package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
  33. package/dist/components/basic/ProgressButton.svelte +221 -0
  34. package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
  35. package/dist/components/graph/GraphApiBridge.svelte +36 -0
  36. package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
  37. package/dist/components/graph/GraphCanvas.svelte +212 -27
  38. package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
  39. package/dist/components/graph/api.d.ts +52 -0
  40. package/dist/components/graph/api.js +66 -0
  41. package/dist/components/graph/index.d.ts +2 -0
  42. package/dist/components/graph/index.js +1 -0
  43. package/dist/components/graph/theme.js +13 -0
  44. package/dist/components/messaging/MessageItem.svelte +5 -0
  45. package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
  46. package/dist/components/table/DataTable.svelte +607 -72
  47. package/dist/components/table/DataTable.svelte.d.ts +23 -2
  48. package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
  49. package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
  50. package/dist/components/table/columnStorage.d.ts +29 -0
  51. package/dist/components/table/columnStorage.js +61 -0
  52. package/dist/components/table/groupRows.d.ts +72 -0
  53. package/dist/components/table/groupRows.js +118 -0
  54. package/dist/components/table/index.d.ts +7 -1
  55. package/dist/components/table/index.js +3 -0
  56. package/dist/components/table/types.d.ts +207 -3
  57. package/dist/index.d.ts +25 -2
  58. package/dist/index.js +32 -1
  59. package/dist/utils/ansiToHtml.d.ts +83 -0
  60. package/dist/utils/ansiToHtml.js +543 -0
  61. package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
  62. package/dist/utils/keyboardShortcutRegistry.js +1070 -76
  63. package/dist/utils/pollBackoff.d.ts +83 -0
  64. package/dist/utils/pollBackoff.js +151 -0
  65. package/dist/utils/snapFlick.d.ts +59 -0
  66. package/dist/utils/snapFlick.js +66 -0
  67. package/dist/video/advisors/qr.d.ts +1 -1
  68. package/dist/virtual/createRowWindow.d.ts +75 -0
  69. package/dist/virtual/createRowWindow.js +129 -0
  70. package/dist/virtual/index.d.ts +4 -0
  71. package/dist/virtual/index.js +5 -0
  72. package/dist/virtual/rowWindowCore.d.ts +202 -0
  73. package/dist/virtual/rowWindowCore.js +510 -0
  74. package/package.json +11 -1
@@ -0,0 +1,280 @@
1
+ <!--
2
+ BottomSheet.svelte — Reusable mobile bottom sheet primitive.
3
+
4
+ Usage:
5
+ <BottomSheet bind:open title="Pick something" onclose={handleClose}>
6
+ Content here
7
+ </BottomSheet>
8
+
9
+ Props:
10
+ open {boolean} — bindable, controls visibility
11
+ title {string} — optional header title
12
+ zIndex {number} — z-index override (default 70)
13
+ onclose {() => void} — called when the sheet is dismissed
14
+ fullHeight {boolean} — sheet takes full viewport height (slot controls scroll)
15
+ snap {'single'|'dual'} — 'dual' enables the inspector pattern: two snap
16
+ heights (peek → expanded) with flick-momentum release and
17
+ visualViewport keyboard avoidance (wayflow eval §6b).
18
+ Default 'single' preserves the legacy drag-to-dismiss sheet.
19
+ peekFraction / expandedFraction {number} — snap heights as a fraction of the
20
+ keyboard-adjusted visible viewport (dual mode only).
21
+
22
+ Drag physics:
23
+ single — pointer drag downward > 80px dismisses (legacy behavior).
24
+ dual — flick release: a fast flick (≥0.5 px/ms) snaps one step in the
25
+ flick's direction (down from peek dismisses); otherwise the sheet
26
+ settles on the snap nearest the post-drag height.
27
+ -->
28
+ <script lang="ts">
29
+ import { onMount, untrack } from 'svelte';
30
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
31
+ import type { Snippet } from 'svelte';
32
+ import { fly, fade } from 'svelte/transition';
33
+ import { cubicOut } from 'svelte/easing';
34
+ import {
35
+ resolveSnapTarget,
36
+ keyboardInsetPx,
37
+ clampSnapHeight,
38
+ type SnapHeight
39
+ } from '../utils/snapFlick';
40
+
41
+ let {
42
+ open = $bindable(false),
43
+ title = '',
44
+ zIndex = 70,
45
+ fullHeight = false,
46
+ snap = 'single',
47
+ peekFraction = 0.4,
48
+ expandedFraction = 0.9,
49
+ onclose = () => {},
50
+ shortcutRegistry,
51
+ shortcutContext = 'bottom-sheet',
52
+ children
53
+ }: {
54
+ open?: boolean;
55
+ title?: string;
56
+ zIndex?: number;
57
+ /** When true, sheet takes full viewport height and content wrapper is overflow:hidden so the slot controls its own scroll zones. */
58
+ fullHeight?: boolean;
59
+ /** 'single' = legacy drag-to-dismiss only. 'dual' = peek/expanded snaps + flick release + keyboard avoidance. */
60
+ snap?: 'single' | 'dual';
61
+ /** Peek snap height as a fraction of the visible (keyboard-adjusted) viewport. */
62
+ peekFraction?: number;
63
+ /** Expanded snap height as a fraction of the visible (keyboard-adjusted) viewport. */
64
+ expandedFraction?: number;
65
+ onclose?: () => void;
66
+ /**
67
+ * Shortcut registry to push a modal context into while open — page
68
+ * shortcuts registered there stop firing behind it (jatui-voc3i.3).
69
+ */
70
+ shortcutRegistry?: ShortcutRegistry;
71
+ /** Context name pushed while open. Default `'bottom-sheet'`. */
72
+ shortcutContext?: string;
73
+ children?: Snippet;
74
+ } = $props();
75
+
76
+ // While open, a modal shortcut context keeps the host page's registry
77
+ // shortcuts from firing behind it.
78
+ $effect(() => {
79
+ if (!open || !shortcutRegistry) return;
80
+ return shortcutRegistry.pushContext(shortcutContext, { modal: true, label: untrack(() => title) || undefined });
81
+ });
82
+
83
+ // ── Keyboard avoidance (visualViewport) ───────────────────────────────────
84
+ // The soft keyboard shrinks the visual viewport; track it so the sheet
85
+ // clamps to what is actually visible and content clears the keyboard.
86
+ let visibleHeight = $state(0);
87
+ let keyboardInset = $state(0);
88
+
89
+ function readViewport() {
90
+ const vv = window.visualViewport;
91
+ if (!vv) {
92
+ visibleHeight = window.innerHeight;
93
+ keyboardInset = 0;
94
+ return;
95
+ }
96
+ visibleHeight = vv.height;
97
+ keyboardInset = keyboardInsetPx(
98
+ { height: vv.height, offsetTop: vv.offsetTop },
99
+ window.innerHeight
100
+ );
101
+ }
102
+
103
+ onMount(() => {
104
+ readViewport();
105
+ const vv = window.visualViewport;
106
+ if (!vv) return;
107
+ const onChange = () => readViewport();
108
+ vv.addEventListener('resize', onChange);
109
+ vv.addEventListener('scroll', onChange);
110
+ return () => {
111
+ vv.removeEventListener('resize', onChange);
112
+ vv.removeEventListener('scroll', onChange);
113
+ };
114
+ });
115
+
116
+ // ── Snap points (dual mode) ───────────────────────────────────────────────
117
+ // Derived from the keyboard-adjusted visible height, so the expanded snap can
118
+ // never extend behind the soft keyboard.
119
+ const isDual = $derived(snap === 'dual');
120
+
121
+ const snapPoints = $derived.by<SnapHeight[]>(() => {
122
+ const avail = Math.max(0, visibleHeight || (typeof window !== 'undefined' ? window.innerHeight : 0));
123
+ return [
124
+ { name: 'peek', heightPx: clampSnapHeight(peekFraction * avail, avail) },
125
+ { name: 'expanded', heightPx: clampSnapHeight(expandedFraction * avail, avail) }
126
+ ];
127
+ });
128
+
129
+ // Resting snap index. Dual mode opens at peek (inspector pattern: peek first,
130
+ // expand on demand); single mode is unaffected.
131
+ // Resting snap index (dual mode). Opens at peek (index 0) — the inspector
132
+ // pattern: content summary first, expand on demand. Single mode never reads it.
133
+ let snapIndex = $state(0);
134
+
135
+ // ── Drag-to-dismiss / snap state ──────────────────────────────────────────
136
+ let dragStartY = 0;
137
+ let dragCurrentY = $state(0);
138
+ let isDragging = $state(false);
139
+ // Last two pointer samples for flick-velocity measurement (px/ms).
140
+ let prevSample: { y: number; t: number } | null = null;
141
+ let lastSample: { y: number; t: number } | null = null;
142
+
143
+ const DISMISS_THRESHOLD = 80;
144
+ const FLICK_VELOCITY_PX_MS = 0.5;
145
+
146
+ function dismiss() {
147
+ open = false;
148
+ onclose();
149
+ }
150
+
151
+ function onPointerDown(e: PointerEvent) {
152
+ dragStartY = e.clientY;
153
+ dragCurrentY = 0;
154
+ isDragging = true;
155
+ prevSample = null;
156
+ lastSample = { y: e.clientY, t: performance.now() };
157
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
158
+ }
159
+
160
+ function onPointerMove(e: PointerEvent) {
161
+ if (!isDragging) return;
162
+ const delta = e.clientY - dragStartY;
163
+ // Dual mode allows upward drag (grow toward expanded); single mode keeps
164
+ // the legacy downward-only gesture.
165
+ dragCurrentY = isDual ? delta : Math.max(0, delta);
166
+ // Velocity sampling for flick release.
167
+ const t = performance.now();
168
+ if (lastSample) prevSample = lastSample;
169
+ lastSample = { y: e.clientY, t: t };
170
+ }
171
+
172
+ function onPointerUp(_e: PointerEvent) {
173
+ if (!isDragging) return;
174
+ isDragging = false;
175
+
176
+ if (isDual) {
177
+ const velocity =
178
+ prevSample && lastSample && lastSample.t > prevSample.t
179
+ ? (lastSample.y - prevSample.y) / (lastSample.t - prevSample.t)
180
+ : 0;
181
+ const result = resolveSnapTarget({
182
+ dragDeltaY: dragCurrentY,
183
+ velocity,
184
+ snapPoints,
185
+ currentIndex: snapIndex
186
+ });
187
+ if (result.dismiss) {
188
+ dismiss();
189
+ } else {
190
+ snapIndex = result.index;
191
+ }
192
+ } else if (dragCurrentY >= DISMISS_THRESHOLD) {
193
+ dismiss();
194
+ }
195
+ dragCurrentY = 0;
196
+ prevSample = null;
197
+ lastSample = null;
198
+ }
199
+
200
+ // Subtle resistance feedback during drag
201
+ const dragOpacity = $derived(isDragging ? Math.max(0.4, 1 - Math.abs(dragCurrentY) / 300) : 1);
202
+
203
+ // Height applied to the panel: dual mode snaps between peek/expanded; single
204
+ // mode keeps the legacy max-height/fullHeight styling.
205
+ const panelHeightStyle = $derived.by(() => {
206
+ if (isDual) {
207
+ const i = Math.min(snapIndex, snapPoints.length - 1);
208
+ return `height: ${snapPoints[i].heightPx}px;`;
209
+ }
210
+ return fullHeight ? 'height: 100dvh;' : 'max-height: 80dvh;';
211
+ });
212
+ </script>
213
+
214
+ {#if open}
215
+ <!-- Backdrop -->
216
+ <div
217
+ class="fixed inset-0 bg-black/50"
218
+ style="z-index: {zIndex}"
219
+ role="button"
220
+ tabindex="-1"
221
+ aria-label="Close"
222
+ transition:fade={{ duration: 150 }}
223
+ onclick={dismiss}
224
+ onkeydown={(e) => { if (e.key === 'Escape') dismiss(); }}
225
+ ></div>
226
+
227
+ <!-- Sheet Panel -->
228
+ <div
229
+ class="fixed bottom-0 inset-x-0 flex flex-col bg-base-100 rounded-t-2xl border-t border-base-300 shadow-2xl"
230
+ style="z-index: {zIndex + 1}; {panelHeightStyle} transform: translateY({dragCurrentY}px); opacity: {dragOpacity}; transition: {isDragging ? 'none' : 'transform 0.15s ease, opacity 0.15s ease, height 0.2s ease'};"
231
+ role="dialog"
232
+ tabindex="-1"
233
+ aria-modal="true"
234
+ aria-label={title || 'Bottom sheet'}
235
+ transition:fly={{ y: 300, duration: 200, easing: cubicOut }}
236
+ onclick={(e) => e.stopPropagation()}
237
+ onkeydown={(e) => { if (e.key === 'Escape') dismiss(); }}
238
+ >
239
+ <!-- Drag handle (pointer capture target) -->
240
+ <div
241
+ class="flex-shrink-0 flex justify-center pt-3 pb-1 cursor-grab active:cursor-grabbing touch-none select-none"
242
+ role="button"
243
+ tabindex="-1"
244
+ aria-label="Drag to resize"
245
+ onpointerdown={onPointerDown}
246
+ onpointermove={onPointerMove}
247
+ onpointerup={onPointerUp}
248
+ onpointercancel={onPointerUp}
249
+ >
250
+ <div class="w-10 h-1.5 bg-base-300 rounded-full"></div>
251
+ </div>
252
+
253
+ <!-- Optional header -->
254
+ {#if title}
255
+ <div class="flex-shrink-0 flex items-center justify-between px-4 py-2 border-b border-base-200">
256
+ <span class="text-sm font-semibold">{title}</span>
257
+ <button
258
+ type="button"
259
+ class="btn btn-sm btn-ghost btn-circle"
260
+ aria-label="Close"
261
+ onclick={dismiss}
262
+ >✕</button>
263
+ </div>
264
+ {/if}
265
+
266
+ <!-- Scrollable content (or overflow-hidden when fullHeight so the slot controls scroll zones).
267
+ Keyboard avoidance: the content's bottom padding clears the occluded strip so
268
+ inputs stay above the soft keyboard (visualViewport-driven). -->
269
+ <div
270
+ class="flex-1 overscroll-contain {fullHeight && !isDual ? 'overflow-hidden' : isDual ? 'overflow-y-auto' : 'overflow-y-auto'}"
271
+ style={!fullHeight
272
+ ? 'padding-bottom: max(1rem, env(safe-area-inset-bottom));'
273
+ : keyboardInset > 0
274
+ ? `padding-bottom: ${keyboardInset}px;`
275
+ : ''}
276
+ >
277
+ {@render children?.()}
278
+ </div>
279
+ </div>
280
+ {/if}
@@ -0,0 +1,27 @@
1
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
2
+ import type { Snippet } from 'svelte';
3
+ type $$ComponentProps = {
4
+ open?: boolean;
5
+ title?: string;
6
+ zIndex?: number;
7
+ /** When true, sheet takes full viewport height and content wrapper is overflow:hidden so the slot controls its own scroll zones. */
8
+ fullHeight?: boolean;
9
+ /** 'single' = legacy drag-to-dismiss only. 'dual' = peek/expanded snaps + flick release + keyboard avoidance. */
10
+ snap?: 'single' | 'dual';
11
+ /** Peek snap height as a fraction of the visible (keyboard-adjusted) viewport. */
12
+ peekFraction?: number;
13
+ /** Expanded snap height as a fraction of the visible (keyboard-adjusted) viewport. */
14
+ expandedFraction?: number;
15
+ onclose?: () => void;
16
+ /**
17
+ * Shortcut registry to push a modal context into while open — page
18
+ * shortcuts registered there stop firing behind it (jatui-voc3i.3).
19
+ */
20
+ shortcutRegistry?: ShortcutRegistry;
21
+ /** Context name pushed while open. Default `'bottom-sheet'`. */
22
+ shortcutContext?: string;
23
+ children?: Snippet;
24
+ };
25
+ declare const BottomSheet: import("svelte").Component<$$ComponentProps, {}, "open">;
26
+ type BottomSheet = ReturnType<typeof BottomSheet>;
27
+ export default BottomSheet;
@@ -3,6 +3,20 @@
3
3
  import { cubicOut } from 'svelte/easing';
4
4
  import { tick } from 'svelte';
5
5
  import { getPending, confirmDialog, cancelDialog } from '../stores/confirmDialog';
6
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
7
+
8
+ let {
9
+ shortcutRegistry,
10
+ shortcutContext = 'confirm-dialog'
11
+ }: {
12
+ /**
13
+ * Shortcut registry to push a modal context into while the dialog is up
14
+ * — page shortcuts registered there stop firing behind it (jatui-voc3i.3).
15
+ */
16
+ shortcutRegistry?: ShortcutRegistry;
17
+ /** Context name pushed while the dialog is up. Default `'confirm-dialog'`. */
18
+ shortcutContext?: string;
19
+ } = $props();
6
20
 
7
21
  const pending = $derived(getPending());
8
22
  const opts = $derived(pending?.options ?? null);
@@ -58,6 +72,13 @@
58
72
  if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); confirmDialog(); }
59
73
  }
60
74
 
75
+ // While the dialog is up, a modal shortcut context keeps the host page's
76
+ // registry shortcuts from firing behind it.
77
+ $effect(() => {
78
+ if (!visible || !shortcutRegistry) return;
79
+ return shortcutRegistry.pushContext(shortcutContext, { modal: true });
80
+ });
81
+
61
82
  $effect(() => {
62
83
  window.addEventListener('keydown', handleKeydown, true);
63
84
  return () => window.removeEventListener('keydown', handleKeydown, true);
@@ -1,3 +1,13 @@
1
- declare const ConfirmDialog: import("svelte").Component<Record<string, never>, {}, "">;
1
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
2
+ type $$ComponentProps = {
3
+ /**
4
+ * Shortcut registry to push a modal context into while the dialog is up
5
+ * — page shortcuts registered there stop firing behind it (jatui-voc3i.3).
6
+ */
7
+ shortcutRegistry?: ShortcutRegistry;
8
+ /** Context name pushed while the dialog is up. Default `'confirm-dialog'`. */
9
+ shortcutContext?: string;
10
+ };
11
+ declare const ConfirmDialog: import("svelte").Component<$$ComponentProps, {}, "">;
2
12
  type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
3
13
  export default ConfirmDialog;
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { fly, fade } from 'svelte/transition';
3
3
  import { cubicOut } from 'svelte/easing';
4
+ import { untrack } from 'svelte';
5
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
4
6
 
5
7
  let {
6
8
  open = $bindable(true),
@@ -13,6 +15,8 @@
13
15
  iconClass = "text-warning",
14
16
  onconfirm,
15
17
  oncancel,
18
+ shortcutRegistry,
19
+ shortcutContext = 'confirm-modal',
16
20
  } = $props<{
17
21
  open?: boolean
18
22
  title?: string
@@ -24,8 +28,19 @@
24
28
  iconClass?: string
25
29
  onconfirm?: () => void
26
30
  oncancel?: () => void
31
+ /** Shortcut registry to push a modal context into while open (jatui-voc3i.3). */
32
+ shortcutRegistry?: ShortcutRegistry
33
+ /** Context name pushed while open. Default `'confirm-modal'`. */
34
+ shortcutContext?: string
27
35
  }>()
28
36
 
37
+ // While open, a modal shortcut context keeps the host page's registry
38
+ // shortcuts from firing behind it.
39
+ $effect(() => {
40
+ if (!open || !shortcutRegistry) return
41
+ return shortcutRegistry.pushContext(shortcutContext, { modal: true, label: untrack(() => title) || undefined })
42
+ })
43
+
29
44
  // Respect prefers-reduced-motion: skip the entrance/exit transitions.
30
45
  const reducedMotion =
31
46
  typeof window !== 'undefined' &&
@@ -1,3 +1,4 @@
1
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
1
2
  type $$ComponentProps = {
2
3
  open?: boolean;
3
4
  title?: string;
@@ -9,6 +10,10 @@ type $$ComponentProps = {
9
10
  iconClass?: string;
10
11
  onconfirm?: () => void;
11
12
  oncancel?: () => void;
13
+ /** Shortcut registry to push a modal context into while open (jatui-voc3i.3). */
14
+ shortcutRegistry?: ShortcutRegistry;
15
+ /** Context name pushed while open. Default `'confirm-modal'`. */
16
+ shortcutContext?: string;
12
17
  };
13
18
  declare const ConfirmModal: import("svelte").Component<$$ComponentProps, {}, "open">;
14
19
  type ConfirmModal = ReturnType<typeof ConfirmModal>;
@@ -5,7 +5,8 @@
5
5
  * Sliding panel overlay with backdrop, escape key, and header actions.
6
6
  */
7
7
 
8
- import type { Snippet } from 'svelte';
8
+ import { untrack, type Snippet } from 'svelte';
9
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
9
10
 
10
11
  interface Props {
11
12
  open?: boolean;
@@ -26,6 +27,13 @@
26
27
  * is open.
27
28
  */
28
29
  trapKeys?: boolean;
30
+ /**
31
+ * Shortcut registry to push a modal context into while open — page
32
+ * shortcuts registered there stop firing behind it (jatui-voc3i.3).
33
+ */
34
+ shortcutRegistry?: ShortcutRegistry;
35
+ /** Context name pushed while open. Default `'drawer'`. */
36
+ shortcutContext?: string;
29
37
  onclose?: () => void;
30
38
  class?: string;
31
39
  headerActions?: Snippet;
@@ -40,6 +48,8 @@
40
48
  showCloseButton = true,
41
49
  contentClass = '',
42
50
  trapKeys = true,
51
+ shortcutRegistry,
52
+ shortcutContext = 'drawer',
43
53
  onclose,
44
54
  class: className = '',
45
55
  headerActions,
@@ -56,6 +66,13 @@
56
66
  full: 'w-full'
57
67
  };
58
68
 
69
+ // While open, a modal shortcut context keeps the host page's registry
70
+ // shortcuts from firing behind it.
71
+ $effect(() => {
72
+ if (!open || !shortcutRegistry) return;
73
+ return shortcutRegistry.pushContext(shortcutContext, { modal: true, label: untrack(() => title) || undefined });
74
+ });
75
+
59
76
  function handleClose() {
60
77
  open = false;
61
78
  onclose?.();
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Sliding panel overlay with backdrop, escape key, and header actions.
5
5
  */
6
- import type { Snippet } from 'svelte';
6
+ import { type Snippet } from 'svelte';
7
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
7
8
  interface Props {
8
9
  open?: boolean;
9
10
  title?: string;
@@ -23,6 +24,13 @@ interface Props {
23
24
  * is open.
24
25
  */
25
26
  trapKeys?: boolean;
27
+ /**
28
+ * Shortcut registry to push a modal context into while open — page
29
+ * shortcuts registered there stop firing behind it (jatui-voc3i.3).
30
+ */
31
+ shortcutRegistry?: ShortcutRegistry;
32
+ /** Context name pushed while open. Default `'drawer'`. */
33
+ shortcutContext?: string;
26
34
  onclose?: () => void;
27
35
  class?: string;
28
36
  headerActions?: Snippet;
@@ -3,6 +3,20 @@
3
3
  import { cubicOut } from 'svelte/easing';
4
4
  import { tick } from 'svelte';
5
5
  import { getPendingInput, submitInput, cancelInput } from '../stores/inputDialog';
6
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
7
+
8
+ let {
9
+ shortcutRegistry,
10
+ shortcutContext = 'input-dialog'
11
+ }: {
12
+ /**
13
+ * Shortcut registry to push a modal context into while the dialog is up
14
+ * — page shortcuts registered there stop firing behind it (jatui-voc3i.3).
15
+ */
16
+ shortcutRegistry?: ShortcutRegistry;
17
+ /** Context name pushed while the dialog is up. Default `'input-dialog'`. */
18
+ shortcutContext?: string;
19
+ } = $props();
6
20
 
7
21
  const pending = $derived(getPendingInput());
8
22
  const opts = $derived(pending?.options ?? null);
@@ -38,6 +52,13 @@
38
52
  if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleSubmit(); }
39
53
  }
40
54
 
55
+ // While the dialog is up, a modal shortcut context keeps the host page's
56
+ // registry shortcuts from firing behind it.
57
+ $effect(() => {
58
+ if (!visible || !shortcutRegistry) return;
59
+ return shortcutRegistry.pushContext(shortcutContext, { modal: true });
60
+ });
61
+
41
62
  $effect(() => {
42
63
  window.addEventListener('keydown', handleKeydown, true);
43
64
  return () => window.removeEventListener('keydown', handleKeydown, true);
@@ -1,3 +1,13 @@
1
- declare const InputDialog: import("svelte").Component<Record<string, never>, {}, "">;
1
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
2
+ type $$ComponentProps = {
3
+ /**
4
+ * Shortcut registry to push a modal context into while the dialog is up
5
+ * — page shortcuts registered there stop firing behind it (jatui-voc3i.3).
6
+ */
7
+ shortcutRegistry?: ShortcutRegistry;
8
+ /** Context name pushed while the dialog is up. Default `'input-dialog'`. */
9
+ shortcutContext?: string;
10
+ };
11
+ declare const InputDialog: import("svelte").Component<$$ComponentProps, {}, "">;
2
12
  type InputDialog = ReturnType<typeof InputDialog>;
3
13
  export default InputDialog;
@@ -133,6 +133,11 @@
133
133
  }
134
134
 
135
135
  function onKeydown(e: KeyboardEvent) {
136
+ // Another listener already claimed this keypress (host app's own '?'
137
+ // handler, an open modal, …) — bail so one keypress can't toggle two
138
+ // overlays at once. Covers the '?' and Escape branches alike.
139
+ if (e.defaultPrevented) return;
140
+
136
141
  if (e.ctrlKey || e.metaKey || e.altKey) return;
137
142
 
138
143
  if (e.key === '?') {
@@ -6,7 +6,8 @@
6
6
  * Converted from Svelte 4 to Svelte 5 runes.
7
7
  */
8
8
 
9
- import { onMount } from 'svelte';
9
+ import { onMount, untrack } from 'svelte';
10
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
10
11
  import { fly, fade } from 'svelte/transition';
11
12
  import { cubicOut } from 'svelte/easing';
12
13
 
@@ -42,6 +43,13 @@
42
43
  * is open.
43
44
  */
44
45
  trapKeys?: boolean;
46
+ /**
47
+ * Shortcut registry to push a modal context into while open — page
48
+ * shortcuts registered there stop firing behind it (jatui-voc3i.3).
49
+ */
50
+ shortcutRegistry?: ShortcutRegistry;
51
+ /** Context name pushed while open. Default `'modal'`. */
52
+ shortcutContext?: string;
45
53
  onclose?: () => void;
46
54
  onopen?: () => void;
47
55
  header?: import('svelte').Snippet;
@@ -64,6 +72,8 @@
64
72
  dismissible = true,
65
73
  zIndex = 1000,
66
74
  trapKeys = true,
75
+ shortcutRegistry,
76
+ shortcutContext = 'modal',
67
77
  onclose,
68
78
  onopen,
69
79
  header,
@@ -110,6 +120,13 @@
110
120
  }
111
121
  });
112
122
 
123
+ // While open, a modal shortcut context keeps the host page's registry
124
+ // shortcuts from firing behind it.
125
+ $effect(() => {
126
+ if (!open || !shortcutRegistry) return;
127
+ return shortcutRegistry.pushContext(shortcutContext, { modal: true, label: untrack(() => title) || undefined });
128
+ });
129
+
113
130
  function close() {
114
131
  if (dismissible) {
115
132
  open = false;
@@ -124,6 +141,21 @@
124
141
  }
125
142
 
126
143
  function handleKeyDown(event: KeyboardEvent) {
144
+ // Escape is owned HERE on the dialog element, not by <svelte:window>.
145
+ // Modal focuses the dialog (tabindex=-1) on open, so in the default state
146
+ // keydown dispatches from the dialog itself — and the trapKeys
147
+ // stopPropagation below (which under Svelte 5 delegated events also
148
+ // blocks every window listener) would swallow Escape before the window
149
+ // handler could act: Esc did nothing unless focus was in a text field.
150
+ // Handling it at element level also guarantees a Modal opened inside a
151
+ // host Drawer closes before the drawer does.
152
+ if (open && event.key === 'Escape' && closeOnEscape) {
153
+ event.preventDefault();
154
+ event.stopPropagation();
155
+ close();
156
+ return;
157
+ }
158
+
127
159
  // Stop keydown events from bubbling to window while the modal is open.
128
160
  // This prevents page-level keyboard navigation (listNav-style handlers
129
161
  // on window) from reacting to arrow keys, j/k, vim chords, digit counts,
@@ -247,4 +279,21 @@
247
279
  align-items: center;
248
280
  justify-content: center;
249
281
  }
282
+
283
+ /*
284
+ * DaisyUI 5 puts .modal-box at opacity:0 (layer daisyui.l1.l2.l3) and only
285
+ * restores it from `.modal[open] .modal-box { opacity: 1 }` in the outer
286
+ * layer — a rule that assumes daisyUI's canonical markup where the box is a
287
+ * direct grid child of the dialog. jatui nests the box inside
288
+ * .modal-backdrop, and the restore rule loses the cascade: the panel stays
289
+ * invisible unless the host adds a global override (the jat IDE had to in
290
+ * app.css, jat-vte6e.17). Un-layered author styles beat @layer rules, so
291
+ * declaring the open state here makes the component render correctly on
292
+ * its own. Entrance motion stays owned by the in:fly transition on the box.
293
+ */
294
+ :global(dialog.modal[open] .modal-box) {
295
+ opacity: 1;
296
+ translate: 0;
297
+ scale: 1;
298
+ }
250
299
  </style>
@@ -1,3 +1,4 @@
1
+ import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
1
2
  interface Props {
2
3
  open?: boolean;
3
4
  title?: string;
@@ -30,6 +31,13 @@ interface Props {
30
31
  * is open.
31
32
  */
32
33
  trapKeys?: boolean;
34
+ /**
35
+ * Shortcut registry to push a modal context into while open — page
36
+ * shortcuts registered there stop firing behind it (jatui-voc3i.3).
37
+ */
38
+ shortcutRegistry?: ShortcutRegistry;
39
+ /** Context name pushed while open. Default `'modal'`. */
40
+ shortcutContext?: string;
33
41
  onclose?: () => void;
34
42
  onopen?: () => void;
35
43
  header?: import('svelte').Snippet;