@joewinke/jatui 0.26.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 (73) hide show
  1. package/CLAUDE.md +106 -1
  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/Modal.svelte +18 -1
  23. package/dist/components/Modal.svelte.d.ts +8 -0
  24. package/dist/components/basic/Empty.svelte +173 -9
  25. package/dist/components/basic/Empty.svelte.d.ts +50 -1
  26. package/dist/components/basic/ErrorState.svelte +128 -0
  27. package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
  28. package/dist/components/basic/JumpToLatest.svelte +60 -0
  29. package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
  30. package/dist/components/basic/PageHeader.svelte +216 -0
  31. package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
  32. package/dist/components/basic/ProgressButton.svelte +221 -0
  33. package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
  34. package/dist/components/graph/GraphApiBridge.svelte +36 -0
  35. package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
  36. package/dist/components/graph/GraphCanvas.svelte +212 -27
  37. package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
  38. package/dist/components/graph/api.d.ts +52 -0
  39. package/dist/components/graph/api.js +66 -0
  40. package/dist/components/graph/index.d.ts +2 -0
  41. package/dist/components/graph/index.js +1 -0
  42. package/dist/components/graph/theme.js +13 -0
  43. package/dist/components/messaging/MessageItem.svelte +5 -0
  44. package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
  45. package/dist/components/table/DataTable.svelte +452 -78
  46. package/dist/components/table/DataTable.svelte.d.ts +2 -2
  47. package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
  48. package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
  49. package/dist/components/table/columnStorage.d.ts +29 -0
  50. package/dist/components/table/columnStorage.js +61 -0
  51. package/dist/components/table/groupRows.d.ts +72 -0
  52. package/dist/components/table/groupRows.js +118 -0
  53. package/dist/components/table/index.d.ts +7 -1
  54. package/dist/components/table/index.js +3 -0
  55. package/dist/components/table/types.d.ts +121 -20
  56. package/dist/index.d.ts +25 -2
  57. package/dist/index.js +32 -1
  58. package/dist/utils/ansiToHtml.d.ts +83 -0
  59. package/dist/utils/ansiToHtml.js +543 -0
  60. package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
  61. package/dist/utils/keyboardShortcutRegistry.js +1070 -76
  62. package/dist/utils/pollBackoff.d.ts +83 -0
  63. package/dist/utils/pollBackoff.js +151 -0
  64. package/dist/utils/snapFlick.d.ts +59 -0
  65. package/dist/utils/snapFlick.js +66 -0
  66. package/dist/video/advisors/qr.d.ts +1 -1
  67. package/dist/virtual/createRowWindow.d.ts +75 -0
  68. package/dist/virtual/createRowWindow.js +129 -0
  69. package/dist/virtual/index.d.ts +4 -0
  70. package/dist/virtual/index.js +5 -0
  71. package/dist/virtual/rowWindowCore.d.ts +202 -0
  72. package/dist/virtual/rowWindowCore.js +510 -0
  73. package/package.json +11 -1
