@human-kit/svelte-components 1.0.0-alpha.4 → 1.0.0-alpha.5

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 (82) hide show
  1. package/dist/checkbox/README.md +53 -0
  2. package/dist/checkbox/TODO.md +16 -0
  3. package/dist/checkbox/index.d.ts +6 -0
  4. package/dist/checkbox/index.js +6 -0
  5. package/dist/checkbox/index.parts.d.ts +2 -0
  6. package/dist/checkbox/index.parts.js +2 -0
  7. package/dist/checkbox/indicator/README.md +23 -0
  8. package/dist/checkbox/indicator/checkbox-indicator.svelte +43 -0
  9. package/dist/checkbox/indicator/checkbox-indicator.svelte.d.ts +10 -0
  10. package/dist/checkbox/root/README.md +47 -0
  11. package/dist/checkbox/root/checkbox-label-test.svelte +10 -0
  12. package/dist/checkbox/root/checkbox-label-test.svelte.d.ts +18 -0
  13. package/dist/checkbox/root/checkbox-root.svelte +361 -0
  14. package/dist/checkbox/root/checkbox-root.svelte.d.ts +23 -0
  15. package/dist/checkbox/root/checkbox-test.svelte +59 -0
  16. package/dist/checkbox/root/checkbox-test.svelte.d.ts +18 -0
  17. package/dist/checkbox/root/context.d.ts +21 -0
  18. package/dist/checkbox/root/context.js +15 -0
  19. package/dist/combobox/list/combobox-listbox.svelte.d.ts +1 -1
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.js +4 -0
  22. package/dist/table/IMPLEMENTATION_NOTES.md +8 -0
  23. package/dist/table/PLAN-HIDDEN-COLUMNS.md +152 -0
  24. package/dist/table/PLAN.md +924 -0
  25. package/dist/table/README.md +116 -0
  26. package/dist/table/SELECTION_CHECKBOX_PLAN.md +234 -0
  27. package/dist/table/TODO.md +100 -0
  28. package/dist/table/body/README.md +24 -0
  29. package/dist/table/body/table-body.svelte +25 -0
  30. package/dist/table/body/table-body.svelte.d.ts +9 -0
  31. package/dist/table/cell/README.md +25 -0
  32. package/dist/table/cell/table-cell.svelte +247 -0
  33. package/dist/table/cell/table-cell.svelte.d.ts +9 -0
  34. package/dist/table/checkbox/README.md +38 -0
  35. package/dist/table/checkbox/table-checkbox-test.svelte +121 -0
  36. package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +16 -0
  37. package/dist/table/checkbox/table-checkbox.svelte +274 -0
  38. package/dist/table/checkbox/table-checkbox.svelte.d.ts +13 -0
  39. package/dist/table/checkbox-indicator/README.md +29 -0
  40. package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte +22 -0
  41. package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte.d.ts +10 -0
  42. package/dist/table/column/README.md +32 -0
  43. package/dist/table/column/table-column.svelte +108 -0
  44. package/dist/table/column/table-column.svelte.d.ts +18 -0
  45. package/dist/table/column-header-cell/README.md +28 -0
  46. package/dist/table/column-header-cell/table-column-header-cell.svelte +281 -0
  47. package/dist/table/column-header-cell/table-column-header-cell.svelte.d.ts +9 -0
  48. package/dist/table/column-resizer/README.md +32 -0
  49. package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte +51 -0
  50. package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte.d.ts +3 -0
  51. package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte +83 -0
  52. package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte.d.ts +3 -0
  53. package/dist/table/column-resizer/table-column-resizer-test.svelte +75 -0
  54. package/dist/table/column-resizer/table-column-resizer-test.svelte.d.ts +3 -0
  55. package/dist/table/column-resizer/table-column-resizer.svelte +616 -0
  56. package/dist/table/column-resizer/table-column-resizer.svelte.d.ts +11 -0
  57. package/dist/table/empty-state/README.md +25 -0
  58. package/dist/table/empty-state/table-empty-state.svelte +38 -0
  59. package/dist/table/empty-state/table-empty-state.svelte.d.ts +8 -0
  60. package/dist/table/footer/README.md +24 -0
  61. package/dist/table/footer/table-footer.svelte +19 -0
  62. package/dist/table/footer/table-footer.svelte.d.ts +9 -0
  63. package/dist/table/header/README.md +24 -0
  64. package/dist/table/header/table-header.svelte +19 -0
  65. package/dist/table/header/table-header.svelte.d.ts +9 -0
  66. package/dist/table/index.d.ts +16 -0
  67. package/dist/table/index.js +16 -0
  68. package/dist/table/index.parts.d.ts +12 -0
  69. package/dist/table/index.parts.js +12 -0
  70. package/dist/table/root/README.md +56 -0
  71. package/dist/table/root/context.d.ts +198 -0
  72. package/dist/table/root/context.js +1426 -0
  73. package/dist/table/root/table-reorder-test.svelte +64 -0
  74. package/dist/table/root/table-reorder-test.svelte.d.ts +3 -0
  75. package/dist/table/root/table-root.svelte +410 -0
  76. package/dist/table/root/table-root.svelte.d.ts +29 -0
  77. package/dist/table/root/table-test.svelte +165 -0
  78. package/dist/table/root/table-test.svelte.d.ts +25 -0
  79. package/dist/table/row/README.md +27 -0
  80. package/dist/table/row/table-row.svelte +321 -0
  81. package/dist/table/row/table-row.svelte.d.ts +13 -0
  82. package/package.json +11 -1
