@joewinke/jatui 0.21.0 → 0.23.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 (47) hide show
  1. package/CLAUDE.md +47 -1
  2. package/dist/actions/reveal.d.ts +37 -0
  3. package/dist/actions/reveal.js +42 -0
  4. package/dist/components/AudioRecorder.svelte +749 -0
  5. package/dist/components/AudioRecorder.svelte.d.ts +36 -0
  6. package/dist/components/IconSelector.svelte +417 -0
  7. package/dist/components/IconSelector.svelte.d.ts +55 -0
  8. package/dist/components/InfiniteGrid.svelte +305 -0
  9. package/dist/components/InfiniteGrid.svelte.d.ts +71 -0
  10. package/dist/components/InfiniteSearchGrid.svelte +131 -0
  11. package/dist/components/InfiniteSearchGrid.svelte.d.ts +67 -0
  12. package/dist/components/KeyboardShortcutsOverlay.svelte +40 -3
  13. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +31 -16
  14. package/dist/components/MediaCaptureIcon.svelte +23 -0
  15. package/dist/components/MediaCaptureIcon.svelte.d.ts +1 -1
  16. package/dist/components/ThumbnailGrid.svelte +137 -0
  17. package/dist/components/ThumbnailGrid.svelte.d.ts +42 -0
  18. package/dist/components/batch/BatchJobTable.svelte +549 -0
  19. package/dist/components/batch/BatchJobTable.svelte.d.ts +114 -0
  20. package/dist/components/batch/QuotaMeter.svelte +78 -0
  21. package/dist/components/batch/QuotaMeter.svelte.d.ts +16 -0
  22. package/dist/components/batch/batchQueue.d.ts +56 -0
  23. package/dist/components/batch/batchQueue.js +81 -0
  24. package/dist/components/batch/elapsedEstimate.d.ts +24 -0
  25. package/dist/components/batch/elapsedEstimate.js +25 -0
  26. package/dist/components/batch/pollOperation.d.ts +45 -0
  27. package/dist/components/batch/pollOperation.js +80 -0
  28. package/dist/components/batch/useElapsedEstimate.d.ts +10 -0
  29. package/dist/components/batch/useElapsedEstimate.js +38 -0
  30. package/dist/components/infiniteGridLoader.d.ts +70 -0
  31. package/dist/components/infiniteGridLoader.js +141 -0
  32. package/dist/components/table/columnResize.d.ts +38 -0
  33. package/dist/components/table/columnResize.js +117 -0
  34. package/dist/components/table/index.d.ts +2 -0
  35. package/dist/components/table/index.js +1 -0
  36. package/dist/components/variant/VariantMatrixPicker.svelte +181 -0
  37. package/dist/components/variant/VariantMatrixPicker.svelte.d.ts +37 -0
  38. package/dist/components/variant/variantCombinations.d.ts +75 -0
  39. package/dist/components/variant/variantCombinations.js +100 -0
  40. package/dist/history.d.ts +44 -0
  41. package/dist/history.js +87 -0
  42. package/dist/index.d.ts +30 -0
  43. package/dist/index.js +27 -0
  44. package/dist/styles/motion.css +100 -0
  45. package/dist/utils/keyboardShortcutRegistry.d.ts +152 -0
  46. package/dist/utils/keyboardShortcutRegistry.js +365 -0
  47. package/package.json +5 -1