@@ -0,0 +1,466 @@
1
+ <script module lang="ts">
2
+ export type ManageColumnsProps = {
3
+ /** Current column order (full ids, including hidden ones) */
4
+ columnOrder: string[];
5
+ /** Ids currently hidden */
6
+ hiddenColumns: Set<string>;
7
+ /** Resolve a column id to its display label */
8
+ getLabel: (id: string) => string;
9
+ /** Column order changed (drag-to-reorder) */
10
+ onReorder: (newOrder: string[]) => void;
11
+ /** A column's visibility was toggled */
12
+ onToggleVisibility: (id: string) => void;
13
+ /** When set, a "Reset to default" footer button is rendered */
14
+ onReset?: () => void;
15
+ /** Trigger button text */
16
+ label?: string;
17
+ /**
18
+ * Render the panel with position:fixed viewport coordinates — escapes
19
+ * overflow-x:auto / overflow:hidden parents that would otherwise clip it
20
+ * (jat-hwt1h: the IDE's original was clipped by its table's scroll
21
+ * container). Default on; turn off for an inline absolute panel.
22
+ */
23
+ appendToBody?: boolean;
24
+ };
25
+ </script>
26
+
27
+ <script lang="ts">
28
+ import { tick } from 'svelte';
29
+
30
+ let {
31
+ columnOrder,
32
+ hiddenColumns,
33
+ getLabel,
34
+ onReorder,
35
+ onToggleVisibility,
36
+ onReset,
37
+ label = 'Columns',
38
+ appendToBody = true,
39
+ }: ManageColumnsProps = $props();
40
+
41
+ // Number of currently visible columns. When exactly one is visible, its
42
+ // toggle must be disabled so the user can't hide the last column.
43
+ const visibleCount = $derived(columnOrder.length - hiddenColumns.size);
44
+
45
+ // Internal open/search state
46
+ let open = $state(false);
47
+ let search = $state('');
48
+ let searchInput: HTMLInputElement | undefined = $state();
49
+ let triggerRef: HTMLButtonElement | undefined = $state();
50
+ let containerRef: HTMLDivElement | undefined = $state();
51
+ let panelStyle = $state('');
52
+
53
+ // Auto-focus the search input when opened
54
+ $effect(() => {
55
+ if (!open) return;
56
+ if (appendToBody) {
57
+ positionPanel();
58
+ // A position:fixed panel is anchored to the VIEWPORT, so re-anchor on
59
+ // every scroll (capture: any scroll container) and resize; close once
60
+ // the trigger leaves the view. Same contract as SearchDropdown's
61
+ // appendToBody mode.
62
+ const reanchor = (e?: Event) => {
63
+ if (e && e.target instanceof Node && containerRef?.contains(e.target)) return;
64
+ const r = triggerRef?.getBoundingClientRect();
65
+ if (!r || r.bottom < 0 || r.top > window.innerHeight) {
66
+ open = false;
67
+ search = '';
68
+ return;
69
+ }
70
+ positionPanel();
71
+ };
72
+ window.addEventListener('scroll', reanchor, { capture: true, passive: true });
73
+ window.addEventListener('resize', reanchor, { passive: true });
74
+ document.addEventListener('mousedown', handleClickOutside);
75
+ tick().then(() => searchInput?.focus());
76
+ return () => {
77
+ window.removeEventListener('scroll', reanchor, { capture: true });
78
+ window.removeEventListener('resize', reanchor);
79
+ document.removeEventListener('mousedown', handleClickOutside);
80
+ };
81
+ }
82
+ tick().then(() => searchInput?.focus());
83
+ document.addEventListener('mousedown', handleClickOutside);
84
+ return () => document.removeEventListener('mousedown', handleClickOutside);
85
+ });
86
+
87
+ function positionPanel() {
88
+ if (!triggerRef) return;
89
+ const rect = triggerRef.getBoundingClientRect();
90
+ const width = Math.max(rect.width, 200);
91
+ // Viewport-aware flip: grow upward when there isn't room below but there
92
+ // is above (rows near the bottom of a scroll area).
93
+ const estPanelHeight = 300;
94
+ const spaceBelow = window.innerHeight - rect.bottom;
95
+ const spaceAbove = rect.top;
96
+ const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
97
+ // Clamp so a right-edge trigger doesn't push the panel off-screen.
98
+ const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8));
99
+ // Each branch must explicitly reset the property it isn't using — the
100
+ // inline panel's class rule sets top: calc(100% + 4px).
101
+ const vertical = flipUp
102
+ ? `bottom: ${window.innerHeight - rect.top + 4}px; top: auto;`
103
+ : `top: ${rect.bottom + 4}px; bottom: auto;`;
104
+ panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
105
+ }
106
+
107
+ function handleClickOutside(e: MouseEvent) {
108
+ if (containerRef && !containerRef.contains(e.target as Node)) {
109
+ open = false;
110
+ search = '';
111
+ }
112
+ }
113
+
114
+ // Drag-to-reorder state (disabled while searching)
115
+ let draggedIndex = $state<number | null>(null);
116
+ let dragOverIndex = $state<number | null>(null);
117
+
118
+ function handleDragStart(index: number, e: DragEvent) {
119
+ draggedIndex = index;
120
+ if (e.dataTransfer) {
121
+ e.dataTransfer.effectAllowed = 'move';
122
+ e.dataTransfer.setData('text/plain', String(index));
123
+ }
124
+ }
125
+
126
+ function handleDragOver(index: number, e: DragEvent) {
127
+ e.preventDefault();
128
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
129
+ dragOverIndex = index;
130
+ }
131
+
132
+ function handleDragEnd() {
133
+ draggedIndex = null;
134
+ dragOverIndex = null;
135
+ }
136
+
137
+ function handleDrop(index: number, e: DragEvent) {
138
+ e.preventDefault();
139
+ if (draggedIndex === null || draggedIndex === index) {
140
+ handleDragEnd();
141
+ return;
142
+ }
143
+ const newOrder = [...columnOrder];
144
+ const [moved] = newOrder.splice(draggedIndex, 1);
145
+ newOrder.splice(index, 0, moved);
146
+ onReorder(newOrder);
147
+ handleDragEnd();
148
+ }
149
+
150
+ function closeDropdown() {
151
+ open = false;
152
+ search = '';
153
+ }
154
+
155
+ const filteredOrder = $derived.by(() => {
156
+ if (!search) return columnOrder;
157
+ const q = search.toLowerCase();
158
+ return columnOrder.filter((id) => getLabel(id).toLowerCase().includes(q));
159
+ });
160
+
161
+ const hasNoMatches = $derived(search.length > 0 && filteredOrder.length === 0);
162
+ </script>
163
+
164
+ <div class="mc-wrapper" bind:this={containerRef}>
165
+ <button
166
+ type="button"
167
+ class="mc-trigger"
168
+ bind:this={triggerRef}
169
+ aria-expanded={open}
170
+ onclick={() => {
171
+ open = !open;
172
+ if (!open) search = '';
173
+ }}
174
+ >
175
+ <span>{label}</span>
176
+ <svg class="mc-chevron" class:mc-chevron-open={open} viewBox="0 0 20 20" fill="currentColor">
177
+ <path
178
+ fill-rule="evenodd"
179
+ d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
180
+ clip-rule="evenodd"
181
+ />
182
+ </svg>
183
+ </button>
184
+
185
+ {#if open}
186
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
187
+ <div class="fixed inset-0 z-40" onclick={closeDropdown} onkeydown={() => {}}></div>
188
+ <div
189
+ class="mc-dropdown"
190
+ class:mc-dropdown-fixed={appendToBody}
191
+ style={appendToBody ? panelStyle : undefined}
192
+ >
193
+ <div class="mc-search">
194
+ <svg
195
+ class="mc-search-icon"
196
+ fill="none"
197
+ viewBox="0 0 24 24"
198
+ stroke="currentColor"
199
+ stroke-width="2"
200
+ >
201
+ <path
202
+ stroke-linecap="round"
203
+ stroke-linejoin="round"
204
+ d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
205
+ />
206
+ </svg>
207
+ <input
208
+ bind:this={searchInput}
209
+ bind:value={search}
210
+ type="text"
211
+ placeholder="Filter columns..."
212
+ class="mc-search-input"
213
+ onkeydown={(e) => {
214
+ if (e.key === 'Escape') {
215
+ e.stopPropagation();
216
+ closeDropdown();
217
+ }
218
+ }}
219
+ autocomplete="off"
220
+ />
221
+ {#if search}
222
+ <button
223
+ type="button"
224
+ aria-label="Clear search"
225
+ class="mc-search-clear"
226
+ onclick={() => {
227
+ search = '';
228
+ searchInput?.focus();
229
+ }}
230
+ >
231
+ <svg
232
+ class="w-3 h-3"
233
+ fill="none"
234
+ viewBox="0 0 24 24"
235
+ stroke="currentColor"
236
+ stroke-width="2"
237
+ >
238
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
239
+ </svg>
240
+ </button>
241
+ {/if}
242
+ </div>
243
+
244
+ {#each filteredOrder as colId (colId)}
245
+ {@const originalIndex = columnOrder.indexOf(colId)}
246
+ {@const isVisible = !hiddenColumns.has(colId)}
247
+ {@const isLastVisible = isVisible && visibleCount === 1}
248
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
249
+ <div
250
+ class="mc-row"
251
+ class:mc-drag-over={dragOverIndex === originalIndex && draggedIndex !== null && draggedIndex !== originalIndex}
252
+ draggable={!search}
253
+ ondragstart={(e) => !search && handleDragStart(originalIndex, e)}
254
+ ondragover={(e) => !search && handleDragOver(originalIndex, e)}
255
+ ondragend={() => !search && handleDragEnd()}
256
+ ondrop={(e) => !search && handleDrop(originalIndex, e)}
257
+ >
258
+ <span class="mc-grip" class:mc-grip-disabled={!!search}>⠿</span>
259
+ <label class="mc-label" class:mc-label-disabled={isLastVisible}>
260
+ <input
261
+ type="checkbox"
262
+ checked={isVisible}
263
+ disabled={isLastVisible}
264
+ title={isLastVisible ? 'At least one must stay visible' : undefined}
265
+ aria-label={
266
+ isLastVisible
267
+ ? `${getLabel(colId)} (at least one column must stay visible)`
268
+ : undefined
269
+ }
270
+ onchange={() => onToggleVisibility(colId)}
271
+ />
272
+ {getLabel(colId)}
273
+ </label>
274
+ </div>
275
+ {/each}
276
+
277
+ {#if hasNoMatches}
278
+ <div class="mc-empty">No columns match "{search}"</div>
279
+ {/if}
280
+
281
+ {#if onReset}
282
+ <div class="mc-footer">
283
+ <button type="button" class="mc-reset" onclick={() => onReset?.()}>
284
+ Reset to default
285
+ </button>
286
+ </div>
287
+ {/if}
288
+ </div>
289
+ {/if}
290
+ </div>
291
+
292
+ <style>
293
+ .mc-wrapper {
294
+ position: relative;
295
+ }
296
+ .mc-trigger {
297
+ padding: 0.25rem 0.5rem;
298
+ border-radius: 0.5rem;
299
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
300
+ font-size: 0.8125rem;
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 0.375rem;
304
+ min-height: 2rem;
305
+ cursor: pointer;
306
+ background: var(--color-base-300, oklch(0.13 0.01 250));
307
+ border: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
308
+ color: var(--color-base-content, currentColor);
309
+ transition:
310
+ background 0.15s,
311
+ border-color 0.15s;
312
+ white-space: nowrap;
313
+ }
314
+ .mc-trigger:hover {
315
+ background: var(--color-base-200, oklch(0.16 0.01 250));
316
+ border-color: var(--color-base-100, oklch(0.2 0.01 250));
317
+ }
318
+ .mc-chevron {
319
+ width: 14px;
320
+ height: 14px;
321
+ opacity: 0.5;
322
+ transition: transform 0.15s;
323
+ }
324
+ .mc-chevron-open {
325
+ transform: rotate(180deg);
326
+ }
327
+ .mc-dropdown {
328
+ position: absolute;
329
+ top: calc(100% + 4px);
330
+ right: 0;
331
+ z-index: 50;
332
+ background: var(--color-base-300, oklch(0.13 0.01 250));
333
+ border: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
334
+ border-radius: 0.375rem;
335
+ box-shadow: 0 8px 24px oklch(0 0 0 / 0.4);
336
+ padding: 0;
337
+ min-width: 180px;
338
+ max-height: 320px;
339
+ overflow-y: auto;
340
+ }
341
+ /* Fixed-mode overrides — positionPanel() sets the inline geometry, the class
342
+ only kills the inline panel's absolute defaults it would otherwise inherit. */
343
+ .mc-dropdown-fixed {
344
+ top: auto;
345
+ right: auto;
346
+ bottom: auto;
347
+ max-height: 320px;
348
+ }
349
+ .mc-search {
350
+ display: flex;
351
+ align-items: center;
352
+ gap: 0.375rem;
353
+ padding: 0.375rem 0.625rem;
354
+ border-bottom: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
355
+ position: sticky;
356
+ top: 0;
357
+ background: var(--color-base-300, oklch(0.13 0.01 250));
358
+ }
359
+ .mc-search-icon {
360
+ width: 0.75rem;
361
+ height: 0.75rem;
362
+ flex-shrink: 0;
363
+ color: oklch(from var(--color-base-content, currentColor) l c h / 50%);
364
+ }
365
+ .mc-search-input {
366
+ flex: 1;
367
+ background: transparent;
368
+ border: none;
369
+ outline: none;
370
+ font-size: 0.625rem;
371
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
372
+ color: oklch(from var(--color-base-content, currentColor) l c h / 95%);
373
+ }
374
+ .mc-search-input::placeholder {
375
+ color: oklch(from var(--color-base-content, currentColor) l c h / 40%);
376
+ }
377
+ .mc-search-clear {
378
+ background: none;
379
+ border: none;
380
+ cursor: pointer;
381
+ padding: 0;
382
+ display: flex;
383
+ color: oklch(from var(--color-base-content, currentColor) l c h / 40%);
384
+ }
385
+ .mc-search-clear:hover {
386
+ opacity: 0.8;
387
+ }
388
+ .mc-empty {
389
+ padding: 0.5rem 0.75rem;
390
+ font-size: 0.625rem;
391
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
392
+ color: oklch(from var(--color-base-content, currentColor) l c h / 50%);
393
+ text-align: center;
394
+ }
395
+ .mc-grip-disabled {
396
+ opacity: 0.2;
397
+ cursor: default;
398
+ }
399
+ .mc-row {
400
+ display: flex;
401
+ align-items: center;
402
+ gap: 0.375rem;
403
+ padding: 0.25rem 0.625rem;
404
+ cursor: grab;
405
+ transition: background 0.1s;
406
+ }
407
+ .mc-row:hover {
408
+ background: var(--color-base-100, oklch(0.2 0.01 250));
409
+ }
410
+ .mc-row.mc-drag-over {
411
+ border-top: 2px solid var(--color-info, oklch(0.72 0.1 240));
412
+ }
413
+ .mc-grip {
414
+ color: oklch(from var(--color-base-content, currentColor) l c h / 40%);
415
+ font-size: 0.75rem;
416
+ cursor: grab;
417
+ user-select: none;
418
+ }
419
+ .mc-label {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 0.375rem;
423
+ font-size: 0.75rem;
424
+ color: oklch(from var(--color-base-content, currentColor) l c h / 95%);
425
+ cursor: pointer;
426
+ user-select: none;
427
+ flex: 1;
428
+ }
429
+ .mc-label input[type='checkbox'] {
430
+ accent-color: var(--color-info, oklch(0.72 0.1 240));
431
+ }
432
+ .mc-label-disabled {
433
+ opacity: 0.55;
434
+ cursor: default;
435
+ }
436
+ .mc-label-disabled input[type='checkbox'] {
437
+ cursor: not-allowed;
438
+ }
439
+ .mc-footer {
440
+ position: sticky;
441
+ bottom: 0;
442
+ padding: 0.375rem 0.625rem;
443
+ border-top: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
444
+ background: var(--color-base-300, oklch(0.13 0.01 250));
445
+ }
446
+ .mc-reset {
447
+ width: 100%;
448
+ background: transparent;
449
+ border: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
450
+ border-radius: 0.375rem;
451
+ padding: 0.25rem 0.5rem;
452
+ font-size: 0.625rem;
453
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
454
+ color: oklch(from var(--color-base-content, currentColor) l c h / 60%);
455
+ cursor: pointer;
456
+ transition:
457
+ background 0.15s,
458
+ border-color 0.15s,
459
+ color 0.15s;
460
+ }
461
+ .mc-reset:hover {
462
+ background: var(--color-base-200, oklch(0.16 0.01 250));
463
+ border-color: var(--color-base-100, oklch(0.2 0.01 250));
464
+ color: oklch(from var(--color-base-content, currentColor) l c h / 95%);
465
+ }
466
+ </style>
@@ -0,0 +1,26 @@
1
+ export type ManageColumnsProps = {
2
+ /** Current column order (full ids, including hidden ones) */
3
+ columnOrder: string[];
4
+ /** Ids currently hidden */
5
+ hiddenColumns: Set<string>;
6
+ /** Resolve a column id to its display label */
7
+ getLabel: (id: string) => string;
8
+ /** Column order changed (drag-to-reorder) */
9
+ onReorder: (newOrder: string[]) => void;
10
+ /** A column's visibility was toggled */
11
+ onToggleVisibility: (id: string) => void;
12
+ /** When set, a "Reset to default" footer button is rendered */
13
+ onReset?: () => void;
14
+ /** Trigger button text */
15
+ label?: string;
16
+ /**
17
+ * Render the panel with position:fixed viewport coordinates — escapes
18
+ * overflow-x:auto / overflow:hidden parents that would otherwise clip it
19
+ * (jat-hwt1h: the IDE's original was clipped by its table's scroll
20
+ * container). Default on; turn off for an inline absolute panel.
21
+ */
22
+ appendToBody?: boolean;
23
+ };
24
+ declare const ManageColumnsDropdown: import("svelte").Component<ManageColumnsProps, {}, "">;
25
+ type ManageColumnsDropdown = ReturnType<typeof ManageColumnsDropdown>;
26
+ export default ManageColumnsDropdown;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Column settings persistence for DataTable-based views (upstreamed from the
3
+ * jat IDE's `ide/src/lib/utils/columnStorage.ts` — audit jat-1alfp wave #4,
4
+ * U9). Pure module: no DOM beyond the guarded `localStorage` reads, no svelte
5
+ * imports — safe to call from any client-side component.
6
+ *
7
+ * Usage:
8
+ * const saved = loadColumnSettings('my-table', allColumnIds);
9
+ * saveColumnSettings('my-table', { order, widths, hidden, sortField, sortDir });
10
+ */
11
+ export interface ColumnSettings {
12
+ order: string[];
13
+ widths: Record<string, number>;
14
+ hidden: string[];
15
+ sortField?: string;
16
+ sortDir?: 'asc' | 'desc';
17
+ }
18
+ /**
19
+ * Save column settings (order, widths, hidden) to localStorage as a single JSON blob.
20
+ */
21
+ export declare function saveColumnSettings(key: string, settings: ColumnSettings): void;
22
+ /**
23
+ * Load column settings from localStorage.
24
+ * Merges any new column IDs (from allColumnIds) not present in the saved order,
25
+ * appending them at the end — so adding a column to a view whose settings were
26
+ * saved earlier does not hide or lose the new column.
27
+ * Returns null if nothing is saved or the data cannot be parsed.
28
+ */
29
+ export declare function loadColumnSettings(key: string, allColumnIds: string[]): ColumnSettings | null;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Column settings persistence for DataTable-based views (upstreamed from the
3
+ * jat IDE's `ide/src/lib/utils/columnStorage.ts` — audit jat-1alfp wave #4,
4
+ * U9). Pure module: no DOM beyond the guarded `localStorage` reads, no svelte
5
+ * imports — safe to call from any client-side component.
6
+ *
7
+ * Usage:
8
+ * const saved = loadColumnSettings('my-table', allColumnIds);
9
+ * saveColumnSettings('my-table', { order, widths, hidden, sortField, sortDir });
10
+ */
11
+ /**
12
+ * Save column settings (order, widths, hidden) to localStorage as a single JSON blob.
13
+ */
14
+ export function saveColumnSettings(key, settings) {
15
+ if (typeof localStorage === 'undefined')
16
+ return;
17
+ try {
18
+ localStorage.setItem(key, JSON.stringify(settings));
19
+ }
20
+ catch {
21
+ // Ignore storage errors (e.g. private browsing quota) — a failed save must
22
+ // never break the table itself; the next save overwrites whatever partial
23
+ // state exists. Deliberate degradation: persistence is best-effort by design.
24
+ }
25
+ }
26
+ /**
27
+ * Load column settings from localStorage.
28
+ * Merges any new column IDs (from allColumnIds) not present in the saved order,
29
+ * appending them at the end — so adding a column to a view whose settings were
30
+ * saved earlier does not hide or lose the new column.
31
+ * Returns null if nothing is saved or the data cannot be parsed.
32
+ */
33
+ export function loadColumnSettings(key, allColumnIds) {
34
+ if (typeof localStorage === 'undefined')
35
+ return null;
36
+ try {
37
+ const raw = localStorage.getItem(key);
38
+ if (!raw)
39
+ return null;
40
+ const saved = JSON.parse(raw);
41
+ const order = (() => {
42
+ if (!saved.order || !Array.isArray(saved.order))
43
+ return allColumnIds;
44
+ const known = new Set(allColumnIds);
45
+ const validOrder = saved.order.filter((id) => known.has(id));
46
+ const missing = allColumnIds.filter((id) => !validOrder.includes(id));
47
+ return [...validOrder, ...missing];
48
+ })();
49
+ const widths = saved.widths && typeof saved.widths === 'object' ? saved.widths : {};
50
+ const hidden = saved.hidden && Array.isArray(saved.hidden) ? saved.hidden : [];
51
+ const sortField = typeof saved.sortField === 'string' ? saved.sortField : undefined;
52
+ const sortDir = saved.sortDir === 'asc' || saved.sortDir === 'desc' ? saved.sortDir : undefined;
53
+ return { order, widths, hidden, sortField, sortDir };
54
+ }
55
+ catch {
56
+ // Corrupt or non-JSON payload: discard and let the caller fall back to its
57
+ // built-in column order. Returning null (rather than throwing) is the whole
58
+ // contract — a broken saved blob must never take the page down with it.
59
+ return null;
60
+ }
61
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * groupRows — grouped render-list flattening (jatui-u6d4e.2).
3
+ *
4
+ * Spec: jat `docs/design/datatable-virtualisation-group-rows.md` §3.2.
5
+ *
6
+ * Turns an already filtered + sorted row array into the flat list a table (or a
7
+ * row window) renders: group header items interleaved with row items, nested to
8
+ * any number of levels. Pure, DOM-free and framework-free so it runs under
9
+ * node:test and can feed a virtualiser directly (one item = one rendered line).
10
+ *
11
+ * - Rows inside a group keep the order the caller passed in — the page's sort
12
+ * comparator and tiebreakers survive untouched. Only GROUPS are reordered,
13
+ * by `level.compare` (default: first-seen order).
14
+ * - A collapsed group still emits its header (with `collapsed: true` and the
15
+ * full `rows`/`count`) but nothing beneath it.
16
+ * - `showHeader` returning false hides that header: its rows (and subgroups)
17
+ * still render, one visual level shallower. A hidden header cannot be
18
+ * toggled, so `collapsed` is ignored for it.
19
+ * - `depth` is the VISUAL nesting level — the number of visible headers above
20
+ * the item — so hidden headers never leave an indentation gap. `path` /
21
+ * `groupPath` always carry the full key path.
22
+ * - `position` (first/middle/last/only) is computed over each contiguous run of
23
+ * rows under the same visible parent, which is what ├ / └ tree glyphs need.
24
+ */
25
+ export interface GroupLevel<T> {
26
+ /** Group key for a row at this level (e.g. task => projectOf(task)). */
27
+ key: (row: T) => string;
28
+ /** Group order. The page owns it (P0-P4, "Unassigned first", milestone
29
+ * sort_order, epics-first…). Default: first-seen order. */
30
+ compare?: (a: string, b: string) => number;
31
+ /** Hide the header when it would add nothing (e.g. < 2 rows and no
32
+ * children). The rows still render, ungrouped. Default: always shown. */
33
+ showHeader?: (key: string, rows: T[]) => boolean;
34
+ }
35
+ export type GroupedRowPosition = 'first' | 'middle' | 'last' | 'only';
36
+ export interface GroupedGroupItem<T> {
37
+ kind: 'group';
38
+ key: string;
39
+ /** Key path from the top level down to (and including) this group. */
40
+ path: string[];
41
+ /** Visual nesting level: number of visible group headers above this one. */
42
+ depth: number;
43
+ label: string;
44
+ /** Every row under this group, collapsed or not, in input order. */
45
+ rows: T[];
46
+ count: number;
47
+ collapsed: boolean;
48
+ }
49
+ export interface GroupedRowItem<T> {
50
+ kind: 'row';
51
+ row: T;
52
+ id: string;
53
+ /** Visual nesting level: number of visible group headers above this row. */
54
+ depth: number;
55
+ /** Key path of the innermost group containing this row ([] when flat). */
56
+ groupPath: string[];
57
+ /** Place within its contiguous run of sibling rows — drives ├ / └ glyphs. */
58
+ position: GroupedRowPosition;
59
+ }
60
+ export type GroupedItem<T = any> = GroupedGroupItem<T> | GroupedRowItem<T>;
61
+ export interface GroupRowsOptions<T> {
62
+ getRowId: (row: T) => string;
63
+ /** Collapsed groups, by path key (see `groupPathKey`), e.g. "jat" or "jat::jat-vte6e". */
64
+ collapsed: ReadonlySet<string>;
65
+ /** Header label for a group path. Default: the group's own key. */
66
+ label?: (path: string[]) => string;
67
+ }
68
+ /** Separator joining a group path into the key used by `collapsed`. */
69
+ export declare const GROUP_PATH_SEPARATOR = "::";
70
+ /** The `collapsed`-set key for a group path: ["jat", "jat-vte6e"] → "jat::jat-vte6e". */
71
+ export declare function groupPathKey(path: readonly string[]): string;
72
+ export declare function groupRows<T>(rows: readonly T[], levels: readonly GroupLevel<T>[], opts: GroupRowsOptions<T>): GroupedItem<T>[];