@@ -0,0 +1,616 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import { getTableCellContext, useTableColumnContext, useTableContext } from '../root/context';
6
+ import {
7
+ shouldShowFocusVisible,
8
+ trackInteractionModality
9
+ } from '../../primitives/input-modality';
10
+
11
+ type TableColumnResizerProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
12
+ step?: number;
13
+ shiftStep?: number;
14
+ children?: Snippet;
15
+ class?: string;
16
+ };
17
+
18
+ let {
19
+ step = 16,
20
+ shiftStep = 48,
21
+ children,
22
+ class: className = '',
23
+ ...restProps
24
+ }: TableColumnResizerProps = $props();
25
+
26
+ const table = useTableContext();
27
+ const column = useTableColumnContext();
28
+ const cellContext = getTableCellContext();
29
+ const layoutVersion = table.layoutVersion;
30
+ const resizeVersion = table.resizeVersion;
31
+ const widthVersion = table.widthVersion;
32
+ table.registerColumnResizer(column.token);
33
+ cellContext?.notifyResizerPresent?.();
34
+
35
+ let element = $state<HTMLDivElement | undefined>(undefined);
36
+ let isFocused = $state(false);
37
+ let isFocusVisible = $state(false);
38
+ let keyboardResizeActive = $state(false);
39
+ let keyboardResizeStartWidth = $state<number | null>(null);
40
+ let removeListeners: (() => void) | null = null;
41
+ let resizeAnnouncement = $state('');
42
+ let announceTimeout: ReturnType<typeof setTimeout> | null = null;
43
+ let focusHeaderTimeout: ReturnType<typeof setTimeout> | null = null;
44
+ let suppressNextDoubleClickAutofit = $state(false);
45
+ let recentClickCandidate = $state<{
46
+ timeStamp: number;
47
+ clientX: number;
48
+ clientY: number;
49
+ button: number;
50
+ pointerType: string;
51
+ } | null>(null);
52
+
53
+ const DOUBLE_PRESS_MAX_DELAY_MS = 500;
54
+ const DOUBLE_PRESS_MAX_DISTANCE_PX = 6;
55
+
56
+ const isResizing = $derived.by(() => {
57
+ void $resizeVersion;
58
+ return table.resizingColumnId === column.id;
59
+ });
60
+ const isResizable = $derived.by(() => {
61
+ void $layoutVersion;
62
+ return !column.isHidden && table.isColumnResizable(column.id);
63
+ });
64
+ const currentWidth = $derived.by(() => {
65
+ void $widthVersion;
66
+ return table.getColumnWidth(column.id) ?? getHeaderWidth();
67
+ });
68
+ const minWidth = $derived.by(() => {
69
+ void $widthVersion;
70
+ return table.getColumnMinWidth(column.id) ?? 75;
71
+ });
72
+ const maxWidth = $derived.by(() => {
73
+ void $widthVersion;
74
+ return table.getColumnMaxWidth(column.id);
75
+ });
76
+ const accessibleLabel = $derived.by(() => {
77
+ const text = column.textValue?.trim() || column.id.replace(/[-_]+/g, ' ').trim();
78
+ return `Resize ${text || 'column'} column`;
79
+ });
80
+ const accessibleValueText = $derived.by(() => {
81
+ const width = currentWidth;
82
+ if (width === undefined) return undefined;
83
+ return `${width}px wide`;
84
+ });
85
+
86
+ function getAnnouncementLabel() {
87
+ const text = column.textValue?.trim() || column.id.replace(/[-_]+/g, ' ').trim();
88
+ return text || 'Column';
89
+ }
90
+
91
+ function getHeaderWidth() {
92
+ return Math.round(element?.closest('th')?.getBoundingClientRect().width ?? 0);
93
+ }
94
+
95
+ function isRightToLeft() {
96
+ const target = element?.closest('table') ?? element;
97
+ return target ? getComputedStyle(target).direction === 'rtl' : false;
98
+ }
99
+
100
+ function cleanupPointerListeners() {
101
+ removeListeners?.();
102
+ removeListeners = null;
103
+ table.endColumnResize();
104
+ }
105
+
106
+ function cleanupAnnouncementTimeout() {
107
+ if (announceTimeout !== null) {
108
+ clearTimeout(announceTimeout);
109
+ announceTimeout = null;
110
+ }
111
+ }
112
+
113
+ function cleanupFocusHeaderTimeout() {
114
+ if (focusHeaderTimeout !== null) {
115
+ clearTimeout(focusHeaderTimeout);
116
+ focusHeaderTimeout = null;
117
+ }
118
+ }
119
+
120
+ function focusHeaderCell() {
121
+ const headerCell = element?.closest('th') as HTMLElement | null;
122
+ headerCell?.focus();
123
+ }
124
+
125
+ function focusResizer() {
126
+ element?.focus({ preventScroll: true });
127
+ }
128
+
129
+ function focusAdjacentHeaderCell(direction: 'left' | 'right') {
130
+ const headerCell = element?.closest('th') as HTMLElement | null;
131
+ const headerRow = headerCell?.closest('tr');
132
+ if (!headerCell || !headerRow) return false;
133
+
134
+ const headerCells = Array.from(
135
+ headerRow.querySelectorAll<HTMLElement>('th[role="columnheader"]')
136
+ );
137
+ const currentIndex = headerCells.indexOf(headerCell);
138
+ if (currentIndex < 0) return false;
139
+
140
+ const target = headerCells[currentIndex + (direction === 'left' ? -1 : 1)] ?? null;
141
+ target?.focus();
142
+ return document.activeElement === target;
143
+ }
144
+
145
+ function updateWidth(nextWidth: number) {
146
+ table.setColumnWidth(column.id, nextWidth);
147
+ }
148
+
149
+ function getResolvedWidth() {
150
+ return table.getColumnWidth(column.id) ?? getHeaderWidth() ?? minWidth;
151
+ }
152
+
153
+ function announceWidth(width: number) {
154
+ const message = `${getAnnouncementLabel()} width ${width}px.`;
155
+ cleanupAnnouncementTimeout();
156
+ resizeAnnouncement = '';
157
+ announceTimeout = setTimeout(() => {
158
+ resizeAnnouncement = message;
159
+ announceTimeout = null;
160
+ }, 0);
161
+ }
162
+
163
+ function commitWidthChange(nextWidth: number, options?: { announce?: boolean }) {
164
+ updateWidth(nextWidth);
165
+ const committedWidth = getResolvedWidth();
166
+ if (options?.announce !== false) {
167
+ announceWidth(committedWidth);
168
+ }
169
+ return committedWidth;
170
+ }
171
+
172
+ function startKeyboardResizeMode() {
173
+ if (keyboardResizeActive) return;
174
+ keyboardResizeActive = true;
175
+ keyboardResizeStartWidth = getResolvedWidth();
176
+ table.startColumnResize(column.id);
177
+ }
178
+
179
+ function stopKeyboardResizeMode(options?: { restoreWidth?: boolean; focusHeader?: boolean }) {
180
+ if (options?.restoreWidth && keyboardResizeStartWidth !== null) {
181
+ updateWidth(keyboardResizeStartWidth);
182
+ }
183
+
184
+ const wasActive = keyboardResizeActive;
185
+ keyboardResizeActive = false;
186
+ keyboardResizeStartWidth = null;
187
+
188
+ if (wasActive) {
189
+ table.endColumnResize();
190
+ }
191
+
192
+ if (options?.focusHeader) {
193
+ cleanupFocusHeaderTimeout();
194
+ focusHeaderTimeout = setTimeout(() => {
195
+ focusHeaderTimeout = null;
196
+ focusHeaderCell();
197
+ }, 0);
198
+ }
199
+ }
200
+
201
+ function getAutoFitWidth() {
202
+ return table.measureColumnContentWidth(column.id) ?? minWidth;
203
+ }
204
+
205
+ function clearRecentClickCandidate() {
206
+ recentClickCandidate = null;
207
+ }
208
+
209
+ function rememberCompletedClick(event: PointerEvent) {
210
+ if (event.pointerType !== 'mouse') {
211
+ clearRecentClickCandidate();
212
+ return;
213
+ }
214
+
215
+ recentClickCandidate = {
216
+ timeStamp: event.timeStamp,
217
+ clientX: event.clientX,
218
+ clientY: event.clientY,
219
+ button: event.button,
220
+ pointerType: event.pointerType
221
+ };
222
+ }
223
+
224
+ function isSecondPressOfDoubleClick(event: PointerEvent) {
225
+ const candidate = recentClickCandidate;
226
+ if (!candidate) return false;
227
+ if (event.pointerType !== 'mouse' || candidate.pointerType !== 'mouse') return false;
228
+ if (event.button !== candidate.button) return false;
229
+ if (event.timeStamp - candidate.timeStamp > DOUBLE_PRESS_MAX_DELAY_MS) return false;
230
+
231
+ const deltaX = event.clientX - candidate.clientX;
232
+ const deltaY = event.clientY - candidate.clientY;
233
+ return Math.hypot(deltaX, deltaY) <= DOUBLE_PRESS_MAX_DISTANCE_PX;
234
+ }
235
+
236
+ function autoFitColumn() {
237
+ stopKeyboardResizeMode();
238
+ table.startColumnResize(column.id);
239
+ commitWidthChange(getAutoFitWidth());
240
+ table.endColumnResize();
241
+ }
242
+
243
+ function handleDoubleClick(event: MouseEvent) {
244
+ if (!isResizable) return;
245
+ if (suppressNextDoubleClickAutofit) {
246
+ suppressNextDoubleClickAutofit = false;
247
+ event.preventDefault();
248
+ event.stopPropagation();
249
+ return;
250
+ }
251
+ event.preventDefault();
252
+ event.stopPropagation();
253
+ trackInteractionModality(event, element ?? null);
254
+ isFocusVisible = false;
255
+ autoFitColumn();
256
+ }
257
+
258
+ function handlePointerDown(event: PointerEvent) {
259
+ if (!isResizable) return;
260
+ if (event.pointerType === 'mouse' && event.button !== 0) return;
261
+ if (event.isPrimary === false) return;
262
+ event.preventDefault();
263
+ event.stopPropagation();
264
+ trackInteractionModality(event, element ?? null);
265
+ isFocusVisible = false;
266
+ focusResizer();
267
+
268
+ if (isSecondPressOfDoubleClick(event)) {
269
+ clearRecentClickCandidate();
270
+ suppressNextDoubleClickAutofit = true;
271
+ autoFitColumn();
272
+ return;
273
+ }
274
+
275
+ suppressNextDoubleClickAutofit = false;
276
+
277
+ stopKeyboardResizeMode();
278
+ table.startColumnResize(column.id);
279
+
280
+ const th = element?.closest('th') as HTMLElement | null;
281
+ const tableEl = th?.closest('table') as HTMLTableElement | null;
282
+ const startX = event.clientX;
283
+ const startWidth = table.getColumnWidth(column.id) ?? getHeaderWidth();
284
+ const pointerId = event.pointerId;
285
+ const isRTL = isRightToLeft();
286
+ let didDrag = false;
287
+ let latestClientX = startX;
288
+ let animationFrameId: number | null = null;
289
+
290
+ // Capture the pointer so we receive move/up events even if the cursor
291
+ // leaves the browser viewport (e.g. in iframes or when dragging fast).
292
+ // The try-catch guards against synthetic events with no active pointer.
293
+ try {
294
+ element?.setPointerCapture(pointerId);
295
+ } catch {
296
+ /* synthetic event */
297
+ }
298
+
299
+ // Note: final clamping is authoritative in context.clampColumnWidth().
300
+ // This local pre-clamp avoids sending clearly out-of-range values through
301
+ // the reactive pipeline during drag, reducing unnecessary width notifications.
302
+ function clampWidth(w: number) {
303
+ let clamped = Math.round(w);
304
+ if (!Number.isFinite(clamped) || clamped < minWidth) clamped = minWidth;
305
+ if (maxWidth !== undefined && clamped > maxWidth) clamped = maxWidth;
306
+ return clamped;
307
+ }
308
+
309
+ function applyTemporaryWidthToDOM(width: number) {
310
+ if (th) th.style.width = `${width}px`;
311
+ if (tableEl) {
312
+ const allThs = tableEl.querySelectorAll<HTMLElement>('thead th[style*="width"]');
313
+ let total = 0;
314
+ for (const cell of allThs) {
315
+ total += parseFloat(cell.style.width) || 0;
316
+ }
317
+ if (total > 0) {
318
+ tableEl.style.width = `${total}px`;
319
+ tableEl.style.minWidth = '0';
320
+ }
321
+ }
322
+ }
323
+
324
+ function flushPendingPointerMove() {
325
+ if (animationFrameId !== null) {
326
+ cancelAnimationFrame(animationFrameId);
327
+ animationFrameId = null;
328
+ }
329
+
330
+ const direction = isRTL ? -1 : 1;
331
+ const delta = (latestClientX - startX) * positionScale * direction;
332
+ const nextWidth = clampWidth(startWidth + delta);
333
+ updateWidth(nextWidth);
334
+ }
335
+
336
+ function schedulePointerMove() {
337
+ if (animationFrameId !== null) return;
338
+ animationFrameId = requestAnimationFrame(() => {
339
+ animationFrameId = null;
340
+ flushPendingPointerMove();
341
+ });
342
+ }
343
+
344
+ // Measure position compensation factor.
345
+ // In centered/flex layouts, growing a column shifts the table's left edge,
346
+ // so the handle moves less than the mouse delta. We detect this by applying
347
+ // a 1px test change and measuring how much the <th> left edge drifts.
348
+ // NOTE: applyTemporaryWidthToDOM intentionally mutates the DOM synchronously
349
+ // outside Svelte's reactive cycle. The mutation is immediately reverted
350
+ // within the same microtask, so no observer or $effect will see it.
351
+ let positionScale = 1;
352
+ if (th) {
353
+ const leftBefore = th.getBoundingClientRect().left;
354
+ applyTemporaryWidthToDOM(startWidth + 1);
355
+ const leftAfter = th.getBoundingClientRect().left;
356
+ applyTemporaryWidthToDOM(startWidth);
357
+ const drift = leftBefore - leftAfter;
358
+ if (drift > 0.01 && drift < 0.99) {
359
+ positionScale = 1 / (1 - drift);
360
+ }
361
+ }
362
+
363
+ const handlePointerMove = (moveEvent: PointerEvent) => {
364
+ if (moveEvent.pointerId !== pointerId) return;
365
+ moveEvent.preventDefault();
366
+ didDrag = true;
367
+ latestClientX = moveEvent.clientX;
368
+ schedulePointerMove();
369
+ };
370
+
371
+ const handlePointerUp = (upEvent: PointerEvent) => {
372
+ if (upEvent.pointerId !== pointerId) return;
373
+ if (didDrag) {
374
+ latestClientX = upEvent.clientX;
375
+ flushPendingPointerMove();
376
+ }
377
+ if (didDrag) {
378
+ clearRecentClickCandidate();
379
+ table.suppressHeaderClickOnce();
380
+ announceWidth(getResolvedWidth());
381
+ } else {
382
+ rememberCompletedClick(upEvent);
383
+ }
384
+ cleanupPointerListeners();
385
+ };
386
+
387
+ const handlePointerCancel = (cancelEvent: PointerEvent) => {
388
+ if (cancelEvent.pointerId !== pointerId) return;
389
+ clearRecentClickCandidate();
390
+ if (animationFrameId !== null) {
391
+ cancelAnimationFrame(animationFrameId);
392
+ animationFrameId = null;
393
+ }
394
+ // Treat system-initiated cancellation the same as Escape:
395
+ // restore the width the column had before the drag started.
396
+ updateWidth(startWidth);
397
+ cleanupPointerListeners();
398
+ };
399
+
400
+ const handleWindowKeyDown = (keyEvent: KeyboardEvent) => {
401
+ if (keyEvent.key !== 'Escape') return;
402
+ keyEvent.preventDefault();
403
+ keyEvent.stopPropagation();
404
+ clearRecentClickCandidate();
405
+ if (animationFrameId !== null) {
406
+ cancelAnimationFrame(animationFrameId);
407
+ animationFrameId = null;
408
+ }
409
+ updateWidth(startWidth);
410
+ cleanupPointerListeners();
411
+ };
412
+
413
+ // With pointer capture active the browser routes all pointer events for
414
+ // this pointerId to the capturing element. Those events then bubble up
415
+ // to window, so we keep using window listeners — this also works in test
416
+ // environments that dispatch synthetic events directly on window.
417
+ window.addEventListener('pointermove', handlePointerMove);
418
+ window.addEventListener('pointerup', handlePointerUp);
419
+ window.addEventListener('pointercancel', handlePointerCancel);
420
+ window.addEventListener('keydown', handleWindowKeyDown, true);
421
+ removeListeners = () => {
422
+ if (animationFrameId !== null) {
423
+ cancelAnimationFrame(animationFrameId);
424
+ animationFrameId = null;
425
+ }
426
+ try {
427
+ element?.releasePointerCapture(pointerId);
428
+ } catch {
429
+ /* already released */
430
+ }
431
+ window.removeEventListener('pointermove', handlePointerMove);
432
+ window.removeEventListener('pointerup', handlePointerUp);
433
+ window.removeEventListener('pointercancel', handlePointerCancel);
434
+ window.removeEventListener('keydown', handleWindowKeyDown, true);
435
+ };
436
+ }
437
+
438
+ function handleClick(event: MouseEvent) {
439
+ event.preventDefault();
440
+ event.stopPropagation();
441
+ }
442
+
443
+ function handleFocus() {
444
+ isFocused = true;
445
+ isFocusVisible = shouldShowFocusVisible(element ?? null);
446
+ }
447
+
448
+ function handleBlur() {
449
+ isFocused = false;
450
+ isFocusVisible = false;
451
+ stopKeyboardResizeMode();
452
+ }
453
+
454
+ function handleKeyDown(event: KeyboardEvent) {
455
+ if (!isResizable) return;
456
+ trackInteractionModality(event, element ?? null);
457
+ isFocusVisible = true;
458
+
459
+ if ((event.ctrlKey || event.metaKey) && event.key === 'Home') {
460
+ event.preventDefault();
461
+ event.stopPropagation();
462
+ table.moveToGridStart();
463
+ return;
464
+ }
465
+
466
+ if ((event.ctrlKey || event.metaKey) && event.key === 'End') {
467
+ event.preventDefault();
468
+ event.stopPropagation();
469
+ table.moveToGridEnd();
470
+ return;
471
+ }
472
+
473
+ if (!keyboardResizeActive) {
474
+ switch (event.key) {
475
+ case 'Enter':
476
+ event.preventDefault();
477
+ if (event.repeat) return;
478
+ event.stopPropagation();
479
+ startKeyboardResizeMode();
480
+ return;
481
+ case 'ArrowLeft':
482
+ event.preventDefault();
483
+ event.stopPropagation();
484
+ focusHeaderCell();
485
+ return;
486
+ case 'ArrowRight':
487
+ event.preventDefault();
488
+ event.stopPropagation();
489
+ focusAdjacentHeaderCell('right');
490
+ return;
491
+ case 'ArrowUp':
492
+ event.preventDefault();
493
+ event.stopPropagation();
494
+ table.moveFocus('up');
495
+ return;
496
+ case 'ArrowDown':
497
+ event.preventDefault();
498
+ event.stopPropagation();
499
+ table.moveFocus('down');
500
+ return;
501
+ case 'Home':
502
+ event.preventDefault();
503
+ event.stopPropagation();
504
+ focusHeaderCell();
505
+ table.moveToRowStart();
506
+ return;
507
+ case 'End':
508
+ event.preventDefault();
509
+ event.stopPropagation();
510
+ focusHeaderCell();
511
+ table.moveToRowEnd();
512
+ return;
513
+ }
514
+ return;
515
+ }
516
+
517
+ event.stopPropagation();
518
+
519
+ const delta = event.shiftKey ? shiftStep : step;
520
+ const direction = isRightToLeft() ? -1 : 1;
521
+ const baseWidth = getResolvedWidth();
522
+
523
+ switch (event.key) {
524
+ case 'ArrowLeft':
525
+ event.preventDefault();
526
+ commitWidthChange(baseWidth - delta * direction);
527
+ return;
528
+ case 'ArrowRight':
529
+ event.preventDefault();
530
+ commitWidthChange(baseWidth + delta * direction);
531
+ return;
532
+ case 'Home':
533
+ event.preventDefault();
534
+ commitWidthChange(minWidth);
535
+ return;
536
+ case 'End':
537
+ event.preventDefault();
538
+ commitWidthChange(getAutoFitWidth());
539
+ return;
540
+ case 'Enter':
541
+ event.preventDefault();
542
+ stopKeyboardResizeMode();
543
+ return;
544
+ case 'Escape':
545
+ event.preventDefault();
546
+ stopKeyboardResizeMode({ restoreWidth: true, focusHeader: true });
547
+ return;
548
+ }
549
+ }
550
+
551
+ onDestroy(() => {
552
+ cleanupAnnouncementTimeout();
553
+ cleanupFocusHeaderTimeout();
554
+ stopKeyboardResizeMode();
555
+ cleanupPointerListeners();
556
+ table.unregisterColumnResizer(column.token);
557
+ cellContext?.notifyResizerRemoved?.();
558
+ });
559
+ </script>
560
+
561
+ {#if !column.isHidden}
562
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
563
+ <div
564
+ bind:this={element}
565
+ role="separator"
566
+ tabindex={isResizable ? 0 : undefined}
567
+ class={className}
568
+ aria-label={accessibleLabel}
569
+ aria-orientation="vertical"
570
+ aria-valuenow={currentWidth ?? undefined}
571
+ aria-valuemin={minWidth}
572
+ aria-valuemax={maxWidth}
573
+ aria-valuetext={accessibleValueText}
574
+ data-focused={isFocused ? 'true' : undefined}
575
+ data-focus-visible={isFocusVisible ? 'true' : undefined}
576
+ data-resizing={isResizing ? 'true' : undefined}
577
+ data-table-column-resizer="true"
578
+ data-resizable-direction="right"
579
+ style:position="absolute"
580
+ style:z-index="2"
581
+ style:top="0"
582
+ style:right="0"
583
+ style:transform="translateX(50%)"
584
+ style:width="0.75rem"
585
+ style:height="100%"
586
+ style:display="flex"
587
+ style:align-items="center"
588
+ style:justify-content="center"
589
+ style:user-select="none"
590
+ style:touch-action="none"
591
+ onpointerdown={handlePointerDown}
592
+ ondblclick={handleDoubleClick}
593
+ onclick={handleClick}
594
+ onfocus={handleFocus}
595
+ onblur={handleBlur}
596
+ onkeydown={handleKeyDown}
597
+ {...restProps}
598
+ >
599
+ {#if children}
600
+ {@render children()}
601
+ {:else}
602
+ <span
603
+ aria-hidden="true"
604
+ style="display:block;width:1px;min-height:1rem;background:currentColor;opacity:0.35;"
605
+ ></span>
606
+ {/if}
607
+ <span
608
+ data-testid="column-resize-status"
609
+ role="status"
610
+ aria-live="polite"
611
+ aria-atomic="true"
612
+ style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;"
613
+ >{resizeAnnouncement}</span
614
+ >
615
+ </div>
616
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type TableColumnResizerProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
4
+ step?: number;
5
+ shiftStep?: number;
6
+ children?: Snippet;
7
+ class?: string;
8
+ };
9
+ declare const TableColumnResizer: import("svelte").Component<TableColumnResizerProps, {}, "">;
10
+ type TableColumnResizer = ReturnType<typeof TableColumnResizer>;
11
+ export default TableColumnResizer;
@@ -0,0 +1,25 @@
1
+ <!-- markdownlint-disable MD024 -->
2
+
3
+ # Table.EmptyState
4
+
5
+ ## API reference
6
+
7
+ ### Table.EmptyState
8
+
9
+ Name: `Table.EmptyState`
10
+ Description: Convenience part for rendering a semantic empty row inside `Table.Body` when no data rows are registered.
11
+
12
+ | Prop | Type | Default | Description |
13
+ | ---------- | --------- | ----------- | ------------------------------------------------------- |
14
+ | `class` | `string` | `''` | Class names for the generated empty row. |
15
+ | `children` | `Snippet` | `undefined` | Empty-state content rendered inside the generated cell. |
16
+
17
+ ### Context utilities
18
+
19
+ Name: `Table.EmptyState` body context
20
+ Description: Reads body scope and table column count to generate a valid empty row.
21
+
22
+ | Prop | Type | Default | Description |
23
+ | ------------------------ | --------------------------- | ------- | --------------------------------------------- |
24
+ | `useTableContext` | `() => TableContext` | `-` | Reads the current table state. |
25
+ | `useTableSectionContext` | `() => TableSectionContext` | `-` | Ensures the part is used inside `Table.Body`. |
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { useTableContext, useTableSectionContext } from '../root/context';
4
+
5
+ type TableEmptyStateProps = {
6
+ children?: Snippet;
7
+ class?: string;
8
+ };
9
+
10
+ let { children, class: className = '' }: TableEmptyStateProps = $props();
11
+
12
+ const table = useTableContext();
13
+ const section = useTableSectionContext();
14
+ if (section.section !== 'body') {
15
+ throw new Error('`Table.EmptyState` must be used inside `Table.Body`.');
16
+ }
17
+
18
+ const layoutVersion = table.layoutVersion;
19
+ const isVisible = $derived.by(() => {
20
+ void $layoutVersion;
21
+ return table.getBodyRowCount() === 0;
22
+ });
23
+ const columnCount = $derived.by(() => {
24
+ void $layoutVersion;
25
+ return Math.max(table.getVisibleColumnCount(), 1);
26
+ });
27
+ </script>
28
+
29
+ {#if isVisible}
30
+ <!-- svelte-ignore a11y_no_redundant_roles -->
31
+ <tr role="row" data-empty class={className}>
32
+ <td role="gridcell" colspan={columnCount} aria-disabled="true">
33
+ {#if children}
34
+ {@render children()}
35
+ {/if}
36
+ </td>
37
+ </tr>
38
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ type TableEmptyStateProps = {
3
+ children?: Snippet;
4
+ class?: string;
5
+ };
6
+ declare const TableEmptyState: import("svelte").Component<TableEmptyStateProps, {}, "">;
7
+ type TableEmptyState = ReturnType<typeof TableEmptyState>;
8
+ export default TableEmptyState;