@rozie-ui/data-table-solid 0.5.0 → 0.7.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.
package/src/DataTable.tsx CHANGED
@@ -3,7 +3,7 @@ import { Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount, s
3
3
  import { Key } from '@solid-primitives/keyed';
4
4
  import { __rozieInjectStyle, createControllableSignal, parseInlineStyle, rozieAttr, rozieClass, rozieContext, rozieDisplay } from '@rozie/runtime-solid';
5
5
  import Popover from '@rozie-ui/popover-solid';
6
- import { isSafeKey, wrapAggregationFn, indexDefsById, collectGroupableLeafDefs } from './helpers/columnDefUtils';
6
+ import { isSafeKey, wrapAggregationFn, indexDefsById, editorKindWarning, collectGroupableLeafDefs } from './helpers/columnDefUtils';
7
7
  import { applyUpdater, clamp, focusables } from './helpers/indexMath';
8
8
  import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
9
9
  import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
@@ -35,33 +35,40 @@ import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, m
35
35
  // snapshot (the rete stale-closure anti-pattern — a top-level $computed/useCallback
36
36
  // freezes the table at the empty-initial state on React).
37
37
 
38
+ // C-09's value-equality guard. Imported HERE, in DataTable.rozie's OWN script, because that
39
+ // is where it is USED: a `.rzts` partial's import of a symbol the partial itself never
40
+ // references is dropped as unused, and the host script then reaches a free identifier. That
41
+ // failed at RUNTIME, not at compile time — `ReferenceError: columnSpecsEquivalent is not
42
+ // defined` on the first paste — which is the only way this class of mistake shows up.
43
+ import { columnSpecsEquivalent } from './helpers/columnDefUtils';
44
+
45
+ // The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
46
+
38
47
  __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
39
- --rdt-font: var(--rozie-data-table-font);
40
- --rdt-color: var(--rozie-data-table-fg);
41
- --rdt-cell-padding: var(--rozie-data-table-cell-padding);
42
- --rdt-border: 1px solid var(--rozie-data-table-border-color);
43
- --rdt-header-bg: var(--rozie-data-table-header-bg);
44
- --rdt-header-weight: var(--rozie-data-table-header-weight);
45
- --rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
46
- --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
47
- --rdt-filter-radius: var(--rozie-data-table-radius);
48
- --rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
49
- --rdt-page-btn-radius: var(--rozie-data-table-radius);
50
- --rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
51
- --rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
52
- --rdt-page-size-radius: var(--rozie-data-table-radius);
53
- --rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
54
- --rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
55
- --rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
56
- --rdt-pin-btn-radius: var(--rozie-data-table-radius);
57
- --rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
58
- --rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color);
59
- --rdt-colvis-summary-radius: var(--rozie-data-table-radius);
60
- --rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color);
61
- --rdt-colvis-menu-radius: var(--rozie-data-table-radius);
62
- --rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg);
63
- --rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow);
64
- --rdt-select-accent: var(--rozie-data-table-select-accent);
48
+ --rdt-color: var(--rozie-data-table-fg, var(--rdt-ds-fg));
49
+ --rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, var(--rdt-ds-select-accent)));
50
+ --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
51
+ --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
52
+ --rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
53
+ --rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
54
+ --rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
55
+ --rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
56
+ --rdt-header-weight: var(--rozie-data-table-header-weight, var(--rdt-ds-header-weight));
57
+ --rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity, var(--rdt-ds-sort-indicator-opacity));
58
+ --rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color, var(--rdt-ds-resize-grip-color));
59
+ --rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active, var(--rozie-data-table-accent, var(--rdt-ds-resize-grip-active)));
60
+ --rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg, var(--rdt-ds-pin-active-bg));
61
+ --rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg, var(--rdt-ds-menu-bg));
62
+ --rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow, var(--rdt-ds-menu-shadow));
63
+ --rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
64
+ --rdt-colvis-menu-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
65
+ --rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
66
+ --rdt-colvis-summary-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
67
+ --rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity, var(--rdt-ds-disabled-opacity));
68
+ --rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
69
+ --rdt-page-btn-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
70
+ --rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
71
+ --rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
65
72
  }
66
73
  .rozie-data-table[data-rozie-s-d5dcab4c] {
67
74
  border-collapse: collapse;
@@ -189,6 +196,16 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
189
196
  flex-direction: column;
190
197
  gap: var(--rdt-chrome-gap, 0.5rem);
191
198
  }
199
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-skeleton[data-rozie-s-d5dcab4c] {
200
+ /* inline-block on the cell's own line box, exactly like a text value (a block box was 2px
201
+ short of a text row). */
202
+ display: inline-block;
203
+ width: 100%;
204
+ vertical-align: baseline;
205
+ border-radius: 4px;
206
+ background: color-mix(in srgb, currentColor 12%, transparent);
207
+ color: transparent;
208
+ }
192
209
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-toolbar[data-rozie-s-d5dcab4c] {
193
210
  display: flex;
194
211
  gap: var(--rdt-toolbar-gap, 0.5rem);
@@ -214,6 +231,9 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
214
231
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] {
215
232
  background: var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015));
216
233
  }
234
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
235
+ box-sizing: border-box;
236
+ }
217
237
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
218
238
  padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
219
239
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
@@ -333,6 +353,9 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
333
353
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-colvis-summary[data-rozie-s-d5dcab4c] {
334
354
  cursor: pointer;
335
355
  font: inherit;
356
+ /* Own the margin too: content-site typography styles bare \`summary\` (VitePress sets
357
+ \`margin: 16px 0\`), which grew the <details> and stretched the toolbar's search input. */
358
+ margin: 0;
336
359
  padding: var(--rdt-colvis-summary-padding, 0.25rem 0.6rem);
337
360
  border: var(--rdt-colvis-summary-border, 1px solid rgba(0, 0, 0, 0.2));
338
361
  border-radius: var(--rdt-colvis-summary-radius, 4px);
@@ -371,6 +394,51 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
371
394
  text-align: var(--rdt-expander-col-align, center);
372
395
  white-space: nowrap;
373
396
  }