@@ -0,0 +1,36 @@
1
+ /** What a finished recording hands back to the host — mirrors MediaCaptureResult's shape for the audio kind. */
2
+ export interface AudioRecordingResult {
3
+ blob: Blob;
4
+ mimeType: string;
5
+ durationSeconds: number;
6
+ sizeBytes: number;
7
+ }
8
+ /** m:ss zero-padded seconds — shared format for the timer display. */
9
+ export declare function formatDuration(totalSeconds: number): string;
10
+ /** 1.2 KB / 3.4 MB style byte formatting for the size readout. */
11
+ export declare function formatFileSize(bytes: number): string;
12
+ interface Props {
13
+ /** `small` — inline chat-style pill (default). `large` — full recorder panel with waveform canvas. */
14
+ variant?: 'small' | 'large';
15
+ /** Auto-stop ceiling in ms. 0 (default) records until stopped manually. */
16
+ maxDurationMs?: number;
17
+ disabled?: boolean;
18
+ /** Fired once the mic stream is live and recording has started. */
19
+ onRecordStart?: () => void;
20
+ onRecordPause?: () => void;
21
+ onRecordResume?: () => void;
22
+ /** Fired with the finished recording. May return a Promise — the
23
+ * component stays in "processing" until it settles, so hosts can
24
+ * upload/transcribe without the user re-triggering a recording. */
25
+ onRecordStop?: (result: AudioRecordingResult) => void | Promise<void>;
26
+ /** Fired when the user cancels — no blob is produced. */
27
+ onRecordCancel?: () => void;
28
+ /** Fired on recorder errors (permission denied, no mic, start failure). */
29
+ onError?: (message: string) => void;
30
+ class?: string;
31
+ }
32
+ declare const AudioRecorder: import("svelte").Component<Props, {
33
+ reset: () => void;
34
+ }, "">;
35
+ type AudioRecorder = ReturnType<typeof AudioRecorder>;
36
+ export default AudioRecorder;
@@ -0,0 +1,417 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * IconSelector — searchable, paged icon picker for assigning an icon to an item.
4
+ *
5
+ * Ported from chimaro's IconSelector.svelte and generalized: the icon set is
6
+ * fully prop-driven (no hardcoded list, no icon-library dependency). Pass any
7
+ * array of `IconSelectorOption`s — for lucide-svelte consumers that is simply
8
+ *
9
+ * const icons = Object.entries(lucideIconMap).map(([name, component]) => ({
10
+ * name, component,
11
+ * }));
12
+ * <IconSelector {icons} bind:selectedIcon={value} />
13
+ *
14
+ * Icons without a `component` (or all icons, via the `icon` snippet) can be
15
+ * rendered with the optional `icon` snippet for full control.
16
+ */
17
+ import type { Component, Snippet } from 'svelte';
18
+
19
+ /** Props an icon component receives when rendered by the grid/trigger. */
20
+ export interface IconComponentProps {
21
+ size?: number;
22
+ class?: string;
23
+ }
24
+
25
+ export interface IconSelectorOption {
26
+ /** Stable identifier — this is the value stored/selected. */
27
+ name: string;
28
+ /** Human-readable label; defaults to `name`. Also searched. */
29
+ label?: string;
30
+ /** Extra search terms (e.g. synonyms). */
31
+ keywords?: string[];
32
+ /** Svelte component rendering the icon; receives `{ size, class }`. */
33
+ component?: Component<IconComponentProps>;
34
+ }
35
+ </script>
36
+
37
+ <script lang="ts">
38
+ import { slide } from 'svelte/transition';
39
+
40
+ interface Props {
41
+ /** Name of the currently selected icon (from `IconSelectorOption.name`). */
42
+ selectedIcon?: string | null;
43
+ /** The icon set to pick from — replaces chimaro's hardcoded list. */
44
+ icons: IconSelectorOption[];
45
+ /** Fires with the chosen name, or `null` when cleared. */
46
+ onSelect?: (icon: string | null) => void;
47
+ /** Extra class forwarded to the rendered icon components (e.g. a text color). */
48
+ colorClass?: string;
49
+ /** Trigger row label. */
50
+ label?: string;
51
+ /** Auto-close the panel shortly after a selection is made. */
52
+ compact?: boolean;
53
+ /** Grid columns per page. */
54
+ columns?: number;
55
+ /** Icons shown per page (default: 4 rows of 8). */
56
+ perPage?: number;
57
+ /** Optional custom renderer; overrides `option.component` when provided. */
58
+ icon?: Snippet<[IconSelectorOption]>;
59
+ class?: string;
60
+ }
61
+
62
+ let {
63
+ selectedIcon = $bindable(null),
64
+ icons,
65
+ onSelect = () => {},
66
+ colorClass = '',
67
+ label = 'Icon',
68
+ compact = false,
69
+ columns = 8,
70
+ perPage = 32,
71
+ icon,
72
+ class: className = ''
73
+ }: Props = $props();
74
+
75
+ let showSelector = $state(false);
76
+ let currentPage = $state(0);
77
+ let searchQuery = $state('');
78
+ let selectionAnimating = $state(false);
79
+ let lastSelectedIcon = $state<string | null>(null);
80
+ let searchInputEl: HTMLInputElement | undefined = $state(undefined);
81
+ let gridEl: HTMLElement | undefined = $state(undefined);
82
+
83
+ const filteredIcons = $derived.by(() => {
84
+ const q = searchQuery.trim().toLowerCase();
85
+ if (!q) return icons;
86
+ return icons.filter((i) => {
87
+ const haystack = `${i.label ?? i.name} ${i.name} ${(i.keywords ?? []).join(' ')}`.toLowerCase();
88
+ return haystack.includes(q);
89
+ });
90
+ });
91
+ const totalPages = $derived(Math.max(1, Math.ceil(filteredIcons.length / perPage)));
92
+
93
+ function nextPage() {
94
+ if (currentPage < totalPages - 1) currentPage++;
95
+ }
96
+ function prevPage() {
97
+ if (currentPage > 0) currentPage--;
98
+ }
99
+
100
+ function handleIconSelect(option: IconSelectorOption) {
101
+ if (option.name !== selectedIcon) {
102
+ lastSelectedIcon = option.name;
103
+ selectionAnimating = true;
104
+ selectedIcon = option.name;
105
+ onSelect(option.name);
106
+ setTimeout(() => {
107
+ selectionAnimating = false;
108
+ if (compact) setTimeout(() => (showSelector = false), 800);
109
+ }, 500);
110
+ } else {
111
+ onSelect(option.name);
112
+ }
113
+ }
114
+
115
+ function clearIcon() {
116
+ selectedIcon = null;
117
+ onSelect(null);
118
+ }
119
+
120
+ function focusGridStart() {
121
+ const btn = gridEl?.querySelector<HTMLButtonElement>('.active-page button.icon-btn');
122
+ btn?.focus();
123
+ }
124
+
125
+ /** Roving-focus keyboard nav over the visible page's icon buttons. */
126
+ function handleGridKeydown(e: KeyboardEvent) {
127
+ const page = gridEl?.querySelector('.active-page');
128
+ const buttons = Array.from(page?.querySelectorAll<HTMLButtonElement>('button.icon-btn') ?? []);
129
+ if (!buttons.length) return;
130
+ const idx = buttons.indexOf(document.activeElement as HTMLButtonElement);
131
+ let next: number | null = null;
132
+ switch (e.key) {
133
+ case 'ArrowRight':
134
+ next = idx < 0 ? 0 : idx + 1;
135
+ break;
136
+ case 'ArrowLeft':
137
+ next = idx <= 0 ? 0 : idx - 1;
138
+ break;
139
+ case 'ArrowDown':
140
+ next = idx < 0 ? 0 : idx + columns;
141
+ break;
142
+ case 'ArrowUp':
143
+ next = idx < 0 ? 0 : idx - columns;
144
+ break;
145
+ case 'Home':
146
+ next = 0;
147
+ break;
148
+ case 'End':
149
+ next = buttons.length - 1;
150
+ break;
151
+ case 'PageDown':
152
+ e.preventDefault();
153
+ nextPage();
154
+ return;
155
+ case 'PageUp':
156
+ e.preventDefault();
157
+ prevPage();
158
+ return;
159
+ case 'Escape':
160
+ showSelector = false;
161
+ return;
162
+ case 'Enter':
163
+ // Explicit so Enter selects reliably even where the native button
164
+ // default is unavailable (synthetic/test key events).
165
+ if (document.activeElement?.classList.contains('icon-btn')) {
166
+ e.preventDefault();
167
+ (document.activeElement as HTMLElement).click();
168
+ }
169
+ return;
170
+ default:
171
+ return;
172
+ }
173
+ e.preventDefault();
174
+ buttons[Math.max(0, Math.min(next, buttons.length - 1))]?.focus();
175
+ }
176
+
177
+ // Reset pagination when the search query changes
178
+ $effect(() => {
179
+ void searchQuery;
180
+ currentPage = 0;
181
+ });
182
+
183
+ // Reset search when the selector is closed and reopened
184
+ $effect(() => {
185
+ if (!showSelector) searchQuery = '';
186
+ });
187
+
188
+ // Focus the search input when the panel opens
189
+ $effect(() => {
190
+ if (showSelector) requestAnimationFrame(() => searchInputEl?.focus());
191
+ });
192
+ </script>
193
+
194
+ <div class="form-control {className}">
195
+ <button
196
+ type="button"
197
+ class="flex w-full items-center justify-between rounded-lg px-2 py-3 transition-colors hover:bg-base-200"
198
+ onclick={() => (showSelector = !showSelector)}
199
+ aria-expanded={showSelector}
200
+ >
201
+ <span class="label flex cursor-pointer items-center gap-2">
202
+ <span class="label-text font-medium">{label}</span>
203
+ {#if selectedIcon}
204
+ {@const selected = icons.find((i) => i.name === selectedIcon)}
205
+ <span class="icon-preview ml-5 inline-flex">
206
+ {#if icon}
207
+ {@render icon(selected!)}
208
+ {:else if selected?.component}
209
+ <selected.component size={22} class={colorClass} />
210
+ {/if}
211
+ </span>
212
+ {/if}
213
+ </span>
214
+ <svg
215
+ xmlns="http://www.w3.org/2000/svg"
216
+ width="18"
217
+ height="18"
218
+ viewBox="0 0 24 24"
219
+ fill="none"
220
+ stroke="currentColor"
221
+ stroke-width="2"
222
+ stroke-linecap="round"
223
+ stroke-linejoin="round"
224
+ class="chevron-icon transition-transform duration-300 {showSelector ? 'rotate-180' : ''}"
225
+ >
226
+ <polyline points="6 9 12 15 18 9"></polyline>
227
+ </svg>
228
+ </button>
229
+
230
+ {#if showSelector}
231
+ <div class="icon-selector-wrapper pt-2" transition:slide={{ duration: 200 }}>
232
+ <!-- Search bar -->
233
+ <div class="relative mb-3">
234
+ <input
235
+ bind:this={searchInputEl}
236
+ type="text"
237
+ bind:value={searchQuery}
238
+ placeholder="Search icons..."
239
+ class="input input-sm input-bordered focus:outline-none w-full pr-8"
240
+ aria-label="Search icons"
241
+ onkeydown={(e) => {
242
+ if (e.key === 'Escape') showSelector = false;
243
+ if (e.key === 'ArrowDown') {
244
+ e.preventDefault();
245
+ focusGridStart();
246
+ }
247
+ }}
248
+ />
249
+ {#if searchQuery}
250
+ <button
251
+ type="button"
252
+ class="absolute top-1/2 right-3 -translate-y-1/2 text-base-content/50 transition-colors hover:text-base-content"
253
+ onclick={() => (searchQuery = '')}
254
+ aria-label="Clear search"
255
+ >
256
+ <svg
257
+ xmlns="http://www.w3.org/2000/svg"
258
+ width="14"
259
+ height="14"
260
+ viewBox="0 0 24 24"
261
+ fill="none"
262
+ stroke="currentColor"
263
+ stroke-width="2"
264
+ stroke-linecap="round"
265
+ stroke-linejoin="round"
266
+ >
267
+ <path d="M18 6 6 18"></path>
268
+ <path d="m6 6 12 12"></path>
269
+ </svg>
270
+ </button>
271
+ {/if}
272
+ </div>
273
+
274
+ <!-- Icons grid with overflow hidden for the pagination slide -->
275
+ <div
276
+ bind:this={gridEl}
277
+ class="icon-grid-container overflow-hidden rounded-lg bg-base-200"
278
+ onkeydown={handleGridKeydown}
279
+ >
280
+ <div
281
+ class="icon-grid-slider flex transition-transform duration-300"
282
+ style="transform: translateX(-{currentPage * 100}%)"
283
+ >
284
+ {#each Array(totalPages) as _, pageIndex (pageIndex)}
285
+ <div
286
+ class="icon-grid-page min-w-full shrink-0"
287
+ class:active-page={pageIndex === currentPage}
288
+ >
289
+ <div class="grid gap-2 p-4" style="grid-template-columns: repeat({columns}, minmax(0, 1fr))">
290
+ {#each filteredIcons.slice(pageIndex * perPage, (pageIndex + 1) * perPage) as option (option.name)}
291
+ <button
292
+ type="button"
293
+ class="icon-btn flex aspect-square items-center justify-center rounded-lg p-1 transition-all duration-200 hover:scale-105 hover:bg-base-300
294
+ {selectedIcon === option.name ? 'bg-base-300 ring-2 ring-primary/50' : ''}
295
+ {selectionAnimating && lastSelectedIcon === option.name ? 'selecting' : ''}"
296
+ onclick={() => handleIconSelect(option)}
297
+ aria-label="Select {option.label ?? option.name} icon"
298
+ aria-pressed={selectedIcon === option.name}
299
+ title={option.label ?? option.name}
300
+ >
301
+ {#if icon}
302
+ {@render icon(option)}
303
+ {:else if option.component}
304
+ <option.component size={24} class={colorClass} />
305
+ {/if}
306
+ </button>
307
+ {:else}
308
+ <div class="col-span-full py-6 text-center text-base-content/50">
309
+ No icons match your search
310
+ </div>
311
+ {/each}
312
+ </div>
313
+ </div>
314
+ {/each}
315
+ </div>
316
+ </div>
317
+
318
+ <!-- Pagination controls -->
319
+ <div class="mt-3 flex items-center justify-between">
320
+ <div class="text-base-content/70 text-xs">
321
+ {#if filteredIcons.length > 0}
322
+ Showing {Math.min(filteredIcons.length, currentPage * perPage + 1)}-{Math.min(
323
+ filteredIcons.length,
324
+ (currentPage + 1) * perPage
325
+ )} of {filteredIcons.length} icons
326
+ {:else}
327
+ No icons found
328
+ {/if}
329
+ </div>
330
+
331
+ <div class="flex gap-2">
332
+ <button
333
+ type="button"
334
+ class="btn btn-ghost btn-sm btn-circle {currentPage === 0 ? 'opacity-30' : ''}"
335
+ onclick={prevPage}
336
+ disabled={currentPage === 0}
337
+ aria-label="Previous page"
338
+ >
339
+ <svg
340
+ xmlns="http://www.w3.org/2000/svg"
341
+ width="16"
342
+ height="16"
343
+ viewBox="0 0 24 24"
344
+ fill="none"
345
+ stroke="currentColor"
346
+ stroke-width="2"
347
+ stroke-linecap="round"
348
+ stroke-linejoin="round"
349
+ >
350
+ <path d="m15 18-6-6 6-6"></path>
351
+ </svg>
352
+ </button>
353
+
354
+ <button
355
+ type="button"
356
+ class="btn btn-ghost btn-sm btn-circle {currentPage >= totalPages - 1 ? 'opacity-30' : ''}"
357
+ onclick={nextPage}
358
+ disabled={currentPage >= totalPages - 1}
359
+ aria-label="Next page"
360
+ >
361
+ <svg
362
+ xmlns="http://www.w3.org/2000/svg"
363
+ width="16"
364
+ height="16"
365
+ viewBox="0 0 24 24"
366
+ fill="none"
367
+ stroke="currentColor"
368
+ stroke-width="2"
369
+ stroke-linecap="round"
370
+ stroke-linejoin="round"
371
+ >
372
+ <path d="m9 18 6-6-6-6"></path>
373
+ </svg>
374
+ </button>
375
+ </div>
376
+ </div>
377
+
378
+ {#if selectedIcon}
379
+ <div class="mt-2 flex justify-end">
380
+ <button
381
+ type="button"
382
+ class="text-base-content/60 hover:bg-base-200 hover:text-base-content rounded px-2 py-1 text-xs transition-colors"
383
+ onclick={clearIcon}
384
+ >
385
+ Clear icon
386
+ </button>
387
+ </div>
388
+ {/if}
389
+ </div>
390
+ {/if}
391
+ </div>
392
+
393
+ <style>
394
+ .icon-btn {
395
+ position: relative;
396
+ }
397
+
398
+ .icon-btn.selecting {
399
+ transform: scale(1.15);
400
+ box-shadow:
401
+ 0 0 0 4px oklch(var(--color-base-100) / 0.3),
402
+ 0 0 15px oklch(var(--color-primary) / 0.5);
403
+ animation: icon-pulse 0.5s ease;
404
+ }
405
+
406
+ @keyframes icon-pulse {
407
+ 0% {
408
+ transform: scale(1);
409
+ }
410
+ 50% {
411
+ transform: scale(1.15);
412
+ }
413
+ 100% {
414
+ transform: scale(1.1);
415
+ }
416
+ }
417
+ </style>
@@ -0,0 +1,55 @@
1
+ /**
2
+ * IconSelector — searchable, paged icon picker for assigning an icon to an item.
3
+ *
4
+ * Ported from chimaro's IconSelector.svelte and generalized: the icon set is
5
+ * fully prop-driven (no hardcoded list, no icon-library dependency). Pass any
6
+ * array of `IconSelectorOption`s — for lucide-svelte consumers that is simply
7
+ *
8
+ * const icons = Object.entries(lucideIconMap).map(([name, component]) => ({
9
+ * name, component,
10
+ * }));
11
+ * <IconSelector {icons} bind:selectedIcon={value} />
12
+ *
13
+ * Icons without a `component` (or all icons, via the `icon` snippet) can be
14
+ * rendered with the optional `icon` snippet for full control.
15
+ */
16
+ import type { Component, Snippet } from 'svelte';
17
+ /** Props an icon component receives when rendered by the grid/trigger. */
18
+ export interface IconComponentProps {
19
+ size?: number;
20
+ class?: string;
21
+ }
22
+ export interface IconSelectorOption {
23
+ /** Stable identifier — this is the value stored/selected. */
24
+ name: string;
25
+ /** Human-readable label; defaults to `name`. Also searched. */
26
+ label?: string;
27
+ /** Extra search terms (e.g. synonyms). */
28
+ keywords?: string[];
29
+ /** Svelte component rendering the icon; receives `{ size, class }`. */
30
+ component?: Component<IconComponentProps>;
31
+ }
32
+ interface Props {
33
+ /** Name of the currently selected icon (from `IconSelectorOption.name`). */
34
+ selectedIcon?: string | null;
35
+ /** The icon set to pick from — replaces chimaro's hardcoded list. */
36
+ icons: IconSelectorOption[];
37
+ /** Fires with the chosen name, or `null` when cleared. */
38
+ onSelect?: (icon: string | null) => void;
39
+ /** Extra class forwarded to the rendered icon components (e.g. a text color). */
40
+ colorClass?: string;
41
+ /** Trigger row label. */
42
+ label?: string;
43
+ /** Auto-close the panel shortly after a selection is made. */
44
+ compact?: boolean;
45
+ /** Grid columns per page. */
46
+ columns?: number;
47
+ /** Icons shown per page (default: 4 rows of 8). */
48
+ perPage?: number;
49
+ /** Optional custom renderer; overrides `option.component` when provided. */
50
+ icon?: Snippet<[IconSelectorOption]>;
51
+ class?: string;
52
+ }
53
+ declare const IconSelector: Component<Props, {}, "selectedIcon">;
54
+ type IconSelector = ReturnType<typeof IconSelector>;
55
+ export default IconSelector;