@joewinke/jatui 0.26.0 → 0.27.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 (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 +24 -2
  57. package/dist/index.js +36 -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 +12 -2
@@ -0,0 +1,510 @@
1
+ /**
2
+ * rowWindowCore — the framework-free engine behind `createRowWindow`
3
+ * (jatui-u6d4e.1, spec: jat docs/design/datatable-virtualisation-group-rows.md §3.1).
4
+ *
5
+ * Variable-height windowing for SPACER-ROW layouts: the consumer renders
6
+ * `paddingTop` px of spacer, the rendered slice in document flow, then
7
+ * `paddingBottom` px of spacer. Nothing is absolutely positioned, so a
8
+ * `<table>` keeps `table-layout: fixed` widths and native sticky `<thead>`s.
9
+ *
10
+ * Layering:
11
+ * - Pure helpers (`normalizeStickyLevels`, `activeStickyIndices`,
12
+ * `extractWindowIndices`, `layoutWindow`, `findScrollRoot`) hold the
13
+ * window policy — threshold, pinned keys, sticky group headers, spacer
14
+ * maths. DOM-free, so `npm run test:unit` covers them under node:test.
15
+ * - `RowWindowEngine` drives `@tanstack/virtual-core` (an OPTIONAL peer,
16
+ * which is why this module only ships behind `@joewinke/jatui/virtual`)
17
+ * for measured sizes, scroll-offset tracking and scroll-anchor correction
18
+ * when a row above the viewport changes height.
19
+ * - `createRowWindow.svelte.ts` is the thin runes wrapper.
20
+ */
21
+ import { Virtualizer, defaultRangeExtractor, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll } from '@tanstack/virtual-core';
22
+ export const EMPTY_SNAPSHOT = Object.freeze({
23
+ items: Object.freeze([]),
24
+ paddingTop: 0,
25
+ paddingBottom: 0,
26
+ totalSize: 0,
27
+ windowed: false,
28
+ count: 0
29
+ });
30
+ // ─── Pure helpers ───────────────────────────────────────────────────────────
31
+ /**
32
+ * `stickyIndices` accepts one flat list (one header level) or one list per
33
+ * level, outermost first (`[projectHeaderIdx[], epicHeaderIdx[]]`). Returns the
34
+ * per-level form, each level sorted ascending with out-of-range entries dropped.
35
+ */
36
+ export function normalizeStickyLevels(input, count) {
37
+ if (!input || input.length === 0)
38
+ return [];
39
+ const levels = Array.isArray(input[0])
40
+ ? input
41
+ : [input];
42
+ return levels.map((level) => level
43
+ .filter((i) => Number.isInteger(i) && i >= 0 && i < count)
44
+ .slice()
45
+ .sort((a, b) => a - b));
46
+ }
47
+ /** Last element of sorted `arr` that is `<= max` and `> floor`, else -1. */
48
+ function lastAtOrBelow(arr, max, floor) {
49
+ let lo = 0;
50
+ let hi = arr.length - 1;
51
+ let found = -1;
52
+ while (lo <= hi) {
53
+ const mid = (lo + hi) >> 1;
54
+ if (arr[mid] <= max) {
55
+ found = arr[mid];
56
+ lo = mid + 1;
57
+ }
58
+ else {
59
+ hi = mid - 1;
60
+ }
61
+ }
62
+ return found > floor ? found : -1;
63
+ }
64
+ /**
65
+ * The sticky header that is "active" at `startIndex` (the first visible item),
66
+ * per level. For level n it is the last level-n header at or above
67
+ * `startIndex` that comes AFTER level n-1's active header, so an epic header
68
+ * from the previous project never sticks under the current project's header.
69
+ */
70
+ export function activeStickyIndices(levels, startIndex) {
71
+ const out = [];
72
+ let floor = -1;
73
+ for (const level of levels) {
74
+ const idx = lastAtOrBelow(level, startIndex, floor);
75
+ if (idx >= 0) {
76
+ out.push(idx);
77
+ floor = idx;
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /**
83
+ * The rangeExtractor: which indices to render for a visible `range`. The
84
+ * visible slice plus overscan, every pinned index, and the active sticky
85
+ * header of each level, sorted and de-duplicated. Below `threshold`, all.
86
+ */
87
+ export function extractWindowIndices(range, policy) {
88
+ const { count } = range;
89
+ if (count <= 0)
90
+ return [];
91
+ if (policy.threshold > 0 && count < policy.threshold) {
92
+ return Array.from({ length: count }, (_, i) => i);
93
+ }
94
+ const base = defaultRangeExtractor(range);
95
+ const extra = [];
96
+ for (const i of activeStickyIndices(policy.stickyLevels, range.startIndex))
97
+ extra.push(i);
98
+ for (const i of policy.pinnedIndices)
99
+ if (i >= 0 && i < count)
100
+ extra.push(i);
101
+ if (extra.length === 0)
102
+ return base;
103
+ const lo = base.length ? base[0] : Infinity;
104
+ const hi = base.length ? base[base.length - 1] : -Infinity;
105
+ const outside = extra.filter((i) => i < lo || i > hi);
106
+ if (outside.length === 0)
107
+ return base;
108
+ return Array.from(new Set([...base, ...outside])).sort((a, b) => a - b);
109
+ }
110
+ /**
111
+ * Turn virtual-core's measured items (absolute offsets that include
112
+ * `scrollMargin`) into the spacer-row snapshot the consumer renders.
113
+ */
114
+ export function layoutWindow(virtualItems, opts) {
115
+ const items = [];
116
+ let prevEnd = 0;
117
+ for (let k = 0; k < virtualItems.length; k++) {
118
+ const v = virtualItems[k];
119
+ const start = v.start - opts.scrollMargin;
120
+ const end = start + v.size;
121
+ items.push({
122
+ index: v.index,
123
+ key: v.key,
124
+ start,
125
+ size: v.size,
126
+ end,
127
+ gapBefore: k === 0 ? 0 : Math.max(0, start - prevEnd),
128
+ sticky: opts.stickySet.has(v.index),
129
+ pinned: opts.pinnedSet.has(v.index)
130
+ });
131
+ prevEnd = end;
132
+ }
133
+ const totalSize = Math.max(opts.totalSize, prevEnd);
134
+ return {
135
+ items,
136
+ paddingTop: items.length ? Math.max(0, items[0].start) : 0,
137
+ paddingBottom: items.length ? Math.max(0, totalSize - prevEnd) : totalSize,
138
+ totalSize,
139
+ windowed: opts.windowed,
140
+ count: opts.count
141
+ };
142
+ }
143
+ /** Cheap structural equality so unchanged layouts don't churn reactive state. */
144
+ export function sameSnapshot(a, b) {
145
+ if (a.paddingTop !== b.paddingTop ||
146
+ a.paddingBottom !== b.paddingBottom ||
147
+ a.totalSize !== b.totalSize ||
148
+ a.windowed !== b.windowed ||
149
+ a.count !== b.count ||
150
+ a.items.length !== b.items.length) {
151
+ return false;
152
+ }
153
+ for (let i = 0; i < a.items.length; i++) {
154
+ const x = a.items[i];
155
+ const y = b.items[i];
156
+ if (x.index !== y.index ||
157
+ x.key !== y.key ||
158
+ x.start !== y.start ||
159
+ x.size !== y.size ||
160
+ x.gapBefore !== y.gapBefore ||
161
+ x.sticky !== y.sticky ||
162
+ x.pinned !== y.pinned) {
163
+ return false;
164
+ }
165
+ }
166
+ return true;
167
+ }
168
+ /**
169
+ * `scrollRoot: 'auto'` — the nearest ancestor that actually scrolls
170
+ * vertically: `overflow-y` is auto/scroll/overlay AND its content overflows it.
171
+ * An `overflow-auto` wrapper that grows with its content never scrolls (the
172
+ * dash TaskTable case), so it is skipped in favour of the real scroller.
173
+ * Returns `null` for "the window".
174
+ */
175
+ export function findScrollRoot(node, overflowY) {
176
+ let el = node.parentElement;
177
+ while (el) {
178
+ const tag = el.tagName.toUpperCase();
179
+ if (tag === 'BODY' || tag === 'HTML')
180
+ return null;
181
+ const o = overflowY(el);
182
+ if ((o === 'auto' || o === 'scroll' || o === 'overlay') && el.scrollHeight > el.clientHeight) {
183
+ return el;
184
+ }
185
+ el = el.parentElement;
186
+ }
187
+ return null;
188
+ }
189
+ function isWindowLike(x) {
190
+ return !!x && typeof x === 'object' && 'document' in x && 'scrollY' in x;
191
+ }
192
+ /** The ResizeObserver constructor of a document's window, if it has one. */
193
+ function resizeObserverOf(view) {
194
+ return view?.ResizeObserver;
195
+ }
196
+ /**
197
+ * Imperative windowing engine. One per list. Feed it the render list with
198
+ * `setItems`, mount it with `attach(anchor)`, register each rendered element
199
+ * with `measure`, and read `snapshot` (or listen via `onChange`).
200
+ */
201
+ export class RowWindowEngine {
202
+ opts;
203
+ v;
204
+ keys = [];
205
+ estimates = [];
206
+ keyIndex = new Map();
207
+ getItemKey = (i) => i;
208
+ estimateSize = () => 0;
209
+ pinnedKeys = [];
210
+ pinnedIndices = [];
211
+ pinnedSet = new Set();
212
+ stickyInput = [];
213
+ stickyLevels = [];
214
+ stickySet = new Set();
215
+ rangeExtractor = defaultRangeExtractor;
216
+ scrollMargin = 0;
217
+ anchorEl = null;
218
+ root = null;
219
+ rootUnsub = null;
220
+ anchorRO = null;
221
+ prevOverflowAnchor = '';
222
+ unmountVirtualizer = null;
223
+ nodes = new Map();
224
+ pendingScroll = null;
225
+ sweepQueued = false;
226
+ destroyed = false;
227
+ snapshot = EMPTY_SNAPSHOT;
228
+ constructor(opts) {
229
+ this.opts = { ...opts };
230
+ this.v = new Virtualizer(this.buildOptions(null));
231
+ }
232
+ // ── inputs ──
233
+ /** Replace the render list. `keys[i]` must be unique. */
234
+ setItems(keys, estimates) {
235
+ this.keys = keys;
236
+ this.estimates = estimates;
237
+ const map = new Map();
238
+ for (let i = 0; i < keys.length; i++)
239
+ map.set(keys[i], i);
240
+ this.keyIndex = map;
241
+ // New closures on purpose: virtual-core memoises its measurements on the
242
+ // getItemKey identity, so a reorder with an unchanged count still relayouts.
243
+ this.getItemKey = (i) => keys[i];
244
+ this.estimateSize = (i) => estimates[i] ?? 0;
245
+ // Mounted elements keep their key but may have a new index.
246
+ for (const [key, node] of this.nodes) {
247
+ const idx = map.get(key);
248
+ node.setAttribute('data-index', idx === undefined ? '-1' : String(idx));
249
+ }
250
+ this.resolvePinned();
251
+ this.stickyLevels = normalizeStickyLevels(this.stickyInput, keys.length);
252
+ this.stickySet = new Set(this.stickyLevels.flat());
253
+ this.rebuildRangeExtractor();
254
+ this.sync();
255
+ }
256
+ setPinned(keys) {
257
+ this.pinnedKeys = Array.from(keys);
258
+ this.resolvePinned();
259
+ this.rebuildRangeExtractor();
260
+ this.sync();
261
+ }
262
+ /** Change the render-everything threshold. */
263
+ setThreshold(threshold) {
264
+ if (threshold === this.opts.threshold)
265
+ return;
266
+ this.opts.threshold = threshold;
267
+ this.rebuildRangeExtractor();
268
+ this.sync();
269
+ }
270
+ setSticky(input) {
271
+ this.stickyInput = input;
272
+ this.stickyLevels = normalizeStickyLevels(input, this.keys.length);
273
+ this.stickySet = new Set(this.stickyLevels.flat());
274
+ this.rebuildRangeExtractor();
275
+ this.sync();
276
+ }
277
+ resolvePinned() {
278
+ const idx = [];
279
+ for (const k of this.pinnedKeys) {
280
+ const i = this.keyIndex.get(k);
281
+ if (i !== undefined)
282
+ idx.push(i);
283
+ }
284
+ this.pinnedIndices = idx;
285
+ this.pinnedSet = new Set(idx);
286
+ }
287
+ rebuildRangeExtractor() {
288
+ const policy = {
289
+ threshold: this.opts.threshold,
290
+ pinnedIndices: this.pinnedIndices,
291
+ stickyLevels: this.stickyLevels
292
+ };
293
+ this.rangeExtractor = (range) => extractWindowIndices(range, policy);
294
+ }
295
+ // ── virtual-core wiring ──
296
+ buildOptions(root) {
297
+ const win = isWindowLike(root);
298
+ return {
299
+ count: this.keys.length,
300
+ getScrollElement: () => root,
301
+ estimateSize: this.estimateSize,
302
+ getItemKey: this.getItemKey,
303
+ rangeExtractor: this.rangeExtractor,
304
+ overscan: this.opts.overscan,
305
+ scrollMargin: this.scrollMargin,
306
+ scrollPaddingStart: this.opts.scrollPaddingStart,
307
+ scrollPaddingEnd: this.opts.scrollPaddingEnd,
308
+ initialRect: { width: 0, height: this.opts.initialViewportSize },
309
+ observeElementRect: (win ? observeWindowRect : observeElementRect),
310
+ observeElementOffset: (win ? observeWindowOffset : observeElementOffset),
311
+ scrollToFn: (win ? windowScroll : elementScroll),
312
+ onChange: () => this.emit()
313
+ };
314
+ }
315
+ /** Push current inputs into virtual-core and recompute the snapshot. */
316
+ sync() {
317
+ if (this.destroyed)
318
+ return;
319
+ this.v.setOptions(this.buildOptions(this.root));
320
+ if (this.anchorEl)
321
+ this.v._willUpdate();
322
+ this.emit();
323
+ }
324
+ emit() {
325
+ if (this.destroyed)
326
+ return;
327
+ const count = this.keys.length;
328
+ const next = layoutWindow(this.v.getVirtualItems(), {
329
+ scrollMargin: this.scrollMargin,
330
+ totalSize: this.v.getTotalSize(),
331
+ count,
332
+ windowed: !(this.opts.threshold > 0 && count < this.opts.threshold),
333
+ stickySet: this.stickySet,
334
+ pinnedSet: this.pinnedSet
335
+ });
336
+ if (sameSnapshot(next, this.snapshot))
337
+ return;
338
+ this.snapshot = next;
339
+ this.opts.onChange?.(next);
340
+ }
341
+ // ── DOM lifecycle ──
342
+ /** Mount on the element whose top is item 0's top (a `<tbody>` or list container). */
343
+ attach(anchor) {
344
+ if (this.destroyed)
345
+ return;
346
+ this.anchorEl = anchor;
347
+ // The engine does its own scroll-anchor correction when a row above the
348
+ // viewport is measured; native CSS scroll anchoring would correct the same
349
+ // shift a second time and jump.
350
+ this.prevOverflowAnchor = anchor.style.overflowAnchor;
351
+ anchor.style.overflowAnchor = 'none';
352
+ this.bindRoot(this.resolveRoot(anchor));
353
+ this.unmountVirtualizer = this.v._didMount();
354
+ this.v._willUpdate();
355
+ for (const node of this.nodes.values()) {
356
+ if (node.isConnected)
357
+ this.v.measureElement(node);
358
+ }
359
+ const RO = resizeObserverOf(anchor.ownerDocument?.defaultView);
360
+ if (RO) {
361
+ this.anchorRO = new RO(() => this.onLayoutShift());
362
+ this.anchorRO.observe(anchor);
363
+ }
364
+ this.emit();
365
+ if (this.pendingScroll) {
366
+ const { index, align } = this.pendingScroll;
367
+ this.pendingScroll = null;
368
+ this.scrollToIndex(index, { align });
369
+ }
370
+ }
371
+ detach() {
372
+ this.rootUnsub?.();
373
+ this.rootUnsub = null;
374
+ this.anchorRO?.disconnect();
375
+ this.anchorRO = null;
376
+ this.unmountVirtualizer?.();
377
+ this.unmountVirtualizer = null;
378
+ if (this.anchorEl)
379
+ this.anchorEl.style.overflowAnchor = this.prevOverflowAnchor;
380
+ this.anchorEl = null;
381
+ this.root = null;
382
+ }
383
+ destroy() {
384
+ this.detach();
385
+ this.nodes.clear();
386
+ this.destroyed = true;
387
+ }
388
+ resolveRoot(anchor) {
389
+ const view = anchor.ownerDocument?.defaultView;
390
+ const configured = this.opts.scrollRoot;
391
+ if (configured !== 'auto')
392
+ return configured;
393
+ const found = findScrollRoot(anchor, (el) => view.getComputedStyle(el).overflowY);
394
+ return found ?? view;
395
+ }
396
+ bindRoot(root) {
397
+ this.rootUnsub?.();
398
+ this.root = root;
399
+ // Registered BEFORE virtual-core's own scroll listener (`_willUpdate` adds
400
+ // it), so the margin is fresh when virtual-core recomputes the range.
401
+ const onScroll = () => this.updateScrollMargin();
402
+ root.addEventListener('scroll', onScroll, { passive: true });
403
+ let rootRO = null;
404
+ const RO = isWindowLike(root) ? undefined : resizeObserverOf(root.ownerDocument?.defaultView);
405
+ if (RO && !isWindowLike(root)) {
406
+ const ro = new RO(() => this.onLayoutShift());
407
+ ro.observe(root);
408
+ rootRO = ro;
409
+ }
410
+ this.rootUnsub = () => {
411
+ root.removeEventListener('scroll', onScroll);
412
+ rootRO?.disconnect();
413
+ };
414
+ this.updateScrollMargin(false);
415
+ this.v.setOptions(this.buildOptions(root));
416
+ }
417
+ /** The anchor or root resized: re-resolve an 'auto' root, refresh the margin. */
418
+ onLayoutShift() {
419
+ if (!this.anchorEl || this.destroyed)
420
+ return;
421
+ if (this.opts.scrollRoot === 'auto') {
422
+ const next = this.resolveRoot(this.anchorEl);
423
+ if (next !== this.root) {
424
+ this.bindRoot(next);
425
+ this.v._willUpdate();
426
+ this.emit();
427
+ return;
428
+ }
429
+ }
430
+ this.updateScrollMargin();
431
+ }
432
+ /** Distance from the scroller's content top to the anchor's top. */
433
+ updateScrollMargin(apply = true) {
434
+ const anchor = this.anchorEl;
435
+ const root = this.root;
436
+ if (!anchor || !root)
437
+ return;
438
+ const anchorTop = anchor.getBoundingClientRect().top;
439
+ let margin;
440
+ if (isWindowLike(root)) {
441
+ margin = anchorTop + root.scrollY;
442
+ }
443
+ else {
444
+ const el = root;
445
+ margin = anchorTop - el.getBoundingClientRect().top - el.clientTop + el.scrollTop;
446
+ }
447
+ margin = Math.round(margin);
448
+ if (margin === this.scrollMargin)
449
+ return;
450
+ this.scrollMargin = margin;
451
+ if (apply)
452
+ this.sync();
453
+ }
454
+ // ── per-item measurement ──
455
+ /** Register a rendered item element. Re-measured on every resize. */
456
+ measure(node, key) {
457
+ const idx = this.keyIndex.get(key);
458
+ node.setAttribute('data-index', idx === undefined ? '-1' : String(idx));
459
+ const prev = this.nodes.get(key);
460
+ if (prev && prev !== node && !prev.isConnected)
461
+ this.queueSweep();
462
+ this.nodes.set(key, node);
463
+ if (idx !== undefined)
464
+ this.v.measureElement(node);
465
+ }
466
+ /** Forget an item element (it unmounted, or its key changed). */
467
+ unmeasure(node, key) {
468
+ if (this.nodes.get(key) === node)
469
+ this.nodes.delete(key);
470
+ this.queueSweep();
471
+ }
472
+ queueSweep() {
473
+ if (this.sweepQueued)
474
+ return;
475
+ this.sweepQueued = true;
476
+ queueMicrotask(() => {
477
+ this.sweepQueued = false;
478
+ if (!this.destroyed)
479
+ this.v.measureElement(null);
480
+ });
481
+ }
482
+ // ── scrolling ──
483
+ scrollToIndex(index, opts = {}) {
484
+ const align = opts.align ?? 'auto';
485
+ if (index < 0 || index >= this.keys.length)
486
+ return;
487
+ if (!this.anchorEl) {
488
+ this.pendingScroll = { index, align };
489
+ return;
490
+ }
491
+ this.v.scrollToIndex(index, { align });
492
+ }
493
+ scrollToKey(key, opts = {}) {
494
+ const idx = this.keyIndex.get(key);
495
+ if (idx !== undefined)
496
+ this.scrollToIndex(idx, opts);
497
+ }
498
+ /** Index of `key` in the render list, or -1. */
499
+ indexOf(key) {
500
+ return this.keyIndex.get(key) ?? -1;
501
+ }
502
+ /** Drop every cached measurement (e.g. after a column-width change). */
503
+ remeasure() {
504
+ this.v.measure();
505
+ for (const node of this.nodes.values())
506
+ if (node.isConnected)
507
+ this.v.measureElement(node);
508
+ this.emit();
509
+ }
510
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -34,6 +34,11 @@
34
34
  "svelte": "./dist/components/table/index.js",
35
35
  "default": "./dist/components/table/index.js"
36
36
  },
37
+ "./virtual": {
38
+ "types": "./dist/virtual/index.d.ts",
39
+ "svelte": "./dist/virtual/index.js",
40
+ "default": "./dist/virtual/index.js"
41
+ },
37
42
  "./graph": {
38
43
  "types": "./dist/components/graph/index.d.ts",
39
44
  "svelte": "./dist/components/graph/index.js",
@@ -76,7 +81,7 @@
76
81
  "build": "vite build",
77
82
  "build:site": "npm run package && vite build",
78
83
  "preview": "vite preview",
79
- "package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs",
84
+ "package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs && node scripts/verify-dist.mjs",
80
85
  "check:demos": "node scripts/check-demo-coverage.mjs",
81
86
  "guard": "node scripts/check-demo-coverage.mjs",
82
87
  "test:dist": "svelte-kit sync && vitest run --config vitest.dist.config.ts",
@@ -91,6 +96,7 @@
91
96
  "@atlaskit/pragmatic-drag-and-drop-live-region": "~2.0.0",
92
97
  "@paper-design/shaders": ">=0.0.50",
93
98
  "@tanstack/table-core": "^8.0.0",
99
+ "@tanstack/virtual-core": "^3.17.0",
94
100
  "@xyflow/svelte": "^1.0.0",
95
101
  "cobe": "^2.0.0",
96
102
  "d3": "^7.0.0",
@@ -127,6 +133,9 @@
127
133
  "@tanstack/table-core": {
128
134
  "optional": true
129
135
  },
136
+ "@tanstack/virtual-core": {
137
+ "optional": true
138
+ },
130
139
  "@xyflow/svelte": {
131
140
  "optional": true
132
141
  },
@@ -163,6 +172,7 @@
163
172
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
164
173
  "@tailwindcss/vite": "^4.0.0",
165
174
  "@tanstack/table-core": "^8.21.3",
175
+ "@tanstack/virtual-core": "^3.17.11",
166
176
  "@types/d3": "^7.4.3",
167
177
  "@vitest/browser": "^3.2.7",
168
178
  "@xyflow/svelte": "^1.6.2",