397
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="1"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
398
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="2"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
399
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="3"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
400
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="4"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
401
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="5"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
402
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="6"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
403
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="7"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
404
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="8"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
405
+ content: '';
406
+ display: inline-block;
407
+ vertical-align: middle;
408
+ }
409
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="1"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
410
+ width: calc(1 * var(--rdt-tree-indent, 1.25rem));
411
+ }
412
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="2"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
413
+ width: calc(2 * var(--rdt-tree-indent, 1.25rem));
414
+ }
415
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="3"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
416
+ width: calc(3 * var(--rdt-tree-indent, 1.25rem));
417
+ }
418
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="4"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
419
+ width: calc(4 * var(--rdt-tree-indent, 1.25rem));
420
+ }
421
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="5"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
422
+ width: calc(5 * var(--rdt-tree-indent, 1.25rem));
423
+ }
424
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="6"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
425
+ width: calc(6 * var(--rdt-tree-indent, 1.25rem));
426
+ }
427
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="7"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
428
+ width: calc(7 * var(--rdt-tree-indent, 1.25rem));
429
+ }
430
+ .rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="8"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
431
+ width: calc(8 * var(--rdt-tree-indent, 1.25rem));
432
+ }
433
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] {
434
+ user-select: none;
435
+ }
436
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] input[data-rozie-s-d5dcab4c],
437
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] textarea[data-rozie-s-d5dcab4c],
438
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] select[data-rozie-s-d5dcab4c],
439
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] [contenteditable="true"][data-rozie-s-d5dcab4c] {
440
+ user-select: text;
441
+ }
374
442
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-all[data-rozie-s-d5dcab4c],
375
443
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-row[data-rozie-s-d5dcab4c] {
376
444
  cursor: pointer;
@@ -381,17 +449,19 @@ interface GroupBarSlotCtx { grouping: any; groupableColumns: any; applyGrouping:
381
449
 
382
450
  interface SelectAllSlotCtx { checked: any; indeterminate: any; toggle: any; }
383
451
 
452
+ interface PlaceholderSlotCtx { index: any; columnId: any; }
453
+
384
454
  interface SelectCellSlotCtx { row: any; checked: any; toggle: any; }
385
455
 
386
456
  interface DetailSlotCtx { row: any; }
387
457
 
388
458
  interface ColHeaderSlotCtx { columnId: any; column: any; label: any; }
389
459
 
390
- interface FilterSlotCtx { columnId: any; value: any; uniqueValues: any; minMax: any; setFilter: any; }
460
+ interface FilterSlotCtx { columnId: any; value: any; uniqueValues: any; minMax: any; columnLabel: any; setFilter: any; }
391
461
 
392
462
  interface CellSlotCtx { columnId: any; column: any; row: any; value: any; }
393
463
 
394
- interface EditorSlotCtx { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any; }
464
+ interface EditorSlotCtx { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; columnLabel: any; autofocus: any; }
395
465
 
396
466
  interface DataTableProps {
397
467
  /**
@@ -456,6 +526,10 @@ interface DataTableProps {
456
526
  expanded?: (Record<string, any> | boolean) | null;
457
527
  defaultExpanded?: (Record<string, any> | boolean) | null;
458
528
  onExpandedChange?: (expanded: (Record<string, any> | boolean) | null) => void;
529
+ /**
530
+ * Row identity `(originalRow, index, parentRow?) => string | number` — the key `rowSelection`, `expanded` and the per-row caches use. Default null → table-core keys rows by position (`"0"`, `"1"`, … and `"0.1"` for sub-rows), so inserting or removing rows above a selected or expanded row moves that state onto whatever row now sits at its index. Supply it whenever rows can be added or removed while state is held (server push, live lists); the result is coerced to a string.
531
+ */
532
+ getRowId?: ((...args: any[]) => any) | null;
459
533
  /**
460
534
  * Table-level child-row accessor `(originalRow, index) => TData[] | undefined` that drives nested sub-rows. When supplied (with `expandable`), table-core flattens the hierarchy and the expand seam reveals depth-indented child rows. Null → the `#detail` scoped slot is the expand mode.
461
535
  */
@@ -547,7 +621,9 @@ interface DataTableProps {
547
621
  onReorderChange?: (...args: unknown[]) => void;
548
622
  onPinChange?: (...args: unknown[]) => void;
549
623
  onHistoryChange?: (...args: unknown[]) => void;
624
+ onVisibleRangeChange?: (...args: unknown[]) => void;
550
625
  onActivecellChange?: (...args: unknown[]) => void;
626
+ onRowActivate?: (...args: unknown[]) => void;
551
627
  onRangeChange?: (...args: unknown[]) => void;
552
628
  onCellEditCommit?: (...args: unknown[]) => void;
553
629
  onRowEditCommit?: (...args: unknown[]) => void;
@@ -555,13 +631,14 @@ interface DataTableProps {
555
631
  children?: JSX.Element;
556
632
  groupBarSlot?: (ctx: GroupBarSlotCtx) => JSX.Element;
557
633
  selectAllSlot?: (ctx: SelectAllSlotCtx) => JSX.Element;
634
+ placeholderSlot?: (ctx: PlaceholderSlotCtx) => JSX.Element;
558
635
  selectCellSlot?: (ctx: SelectCellSlotCtx) => JSX.Element;
559
636
  detailSlot?: (ctx: DetailSlotCtx) => JSX.Element;
560
637
  colHeaderSlot?: (ctx: ColHeaderSlotCtx) => JSX.Element;
561
638
  filterSlot?: (ctx: FilterSlotCtx) => JSX.Element;
562
639
  cellSlot?: (ctx: CellSlotCtx) => JSX.Element;
563
640
  editorSlot?: (ctx: EditorSlotCtx) => JSX.Element;
564
- slots?: { [key: `colHeader-${string}`]: ((ctx: { columnId: any; column: any; label: any }) => JSX.Element) | undefined; [key: `filter-${string}`]: ((ctx: { columnId: any; value: any; uniqueValues: any; minMax: any; setFilter: any }) => JSX.Element) | undefined; [key: `cell-${string}`]: ((ctx: { columnId: any; column: any; row: any; value: any }) => JSX.Element) | undefined; [key: `editor-${string}`]: ((ctx: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any }) => JSX.Element) | undefined; [key: string]: ((...args: any[]) => JSX.Element) | undefined; };
641
+ slots?: { [key: `colHeader-${string}`]: ((ctx: { columnId: any; column: any; label: any }) => JSX.Element) | undefined; [key: `filter-${string}`]: ((ctx: { columnId: any; value: any; uniqueValues: any; minMax: any; columnLabel: any; setFilter: any }) => JSX.Element) | undefined; [key: `cell-${string}`]: ((ctx: { columnId: any; column: any; row: any; value: any }) => JSX.Element) | undefined; [key: `editor-${string}`]: ((ctx: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; columnLabel: any; autofocus: any }) => JSX.Element) | undefined; [key: string]: ((...args: any[]) => JSX.Element) | undefined; };
565
642
  ref?: (h: DataTableHandle) => void;
566
643
  }
567
644
 
@@ -589,6 +666,8 @@ export interface DataTableHandle {
589
666
  focusCell: (...args: any[]) => any;
590
667
  getActiveCell: (...args: any[]) => any;
591
668
  clearActiveCell: (...args: any[]) => any;
669
+ scrollToRow(index: any, options?: { align?: 'start' | 'center' | 'end' | 'auto'; behavior?: 'auto' | 'smooth' | 'instant'; }): void;
670
+ getScrollElement(): any;
592
671
  getRowIndexRelativeToPage: (...args: any[]) => any;
593
672
  editCell: (...args: any[]) => any;
594
673
  commitEditing: (...args: any[]) => any;
@@ -603,10 +682,10 @@ export interface DataTableHandle {
603
682
  }
604
683
 
605
684
  export default function DataTable(_props: DataTableProps): JSX.Element {
606
- const _merged = mergeProps({ columns: (() => [])() as any[], selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, virtual: false, estimateRowHeight: 40, autoMeasure: false, maxHeight: '' }, _props);
607
- const [local, attrs] = splitProps(_merged, ['data', 'columns', 'selectionMode', 'sorting', 'globalFilter', 'columnFilters', 'pagination', 'manual', 'rowCount', 'pageCount', 'expandable', 'expanded', 'getSubRows', 'groupable', 'grouping', 'rowSelection', 'columnVisibility', 'columnSizing', 'columnOrder', 'columnPinning', 'stickyHeader', 'interactionMode', 'singleClickEdit', 'undoable', 'undoLimit', 'virtual', 'estimateRowHeight', 'autoMeasure', 'maxHeight', 'children', 'ref', 'onSortChange', 'onExpandChange', 'onGroupChange', 'onFilterChange', 'onPageChange', 'onSelectionChange', 'onVisibilityChange', 'onResizeChange', 'onReorderChange', 'onPinChange', 'onHistoryChange', 'onActivecellChange', 'onRangeChange', 'onCellEditCommit', 'onRowEditCommit']);
685
+ const _merged = mergeProps({ columns: (() => [])() as any[], selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getRowId: null, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, virtual: false, estimateRowHeight: 40, autoMeasure: false, maxHeight: '' }, _props);
686
+ const [local, attrs] = splitProps(_merged, ['data', 'columns', 'selectionMode', 'sorting', 'globalFilter', 'columnFilters', 'pagination', 'manual', 'rowCount', 'pageCount', 'expandable', 'expanded', 'getRowId', 'getSubRows', 'groupable', 'grouping', 'rowSelection', 'columnVisibility', 'columnSizing', 'columnOrder', 'columnPinning', 'stickyHeader', 'interactionMode', 'singleClickEdit', 'undoable', 'undoLimit', 'virtual', 'estimateRowHeight', 'autoMeasure', 'maxHeight', 'children', 'ref', 'onSortChange', 'onExpandChange', 'onGroupChange', 'onFilterChange', 'onPageChange', 'onSelectionChange', 'onVisibilityChange', 'onResizeChange', 'onReorderChange', 'onPinChange', 'onHistoryChange', 'onVisibleRangeChange', 'onActivecellChange', 'onRowActivate', 'onRangeChange', 'onCellEditCommit', 'onRowEditCommit']);
608
687
  const resolved = () => local.children;
609
- onMount(() => { local.ref?.({ sortColumn, clearSorting, toggleRowExpanded, expandAll, collapseAll, getExpandedRows, applyGrouping, clearGrouping, getFacetedUniqueValues, getFacetedMinMaxValues, getColumnDefs, toggleAllRows, clearSelection, getSelectedRows, setPage, setRowsPerPage, toggleColumnVisibility, applyColumnOrder, resetColumnSizing, pinColumn, focusCell, getActiveCell, clearActiveCell, getRowIndexRelativeToPage, editCell, commitEditing, editRow, getSelectedRange, cut, undo, redo, canUndo, canRedo, clearHistory }); });
688
+ onMount(() => { local.ref?.({ sortColumn, clearSorting, toggleRowExpanded, expandAll, collapseAll, getExpandedRows, applyGrouping, clearGrouping, getFacetedUniqueValues, getFacetedMinMaxValues, getColumnDefs, toggleAllRows, clearSelection, getSelectedRows, setPage, setRowsPerPage, toggleColumnVisibility, applyColumnOrder, resetColumnSizing, pinColumn, focusCell, getActiveCell, clearActiveCell, scrollToRow, getScrollElement, getRowIndexRelativeToPage, editCell, commitEditing, editRow, getSelectedRange, cut, undo, redo, canUndo, canRedo, clearHistory }); });
610
689
 
611
690
  const __ctx_data_table_columns = rozieContext("data-table:columns");
612
691
  const [data, setData] = createControllableSignal<any[]>(_props as unknown as Record<string, unknown>, 'data', []);
@@ -674,6 +753,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
674
753
  const [rangeAnchor, setRangeAnchor] = createSignal<any>(null);
675
754
  const [rangeFocus, setRangeFocus] = createSignal<any>(null);
676
755
  const [pasteAnnounce, setPasteAnnounce] = createSignal('');
756
+ const [rangeAnnounce, setRangeAnnounce] = createSignal('');
677
757
  const [liveAnnounce, setLiveAnnounce] = createSignal('');
678
758
  onMount(() => {
679
759
  // Seed the uncontrolled `data` fallback (Phase 51 req-4) from the initial prop so an
@@ -698,7 +778,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
698
778
  // the getter bought nothing. Snapshot the initial data here; setOptions owns updates.
699
779
  // currentData() = the bound prop when controlled, else the uncontrolled $data.dataDefault
700
780
  // (Phase 51 req-4 — so a committed edit's writeData re-feed is observed either way).
701
- data: currentData(),
781
+ data: feedData(),
702
782
  columns: tableColumns(),
703
783
  state: currentState(),
704
784
  getCoreRowModel: getCoreRowModel(),
@@ -713,7 +793,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
713
793
  // default `!!subRows.length` rule applies (only parents with children expand).
714
794
  getExpandedRowModel: getExpandedRowModel(),
715
795
  getSubRows: (local.getSubRows || undefined) as any,
716
- getRowCanExpand: local.expandable === true && local.getSubRows == null ? () => true : undefined,
796
+ // Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
797
+ // other row-model fns.
798
+ getRowId: rowIdOption(),
799
+ getRowCanExpand: local.expandable === true && local.getSubRows == null ? (row: any) => !rowIsLazyPlaceholder(row) : undefined,
717
800
  onExpandedChange: onExpandedChangeCb,
718
801
  // Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
719
802
  // POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
@@ -758,7 +841,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
758
841
  // Row selection (req-7): enabled unless 'none'; 'single' caps at ≤1
759
842
  // (enableMultiRowSelection:false). Select-all scope = filtered rows (TanStack
760
843
  // default, D-06 — NOT overridden).
761
- enableRowSelection: local.selectionMode !== 'none',
844
+ enableRowSelection: rowSelectionOption(),
762
845
  enableMultiRowSelection: local.selectionMode === 'multiple',
763
846
  // PER-SLICE callbacks (Open-Q1: each maps 1:1 to a slice's r-model + change event,
764
847
  // no global onStateChange diff) — hoisted top-level consts, re-passed by the re-feed
@@ -802,6 +885,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
802
885
  // NEVER in a render helper (Pitfall 1). Pass the COMPLETE options set (virtual-core's
803
886
  // setOptions replaces, not merges). Guarded so the off path executes no virtual-core code.
804
887
  if (rowsWindowed() && virtualizer) {
888
+ // Rekey any placeholder rows this re-feed just filled (quick 260926, VR probe) BEFORE
889
+ // handing the virtualizer its fresh options — see migratePlaceholderSizeCache()'s own
890
+ // comment (virtualization.rzts) for the itemSizeCache/getMeasurements memo root cause.
891
+ migratePlaceholderSizeCache();
805
892
  virtualizer.setOptions(virtualizerOptions());
806
893
  virtualizer._willUpdate();
807
894
  }
@@ -872,6 +959,21 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
872
959
  // cell selectors hang off (the exact idiom proven ×6 by plan 01's probe). Captured here
873
960
  // (post-mount) so it is non-null and ROZ123-clean.
874
961
  gridRoot = __rozieRootRef! ? __rozieRootRef!.querySelector('.rozie-data-table') : null;
962
+ // B-02: the click-away detector. `composedPath()` rather than `ev.target` because on Lit the
963
+ // grid lives in a shadow root and a document-level listener sees the retargeted HOST element,
964
+ // which is never `gridRoot.contains(...)` — every in-grid click would read as "outside".
965
+ if (typeof document !== 'undefined') {
966
+ docPointerDown = (ev: any) => {
967
+ if (!gridRoot) {
968
+ outsidePointerDown = false;
969
+ return;
970
+ }
971
+ const path = ev && typeof ev.composedPath === 'function' ? ev.composedPath() : null;
972
+ const inside = path ? path.indexOf(gridRoot) !== -1 : !!(ev && ev.target && gridRoot.contains && gridRoot.contains(ev.target));
973
+ outsidePointerDown = !inside;
974
+ };
975
+ document.addEventListener('pointerdown', docPointerDown, true);
976
+ }
875
977
  // WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
876
978
  // page load AND was non-deterministic on React/Solid (the entry cell may not be
877
979
  // committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
@@ -1001,6 +1103,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1001
1103
  announceState.globalFilter = effectiveGlobalFilter();
1002
1104
  });
1003
1105
  onCleanup(() => {
1106
+ // B-02: drop the document click-away listener (it is attached for the instance's lifetime,
1107
+ // not per edit session — attaching it per session would freeze a stale closure on React).
1108
+ if (docPointerDown && typeof document !== 'undefined') {
1109
+ document.removeEventListener('pointerdown', docPointerDown, true);
1110
+ docPointerDown = null;
1111
+ }
1004
1112
  if (virtualizerCleanup) virtualizerCleanup();
1005
1113
  // Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
1006
1114
  // No-op when column windowing was off (cleanup stays null).
@@ -1020,6 +1128,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1020
1128
  });
1021
1129
  createEffect(() => {
1022
1130
  maybeClearHistoryOnExternalSwap();
1131
+ // visible-range-change backstop (quick 260925-dtl part 2): a re-render from new data can move
1132
+ // the rendered window WITHOUT a window-version bump (measured on React: filling placeholders
1133
+ // shifted the window by two rows and nothing was reported). Deduped, so a render that leaves
1134
+ // the window where it was emits nothing.
1135
+ emitVisibleRangeIfChanged();
1136
+ // A-02: the post-render hook is the one place that observes EVERY column-width change on all
1137
+ // six targets — an interactive resize writes `columnSizing` through table-core under
1138
+ // `columnResizeMode: 'onChange'` and never reaches the coarse re-feed watch (which keys on
1139
+ // `$props.columnSizing`, undefined on the uncontrolled path). Self-guarded on the width
1140
+ // signature, so a tick that changed no width costs one loop over the leaf columns.
1141
+ remeasureColumnSizes();
1023
1142
  if (!table) return;
1024
1143
  // Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
1025
1144
  // $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
@@ -1031,6 +1150,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1031
1150
  lastDataLen = d.length;
1032
1151
  reFeed();
1033
1152
  });
1153
+ createEffect(on(() => (() => windowVer())(), (v) => untrack(() => (() => emitVisibleRangeIfChanged())()), { defer: true }));
1034
1154
  createEffect(on(() => (() => [sorting(), globalFilter(), columnFilters(), pagination(),
1035
1155
  // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
1036
1156
  // changes at runtime (e.g. a server response updates the total) so getPageCount() and the
@@ -1105,6 +1225,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1105
1225
  // handle and TS2322s on a numeric seed.
1106
1226
  let remeasureRaf: any = null;
1107
1227
  let remeasureDisposed = false;
1228
+ // D-19 (quick 260923-rrr): "the user scrolled to the end" — recorded only when the scroll
1229
+ // offset actually MOVED (a growth-driven onChange leaves it where it was), with the row count
1230
+ // and offset at that moment. See scheduleRemeasure() in virtualization.rzts.
1231
+ let scrollEndPinned: boolean = false;
1232
+ let scrollEndPinnedCount: number = -1;
1233
+ let scrollEndPinnedTop: number = -1;
1108
1234
 
1109
1235
  // ── Grid interaction-mode constants + DOM root (phase 49, REQ-2/6) ────────────────────
1110
1236
  // Fixed PageUp/PageDown row step (D-06). Phase 53 swaps this for the visible-window size
@@ -1117,6 +1243,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1117
1243
  // querySelector-off-root precedent, proven ×6 by plan 01's probe). NEVER read in a
1118
1244
  // computed/template binding (ROZ123).
1119
1245
  let gridRoot: any = null;
1246
+ // ── B-02: "did the user just click OUTSIDE the grid?" ──────────────────────────────────
1247
+ // Set by the document-level pointerdown listener registered in $onMount, consumed by
1248
+ // onEditorBlur's null-relatedTarget branch (editorBindings.rzts), which explains in full why
1249
+ // the decision has to come from here and be routed back there. In short: a null relatedTarget
1250
+ // on its own cannot tell a click onto a non-focusable page region (the edit must commit) from
1251
+ // an editor being recycled out of the virtual window (it must NOT), and a document listener
1252
+ // cannot commit for itself on React because it closes over the render that attached it — the
1253
+ // draft it would write is the one from editor-open. A plain module-let is hoisted to a
1254
+ // per-instance `useRef` there, so it is read FRESH from the template-bound blur handler.
1255
+ // Capture phase, so a stopPropagation anywhere in the page cannot hide the click from us.
1256
+ let outsidePointerDown = false;
1257
+ let docPointerDown: any = null;
1120
1258
 
1121
1259
  // Echo-guard: while WE are writing a slice back, the re-feed watcher must not re-enter
1122
1260
  // the funnel. A counter (not a boolean) so nested writes are safe.
@@ -1360,6 +1498,54 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1360
1498
  // ({ id?, header, columns: [...] }) maps to a multi-level header GROUP column
1361
1499
  // whose children are built recursively (B12 — grouped/multi-level column headers).
1362
1500
  // Returns null for an unusable entry (no id/field, unsafe key, empty group).
1501
+ /**
1502
+ * A-01 — translate the public per-column `width` into table-core's `size`.
1503
+ *
1504
+ * `width` was written onto the ColumnDef in both build branches and read by NOTHING:
1505
+ * table-core resolves column width from `size` (a NUMBER of px), which only the two
1506
+ * chrome columns ever set. So the documented API was completely inert — the exact
1507
+ * sibling of the inert `pinned` that 0.5.0 closed.
1508
+ *
1509
+ * `size` is numeric px by construction, so only a number or a px string can map onto
1510
+ * it; any other CSS length (`12rem`, `20%`, `auto`) has no numeric px value at build
1511
+ * time and is deliberately NOT guessed — it returns null and the column keeps
1512
+ * table-core's default. The `width` prop's docs say exactly this.
1513
+ *
1514
+ * Returns null (not undefined) for "no usable width" so callers can test `!= null`
1515
+ * without tripping over a legitimate 0.
1516
+ */
1517
+ function parseWidthToSize(w: any) {
1518
+ if (typeof w === 'number') return Number.isFinite(w) && w > 0 ? w : null;
1519
+ if (typeof w !== 'string') return null;
1520
+ const t = w.trim();
1521
+ if (t === '') return null;
1522
+ const m = /^(\d+(?:\.\d+)?)(px)?$/i.exec(t);
1523
+ if (!m) return null;
1524
+ const n = Number.parseFloat(m[1]);
1525
+ return Number.isFinite(n) && n > 0 ? n : null;
1526
+ }
1527
+
1528
+ // ── E-03 ────────────────────────────────────────────────────────────────────────────────
1529
+ // The union check itself is `editorKindWarning` in helpers/columnDefUtils.ts (pure, so the
1530
+ // contract is unit-testable). Here we own the two things that are not pure: WHEN it runs and
1531
+ // how often it speaks.
1532
+ //
1533
+ // WHEN: at column-BUILD time, not in `editorTypeOf`, which is called per cell per render (D-23).
1534
+ // HOW OFTEN: latched per `id:value`, so a rebuild — the memo misses whenever the column set
1535
+ // changes identity — cannot turn one misconfiguration into a console flood.
1536
+ // Unguarded console.warn, matching the D-07/D-08 virtual-misconfig warns in DataTable.rozie:
1537
+ // a process.env guard is not bundler-portable across six targets, and always-warn-on-misconfig
1538
+ // is the established call in this component.
1539
+ const editorWarned = Object.create(null);
1540
+ function checkEditorKind(id: any, editor: any) {
1541
+ const msg = editorKindWarning(id, editor);
1542
+ if (!msg) return editor;
1543
+ const key = id + '\u0000' + String(editor);
1544
+ if (editorWarned[key]) return editor;
1545
+ editorWarned[key] = true;
1546
+ console.warn(msg);
1547
+ return editor;
1548
+ }
1363
1549
  function buildConfigDef(c: any) {
1364
1550
  if (!c) return null;
1365
1551
  // Grouped (multi-level) header column: an entry carrying a `columns` array. table-core's
@@ -1415,11 +1601,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1415
1601
  aggregationFn: wrapAggregationFn(c.aggregationFn),
1416
1602
  pinned: c.pinned != null ? c.pinned : '',
1417
1603
  width: c.width != null ? c.width : '',
1604
+ // A-01 — `width` alone is inert; table-core sizes from `size`. Spread so an
1605
+ // unusable width leaves `size` ABSENT rather than explicitly undefined, which
1606
+ // would override a consumer's `defaultColumn.size`.
1607
+ ...(parseWidthToSize(c.width) != null ? {
1608
+ size: parseWidthToSize(c.width)
1609
+ } : {}),
1418
1610
  // Editable-cell config (Phase 51) → ColumnDef.meta, the table-core per-column
1419
1611
  // metadata carrier the display↔editor branch + runValidator read. Off by default.
1420
1612
  meta: {
1421
1613
  editable: c.editable === true,
1422
- editor: c.editor != null ? c.editor : 'text',
1614
+ editor: c.editor != null ? checkEditorKind(id, c.editor) : 'text',
1423
1615
  editorOptions: c.editorOptions != null ? c.editorOptions : [],
1424
1616
  validate: typeof c.validate === 'function' ? c.validate : null
1425
1617
  }
@@ -1499,10 +1691,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1499
1691
  aggregationFn: wrapAggregationFn(spec.aggregationFn),
1500
1692
  pinned: spec.pinned != null ? spec.pinned : '',
1501
1693
  width: spec.width != null ? spec.width : '',
1694
+ // A-01 — see buildConfigDef; same mapping for the <Column> registry branch.
1695
+ ...(parseWidthToSize(spec.width) != null ? {
1696
+ size: parseWidthToSize(spec.width)
1697
+ } : {}),
1502
1698
  // Editable-cell config (Phase 51) → ColumnDef.meta from the <Column> registry spec.
1503
1699
  meta: {
1504
1700
  editable: spec.editable === true,
1505
- editor: spec.editor != null ? spec.editor : 'text',
1701
+ editor: spec.editor != null ? checkEditorKind(id, spec.editor) : 'text',
1506
1702
  editorOptions: spec.editorOptions != null ? spec.editorOptions : [],
1507
1703
  validate: typeof spec.validate === 'function' ? spec.validate : null
1508
1704
  }
@@ -2130,9 +2326,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2130
2326
  }
2131
2327
  }
2132
2328
  // Active cell's column (D-10): required or focusActiveCell()'s [data-col-index="N"]
2133
- // querySelector cannot resolve for an off-window active cell. Body cells only — a header-
2134
- // active cell has no column-axis rendering dependency here (headers window separately).
2135
- if (!activeIsHeader() && activeColIndex() >= 0 && out.indexOf(activeColIndex()) === -1) {
2329
+ // querySelector cannot resolve for an off-window active cell.
2330
+ //
2331
+ // B-03: this used to be body-cells-only, on the stated grounds that "a header-active cell
2332
+ // has no column-axis rendering dependency here (headers window separately)". That is false —
2333
+ // `windowedHeadersFor()` slices the header row from `windowedColIndices()`, the very set this
2334
+ // function feeds, so a header cell has EXACTLY the same dependency a body cell does. With the
2335
+ // active header column unforced, focusing a leaf header in a 60-column windowed grid and
2336
+ // pressing End left `resolveCellEl` with nothing to find: focus froze on the old `<th>` while
2337
+ // the roving model moved on, and `cellTabindex` handed `tabindex="0"` to a cell that does not
2338
+ // exist — so the grid had no tab stop at all. The gate is gone; the column is forced for
2339
+ // whichever axis the active cell is on.
2340
+ if (activeColIndex() >= 0 && out.indexOf(activeColIndex()) === -1) {
2136
2341
  out.push(activeColIndex());
2137
2342
  }
2138
2343
  // Editing column (D-10): required or an open editor unmounts mid-keystroke when a
@@ -2210,21 +2415,64 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2210
2415
  // committed yet), so the rAF pass is what observes React's recycled rows.
2211
2416
  // Each pass only OBSERVES + measures the live window; measureElement is idempotent on an
2212
2417
  // already-observed node, so running both is cheap and loop-free.
2418
+ // Upper bound on the frames scheduleRemeasure() will wait for the framework to commit a recycled
2419
+ // window (N-05). Measured commit lag is 1 frame on React/Angular; 10 is generous headroom.
2420
+ const REMEASURE_MAX_FRAMES = 10;
2213
2421
  function scheduleRemeasure() {
2214
2422
  // 87-13: never arm a new sweep after teardown — an onChange can still fire from a
2215
2423
  // virtualizer whose own ResizeObserver cleanup has run but whose queued tick was already
2216
2424
  // in flight.
2217
2425
  if (remeasureDisposed) return;
2426
+ // D-19 (quick 260923-rrr): remember whether the view is at the END — but only when the scroll
2427
+ // offset MOVED since the last record. A measurement-driven onChange (rows growing) arrives
2428
+ // with the offset unchanged, and it is exactly that growth that moves the view off the end,
2429
+ // so it must not clear the pin. (virtual-core's isScrolling cannot tell the two apart: it
2430
+ // stays true for 150ms after the user's scroll, spanning the growth.)
2431
+ if (virtualizer && gridScrollEl && !virtualizer.scrollState && gridScrollEl.scrollTop !== scrollEndPinnedTop) {
2432
+ scrollEndPinnedTop = gridScrollEl.scrollTop;
2433
+ // Judged on virtual-core's MODEL (total size − viewport − offset), not DOM geometry: its own
2434
+ // above-viewport adjustments move scrollTop before the framework commits the matching
2435
+ // spacer, so the DOM transiently reads "not at the end" for a view that is (measured on
2436
+ // Angular: a 12px adjustment cleared a DOM-judged pin and left the view 12px short).
2437
+ scrollEndPinned = virtualizer.getVirtualDistanceFromEnd() <= 1;
2438
+ scrollEndPinnedCount = windowSource().length;
2439
+ }
2218
2440
  if (remeasurePending) return;
2219
2441
  remeasurePending = true;
2220
2442
  let ranMicro = false;
2221
2443
  const microPass = () => {
2222
2444
  remeasureWindow();
2223
2445
  };
2446
+ // N-05 (quick 260923-rrr): the rAF pass keys on the OUTCOME, not on "a frame went by". React
2447
+ // and Angular commit the recycled window AFTER that first rAF, so a single pass measured the
2448
+ // OLD rows and the new ones were only handed to measureElement when virtual-core's 150ms
2449
+ // scrolling-ended tick fired an onChange — the user saw the un-adjusted layout, then a 1–3 row
2450
+ // jump (measured). While the committed <tr> set does not yet cover the virtualizer's window,
2451
+ // re-run next frame, bounded so a window that can never be covered cannot spin forever.
2452
+ let rafAttempts = 0;
2224
2453
  const rafPass = () => {
2225
2454
  remeasureRaf = null;
2455
+ const covered = remeasureWindow();
2456
+ rafAttempts = rafAttempts + 1;
2457
+ if (!covered && rafAttempts < REMEASURE_MAX_FRAMES && !remeasureDisposed && typeof requestAnimationFrame === 'function') {
2458
+ remeasureRaf = requestAnimationFrame(rafPass);
2459
+ return;
2460
+ }
2461
+ // D-19: keep a user who scrolled to the END at the end. When rows at/above the viewport
2462
+ // measure taller than their estimate, virtual-core compensates with a scroll adjustment — but
2463
+ // on React/Angular that adjustment is written BEFORE the framework commits the taller spacer,
2464
+ // so the browser clamps it to the OLD maximum and it is lost (measured: the view ended
2465
+ // 593–1025px above the bottom on React, 30px on Angular). This pass runs after the commit, so
2466
+ // the real maximum is known. Not when the row count changed: appending rows must not
2467
+ // auto-follow them.
2468
+ if (scrollEndPinned && gridScrollEl && !remeasureDisposed && windowSource().length === scrollEndPinnedCount) {
2469
+ const maxTop = gridScrollEl.scrollHeight - gridScrollEl.clientHeight;
2470
+ if (maxTop - gridScrollEl.scrollTop > 1) {
2471
+ gridScrollEl.scrollTop = maxTop;
2472
+ scrollEndPinnedTop = gridScrollEl.scrollTop;
2473
+ }
2474
+ }
2226
2475
  remeasurePending = false;
2227
- remeasureWindow();
2228
2476
  };
2229
2477
  if (typeof queueMicrotask !== 'undefined') {
2230
2478
  ranMicro = true;
@@ -2258,9 +2506,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2258
2506
  // unioned into the windowed slice when it scrolls off-window and its height is subtracted from
2259
2507
  // the appropriate spacer so the total stays exactly getTotalSize() (the 51-01-proven mechanism).
2260
2508
  // This is the data-table half of the D-05 windowing.rzts pin-extension hook (listbox provides none).
2509
+ //
2510
+ // B-06 widens this from "the editing row" to "the row that must not recycle out of the
2511
+ // window", which now includes the ACTIVE row in grid mode. The active cell is the grid's ONLY
2512
+ // `tabindex="0"` — `cellTabindex()` returns 0 for it and -1 for every other cell — so once a
2513
+ // wheel-scroll carried it out of the window the grid had NO tab stop at all: Tab could not
2514
+ // re-enter it, and the roving model still pointed at a cell with no DOM node, so
2515
+ // `resolveCellEl` returned null and a subsequent arrow key moved state with nothing to focus.
2516
+ // The column axis has forced the active-cell COLUMN since D-10 (`forcedColumns`); this is the
2517
+ // row-axis twin of that decision, and it costs at most one extra rendered row, in flow, with
2518
+ // its height subtracted from the spacer by the same 51-01 mechanism the editing pin uses.
2519
+ //
2520
+ // `$props.interactionMode` is read directly rather than through `isGrid()` (gridFocusNav.rzts):
2521
+ // that partial is imported AFTER this one, and this file's header documents the
2522
+ // forward-cross-partial-reference TDZ on React that the 87-02 lesson established. A prop read
2523
+ // has no such hazard.
2261
2524
  function pinnedEditIndex() {
2262
2525
  if (editingRow() >= 0) return editingRow();
2263
2526
  if (editingRowIndex() != null) return editingRowIndex();
2527
+ if (local.interactionMode === 'grid' && !activeIsHeader() && activeRow() >= 0) return activeRow();
2264
2528
  return -1;
2265
2529
  }
2266
2530
  // pinnedMeasurement(pin): the virtual-core measurement { index, start, size, end, key } for the
@@ -2287,8 +2551,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2287
2551
  // cover unmount — virtualizer/gridRoot are assigned in $onMount and never nulled — so this
2288
2552
  // is the only thing standing between a pass queued pre-unmount and refineRowEstimate()'s
2289
2553
  // setOptions/scrollTop/$data.windowVer writes against a torn-down instance.
2290
- if (remeasureDisposed) return;
2291
- if (!virtualizer || !gridRoot) return;
2554
+ if (remeasureDisposed) return true;
2555
+ if (!virtualizer || !gridRoot) return true;
2292
2556
  // Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
2293
2557
  // exclusively during scrollToIndex / scrollToOffset (the D-12 scroll-then-focus seam) and
2294
2558
  // null for ordinary user/scrollTop-driven scrolling (verified virtual-core@3.17.1: set in
@@ -2296,9 +2560,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2296
2560
  // resizeItem nudge the offset and starve the scroll target (the Solid off-window focus
2297
2561
  // regression); the next settled onChange re-measures the stable window. Manual-scroll
2298
2562
  // recycling (the CR-01 case) has scrollState === null, so it measures normally.
2299
- if (virtualizer.scrollState) return;
2563
+ if (virtualizer.scrollState) return true;
2300
2564
  const trs = gridRoot.querySelectorAll('tbody.rdt-tbody > tr[data-index]');
2301
- for (const tr of trs as any) virtualizer.measureElement(tr);
2565
+ const rendered = new Set();
2566
+ for (const tr of trs as any) {
2567
+ virtualizer.measureElement(tr);
2568
+ rendered.add(tr.getAttribute('data-index'));
2569
+ }
2570
+ // N-05: did the committed rows cover the virtualizer's CURRENT window? False while the
2571
+ // framework has not yet committed the recycled set (see scheduleRemeasure).
2572
+ let covered = true;
2573
+ const items = virtualizer.getVirtualItems();
2574
+ for (let i = 0; i < items.length; i++) {
2575
+ if (!rendered.has(String(items[i].index))) {
2576
+ covered = false;
2577
+ break;
2578
+ }
2579
+ }
2302
2580
  // D-15 (Phase 87 87-07): trigger the shared engine's post-measurement fold + hysteresis
2303
2581
  // re-feed via the afterRowRemeasure mutable-let hook (DataTable.rozie's own script,
2304
2582
  // assigned to windowing.rzts's refineRowEstimate()) — NEVER a direct call from this file
@@ -2307,6 +2585,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2307
2585
  // documents for columnVirtualizerOptions()). Inert (no-op) until the host assigns it; the
2308
2586
  // off/pre-mount path never reaches this line since virtualizer is null there.
2309
2587
  if (afterRowRemeasure) afterRowRemeasure();
2588
+ return covered;
2310
2589
  }
2311
2590
 
2312
2591
  // D-04: this shell exports ONLY the impure, data-table-specific host pieces. The pure windowing
@@ -2315,6 +2594,144 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2315
2594
  // `@rozie-ui/headless-core/windowing.rzts` via bare specifier — the P0-proven cross-package inline
2316
2595
  // path that DISSOLVES the partial into the leaf (a re-export-from THROUGH this shell would survive as
2317
2596
  // a runtime import, not inline — verified). The math closes over these host symbols by convention.
2597
+ // ── Lazy rows (quick 260925-dtl part 2, oinbox dogfooding) ─────────────────────────────────────
2598
+ // `virtual` + `manual` + `rowCount`: the row space is rowCount long and `data` may be SPARSE —
2599
+ // an `undefined`/`null` entry (or anything past data.length) is a row not loaded yet. Such holes
2600
+ // are fed to table-core as placeholder sentinels (one cached object per index, so a re-feed does
2601
+ // not churn them) that render as placeholder rows and can't be selected, expanded, edited or
2602
+ // activated. ONLY the table-core feed is padded: every write-back derives from currentData(), the
2603
+ // consumer's own array, so a sentinel can never leak into `data`.
2604
+ const LAZY_PLACEHOLDER_KEY = '__rdtLazyPlaceholder';
2605
+ // LAZY_PLACEHOLDER_ROW_ID_PREFIX: the table-core row-id table.rowIdOption() (DataTable.rozie)
2606
+ // assigns to a placeholder ('__rdt_ph_' + index — no consumer row to ask, so it keys by
2607
+ // position). Shared here (not re-literaled) because migratePlaceholderSizeCache() below and
2608
+ // rowIdOption() must agree byte-for-byte on the prefix or the migration's key scan silently
2609
+ // finds nothing.
2610
+ const LAZY_PLACEHOLDER_ROW_ID_PREFIX = '__rdt_ph_';
2611
+ let lazyPlaceholders: Record<number, any> = {};
2612
+ function lazyRowsActive() {
2613
+ return rowsWindowed() && !!local.manual && local.rowCount != null && local.rowCount > 0;
2614
+ }
2615
+ function isLazyPlaceholder(orig: any): boolean {
2616
+ return !!orig && orig[LAZY_PLACEHOLDER_KEY] === true;
2617
+ }
2618
+ function rowIsLazyPlaceholder(row: any): boolean {
2619
+ return !!row && isLazyPlaceholder(row.original);
2620
+ }
2621
+ function feedData() {
2622
+ const d: any[] = currentData() || [];
2623
+ if (!lazyRowsActive()) return d;
2624
+ const n = Math.max(d.length, Number(local.rowCount));
2625
+ const out: any[] = new Array(n);
2626
+ for (let i = 0; i < n; i++) {
2627
+ const v = d[i];
2628
+ if (v != null) {
2629
+ out[i] = v;
2630
+ } else {
2631
+ if (!lazyPlaceholders[i]) lazyPlaceholders[i] = {
2632
+ [LAZY_PLACEHOLDER_KEY]: true,
2633
+ index: i
2634
+ };
2635
+ out[i] = lazyPlaceholders[i];
2636
+ }
2637
+ }
2638
+ return out;
2639
+ }
2640
+
2641
+ // migratePlaceholderSizeCache() (quick 260926, VR probe on data-table-lazy [react]): a filled
2642
+ // placeholder's row id changes ('__rdt_ph_' + i -> the real getRowId id), and virtual-core's
2643
+ // getItemKey re-keys by it — but virtual-core's itemSizeCache (dist/esm/index.js@3.17.1) is a
2644
+ // bare Map keyed by the OLD key, so the new key has no entry. The next getMeasurements() recompute
2645
+ // then estimates that row at $props.estimateRowHeight instead of its already-known measured size;
2646
+ // the CR-01 sweep re-measures it moments later and each delta fires one applyScrollAdjustment,
2647
+ // walking the window's anchor off by their sum (measured: 9 rows x -4px = -36px, row 5000 -> 4999
2648
+ // on React — the other 5 targets happened to keep reading the stale pre-refeed measurements array
2649
+ // and never saw the estimate at all). Fix: carry the cached size across the rekey, keyed off
2650
+ // table-core's OWN freshly-assigned id (table.getCoreRowModel().rows[i].id, NOT a locally
2651
+ // recomputed getRowId() call — must be byte-identical to what virtualItemKey(i) reads next) so
2652
+ // the cache is already correct before the caller's virtualizer.setOptions()/_willUpdate() re-feed.
2653
+ // Invalidating the memo needs BOTH strategies below, not just one: getMeasurements() is
2654
+ // memoized on [getMeasurementOptions(), itemSizeCacheVersion] on 3.15+ (confirmed from the
2655
+ // installed source; the same fact windowing.rzts's refineRowEstimate() comment documents) —
2656
+ // but our peer range is `^3`, and versions <=3.14 have NO itemSizeCacheVersion at all and key
2657
+ // the SAME memo on `this.itemSizeCache` (Map IDENTITY) instead — verified against the 3.14.0
2658
+ // tarball, whose own resizeItem() invalidates by reassigning `this.itemSizeCache = new Map(...)`,
2659
+ // never by bumping a version counter. Mutating the Map in place would be silently invisible to
2660
+ // that memo on <=3.14 (a stray `itemSizeCacheVersion++` there just creates a NaN-valued own
2661
+ // property nobody reads). So this reassigns the Map (a public field on every 3.x we support —
2662
+ // harmless on 3.15+, whose memo body re-reads `this.itemSizeCache` fresh) AND bumps the version
2663
+ // counter only where it actually exists. Scans the CACHE's own placeholder-prefixed keys
2664
+ // (bounded by rows actually measured so far), never rowCount (unbounded — 10,000+ here).
2665
+ function migratePlaceholderSizeCache(): void {
2666
+ if (!virtualizer || !virtualizer.itemSizeCache || !table) return;
2667
+ const cache = virtualizer.itemSizeCache;
2668
+ const coreRows = table.getCoreRowModel().rows;
2669
+ let migrated = false;
2670
+ for (const oldKey of Array.from(cache.keys()) as any) {
2671
+ if (typeof oldKey !== 'string' || oldKey.indexOf(LAZY_PLACEHOLDER_ROW_ID_PREFIX) !== 0) continue;
2672
+ const i = Number(oldKey.slice(LAZY_PLACEHOLDER_ROW_ID_PREFIX.length));
2673
+ const row = coreRows[i];
2674
+ if (!row || rowIsLazyPlaceholder(row) || row.id === oldKey) continue;
2675
+ cache.set(row.id, cache.get(oldKey));
2676
+ cache.delete(oldKey);
2677
+ migrated = true;
2678
+ }
2679
+ if (!migrated) return;
2680
+ virtualizer.itemSizeCache = new Map(cache);
2681
+ if (typeof virtualizer.itemSizeCacheVersion === 'number') virtualizer.itemSizeCacheVersion++;
2682
+ }
2683
+
2684
+ // `visible-range-change { start, end }` — the RENDERED row window (overscan included), `end`
2685
+ // exclusive, over the full row space. Driven by the window-version bump every virtualizer change
2686
+ // routes through, deduped here so a scroll that doesn't move the window emits nothing.
2687
+ let lastRangeStart = -1;
2688
+ let lastRangeEnd = -1;
2689
+ function emitVisibleRangeIfChanged() {
2690
+ if (!rowsWindowed() || !virtualizer) return;
2691
+ const items = virtualizer.getVirtualItems();
2692
+ if (!items.length) return;
2693
+ const start = items[0].index;
2694
+ const end = items[items.length - 1].index + 1;
2695
+ if (start === lastRangeStart && end === lastRangeEnd) return;
2696
+ lastRangeStart = start;
2697
+ lastRangeEnd = end;
2698
+ _props.onVisibleRangeChange?.({
2699
+ start,
2700
+ end
2701
+ });
2702
+ }
2703
+
2704
+ // scrollToRow(index, options?) — quick 260927-a2r (oinbox dogfooding): plain scroll-into-view
2705
+ // for a windowed row, forwarding TanStack virtual-core's own ScrollToOptions ({ align?,
2706
+ // behavior? }) UNCHANGED to virtualizer.scrollToIndex — the SAME call focusActiveCell's D-12
2707
+ // off-window scroll-then-focus path (gridFocusNav.rzts) already drives, exposed here as a
2708
+ // standalone public verb rather than invented plumbing. INDEPENDENT of grid-mode focus
2709
+ // (feedback_no_false_equivalent_options / the todo's own open question): never reads or
2710
+ // writes $data.activeRow/activeColIndex, never emits activecell-change, and is callable in
2711
+ // EITHER interactionMode ('table' or 'grid') — a scroll, not a focus move. No-op when rows
2712
+ // are not windowed or the row virtualizer has not been constructed yet — mirrors the
2713
+ // isGrid()-gated no-op precedent on focusCell/clearActiveCell (gridActiveCellVerbs.rzts) for
2714
+ // the identical reason: the capability this verb drives isn't active. `index` is coerced
2715
+ // (T-260927A2R-01) — never range-clamped here: virtual-core's own scrollToIndex already
2716
+ // clamps to [0, options.count-1] internally (verified in @tanstack/virtual-core source).
2717
+ function scrollToRow(index: any, options?: {
2718
+ align?: 'start' | 'center' | 'end' | 'auto';
2719
+ behavior?: 'auto' | 'smooth' | 'instant';
2720
+ }): void {
2721
+ if (!rowsWindowed() || !virtualizer) return;
2722
+ virtualizer.scrollToIndex(Math.trunc(Number(index)) || 0, options || undefined);
2723
+ }
2724
+
2725
+ // getScrollElement() — quick 260927-a2r: returns the DOM node the row AND column
2726
+ // virtualizer(s) observe/scroll (the `.rdt-scroll` div captured post-mount in
2727
+ // DataTable.rozie's own $onMount, D-03) — the supported replacement for a consumer reaching
2728
+ // into the internal `.rdt-scroll` class selector. Returns null when nothing is windowed:
2729
+ // gridScrollEl is only ever assigned inside the isWindowed() guard, so no extra gate is
2730
+ // needed here beyond that natural null.
2731
+ function getScrollElement(): any {
2732
+ return gridScrollEl;
2733
+ }
2734
+
2318
2735
  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
2319
2736
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
2320
2737
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
@@ -2691,6 +3108,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2691
3108
  // makes the eventual render land strictly AFTER this fold + re-feed has fully landed, on
2692
3109
  // every target (the 87-10 fix for the confirmed Solid-specific rendering gap).
2693
3110
  bumpWindowVer();
3111
+ // D-19 (quick 260923-rrr): a re-feed changes every unmeasured row's size, but — unlike a
3112
+ // measurement — it does not go through virtual-core's onChange, so the host's post-commit
3113
+ // pass (which restores an at-the-end view the growth moved off) never ran for it. Schedule it.
3114
+ scheduleRemeasure();
2694
3115
  }
2695
3116
 
2696
3117
  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
@@ -3090,6 +3511,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3090
3511
  }
3091
3512
  return '';
3092
3513
  }
3514
+
3515
+ // The table-core `getRowId` option (quick 260925-rew). Without it table-core keys
3516
+ // rowSelection / expanded by position, so a row inserted above a selected one moves the
3517
+ // selection onto another row. The prop is captured into a local so the null check narrows
3518
+ // inside the arrow (TS2721 on the Vue leaf otherwise), and String() because table-core
3519
+ // compares ids as strings.
3520
+ function rowIdOption() {
3521
+ const f = local.getRowId;
3522
+ // A lazy placeholder has no consumer row to ask — key it by its index (see feedData()).
3523
+ // Prefix shared with migratePlaceholderSizeCache() (virtualization.rzts) — see its own comment.
3524
+ return f ? (originalRow: any, index: any, parent: any) => isLazyPlaceholder(originalRow) ? LAZY_PLACEHOLDER_ROW_ID_PREFIX + originalRow.index : String(f(originalRow, index, parent)) : undefined;
3525
+ }
3526
+
3527
+ // table-core `enableRowSelection`: off for selectionMode 'none'; with lazy rows a placeholder
3528
+ // (a row not loaded yet) is never selectable, so select-all takes the loaded rows only.
3529
+ function rowSelectionOption() {
3530
+ if (local.selectionMode === 'none') return false;
3531
+ return lazyRowsActive() ? (row: any) => !rowIsLazyPlaceholder(row) : true;
3532
+ }
3093
3533
  // Push fresh options into table-core + re-pull the row model. Extracted so BOTH the
3094
3534
  // re-feed $watch (above) and the Lit data-change $onUpdate (below) call it.
3095
3535
  function reFeed() {
@@ -3102,10 +3542,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3102
3542
  // below (`maybeClearHistoryOnExternalSwap`), which runs on all six targets.
3103
3543
  table.setOptions((prev: any) => ({
3104
3544
  ...prev,
3105
- data: currentData(),
3545
+ data: feedData(),
3106
3546
  columns: tableColumns(),
3107
3547
  state: currentState(),
3108
- enableRowSelection: local.selectionMode !== 'none',
3548
+ enableRowSelection: rowSelectionOption(),
3109
3549
  enableMultiRowSelection: local.selectionMode === 'multiple',
3110
3550
  // Re-pass the server-side page-count sources (#2) so a RUNTIME rowCount/pageCount change
3111
3551
  // takes effect: setOptions REPLACES via `...prev`, which holds the value captured at
@@ -3118,7 +3558,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3118
3558
  // onExpandedChange callback must re-capture fresh currentState each cycle, F6).
3119
3559
  getExpandedRowModel: getExpandedRowModel(),
3120
3560
  getSubRows: (local.getSubRows || undefined) as any,
3121
- getRowCanExpand: local.expandable === true && local.getSubRows == null ? () => true : undefined,
3561
+ // Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
3562
+ // other row-model fns.
3563
+ getRowId: rowIdOption(),
3564
+ getRowCanExpand: local.expandable === true && local.getSubRows == null ? (row: any) => !rowIsLazyPlaceholder(row) : undefined,
3122
3565
  onExpandedChange: onExpandedChangeCb,
3123
3566
  // Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
3124
3567
  // POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
@@ -3444,9 +3887,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3444
3887
  // read header.getSize() — the group-aware size (see headerWidth's own comment). pinStyle still
3445
3888
  // keys off the plain column id (pinning is a leaf-column-only concern; a grouped header never
3446
3889
  // pins as a unit in this codebase's model).
3447
- function thStyle(header: any) {
3890
+ // A-03: `widthPx` is the in-window leaf-width sum windowedHeadersFor() computed alongside the
3891
+ // clamped colspan. When it is present it WINS over headerWidth(), which reports
3892
+ // Header.getSize() — every descendant leaf, in-window or not — and so over-declares a straddling
3893
+ // group by exactly the leaves the colspan already excluded. Null on the unwindowed path and from
3894
+ // the non-windowed header body, which both keep headerWidth() verbatim.
3895
+ // `widthPx` carries an EXPLICIT `= null` default for the same reason setRangeFocus's anchor
3896
+ // params do: the emitted signature types a declared param as required, and the non-windowed
3897
+ // header body calls `thStyle(header)` with one argument — TS2554 on the strict leaf typecheck.
3898
+ function thStyle(header: any, widthPx = null) {
3448
3899
  let s = '';
3449
- const w = headerWidth(header);
3900
+ const w = widthPx != null && widthPx > 0 ? widthPx + 'px' : headerWidth(header);
3450
3901
  if (w) s += 'width:' + w + ';';
3451
3902
  s += pinStyle(header && header.column ? header.column.id : null, 2);
3452
3903
  return s;
@@ -3830,7 +4281,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3830
4281
  if (selectAllBox) selectAllBox.indeterminate = isSomeRowsSelected() && !isAllRowsSelected();
3831
4282
  }
3832
4283
 
3833
- // The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
4284
+ // C-09's value-equality guard. Imported HERE, in DataTable.rozie's OWN script, because that
4285
+ // is where it is USED: a `.rzts` partial's import of a symbol the partial itself never
4286
+ // references is dropped as unused, and the host script then reaches a free identifier. That
4287
+ // failed at RUNTIME, not at compile time — `ReferenceError: columnSpecsEquivalent is not
4288
+ // defined` on the first paste — which is the only way this class of mistake shows up.
3834
4289
 
3835
4290
  // Imperative handle (consumer-callable). Each verb is a PRE-DECLARED top-level
3836
4291
  // `const` (the canonical $expose contract — `$expose({ name })` references a
@@ -3955,6 +4410,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3955
4410
  return (hg && hg.headers ? hg.headers : []).indexOf(header);
3956
4411
  }
3957
4412
 
4413
+ // E-05 — a header's ABSOLUTE LEAF-column start index, which is NOT the same as its position
4414
+ // in the group's header array: a grouped header occupies ONE array slot but spans colSpan leaf
4415
+ // columns, so positions and leaf indices diverge the moment a header group exists. Walk the
4416
+ // group accumulating colSpan to the target. Window-independent by construction (a header's
4417
+ // leaf start is a full-model property), so both the windowed grid header and the plain
4418
+ // non-grid header can share it.
4419
+ function headerLeafStart(hg: any, header: any) {
4420
+ const list = hg && hg.headers ? hg.headers : [];
4421
+ let leaf = 0;
4422
+ for (let i = 0; i < list.length; i++) {
4423
+ if (list[i] === header) return leaf;
4424
+ const h = list[i];
4425
+ leaf = leaf + (h && h.colSpan > 1 ? h.colSpan : 1);
4426
+ }
4427
+ return -1;
4428
+ }
4429
+
3958
4430
  // ── C1 (phase 63 wave-6) absolute-index bridge ─────────────────────────────────────────
3959
4431
  // The PUBLIC active-cell rowIndex (focusCell/getActiveCell/activecell-change) is the ABSOLUTE
3960
4432
  // display-order position in getPrePaginationRowModel().rows (filter+sort+expand applied, BEFORE
@@ -4041,7 +4513,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4041
4513
  // post-mount root. Uniform on all six, shadow-safe (the query runs from inside the
4042
4514
  // component's own scope). rowKey is the literal '__header' or a String(integer index) and
4043
4515
  // colIndex is an integer — NO consumer string is interpolated into the selector (T-49-01).
4044
- function resolveCellEl(rowKey: any, colIndex: any, level = null) {
4516
+ // `level` is ANNOTATED, not just defaulted: an untyped `level = null` infers the param type as
4517
+ // `null`, so every header call site passing a real level (`lvl`, activeHeaderLevel) was a
4518
+ // TS2345 under the strict leaf typecheck (quick 260922-mkb — B-03 added one more such call).
4519
+ function resolveCellEl(rowKey: any, colIndex: any, level: number | null = null) {
4045
4520
  if (!gridRoot) return null;
4046
4521
  // B12: a grouped multi-level header has MULTIPLE cells sharing data-row="__header" at the
4047
4522
  // same data-col-index across levels (parent vs leaf). Disambiguate header lookups by the
@@ -4101,10 +4576,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4101
4576
  // both are out, and the ONE existing poll below is reused unchanged — it already resolves the
4102
4577
  // target cell by BOTH (row, col) via resolveCellEl(String(r), c), so a cell that becomes
4103
4578
  // reachable via the column axis needs no new poll logic (per the plan's own key_links note).
4579
+ // B-03: the COLUMN axis applies to HEADER cells too. This guard used to be `!header &&
4580
+ // (rowOut || colOut)`, so a header-active move to an off-window column never scrolled it in:
4581
+ // `resolveCellEl` returned null, focus froze on the old `<th>` while the state and the ring
4582
+ // ran away to a column with no DOM node, and `cellTabindex` then reported 0 for an unrendered
4583
+ // cell — leaving the grid with ZERO `tabindex="0"` cells and no way back in with Tab. Header
4584
+ // rows genuinely have no ROW-axis window (headers are not row-virtualized), so the row half
4585
+ // stays body-only; only the column half widens.
4104
4586
  const rowOut = rowsWindowed() && virtualizer && rowIsOutsideWindow(r);
4105
4587
  const colOut = colsWindowed() && colVirtualizer && colIsOutsideWindow(c);
4106
- if (!header && (rowOut || colOut)) {
4107
- if (rowOut) virtualizer.scrollToIndex(r, {
4588
+ const rowScroll = !header && rowOut;
4589
+ if (rowScroll || colOut) {
4590
+ if (rowScroll) virtualizer.scrollToIndex(r, {
4108
4591
  align: 'center'
4109
4592
  });
4110
4593
  if (colOut) colVirtualizer.scrollToIndex(c, {
@@ -4128,7 +4611,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4128
4611
  // A newer focus intent superseded this poll — abort WITHOUT focusing (the user has since
4129
4612
  // navigated / clicked elsewhere; re-focusing this off-window target would yank focus back).
4130
4613
  if (focusIntentEpoch !== myEpoch) return;
4131
- const el = resolveCellEl(String(r), c);
4614
+ // B-03: resolve with the SAME key the synchronous path below uses, so the poll can
4615
+ // find a header cell. `String(r)` alone addresses body rows only.
4616
+ const el = resolveCellEl(header ? '__header' : String(r), c, header ? lvl : null);
4132
4617
  if (el) {
4133
4618
  el.focus();
4134
4619
  return;
@@ -4161,8 +4646,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4161
4646
  // HTMLElement ARIA-reflected property (`Element.ariaRowCount: string`), so a same-named method
4162
4647
  // becomes a class field that shadows it on Lit → TS2416 cascades to EVERY @property decorator
4163
4648
  // (the `valueOf`/`nodeType` inherited-DOM-member collision class, authoring playbook §6).
4649
+ // A-04: under `manual` the filtered row model is NOT the total — table-core holds only the
4650
+ // page the consumer handed it, so getFilteredRowModel().rows.length is the PAGE SIZE. The
4651
+ // server-side total lives in the props the consumer passes for exactly this purpose
4652
+ // (`rowCount`, else `pageCount`), which is also table-core's own precedence for getPageCount()
4653
+ // (explicit pageCount wins, else ⌈rowCount / pageSize⌉). Taking them here keeps aria-rowcount
4654
+ // consistent with bodyAriaRowIndex, which already offsets by pageIndex * pageSize and would
4655
+ // otherwise hand a screen reader an index LARGER than the advertised count on any page past
4656
+ // the first. `pageCount * pageSize` is an upper bound (the last page is usually partial) —
4657
+ // an over-count is the safe direction: an aria-rowindex can never exceed it.
4658
+ // With `manual` and NEITHER count set, table-core pins getCanNextPage() to false, so pageIndex
4659
+ // stays 0, the offset is 0 and page-slice-as-total is self-consistent; we leave it alone.
4164
4660
  function totalRowCount() {
4165
4661
  if (!table) return (rows() || []).length;
4662
+ if (local.manual === true) {
4663
+ if (local.rowCount != null) return local.rowCount;
4664
+ if (local.pageCount != null) return local.pageCount * pageSize();
4665
+ }
4166
4666
  const fm = table.getFilteredRowModel();
4167
4667
  return fm && fm.rows ? fm.rows.length : (rows() || []).length;
4168
4668
  }
@@ -4201,6 +4701,27 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4201
4701
  return headerRowCount() + rowIndexOf(row) + ariaPageOffset() + 1;
4202
4702
  }
4203
4703
 
4704
+ // ── E-05: the COLUMN-axis twin of gridAriaRowCount / bodyAriaRowIndex ────────────────
4705
+ // WAI-ARIA: when aria-colcount is set, every cell must carry an aria-colindex and the count
4706
+ // must equal the largest index any cell declares. The row axis has had this since #13; the
4707
+ // column axis had NOTHING repo-wide (0 occurrences of aria-colcount/aria-colindex), which
4708
+ // matters most under column windowing — the rendered <td>s are a WINDOW, so without an
4709
+ // explicit index a screen reader counts the window and announces "column 3 of 6" while the
4710
+ // user sits in absolute column 41.
4711
+ //
4712
+ // The index space is `visibleCellsFor(row)` / the header group's leaf headers — the same one
4713
+ // `colIndexOf` and `data-col-index` already use, INCLUDING the auto-injected select/expander
4714
+ // chrome columns (they are real columns in that list). `visibleColCount()` counts the same
4715
+ // list, so count and indices are mutually consistent by construction.
4716
+ //
4717
+ // NB named gridAriaColCount, NOT ariaColCount: `Element.ariaColCount` / `.ariaColIndex` are
4718
+ // inherited ARIA-reflected properties, so a same-named method becomes a Lit class field that
4719
+ // shadows them → TS2416 cascading to every @property decorator. Exactly the rename discipline
4720
+ // totalRowCount / gridAriaRowCount / bodyAriaRowIndex already document above.
4721
+ function gridAriaColCount() {
4722
+ return visibleColCount();
4723
+ }
4724
+
4204
4725
  // Column count = the visible cell list length (uniform header+body in a flat grid). Reads
4205
4726
  // $data.rows (reactive) so it is fine-grained-correct on Solid/Lit; falls back to the
4206
4727
  // header leaf count when there are no body rows.
@@ -4458,6 +4979,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4458
4979
  // Enter/F2 → enter interaction mode: focus the active cell's FIRST interactive control
4459
4980
  // (D-07 — uniform for header sort buttons and body controls; Enter does NOT sort directly).
4460
4981
  // No-op (stay in navigation mode) if the cell has no focusable control.
4982
+ // True when the active cell holds its own focusable controls (a link, a button, a drop-in
4983
+ // widget). Enter then ENTERS them (enterControl) rather than activating the row (row-activate).
4984
+ function activeCellHasControls() {
4985
+ return focusables(currentCellEl()).length > 0;
4986
+ }
4461
4987
  function enterControl() {
4462
4988
  const cellEl = currentCellEl();
4463
4989
  const list = focusables(cellEl);
@@ -4550,7 +5076,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4550
5076
  // nextRow/nextCol locals and FALLS THROUGH to the shared focus seam (like Ctrl+Home/End). ──
4551
5077
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && !activeIsHeader() && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
4552
5078
  e.preventDefault();
4553
- if (key === 'ArrowUp') extendRange(-activeRow(), 0);else if (key === 'ArrowDown') extendRange(bodyRowCount() - 1 - activeRow(), 0);else if (key === 'ArrowLeft') extendRange(0, -activeColIndex());else extendRange(0, visibleColCount() - 1 - activeColIndex());
5079
+ // B-04: measured from the range's FOCUS corner, which is what extendRange moves — not
5080
+ // from the active cell, which a drag-select or Ctrl+A leaves somewhere else entirely.
5081
+ if (key === 'ArrowUp') extendRange(-rangeFocusRow(), 0);else if (key === 'ArrowDown') extendRange(bodyRowCount() - 1 - rangeFocusRow(), 0);else if (key === 'ArrowLeft') extendRange(0, -rangeFocusCol());else extendRange(0, visibleColCount() - 1 - rangeFocusCol());
4554
5082
  return;
4555
5083
  } else if ((e.ctrlKey || e.metaKey) && !activeIsHeader() && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
4556
5084
  e.preventDefault();
@@ -4648,11 +5176,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4648
5176
  // gated by clipboardActiveAllowed() (== !activeIsHeader) so a header-active Ctrl+C/Ctrl+V
4649
5177
  // falls through to NATIVE behavior — never preventDefault'd, never a silent body mutation
4650
5178
  // (copyRange/pasteRange also self-guard; the verb guard is what plan 63-09's Cut reuses). ──
4651
- else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed()) {
5179
+ // B-07: `clipboardWriteAvailable()` / `clipboardReadAvailable()` are part of the BRANCH
5180
+ // CONDITION, not a guard inside the verb. preventDefault ran first and copyRange/pasteRange
5181
+ // then returned early when `navigator.clipboard` was absent — an insecure `http://` origin, a
5182
+ // permissions policy, an older embedded webview — so the browser's own copy/paste was
5183
+ // suppressed and nothing replaced it: Ctrl+C did nothing whatsoever. Falling through leaves
5184
+ // the native default in charge, which is the only correct behaviour when we cannot act.
5185
+ else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed() && clipboardWriteAvailable()) {
4652
5186
  e.preventDefault();
4653
5187
  copyRange();
4654
5188
  return;
4655
- } else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed()) {
5189
+ } else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed() && clipboardReadAvailable()) {
4656
5190
  e.preventDefault();
4657
5191
  pasteRange();
4658
5192
  return;
@@ -4662,7 +5196,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4662
5196
  // Ctrl+C/Ctrl+V (clipboardActiveAllowed) so a header-active Ctrl+X falls through to NATIVE cut
4663
5197
  // and never silently clears a body cell (cutRange also self-guards). Placed beside the C/V
4664
5198
  // shortcuts, BEFORE the printable-key edit-entry branch (which excludes ctrl/meta). ──
4665
- else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed()) {
5199
+ // B-07 applies to Cut too, and gating it on WRITE availability is deliberate: a cut that
5200
+ // cannot copy is data destruction with nothing on the clipboard to paste back.
5201
+ else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed() && clipboardWriteAvailable()) {
4666
5202
  e.preventDefault();
4667
5203
  cutRange();
4668
5204
  return;
@@ -4714,6 +5250,44 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4714
5250
  if (!activeIsHeader()) selectAllBody();
4715
5251
  return;
4716
5252
  }
5253
+ // ── B-15: the APG grid pattern's row/column selection keys, and Escape in NAVIGATION mode.
5254
+ // All three were simply absent. They are placed HERE, before every `key === ' '` branch
5255
+ // below, because the boolean in-place toggle matches a bare `' '` without excluding
5256
+ // modifiers — so a Shift+Space on a checkbox cell would have toggled the checkbox instead
5257
+ // of selecting the row, and Ctrl+Space likewise.
5258
+ //
5259
+ // Shift+Space selects the ROW (table-core's own rowSelection slice, through the row's
5260
+ // toggleSelected — the same funnel the per-row checkbox uses, so the change event and the
5261
+ // two-way model behave identically). A no-op when selectionMode is 'none', which is the
5262
+ // honest behaviour: there is no row selection to make.
5263
+ //
5264
+ // Ctrl+Space selects the COLUMN as a full-height CELL RANGE rather than inventing a third
5265
+ // selection concept — the same corners Ctrl+A and shift+arrow drive, so Ctrl+C / Ctrl+X /
5266
+ // Delete / the fill handle all act on it unchanged.
5267
+ //
5268
+ // Escape in navigation mode collapses the range. In-control mode already consumed Escape at
5269
+ // the top of this handler; in navigation mode it did nothing at all, leaving a user who had
5270
+ // shift-arrowed out a rectangle with no keyboard way to drop it short of an arrow key, which
5271
+ // also MOVES. Only preventDefault'd when there is a range to clear, so a consumer's own
5272
+ // Escape handling on a table with no selection is untouched.
5273
+ else if (key === ' ' && e.shiftKey && !e.ctrlKey && !e.metaKey && !activeIsHeader()) {
5274
+ e.preventDefault();
5275
+ if (local.selectionMode !== 'none') {
5276
+ const selRow = (rows() || [])[activeRow()];
5277
+ if (selRow && selRow.toggleSelected) selRow.toggleSelected(!rowIsSelected(selRow));
5278
+ }
5279
+ return;
5280
+ } else if (key === ' ' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !activeIsHeader()) {
5281
+ e.preventDefault();
5282
+ selectActiveColumn();
5283
+ return;
5284
+ } else if (key === 'Escape') {
5285
+ if (rangeActive || rangeAnchor() != null) {
5286
+ e.preventDefault();
5287
+ clearRange();
5288
+ }
5289
+ return;
5290
+ }
4717
5291
  // ── Full-row edit entry (phase 51 req-6 / D-06) — Shift+F2 on an editable active cell puts
4718
5292
  // EVERY editable cell in the active row into edit at once. Tested BEFORE the plain F2 branch
4719
5293
  // (a Shift+F2 must NOT fall through to single-cell F2). Shift+F2 was chosen for the lowest
@@ -4793,6 +5367,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4793
5367
  // rAF poll must not steal focus back after the user has already ArrowDown'd to another row.
4794
5368
  recoverGridFocus(String(grpRow), grpCol, null, true);
4795
5369
  return;
5370
+ }
5371
+ // ── row-activate (quick 260925-dtl): Enter on a NON-editable body cell with no controls of
5372
+ // its own opens the row (a list-style "open this item"). Reached only after every edit-entry
5373
+ // and group-toggle branch above has declined; a cell WITH controls keeps entering them.
5374
+ else if (key === 'Enter' && !activeIsHeader() && !activeCellHasControls()) {
5375
+ e.preventDefault();
5376
+ activateRowAt(activeRow(), 'keyboard');
5377
+ return;
4796
5378
  } else if (key === 'Enter' || key === 'F2') {
4797
5379
  e.preventDefault();
4798
5380
  enterControl();
@@ -4887,11 +5469,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4887
5469
  // focusin that follows a Shift+Click (rangeClickPending): @mousedown already set the range
4888
5470
  // BEFORE this focusin fires, and a focusin carries no reliable shiftKey, so the @mousedown
4889
5471
  // path owns the shift case and flags it here so the collapse is skipped.
5472
+ // B-11: `rangeClickPending` carries the CELL the Shift+Click landed on, not a bare boolean.
5473
+ // A boolean leaks: Shift+Click on the cell that ALREADY holds focus fires no focusin at all,
5474
+ // so nothing consumed the flag, and the next ordinary navigation click (or Tab) hit this
5475
+ // branch instead and skipped its collapse — leaving a stale rectangle painted while the
5476
+ // active cell moved away. Matching on the coordinates makes a leak self-healing: a focusin on
5477
+ // any OTHER cell falls through to clearRange() and clears the flag on the way past.
4890
5478
  if (rangeTransition) {
4891
5479
  rangeTransition = false;
4892
- } else if (rangeClickPending) {
4893
- rangeClickPending = false;
5480
+ } else if (rangeClickPending && rangeClickPending.isHeader === isHeader && rangeClickPending.r === movedRow && rangeClickPending.c === col) {
5481
+ rangeClickPending = null;
4894
5482
  } else {
5483
+ rangeClickPending = null;
4895
5484
  clearRange();
4896
5485
  }
4897
5486
  // The cell box (not an inner control) receiving focus = navigation mode.
@@ -4929,13 +5518,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4929
5518
  setActiveIsHeader(false);
4930
5519
  setActiveRow(row);
4931
5520
  setActiveColIndex(col);
4932
- rangeClickPending = true;
5521
+ // B-11: arm the skip for THIS cell only (see syncActiveFromEvent). Body cell → isHeader
5522
+ // false; the header path never reaches here (rowAttr === '__header' returned above).
5523
+ rangeClickPending = {
5524
+ isHeader: false,
5525
+ r: row,
5526
+ c: col
5527
+ };
4933
5528
  return;
4934
5529
  }
4935
- // §6 plain mousedown → begin a document-level drag-select anchored at this cell. The mousedown's
4936
- // native focusin commits the ACTIVE cell to (row,col); beginRangeDrag's first cross-cell
4937
- // pointermove paints the range via setRangeFocus (anchored at the active cell). A mousedown with
4938
- // no move collapses to a single active cell (no range).
5530
+ // B-16: a mousedown INSIDE the cell that is currently in edit belongs to the editor — it is
5531
+ // the user placing a caret or drag-selecting TEXT. Starting a cell drag-select from it paints
5532
+ // a grid range over the very cell being typed into, and (with `undoable`) a drag that leaves
5533
+ // the cell then lets Delete/Ctrl+X act on a rectangle the user was never selecting. The
5534
+ // editor owns the gesture; return before any document listener is attached.
5535
+ if (isEditing(row, col)) return;
5536
+ // §6 plain mousedown → begin a document-level drag-select anchored at this cell. The anchor is
5537
+ // passed EXPLICITLY into setRangeFocus by beginRangeDrag (B-01) rather than seeded from the
5538
+ // active cell, which is still the PRE-mousedown one at this point (native focus is a default
5539
+ // action that runs after this handler). A mousedown with no move collapses to a single active
5540
+ // cell (no range).
4939
5541
  beginRangeDrag(row, col);
4940
5542
  }
4941
5543
 
@@ -4992,7 +5594,52 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4992
5594
  // editor). A plain click on an EDITABLE body cell opens its editor via the SAME beginEdit funnel;
4993
5595
  // shift+click (range extend) and non-editable cells are unaffected. Same closest/parse/header-skip
4994
5596
  // /finite guards as onGridDblClick. isGrid()-gated so 'table' mode never runs it.
5597
+ // ── row-activate (quick 260925-dtl, oinbox dogfooding) ─────────────────────────────────────
5598
+ // `row-activate { row, index, trigger }`: `row` = the original data object, `index` = its
5599
+ // position in the rendered model ($data.rows — page-relative, or the full model when virtual).
5600
+ // Group-header rows never activate; detail rows carry no data-row so a click there never
5601
+ // resolves a row.
5602
+ function activateRowAt(index: any, trigger: any) {
5603
+ const row = (rows() || [])[index];
5604
+ if (!row || rowIsGrouped(row) || rowIsLazyPlaceholder(row)) return;
5605
+ _props.onRowActivate?.({
5606
+ row: row.original,
5607
+ index,
5608
+ trigger
5609
+ });
5610
+ }
5611
+
5612
+ // Controls a click must not activate THROUGH — the selection checkbox, the expander, links,
5613
+ // drop-in widgets and an open editor. Matched only INSIDE the clicked cell, so a host page that
5614
+ // happens to wrap the table in one of these does not silence every row.
5615
+ const ROW_ACTIVATE_CONTROLS = 'input, button, a[href], select, textarea, label, summary, [contenteditable=""], [contenteditable="true"], [role="button"], [role="checkbox"], [role="link"], [data-editing-cell]';
5616
+
5617
+ // Both interaction modes: a plain click on a body data cell, outside any control. Not a
5618
+ // modified click (shift/ctrl/meta are selection gestures), not the end of a drag-select, and
5619
+ // not a singleClickEdit gesture on an editable grid cell (that click opens the editor instead).
5620
+ function maybeActivateRowFromClick(e: any) {
5621
+ if (!e || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
5622
+ if (rangeDragMoved) return;
5623
+ const tgt = e.target;
5624
+ if (!tgt || !tgt.closest) return;
5625
+ const cellEl = tgt.closest('[data-row]');
5626
+ if (!cellEl) return;
5627
+ const control = tgt.closest(ROW_ACTIVATE_CONTROLS);
5628
+ if (control && cellEl.contains(control)) return;
5629
+ const rowAttr = cellEl.getAttribute('data-row');
5630
+ if (rowAttr == null || rowAttr === '__header') return;
5631
+ const index = parseInt(rowAttr, 10);
5632
+ if (!Number.isFinite(index)) return;
5633
+ if (isGrid() && local.singleClickEdit) {
5634
+ const colAttr = cellEl.getAttribute('data-col-index');
5635
+ const col = colAttr == null ? NaN : parseInt(colAttr, 10);
5636
+ const colId = Number.isFinite(col) ? columnIdAt(index, col) : null;
5637
+ if (colId != null && columnEditable(colId)) return;
5638
+ }
5639
+ activateRowAt(index, 'click');
5640
+ }
4995
5641
  function onGridClick(e: any) {
5642
+ maybeActivateRowFromClick(e);
4996
5643
  if (!isGrid() || !e) return;
4997
5644
  if (!local.singleClickEdit) return;
4998
5645
  if (e.shiftKey) return;
@@ -5188,12 +5835,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5188
5835
  // UNCHANGED (span = the header's own colSpan, or 1) when columns are not windowed — the
5189
5836
  // off-path the template's `:colspan="wh.span > 1 ? wh.span : null"` binding preserves
5190
5837
  // byte-identically (a bare colspan="1" would change the emitted markup on the unwindowed path).
5838
+ //
5839
+ // A-03: the emitted entry also carries the WIDTH of the in-window leaves, because the colspan
5840
+ // clamp above is only half the job. `thStyle` declared `Header.getSize()` — the sum of ALL
5841
+ // descendant leaves — against a colspan clamped to the leaves actually rendered. Under
5842
+ // `table-layout: fixed` a first-row spanning cell's declared width is divided across the
5843
+ // columns it spans, and only the first row establishes widths, so a group straddling the window
5844
+ // edge inflated every column it still rendered: measured on "Group A" (cols 10-14 @150px,
5845
+ // getSize() 750), each rendered group column went 150 -> 187.5 -> 250 -> 375 -> 750px as the
5846
+ // group scrolled out, and scrollWidth with it, 9000 -> 9600. Identical on all five light-DOM
5847
+ // targets. This is the SAME getSize()-vs-colspan mismatch the earlier Column.getSize() ->
5848
+ // Header.getSize() fix cured for the fully-in-window case, inverted: that one was a 5x
5849
+ // COLLAPSE, this is a 5x EXPANSION for the partial case. Summing the same leaves the clamp
5850
+ // counted keeps the declared width and the colspan describing the same set by construction.
5191
5851
  function windowedHeadersFor(hg: any, hgLevel: any) {
5192
5852
  const headers = hg && hg.headers || [];
5193
5853
  if (!colsWindowed()) {
5194
5854
  return headers.map((h: any) => ({
5195
5855
  header: h,
5196
- span: h && h.colSpan > 1 ? h.colSpan : 1
5856
+ span: h && h.colSpan > 1 ? h.colSpan : 1,
5857
+ width: null
5197
5858
  }));
5198
5859
  }
5199
5860
  const idx = windowedColIndices();
@@ -5208,10 +5869,15 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5208
5869
  const end = cursor + leafCount;
5209
5870
  cursor = end;
5210
5871
  let span = 0;
5211
- for (let c = start; c < end; c++) if (idxSet[c]) span = span + 1;
5872
+ let width = 0;
5873
+ for (let c = start; c < end; c++) if (idxSet[c]) {
5874
+ span = span + 1;
5875
+ width = width + columnSize(c);
5876
+ }
5212
5877
  if (span > 0) out.push({
5213
5878
  header: h,
5214
- span
5879
+ span,
5880
+ width
5215
5881
  });
5216
5882
  }
5217
5883
  return out;
@@ -5224,6 +5890,36 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5224
5890
  function windowedColSpan() {
5225
5891
  return colsWindowed() ? windowedColIndices().length + 2 : visibleColCount();
5226
5892
  }
5893
+ // ── A-02: a column RESIZE must invalidate the column virtualizer's measurement cache ──────
5894
+ // `setOptions` is not enough, and this is a property of virtual-core, not of how we call it.
5895
+ // `getMeasurements()` is memoized on `[getMeasurementOptions(), itemSizeCacheVersion]`, and
5896
+ // `getMeasurementOptions()` is itself memoized on count / paddingStart / scrollMargin /
5897
+ // getItemKey / enabled / lanes / laneAssignmentMode — `estimateSize` is NOT among them. The
5898
+ // column axis feeds its widths ENTIRELY through `estimateSize: (i) => columnSize(i)` (it never
5899
+ // calls `measureElement`), so after a resize the memo returns the array built from the OLD
5900
+ // widths: measured on a 60x150px grid, dragging one column +1200px left `scrollWidth`
5901
+ // oscillating 9000 <-> 10200 as that column entered and left the window, and at scrollLeft 6000
5902
+ // the window started at index 37 where fresh geometry predicts 29. `measure()` is the only API
5903
+ // that bumps `itemSizeCacheVersion`, and it appeared ZERO times in this package or in
5904
+ // headless-core. Same defect for `resetColumnSizing()` and any programmatic `columnSizing` write.
5905
+ //
5906
+ // Guarded on a SIGNATURE rather than called unconditionally. `columnResizeMode` is 'onChange',
5907
+ // so `writeColumnSizing` fires on every pointermove of a drag; `measure()` clears the size cache
5908
+ // and calls `notify(false)`, which bumps `$data.windowVer` and therefore re-renders. Hashing the
5909
+ // widths (order-sensitive, so a pure A<->B swap that preserves the sum still trips it) makes the
5910
+ // steady state a ~N-integer-op no-op and guarantees the notify cannot feed itself: the second
5911
+ // pass sees an unchanged signature and returns.
5912
+ let lastColSizeSig = 0;
5913
+ function remeasureColumnSizes() {
5914
+ if (!colsWindowed() || !colVirtualizer || !colVirtualizer.measure) return;
5915
+ const n = columnCount();
5916
+ let sig = n;
5917
+ for (let i = 0; i < n; i++) sig = Math.imul(sig, 31) + columnSize(i) | 0;
5918
+ if (sig === lastColSizeSig) return;
5919
+ lastColSizeSig = sig;
5920
+ colVirtualizer.measure();
5921
+ }
5922
+
5227
5923
  // remeasureColumnWindow() (Phase 87 87-04): push a fresh column count/size into the SECOND
5228
5924
  // (horizontal) virtualizer on every row-model refresh — mirrors the row axis's
5229
5925
  // virtualizer.setOptions(virtualizerOptions()) + _willUpdate() re-feed in refreshRowModel, so a
@@ -5237,6 +5933,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5237
5933
  function remeasureColumnWindow() {
5238
5934
  if (!colsWindowed() || !colVirtualizer) return;
5239
5935
  colVirtualizer.setOptions(columnVirtualizerOptions());
5936
+ remeasureColumnSizes();
5240
5937
  colVirtualizer._willUpdate();
5241
5938
  }
5242
5939
 
@@ -5273,8 +5970,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5273
5970
  let rangeTransition = false;
5274
5971
  // rangeClickPending: set by onGridMouseDown on a Shift+Click (the range is set off the
5275
5972
  // pointer event's shiftKey BEFORE the cell's focusin fires); the follow-up focusin reads it
5276
- // to SKIP the range-collapse (a focusin carries no reliable shiftKey). Reset on consumption.
5277
- let rangeClickPending = false;
5973
+ // to SKIP the range-collapse (a focusin carries no reliable shiftKey).
5974
+ // B-11: it holds the CELL `{ isHeader, r, c }` the Shift+Click landed on, not a boolean, and
5975
+ // is null when idle. Shift+Click on the already-focused cell fires no focusin, so a boolean
5976
+ // was left armed and the NEXT ordinary click/Tab consumed it and skipped its own collapse,
5977
+ // stranding a painted rectangle. Coordinate-matched, that leak is self-healing: a focusin on
5978
+ // any other cell clears it and collapses normally. Cleared on BOTH branches of the consume.
5979
+ let rangeClickPending: any = null;
5278
5980
  // B19: a SYNCHRONOUS mirror of "a range currently exists" — extendRange/setRangeFocus set it
5279
5981
  // true, clearRange/clampRange-to-empty set it false. clearRange is invoked TWICE in one plain-
5280
5982
  // arrow keydown (the explicit collapse + the focusin that follows the programmatic focus move);
@@ -5343,7 +6045,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5343
6045
  // pre-write value (ROZ138). extendRange/setRangeFocus thread the just-computed locals through
5344
6046
  // here so the emitted payload matches the write. The single call site keeps the count
5345
6047
  // predictable (React multi-emit dedup, D-07). One-way notification.
6048
+ // B-10 — announce the selection alongside the emit. This is the SINGLE site every range
6049
+ // mutation funnels through (shift+arrow via extendRange, shift+click and drag-select and
6050
+ // fill-drag via setRangeFocus, Ctrl+A via selectAllBody), so one announce here covers all of
6051
+ // them; guarding the callers individually would be five chances to miss one.
6052
+ //
6053
+ // Writes $data.rangeAnnounce directly rather than importing clipboardFill's announce(): that
6054
+ // helper drives pasteAnnounce, and a range move must not overwrite a paste/validation message
6055
+ // the user has not heard yet (hence the separate key + region). `polite` + `aria-atomic` means
6056
+ // a fast drag replaces the pending text instead of queueing every intermediate rectangle.
6057
+ function rangeSummary(anchor: any, focus: any) {
6058
+ if (!anchor || !focus) return '';
6059
+ const rows = Math.abs(focus.rowIndex - anchor.rowIndex) + 1;
6060
+ const cols = Math.abs(focus.colIndex - anchor.colIndex) + 1;
6061
+ if (rows === 1 && cols === 1) return '1 cell selected';
6062
+ const rowPart = rows + (rows === 1 ? ' row' : ' rows');
6063
+ const colPart = cols + (cols === 1 ? ' column' : ' columns');
6064
+ return rowPart + ' by ' + colPart + ' selected, ' + rows * cols + ' cells';
6065
+ }
5346
6066
  function emitRangeChange(anchor: any, focus: any) {
6067
+ setRangeAnnounce(rangeSummary(anchor, focus));
5347
6068
  _props.onRangeChange?.({
5348
6069
  anchor,
5349
6070
  focus
@@ -5399,14 +6120,39 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5399
6120
  }
5400
6121
  }
5401
6122
 
5402
- // setRangeFocus(rIdx, cIdx): set the moving corner to an explicit cell (Shift+Click),
5403
- // seeding the anchor from the active cell when no range exists yet. Clamped to bounds.
5404
- // Emits range-change from this single site.
5405
- function setRangeFocus$local(rIdx: any, cIdx: any) {
6123
+ // setRangeFocus(rIdx, cIdx, anchorR, anchorC): set the moving corner to an explicit cell
6124
+ // (Shift+Click, or a drag-select's pointermove), seeding the anchor when no range exists yet.
6125
+ // Clamped to bounds. Emits range-change from this single site.
6126
+ //
6127
+ // B-01: the anchor seed is EXPLICIT when the caller knows it. Seeding it from
6128
+ // `$data.activeRow`/`$data.activeColIndex` is only correct if that state is live at the moment
6129
+ // the seed happens — and inside a drag-select it is not on React, where the document
6130
+ // pointermove closure captures the render-scoped consts from the render that ran BEFORE the
6131
+ // mousedown. Native focus moves as mousedown's DEFAULT ACTION, after handlers return, so at
6132
+ // `beginRangeDrag` time the active cell is still the PRE-mousedown one and React can never
6133
+ // show that closure a newer value. Dragging (5,2)→(7,3) on a fresh grid anchored at (0,0) and
6134
+ // selected the whole rectangle from there — which Ctrl+X and Delete then destroy.
6135
+ // The other five targets read `$data.activeRow` LIVE inside `move` (by then focusin has synced
6136
+ // it), so they were accidentally correct; taking the anchor from the caller removes the read
6137
+ // entirely and makes all six correct for the same reason. Falls back to the active cell when no
6138
+ // explicit anchor is passed — Shift+Click, where "the anchor is where you already were" IS the
6139
+ // active cell and the read happens synchronously in the same handler.
6140
+ // The two anchor params carry EXPLICIT `= null` defaults, and that is load-bearing, not style:
6141
+ // the emitted per-target signature types every declared param as REQUIRED, so the Shift+Click
6142
+ // call site's two-argument `setRangeFocus(row, col)` fails the strict leaf typecheck with
6143
+ // TS2554 "Expected 4 arguments, but got 2" on react and solid. Same rule the `focusActiveCell`
6144
+ // comment in gridKeydownHandlers records for its own optional params.
6145
+ function setRangeFocus$local(rIdx: any, cIdx: any, anchorR = null, anchorC = null) {
5406
6146
  const maxRow = bodyRowCount() - 1;
5407
6147
  const maxCol = visibleColCount() - 1;
5408
6148
  if (maxRow < 0 || maxCol < 0) return;
5409
6149
  let anchor = rangeAnchor();
6150
+ if (!anchor && anchorR != null && anchorC != null) {
6151
+ anchor = {
6152
+ rowIndex: clamp(Math.trunc(Number(anchorR)) || 0, 0, maxRow),
6153
+ colIndex: clamp(Math.trunc(Number(anchorC)) || 0, 0, maxCol)
6154
+ };
6155
+ }
5410
6156
  if (!anchor) anchor = {
5411
6157
  rowIndex: activeRow(),
5412
6158
  colIndex: activeColIndex()
@@ -5447,6 +6193,53 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5447
6193
  emitRangeChange(anchor, focus);
5448
6194
  }
5449
6195
 
6196
+ // ── B-04: where does a range EXTENSION measure its delta from? ──────────────────────────
6197
+ // `extendRange(dRow, dCol)` applies its delta to the range's FOCUS corner, not to the active
6198
+ // cell. For shift+arrow the two always agree (extendRange writes the active cell to the new
6199
+ // focus on every step), but two paths deliberately move the focus corner WITHOUT syncing the
6200
+ // active cell: a drag-select (`setRangeFocus`, which leaves the active cell on the mousedown
6201
+ // anchor) and Ctrl+A (`selectAllBody`, which sets both corners outright). After either,
6202
+ // Ctrl/Cmd+Shift+Arrow computed "distance from the active cell to the edge" and then added it
6203
+ // to a DIFFERENT cell. Drag (5,2)->(7,3) then Ctrl+Shift+ArrowUp extended to row 2 instead of
6204
+ // row 0; Ctrl+Shift+ArrowLeft to column 1 instead of column 0 — an extend-to-edge that stops
6205
+ // short of the edge by exactly the gap between the two.
6206
+ //
6207
+ // These report the corner extendRange will actually move, so the delta and its target are the
6208
+ // same cell by construction. No range yet -> the active cell IS the focus corner.
6209
+ function rangeFocusRow() {
6210
+ const f = rangeFocus();
6211
+ return f && f.rowIndex != null ? f.rowIndex : activeRow();
6212
+ }
6213
+ function rangeFocusCol() {
6214
+ const f = rangeFocus();
6215
+ return f && f.colIndex != null ? f.colIndex : activeColIndex();
6216
+ }
6217
+
6218
+ // B-15: the APG grid pattern's Ctrl+Space — select the COLUMN containing the focused cell.
6219
+ // Expressed as a full-height rectangle over the SAME range corners shift+arrow and Ctrl+A
6220
+ // drive, so Ctrl+C / Ctrl+X / Delete / the fill handle all act on it with no new machinery
6221
+ // and no second selection concept for a consumer to reason about. Emits through the single
6222
+ // emitRangeChange funnel (fresh corners, never a $data re-read). No-op on an empty grid, and
6223
+ // header-active is gated OUT by the caller, exactly as selectAllBody is.
6224
+ function selectActiveColumn() {
6225
+ const maxRow = bodyRowCount() - 1;
6226
+ const maxCol = visibleColCount() - 1;
6227
+ if (maxRow < 0 || maxCol < 0) return;
6228
+ const c = clamp(Math.trunc(Number(activeColIndex())) || 0, 0, maxCol);
6229
+ const anchor = {
6230
+ rowIndex: 0,
6231
+ colIndex: c
6232
+ };
6233
+ const focus = {
6234
+ rowIndex: maxRow,
6235
+ colIndex: c
6236
+ };
6237
+ setRangeAnchor(anchor);
6238
+ setRangeFocus(focus);
6239
+ rangeActive = true;
6240
+ emitRangeChange(anchor, focus);
6241
+ }
6242
+
5450
6243
  // clearRange(): drop the rectangle (a non-shift navigation / edit-entry collapses any
5451
6244
  // range back to a single active cell). Cheap no-op when no range is set (the guard keeps a
5452
6245
  // plain navigation with no active range from emitting). B19: when a range DID exist, emit
@@ -5583,6 +6376,20 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5583
6376
 
5584
6377
  // copyRange(): write the current range as TSV to the clipboard (async). No-op when the
5585
6378
  // async Clipboard API is unavailable (older/insecure contexts) — a copy is best-effort.
6379
+ // B-07: is the async Clipboard API actually usable? Ctrl+C / Ctrl+X / Ctrl+V used to
6380
+ // `e.preventDefault()` FIRST and only then call a verb that self-guards on
6381
+ // `navigator.clipboard`, so on any page where the API is absent — an insecure `http://` origin,
6382
+ // a permissions policy, an older embedded webview — the browser's OWN copy/cut/paste was
6383
+ // suppressed and nothing took its place. The keystroke did nothing at all. These predicates
6384
+ // move the check into the keymap's branch CONDITION (gridKeydownHandlers.rzts), so an
6385
+ // unavailable API means the branch does not match and the native default runs. Separate
6386
+ // read/write predicates because a context can grant one and not the other.
6387
+ function clipboardWriteAvailable() {
6388
+ return typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.writeText === 'function';
6389
+ }
6390
+ function clipboardReadAvailable() {
6391
+ return typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.readText === 'function';
6392
+ }
5586
6393
  function copyRange() {
5587
6394
  // B11: never copy from a header-active state (the reusable clipboard guard).
5588
6395
  if (!clipboardActiveAllowed()) return;
@@ -5601,15 +6408,33 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5601
6408
  // running fresh array (replaceRowValue) and record the committed cell. After the walk: ONE
5602
6409
  // writeData (the single r-model:data write), ONE cell-edit-commit per COMMITTED cell, and the
5603
6410
  // N-of-M aria-live announce. Returns { wrote, total }.
5604
- function applyGridToRange(grid: any, originRow: any, originCol: any) {
6411
+ // B-09 — `verb` names the OPERATION in the aria-live summary. This funnel serves paste, cut,
6412
+ // range-delete AND fill, so hardcoding "pasted" made a Cut announce "3 of 3 cells pasted" and a
6413
+ // Delete announce the same — actively wrong for a screen-reader user, who has no other signal
6414
+ // that the destructive operation they invoked actually happened. Defaults to 'pasted' so the
6415
+ // paste path's wording is unchanged.
6416
+ function applyGridToRange(grid: any, originRow: any, originCol: any, verb: any) {
6417
+ const opVerb = typeof verb === 'string' && verb !== '' ? verb : 'pasted';
5605
6418
  const maxRow = bodyRowCount() - 1;
5606
6419
  const maxCol = visibleColCount() - 1;
5607
6420
  if (maxRow < 0 || maxCol < 0) return {
5608
6421
  wrote: 0,
6422
+ changed: 0,
5609
6423
  total: 0
5610
6424
  };
6425
+ // B-08: `applied` and `committed` are DIFFERENT counts and used to be one.
6426
+ // applied — targets whose value now equals what was pasted, INCLUDING the ones that
6427
+ // already did. This is the announce's numerator: from the user's point of
6428
+ // view a cell that already held the pasted value was pasted successfully, and
6429
+ // reporting "3 of 5" for a 5-cell paste where 2 matched would be a lie.
6430
+ // committed — targets whose value actually CHANGED. This drives the write, the per-cell
6431
+ // `cell-edit-commit` emits and (through writeData) the undo step.
6432
+ // Conflating them meant an unchanged cell emitted an edit event and recorded an undo entry:
6433
+ // a 1->5 fill fired 5 commits when 4 cells already held the value, and a Delete over an
6434
+ // already-empty cell pushed an undo step that undoes nothing. Consumers persisting on
6435
+ // `cell-edit-commit` were issuing writes for data that had not moved.
5611
6436
  let total = 0;
5612
- let wrote = 0;
6437
+ let applied = 0;
5613
6438
  const committed = [];
5614
6439
  // Build the fresh data array incrementally so the whole paste is ONE writeData.
5615
6440
  let next = currentData();
@@ -5623,15 +6448,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5623
6448
  // it to this single funnel, so guarding the four callers individually would be four
5624
6449
  // chances to miss one.
5625
6450
  const rowGrouped = rowIndexIsGrouped(r);
6451
+ // Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (virtual+manual+rowCount, a row not
6452
+ // loaded yet) has no real underlying record — rowOriginalAt(r) returns the placeholder
6453
+ // sentinel ({__rdtLazyPlaceholder:true,index:i}, virtualization.rzts), which is never IN
6454
+ // currentData() (only the consumer's own array is), so sourceIndexOfRow's data.indexOf(...)
6455
+ // is always -1 and falls back to the VISIBLE index — either a no-op write with a FABRICATED
6456
+ // cell-edit-commit, or, inside a sparse hole, the hole gets clobbered with a partial row via
6457
+ // replaceRowValue. Hoisted per-ROW exactly like rowGrouped above (the SAME single placement
6458
+ // closes pasteRange/cutRange/clearActiveRange/fillRange all at once).
6459
+ const rowPlaceholder = rowIsLazyPlaceholder((rows() || [])[r]);
5626
6460
  const cols = grid[gr] || [];
5627
6461
  for (let gc = 0; gc < cols.length; gc++) {
5628
6462
  const c = originCol + gc;
5629
6463
  if (c > maxCol) break;
5630
- // A group-row cell still counts toward `total` — it is a skipped TARGET, exactly like
5631
- // a non-editable or validator-rejected cell — so the "N of M cells pasted" / "no cells
5632
- // pasted" announce keeps its existing semantics (never silently under-reports M).
6464
+ // A group-row / placeholder-row cell still counts toward `total` — it is a skipped
6465
+ // TARGET, exactly like a non-editable or validator-rejected cell — so the "N of M cells
6466
+ // pasted" / "no cells pasted" announce keeps its existing semantics (never silently
6467
+ // under-reports M).
5633
6468
  total = total + 1;
5634
- if (rowGrouped) continue;
6469
+ if (rowGrouped || rowPlaceholder) continue;
5635
6470
  const colId = columnIdAt(r, c);
5636
6471
  if (colId == null || !columnEditable(colId)) continue;
5637
6472
  const rowObj = rowOriginalAt(r);
@@ -5645,6 +6480,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5645
6480
  const field = fieldOfColId(colId);
5646
6481
  const srcIndex = sourceIndexOfRow(r);
5647
6482
  const oldValue = rowObj ? rowObj[field] : null;
6483
+ applied = applied + 1;
6484
+ // B-08: the cell already holds this value — it counts as applied, but there is nothing
6485
+ // to write, nothing to emit and nothing to undo.
6486
+ if (oldValue === value) continue;
5648
6487
  next = replaceRowValue(next, srcIndex, field, value);
5649
6488
  committed.push({
5650
6489
  rowId: rowIdAt(r),
@@ -5652,22 +6491,37 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5652
6491
  oldValue,
5653
6492
  newValue: value
5654
6493
  });
5655
- wrote = wrote + 1;
5656
6494
  }
5657
6495
  }
5658
- if (wrote > 0) {
6496
+ if (committed.length > 0) {
5659
6497
  editTransition = true;
5660
6498
  writeData(next);
5661
6499
  editTransition = false;
5662
6500
  // One cell-edit-commit per COMMITTED cell (the per-cell event contract, D-03).
5663
- for (let i = 0; i < committed.length; i++) _props.onCellEditCommit?.(committed[i]);
6501
+ // A-08: each emit is guarded INDIVIDUALLY. These listeners are consumer code, and a single
6502
+ // throwing one used to abort the loop — so on a 40-cell paste, cells 0..k notified and
6503
+ // k+1..39 silently never did, leaving a consumer persisting on this event with a partial
6504
+ // write of a model that had already been fully updated. Measured while building this: a
6505
+ // throwing VALIDATOR is NOT the reachable vehicle here — `runValidator`
6506
+ // (editCellLifecycle.rzts) already catches and reports 'Invalid value' — so the listener
6507
+ // is the first-party code that actually escapes.
6508
+ for (let i = 0; i < committed.length; i++) {
6509
+ try {
6510
+ _props.onCellEditCommit?.(committed[i]);
6511
+ } catch (err: any) {
6512
+ console.error('[rozie-data-table] paste failed partway: a cell-edit-commit listener threw. The ' + 'model was already written; the remaining cells still notify.', err);
6513
+ }
6514
+ }
5664
6515
  }
5665
6516
  // WR-02: announce the N-of-M summary only when at least one cell was written. When the paste
5666
6517
  // targeted real cells but every one was skipped (validation-failed / non-editable), announce a
5667
6518
  // distinct validation-failed message instead of a misleading "0 of M cells pasted".
5668
- if (wrote > 0) announce(wrote + ' of ' + total + ' cells pasted');else if (total > 0) announce('No cells pasted — ' + total + ' cells were invalid or read-only');
6519
+ if (applied > 0) announce(applied + ' of ' + total + ' cells ' + opVerb);else if (total > 0) announce('No cells ' + opVerb + ' — ' + total + ' cells were invalid or read-only');
6520
+ // `wrote` stays the ANNOUNCED count (targets satisfied), not the written one, so every
6521
+ // existing caller and assertion keeps its meaning; `changed` is the new, narrower number.
5669
6522
  return {
5670
- wrote,
6523
+ wrote: applied,
6524
+ changed: committed.length,
5671
6525
  total
5672
6526
  };
5673
6527
  }
@@ -5714,14 +6568,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5714
6568
  return;
5715
6569
  }
5716
6570
  if (!p || !p.then) return;
6571
+ // A-08: the two failure modes are separated. A REJECTED clipboard read (permission denied,
6572
+ // insecure context, an empty selection) is a legitimate silent no-op — that is the rejection
6573
+ // handler, the second argument. Anything the fulfilment handler throws is FIRST-PARTY: a
6574
+ // consumer's row accessor, a `data`-model setter, or a `cell-edit-commit` listener (which
6575
+ // `applyGridToRange` now also guards per-emit). A single trailing `.catch(() => {})`
6576
+ // swallowed both, so consumer code failing inside a paste produced no error, no warning and
6577
+ // nothing in the console to search for. NOT a column validator: `runValidator` already
6578
+ // catches a throwing one and reports 'Invalid value' — checked rather than assumed.
5717
6579
  p.then((text: any) => {
5718
- const grid = parseTsv(text);
5719
- if (!grid.length) return;
5720
- // C3: tile the clipboard block to fill the destination range (single→range fill,
5721
- // smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
5722
- const tiled = tileGridToBox(grid, destBox);
5723
- applyGridToRange(tiled, anchorRow, anchorCol);
5724
- }).catch(() => {});
6580
+ try {
6581
+ const grid = parseTsv(text);
6582
+ if (!grid.length) return;
6583
+ // C3: tile the clipboard block to fill the destination range (single→range fill,
6584
+ // smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
6585
+ const tiled = tileGridToBox(grid, destBox);
6586
+ applyGridToRange(tiled, anchorRow, anchorCol, 'pasted');
6587
+ } catch (err: any) {
6588
+ console.error('[rozie-data-table] paste failed: a row accessor, the data model setter or other ' + 'consumer code threw. The paste was abandoned.', err);
6589
+ }
6590
+ }, () => {/* clipboard read rejected (permission / insecure context) — silent by design */});
5725
6591
  }
5726
6592
 
5727
6593
  // cutRange(): C3 Cut — copy the current range to the clipboard (rangeToTsv — the SAME escaped
@@ -5755,7 +6621,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5755
6621
  for (let c = c0; c <= c1; c++) cols.push('');
5756
6622
  grid.push(cols);
5757
6623
  }
5758
- applyGridToRange(grid, r0, c0);
6624
+ applyGridToRange(grid, r0, c0, 'cut');
5759
6625
  }
5760
6626
 
5761
6627
  // clearActiveRange(): the §7 (260709-3qt) Delete/Backspace clear — cutRange() MINUS the clipboard
@@ -5781,7 +6647,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5781
6647
  for (let c = c0; c <= c1; c++) cols.push('');
5782
6648
  grid.push(cols);
5783
6649
  }
5784
- applyGridToRange(grid, r0, c0);
6650
+ applyGridToRange(grid, r0, c0, 'cleared');
5785
6651
  }
5786
6652
 
5787
6653
  // fillRange(sourceBox): drag-fill (D-04 — VALUE-COPY ONLY, no series detection). B7: the fill
@@ -5836,7 +6702,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5836
6702
  }
5837
6703
  grid.push(cols);
5838
6704
  }
5839
- applyGridToRange(grid, box.r0, box.c0);
6705
+ applyGridToRange(grid, box.r0, box.c0, 'filled');
5840
6706
  }
5841
6707
 
5842
6708
  // onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
@@ -6111,10 +6977,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6111
6977
  rangeDragging = false;
6112
6978
  }
6113
6979
 
6114
- // beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell. The
6115
- // mousedown's native focus/focusin already committed the ACTIVE cell to (anchorR, anchorC), so
6116
- // setRangeFocus (which seeds the anchor from the ACTIVE cell) spans mousedown-cell→pointer-cell —
6117
- // we NEVER write $data.rangeAnchor directly (it is React-stale, ROZ138). rangeDragMoved starts
6980
+ // beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell.
6981
+ // B-01: the anchor is handed to setRangeFocus EXPLICITLY, never left to be seeded from the
6982
+ // active cell. The mousedown's native focus is a DEFAULT ACTION that runs after this handler
6983
+ // returns, so at this point $data.activeRow/activeColIndex still hold the PRE-mousedown cell;
6984
+ // on React the pointermove closure below is frozen at that render and can never observe the
6985
+ // post-focusin value, so the whole gesture anchored at wherever the user happened to be
6986
+ // (a fresh grid: (0,0)). We still never WRITE $data.rangeAnchor directly (React-stale,
6987
+ // ROZ138) — setRangeFocus owns that write. rangeDragMoved starts
6118
6988
  // false and flips true only once the pointer reaches a DIFFERENT cell, so a mousedown-with-no-move
6119
6989
  // leaves a single active cell + no range (a normal click). lastCell dedups the many pointermove
6120
6990
  // events per cell (setRangeFocus emits range-change — only extend on a NEW cell, mirroring fillDrag's
@@ -6139,7 +7009,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6139
7009
  if (cell && (cell.r !== lastCell.r || cell.c !== lastCell.c)) {
6140
7010
  lastCell = cell;
6141
7011
  rangeDragMoved = true;
6142
- setRangeFocus$local(cell.r, cell.c);
7012
+ setRangeFocus$local(cell.r, cell.c, anchorR, anchorC);
6143
7013
  }
6144
7014
  };
6145
7015
  const up = () => {
@@ -6177,6 +7047,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6177
7047
  // row reaches the group-toggle branch below them instead of opening an editor.
6178
7048
  function isActiveCellEditable() {
6179
7049
  if (rowIndexIsGrouped(activeRow())) return false;
7050
+ // A lazy placeholder (a row not loaded yet) has nothing to edit (quick 260925-dtl part 2).
7051
+ if (rowIsLazyPlaceholder((rows() || [])[activeRow()])) return false;
6180
7052
  const colId = activeCellColumnId();
6181
7053
  return colId != null && columnEditable(colId);
6182
7054
  }
@@ -6408,6 +7280,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6408
7280
  // calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
6409
7281
  // does not run on that path at all.
6410
7282
  if (rowIndexIsGrouped(rowIndex)) return;
7283
+ if (rowIsLazyPlaceholder((rows() || [])[rowIndex])) return;
6411
7284
  const colId = columnIdAt(rowIndex, colIndex);
6412
7285
  if (colId == null || !columnEditable(colId)) return;
6413
7286
  // A new edit session starts — reset the sync idempotency latch so THIS session's eventual
@@ -6418,6 +7291,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6418
7291
  // editor clears any row-edit state so isEditing never resolves both modes for one cell.
6419
7292
  setEditingRowIndex(null);
6420
7293
  setRowDraft({});
7294
+ rowDraftSync = {};
6421
7295
  setEditingRow(rowIndex);
6422
7296
  setEditingCol(colIndex);
6423
7297
  setDraftValue(seed != null ? seed : cellValueAt(rowIndex, colIndex));
@@ -6490,6 +7364,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6490
7364
  function endRowEdit() {
6491
7365
  setEditingRowIndex(null);
6492
7366
  setRowDraft({});
7367
+ rowDraftSync = {};
6493
7368
  setInvalidMsg('');
6494
7369
  setActiveInControl(false);
6495
7370
  setEditVer(editVer() + 1);
@@ -6518,7 +7393,28 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6518
7393
  // other editor type commits the value verbatim. Idempotent for the #editor drop-in path
6519
7394
  // (an already-numeric override passes through; an explicit null stays null).
6520
7395
  function coerceCellValue(colId: any, raw: any) {
6521
- if (editorTypeOf(colId) !== 'number') return raw;
7396
+ const kind = editorTypeOf(colId);
7397
+ // ── C-13 ────────────────────────────────────────────────────────────────────────────
7398
+ // This funnel exists so "no mixed/garbage types ever reach the model" (B9, T-63-03-01), and
7399
+ // it covered ONE of the four built-in editor types. Every path that writes without opening
7400
+ // the built-in control — paste, Cut, Delete/Backspace over a range, a fill-drag — arrives
7401
+ // here with a plain TSV STRING, so a `checkbox` column took `''` from a Delete and the
7402
+ // literal text `'true'` from a paste, straight into a field the consumer declared boolean.
7403
+ // `select` is deliberately NOT coerced: its model type IS the option string, so a string is
7404
+ // already correct there, and an out-of-range option is a validation question (C-03's class),
7405
+ // not a type one.
7406
+ //
7407
+ // Recognised spellings both ways; ANYTHING else — including '' and unrecognised text —
7408
+ // becomes `false`. That is lossy by construction and deterministic by design: the
7409
+ // alternative is letting a non-boolean reach a boolean field, which is the defect. It is
7410
+ // documented in the editing page's emptied-cell table.
7411
+ if (kind === 'checkbox') {
7412
+ if (typeof raw === 'boolean') return raw;
7413
+ if (raw == null) return false;
7414
+ if (typeof raw === 'number') return raw !== 0;
7415
+ return /^\s*(true|1|yes|y|on)\s*$/i.test(String(raw));
7416
+ }
7417
+ if (kind !== 'number') return raw;
6522
7418
  if (raw == null) return null;
6523
7419
  if (typeof raw === 'number') return Number.isNaN(raw) ? null : raw;
6524
7420
  const s = String(raw).trim();
@@ -6638,11 +7534,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6638
7534
  // checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
6639
7535
  // write ever targets a group row" a property of the write funnels themselves.
6640
7536
  if (rowIndexIsGrouped(activeRow())) return;
6641
- const colId = columnIdAt(activeRow(), activeColIndex());
6642
- if (colId == null || !columnEditable(colId)) return;
6643
7537
  const rowList = rows() || [];
6644
7538
  const row = rowList[activeRow()];
6645
7539
  if (!row) return;
7540
+ // Debug 260926-dtl-edit-guard: defense-in-depth, matching the redundant rowIndexIsGrouped
7541
+ // check just above (Layer 4 is reachable ONLY through the Layer-1-gated checkbox branch in
7542
+ // onGridKeyDown, whose isActiveCellEditable() already excludes a placeholder row — this makes
7543
+ // "no write ever targets a placeholder row" a property of the write funnel itself, not just of
7544
+ // its one caller).
7545
+ if (rowIsLazyPlaceholder(row)) return;
7546
+ const colId = columnIdAt(activeRow(), activeColIndex());
7547
+ if (colId == null || !columnEditable(colId)) return;
6646
7548
  const rowOriginal = row.original;
6647
7549
  const rowId = row.id;
6648
7550
  const oldValue = cellValueAt(activeRow(), activeColIndex());
@@ -6775,6 +7677,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6775
7677
  // redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
6776
7678
  // and the natural call site for a future #rowActions "Edit" drop-in button.
6777
7679
  if (rowIndexIsGrouped(rowIndex)) return;
7680
+ // Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (a row not loaded yet, virtual+manual+
7681
+ // rowCount) has nothing to edit — beginEdit (editCellLifecycle.rzts) already carries this
7682
+ // guard for single-cell entry; this full-row twin was missing it, so the public editRow()
7683
+ // verb (and, redundantly with isActiveCellEditable's own placeholder check, Shift+F2) could
7684
+ // seed a row editor from a placeholder's undefined fields and commit through the same
7685
+ // sourceIndexOfRow fallback bug applyGridToRange's placeholder skip closes.
7686
+ if (rowIsLazyPlaceholder((rows() || [])[rowIndex])) return;
6778
7687
  const editable = editableColumnsForRow(rowIndex);
6779
7688
  if (editable.length === 0) return;
6780
7689
  // A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
@@ -6794,6 +7703,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6794
7703
  draft[ec.colId] = orig ? orig[ec.field] : null;
6795
7704
  }
6796
7705
  setRowDraft(draft);
7706
+ rowDraftSync = draft;
6797
7707
  setEditingRowIndex(rowIndex);
6798
7708
  setActiveInControl(true);
6799
7709
  setEditVer(editVer() + 1);
@@ -6824,7 +7734,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6824
7734
  const r = rowList[rowIndex];
6825
7735
  const rowOriginal = r ? r.original : null;
6826
7736
  const rowId = r ? r.id : null;
6827
- const draft = rowDraft() || {};
7737
+ // C-05: the SYNC mirror, not `$data.rowDraft` — see rowDraftSync in DataTable.rozie.
7738
+ const draft = rowDraftSync || {};
6828
7739
  // Validate every edited column FIRST (D-01: a single failure blocks the whole row commit).
6829
7740
  // B3 (Rule 1): coerce each draft by the column's editor type BEFORE validation + write — a
6830
7741
  // 'number' editor must commit a real Number/null, never the raw editor STRING (the single-cell
@@ -7016,6 +7927,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7016
7927
  // A top-level `let` (React hoists to useRef → persists).
7017
7928
  let committedThisSession = false;
7018
7929
 
7930
+ // rowDraftSync (quick 260922-mkb, C-05): a SYNCHRONOUS mirror of `$data.rowDraft`, written at
7931
+ // every rowDraft write and read by commitRow. A custom #editor drop-in stages its value with
7932
+ // `commit(v)` → setRowDraft in the SAME keydown that then commits the row (the host's drop-in
7933
+ // wrapper runs in the bubble phase right after the drop-in's own Enter handler). On React
7934
+ // `$data.rowDraft` is a `useState` read, so commitRow in that same tick saw the pre-write draft,
7935
+ // found no change and wrote nothing. The built-in editors never hit this only because they stage
7936
+ // on every `input` event, long before Enter. Same top-level-`let` technique as
7937
+ // committedThisSession above; on the five synchronous targets it always equals $data.rowDraft.
7938
+ let rowDraftSync: Record<string, any> = {};
7939
+
7940
+ // dropinTabFlush (quick 260922-mkb, C-10): non-null only for the duration of the drop-in
7941
+ // wrapper's Tab flush (see onEditorDropinKeyDown). While set, a drop-in's `commit(v)` records
7942
+ // that it ran and whether it committed, and commits WITHOUT the focus return (the Tab handler
7943
+ // moves focus on to the next editor itself).
7944
+ let dropinTabFlush: {
7945
+ called: boolean;
7946
+ committed: boolean;
7947
+ } | null = null;
7948
+
7019
7949
  // ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
7020
7950
  // In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
7021
7951
  // (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
@@ -7044,6 +7974,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7044
7974
  setRowDraft$local(colId, value);
7045
7975
  return;
7046
7976
  }
7977
+ // C-10: during the drop-in wrapper's Tab flush, record the outcome and skip the focus
7978
+ // return — the Tab handler advances focus to the next editor itself.
7979
+ if (dropinTabFlush) {
7980
+ dropinTabFlush.called = true;
7981
+ // STICKY: only a successful commit may set the outcome. A re-entrant commit within the
7982
+ // same flush (any blur the write itself provokes) is latched to false by commitEdit and
7983
+ // must not clear it. Measured: before the Solid slot-fill fix this re-entry was real.
7984
+ if (commitEdit(value, true)) dropinTabFlush.committed = true;
7985
+ return;
7986
+ }
7047
7987
  commitEdit(value);
7048
7988
  };
7049
7989
  }
@@ -7081,10 +8021,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7081
8021
  // setRowDraft: write ONE key into a FRESH rowDraft object (whole-object replace — an
7082
8022
  // in-place mutation is silently dropped on React/Solid; the family immutable rule).
7083
8023
  function setRowDraft$local(colId: any, value: any) {
7084
- const src = rowDraft() || {};
8024
+ // C-05: build from the SYNC mirror, so two same-tick writes (and a same-tick commitRow) never
8025
+ // see a React-stale `$data.rowDraft`.
8026
+ const src = rowDraftSync || {};
7085
8027
  const next = {};
7086
8028
  for (const k in src) next[k] = src[k];
7087
8029
  next[colId] = value;
8030
+ rowDraftSync = next;
7088
8031
  setRowDraft(next);
7089
8032
  }
7090
8033
 
@@ -7174,6 +8117,105 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7174
8117
  }
7175
8118
  }
7176
8119
 
8120
+ // DECLARED AFTER onEditorKeyDown ON PURPOSE. It delegates to it, and the React emitter lowers
8121
+ // both to `useCallback`s whose DEPENDENCY ARRAY is evaluated EAGERLY at declaration time — so
8122
+ // with this one first, its deps array referenced `onEditorKeyDown` before the const existed and
8123
+ // every React cell in the family died with `ReferenceError: Cannot access 'ze' before
8124
+ // initialization` at mount. That is the 87-02 useCallback-deps-array ordering lesson, and it is
8125
+ // invisible on the other five targets. Keep this block below.
8126
+ // ── C-10 + C-05: the grid keymap for a CUSTOM #editor drop-in ───────────────────────────
8127
+ // The built-in editors bind `onEditorKeyDown` directly, so Tab commits-and-advances and, in
8128
+ // row mode, Enter commits the row. A drop-in binds nothing of ours: it is consumer markup
8129
+ // inside the `#editor` slot. Its own keydown handler covers Enter and Escape (the drop-in
8130
+ // contract) and stops there, and the keydown then bubbles to the table root where
8131
+ // `onGridKeyDown` early-returns because an editor is open. Result:
8132
+ // C-10 — Tab in a drop-in did not commit and advance. It fell through to NATIVE tab order
8133
+ // and walked focus out of the grid, leaving the editor open behind it.
8134
+ // C-05 — in FULL-ROW mode a drop-in's `commit(v)` is draft-only BY DESIGN (committing per
8135
+ // cell would split the row into N writes and N events), and the row commits only
8136
+ // through handlers bound on the built-in inputs — which a row of drop-ins does not
8137
+ // have. So Shift+F2, documented as "Enter commits the whole row in one
8138
+ // `r-model:data` write", wrote a draft and nothing else, `editingRowIndex` stayed
8139
+ // set, and `onGridKeyDown`'s early-return left arrow nav dead. The only exits were
8140
+ // Escape (reverting the whole row) and the imperative verb.
8141
+ //
8142
+ // Bound on the `display: contents` span that wraps the slot, so it sees the bubbled event
8143
+ // from whatever the consumer rendered — including a bespoke editor that never heard of this
8144
+ // component. The host owning the GRID keymap while the drop-in owns its own CONTROL is the
8145
+ // same division the built-in path already has.
8146
+ //
8147
+ // Row mode ignores `defaultPrevented` for Enter/Escape on purpose: a drop-in preventDefaults
8148
+ // Enter to mean "do not insert a newline", not "the row is handled". Its own handler has
8149
+ // already run (target phase before bubble), so the draft is staged by the time we commit.
8150
+ // Single-cell mode honours `defaultPrevented` for Tab, so a drop-in that genuinely implements
8151
+ // its own Tab (a multi-field editor) keeps it.
8152
+ //
8153
+ // Single-cell Tab FLUSHES the drop-in before advancing (quick 260922-mkb). A drop-in owns its
8154
+ // draft: the host's `draftValue` still holds the value the editor opened with, so routing Tab
8155
+ // straight into onEditorKeyDown committed that stale value, opened the next cell's editor, and
8156
+ // then the old drop-in's unmount blur ran `commit(typed)` against the NOW-editing cell — the
8157
+ // typed text landed in the NEXT column (measured: `status=Zeta` from a Name edit, on four
8158
+ // targets). Instead the handler blurs the drop-in while its own cell is still the editing one,
8159
+ // so the drop-in's blur-commit (the drop-in contract) writes the right cell, and only then
8160
+ // advances. `dropinTabFlush` tells editorCommitFor to report the outcome and skip the focus
8161
+ // return. A drop-in that does not commit on blur (EditorCheckbox commits on change, so it holds
8162
+ // nothing pending) falls back to the host-draft path, which is exactly right in that case.
8163
+ function onEditorDropinKeyDown(e: any) {
8164
+ if (!e) return;
8165
+ const key = e.key;
8166
+ if (inRowEdit()) {
8167
+ if (key === 'Enter') {
8168
+ e.preventDefault();
8169
+ commitRow();
8170
+ return;
8171
+ }
8172
+ if (key === 'Escape') {
8173
+ e.preventDefault();
8174
+ cancelRow();
8175
+ return;
8176
+ }
8177
+ if (key === 'Tab') {
8178
+ e.preventDefault();
8179
+ rowEditTab(e.target, e.shiftKey);
8180
+ return;
8181
+ }
8182
+ return;
8183
+ }
8184
+ if (key !== 'Tab' || e.defaultPrevented) return;
8185
+ e.preventDefault();
8186
+ const fromRow = editingRow();
8187
+ const fromCol = editingCol();
8188
+ const target = e.shiftKey ? prevEditableCell(fromRow, fromCol) : nextEditableCell(fromRow, fromCol);
8189
+ const el = e.target;
8190
+ // Move focus to the drop-in's OWN cell rather than calling blur(): that still fires the
8191
+ // drop-in's blur-commit, but keeps focus inside the grid instead of dropping it to <body>.
8192
+ const ownCell = el && el.closest ? el.closest('[data-grid-cell]') : null;
8193
+ const flush = {
8194
+ called: false,
8195
+ committed: false
8196
+ };
8197
+ dropinTabFlush = flush;
8198
+ if (ownCell && ownCell.focus) ownCell.focus();else if (el && el.blur) el.blur();
8199
+ dropinTabFlush = null;
8200
+ if (!flush.called) {
8201
+ onEditorKeyDown(e);
8202
+ return;
8203
+ }
8204
+ // A validation failure keeps the editor open: give the drop-in its focus back.
8205
+ if (!flush.committed) {
8206
+ if (el && el.focus) el.focus();
8207
+ return;
8208
+ }
8209
+ if (target) {
8210
+ setActiveRow(target.row);
8211
+ setActiveColIndex(target.col);
8212
+ beginEdit(target.row, target.col, null);
8213
+ } else {
8214
+ // B5 twin: no editable cell in the Tab direction — keep focus INSIDE the grid.
8215
+ focusCellWhenReady(fromRow, fromCol);
8216
+ }
8217
+ }
8218
+
7177
8219
  // onEditorBlur: commit on a genuine click/focus-away (D-01 — an invalid value keeps the
7178
8220
  // editor open via commitEdit's reject path). SKIP when:
7179
8221
  // - editTransition is set (a synchronous commit/cancel teardown is unmounting the editor), or
@@ -7210,11 +8252,52 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7210
8252
  }
7211
8253
  if (editingRow() < 0 || editTransition) return;
7212
8254
  const next = e ? e.relatedTarget : null;
7213
- // A null relatedTarget is an unmount-blur (the editor left the DOM) or a focus drop the
7214
- // keyboard path owns; committing here would double-count (WR-04: the OLD editor's blur on
7215
- // a Tab-advance fires with a TRANSIENT null relatedTarget while it unmounts). Keep the
7216
- // conservative null=skip behavior.
7217
- if (next == null) return;
8255
+ const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
8256
+ // ── B-02 ────────────────────────────────────────────────────────────────────────────
8257
+ // A null relatedTarget used to return unconditionally. That is correct for the WR-04 case
8258
+ // the comment cited — a Tab-advance fires the OLD editor's blur with a TRANSIENT null
8259
+ // relatedTarget while it unmounts, and committing there double-counts — but it is NOT the
8260
+ // only shape. Clicking any non-focusable region of the page (the body, a heading, whitespace)
8261
+ // also blurs the editor with a null relatedTarget, and that returned WITHOUT committing and
8262
+ // WITHOUT endEdit(): the typed value was silently discarded, the editor stayed open, and
8263
+ // `$data.editingRow` stayed >= 0 — which gridKeydownHandlers' `if ($data.editingRow >= 0)
8264
+ // return` reads as "an editor owns the keyboard", so arrows/Enter/F2/Home/Ctrl+C were all
8265
+ // dead until the user double-clicked somewhere else. There is no document-level click-away
8266
+ // listener anywhere in the family to catch it afterwards.
8267
+ //
8268
+ // The two shapes ARE separable, with the discriminator the branch below already uses: on a
8269
+ // Tab-advance the keyboard path has already moved the editing pair to the NEXT cell, so the
8270
+ // blurring editor's own cell no longer matches ($data.editingRow/editingCol), while a
8271
+ // click-away leaves the pair still sitting on it. The row-edit branch twenty lines above
8272
+ // reaches the same conclusion by the same reasoning and commits on null; this is that branch's
8273
+ // single-cell twin.
8274
+ if (next == null) {
8275
+ // Not a click-away unless a pointerdown OUTSIDE the grid just said so. `outsidePointerDown`
8276
+ // is the discriminator, and nothing weaker works: an editor recycled out of the virtual
8277
+ // window (or replaced by Solid's keyed-row reconcile) blurs with a null relatedTarget too,
8278
+ // and `closest()` still resolves an owning `<td>` inside a DETACHED subtree, so every
8279
+ // synchronous DOM test reads it as a click-away — measured, it closed the pinned editor in
8280
+ // data-table-edit's "open editor + index-based range survive virtualization recycle" case on
8281
+ // solid. Deferring a frame does not help either: by then the node is reattached, focus sits
8282
+ // on <body>, and the state is indistinguishable from a real click-away.
8283
+ //
8284
+ // The flag is a plain module-let set by the document pointerdown listener in DataTable.rozie
8285
+ // (NOT a $data field, and NOT a closure over one): a document listener is attached once, so
8286
+ // on React any `$data` it closes over is frozen at attach time, and a commit driven from
8287
+ // that closure would write the draft as of editor-OPEN and lose everything typed. Routing
8288
+ // the decision back through THIS handler — template-bound, so React re-binds it every
8289
+ // render — keeps the commit on the always-fresh path. Consume it here so an unmount blur
8290
+ // arriving later can never inherit it.
8291
+ if (!outsidePointerDown) return;
8292
+ outsidePointerDown = false;
8293
+ if (!fromCell) return;
8294
+ if (fromCell.getAttribute('data-row') !== String(editingRow()) || fromCell.getAttribute('data-col-index') !== String(editingCol())) return;
8295
+ // Genuine click-away onto a non-focusable region outside the grid → commit + close (D-01's
8296
+ // reject path keeps the editor open on an invalid value, exactly as for a click onto another
8297
+ // cell). skipFocusReturn: the user deliberately put focus elsewhere; do not yank it back.
8298
+ commitEdit(undefined, true);
8299
+ return;
8300
+ }
7218
8301
  // Focus moving OUTSIDE the grid (a click into another widget) → commit (D-01 reject keeps
7219
8302
  // the editor open on an invalid value).
7220
8303
  if (!(gridRoot && gridRoot.contains && gridRoot.contains(next))) {
@@ -7225,7 +8308,6 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7225
8308
  // keyboard handler already committed) from a genuine click-away to ANOTHER grid cell
7226
8309
  // (which must commit + close so the grid is not wedged with an open editor).
7227
8310
  const nextCell = next.closest ? next.closest('[data-grid-cell]') : null;
7228
- const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
7229
8311
  // Same cell (an inner control / the editing cell itself on an Enter focus-return) → a
7230
8312
  // controlled move; skip. Also skip when either cell can't be resolved (an unmounting
7231
8313
  // editor has no owning cell — the Tab-advance remount-blur path, never a click-away).
@@ -7560,6 +8642,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7560
8642
  if (id == null) return;
7561
8643
  const key = String(id);
7562
8644
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
8645
+ // C-09: skip the state write when the incoming spec is EQUIVALENT to the stored one.
8646
+ // `<Column>`'s re-register watch keys on reference-typed props whose documented wiring is
8647
+ // an inline literal — a fresh identity every consumer render — so without this guard every
8648
+ // render whole-object-replaced `$data.colReg`, which the parent's re-feed watch keys on,
8649
+ // which re-rendered the parent, which produced a new identity: a feedback loop, not a slow
8650
+ // path. Guarded HERE rather than in the watch because this is the single seam every
8651
+ // registration passes through, and because value-equality is the correct test for the
8652
+ // registry's own purpose (has anything the table renders from actually changed?).
8653
+ const prev = colReg() ? (colReg() as Record<string, any>)[key] : undefined;
8654
+ if (prev !== undefined && columnSpecsEquivalent(prev, spec)) return;
7563
8655
  setColReg({
7564
8656
  ...colReg(),
7565
8657
  [key]: spec
@@ -7580,7 +8672,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7580
8672
 
7581
8673
  <div class={"rdt-column-defs"} style={{ display: "none" }} aria-hidden="true" data-rozie-s-d5dcab4c="">{resolved()}</div>
7582
8674
 
7583
- {<Show when={!!invalidMsg()}><div class={"rdt-sr-live"} role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{invalidMsg()}</div></Show>}{<Show when={!!pasteAnnounce()}><div class={"rdt-sr-live rdt-sr-paste"} data-testid="paste-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{pasteAnnounce()}</div></Show>}{<Show when={!!liveAnnounce()}><div class={"rdt-sr-live rdt-sr-sortfilter"} data-testid="sortfilter-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{liveAnnounce()}</div></Show>}<div class={"rdt-toolbar"} data-rozie-s-d5dcab4c="">
8675
+ {<Show when={!!invalidMsg()}><div class={"rdt-sr-live"} role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{invalidMsg()}</div></Show>}{<Show when={!!pasteAnnounce()}><div class={"rdt-sr-live rdt-sr-paste"} data-testid="paste-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{pasteAnnounce()}</div></Show>}{<Show when={!!rangeAnnounce()}><div class={"rdt-sr-live rdt-sr-range"} data-testid="range-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{rangeAnnounce()}</div></Show>}{<Show when={!!liveAnnounce()}><div class={"rdt-sr-live rdt-sr-sortfilter"} data-testid="sortfilter-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c="">{liveAnnounce()}</div></Show>}<div class={"rdt-toolbar"} data-rozie-s-d5dcab4c="">
7584
8676
  <input type="text" role="searchbox" aria-label="Search table" class={"rdt-global-filter"} value={globalFilterValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onGlobalFilterInput($event); }} data-rozie-s-d5dcab4c="" />
7585
8677
 
7586
8678
  {<Show when={allLeafColumns().length}><details class={"rdt-colvis"} data-rozie-s-d5dcab4c="">
@@ -7596,10 +8688,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7596
8688
 
7597
8689
  {<Show when={local.groupable}><div class={"rdt-group-bar-host"} data-rozie-s-d5dcab4c="">
7598
8690
  {(_props.groupBarSlot ?? _props.slots?.['groupBar'])?.({ get grouping() { return groupingKeys(); }, get groupableColumns() { return groupableColumns(); }, applyGrouping, clearGrouping }) ?? <Key each={groupingKeys() as readonly any[]} by={(gk) => gk}>{(gk) => <span class={"rdt-group-token"} data-group-token="" data-rozie-s-d5dcab4c="">{rozieDisplay(gk())}</span>}</Key>}
7599
- </div></Show>}{<Show when={isWindowed()} fallback={<table aria-rowcount={rozieAttr(gridAriaRowCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
8691
+ </div></Show>}{<Show when={isWindowed()} fallback={<table aria-rowcount={rozieAttr(gridAriaRowCount())} aria-colcount={rozieAttr(gridAriaColCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
7600
8692
  <thead class={"rdt-thead"} role="rowgroup" data-rozie-s-d5dcab4c="">
7601
8693
  <Key each={headerGroups() as readonly any[]} by={(hg) => hg.id}>{(hg, hgLevel) => <tr class={"rdt-tr"} role="row" aria-rowindex={rozieAttr(hgLevel() + 1)} data-rozie-s-d5dcab4c="">
7602
- <Key each={hg().headers as readonly any[]} by={(header) => header.id}>{(header) => <th class={"rdt-th" + " " + rozieClass({ 'rdt-select-th': isSelectColumn(header().column.id), 'rdt-expander-th': isExpanderColumn(header().column.id), 'rdt-th-resizing': columnIsResizing(header().column.id), 'rdt-cell-active': isActiveCell('__header', headerColIndexOf(hg(), header()), hgLevel()) })} role="columnheader" data-col={rozieAttr(header().column.id)} data-grid-cell="" data-row="__header" data-header-level={rozieAttr(hgLevel())} colSpan={rozieAttr(header().colSpan > 1 ? header().colSpan : null)} data-col-index={rozieAttr(headerColIndexOf(hg(), header()))} tabIndex={rozieAttr(cellTabindex('__header', headerColIndexOf(hg(), header()), hgLevel()))} aria-sort={rozieAttr(ariaSortFor(header().column.id))} style={parseInlineStyle(thStyle(header()))} data-rozie-s-d5dcab4c="">
8694
+ <Key each={hg().headers as readonly any[]} by={(header) => header.id}>{(header) => <th class={"rdt-th" + " " + rozieClass({ 'rdt-select-th': isSelectColumn(header().column.id), 'rdt-expander-th': isExpanderColumn(header().column.id), 'rdt-pinned': columnPinSide(header().column.id) !== '', 'rdt-th-resizing': columnIsResizing(header().column.id), 'rdt-cell-active': isActiveCell('__header', headerColIndexOf(hg(), header()), hgLevel()) })} role="columnheader" data-col={rozieAttr(header().column.id)} data-grid-cell="" data-row="__header" data-header-level={rozieAttr(hgLevel())} colSpan={rozieAttr(header().colSpan > 1 ? header().colSpan : null)} data-col-index={rozieAttr(headerColIndexOf(hg(), header()))} aria-colindex={rozieAttr(headerLeafStart(hg(), header()) + 1)} tabIndex={rozieAttr(cellTabindex('__header', headerColIndexOf(hg(), header()), hgLevel()))} aria-sort={rozieAttr(ariaSortFor(header().column.id))} style={parseInlineStyle(thStyle(header()))} data-rozie-s-d5dcab4c="">
7603
8695
 
7604
8696
 
7605
8697
  {<Show when={isSelectColumn(header().column.id)} fallback={<Show when={isExpanderColumn(header().column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -7631,10 +8723,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7631
8723
  </tr>}</Key>
7632
8724
 
7633
8725
  {<Show when={hasAnyFilterableColumn()}><tr class={"rdt-filter-row"} data-rozie-s-d5dcab4c="">
7634
- <Key each={headerGroups()[headerGroups().length - 1].headers as readonly any[]} by={(header) => header.id}>{(header) => <th class={"rdt-filter-cell"} role="presentation" style={parseInlineStyle(pinStyle(header().column.id))} data-rozie-s-d5dcab4c="">
8726
+ <Key each={headerGroups()[headerGroups().length - 1].headers as readonly any[]} by={(header) => header.id}>{(header) => <th class={"rdt-filter-cell" + " " + rozieClass({ 'rdt-pinned': columnPinSide(header().column.id) !== '' })} role="presentation" style={parseInlineStyle(pinStyle(header().column.id))} data-rozie-s-d5dcab4c="">
7635
8727
  {<Show when={isSelectColumn(header().column.id)} fallback={<Show when={isExpanderColumn(header().column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7636
8728
  {<Show when={columnIsFilterable(header().column.id)}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7637
- {_props.slots?.[`filter-${header().column.id}`]?.({ get columnId() { return header().column.id; }, get value() { return columnFilterValue(header().column.id); }, get uniqueValues() { return getFacetedUniqueValues(header().column.id); }, get minMax() { return getFacetedMinMaxValues(header().column.id); }, get setFilter() { return setColumnFilter; } }) ?? (_props.filterSlot ?? _props.slots?.['filter'])?.({ get columnId() { return header().column.id; }, get value() { return columnFilterValue(header().column.id); }, get uniqueValues() { return getFacetedUniqueValues(header().column.id); }, get minMax() { return getFacetedMinMaxValues(header().column.id); }, get setFilter() { return setColumnFilter; } }) ?? <input type="text" aria-label={rozieAttr('Filter ' + headerLabel(header().column.id))} class={"rdt-col-filter"} value={columnFilterValue(header().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onColumnFilterInput(header().column.id, $event); }} onClick={($event: MouseEvent & { currentTarget: HTMLInputElement; target: Element }) => { stopEvent($event); }} data-rozie-s-d5dcab4c="" />}
8729
+ {_props.slots?.[`filter-${header().column.id}`]?.({ get columnId() { return header().column.id; }, get value() { return columnFilterValue(header().column.id); }, get uniqueValues() { return getFacetedUniqueValues(header().column.id); }, get minMax() { return getFacetedMinMaxValues(header().column.id); }, get columnLabel() { return headerLabel(header().column.id); }, get setFilter() { return setColumnFilter; } }) ?? (_props.filterSlot ?? _props.slots?.['filter'])?.({ get columnId() { return header().column.id; }, get value() { return columnFilterValue(header().column.id); }, get uniqueValues() { return getFacetedUniqueValues(header().column.id); }, get minMax() { return getFacetedMinMaxValues(header().column.id); }, get columnLabel() { return headerLabel(header().column.id); }, get setFilter() { return setColumnFilter; } }) ?? <input type="text" aria-label={rozieAttr('Filter ' + headerLabel(header().column.id))} class={"rdt-col-filter"} value={columnFilterValue(header().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onColumnFilterInput(header().column.id, $event); }} onClick={($event: MouseEvent & { currentTarget: HTMLInputElement; target: Element }) => { stopEvent($event); }} data-rozie-s-d5dcab4c="" />}
7638
8730
  </span></Show>}</span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}</th>}</Key>
7639
8731
  </tr></Show>}</thead>
7640
8732
 
@@ -7642,16 +8734,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7642
8734
 
7643
8735
  <Key each={rows() as readonly any[]} by={(row) => row.id}>{(row) => <>
7644
8736
  <tr class={"rdt-tr" + " " + rozieClass({ 'rdt-group-header': rowIsGrouped(row()) })} role="row" data-depth={rozieAttr(row().depth)} aria-rowindex={rozieAttr(bodyAriaRowIndex(row()))} data-group-header={rozieAttr(rowIsGrouped(row()) ? row().id : null)} data-group-leaf={rozieAttr(groupingActive() && !rowIsGrouped(row()) ? row().id : null)} aria-expanded={(rowIsGrouped(row()) ? !!rowIsExpanded(row()) : null) ?? undefined} aria-selected={(local.selectionMode !== 'none' ? !!rowIsSelected(row()) : null) ?? undefined} aria-level={rozieAttr(groupingActive() ? row().depth + 1 : null)} data-rozie-s-d5dcab4c="">
7645
- <Key each={visibleCellsFor(row()) as readonly any[]} by={(cell) => cell.id}>{(cell) => <td class={"rdt-td" + " " + rozieClass({ 'rdt-select-td': isSelectColumn(cell().column.id), 'rdt-expander-td': isExpanderColumn(cell().column.id), 'rdt-in-range': inRange(rowIndexOf(row()), colIndexOf(row(), cell())), 'rdt-cell-active': isActiveCell(String(rowIndexOf(row())), colIndexOf(row(), cell())) })} role={rozieAttr(cellRole())} data-col={rozieAttr(cell().column.id)} data-grid-cell="" data-row={rozieAttr(rowIndexOf(row()))} data-col-index={rozieAttr(colIndexOf(row(), cell()))} tabIndex={rozieAttr(cellTabindex(String(rowIndexOf(row())), colIndexOf(row(), cell())))} style={parseInlineStyle(bodyCellStyle(row(), cell().column.id))} aria-invalid={rozieAttr(cellAriaInvalid(rowIndexOf(row()), colIndexOf(row(), cell())))} data-in-range={rozieAttr(inRange(rowIndexOf(row()), colIndexOf(row(), cell())) ? 'true' : null)} data-agg-cell={rozieAttr(cellIsAggregated(cell()) ? cell().column.id : null)} data-rozie-s-d5dcab4c="">
8737
+ <Key each={visibleCellsFor(row()) as readonly any[]} by={(cell) => cell.id}>{(cell) => <td class={"rdt-td" + " " + rozieClass({ 'rdt-select-td': isSelectColumn(cell().column.id), 'rdt-expander-td': isExpanderColumn(cell().column.id), 'rdt-pinned': columnPinSide(cell().column.id) !== '', 'rdt-in-range': inRange(rowIndexOf(row()), colIndexOf(row(), cell())), 'rdt-cell-active': isActiveCell(String(rowIndexOf(row())), colIndexOf(row(), cell())) })} role={rozieAttr(cellRole())} data-col={rozieAttr(cell().column.id)} data-grid-cell="" data-row={rozieAttr(rowIndexOf(row()))} data-col-index={rozieAttr(colIndexOf(row(), cell()))} tabIndex={rozieAttr(cellTabindex(String(rowIndexOf(row())), colIndexOf(row(), cell())))} style={parseInlineStyle(bodyCellStyle(row(), cell().column.id))} aria-invalid={rozieAttr(cellAriaInvalid(rowIndexOf(row()), colIndexOf(row(), cell())))} aria-colindex={rozieAttr(colIndexOf(row(), cell()) + 1)} aria-selected={(inRange(rowIndexOf(row()), colIndexOf(row(), cell())) ? 'true' : null) ?? undefined} data-in-range={rozieAttr(inRange(rowIndexOf(row()), colIndexOf(row(), cell())) ? 'true' : null)} data-agg-cell={rozieAttr(cellIsAggregated(cell()) ? cell().column.id : null)} data-rozie-s-d5dcab4c="">
7646
8738
 
7647
8739
  {<Show when={isExpanderColumn(cell().column.id)} fallback={<Show when={isSelectColumn(cell().column.id)} fallback={<Show when={cellIsGrouped(cell())} fallback={<Show when={isEditing(rowIndexOf(row()), colIndexOf(row(), cell()))} fallback={<Show when={cellIsPlaceholder(cell())} fallback={<span class={"rdt-cell-value"} data-rozie-s-d5dcab4c="">
7648
8740
  {_props.slots?.[`cell-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return cell().getValue(); } }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return cell().getValue(); } }) ?? rozieDisplay(cell().getValue())}
7649
8741
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7650
- {<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<Show when={editorTypeOf(cell().column.id) === 'custom'} fallback={<input type="text" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7651
- {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
7652
- </span></Show>}><input type="checkbox" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
8742
+ {<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<Show when={editorTypeOf(cell().column.id) === 'custom'} fallback={<input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><span style={{ display: "contents" }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c="">
8743
+ {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
8744
+ </span></Show>}><input type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
7653
8745
  <Key each={editorOptionsOf(cell().column.id) as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-d5dcab4c="">{rozieDisplay(opt().label)}</option>}</Key>
7654
- </select></Show>}><input type="number" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
8746
+ </select></Show>}><input type="number" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7655
8747
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(row())} aria-label={rozieAttr(rowIsExpanded(row()) ? 'Collapse group' : 'Expand group')} class={"rdt-expander rdt-group-toggle"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(row(), $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(row()) ? '▾' : '▸')}</button>
7656
8748
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
7657
8749
  {_props.slots?.[`cell-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return cell().getValue(); } }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return cell().getValue(); } }) ?? rozieDisplay(cell().getValue())}
@@ -7670,11 +8762,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7670
8762
  </tr></Show>}</>}</Key>
7671
8763
  </tbody>
7672
8764
  </table>}><div class={"rdt-scroll"} style={parseInlineStyle(rowsWindowed() && local.maxHeight ? 'max-height:' + local.maxHeight + ';overflow:auto;--rozie-data-table-max-height:' + local.maxHeight : 'overflow:auto')} data-rozie-s-d5dcab4c="">
7673
- <table aria-rowcount={rozieAttr(gridAriaRowCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader, 'rdt-col-windowed': colsWindowed() })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
8765
+ <table aria-rowcount={rozieAttr(gridAriaRowCount())} aria-colcount={rozieAttr(gridAriaColCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader, 'rdt-col-windowed': colsWindowed() })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
7674
8766
  <thead class={"rdt-thead"} role="rowgroup" data-rozie-s-d5dcab4c="">
7675
8767
  <Key each={headerGroups() as readonly any[]} by={(hg) => hg.id}>{(hg, hgLevel) => <tr class={"rdt-tr"} role="row" aria-rowindex={rozieAttr(hgLevel() + 1)} data-rozie-s-d5dcab4c="">
7676
8768
 
7677
- {<Show when={colsWindowed()}><th class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedHeadersFor(hg(), hgLevel()) as readonly any[]} by={(wh) => wh.header.id}>{(wh) => <th class={"rdt-th" + " " + rozieClass({ 'rdt-select-th': isSelectColumn(wh().header.column.id), 'rdt-expander-th': isExpanderColumn(wh().header.column.id), 'rdt-th-resizing': columnIsResizing(wh().header.column.id), 'rdt-cell-active': isActiveCell('__header', headerColIndexOf(hg(), wh().header), hgLevel()) })} role="columnheader" data-col={rozieAttr(wh().header.column.id)} data-grid-cell="" data-row="__header" data-header-level={rozieAttr(hgLevel())} colSpan={rozieAttr(wh().span > 1 ? wh().span : null)} data-col-index={rozieAttr(headerColIndexOf(hg(), wh().header))} tabIndex={rozieAttr(cellTabindex('__header', headerColIndexOf(hg(), wh().header), hgLevel()))} aria-sort={rozieAttr(ariaSortFor(wh().header.column.id))} style={parseInlineStyle(thStyle(wh().header))} data-rozie-s-d5dcab4c="">
8769
+ {<Show when={colsWindowed()}><th class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedHeadersFor(hg(), hgLevel()) as readonly any[]} by={(wh) => wh.header.id}>{(wh) => <th class={"rdt-th" + " " + rozieClass({ 'rdt-select-th': isSelectColumn(wh().header.column.id), 'rdt-expander-th': isExpanderColumn(wh().header.column.id), 'rdt-pinned': columnPinSide(wh().header.column.id) !== '', 'rdt-th-resizing': columnIsResizing(wh().header.column.id), 'rdt-cell-active': isActiveCell('__header', headerColIndexOf(hg(), wh().header), hgLevel()) })} role="columnheader" data-col={rozieAttr(wh().header.column.id)} data-grid-cell="" data-row="__header" data-header-level={rozieAttr(hgLevel())} colSpan={rozieAttr(wh().span > 1 ? wh().span : null)} data-col-index={rozieAttr(headerColIndexOf(hg(), wh().header))} aria-colindex={rozieAttr(headerLeafStart(hg(), wh().header) + 1)} tabIndex={rozieAttr(cellTabindex('__header', headerColIndexOf(hg(), wh().header), hgLevel()))} aria-sort={rozieAttr(ariaSortFor(wh().header.column.id))} style={parseInlineStyle(thStyle(wh().header, wh().width))} data-rozie-s-d5dcab4c="">
7678
8770
  {<Show when={isSelectColumn(wh().header.column.id)} fallback={<Show when={isExpanderColumn(wh().header.column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7679
8771
  {<Show when={wh().header.column.getCanSort && wh().header.column.getCanSort()} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7680
8772
  <span class={"rdt-header-label"} data-rozie-s-d5dcab4c="">
@@ -7702,10 +8794,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7702
8794
  {<Show when={colsWindowed()}><th class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}</tr>}</Key>
7703
8795
 
7704
8796
  {<Show when={hasAnyFilterableColumn()}><tr class={"rdt-filter-row"} data-rozie-s-d5dcab4c="">
7705
- {<Show when={colsWindowed()}><th class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedHeadersFor(headerGroups()[headerGroups().length - 1], headerGroups().length - 1) as readonly any[]} by={(wh) => wh.header.id}>{(wh) => <th class={"rdt-filter-cell"} role="presentation" data-col={rozieAttr(wh().header.column.id)} style={parseInlineStyle(pinStyle(wh().header.column.id))} data-rozie-s-d5dcab4c="">
8797
+ {<Show when={colsWindowed()}><th class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedHeadersFor(headerGroups()[headerGroups().length - 1], headerGroups().length - 1) as readonly any[]} by={(wh) => wh.header.id}>{(wh) => <th class={"rdt-filter-cell" + " " + rozieClass({ 'rdt-pinned': columnPinSide(wh().header.column.id) !== '' })} role="presentation" data-col={rozieAttr(wh().header.column.id)} style={parseInlineStyle(pinStyle(wh().header.column.id))} data-rozie-s-d5dcab4c="">
7706
8798
  {<Show when={isSelectColumn(wh().header.column.id)} fallback={<Show when={isExpanderColumn(wh().header.column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7707
8799
  {<Show when={columnIsFilterable(wh().header.column.id)}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7708
- {_props.slots?.[`filter-${wh().header.column.id}`]?.({ get columnId() { return wh().header.column.id; }, get value() { return columnFilterValue(wh().header.column.id); }, get uniqueValues() { return getFacetedUniqueValues(wh().header.column.id); }, get minMax() { return getFacetedMinMaxValues(wh().header.column.id); }, get setFilter() { return setColumnFilter; } }) ?? (_props.filterSlot ?? _props.slots?.['filter'])?.({ get columnId() { return wh().header.column.id; }, get value() { return columnFilterValue(wh().header.column.id); }, get uniqueValues() { return getFacetedUniqueValues(wh().header.column.id); }, get minMax() { return getFacetedMinMaxValues(wh().header.column.id); }, get setFilter() { return setColumnFilter; } }) ?? <input type="text" aria-label={rozieAttr('Filter ' + headerLabel(wh().header.column.id))} class={"rdt-col-filter"} value={columnFilterValue(wh().header.column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onColumnFilterInput(wh().header.column.id, $event); }} onClick={($event: MouseEvent & { currentTarget: HTMLInputElement; target: Element }) => { stopEvent($event); }} data-rozie-s-d5dcab4c="" />}
8800
+ {_props.slots?.[`filter-${wh().header.column.id}`]?.({ get columnId() { return wh().header.column.id; }, get value() { return columnFilterValue(wh().header.column.id); }, get uniqueValues() { return getFacetedUniqueValues(wh().header.column.id); }, get minMax() { return getFacetedMinMaxValues(wh().header.column.id); }, get columnLabel() { return headerLabel(wh().header.column.id); }, get setFilter() { return setColumnFilter; } }) ?? (_props.filterSlot ?? _props.slots?.['filter'])?.({ get columnId() { return wh().header.column.id; }, get value() { return columnFilterValue(wh().header.column.id); }, get uniqueValues() { return getFacetedUniqueValues(wh().header.column.id); }, get minMax() { return getFacetedMinMaxValues(wh().header.column.id); }, get columnLabel() { return headerLabel(wh().header.column.id); }, get setFilter() { return setColumnFilter; } }) ?? <input type="text" aria-label={rozieAttr('Filter ' + headerLabel(wh().header.column.id))} class={"rdt-col-filter"} value={columnFilterValue(wh().header.column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onColumnFilterInput(wh().header.column.id, $event); }} onClick={($event: MouseEvent & { currentTarget: HTMLInputElement; target: Element }) => { stopEvent($event); }} data-rozie-s-d5dcab4c="" />}
7709
8801
  </span></Show>}</span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}</th>}</Key>
7710
8802
  {<Show when={colsWindowed()}><th class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}</tr></Show>}</thead>
7711
8803
 
@@ -7716,18 +8808,19 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7716
8808
  </tr>
7717
8809
 
7718
8810
  <Key each={windowedRows() as readonly any[]} by={(wr) => wr.row.id}>{(wr) => <>
7719
- <tr class={"rdt-tr" + " " + rozieClass({ 'rdt-group-header': rowIsGrouped(wr().row), 'rdt-row-pinned': wr().pinned })} role="row" data-row={rozieAttr(wr().vi.index)} aria-rowindex={rozieAttr(headerRowCount() + wr().vi.index + 1)} data-index={rozieAttr(wr().vi.index)} data-pinned={rozieAttr(wr().pinned ? 'true' : null)} data-depth={rozieAttr(wr().row.depth)} data-group-header={rozieAttr(rowIsGrouped(wr().row) ? wr().row.id : null)} data-group-leaf={rozieAttr(groupingActive() && !rowIsGrouped(wr().row) ? wr().row.id : null)} aria-expanded={(rowIsGrouped(wr().row) ? !!rowIsExpanded(wr().row) : null) ?? undefined} aria-selected={(local.selectionMode !== 'none' ? !!rowIsSelected(wr().row) : null) ?? undefined} aria-level={rozieAttr(groupingActive() ? wr().row.depth + 1 : null)} data-rozie-s-d5dcab4c="">
8811
+ <tr class={"rdt-tr" + " " + rozieClass({ 'rdt-group-header': rowIsGrouped(wr().row), 'rdt-row-pinned': wr().pinned, 'rdt-placeholder-row': rowIsLazyPlaceholder(wr().row) })} role="row" data-row={rozieAttr(wr().vi.index)} aria-rowindex={rozieAttr(headerRowCount() + wr().vi.index + 1)} data-index={rozieAttr(wr().vi.index)} data-pinned={rozieAttr(wr().pinned ? 'true' : null)} data-depth={rozieAttr(wr().row.depth)} data-group-header={rozieAttr(rowIsGrouped(wr().row) ? wr().row.id : null)} data-group-leaf={rozieAttr(groupingActive() && !rowIsGrouped(wr().row) ? wr().row.id : null)} aria-expanded={(rowIsGrouped(wr().row) ? !!rowIsExpanded(wr().row) : null) ?? undefined} aria-selected={(local.selectionMode !== 'none' ? !!rowIsSelected(wr().row) : null) ?? undefined} aria-level={rozieAttr(groupingActive() ? wr().row.depth + 1 : null)} aria-busy={(rowIsLazyPlaceholder(wr().row) ? 'true' : null) ?? undefined} data-rozie-s-d5dcab4c="">
7720
8812
 
7721
- {<Show when={colsWindowed()}><td class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedCells(wr().row) as readonly any[]} by={(cell) => cell.id}>{(cell) => <td class={"rdt-td" + " " + rozieClass({ 'rdt-select-td': isSelectColumn(cell().column.id), 'rdt-expander-td': isExpanderColumn(cell().column.id), 'rdt-in-range': inRange(wr().vi.index, colIndexOf(wr().row, cell())), 'rdt-cell-active': isActiveCell(String(wr().vi.index), colIndexOf(wr().row, cell())) })} role={rozieAttr(cellRole())} data-col={rozieAttr(cell().column.id)} data-grid-cell="" data-row={rozieAttr(wr().vi.index)} data-col-index={rozieAttr(colIndexOf(wr().row, cell()))} tabIndex={rozieAttr(cellTabindex(String(wr().vi.index), colIndexOf(wr().row, cell())))} style={parseInlineStyle(bodyCellStyle(wr().row, cell().column.id))} aria-invalid={rozieAttr(cellAriaInvalid(wr().vi.index, colIndexOf(wr().row, cell())))} data-in-range={rozieAttr(inRange(wr().vi.index, colIndexOf(wr().row, cell())) ? 'true' : null)} data-agg-cell={rozieAttr(cellIsAggregated(cell()) ? cell().column.id : null)} data-rozie-s-d5dcab4c="">
8813
+ {<Show when={colsWindowed()}><td class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedCells(wr().row) as readonly any[]} by={(cell) => cell.id}>{(cell) => <td class={"rdt-td" + " " + rozieClass({ 'rdt-select-td': isSelectColumn(cell().column.id), 'rdt-expander-td': isExpanderColumn(cell().column.id), 'rdt-pinned': columnPinSide(cell().column.id) !== '', 'rdt-in-range': inRange(wr().vi.index, colIndexOf(wr().row, cell())), 'rdt-cell-active': isActiveCell(String(wr().vi.index), colIndexOf(wr().row, cell())) })} role={rozieAttr(cellRole())} data-col={rozieAttr(cell().column.id)} data-grid-cell="" data-row={rozieAttr(wr().vi.index)} data-col-index={rozieAttr(colIndexOf(wr().row, cell()))} tabIndex={rozieAttr(cellTabindex(String(wr().vi.index), colIndexOf(wr().row, cell())))} style={parseInlineStyle(bodyCellStyle(wr().row, cell().column.id))} aria-invalid={rozieAttr(cellAriaInvalid(wr().vi.index, colIndexOf(wr().row, cell())))} aria-colindex={rozieAttr(colIndexOf(wr().row, cell()) + 1)} aria-selected={(inRange(wr().vi.index, colIndexOf(wr().row, cell())) ? 'true' : null) ?? undefined} data-in-range={rozieAttr(inRange(wr().vi.index, colIndexOf(wr().row, cell())) ? 'true' : null)} data-agg-cell={rozieAttr(cellIsAggregated(cell()) ? cell().column.id : null)} data-rozie-s-d5dcab4c="">
7722
8814
 
7723
- {<Show when={isExpanderColumn(cell().column.id)} fallback={<Show when={isSelectColumn(cell().column.id)} fallback={<Show when={cellIsGrouped(cell())} fallback={<Show when={isEditing(wr().vi.index, colIndexOf(wr().row, cell()))} fallback={<Show when={cellIsPlaceholder(cell())} fallback={<span class={"rdt-cell-value"} data-rozie-s-d5dcab4c="">
8815
+
8816
+ {<Show when={rowIsLazyPlaceholder(wr().row) && isSelectColumn(cell().column.id)} fallback={<Show when={rowIsLazyPlaceholder(wr().row) && isExpanderColumn(cell().column.id)} fallback={<Show when={rowIsLazyPlaceholder(wr().row)} fallback={<Show when={isExpanderColumn(cell().column.id)} fallback={<Show when={isSelectColumn(cell().column.id)} fallback={<Show when={cellIsGrouped(cell())} fallback={<Show when={isEditing(wr().vi.index, colIndexOf(wr().row, cell()))} fallback={<Show when={cellIsPlaceholder(cell())} fallback={<span class={"rdt-cell-value"} data-rozie-s-d5dcab4c="">
7724
8817
  {_props.slots?.[`cell-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return cell().getValue(); } }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return cell().getValue(); } }) ?? rozieDisplay(cell().getValue())}
7725
8818
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7726
- {<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<Show when={editorTypeOf(cell().column.id) === 'custom'} fallback={<input type="text" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7727
- {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
7728
- </span></Show>}><input type="checkbox" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
8819
+ {<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<Show when={editorTypeOf(cell().column.id) === 'custom'} fallback={<input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><span style={{ display: "contents" }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c="">
8820
+ {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
8821
+ </span></Show>}><input type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
7729
8822
  <Key each={editorOptionsOf(cell().column.id) as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-d5dcab4c="">{rozieDisplay(opt().label)}</option>}</Key>
7730
- </select></Show>}><input type="number" data-editing-cell="" data-builtin-editor="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
8823
+ </select></Show>}><input type="number" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7731
8824
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={rozieAttr(rowIsExpanded(wr().row) ? 'Collapse group' : 'Expand group')} class={"rdt-expander rdt-group-toggle"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(wr().row, $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(wr().row) ? '▾' : '▸')}</button>
7732
8825
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
7733
8826
  {_props.slots?.[`cell-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return cell().getValue(); } }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return cell().getValue(); } }) ?? rozieDisplay(cell().getValue())}
@@ -7736,7 +8829,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7736
8829
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7737
8830
  {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ get row() { return cellSlotRow(wr().row); }, get checked() { return rowIsSelected(wr().row); }, toggle: e => onToggleRow(wr().row, e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(wr().row)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(wr().row, $event); }} data-rozie-s-d5dcab4c="" />}
7738
8831
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7739
- {<Show when={rowCanExpand(wr().row)}><button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={rozieAttr(rowIsExpanded(wr().row) ? 'Collapse row' : 'Expand row')} class={"rdt-expander"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(wr().row, $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(wr().row) ? '▾' : '▸')}</button></Show>}</span></Show>}{<Show when={isFillHandleCell(wr().vi.index, colIndexOf(wr().row, cell()))}><span data-fill-handle="" data-testid="fill-handle" aria-hidden="true" class={"rdt-fill-handle"} onPointerDown={($event: PointerEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c="" /></Show>}</td>}</Key>
8832
+ {<Show when={rowCanExpand(wr().row)}><button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={rozieAttr(rowIsExpanded(wr().row) ? 'Collapse row' : 'Expand row')} class={"rdt-expander"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(wr().row, $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(wr().row) ? '▾' : '▸')}</button></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
8833
+ {(_props.placeholderSlot ?? _props.slots?.['placeholder'])?.({ get index() { return wr().vi.index; }, get columnId() { return cell().column.id; } }) ?? <span class={"rdt-skeleton"} aria-hidden="true" data-rozie-s-d5dcab4c="">&#8203;</span>}
8834
+ </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
8835
+
8836
+ <input class={"rdt-select-placeholder"} type="checkbox" disabled={true} tabIndex={-1} aria-hidden="true" style={{ visibility: "hidden" }} data-rozie-s-d5dcab4c="" />
8837
+ </span></Show>}{<Show when={isFillHandleCell(wr().vi.index, colIndexOf(wr().row, cell()))}><span data-fill-handle="" data-testid="fill-handle" aria-hidden="true" class={"rdt-fill-handle"} onPointerDown={($event: PointerEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c="" /></Show>}</td>}</Key>
7740
8838
 
7741
8839
  {<Show when={colsWindowed()}><td class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}</tr>
7742
8840