@rozie-ui/data-table-solid 0.4.0 → 0.6.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,7 +35,42 @@ 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
- __rozieInjectStyle('DataTable-d5dcab4c', `.rozie-data-table[data-rozie-s-d5dcab4c] {
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
+
47
+ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
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));
72
+ }
73
+ .rozie-data-table[data-rozie-s-d5dcab4c] {
39
74
  border-collapse: collapse;
40
75
  width: 100%;
41
76
  font: var(--rdt-font, 14px system-ui, sans-serif);
@@ -161,6 +196,16 @@ __rozieInjectStyle('DataTable-d5dcab4c', `.rozie-data-table[data-rozie-s-d5dcab4
161
196
  flex-direction: column;
162
197
  gap: var(--rdt-chrome-gap, 0.5rem);
163
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
+ }
164
209
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-toolbar[data-rozie-s-d5dcab4c] {
165
210
  display: flex;
166
211
  gap: var(--rdt-toolbar-gap, 0.5rem);
@@ -186,6 +231,9 @@ __rozieInjectStyle('DataTable-d5dcab4c', `.rozie-data-table[data-rozie-s-d5dcab4
186
231
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] {
187
232
  background: var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015));
188
233
  }
234
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
235
+ box-sizing: border-box;
236
+ }
189
237
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
190
238
  padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
191
239
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
@@ -305,6 +353,9 @@ __rozieInjectStyle('DataTable-d5dcab4c', `.rozie-data-table[data-rozie-s-d5dcab4
305
353
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-colvis-summary[data-rozie-s-d5dcab4c] {
306
354
  cursor: pointer;
307
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;
308
359
  padding: var(--rdt-colvis-summary-padding, 0.25rem 0.6rem);
309
360
  border: var(--rdt-colvis-summary-border, 1px solid rgba(0, 0, 0, 0.2));
310
361
  border-radius: var(--rdt-colvis-summary-radius, 4px);
@@ -343,6 +394,51 @@ __rozieInjectStyle('DataTable-d5dcab4c', `.rozie-data-table[data-rozie-s-d5dcab4
343
394
  text-align: var(--rdt-expander-col-align, center);
344
395
  white-space: nowrap;
345
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
+ }
346
442
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-all[data-rozie-s-d5dcab4c],
347
443
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-row[data-rozie-s-d5dcab4c] {
348
444
  cursor: pointer;
@@ -353,21 +449,23 @@ interface GroupBarSlotCtx { grouping: any; groupableColumns: any; applyGrouping:
353
449
 
354
450
  interface SelectAllSlotCtx { checked: any; indeterminate: any; toggle: any; }
355
451
 
452
+ interface PlaceholderSlotCtx { index: any; columnId: any; }
453
+
356
454
  interface SelectCellSlotCtx { row: any; checked: any; toggle: any; }
357
455
 
358
456
  interface DetailSlotCtx { row: any; }
359
457
 
360
458
  interface ColHeaderSlotCtx { columnId: any; column: any; label: any; }
361
459
 
362
- 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; }
363
461
 
364
462
  interface CellSlotCtx { columnId: any; column: any; row: any; value: any; }
365
463
 
366
- 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; }
367
465
 
368
466
  interface DataTableProps {
369
467
  /**
370
- * The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `r-model:data` (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
468
+ * The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
371
469
  * @example
372
470
  * <DataTable data={rows()} onDataChange={setRows} columns={cols} />
373
471
  */
@@ -375,7 +473,7 @@ interface DataTableProps {
375
473
  defaultData?: any[];
376
474
  onDataChange?: (data: any[]) => void;
377
475
  /**
378
- * Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union).
476
+ * Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
379
477
  */
380
478
  columns?: any[];
381
479
  /**
@@ -419,7 +517,7 @@ interface DataTableProps {
419
517
  */
420
518
  pageCount?: (number) | null;
421
519
  /**
422
- * Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Bind `:expandable="true"` (a bare attr only coerces on Vue+Lit).
520
+ * Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit.
423
521
  */
424
522
  expandable?: boolean;
425
523
  /**
@@ -428,6 +526,10 @@ interface DataTableProps {
428
526
  expanded?: (Record<string, any> | boolean) | null;
429
527
  defaultExpanded?: (Record<string, any> | boolean) | null;
430
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;
431
533
  /**
432
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.
433
535
  */
@@ -437,7 +539,7 @@ interface DataTableProps {
437
539
  */
438
540
  groupable?: boolean;
439
541
  /**
440
- * `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `r-model:grouping` (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
542
+ * `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
441
543
  */
442
544
  grouping?: (any[]) | null;
443
545
  defaultGrouping?: (any[]) | null;
@@ -477,7 +579,7 @@ interface DataTableProps {
477
579
  */
478
580
  stickyHeader?: boolean;
479
581
  /**
480
- * `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` (GA since Phase 63) — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
582
+ * `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
481
583
  */
482
584
  interactionMode?: string;
483
585
  /**
@@ -519,7 +621,9 @@ interface DataTableProps {
519
621
  onReorderChange?: (...args: unknown[]) => void;
520
622
  onPinChange?: (...args: unknown[]) => void;
521
623
  onHistoryChange?: (...args: unknown[]) => void;
624
+ onVisibleRangeChange?: (...args: unknown[]) => void;
522
625
  onActivecellChange?: (...args: unknown[]) => void;
626
+ onRowActivate?: (...args: unknown[]) => void;
523
627
  onRangeChange?: (...args: unknown[]) => void;
524
628
  onCellEditCommit?: (...args: unknown[]) => void;
525
629
  onRowEditCommit?: (...args: unknown[]) => void;
@@ -527,13 +631,14 @@ interface DataTableProps {
527
631
  children?: JSX.Element;
528
632
  groupBarSlot?: (ctx: GroupBarSlotCtx) => JSX.Element;
529
633
  selectAllSlot?: (ctx: SelectAllSlotCtx) => JSX.Element;
634
+ placeholderSlot?: (ctx: PlaceholderSlotCtx) => JSX.Element;
530
635
  selectCellSlot?: (ctx: SelectCellSlotCtx) => JSX.Element;
531
636
  detailSlot?: (ctx: DetailSlotCtx) => JSX.Element;
532
637
  colHeaderSlot?: (ctx: ColHeaderSlotCtx) => JSX.Element;
533
638
  filterSlot?: (ctx: FilterSlotCtx) => JSX.Element;
534
639
  cellSlot?: (ctx: CellSlotCtx) => JSX.Element;
535
640
  editorSlot?: (ctx: EditorSlotCtx) => JSX.Element;
536
- 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; };
537
642
  ref?: (h: DataTableHandle) => void;
538
643
  }
539
644
 
@@ -575,8 +680,8 @@ export interface DataTableHandle {
575
680
  }
576
681
 
577
682
  export default function DataTable(_props: DataTableProps): JSX.Element {
578
- 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);
579
- 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']);
683
+ 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);
684
+ 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']);
580
685
  const resolved = () => local.children;
581
686
  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 }); });
582
687
 
@@ -646,6 +751,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
646
751
  const [rangeAnchor, setRangeAnchor] = createSignal<any>(null);
647
752
  const [rangeFocus, setRangeFocus] = createSignal<any>(null);
648
753
  const [pasteAnnounce, setPasteAnnounce] = createSignal('');
754
+ const [rangeAnnounce, setRangeAnnounce] = createSignal('');
649
755
  const [liveAnnounce, setLiveAnnounce] = createSignal('');
650
756
  onMount(() => {
651
757
  // Seed the uncontrolled `data` fallback (Phase 51 req-4) from the initial prop so an
@@ -653,6 +759,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
653
759
  // a one-way `:data`) has a base array to whole-array-replace. currentData() then sources
654
760
  // the bound prop when controlled, this fallback otherwise.
655
761
  setDataDefault(data() || []);
762
+ // Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
763
+ // reads currentState() below, so the first row model table-core builds is already correctly
764
+ // ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
765
+ // re-ordering a render later. Covers the `:columns` config-array form, which is available
766
+ // now; `<Column>` children register during mount and are covered by the re-feed watch's own
767
+ // call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
768
+ seedColumnPinning();
656
769
  // Build the table instance HERE so the closures below capture the live `table`.
657
770
  table = createTable({
658
771
  // Plain value (NOT a `get data()` getter): an object-literal getter rebinds
@@ -663,7 +776,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
663
776
  // the getter bought nothing. Snapshot the initial data here; setOptions owns updates.
664
777
  // currentData() = the bound prop when controlled, else the uncontrolled $data.dataDefault
665
778
  // (Phase 51 req-4 — so a committed edit's writeData re-feed is observed either way).
666
- data: currentData(),
779
+ data: feedData(),
667
780
  columns: tableColumns(),
668
781
  state: currentState(),
669
782
  getCoreRowModel: getCoreRowModel(),
@@ -678,7 +791,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
678
791
  // default `!!subRows.length` rule applies (only parents with children expand).
679
792
  getExpandedRowModel: getExpandedRowModel(),
680
793
  getSubRows: (local.getSubRows || undefined) as any,
681
- getRowCanExpand: local.expandable === true && local.getSubRows == null ? () => true : undefined,
794
+ // Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
795
+ // other row-model fns.
796
+ getRowId: rowIdOption(),
797
+ getRowCanExpand: local.expandable === true && local.getSubRows == null ? (row: any) => !rowIsLazyPlaceholder(row) : undefined,
682
798
  onExpandedChange: onExpandedChangeCb,
683
799
  // Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
684
800
  // POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
@@ -723,7 +839,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
723
839
  // Row selection (req-7): enabled unless 'none'; 'single' caps at ≤1
724
840
  // (enableMultiRowSelection:false). Select-all scope = filtered rows (TanStack
725
841
  // default, D-06 — NOT overridden).
726
- enableRowSelection: local.selectionMode !== 'none',
842
+ enableRowSelection: rowSelectionOption(),
727
843
  enableMultiRowSelection: local.selectionMode === 'multiple',
728
844
  // PER-SLICE callbacks (Open-Q1: each maps 1:1 to a slice's r-model + change event,
729
845
  // no global onStateChange diff) — hoisted top-level consts, re-passed by the re-feed
@@ -767,6 +883,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
767
883
  // NEVER in a render helper (Pitfall 1). Pass the COMPLETE options set (virtual-core's
768
884
  // setOptions replaces, not merges). Guarded so the off path executes no virtual-core code.
769
885
  if (rowsWindowed() && virtualizer) {
886
+ // Rekey any placeholder rows this re-feed just filled (quick 260926, VR probe) BEFORE
887
+ // handing the virtualizer its fresh options — see migratePlaceholderSizeCache()'s own
888
+ // comment (virtualization.rzts) for the itemSizeCache/getMeasurements memo root cause.
889
+ migratePlaceholderSizeCache();
770
890
  virtualizer.setOptions(virtualizerOptions());
771
891
  virtualizer._willUpdate();
772
892
  }
@@ -837,6 +957,21 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
837
957
  // cell selectors hang off (the exact idiom proven ×6 by plan 01's probe). Captured here
838
958
  // (post-mount) so it is non-null and ROZ123-clean.
839
959
  gridRoot = __rozieRootRef! ? __rozieRootRef!.querySelector('.rozie-data-table') : null;
960
+ // B-02: the click-away detector. `composedPath()` rather than `ev.target` because on Lit the
961
+ // grid lives in a shadow root and a document-level listener sees the retargeted HOST element,
962
+ // which is never `gridRoot.contains(...)` — every in-grid click would read as "outside".
963
+ if (typeof document !== 'undefined') {
964
+ docPointerDown = (ev: any) => {
965
+ if (!gridRoot) {
966
+ outsidePointerDown = false;
967
+ return;
968
+ }
969
+ const path = ev && typeof ev.composedPath === 'function' ? ev.composedPath() : null;
970
+ const inside = path ? path.indexOf(gridRoot) !== -1 : !!(ev && ev.target && gridRoot.contains && gridRoot.contains(ev.target));
971
+ outsidePointerDown = !inside;
972
+ };
973
+ document.addEventListener('pointerdown', docPointerDown, true);
974
+ }
840
975
  // WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
841
976
  // page load AND was non-deterministic on React/Solid (the entry cell may not be
842
977
  // committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
@@ -928,6 +1063,31 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
928
1063
  if (!w) {
929
1064
  console.warn('[rozie-data-table] virtual is on for columns but the scroll container has no bounded width; set a CSS width on an ancestor so the column window can be measured');
930
1065
  }
1066
+ // COLUMN-AXIS FIRST-WINDOW BUMP (quick 260908-vcy). The mount-kick above fires right
1067
+ // after `new Virtualizer(...)` — BEFORE colVirtualizer has measured, and before the
1068
+ // spacer effects even exist. Any fine-grained subscriber created inside that measure
1069
+ // gap takes colPadLeft()/colPadRight()'s `if (!items.length) return 0` early return,
1070
+ // which reads ONLY windowVer + editVer and never reaches the live path's reads
1071
+ // (forcedColumns()/columnSize()/column state). Its dependency set is therefore those
1072
+ // two counters alone — and without this bump nothing ever touches them again, so the
1073
+ // subscriber stays frozen at 0 for the lifetime of the component. Measured on Svelte:
1074
+ // the FIRST header row's trailing spacer froze at 0px while its siblings (created
1075
+ // after measurement, on the live path) tracked correctly; under table-layout:fixed the
1076
+ // first row alone establishes column widths, so that one cell collapsed the whole
1077
+ // table's scrollWidth from 9000 to 1350. The five other targets re-render wholesale
1078
+ // (Solid re-creates these rows), which masked it.
1079
+ //
1080
+ // The ROW axis has always been safe here only by accident: remeasureWindow() above
1081
+ // already routes through bumpWindowVer() on this same post-first-frame callback. This
1082
+ // is the column axis's missing equivalent — not a Svelte workaround (the emitted
1083
+ // Svelte binding is a correct reactive template_effect; the dependency set is what was
1084
+ // wrong), so it is fixed once, in the shared engine's consumer, for all six targets.
1085
+ //
1086
+ // Routed through bumpWindowVer() rather than a raw `$data.windowVer = $data.windowVer + 1`
1087
+ // to keep 87-10's microtask coalescing AND to avoid manufacturing the ROZ138
1088
+ // false positive that a second in-body counter self-read would trip (see the
1089
+ // mount-kick's own comment above).
1090
+ bumpWindowVer();
931
1091
  }
932
1092
  };
933
1093
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));else setTimeout(afterFirstFrame, 0);
@@ -941,6 +1101,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
941
1101
  announceState.globalFilter = effectiveGlobalFilter();
942
1102
  });
943
1103
  onCleanup(() => {
1104
+ // B-02: drop the document click-away listener (it is attached for the instance's lifetime,
1105
+ // not per edit session — attaching it per session would freeze a stale closure on React).
1106
+ if (docPointerDown && typeof document !== 'undefined') {
1107
+ document.removeEventListener('pointerdown', docPointerDown, true);
1108
+ docPointerDown = null;
1109
+ }
944
1110
  if (virtualizerCleanup) virtualizerCleanup();
945
1111
  // Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
946
1112
  // No-op when column windowing was off (cleanup stays null).
@@ -960,10 +1126,21 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
960
1126
  });
961
1127
  createEffect(() => {
962
1128
  maybeClearHistoryOnExternalSwap();
1129
+ // visible-range-change backstop (quick 260925-dtl part 2): a re-render from new data can move
1130
+ // the rendered window WITHOUT a window-version bump (measured on React: filling placeholders
1131
+ // shifted the window by two rows and nothing was reported). Deduped, so a render that leaves
1132
+ // the window where it was emits nothing.
1133
+ emitVisibleRangeIfChanged();
1134
+ // A-02: the post-render hook is the one place that observes EVERY column-width change on all
1135
+ // six targets — an interactive resize writes `columnSizing` through table-core under
1136
+ // `columnResizeMode: 'onChange'` and never reaches the coarse re-feed watch (which keys on
1137
+ // `$props.columnSizing`, undefined on the uncontrolled path). Self-guarded on the width
1138
+ // signature, so a tick that changed no width costs one loop over the leaf columns.
1139
+ remeasureColumnSizes();
963
1140
  if (!table) return;
964
1141
  // Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
965
- // $data.dataDefault) so a committed edit re-feeds on Lit whether or not r-model:data is
966
- // bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
1142
+ // $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
1143
+ // `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
967
1144
  // identical length) still re-feeds.
968
1145
  const d = currentData() || [];
969
1146
  if (d === lastData && d.length === lastDataLen) return;
@@ -971,6 +1148,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
971
1148
  lastDataLen = d.length;
972
1149
  reFeed();
973
1150
  });
1151
+ createEffect(on(() => (() => windowVer())(), (v) => untrack(() => (() => emitVisibleRangeIfChanged())()), { defer: true }));
974
1152
  createEffect(on(() => (() => [sorting(), globalFilter(), columnFilters(), pagination(),
975
1153
  // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
976
1154
  // changes at runtime (e.g. a server response updates the total) so getPageCount() and the
@@ -987,6 +1165,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
987
1165
  // (Consumers memoize the array as with $props.data/$props.sorting; the uncontrolled
988
1166
  // <Column>-children path leaves $props.columns undefined — a stable no-op getter.)
989
1167
  local.columns, colReg()])(), (v) => untrack(() => (() => {
1168
+ // Seed BEFORE the re-feed so the pinning this call may write is part of the state object
1169
+ // table-core receives on this same tick (rather than landing a frame late and re-ordering
1170
+ // getVisibleCells() a render after the columns appear). One-shot and self-latching — see
1171
+ // seedColumnPinning() in stateAssembly.rzts. This is the `<Column>`-children entry point:
1172
+ // those register during mount, so the $onMount call below can find zero columns.
1173
+ seedColumnPinning();
990
1174
  reFeed();
991
1175
  maybeClearHistoryOnExternalSwap();
992
1176
  })()), { defer: true }));
@@ -1039,6 +1223,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1039
1223
  // handle and TS2322s on a numeric seed.
1040
1224
  let remeasureRaf: any = null;
1041
1225
  let remeasureDisposed = false;
1226
+ // D-19 (quick 260923-rrr): "the user scrolled to the end" — recorded only when the scroll
1227
+ // offset actually MOVED (a growth-driven onChange leaves it where it was), with the row count
1228
+ // and offset at that moment. See scheduleRemeasure() in virtualization.rzts.
1229
+ let scrollEndPinned: boolean = false;
1230
+ let scrollEndPinnedCount: number = -1;
1231
+ let scrollEndPinnedTop: number = -1;
1042
1232
 
1043
1233
  // ── Grid interaction-mode constants + DOM root (phase 49, REQ-2/6) ────────────────────
1044
1234
  // Fixed PageUp/PageDown row step (D-06). Phase 53 swaps this for the visible-window size
@@ -1051,6 +1241,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1051
1241
  // querySelector-off-root precedent, proven ×6 by plan 01's probe). NEVER read in a
1052
1242
  // computed/template binding (ROZ123).
1053
1243
  let gridRoot: any = null;
1244
+ // ── B-02: "did the user just click OUTSIDE the grid?" ──────────────────────────────────
1245
+ // Set by the document-level pointerdown listener registered in $onMount, consumed by
1246
+ // onEditorBlur's null-relatedTarget branch (editorBindings.rzts), which explains in full why
1247
+ // the decision has to come from here and be routed back there. In short: a null relatedTarget
1248
+ // on its own cannot tell a click onto a non-focusable page region (the edit must commit) from
1249
+ // an editor being recycled out of the virtual window (it must NOT), and a document listener
1250
+ // cannot commit for itself on React because it closes over the render that attached it — the
1251
+ // draft it would write is the one from editor-open. A plain module-let is hoisted to a
1252
+ // per-instance `useRef` there, so it is read FRESH from the template-bound blur handler.
1253
+ // Capture phase, so a stopPropagation anywhere in the page cannot hide the click from us.
1254
+ let outsidePointerDown = false;
1255
+ let docPointerDown: any = null;
1054
1256
 
1055
1257
  // Echo-guard: while WE are writing a slice back, the re-feed watcher must not re-enter
1056
1258
  // the funnel. A counter (not a boolean) so nested writes are safe.
@@ -1149,6 +1351,95 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1149
1351
  };
1150
1352
  }
1151
1353
 
1354
+ // seedColumnPinning(): apply the per-column `pinned` declaration ('left' | 'right') — from the
1355
+ // `:columns` config array or a `<Column pinned="...">` child — as the INITIAL columnPinning
1356
+ // state, exactly once.
1357
+ //
1358
+ // Until now that field was carried onto every ColumnDef by columnBuilders.rzts and then read by
1359
+ // nobody: `column.getIsPinned()` returned false for a column declared `pinned="left"`, so the
1360
+ // documented per-column API was inert while `r-model:columnPinning` was the only mechanism that
1361
+ // actually pinned anything.
1362
+ //
1363
+ // Three invariants, each load-bearing:
1364
+ // 1. ONE-SHOT, LATCHED ON THE OUTCOME — never merely on "we ran". The latch flips only when
1365
+ // a seed is actually applied, or when a consumer pin is observed; a call that finds no
1366
+ // declared pins leaves it DOWN and will look again on the next re-feed. Latching on the
1367
+ // first call that saw any column at all was wrong, and Lit proved it: `<Column>` children
1368
+ // register INCREMENTALLY there, one re-feed per child, so the first call sees only
1369
+ // `name` — no pins declared yet — and a run-once latch would disable the seed
1370
+ // permanently before `amount pinned="right"` ever registered. The other five targets
1371
+ // batch all children into a single flush, which hid the race entirely. Once a seed HAS
1372
+ // been applied the latch holds, so a subsequent interactive UNPIN is never re-seeded —
1373
+ // which is the whole reason this is not folded into effectiveColumnPinning().
1374
+ // 2. ONLY WHEN NOTHING IS PINNED YET. Note this is NOT an `$props.columnPinning == null`
1375
+ // check: the prop carries `default: () => ({ left: [], right: [] })`, so it is never null
1376
+ // and a bound-vs-unbound test is impossible here. The meaningful question is whether the
1377
+ // consumer has actually expressed a pin — if they have, they own the slice and the
1378
+ // per-column declaration must not fight it; if the state is empty, `pinned` IS the
1379
+ // initial state and applying it is the whole point.
1380
+ // 3. NO CHROME IDS. The select/expander rail is injected by effectiveColumnPinning() on read
1381
+ // and stripped by writeColumnPinning() on write; the persisted state must never contain
1382
+ // them. Seeding only real column ids keeps that discipline.
1383
+ //
1384
+ // Writes BOTH the uncontrolled holder and `$model.columnPinning` so a consumer with a two-way
1385
+ // binding sees the pinning that is actually in effect rather than a stale empty object — but
1386
+ // does NOT emit `pin-change`: this is initial state derived from the consumer's own markup, not
1387
+ // a user action, and firing a change event for it on mount would be noise.
1388
+ //
1389
+ // Called from $onMount (covers the `:columns` config-array form, known at mount) AND from the
1390
+ // re-feed watch (covers `<Column>` children, which register during mount, and a runtime
1391
+ // `:columns` swap that arrives before any pin interaction). The `!defs.length` bail is what
1392
+ // makes the mount call harmless when columns are not known yet — the latch stays down and the
1393
+ // next re-feed seeds instead.
1394
+ let pinSeedApplied = false;
1395
+ function seedColumnPinning(): void {
1396
+ if (pinSeedApplied) return;
1397
+ // Invariant 2: bail if the consumer already pinned something (either an initial bound value
1398
+ // or an interaction that beat us here). Read the same source order effectiveColumnPinning()
1399
+ // does, minus the chrome rail it injects on read — the rail is structural, not a consumer pin.
1400
+ const live = columnPinning() != null ? columnPinning() : columnPinningDefault();
1401
+ const isRealPin = (id: string) => id !== SELECT_COL_ID && id !== EXPANDER_COL_ID;
1402
+ const pinnedAlready = (live && live.left ? live.left : []).filter(isRealPin).length > 0 || (live && live.right ? live.right : []).filter(isRealPin).length > 0;
1403
+ if (pinnedAlready) {
1404
+ pinSeedApplied = true;
1405
+ return;
1406
+ }
1407
+ const defs = columnDefs();
1408
+ if (!defs.length) return;
1409
+ const left: string[] = [];
1410
+ const right: string[] = [];
1411
+ for (let i = 0; i < defs.length; i++) {
1412
+ const def = defs[i];
1413
+ if (!def) continue;
1414
+ if (def.pinned === 'left') left.push(def.id);else if (def.pinned === 'right') right.push(def.id);
1415
+ }
1416
+ // Invariant 1: no declared pins in the column set we can currently SEE is not evidence there
1417
+ // are none — on Lit the set is still filling in. Leave the latch down and try again on the
1418
+ // next re-feed; the only exits are "seeded" (below) and "consumer already pinned" (above).
1419
+ if (!left.length && !right.length) return;
1420
+ pinSeedApplied = true;
1421
+ // Nothing was pinned (invariant 2 above), so this is a plain assignment, not a merge — the
1422
+ // only ids that could be in `live` are the structural chrome rail, which must never be
1423
+ // persisted (invariant 3). Writes the uncontrolled holder AND the two-way model so both
1424
+ // binding modes observe the pinning that is actually in effect; deliberately does NOT route
1425
+ // through writeColumnPinning(), whose job is to emit `pin-change` for real user actions.
1426
+ // Annotated `any` for the same reason currentState() is (see its comment below): the
1427
+ // uncontrolled holder's initial value is the empty literal `{ left: [], right: [] }`, so the
1428
+ // emitted per-target state holder infers `{ left: never[]; right: never[] }` and a genuinely
1429
+ // populated `{ left: string[]; right: string[] }` will not assign to it under the strict
1430
+ // bundled-leaf tsc (react TS2345 on setColumnPinningDefault, solid TS2769, lit TS2322 — one
1431
+ // per target, all the same never[] inference). The runtime shape is correct; this is the
1432
+ // established idiom here rather than widening three per-leaf strict baselines for it.
1433
+ // writeColumnPinning() never hits this because its payload flows through `strip()`, which
1434
+ // returns `any[]`.
1435
+ const seeded: any = {
1436
+ left,
1437
+ right
1438
+ };
1439
+ setColumnPinningDefault(seeded);
1440
+ setColumnPinning(seeded);
1441
+ }
1442
+
1152
1443
  // Assemble the live state object from bound r-model slices (?? uncontrolled fallback).
1153
1444
  // All NINE slices are wired (each ?? its own $data.<slice>Default). table-core reads
1154
1445
  // this whole object as `state`. Return type annotated `any`: the inferred object-literal
@@ -1205,6 +1496,54 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1205
1496
  // ({ id?, header, columns: [...] }) maps to a multi-level header GROUP column
1206
1497
  // whose children are built recursively (B12 — grouped/multi-level column headers).
1207
1498
  // Returns null for an unusable entry (no id/field, unsafe key, empty group).
1499
+ /**
1500
+ * A-01 — translate the public per-column `width` into table-core's `size`.
1501
+ *
1502
+ * `width` was written onto the ColumnDef in both build branches and read by NOTHING:
1503
+ * table-core resolves column width from `size` (a NUMBER of px), which only the two
1504
+ * chrome columns ever set. So the documented API was completely inert — the exact
1505
+ * sibling of the inert `pinned` that 0.5.0 closed.
1506
+ *
1507
+ * `size` is numeric px by construction, so only a number or a px string can map onto
1508
+ * it; any other CSS length (`12rem`, `20%`, `auto`) has no numeric px value at build
1509
+ * time and is deliberately NOT guessed — it returns null and the column keeps
1510
+ * table-core's default. The `width` prop's docs say exactly this.
1511
+ *
1512
+ * Returns null (not undefined) for "no usable width" so callers can test `!= null`
1513
+ * without tripping over a legitimate 0.
1514
+ */
1515
+ function parseWidthToSize(w: any) {
1516
+ if (typeof w === 'number') return Number.isFinite(w) && w > 0 ? w : null;
1517
+ if (typeof w !== 'string') return null;
1518
+ const t = w.trim();
1519
+ if (t === '') return null;
1520
+ const m = /^(\d+(?:\.\d+)?)(px)?$/i.exec(t);
1521
+ if (!m) return null;
1522
+ const n = Number.parseFloat(m[1]);
1523
+ return Number.isFinite(n) && n > 0 ? n : null;
1524
+ }
1525
+
1526
+ // ── E-03 ────────────────────────────────────────────────────────────────────────────────
1527
+ // The union check itself is `editorKindWarning` in helpers/columnDefUtils.ts (pure, so the
1528
+ // contract is unit-testable). Here we own the two things that are not pure: WHEN it runs and
1529
+ // how often it speaks.
1530
+ //
1531
+ // WHEN: at column-BUILD time, not in `editorTypeOf`, which is called per cell per render (D-23).
1532
+ // HOW OFTEN: latched per `id:value`, so a rebuild — the memo misses whenever the column set
1533
+ // changes identity — cannot turn one misconfiguration into a console flood.
1534
+ // Unguarded console.warn, matching the D-07/D-08 virtual-misconfig warns in DataTable.rozie:
1535
+ // a process.env guard is not bundler-portable across six targets, and always-warn-on-misconfig
1536
+ // is the established call in this component.
1537
+ const editorWarned = Object.create(null);
1538
+ function checkEditorKind(id: any, editor: any) {
1539
+ const msg = editorKindWarning(id, editor);
1540
+ if (!msg) return editor;
1541
+ const key = id + '\u0000' + String(editor);
1542
+ if (editorWarned[key]) return editor;
1543
+ editorWarned[key] = true;
1544
+ console.warn(msg);
1545
+ return editor;
1546
+ }
1208
1547
  function buildConfigDef(c: any) {
1209
1548
  if (!c) return null;
1210
1549
  // Grouped (multi-level) header column: an entry carrying a `columns` array. table-core's
@@ -1260,11 +1599,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1260
1599
  aggregationFn: wrapAggregationFn(c.aggregationFn),
1261
1600
  pinned: c.pinned != null ? c.pinned : '',
1262
1601
  width: c.width != null ? c.width : '',
1602
+ // A-01 — `width` alone is inert; table-core sizes from `size`. Spread so an
1603
+ // unusable width leaves `size` ABSENT rather than explicitly undefined, which
1604
+ // would override a consumer's `defaultColumn.size`.
1605
+ ...(parseWidthToSize(c.width) != null ? {
1606
+ size: parseWidthToSize(c.width)
1607
+ } : {}),
1263
1608
  // Editable-cell config (Phase 51) → ColumnDef.meta, the table-core per-column
1264
1609
  // metadata carrier the display↔editor branch + runValidator read. Off by default.
1265
1610
  meta: {
1266
1611
  editable: c.editable === true,
1267
- editor: c.editor != null ? c.editor : 'text',
1612
+ editor: c.editor != null ? checkEditorKind(id, c.editor) : 'text',
1268
1613
  editorOptions: c.editorOptions != null ? c.editorOptions : [],
1269
1614
  validate: typeof c.validate === 'function' ? c.validate : null
1270
1615
  }
@@ -1344,10 +1689,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1344
1689
  aggregationFn: wrapAggregationFn(spec.aggregationFn),
1345
1690
  pinned: spec.pinned != null ? spec.pinned : '',
1346
1691
  width: spec.width != null ? spec.width : '',
1692
+ // A-01 — see buildConfigDef; same mapping for the <Column> registry branch.
1693
+ ...(parseWidthToSize(spec.width) != null ? {
1694
+ size: parseWidthToSize(spec.width)
1695
+ } : {}),
1347
1696
  // Editable-cell config (Phase 51) → ColumnDef.meta from the <Column> registry spec.
1348
1697
  meta: {
1349
1698
  editable: spec.editable === true,
1350
- editor: spec.editor != null ? spec.editor : 'text',
1699
+ editor: spec.editor != null ? checkEditorKind(id, spec.editor) : 'text',
1351
1700
  editorOptions: spec.editorOptions != null ? spec.editorOptions : [],
1352
1701
  validate: typeof spec.validate === 'function' ? spec.validate : null
1353
1702
  }
@@ -1826,9 +2175,42 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1826
2175
  function onColumnPinningChangeCb(updater: any) {
1827
2176
  writeColumnPinning(applyUpdater(updater, currentState().columnPinning));
1828
2177
  }
2178
+ // columnSizingInfo is the ONLY slice table-core drives as a rapid, multi-call gesture: a resize
2179
+ // drag fires this updater once on pointerdown (seeding startOffset/startSize/isResizingColumn)
2180
+ // and then again on EVERY mousemove, each call reading the values the previous one wrote.
2181
+ //
2182
+ // That read-your-own-last-write requirement is what makes a plain `$data` read wrong here on
2183
+ // React. These callbacks are handed to createTable in $onMount, so on React the closure binds a
2184
+ // MOUNT-TIME `$data.columnSizingInfo` and never sees its own writes. Measured on the shipped
2185
+ // leaf (quick 260909, instrumented build): pointerdown correctly wrote
2186
+ // `{ startOffset: 163, isResizingColumn: 'col0' }`, and then every single mousemove still saw
2187
+ // `startOffset: null, isResizingColumn: false` — so it computed deltaOffset from 0 instead of
2188
+ // 163 (179, 195, 211… instead of 16, 32, 48…) AND wrote `isResizingColumn: false` back, wiping
2189
+ // the gesture on the first move. Net effect: column resizing did nothing at all on React while
2190
+ // working on the other five. It was masked because the one VR case that drives a real drag was
2191
+ // `test.fixme`'d on React as a presumed harness limitation (87-05 Task 3) — a real CDP drag
2192
+ // with intermediate steps fails there too, which is what proved it a product bug.
2193
+ //
2194
+ // Why not just read `currentState().columnSizingInfo` like the eleven siblings above: that
2195
+ // idiom relies on the re-feed $watch re-passing fresh callbacks into setOptions, and that watch
2196
+ // fires on data/column changes — NOT on every render, and never mid-gesture. It is sufficient
2197
+ // for one-shot updaters (a sort toggle, a selection click) and insufficient here, where
2198
+ // table-core issues a burst of updaters between any two re-feeds.
2199
+ //
2200
+ // Fix: a synchronous module-scope mirror, written on the same tick as the state. Reads prefer
2201
+ // the mirror, so each updater in a gesture composes on the previous one's result regardless of
2202
+ // when the host framework commits. Behaviourally identical on the five targets whose state read
2203
+ // was already synchronous (mirror and state agree), and the slice is transient gesture state
2204
+ // owned entirely by table-core — nothing else writes it — so the mirror cannot drift.
2205
+ // Same trick as the `committedThisSession` commit latch (editCellLifecycle.rzts), for the same
2206
+ // reason: a sync latch survives React's async window.
2207
+ let columnSizingInfoSync: any = null;
1829
2208
  function onColumnSizingInfoChangeCb(updater: any) {
1830
- const next = applyUpdater(updater, columnSizingInfo());
1831
- setColumnSizingInfo(next != null ? next : columnSizingInfo());
2209
+ const base = columnSizingInfoSync != null ? columnSizingInfoSync : columnSizingInfo();
2210
+ const next = applyUpdater(updater, base);
2211
+ if (next == null) return;
2212
+ columnSizingInfoSync = next;
2213
+ setColumnSizingInfo(next);
1832
2214
  }
1833
2215
 
1834
2216
  // ══ Vertical row windowing (phase 53, req-1/2/3/6/9/10) — the virtual-core bridge ════════
@@ -1942,9 +2324,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1942
2324
  }
1943
2325
  }
1944
2326
  // Active cell's column (D-10): required or focusActiveCell()'s [data-col-index="N"]
1945
- // querySelector cannot resolve for an off-window active cell. Body cells only — a header-
1946
- // active cell has no column-axis rendering dependency here (headers window separately).
1947
- if (!activeIsHeader() && activeColIndex() >= 0 && out.indexOf(activeColIndex()) === -1) {
2327
+ // querySelector cannot resolve for an off-window active cell.
2328
+ //
2329
+ // B-03: this used to be body-cells-only, on the stated grounds that "a header-active cell
2330
+ // has no column-axis rendering dependency here (headers window separately)". That is false —
2331
+ // `windowedHeadersFor()` slices the header row from `windowedColIndices()`, the very set this
2332
+ // function feeds, so a header cell has EXACTLY the same dependency a body cell does. With the
2333
+ // active header column unforced, focusing a leaf header in a 60-column windowed grid and
2334
+ // pressing End left `resolveCellEl` with nothing to find: focus froze on the old `<th>` while
2335
+ // the roving model moved on, and `cellTabindex` handed `tabindex="0"` to a cell that does not
2336
+ // exist — so the grid had no tab stop at all. The gate is gone; the column is forced for
2337
+ // whichever axis the active cell is on.
2338
+ if (activeColIndex() >= 0 && out.indexOf(activeColIndex()) === -1) {
1948
2339
  out.push(activeColIndex());
1949
2340
  }
1950
2341
  // Editing column (D-10): required or an open editor unmounts mid-keystroke when a
@@ -2022,21 +2413,64 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2022
2413
  // committed yet), so the rAF pass is what observes React's recycled rows.
2023
2414
  // Each pass only OBSERVES + measures the live window; measureElement is idempotent on an
2024
2415
  // already-observed node, so running both is cheap and loop-free.
2416
+ // Upper bound on the frames scheduleRemeasure() will wait for the framework to commit a recycled
2417
+ // window (N-05). Measured commit lag is 1 frame on React/Angular; 10 is generous headroom.
2418
+ const REMEASURE_MAX_FRAMES = 10;
2025
2419
  function scheduleRemeasure() {
2026
2420
  // 87-13: never arm a new sweep after teardown — an onChange can still fire from a
2027
2421
  // virtualizer whose own ResizeObserver cleanup has run but whose queued tick was already
2028
2422
  // in flight.
2029
2423
  if (remeasureDisposed) return;
2424
+ // D-19 (quick 260923-rrr): remember whether the view is at the END — but only when the scroll
2425
+ // offset MOVED since the last record. A measurement-driven onChange (rows growing) arrives
2426
+ // with the offset unchanged, and it is exactly that growth that moves the view off the end,
2427
+ // so it must not clear the pin. (virtual-core's isScrolling cannot tell the two apart: it
2428
+ // stays true for 150ms after the user's scroll, spanning the growth.)
2429
+ if (virtualizer && gridScrollEl && !virtualizer.scrollState && gridScrollEl.scrollTop !== scrollEndPinnedTop) {
2430
+ scrollEndPinnedTop = gridScrollEl.scrollTop;
2431
+ // Judged on virtual-core's MODEL (total size − viewport − offset), not DOM geometry: its own
2432
+ // above-viewport adjustments move scrollTop before the framework commits the matching
2433
+ // spacer, so the DOM transiently reads "not at the end" for a view that is (measured on
2434
+ // Angular: a 12px adjustment cleared a DOM-judged pin and left the view 12px short).
2435
+ scrollEndPinned = virtualizer.getVirtualDistanceFromEnd() <= 1;
2436
+ scrollEndPinnedCount = windowSource().length;
2437
+ }
2030
2438
  if (remeasurePending) return;
2031
2439
  remeasurePending = true;
2032
2440
  let ranMicro = false;
2033
2441
  const microPass = () => {
2034
2442
  remeasureWindow();
2035
2443
  };
2444
+ // N-05 (quick 260923-rrr): the rAF pass keys on the OUTCOME, not on "a frame went by". React
2445
+ // and Angular commit the recycled window AFTER that first rAF, so a single pass measured the
2446
+ // OLD rows and the new ones were only handed to measureElement when virtual-core's 150ms
2447
+ // scrolling-ended tick fired an onChange — the user saw the un-adjusted layout, then a 1–3 row
2448
+ // jump (measured). While the committed <tr> set does not yet cover the virtualizer's window,
2449
+ // re-run next frame, bounded so a window that can never be covered cannot spin forever.
2450
+ let rafAttempts = 0;
2036
2451
  const rafPass = () => {
2037
2452
  remeasureRaf = null;
2453
+ const covered = remeasureWindow();
2454
+ rafAttempts = rafAttempts + 1;
2455
+ if (!covered && rafAttempts < REMEASURE_MAX_FRAMES && !remeasureDisposed && typeof requestAnimationFrame === 'function') {
2456
+ remeasureRaf = requestAnimationFrame(rafPass);
2457
+ return;
2458
+ }
2459
+ // D-19: keep a user who scrolled to the END at the end. When rows at/above the viewport
2460
+ // measure taller than their estimate, virtual-core compensates with a scroll adjustment — but
2461
+ // on React/Angular that adjustment is written BEFORE the framework commits the taller spacer,
2462
+ // so the browser clamps it to the OLD maximum and it is lost (measured: the view ended
2463
+ // 593–1025px above the bottom on React, 30px on Angular). This pass runs after the commit, so
2464
+ // the real maximum is known. Not when the row count changed: appending rows must not
2465
+ // auto-follow them.
2466
+ if (scrollEndPinned && gridScrollEl && !remeasureDisposed && windowSource().length === scrollEndPinnedCount) {
2467
+ const maxTop = gridScrollEl.scrollHeight - gridScrollEl.clientHeight;
2468
+ if (maxTop - gridScrollEl.scrollTop > 1) {
2469
+ gridScrollEl.scrollTop = maxTop;
2470
+ scrollEndPinnedTop = gridScrollEl.scrollTop;
2471
+ }
2472
+ }
2038
2473
  remeasurePending = false;
2039
- remeasureWindow();
2040
2474
  };
2041
2475
  if (typeof queueMicrotask !== 'undefined') {
2042
2476
  ranMicro = true;
@@ -2070,9 +2504,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2070
2504
  // unioned into the windowed slice when it scrolls off-window and its height is subtracted from
2071
2505
  // the appropriate spacer so the total stays exactly getTotalSize() (the 51-01-proven mechanism).
2072
2506
  // This is the data-table half of the D-05 windowing.rzts pin-extension hook (listbox provides none).
2507
+ //
2508
+ // B-06 widens this from "the editing row" to "the row that must not recycle out of the
2509
+ // window", which now includes the ACTIVE row in grid mode. The active cell is the grid's ONLY
2510
+ // `tabindex="0"` — `cellTabindex()` returns 0 for it and -1 for every other cell — so once a
2511
+ // wheel-scroll carried it out of the window the grid had NO tab stop at all: Tab could not
2512
+ // re-enter it, and the roving model still pointed at a cell with no DOM node, so
2513
+ // `resolveCellEl` returned null and a subsequent arrow key moved state with nothing to focus.
2514
+ // The column axis has forced the active-cell COLUMN since D-10 (`forcedColumns`); this is the
2515
+ // row-axis twin of that decision, and it costs at most one extra rendered row, in flow, with
2516
+ // its height subtracted from the spacer by the same 51-01 mechanism the editing pin uses.
2517
+ //
2518
+ // `$props.interactionMode` is read directly rather than through `isGrid()` (gridFocusNav.rzts):
2519
+ // that partial is imported AFTER this one, and this file's header documents the
2520
+ // forward-cross-partial-reference TDZ on React that the 87-02 lesson established. A prop read
2521
+ // has no such hazard.
2073
2522
  function pinnedEditIndex() {
2074
2523
  if (editingRow() >= 0) return editingRow();
2075
2524
  if (editingRowIndex() != null) return editingRowIndex();
2525
+ if (local.interactionMode === 'grid' && !activeIsHeader() && activeRow() >= 0) return activeRow();
2076
2526
  return -1;
2077
2527
  }
2078
2528
  // pinnedMeasurement(pin): the virtual-core measurement { index, start, size, end, key } for the
@@ -2099,8 +2549,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2099
2549
  // cover unmount — virtualizer/gridRoot are assigned in $onMount and never nulled — so this
2100
2550
  // is the only thing standing between a pass queued pre-unmount and refineRowEstimate()'s
2101
2551
  // setOptions/scrollTop/$data.windowVer writes against a torn-down instance.
2102
- if (remeasureDisposed) return;
2103
- if (!virtualizer || !gridRoot) return;
2552
+ if (remeasureDisposed) return true;
2553
+ if (!virtualizer || !gridRoot) return true;
2104
2554
  // Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
2105
2555
  // exclusively during scrollToIndex / scrollToOffset (the D-12 scroll-then-focus seam) and
2106
2556
  // null for ordinary user/scrollTop-driven scrolling (verified virtual-core@3.17.1: set in
@@ -2108,9 +2558,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2108
2558
  // resizeItem nudge the offset and starve the scroll target (the Solid off-window focus
2109
2559
  // regression); the next settled onChange re-measures the stable window. Manual-scroll
2110
2560
  // recycling (the CR-01 case) has scrollState === null, so it measures normally.
2111
- if (virtualizer.scrollState) return;
2561
+ if (virtualizer.scrollState) return true;
2112
2562
  const trs = gridRoot.querySelectorAll('tbody.rdt-tbody > tr[data-index]');
2113
- for (const tr of trs as any) virtualizer.measureElement(tr);
2563
+ const rendered = new Set();
2564
+ for (const tr of trs as any) {
2565
+ virtualizer.measureElement(tr);
2566
+ rendered.add(tr.getAttribute('data-index'));
2567
+ }
2568
+ // N-05: did the committed rows cover the virtualizer's CURRENT window? False while the
2569
+ // framework has not yet committed the recycled set (see scheduleRemeasure).
2570
+ let covered = true;
2571
+ const items = virtualizer.getVirtualItems();
2572
+ for (let i = 0; i < items.length; i++) {
2573
+ if (!rendered.has(String(items[i].index))) {
2574
+ covered = false;
2575
+ break;
2576
+ }
2577
+ }
2114
2578
  // D-15 (Phase 87 87-07): trigger the shared engine's post-measurement fold + hysteresis
2115
2579
  // re-feed via the afterRowRemeasure mutable-let hook (DataTable.rozie's own script,
2116
2580
  // assigned to windowing.rzts's refineRowEstimate()) — NEVER a direct call from this file
@@ -2119,6 +2583,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2119
2583
  // documents for columnVirtualizerOptions()). Inert (no-op) until the host assigns it; the
2120
2584
  // off/pre-mount path never reaches this line since virtualizer is null there.
2121
2585
  if (afterRowRemeasure) afterRowRemeasure();
2586
+ return covered;
2122
2587
  }
2123
2588
 
2124
2589
  // D-04: this shell exports ONLY the impure, data-table-specific host pieces. The pure windowing
@@ -2127,6 +2592,113 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2127
2592
  // `@rozie-ui/headless-core/windowing.rzts` via bare specifier — the P0-proven cross-package inline
2128
2593
  // path that DISSOLVES the partial into the leaf (a re-export-from THROUGH this shell would survive as
2129
2594
  // a runtime import, not inline — verified). The math closes over these host symbols by convention.
2595
+ // ── Lazy rows (quick 260925-dtl part 2, oinbox dogfooding) ─────────────────────────────────────
2596
+ // `virtual` + `manual` + `rowCount`: the row space is rowCount long and `data` may be SPARSE —
2597
+ // an `undefined`/`null` entry (or anything past data.length) is a row not loaded yet. Such holes
2598
+ // are fed to table-core as placeholder sentinels (one cached object per index, so a re-feed does
2599
+ // not churn them) that render as placeholder rows and can't be selected, expanded, edited or
2600
+ // activated. ONLY the table-core feed is padded: every write-back derives from currentData(), the
2601
+ // consumer's own array, so a sentinel can never leak into `data`.
2602
+ const LAZY_PLACEHOLDER_KEY = '__rdtLazyPlaceholder';
2603
+ // LAZY_PLACEHOLDER_ROW_ID_PREFIX: the table-core row-id table.rowIdOption() (DataTable.rozie)
2604
+ // assigns to a placeholder ('__rdt_ph_' + index — no consumer row to ask, so it keys by
2605
+ // position). Shared here (not re-literaled) because migratePlaceholderSizeCache() below and
2606
+ // rowIdOption() must agree byte-for-byte on the prefix or the migration's key scan silently
2607
+ // finds nothing.
2608
+ const LAZY_PLACEHOLDER_ROW_ID_PREFIX = '__rdt_ph_';
2609
+ let lazyPlaceholders: Record<number, any> = {};
2610
+ function lazyRowsActive() {
2611
+ return rowsWindowed() && !!local.manual && local.rowCount != null && local.rowCount > 0;
2612
+ }
2613
+ function isLazyPlaceholder(orig: any): boolean {
2614
+ return !!orig && orig[LAZY_PLACEHOLDER_KEY] === true;
2615
+ }
2616
+ function rowIsLazyPlaceholder(row: any): boolean {
2617
+ return !!row && isLazyPlaceholder(row.original);
2618
+ }
2619
+ function feedData() {
2620
+ const d: any[] = currentData() || [];
2621
+ if (!lazyRowsActive()) return d;
2622
+ const n = Math.max(d.length, Number(local.rowCount));
2623
+ const out: any[] = new Array(n);
2624
+ for (let i = 0; i < n; i++) {
2625
+ const v = d[i];
2626
+ if (v != null) {
2627
+ out[i] = v;
2628
+ } else {
2629
+ if (!lazyPlaceholders[i]) lazyPlaceholders[i] = {
2630
+ [LAZY_PLACEHOLDER_KEY]: true,
2631
+ index: i
2632
+ };
2633
+ out[i] = lazyPlaceholders[i];
2634
+ }
2635
+ }
2636
+ return out;
2637
+ }
2638
+
2639
+ // migratePlaceholderSizeCache() (quick 260926, VR probe on data-table-lazy [react]): a filled
2640
+ // placeholder's row id changes ('__rdt_ph_' + i -> the real getRowId id), and virtual-core's
2641
+ // getItemKey re-keys by it — but virtual-core's itemSizeCache (dist/esm/index.js@3.17.1) is a
2642
+ // bare Map keyed by the OLD key, so the new key has no entry. The next getMeasurements() recompute
2643
+ // then estimates that row at $props.estimateRowHeight instead of its already-known measured size;
2644
+ // the CR-01 sweep re-measures it moments later and each delta fires one applyScrollAdjustment,
2645
+ // walking the window's anchor off by their sum (measured: 9 rows x -4px = -36px, row 5000 -> 4999
2646
+ // on React — the other 5 targets happened to keep reading the stale pre-refeed measurements array
2647
+ // and never saw the estimate at all). Fix: carry the cached size across the rekey, keyed off
2648
+ // table-core's OWN freshly-assigned id (table.getCoreRowModel().rows[i].id, NOT a locally
2649
+ // recomputed getRowId() call — must be byte-identical to what virtualItemKey(i) reads next) so
2650
+ // the cache is already correct before the caller's virtualizer.setOptions()/_willUpdate() re-feed.
2651
+ // Invalidating the memo needs BOTH strategies below, not just one: getMeasurements() is
2652
+ // memoized on [getMeasurementOptions(), itemSizeCacheVersion] on 3.15+ (confirmed from the
2653
+ // installed source; the same fact windowing.rzts's refineRowEstimate() comment documents) —
2654
+ // but our peer range is `^3`, and versions <=3.14 have NO itemSizeCacheVersion at all and key
2655
+ // the SAME memo on `this.itemSizeCache` (Map IDENTITY) instead — verified against the 3.14.0
2656
+ // tarball, whose own resizeItem() invalidates by reassigning `this.itemSizeCache = new Map(...)`,
2657
+ // never by bumping a version counter. Mutating the Map in place would be silently invisible to
2658
+ // that memo on <=3.14 (a stray `itemSizeCacheVersion++` there just creates a NaN-valued own
2659
+ // property nobody reads). So this reassigns the Map (a public field on every 3.x we support —
2660
+ // harmless on 3.15+, whose memo body re-reads `this.itemSizeCache` fresh) AND bumps the version
2661
+ // counter only where it actually exists. Scans the CACHE's own placeholder-prefixed keys
2662
+ // (bounded by rows actually measured so far), never rowCount (unbounded — 10,000+ here).
2663
+ function migratePlaceholderSizeCache(): void {
2664
+ if (!virtualizer || !virtualizer.itemSizeCache || !table) return;
2665
+ const cache = virtualizer.itemSizeCache;
2666
+ const coreRows = table.getCoreRowModel().rows;
2667
+ let migrated = false;
2668
+ for (const oldKey of Array.from(cache.keys()) as any) {
2669
+ if (typeof oldKey !== 'string' || oldKey.indexOf(LAZY_PLACEHOLDER_ROW_ID_PREFIX) !== 0) continue;
2670
+ const i = Number(oldKey.slice(LAZY_PLACEHOLDER_ROW_ID_PREFIX.length));
2671
+ const row = coreRows[i];
2672
+ if (!row || rowIsLazyPlaceholder(row) || row.id === oldKey) continue;
2673
+ cache.set(row.id, cache.get(oldKey));
2674
+ cache.delete(oldKey);
2675
+ migrated = true;
2676
+ }
2677
+ if (!migrated) return;
2678
+ virtualizer.itemSizeCache = new Map(cache);
2679
+ if (typeof virtualizer.itemSizeCacheVersion === 'number') virtualizer.itemSizeCacheVersion++;
2680
+ }
2681
+
2682
+ // `visible-range-change { start, end }` — the RENDERED row window (overscan included), `end`
2683
+ // exclusive, over the full row space. Driven by the window-version bump every virtualizer change
2684
+ // routes through, deduped here so a scroll that doesn't move the window emits nothing.
2685
+ let lastRangeStart = -1;
2686
+ let lastRangeEnd = -1;
2687
+ function emitVisibleRangeIfChanged() {
2688
+ if (!rowsWindowed() || !virtualizer) return;
2689
+ const items = virtualizer.getVirtualItems();
2690
+ if (!items.length) return;
2691
+ const start = items[0].index;
2692
+ const end = items[items.length - 1].index + 1;
2693
+ if (start === lastRangeStart && end === lastRangeEnd) return;
2694
+ lastRangeStart = start;
2695
+ lastRangeEnd = end;
2696
+ _props.onVisibleRangeChange?.({
2697
+ start,
2698
+ end
2699
+ });
2700
+ }
2701
+
2130
2702
  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
2131
2703
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
2132
2704
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
@@ -2503,6 +3075,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2503
3075
  // makes the eventual render land strictly AFTER this fold + re-feed has fully landed, on
2504
3076
  // every target (the 87-10 fix for the confirmed Solid-specific rendering gap).
2505
3077
  bumpWindowVer();
3078
+ // D-19 (quick 260923-rrr): a re-feed changes every unmeasured row's size, but — unlike a
3079
+ // measurement — it does not go through virtual-core's onChange, so the host's post-commit
3080
+ // pass (which restores an at-the-end view the growth moved off) never ran for it. Schedule it.
3081
+ scheduleRemeasure();
2506
3082
  }
2507
3083
 
2508
3084
  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
@@ -2902,6 +3478,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2902
3478
  }
2903
3479
  return '';
2904
3480
  }
3481
+
3482
+ // The table-core `getRowId` option (quick 260925-rew). Without it table-core keys
3483
+ // rowSelection / expanded by position, so a row inserted above a selected one moves the
3484
+ // selection onto another row. The prop is captured into a local so the null check narrows
3485
+ // inside the arrow (TS2721 on the Vue leaf otherwise), and String() because table-core
3486
+ // compares ids as strings.
3487
+ function rowIdOption() {
3488
+ const f = local.getRowId;
3489
+ // A lazy placeholder has no consumer row to ask — key it by its index (see feedData()).
3490
+ // Prefix shared with migratePlaceholderSizeCache() (virtualization.rzts) — see its own comment.
3491
+ return f ? (originalRow: any, index: any, parent: any) => isLazyPlaceholder(originalRow) ? LAZY_PLACEHOLDER_ROW_ID_PREFIX + originalRow.index : String(f(originalRow, index, parent)) : undefined;
3492
+ }
3493
+
3494
+ // table-core `enableRowSelection`: off for selectionMode 'none'; with lazy rows a placeholder
3495
+ // (a row not loaded yet) is never selectable, so select-all takes the loaded rows only.
3496
+ function rowSelectionOption() {
3497
+ if (local.selectionMode === 'none') return false;
3498
+ return lazyRowsActive() ? (row: any) => !rowIsLazyPlaceholder(row) : true;
3499
+ }
2905
3500
  // Push fresh options into table-core + re-pull the row model. Extracted so BOTH the
2906
3501
  // re-feed $watch (above) and the Lit data-change $onUpdate (below) call it.
2907
3502
  function reFeed() {
@@ -2914,10 +3509,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2914
3509
  // below (`maybeClearHistoryOnExternalSwap`), which runs on all six targets.
2915
3510
  table.setOptions((prev: any) => ({
2916
3511
  ...prev,
2917
- data: currentData(),
3512
+ data: feedData(),
2918
3513
  columns: tableColumns(),
2919
3514
  state: currentState(),
2920
- enableRowSelection: local.selectionMode !== 'none',
3515
+ enableRowSelection: rowSelectionOption(),
2921
3516
  enableMultiRowSelection: local.selectionMode === 'multiple',
2922
3517
  // Re-pass the server-side page-count sources (#2) so a RUNTIME rowCount/pageCount change
2923
3518
  // takes effect: setOptions REPLACES via `...prev`, which holds the value captured at
@@ -2930,7 +3525,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
2930
3525
  // onExpandedChange callback must re-capture fresh currentState each cycle, F6).
2931
3526
  getExpandedRowModel: getExpandedRowModel(),
2932
3527
  getSubRows: (local.getSubRows || undefined) as any,
2933
- getRowCanExpand: local.expandable === true && local.getSubRows == null ? () => true : undefined,
3528
+ // Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
3529
+ // other row-model fns.
3530
+ getRowId: rowIdOption(),
3531
+ getRowCanExpand: local.expandable === true && local.getSubRows == null ? (row: any) => !rowIsLazyPlaceholder(row) : undefined,
2934
3532
  onExpandedChange: onExpandedChangeCb,
2935
3533
  // Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
2936
3534
  // POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
@@ -3256,9 +3854,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3256
3854
  // read header.getSize() — the group-aware size (see headerWidth's own comment). pinStyle still
3257
3855
  // keys off the plain column id (pinning is a leaf-column-only concern; a grouped header never
3258
3856
  // pins as a unit in this codebase's model).
3259
- function thStyle(header: any) {
3857
+ // A-03: `widthPx` is the in-window leaf-width sum windowedHeadersFor() computed alongside the
3858
+ // clamped colspan. When it is present it WINS over headerWidth(), which reports
3859
+ // Header.getSize() — every descendant leaf, in-window or not — and so over-declares a straddling
3860
+ // group by exactly the leaves the colspan already excluded. Null on the unwindowed path and from
3861
+ // the non-windowed header body, which both keep headerWidth() verbatim.
3862
+ // `widthPx` carries an EXPLICIT `= null` default for the same reason setRangeFocus's anchor
3863
+ // params do: the emitted signature types a declared param as required, and the non-windowed
3864
+ // header body calls `thStyle(header)` with one argument — TS2554 on the strict leaf typecheck.
3865
+ function thStyle(header: any, widthPx = null) {
3260
3866
  let s = '';
3261
- const w = headerWidth(header);
3867
+ const w = widthPx != null && widthPx > 0 ? widthPx + 'px' : headerWidth(header);
3262
3868
  if (w) s += 'width:' + w + ';';
3263
3869
  s += pinStyle(header && header.column ? header.column.id : null, 2);
3264
3870
  return s;
@@ -3642,7 +4248,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3642
4248
  if (selectAllBox) selectAllBox.indeterminate = isSomeRowsSelected() && !isAllRowsSelected();
3643
4249
  }
3644
4250
 
3645
- // The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
4251
+ // C-09's value-equality guard. Imported HERE, in DataTable.rozie's OWN script, because that
4252
+ // is where it is USED: a `.rzts` partial's import of a symbol the partial itself never
4253
+ // references is dropped as unused, and the host script then reaches a free identifier. That
4254
+ // failed at RUNTIME, not at compile time — `ReferenceError: columnSpecsEquivalent is not
4255
+ // defined` on the first paste — which is the only way this class of mistake shows up.
3646
4256
 
3647
4257
  // Imperative handle (consumer-callable). Each verb is a PRE-DECLARED top-level
3648
4258
  // `const` (the canonical $expose contract — `$expose({ name })` references a
@@ -3767,6 +4377,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3767
4377
  return (hg && hg.headers ? hg.headers : []).indexOf(header);
3768
4378
  }
3769
4379
 
4380
+ // E-05 — a header's ABSOLUTE LEAF-column start index, which is NOT the same as its position
4381
+ // in the group's header array: a grouped header occupies ONE array slot but spans colSpan leaf
4382
+ // columns, so positions and leaf indices diverge the moment a header group exists. Walk the
4383
+ // group accumulating colSpan to the target. Window-independent by construction (a header's
4384
+ // leaf start is a full-model property), so both the windowed grid header and the plain
4385
+ // non-grid header can share it.
4386
+ function headerLeafStart(hg: any, header: any) {
4387
+ const list = hg && hg.headers ? hg.headers : [];
4388
+ let leaf = 0;
4389
+ for (let i = 0; i < list.length; i++) {
4390
+ if (list[i] === header) return leaf;
4391
+ const h = list[i];
4392
+ leaf = leaf + (h && h.colSpan > 1 ? h.colSpan : 1);
4393
+ }
4394
+ return -1;
4395
+ }
4396
+
3770
4397
  // ── C1 (phase 63 wave-6) absolute-index bridge ─────────────────────────────────────────
3771
4398
  // The PUBLIC active-cell rowIndex (focusCell/getActiveCell/activecell-change) is the ABSOLUTE
3772
4399
  // display-order position in getPrePaginationRowModel().rows (filter+sort+expand applied, BEFORE
@@ -3853,7 +4480,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3853
4480
  // post-mount root. Uniform on all six, shadow-safe (the query runs from inside the
3854
4481
  // component's own scope). rowKey is the literal '__header' or a String(integer index) and
3855
4482
  // colIndex is an integer — NO consumer string is interpolated into the selector (T-49-01).
3856
- function resolveCellEl(rowKey: any, colIndex: any, level = null) {
4483
+ // `level` is ANNOTATED, not just defaulted: an untyped `level = null` infers the param type as
4484
+ // `null`, so every header call site passing a real level (`lvl`, activeHeaderLevel) was a
4485
+ // TS2345 under the strict leaf typecheck (quick 260922-mkb — B-03 added one more such call).
4486
+ function resolveCellEl(rowKey: any, colIndex: any, level: number | null = null) {
3857
4487
  if (!gridRoot) return null;
3858
4488
  // B12: a grouped multi-level header has MULTIPLE cells sharing data-row="__header" at the
3859
4489
  // same data-col-index across levels (parent vs leaf). Disambiguate header lookups by the
@@ -3913,10 +4543,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3913
4543
  // both are out, and the ONE existing poll below is reused unchanged — it already resolves the
3914
4544
  // target cell by BOTH (row, col) via resolveCellEl(String(r), c), so a cell that becomes
3915
4545
  // reachable via the column axis needs no new poll logic (per the plan's own key_links note).
4546
+ // B-03: the COLUMN axis applies to HEADER cells too. This guard used to be `!header &&
4547
+ // (rowOut || colOut)`, so a header-active move to an off-window column never scrolled it in:
4548
+ // `resolveCellEl` returned null, focus froze on the old `<th>` while the state and the ring
4549
+ // ran away to a column with no DOM node, and `cellTabindex` then reported 0 for an unrendered
4550
+ // cell — leaving the grid with ZERO `tabindex="0"` cells and no way back in with Tab. Header
4551
+ // rows genuinely have no ROW-axis window (headers are not row-virtualized), so the row half
4552
+ // stays body-only; only the column half widens.
3916
4553
  const rowOut = rowsWindowed() && virtualizer && rowIsOutsideWindow(r);
3917
4554
  const colOut = colsWindowed() && colVirtualizer && colIsOutsideWindow(c);
3918
- if (!header && (rowOut || colOut)) {
3919
- if (rowOut) virtualizer.scrollToIndex(r, {
4555
+ const rowScroll = !header && rowOut;
4556
+ if (rowScroll || colOut) {
4557
+ if (rowScroll) virtualizer.scrollToIndex(r, {
3920
4558
  align: 'center'
3921
4559
  });
3922
4560
  if (colOut) colVirtualizer.scrollToIndex(c, {
@@ -3940,7 +4578,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3940
4578
  // A newer focus intent superseded this poll — abort WITHOUT focusing (the user has since
3941
4579
  // navigated / clicked elsewhere; re-focusing this off-window target would yank focus back).
3942
4580
  if (focusIntentEpoch !== myEpoch) return;
3943
- const el = resolveCellEl(String(r), c);
4581
+ // B-03: resolve with the SAME key the synchronous path below uses, so the poll can
4582
+ // find a header cell. `String(r)` alone addresses body rows only.
4583
+ const el = resolveCellEl(header ? '__header' : String(r), c, header ? lvl : null);
3944
4584
  if (el) {
3945
4585
  el.focus();
3946
4586
  return;
@@ -3973,8 +4613,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3973
4613
  // HTMLElement ARIA-reflected property (`Element.ariaRowCount: string`), so a same-named method
3974
4614
  // becomes a class field that shadows it on Lit → TS2416 cascades to EVERY @property decorator
3975
4615
  // (the `valueOf`/`nodeType` inherited-DOM-member collision class, authoring playbook §6).
4616
+ // A-04: under `manual` the filtered row model is NOT the total — table-core holds only the
4617
+ // page the consumer handed it, so getFilteredRowModel().rows.length is the PAGE SIZE. The
4618
+ // server-side total lives in the props the consumer passes for exactly this purpose
4619
+ // (`rowCount`, else `pageCount`), which is also table-core's own precedence for getPageCount()
4620
+ // (explicit pageCount wins, else ⌈rowCount / pageSize⌉). Taking them here keeps aria-rowcount
4621
+ // consistent with bodyAriaRowIndex, which already offsets by pageIndex * pageSize and would
4622
+ // otherwise hand a screen reader an index LARGER than the advertised count on any page past
4623
+ // the first. `pageCount * pageSize` is an upper bound (the last page is usually partial) —
4624
+ // an over-count is the safe direction: an aria-rowindex can never exceed it.
4625
+ // With `manual` and NEITHER count set, table-core pins getCanNextPage() to false, so pageIndex
4626
+ // stays 0, the offset is 0 and page-slice-as-total is self-consistent; we leave it alone.
3976
4627
  function totalRowCount() {
3977
4628
  if (!table) return (rows() || []).length;
4629
+ if (local.manual === true) {
4630
+ if (local.rowCount != null) return local.rowCount;
4631
+ if (local.pageCount != null) return local.pageCount * pageSize();
4632
+ }
3978
4633
  const fm = table.getFilteredRowModel();
3979
4634
  return fm && fm.rows ? fm.rows.length : (rows() || []).length;
3980
4635
  }
@@ -4013,6 +4668,27 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4013
4668
  return headerRowCount() + rowIndexOf(row) + ariaPageOffset() + 1;
4014
4669
  }
4015
4670
 
4671
+ // ── E-05: the COLUMN-axis twin of gridAriaRowCount / bodyAriaRowIndex ────────────────
4672
+ // WAI-ARIA: when aria-colcount is set, every cell must carry an aria-colindex and the count
4673
+ // must equal the largest index any cell declares. The row axis has had this since #13; the
4674
+ // column axis had NOTHING repo-wide (0 occurrences of aria-colcount/aria-colindex), which
4675
+ // matters most under column windowing — the rendered <td>s are a WINDOW, so without an
4676
+ // explicit index a screen reader counts the window and announces "column 3 of 6" while the
4677
+ // user sits in absolute column 41.
4678
+ //
4679
+ // The index space is `visibleCellsFor(row)` / the header group's leaf headers — the same one
4680
+ // `colIndexOf` and `data-col-index` already use, INCLUDING the auto-injected select/expander
4681
+ // chrome columns (they are real columns in that list). `visibleColCount()` counts the same
4682
+ // list, so count and indices are mutually consistent by construction.
4683
+ //
4684
+ // NB named gridAriaColCount, NOT ariaColCount: `Element.ariaColCount` / `.ariaColIndex` are
4685
+ // inherited ARIA-reflected properties, so a same-named method becomes a Lit class field that
4686
+ // shadows them → TS2416 cascading to every @property decorator. Exactly the rename discipline
4687
+ // totalRowCount / gridAriaRowCount / bodyAriaRowIndex already document above.
4688
+ function gridAriaColCount() {
4689
+ return visibleColCount();
4690
+ }
4691
+
4016
4692
  // Column count = the visible cell list length (uniform header+body in a flat grid). Reads
4017
4693
  // $data.rows (reactive) so it is fine-grained-correct on Solid/Lit; falls back to the
4018
4694
  // header leaf count when there are no body rows.
@@ -4270,6 +4946,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4270
4946
  // Enter/F2 → enter interaction mode: focus the active cell's FIRST interactive control
4271
4947
  // (D-07 — uniform for header sort buttons and body controls; Enter does NOT sort directly).
4272
4948
  // No-op (stay in navigation mode) if the cell has no focusable control.
4949
+ // True when the active cell holds its own focusable controls (a link, a button, a drop-in
4950
+ // widget). Enter then ENTERS them (enterControl) rather than activating the row (row-activate).
4951
+ function activeCellHasControls() {
4952
+ return focusables(currentCellEl()).length > 0;
4953
+ }
4273
4954
  function enterControl() {
4274
4955
  const cellEl = currentCellEl();
4275
4956
  const list = focusables(cellEl);
@@ -4362,7 +5043,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4362
5043
  // nextRow/nextCol locals and FALLS THROUGH to the shared focus seam (like Ctrl+Home/End). ──
4363
5044
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && !activeIsHeader() && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
4364
5045
  e.preventDefault();
4365
- 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());
5046
+ // B-04: measured from the range's FOCUS corner, which is what extendRange moves — not
5047
+ // from the active cell, which a drag-select or Ctrl+A leaves somewhere else entirely.
5048
+ 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());
4366
5049
  return;
4367
5050
  } else if ((e.ctrlKey || e.metaKey) && !activeIsHeader() && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
4368
5051
  e.preventDefault();
@@ -4460,11 +5143,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4460
5143
  // gated by clipboardActiveAllowed() (== !activeIsHeader) so a header-active Ctrl+C/Ctrl+V
4461
5144
  // falls through to NATIVE behavior — never preventDefault'd, never a silent body mutation
4462
5145
  // (copyRange/pasteRange also self-guard; the verb guard is what plan 63-09's Cut reuses). ──
4463
- else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed()) {
5146
+ // B-07: `clipboardWriteAvailable()` / `clipboardReadAvailable()` are part of the BRANCH
5147
+ // CONDITION, not a guard inside the verb. preventDefault ran first and copyRange/pasteRange
5148
+ // then returned early when `navigator.clipboard` was absent — an insecure `http://` origin, a
5149
+ // permissions policy, an older embedded webview — so the browser's own copy/paste was
5150
+ // suppressed and nothing replaced it: Ctrl+C did nothing whatsoever. Falling through leaves
5151
+ // the native default in charge, which is the only correct behaviour when we cannot act.
5152
+ else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed() && clipboardWriteAvailable()) {
4464
5153
  e.preventDefault();
4465
5154
  copyRange();
4466
5155
  return;
4467
- } else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed()) {
5156
+ } else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed() && clipboardReadAvailable()) {
4468
5157
  e.preventDefault();
4469
5158
  pasteRange();
4470
5159
  return;
@@ -4474,7 +5163,9 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4474
5163
  // Ctrl+C/Ctrl+V (clipboardActiveAllowed) so a header-active Ctrl+X falls through to NATIVE cut
4475
5164
  // and never silently clears a body cell (cutRange also self-guards). Placed beside the C/V
4476
5165
  // shortcuts, BEFORE the printable-key edit-entry branch (which excludes ctrl/meta). ──
4477
- else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed()) {
5166
+ // B-07 applies to Cut too, and gating it on WRITE availability is deliberate: a cut that
5167
+ // cannot copy is data destruction with nothing on the clipboard to paste back.
5168
+ else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && clipboardActiveAllowed() && clipboardWriteAvailable()) {
4478
5169
  e.preventDefault();
4479
5170
  cutRange();
4480
5171
  return;
@@ -4526,6 +5217,44 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4526
5217
  if (!activeIsHeader()) selectAllBody();
4527
5218
  return;
4528
5219
  }
5220
+ // ── B-15: the APG grid pattern's row/column selection keys, and Escape in NAVIGATION mode.
5221
+ // All three were simply absent. They are placed HERE, before every `key === ' '` branch
5222
+ // below, because the boolean in-place toggle matches a bare `' '` without excluding
5223
+ // modifiers — so a Shift+Space on a checkbox cell would have toggled the checkbox instead
5224
+ // of selecting the row, and Ctrl+Space likewise.
5225
+ //
5226
+ // Shift+Space selects the ROW (table-core's own rowSelection slice, through the row's
5227
+ // toggleSelected — the same funnel the per-row checkbox uses, so the change event and the
5228
+ // two-way model behave identically). A no-op when selectionMode is 'none', which is the
5229
+ // honest behaviour: there is no row selection to make.
5230
+ //
5231
+ // Ctrl+Space selects the COLUMN as a full-height CELL RANGE rather than inventing a third
5232
+ // selection concept — the same corners Ctrl+A and shift+arrow drive, so Ctrl+C / Ctrl+X /
5233
+ // Delete / the fill handle all act on it unchanged.
5234
+ //
5235
+ // Escape in navigation mode collapses the range. In-control mode already consumed Escape at
5236
+ // the top of this handler; in navigation mode it did nothing at all, leaving a user who had
5237
+ // shift-arrowed out a rectangle with no keyboard way to drop it short of an arrow key, which
5238
+ // also MOVES. Only preventDefault'd when there is a range to clear, so a consumer's own
5239
+ // Escape handling on a table with no selection is untouched.
5240
+ else if (key === ' ' && e.shiftKey && !e.ctrlKey && !e.metaKey && !activeIsHeader()) {
5241
+ e.preventDefault();
5242
+ if (local.selectionMode !== 'none') {
5243
+ const selRow = (rows() || [])[activeRow()];
5244
+ if (selRow && selRow.toggleSelected) selRow.toggleSelected(!rowIsSelected(selRow));
5245
+ }
5246
+ return;
5247
+ } else if (key === ' ' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !activeIsHeader()) {
5248
+ e.preventDefault();
5249
+ selectActiveColumn();
5250
+ return;
5251
+ } else if (key === 'Escape') {
5252
+ if (rangeActive || rangeAnchor() != null) {
5253
+ e.preventDefault();
5254
+ clearRange();
5255
+ }
5256
+ return;
5257
+ }
4529
5258
  // ── Full-row edit entry (phase 51 req-6 / D-06) — Shift+F2 on an editable active cell puts
4530
5259
  // EVERY editable cell in the active row into edit at once. Tested BEFORE the plain F2 branch
4531
5260
  // (a Shift+F2 must NOT fall through to single-cell F2). Shift+F2 was chosen for the lowest
@@ -4605,6 +5334,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4605
5334
  // rAF poll must not steal focus back after the user has already ArrowDown'd to another row.
4606
5335
  recoverGridFocus(String(grpRow), grpCol, null, true);
4607
5336
  return;
5337
+ }
5338
+ // ── row-activate (quick 260925-dtl): Enter on a NON-editable body cell with no controls of
5339
+ // its own opens the row (a list-style "open this item"). Reached only after every edit-entry
5340
+ // and group-toggle branch above has declined; a cell WITH controls keeps entering them.
5341
+ else if (key === 'Enter' && !activeIsHeader() && !activeCellHasControls()) {
5342
+ e.preventDefault();
5343
+ activateRowAt(activeRow(), 'keyboard');
5344
+ return;
4608
5345
  } else if (key === 'Enter' || key === 'F2') {
4609
5346
  e.preventDefault();
4610
5347
  enterControl();
@@ -4699,11 +5436,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4699
5436
  // focusin that follows a Shift+Click (rangeClickPending): @mousedown already set the range
4700
5437
  // BEFORE this focusin fires, and a focusin carries no reliable shiftKey, so the @mousedown
4701
5438
  // path owns the shift case and flags it here so the collapse is skipped.
5439
+ // B-11: `rangeClickPending` carries the CELL the Shift+Click landed on, not a bare boolean.
5440
+ // A boolean leaks: Shift+Click on the cell that ALREADY holds focus fires no focusin at all,
5441
+ // so nothing consumed the flag, and the next ordinary navigation click (or Tab) hit this
5442
+ // branch instead and skipped its collapse — leaving a stale rectangle painted while the
5443
+ // active cell moved away. Matching on the coordinates makes a leak self-healing: a focusin on
5444
+ // any OTHER cell falls through to clearRange() and clears the flag on the way past.
4702
5445
  if (rangeTransition) {
4703
5446
  rangeTransition = false;
4704
- } else if (rangeClickPending) {
4705
- rangeClickPending = false;
5447
+ } else if (rangeClickPending && rangeClickPending.isHeader === isHeader && rangeClickPending.r === movedRow && rangeClickPending.c === col) {
5448
+ rangeClickPending = null;
4706
5449
  } else {
5450
+ rangeClickPending = null;
4707
5451
  clearRange();
4708
5452
  }
4709
5453
  // The cell box (not an inner control) receiving focus = navigation mode.
@@ -4741,13 +5485,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4741
5485
  setActiveIsHeader(false);
4742
5486
  setActiveRow(row);
4743
5487
  setActiveColIndex(col);
4744
- rangeClickPending = true;
5488
+ // B-11: arm the skip for THIS cell only (see syncActiveFromEvent). Body cell → isHeader
5489
+ // false; the header path never reaches here (rowAttr === '__header' returned above).
5490
+ rangeClickPending = {
5491
+ isHeader: false,
5492
+ r: row,
5493
+ c: col
5494
+ };
4745
5495
  return;
4746
5496
  }
4747
- // §6 plain mousedown → begin a document-level drag-select anchored at this cell. The mousedown's
4748
- // native focusin commits the ACTIVE cell to (row,col); beginRangeDrag's first cross-cell
4749
- // pointermove paints the range via setRangeFocus (anchored at the active cell). A mousedown with
4750
- // no move collapses to a single active cell (no range).
5497
+ // B-16: a mousedown INSIDE the cell that is currently in edit belongs to the editor — it is
5498
+ // the user placing a caret or drag-selecting TEXT. Starting a cell drag-select from it paints
5499
+ // a grid range over the very cell being typed into, and (with `undoable`) a drag that leaves
5500
+ // the cell then lets Delete/Ctrl+X act on a rectangle the user was never selecting. The
5501
+ // editor owns the gesture; return before any document listener is attached.
5502
+ if (isEditing(row, col)) return;
5503
+ // §6 plain mousedown → begin a document-level drag-select anchored at this cell. The anchor is
5504
+ // passed EXPLICITLY into setRangeFocus by beginRangeDrag (B-01) rather than seeded from the
5505
+ // active cell, which is still the PRE-mousedown one at this point (native focus is a default
5506
+ // action that runs after this handler). A mousedown with no move collapses to a single active
5507
+ // cell (no range).
4751
5508
  beginRangeDrag(row, col);
4752
5509
  }
4753
5510
 
@@ -4804,7 +5561,52 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4804
5561
  // editor). A plain click on an EDITABLE body cell opens its editor via the SAME beginEdit funnel;
4805
5562
  // shift+click (range extend) and non-editable cells are unaffected. Same closest/parse/header-skip
4806
5563
  // /finite guards as onGridDblClick. isGrid()-gated so 'table' mode never runs it.
5564
+ // ── row-activate (quick 260925-dtl, oinbox dogfooding) ─────────────────────────────────────
5565
+ // `row-activate { row, index, trigger }`: `row` = the original data object, `index` = its
5566
+ // position in the rendered model ($data.rows — page-relative, or the full model when virtual).
5567
+ // Group-header rows never activate; detail rows carry no data-row so a click there never
5568
+ // resolves a row.
5569
+ function activateRowAt(index: any, trigger: any) {
5570
+ const row = (rows() || [])[index];
5571
+ if (!row || rowIsGrouped(row) || rowIsLazyPlaceholder(row)) return;
5572
+ _props.onRowActivate?.({
5573
+ row: row.original,
5574
+ index,
5575
+ trigger
5576
+ });
5577
+ }
5578
+
5579
+ // Controls a click must not activate THROUGH — the selection checkbox, the expander, links,
5580
+ // drop-in widgets and an open editor. Matched only INSIDE the clicked cell, so a host page that
5581
+ // happens to wrap the table in one of these does not silence every row.
5582
+ const ROW_ACTIVATE_CONTROLS = 'input, button, a[href], select, textarea, label, summary, [contenteditable=""], [contenteditable="true"], [role="button"], [role="checkbox"], [role="link"], [data-editing-cell]';
5583
+
5584
+ // Both interaction modes: a plain click on a body data cell, outside any control. Not a
5585
+ // modified click (shift/ctrl/meta are selection gestures), not the end of a drag-select, and
5586
+ // not a singleClickEdit gesture on an editable grid cell (that click opens the editor instead).
5587
+ function maybeActivateRowFromClick(e: any) {
5588
+ if (!e || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
5589
+ if (rangeDragMoved) return;
5590
+ const tgt = e.target;
5591
+ if (!tgt || !tgt.closest) return;
5592
+ const cellEl = tgt.closest('[data-row]');
5593
+ if (!cellEl) return;
5594
+ const control = tgt.closest(ROW_ACTIVATE_CONTROLS);
5595
+ if (control && cellEl.contains(control)) return;
5596
+ const rowAttr = cellEl.getAttribute('data-row');
5597
+ if (rowAttr == null || rowAttr === '__header') return;
5598
+ const index = parseInt(rowAttr, 10);
5599
+ if (!Number.isFinite(index)) return;
5600
+ if (isGrid() && local.singleClickEdit) {
5601
+ const colAttr = cellEl.getAttribute('data-col-index');
5602
+ const col = colAttr == null ? NaN : parseInt(colAttr, 10);
5603
+ const colId = Number.isFinite(col) ? columnIdAt(index, col) : null;
5604
+ if (colId != null && columnEditable(colId)) return;
5605
+ }
5606
+ activateRowAt(index, 'click');
5607
+ }
4807
5608
  function onGridClick(e: any) {
5609
+ maybeActivateRowFromClick(e);
4808
5610
  if (!isGrid() || !e) return;
4809
5611
  if (!local.singleClickEdit) return;
4810
5612
  if (e.shiftKey) return;
@@ -5000,12 +5802,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5000
5802
  // UNCHANGED (span = the header's own colSpan, or 1) when columns are not windowed — the
5001
5803
  // off-path the template's `:colspan="wh.span > 1 ? wh.span : null"` binding preserves
5002
5804
  // byte-identically (a bare colspan="1" would change the emitted markup on the unwindowed path).
5805
+ //
5806
+ // A-03: the emitted entry also carries the WIDTH of the in-window leaves, because the colspan
5807
+ // clamp above is only half the job. `thStyle` declared `Header.getSize()` — the sum of ALL
5808
+ // descendant leaves — against a colspan clamped to the leaves actually rendered. Under
5809
+ // `table-layout: fixed` a first-row spanning cell's declared width is divided across the
5810
+ // columns it spans, and only the first row establishes widths, so a group straddling the window
5811
+ // edge inflated every column it still rendered: measured on "Group A" (cols 10-14 @150px,
5812
+ // getSize() 750), each rendered group column went 150 -> 187.5 -> 250 -> 375 -> 750px as the
5813
+ // group scrolled out, and scrollWidth with it, 9000 -> 9600. Identical on all five light-DOM
5814
+ // targets. This is the SAME getSize()-vs-colspan mismatch the earlier Column.getSize() ->
5815
+ // Header.getSize() fix cured for the fully-in-window case, inverted: that one was a 5x
5816
+ // COLLAPSE, this is a 5x EXPANSION for the partial case. Summing the same leaves the clamp
5817
+ // counted keeps the declared width and the colspan describing the same set by construction.
5003
5818
  function windowedHeadersFor(hg: any, hgLevel: any) {
5004
5819
  const headers = hg && hg.headers || [];
5005
5820
  if (!colsWindowed()) {
5006
5821
  return headers.map((h: any) => ({
5007
5822
  header: h,
5008
- span: h && h.colSpan > 1 ? h.colSpan : 1
5823
+ span: h && h.colSpan > 1 ? h.colSpan : 1,
5824
+ width: null
5009
5825
  }));
5010
5826
  }
5011
5827
  const idx = windowedColIndices();
@@ -5020,10 +5836,15 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5020
5836
  const end = cursor + leafCount;
5021
5837
  cursor = end;
5022
5838
  let span = 0;
5023
- for (let c = start; c < end; c++) if (idxSet[c]) span = span + 1;
5839
+ let width = 0;
5840
+ for (let c = start; c < end; c++) if (idxSet[c]) {
5841
+ span = span + 1;
5842
+ width = width + columnSize(c);
5843
+ }
5024
5844
  if (span > 0) out.push({
5025
5845
  header: h,
5026
- span
5846
+ span,
5847
+ width
5027
5848
  });
5028
5849
  }
5029
5850
  return out;
@@ -5036,6 +5857,36 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5036
5857
  function windowedColSpan() {
5037
5858
  return colsWindowed() ? windowedColIndices().length + 2 : visibleColCount();
5038
5859
  }
5860
+ // ── A-02: a column RESIZE must invalidate the column virtualizer's measurement cache ──────
5861
+ // `setOptions` is not enough, and this is a property of virtual-core, not of how we call it.
5862
+ // `getMeasurements()` is memoized on `[getMeasurementOptions(), itemSizeCacheVersion]`, and
5863
+ // `getMeasurementOptions()` is itself memoized on count / paddingStart / scrollMargin /
5864
+ // getItemKey / enabled / lanes / laneAssignmentMode — `estimateSize` is NOT among them. The
5865
+ // column axis feeds its widths ENTIRELY through `estimateSize: (i) => columnSize(i)` (it never
5866
+ // calls `measureElement`), so after a resize the memo returns the array built from the OLD
5867
+ // widths: measured on a 60x150px grid, dragging one column +1200px left `scrollWidth`
5868
+ // oscillating 9000 <-> 10200 as that column entered and left the window, and at scrollLeft 6000
5869
+ // the window started at index 37 where fresh geometry predicts 29. `measure()` is the only API
5870
+ // that bumps `itemSizeCacheVersion`, and it appeared ZERO times in this package or in
5871
+ // headless-core. Same defect for `resetColumnSizing()` and any programmatic `columnSizing` write.
5872
+ //
5873
+ // Guarded on a SIGNATURE rather than called unconditionally. `columnResizeMode` is 'onChange',
5874
+ // so `writeColumnSizing` fires on every pointermove of a drag; `measure()` clears the size cache
5875
+ // and calls `notify(false)`, which bumps `$data.windowVer` and therefore re-renders. Hashing the
5876
+ // widths (order-sensitive, so a pure A<->B swap that preserves the sum still trips it) makes the
5877
+ // steady state a ~N-integer-op no-op and guarantees the notify cannot feed itself: the second
5878
+ // pass sees an unchanged signature and returns.
5879
+ let lastColSizeSig = 0;
5880
+ function remeasureColumnSizes() {
5881
+ if (!colsWindowed() || !colVirtualizer || !colVirtualizer.measure) return;
5882
+ const n = columnCount();
5883
+ let sig = n;
5884
+ for (let i = 0; i < n; i++) sig = Math.imul(sig, 31) + columnSize(i) | 0;
5885
+ if (sig === lastColSizeSig) return;
5886
+ lastColSizeSig = sig;
5887
+ colVirtualizer.measure();
5888
+ }
5889
+
5039
5890
  // remeasureColumnWindow() (Phase 87 87-04): push a fresh column count/size into the SECOND
5040
5891
  // (horizontal) virtualizer on every row-model refresh — mirrors the row axis's
5041
5892
  // virtualizer.setOptions(virtualizerOptions()) + _willUpdate() re-feed in refreshRowModel, so a
@@ -5049,6 +5900,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5049
5900
  function remeasureColumnWindow() {
5050
5901
  if (!colsWindowed() || !colVirtualizer) return;
5051
5902
  colVirtualizer.setOptions(columnVirtualizerOptions());
5903
+ remeasureColumnSizes();
5052
5904
  colVirtualizer._willUpdate();
5053
5905
  }
5054
5906
 
@@ -5085,8 +5937,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5085
5937
  let rangeTransition = false;
5086
5938
  // rangeClickPending: set by onGridMouseDown on a Shift+Click (the range is set off the
5087
5939
  // pointer event's shiftKey BEFORE the cell's focusin fires); the follow-up focusin reads it
5088
- // to SKIP the range-collapse (a focusin carries no reliable shiftKey). Reset on consumption.
5089
- let rangeClickPending = false;
5940
+ // to SKIP the range-collapse (a focusin carries no reliable shiftKey).
5941
+ // B-11: it holds the CELL `{ isHeader, r, c }` the Shift+Click landed on, not a boolean, and
5942
+ // is null when idle. Shift+Click on the already-focused cell fires no focusin, so a boolean
5943
+ // was left armed and the NEXT ordinary click/Tab consumed it and skipped its own collapse,
5944
+ // stranding a painted rectangle. Coordinate-matched, that leak is self-healing: a focusin on
5945
+ // any other cell clears it and collapses normally. Cleared on BOTH branches of the consume.
5946
+ let rangeClickPending: any = null;
5090
5947
  // B19: a SYNCHRONOUS mirror of "a range currently exists" — extendRange/setRangeFocus set it
5091
5948
  // true, clearRange/clampRange-to-empty set it false. clearRange is invoked TWICE in one plain-
5092
5949
  // arrow keydown (the explicit collapse + the focusin that follows the programmatic focus move);
@@ -5155,7 +6012,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5155
6012
  // pre-write value (ROZ138). extendRange/setRangeFocus thread the just-computed locals through
5156
6013
  // here so the emitted payload matches the write. The single call site keeps the count
5157
6014
  // predictable (React multi-emit dedup, D-07). One-way notification.
6015
+ // B-10 — announce the selection alongside the emit. This is the SINGLE site every range
6016
+ // mutation funnels through (shift+arrow via extendRange, shift+click and drag-select and
6017
+ // fill-drag via setRangeFocus, Ctrl+A via selectAllBody), so one announce here covers all of
6018
+ // them; guarding the callers individually would be five chances to miss one.
6019
+ //
6020
+ // Writes $data.rangeAnnounce directly rather than importing clipboardFill's announce(): that
6021
+ // helper drives pasteAnnounce, and a range move must not overwrite a paste/validation message
6022
+ // the user has not heard yet (hence the separate key + region). `polite` + `aria-atomic` means
6023
+ // a fast drag replaces the pending text instead of queueing every intermediate rectangle.
6024
+ function rangeSummary(anchor: any, focus: any) {
6025
+ if (!anchor || !focus) return '';
6026
+ const rows = Math.abs(focus.rowIndex - anchor.rowIndex) + 1;
6027
+ const cols = Math.abs(focus.colIndex - anchor.colIndex) + 1;
6028
+ if (rows === 1 && cols === 1) return '1 cell selected';
6029
+ const rowPart = rows + (rows === 1 ? ' row' : ' rows');
6030
+ const colPart = cols + (cols === 1 ? ' column' : ' columns');
6031
+ return rowPart + ' by ' + colPart + ' selected, ' + rows * cols + ' cells';
6032
+ }
5158
6033
  function emitRangeChange(anchor: any, focus: any) {
6034
+ setRangeAnnounce(rangeSummary(anchor, focus));
5159
6035
  _props.onRangeChange?.({
5160
6036
  anchor,
5161
6037
  focus
@@ -5211,14 +6087,39 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5211
6087
  }
5212
6088
  }
5213
6089
 
5214
- // setRangeFocus(rIdx, cIdx): set the moving corner to an explicit cell (Shift+Click),
5215
- // seeding the anchor from the active cell when no range exists yet. Clamped to bounds.
5216
- // Emits range-change from this single site.
5217
- function setRangeFocus$local(rIdx: any, cIdx: any) {
6090
+ // setRangeFocus(rIdx, cIdx, anchorR, anchorC): set the moving corner to an explicit cell
6091
+ // (Shift+Click, or a drag-select's pointermove), seeding the anchor when no range exists yet.
6092
+ // Clamped to bounds. Emits range-change from this single site.
6093
+ //
6094
+ // B-01: the anchor seed is EXPLICIT when the caller knows it. Seeding it from
6095
+ // `$data.activeRow`/`$data.activeColIndex` is only correct if that state is live at the moment
6096
+ // the seed happens — and inside a drag-select it is not on React, where the document
6097
+ // pointermove closure captures the render-scoped consts from the render that ran BEFORE the
6098
+ // mousedown. Native focus moves as mousedown's DEFAULT ACTION, after handlers return, so at
6099
+ // `beginRangeDrag` time the active cell is still the PRE-mousedown one and React can never
6100
+ // show that closure a newer value. Dragging (5,2)→(7,3) on a fresh grid anchored at (0,0) and
6101
+ // selected the whole rectangle from there — which Ctrl+X and Delete then destroy.
6102
+ // The other five targets read `$data.activeRow` LIVE inside `move` (by then focusin has synced
6103
+ // it), so they were accidentally correct; taking the anchor from the caller removes the read
6104
+ // entirely and makes all six correct for the same reason. Falls back to the active cell when no
6105
+ // explicit anchor is passed — Shift+Click, where "the anchor is where you already were" IS the
6106
+ // active cell and the read happens synchronously in the same handler.
6107
+ // The two anchor params carry EXPLICIT `= null` defaults, and that is load-bearing, not style:
6108
+ // the emitted per-target signature types every declared param as REQUIRED, so the Shift+Click
6109
+ // call site's two-argument `setRangeFocus(row, col)` fails the strict leaf typecheck with
6110
+ // TS2554 "Expected 4 arguments, but got 2" on react and solid. Same rule the `focusActiveCell`
6111
+ // comment in gridKeydownHandlers records for its own optional params.
6112
+ function setRangeFocus$local(rIdx: any, cIdx: any, anchorR = null, anchorC = null) {
5218
6113
  const maxRow = bodyRowCount() - 1;
5219
6114
  const maxCol = visibleColCount() - 1;
5220
6115
  if (maxRow < 0 || maxCol < 0) return;
5221
6116
  let anchor = rangeAnchor();
6117
+ if (!anchor && anchorR != null && anchorC != null) {
6118
+ anchor = {
6119
+ rowIndex: clamp(Math.trunc(Number(anchorR)) || 0, 0, maxRow),
6120
+ colIndex: clamp(Math.trunc(Number(anchorC)) || 0, 0, maxCol)
6121
+ };
6122
+ }
5222
6123
  if (!anchor) anchor = {
5223
6124
  rowIndex: activeRow(),
5224
6125
  colIndex: activeColIndex()
@@ -5259,6 +6160,53 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5259
6160
  emitRangeChange(anchor, focus);
5260
6161
  }
5261
6162
 
6163
+ // ── B-04: where does a range EXTENSION measure its delta from? ──────────────────────────
6164
+ // `extendRange(dRow, dCol)` applies its delta to the range's FOCUS corner, not to the active
6165
+ // cell. For shift+arrow the two always agree (extendRange writes the active cell to the new
6166
+ // focus on every step), but two paths deliberately move the focus corner WITHOUT syncing the
6167
+ // active cell: a drag-select (`setRangeFocus`, which leaves the active cell on the mousedown
6168
+ // anchor) and Ctrl+A (`selectAllBody`, which sets both corners outright). After either,
6169
+ // Ctrl/Cmd+Shift+Arrow computed "distance from the active cell to the edge" and then added it
6170
+ // to a DIFFERENT cell. Drag (5,2)->(7,3) then Ctrl+Shift+ArrowUp extended to row 2 instead of
6171
+ // row 0; Ctrl+Shift+ArrowLeft to column 1 instead of column 0 — an extend-to-edge that stops
6172
+ // short of the edge by exactly the gap between the two.
6173
+ //
6174
+ // These report the corner extendRange will actually move, so the delta and its target are the
6175
+ // same cell by construction. No range yet -> the active cell IS the focus corner.
6176
+ function rangeFocusRow() {
6177
+ const f = rangeFocus();
6178
+ return f && f.rowIndex != null ? f.rowIndex : activeRow();
6179
+ }
6180
+ function rangeFocusCol() {
6181
+ const f = rangeFocus();
6182
+ return f && f.colIndex != null ? f.colIndex : activeColIndex();
6183
+ }
6184
+
6185
+ // B-15: the APG grid pattern's Ctrl+Space — select the COLUMN containing the focused cell.
6186
+ // Expressed as a full-height rectangle over the SAME range corners shift+arrow and Ctrl+A
6187
+ // drive, so Ctrl+C / Ctrl+X / Delete / the fill handle all act on it with no new machinery
6188
+ // and no second selection concept for a consumer to reason about. Emits through the single
6189
+ // emitRangeChange funnel (fresh corners, never a $data re-read). No-op on an empty grid, and
6190
+ // header-active is gated OUT by the caller, exactly as selectAllBody is.
6191
+ function selectActiveColumn() {
6192
+ const maxRow = bodyRowCount() - 1;
6193
+ const maxCol = visibleColCount() - 1;
6194
+ if (maxRow < 0 || maxCol < 0) return;
6195
+ const c = clamp(Math.trunc(Number(activeColIndex())) || 0, 0, maxCol);
6196
+ const anchor = {
6197
+ rowIndex: 0,
6198
+ colIndex: c
6199
+ };
6200
+ const focus = {
6201
+ rowIndex: maxRow,
6202
+ colIndex: c
6203
+ };
6204
+ setRangeAnchor(anchor);
6205
+ setRangeFocus(focus);
6206
+ rangeActive = true;
6207
+ emitRangeChange(anchor, focus);
6208
+ }
6209
+
5262
6210
  // clearRange(): drop the rectangle (a non-shift navigation / edit-entry collapses any
5263
6211
  // range back to a single active cell). Cheap no-op when no range is set (the guard keeps a
5264
6212
  // plain navigation with no active range from emitting). B19: when a range DID exist, emit
@@ -5395,6 +6343,20 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5395
6343
 
5396
6344
  // copyRange(): write the current range as TSV to the clipboard (async). No-op when the
5397
6345
  // async Clipboard API is unavailable (older/insecure contexts) — a copy is best-effort.
6346
+ // B-07: is the async Clipboard API actually usable? Ctrl+C / Ctrl+X / Ctrl+V used to
6347
+ // `e.preventDefault()` FIRST and only then call a verb that self-guards on
6348
+ // `navigator.clipboard`, so on any page where the API is absent — an insecure `http://` origin,
6349
+ // a permissions policy, an older embedded webview — the browser's OWN copy/cut/paste was
6350
+ // suppressed and nothing took its place. The keystroke did nothing at all. These predicates
6351
+ // move the check into the keymap's branch CONDITION (gridKeydownHandlers.rzts), so an
6352
+ // unavailable API means the branch does not match and the native default runs. Separate
6353
+ // read/write predicates because a context can grant one and not the other.
6354
+ function clipboardWriteAvailable() {
6355
+ return typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.writeText === 'function';
6356
+ }
6357
+ function clipboardReadAvailable() {
6358
+ return typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.readText === 'function';
6359
+ }
5398
6360
  function copyRange() {
5399
6361
  // B11: never copy from a header-active state (the reusable clipboard guard).
5400
6362
  if (!clipboardActiveAllowed()) return;
@@ -5413,15 +6375,33 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5413
6375
  // running fresh array (replaceRowValue) and record the committed cell. After the walk: ONE
5414
6376
  // writeData (the single r-model:data write), ONE cell-edit-commit per COMMITTED cell, and the
5415
6377
  // N-of-M aria-live announce. Returns { wrote, total }.
5416
- function applyGridToRange(grid: any, originRow: any, originCol: any) {
6378
+ // B-09 — `verb` names the OPERATION in the aria-live summary. This funnel serves paste, cut,
6379
+ // range-delete AND fill, so hardcoding "pasted" made a Cut announce "3 of 3 cells pasted" and a
6380
+ // Delete announce the same — actively wrong for a screen-reader user, who has no other signal
6381
+ // that the destructive operation they invoked actually happened. Defaults to 'pasted' so the
6382
+ // paste path's wording is unchanged.
6383
+ function applyGridToRange(grid: any, originRow: any, originCol: any, verb: any) {
6384
+ const opVerb = typeof verb === 'string' && verb !== '' ? verb : 'pasted';
5417
6385
  const maxRow = bodyRowCount() - 1;
5418
6386
  const maxCol = visibleColCount() - 1;
5419
6387
  if (maxRow < 0 || maxCol < 0) return {
5420
6388
  wrote: 0,
6389
+ changed: 0,
5421
6390
  total: 0
5422
6391
  };
6392
+ // B-08: `applied` and `committed` are DIFFERENT counts and used to be one.
6393
+ // applied — targets whose value now equals what was pasted, INCLUDING the ones that
6394
+ // already did. This is the announce's numerator: from the user's point of
6395
+ // view a cell that already held the pasted value was pasted successfully, and
6396
+ // reporting "3 of 5" for a 5-cell paste where 2 matched would be a lie.
6397
+ // committed — targets whose value actually CHANGED. This drives the write, the per-cell
6398
+ // `cell-edit-commit` emits and (through writeData) the undo step.
6399
+ // Conflating them meant an unchanged cell emitted an edit event and recorded an undo entry:
6400
+ // a 1->5 fill fired 5 commits when 4 cells already held the value, and a Delete over an
6401
+ // already-empty cell pushed an undo step that undoes nothing. Consumers persisting on
6402
+ // `cell-edit-commit` were issuing writes for data that had not moved.
5423
6403
  let total = 0;
5424
- let wrote = 0;
6404
+ let applied = 0;
5425
6405
  const committed = [];
5426
6406
  // Build the fresh data array incrementally so the whole paste is ONE writeData.
5427
6407
  let next = currentData();
@@ -5435,15 +6415,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5435
6415
  // it to this single funnel, so guarding the four callers individually would be four
5436
6416
  // chances to miss one.
5437
6417
  const rowGrouped = rowIndexIsGrouped(r);
6418
+ // Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (virtual+manual+rowCount, a row not
6419
+ // loaded yet) has no real underlying record — rowOriginalAt(r) returns the placeholder
6420
+ // sentinel ({__rdtLazyPlaceholder:true,index:i}, virtualization.rzts), which is never IN
6421
+ // currentData() (only the consumer's own array is), so sourceIndexOfRow's data.indexOf(...)
6422
+ // is always -1 and falls back to the VISIBLE index — either a no-op write with a FABRICATED
6423
+ // cell-edit-commit, or, inside a sparse hole, the hole gets clobbered with a partial row via
6424
+ // replaceRowValue. Hoisted per-ROW exactly like rowGrouped above (the SAME single placement
6425
+ // closes pasteRange/cutRange/clearActiveRange/fillRange all at once).
6426
+ const rowPlaceholder = rowIsLazyPlaceholder((rows() || [])[r]);
5438
6427
  const cols = grid[gr] || [];
5439
6428
  for (let gc = 0; gc < cols.length; gc++) {
5440
6429
  const c = originCol + gc;
5441
6430
  if (c > maxCol) break;
5442
- // A group-row cell still counts toward `total` — it is a skipped TARGET, exactly like
5443
- // a non-editable or validator-rejected cell — so the "N of M cells pasted" / "no cells
5444
- // pasted" announce keeps its existing semantics (never silently under-reports M).
6431
+ // A group-row / placeholder-row cell still counts toward `total` — it is a skipped
6432
+ // TARGET, exactly like a non-editable or validator-rejected cell — so the "N of M cells
6433
+ // pasted" / "no cells pasted" announce keeps its existing semantics (never silently
6434
+ // under-reports M).
5445
6435
  total = total + 1;
5446
- if (rowGrouped) continue;
6436
+ if (rowGrouped || rowPlaceholder) continue;
5447
6437
  const colId = columnIdAt(r, c);
5448
6438
  if (colId == null || !columnEditable(colId)) continue;
5449
6439
  const rowObj = rowOriginalAt(r);
@@ -5457,6 +6447,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5457
6447
  const field = fieldOfColId(colId);
5458
6448
  const srcIndex = sourceIndexOfRow(r);
5459
6449
  const oldValue = rowObj ? rowObj[field] : null;
6450
+ applied = applied + 1;
6451
+ // B-08: the cell already holds this value — it counts as applied, but there is nothing
6452
+ // to write, nothing to emit and nothing to undo.
6453
+ if (oldValue === value) continue;
5460
6454
  next = replaceRowValue(next, srcIndex, field, value);
5461
6455
  committed.push({
5462
6456
  rowId: rowIdAt(r),
@@ -5464,22 +6458,37 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5464
6458
  oldValue,
5465
6459
  newValue: value
5466
6460
  });
5467
- wrote = wrote + 1;
5468
6461
  }
5469
6462
  }
5470
- if (wrote > 0) {
6463
+ if (committed.length > 0) {
5471
6464
  editTransition = true;
5472
6465
  writeData(next);
5473
6466
  editTransition = false;
5474
6467
  // One cell-edit-commit per COMMITTED cell (the per-cell event contract, D-03).
5475
- for (let i = 0; i < committed.length; i++) _props.onCellEditCommit?.(committed[i]);
6468
+ // A-08: each emit is guarded INDIVIDUALLY. These listeners are consumer code, and a single
6469
+ // throwing one used to abort the loop — so on a 40-cell paste, cells 0..k notified and
6470
+ // k+1..39 silently never did, leaving a consumer persisting on this event with a partial
6471
+ // write of a model that had already been fully updated. Measured while building this: a
6472
+ // throwing VALIDATOR is NOT the reachable vehicle here — `runValidator`
6473
+ // (editCellLifecycle.rzts) already catches and reports 'Invalid value' — so the listener
6474
+ // is the first-party code that actually escapes.
6475
+ for (let i = 0; i < committed.length; i++) {
6476
+ try {
6477
+ _props.onCellEditCommit?.(committed[i]);
6478
+ } catch (err: any) {
6479
+ 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);
6480
+ }
6481
+ }
5476
6482
  }
5477
6483
  // WR-02: announce the N-of-M summary only when at least one cell was written. When the paste
5478
6484
  // targeted real cells but every one was skipped (validation-failed / non-editable), announce a
5479
6485
  // distinct validation-failed message instead of a misleading "0 of M cells pasted".
5480
- if (wrote > 0) announce(wrote + ' of ' + total + ' cells pasted');else if (total > 0) announce('No cells pasted — ' + total + ' cells were invalid or read-only');
6486
+ if (applied > 0) announce(applied + ' of ' + total + ' cells ' + opVerb);else if (total > 0) announce('No cells ' + opVerb + ' — ' + total + ' cells were invalid or read-only');
6487
+ // `wrote` stays the ANNOUNCED count (targets satisfied), not the written one, so every
6488
+ // existing caller and assertion keeps its meaning; `changed` is the new, narrower number.
5481
6489
  return {
5482
- wrote,
6490
+ wrote: applied,
6491
+ changed: committed.length,
5483
6492
  total
5484
6493
  };
5485
6494
  }
@@ -5526,14 +6535,26 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5526
6535
  return;
5527
6536
  }
5528
6537
  if (!p || !p.then) return;
6538
+ // A-08: the two failure modes are separated. A REJECTED clipboard read (permission denied,
6539
+ // insecure context, an empty selection) is a legitimate silent no-op — that is the rejection
6540
+ // handler, the second argument. Anything the fulfilment handler throws is FIRST-PARTY: a
6541
+ // consumer's row accessor, a `data`-model setter, or a `cell-edit-commit` listener (which
6542
+ // `applyGridToRange` now also guards per-emit). A single trailing `.catch(() => {})`
6543
+ // swallowed both, so consumer code failing inside a paste produced no error, no warning and
6544
+ // nothing in the console to search for. NOT a column validator: `runValidator` already
6545
+ // catches a throwing one and reports 'Invalid value' — checked rather than assumed.
5529
6546
  p.then((text: any) => {
5530
- const grid = parseTsv(text);
5531
- if (!grid.length) return;
5532
- // C3: tile the clipboard block to fill the destination range (single→range fill,
5533
- // smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
5534
- const tiled = tileGridToBox(grid, destBox);
5535
- applyGridToRange(tiled, anchorRow, anchorCol);
5536
- }).catch(() => {});
6547
+ try {
6548
+ const grid = parseTsv(text);
6549
+ if (!grid.length) return;
6550
+ // C3: tile the clipboard block to fill the destination range (single→range fill,
6551
+ // smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
6552
+ const tiled = tileGridToBox(grid, destBox);
6553
+ applyGridToRange(tiled, anchorRow, anchorCol, 'pasted');
6554
+ } catch (err: any) {
6555
+ console.error('[rozie-data-table] paste failed: a row accessor, the data model setter or other ' + 'consumer code threw. The paste was abandoned.', err);
6556
+ }
6557
+ }, () => {/* clipboard read rejected (permission / insecure context) — silent by design */});
5537
6558
  }
5538
6559
 
5539
6560
  // cutRange(): C3 Cut — copy the current range to the clipboard (rangeToTsv — the SAME escaped
@@ -5567,7 +6588,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5567
6588
  for (let c = c0; c <= c1; c++) cols.push('');
5568
6589
  grid.push(cols);
5569
6590
  }
5570
- applyGridToRange(grid, r0, c0);
6591
+ applyGridToRange(grid, r0, c0, 'cut');
5571
6592
  }
5572
6593
 
5573
6594
  // clearActiveRange(): the §7 (260709-3qt) Delete/Backspace clear — cutRange() MINUS the clipboard
@@ -5593,7 +6614,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5593
6614
  for (let c = c0; c <= c1; c++) cols.push('');
5594
6615
  grid.push(cols);
5595
6616
  }
5596
- applyGridToRange(grid, r0, c0);
6617
+ applyGridToRange(grid, r0, c0, 'cleared');
5597
6618
  }
5598
6619
 
5599
6620
  // fillRange(sourceBox): drag-fill (D-04 — VALUE-COPY ONLY, no series detection). B7: the fill
@@ -5648,7 +6669,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5648
6669
  }
5649
6670
  grid.push(cols);
5650
6671
  }
5651
- applyGridToRange(grid, box.r0, box.c0);
6672
+ applyGridToRange(grid, box.r0, box.c0, 'filled');
5652
6673
  }
5653
6674
 
5654
6675
  // onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
@@ -5923,10 +6944,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5923
6944
  rangeDragging = false;
5924
6945
  }
5925
6946
 
5926
- // beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell. The
5927
- // mousedown's native focus/focusin already committed the ACTIVE cell to (anchorR, anchorC), so
5928
- // setRangeFocus (which seeds the anchor from the ACTIVE cell) spans mousedown-cell→pointer-cell —
5929
- // we NEVER write $data.rangeAnchor directly (it is React-stale, ROZ138). rangeDragMoved starts
6947
+ // beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell.
6948
+ // B-01: the anchor is handed to setRangeFocus EXPLICITLY, never left to be seeded from the
6949
+ // active cell. The mousedown's native focus is a DEFAULT ACTION that runs after this handler
6950
+ // returns, so at this point $data.activeRow/activeColIndex still hold the PRE-mousedown cell;
6951
+ // on React the pointermove closure below is frozen at that render and can never observe the
6952
+ // post-focusin value, so the whole gesture anchored at wherever the user happened to be
6953
+ // (a fresh grid: (0,0)). We still never WRITE $data.rangeAnchor directly (React-stale,
6954
+ // ROZ138) — setRangeFocus owns that write. rangeDragMoved starts
5930
6955
  // false and flips true only once the pointer reaches a DIFFERENT cell, so a mousedown-with-no-move
5931
6956
  // leaves a single active cell + no range (a normal click). lastCell dedups the many pointermove
5932
6957
  // events per cell (setRangeFocus emits range-change — only extend on a NEW cell, mirroring fillDrag's
@@ -5951,7 +6976,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5951
6976
  if (cell && (cell.r !== lastCell.r || cell.c !== lastCell.c)) {
5952
6977
  lastCell = cell;
5953
6978
  rangeDragMoved = true;
5954
- setRangeFocus$local(cell.r, cell.c);
6979
+ setRangeFocus$local(cell.r, cell.c, anchorR, anchorC);
5955
6980
  }
5956
6981
  };
5957
6982
  const up = () => {
@@ -5989,6 +7014,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5989
7014
  // row reaches the group-toggle branch below them instead of opening an editor.
5990
7015
  function isActiveCellEditable() {
5991
7016
  if (rowIndexIsGrouped(activeRow())) return false;
7017
+ // A lazy placeholder (a row not loaded yet) has nothing to edit (quick 260925-dtl part 2).
7018
+ if (rowIsLazyPlaceholder((rows() || [])[activeRow()])) return false;
5992
7019
  const colId = activeCellColumnId();
5993
7020
  return colId != null && columnEditable(colId);
5994
7021
  }
@@ -6220,6 +7247,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6220
7247
  // calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
6221
7248
  // does not run on that path at all.
6222
7249
  if (rowIndexIsGrouped(rowIndex)) return;
7250
+ if (rowIsLazyPlaceholder((rows() || [])[rowIndex])) return;
6223
7251
  const colId = columnIdAt(rowIndex, colIndex);
6224
7252
  if (colId == null || !columnEditable(colId)) return;
6225
7253
  // A new edit session starts — reset the sync idempotency latch so THIS session's eventual
@@ -6230,6 +7258,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6230
7258
  // editor clears any row-edit state so isEditing never resolves both modes for one cell.
6231
7259
  setEditingRowIndex(null);
6232
7260
  setRowDraft({});
7261
+ rowDraftSync = {};
6233
7262
  setEditingRow(rowIndex);
6234
7263
  setEditingCol(colIndex);
6235
7264
  setDraftValue(seed != null ? seed : cellValueAt(rowIndex, colIndex));
@@ -6258,6 +7287,21 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6258
7287
  const tryFocus = () => {
6259
7288
  const el = resolveCellEl(String(row), col);
6260
7289
  if (el) {
7290
+ // B-05 / F-07: do NOT stomp focus a later interaction has already placed on a DIFFERENT
7291
+ // grid cell. This poll runs up to 30 frames after commit/cancel, so without this guard a
7292
+ // user (or a test) who moves to the next cell in that window gets yanked BACK to the cell
7293
+ // they just left — and because the steal depends on rAF timing it presents as a flake that
7294
+ // rotates across targets rather than as a reproducible bug. Measured 2026-09-12: the
7295
+ // data-table-dropins editor case failed with the active cell pinned to the PREVIOUS column
7296
+ // (want 0/2, active 0/1; want 0/1, active 0/0) with no editor open.
7297
+ // focusEditorWhenReady already carries the equivalent guard against the same class of
7298
+ // theft; this is its missing twin. Compare the OWNING CELL (not node identity) so a
7299
+ // node-replacing re-render on Solid still resolves a genuinely dropped focus as recoverable.
7300
+ const ae = gridRoot && gridRoot.getRootNode ? gridRoot.getRootNode().activeElement : null;
7301
+ if (ae && ae !== el && gridRoot.contains && gridRoot.contains(ae) && ae.closest) {
7302
+ const aeCell = ae.closest('[data-grid-cell]');
7303
+ if (aeCell && aeCell !== el) return;
7304
+ }
6261
7305
  el.focus();
6262
7306
  return;
6263
7307
  }
@@ -6287,6 +7331,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6287
7331
  function endRowEdit() {
6288
7332
  setEditingRowIndex(null);
6289
7333
  setRowDraft({});
7334
+ rowDraftSync = {};
6290
7335
  setInvalidMsg('');
6291
7336
  setActiveInControl(false);
6292
7337
  setEditVer(editVer() + 1);
@@ -6315,7 +7360,28 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6315
7360
  // other editor type commits the value verbatim. Idempotent for the #editor drop-in path
6316
7361
  // (an already-numeric override passes through; an explicit null stays null).
6317
7362
  function coerceCellValue(colId: any, raw: any) {
6318
- if (editorTypeOf(colId) !== 'number') return raw;
7363
+ const kind = editorTypeOf(colId);
7364
+ // ── C-13 ────────────────────────────────────────────────────────────────────────────
7365
+ // This funnel exists so "no mixed/garbage types ever reach the model" (B9, T-63-03-01), and
7366
+ // it covered ONE of the four built-in editor types. Every path that writes without opening
7367
+ // the built-in control — paste, Cut, Delete/Backspace over a range, a fill-drag — arrives
7368
+ // here with a plain TSV STRING, so a `checkbox` column took `''` from a Delete and the
7369
+ // literal text `'true'` from a paste, straight into a field the consumer declared boolean.
7370
+ // `select` is deliberately NOT coerced: its model type IS the option string, so a string is
7371
+ // already correct there, and an out-of-range option is a validation question (C-03's class),
7372
+ // not a type one.
7373
+ //
7374
+ // Recognised spellings both ways; ANYTHING else — including '' and unrecognised text —
7375
+ // becomes `false`. That is lossy by construction and deterministic by design: the
7376
+ // alternative is letting a non-boolean reach a boolean field, which is the defect. It is
7377
+ // documented in the editing page's emptied-cell table.
7378
+ if (kind === 'checkbox') {
7379
+ if (typeof raw === 'boolean') return raw;
7380
+ if (raw == null) return false;
7381
+ if (typeof raw === 'number') return raw !== 0;
7382
+ return /^\s*(true|1|yes|y|on)\s*$/i.test(String(raw));
7383
+ }
7384
+ if (kind !== 'number') return raw;
6319
7385
  if (raw == null) return null;
6320
7386
  if (typeof raw === 'number') return Number.isNaN(raw) ? null : raw;
6321
7387
  const s = String(raw).trim();
@@ -6435,11 +7501,17 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6435
7501
  // checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
6436
7502
  // write ever targets a group row" a property of the write funnels themselves.
6437
7503
  if (rowIndexIsGrouped(activeRow())) return;
6438
- const colId = columnIdAt(activeRow(), activeColIndex());
6439
- if (colId == null || !columnEditable(colId)) return;
6440
7504
  const rowList = rows() || [];
6441
7505
  const row = rowList[activeRow()];
6442
7506
  if (!row) return;
7507
+ // Debug 260926-dtl-edit-guard: defense-in-depth, matching the redundant rowIndexIsGrouped
7508
+ // check just above (Layer 4 is reachable ONLY through the Layer-1-gated checkbox branch in
7509
+ // onGridKeyDown, whose isActiveCellEditable() already excludes a placeholder row — this makes
7510
+ // "no write ever targets a placeholder row" a property of the write funnel itself, not just of
7511
+ // its one caller).
7512
+ if (rowIsLazyPlaceholder(row)) return;
7513
+ const colId = columnIdAt(activeRow(), activeColIndex());
7514
+ if (colId == null || !columnEditable(colId)) return;
6443
7515
  const rowOriginal = row.original;
6444
7516
  const rowId = row.id;
6445
7517
  const oldValue = cellValueAt(activeRow(), activeColIndex());
@@ -6572,6 +7644,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6572
7644
  // redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
6573
7645
  // and the natural call site for a future #rowActions "Edit" drop-in button.
6574
7646
  if (rowIndexIsGrouped(rowIndex)) return;
7647
+ // Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (a row not loaded yet, virtual+manual+
7648
+ // rowCount) has nothing to edit — beginEdit (editCellLifecycle.rzts) already carries this
7649
+ // guard for single-cell entry; this full-row twin was missing it, so the public editRow()
7650
+ // verb (and, redundantly with isActiveCellEditable's own placeholder check, Shift+F2) could
7651
+ // seed a row editor from a placeholder's undefined fields and commit through the same
7652
+ // sourceIndexOfRow fallback bug applyGridToRange's placeholder skip closes.
7653
+ if (rowIsLazyPlaceholder((rows() || [])[rowIndex])) return;
6575
7654
  const editable = editableColumnsForRow(rowIndex);
6576
7655
  if (editable.length === 0) return;
6577
7656
  // A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
@@ -6591,6 +7670,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6591
7670
  draft[ec.colId] = orig ? orig[ec.field] : null;
6592
7671
  }
6593
7672
  setRowDraft(draft);
7673
+ rowDraftSync = draft;
6594
7674
  setEditingRowIndex(rowIndex);
6595
7675
  setActiveInControl(true);
6596
7676
  setEditVer(editVer() + 1);
@@ -6621,7 +7701,8 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6621
7701
  const r = rowList[rowIndex];
6622
7702
  const rowOriginal = r ? r.original : null;
6623
7703
  const rowId = r ? r.id : null;
6624
- const draft = rowDraft() || {};
7704
+ // C-05: the SYNC mirror, not `$data.rowDraft` — see rowDraftSync in DataTable.rozie.
7705
+ const draft = rowDraftSync || {};
6625
7706
  // Validate every edited column FIRST (D-01: a single failure blocks the whole row commit).
6626
7707
  // B3 (Rule 1): coerce each draft by the column's editor type BEFORE validation + write — a
6627
7708
  // 'number' editor must commit a real Number/null, never the raw editor STRING (the single-cell
@@ -6813,6 +7894,25 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6813
7894
  // A top-level `let` (React hoists to useRef → persists).
6814
7895
  let committedThisSession = false;
6815
7896
 
7897
+ // rowDraftSync (quick 260922-mkb, C-05): a SYNCHRONOUS mirror of `$data.rowDraft`, written at
7898
+ // every rowDraft write and read by commitRow. A custom #editor drop-in stages its value with
7899
+ // `commit(v)` → setRowDraft in the SAME keydown that then commits the row (the host's drop-in
7900
+ // wrapper runs in the bubble phase right after the drop-in's own Enter handler). On React
7901
+ // `$data.rowDraft` is a `useState` read, so commitRow in that same tick saw the pre-write draft,
7902
+ // found no change and wrote nothing. The built-in editors never hit this only because they stage
7903
+ // on every `input` event, long before Enter. Same top-level-`let` technique as
7904
+ // committedThisSession above; on the five synchronous targets it always equals $data.rowDraft.
7905
+ let rowDraftSync: Record<string, any> = {};
7906
+
7907
+ // dropinTabFlush (quick 260922-mkb, C-10): non-null only for the duration of the drop-in
7908
+ // wrapper's Tab flush (see onEditorDropinKeyDown). While set, a drop-in's `commit(v)` records
7909
+ // that it ran and whether it committed, and commits WITHOUT the focus return (the Tab handler
7910
+ // moves focus on to the next editor itself).
7911
+ let dropinTabFlush: {
7912
+ called: boolean;
7913
+ committed: boolean;
7914
+ } | null = null;
7915
+
6816
7916
  // ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
6817
7917
  // In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
6818
7918
  // (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
@@ -6841,6 +7941,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6841
7941
  setRowDraft$local(colId, value);
6842
7942
  return;
6843
7943
  }
7944
+ // C-10: during the drop-in wrapper's Tab flush, record the outcome and skip the focus
7945
+ // return — the Tab handler advances focus to the next editor itself.
7946
+ if (dropinTabFlush) {
7947
+ dropinTabFlush.called = true;
7948
+ // STICKY: only a successful commit may set the outcome. A re-entrant commit within the
7949
+ // same flush (any blur the write itself provokes) is latched to false by commitEdit and
7950
+ // must not clear it. Measured: before the Solid slot-fill fix this re-entry was real.
7951
+ if (commitEdit(value, true)) dropinTabFlush.committed = true;
7952
+ return;
7953
+ }
6844
7954
  commitEdit(value);
6845
7955
  };
6846
7956
  }
@@ -6878,10 +7988,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6878
7988
  // setRowDraft: write ONE key into a FRESH rowDraft object (whole-object replace — an
6879
7989
  // in-place mutation is silently dropped on React/Solid; the family immutable rule).
6880
7990
  function setRowDraft$local(colId: any, value: any) {
6881
- const src = rowDraft() || {};
7991
+ // C-05: build from the SYNC mirror, so two same-tick writes (and a same-tick commitRow) never
7992
+ // see a React-stale `$data.rowDraft`.
7993
+ const src = rowDraftSync || {};
6882
7994
  const next = {};
6883
7995
  for (const k in src) next[k] = src[k];
6884
7996
  next[colId] = value;
7997
+ rowDraftSync = next;
6885
7998
  setRowDraft(next);
6886
7999
  }
6887
8000
 
@@ -6971,6 +8084,105 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6971
8084
  }
6972
8085
  }
6973
8086
 
8087
+ // DECLARED AFTER onEditorKeyDown ON PURPOSE. It delegates to it, and the React emitter lowers
8088
+ // both to `useCallback`s whose DEPENDENCY ARRAY is evaluated EAGERLY at declaration time — so
8089
+ // with this one first, its deps array referenced `onEditorKeyDown` before the const existed and
8090
+ // every React cell in the family died with `ReferenceError: Cannot access 'ze' before
8091
+ // initialization` at mount. That is the 87-02 useCallback-deps-array ordering lesson, and it is
8092
+ // invisible on the other five targets. Keep this block below.
8093
+ // ── C-10 + C-05: the grid keymap for a CUSTOM #editor drop-in ───────────────────────────
8094
+ // The built-in editors bind `onEditorKeyDown` directly, so Tab commits-and-advances and, in
8095
+ // row mode, Enter commits the row. A drop-in binds nothing of ours: it is consumer markup
8096
+ // inside the `#editor` slot. Its own keydown handler covers Enter and Escape (the drop-in
8097
+ // contract) and stops there, and the keydown then bubbles to the table root where
8098
+ // `onGridKeyDown` early-returns because an editor is open. Result:
8099
+ // C-10 — Tab in a drop-in did not commit and advance. It fell through to NATIVE tab order
8100
+ // and walked focus out of the grid, leaving the editor open behind it.
8101
+ // C-05 — in FULL-ROW mode a drop-in's `commit(v)` is draft-only BY DESIGN (committing per
8102
+ // cell would split the row into N writes and N events), and the row commits only
8103
+ // through handlers bound on the built-in inputs — which a row of drop-ins does not
8104
+ // have. So Shift+F2, documented as "Enter commits the whole row in one
8105
+ // `r-model:data` write", wrote a draft and nothing else, `editingRowIndex` stayed
8106
+ // set, and `onGridKeyDown`'s early-return left arrow nav dead. The only exits were
8107
+ // Escape (reverting the whole row) and the imperative verb.
8108
+ //
8109
+ // Bound on the `display: contents` span that wraps the slot, so it sees the bubbled event
8110
+ // from whatever the consumer rendered — including a bespoke editor that never heard of this
8111
+ // component. The host owning the GRID keymap while the drop-in owns its own CONTROL is the
8112
+ // same division the built-in path already has.
8113
+ //
8114
+ // Row mode ignores `defaultPrevented` for Enter/Escape on purpose: a drop-in preventDefaults
8115
+ // Enter to mean "do not insert a newline", not "the row is handled". Its own handler has
8116
+ // already run (target phase before bubble), so the draft is staged by the time we commit.
8117
+ // Single-cell mode honours `defaultPrevented` for Tab, so a drop-in that genuinely implements
8118
+ // its own Tab (a multi-field editor) keeps it.
8119
+ //
8120
+ // Single-cell Tab FLUSHES the drop-in before advancing (quick 260922-mkb). A drop-in owns its
8121
+ // draft: the host's `draftValue` still holds the value the editor opened with, so routing Tab
8122
+ // straight into onEditorKeyDown committed that stale value, opened the next cell's editor, and
8123
+ // then the old drop-in's unmount blur ran `commit(typed)` against the NOW-editing cell — the
8124
+ // typed text landed in the NEXT column (measured: `status=Zeta` from a Name edit, on four
8125
+ // targets). Instead the handler blurs the drop-in while its own cell is still the editing one,
8126
+ // so the drop-in's blur-commit (the drop-in contract) writes the right cell, and only then
8127
+ // advances. `dropinTabFlush` tells editorCommitFor to report the outcome and skip the focus
8128
+ // return. A drop-in that does not commit on blur (EditorCheckbox commits on change, so it holds
8129
+ // nothing pending) falls back to the host-draft path, which is exactly right in that case.
8130
+ function onEditorDropinKeyDown(e: any) {
8131
+ if (!e) return;
8132
+ const key = e.key;
8133
+ if (inRowEdit()) {
8134
+ if (key === 'Enter') {
8135
+ e.preventDefault();
8136
+ commitRow();
8137
+ return;
8138
+ }
8139
+ if (key === 'Escape') {
8140
+ e.preventDefault();
8141
+ cancelRow();
8142
+ return;
8143
+ }
8144
+ if (key === 'Tab') {
8145
+ e.preventDefault();
8146
+ rowEditTab(e.target, e.shiftKey);
8147
+ return;
8148
+ }
8149
+ return;
8150
+ }
8151
+ if (key !== 'Tab' || e.defaultPrevented) return;
8152
+ e.preventDefault();
8153
+ const fromRow = editingRow();
8154
+ const fromCol = editingCol();
8155
+ const target = e.shiftKey ? prevEditableCell(fromRow, fromCol) : nextEditableCell(fromRow, fromCol);
8156
+ const el = e.target;
8157
+ // Move focus to the drop-in's OWN cell rather than calling blur(): that still fires the
8158
+ // drop-in's blur-commit, but keeps focus inside the grid instead of dropping it to <body>.
8159
+ const ownCell = el && el.closest ? el.closest('[data-grid-cell]') : null;
8160
+ const flush = {
8161
+ called: false,
8162
+ committed: false
8163
+ };
8164
+ dropinTabFlush = flush;
8165
+ if (ownCell && ownCell.focus) ownCell.focus();else if (el && el.blur) el.blur();
8166
+ dropinTabFlush = null;
8167
+ if (!flush.called) {
8168
+ onEditorKeyDown(e);
8169
+ return;
8170
+ }
8171
+ // A validation failure keeps the editor open: give the drop-in its focus back.
8172
+ if (!flush.committed) {
8173
+ if (el && el.focus) el.focus();
8174
+ return;
8175
+ }
8176
+ if (target) {
8177
+ setActiveRow(target.row);
8178
+ setActiveColIndex(target.col);
8179
+ beginEdit(target.row, target.col, null);
8180
+ } else {
8181
+ // B5 twin: no editable cell in the Tab direction — keep focus INSIDE the grid.
8182
+ focusCellWhenReady(fromRow, fromCol);
8183
+ }
8184
+ }
8185
+
6974
8186
  // onEditorBlur: commit on a genuine click/focus-away (D-01 — an invalid value keeps the
6975
8187
  // editor open via commitEdit's reject path). SKIP when:
6976
8188
  // - editTransition is set (a synchronous commit/cancel teardown is unmounting the editor), or
@@ -7007,11 +8219,52 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7007
8219
  }
7008
8220
  if (editingRow() < 0 || editTransition) return;
7009
8221
  const next = e ? e.relatedTarget : null;
7010
- // A null relatedTarget is an unmount-blur (the editor left the DOM) or a focus drop the
7011
- // keyboard path owns; committing here would double-count (WR-04: the OLD editor's blur on
7012
- // a Tab-advance fires with a TRANSIENT null relatedTarget while it unmounts). Keep the
7013
- // conservative null=skip behavior.
7014
- if (next == null) return;
8222
+ const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
8223
+ // ── B-02 ────────────────────────────────────────────────────────────────────────────
8224
+ // A null relatedTarget used to return unconditionally. That is correct for the WR-04 case
8225
+ // the comment cited — a Tab-advance fires the OLD editor's blur with a TRANSIENT null
8226
+ // relatedTarget while it unmounts, and committing there double-counts — but it is NOT the
8227
+ // only shape. Clicking any non-focusable region of the page (the body, a heading, whitespace)
8228
+ // also blurs the editor with a null relatedTarget, and that returned WITHOUT committing and
8229
+ // WITHOUT endEdit(): the typed value was silently discarded, the editor stayed open, and
8230
+ // `$data.editingRow` stayed >= 0 — which gridKeydownHandlers' `if ($data.editingRow >= 0)
8231
+ // return` reads as "an editor owns the keyboard", so arrows/Enter/F2/Home/Ctrl+C were all
8232
+ // dead until the user double-clicked somewhere else. There is no document-level click-away
8233
+ // listener anywhere in the family to catch it afterwards.
8234
+ //
8235
+ // The two shapes ARE separable, with the discriminator the branch below already uses: on a
8236
+ // Tab-advance the keyboard path has already moved the editing pair to the NEXT cell, so the
8237
+ // blurring editor's own cell no longer matches ($data.editingRow/editingCol), while a
8238
+ // click-away leaves the pair still sitting on it. The row-edit branch twenty lines above
8239
+ // reaches the same conclusion by the same reasoning and commits on null; this is that branch's
8240
+ // single-cell twin.
8241
+ if (next == null) {
8242
+ // Not a click-away unless a pointerdown OUTSIDE the grid just said so. `outsidePointerDown`
8243
+ // is the discriminator, and nothing weaker works: an editor recycled out of the virtual
8244
+ // window (or replaced by Solid's keyed-row reconcile) blurs with a null relatedTarget too,
8245
+ // and `closest()` still resolves an owning `<td>` inside a DETACHED subtree, so every
8246
+ // synchronous DOM test reads it as a click-away — measured, it closed the pinned editor in
8247
+ // data-table-edit's "open editor + index-based range survive virtualization recycle" case on
8248
+ // solid. Deferring a frame does not help either: by then the node is reattached, focus sits
8249
+ // on <body>, and the state is indistinguishable from a real click-away.
8250
+ //
8251
+ // The flag is a plain module-let set by the document pointerdown listener in DataTable.rozie
8252
+ // (NOT a $data field, and NOT a closure over one): a document listener is attached once, so
8253
+ // on React any `$data` it closes over is frozen at attach time, and a commit driven from
8254
+ // that closure would write the draft as of editor-OPEN and lose everything typed. Routing
8255
+ // the decision back through THIS handler — template-bound, so React re-binds it every
8256
+ // render — keeps the commit on the always-fresh path. Consume it here so an unmount blur
8257
+ // arriving later can never inherit it.
8258
+ if (!outsidePointerDown) return;
8259
+ outsidePointerDown = false;
8260
+ if (!fromCell) return;
8261
+ if (fromCell.getAttribute('data-row') !== String(editingRow()) || fromCell.getAttribute('data-col-index') !== String(editingCol())) return;
8262
+ // Genuine click-away onto a non-focusable region outside the grid → commit + close (D-01's
8263
+ // reject path keeps the editor open on an invalid value, exactly as for a click onto another
8264
+ // cell). skipFocusReturn: the user deliberately put focus elsewhere; do not yank it back.
8265
+ commitEdit(undefined, true);
8266
+ return;
8267
+ }
7015
8268
  // Focus moving OUTSIDE the grid (a click into another widget) → commit (D-01 reject keeps
7016
8269
  // the editor open on an invalid value).
7017
8270
  if (!(gridRoot && gridRoot.contains && gridRoot.contains(next))) {
@@ -7022,7 +8275,6 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7022
8275
  // keyboard handler already committed) from a genuine click-away to ANOTHER grid cell
7023
8276
  // (which must commit + close so the grid is not wedged with an open editor).
7024
8277
  const nextCell = next.closest ? next.closest('[data-grid-cell]') : null;
7025
- const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
7026
8278
  // Same cell (an inner control / the editing cell itself on an Enter focus-return) → a
7027
8279
  // controlled move; skip. Also skip when either cell can't be resolved (an unmounting
7028
8280
  // editor has no owning cell — the Tab-advance remount-blur path, never a click-away).
@@ -7357,6 +8609,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7357
8609
  if (id == null) return;
7358
8610
  const key = String(id);
7359
8611
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
8612
+ // C-09: skip the state write when the incoming spec is EQUIVALENT to the stored one.
8613
+ // `<Column>`'s re-register watch keys on reference-typed props whose documented wiring is
8614
+ // an inline literal — a fresh identity every consumer render — so without this guard every
8615
+ // render whole-object-replaced `$data.colReg`, which the parent's re-feed watch keys on,
8616
+ // which re-rendered the parent, which produced a new identity: a feedback loop, not a slow
8617
+ // path. Guarded HERE rather than in the watch because this is the single seam every
8618
+ // registration passes through, and because value-equality is the correct test for the
8619
+ // registry's own purpose (has anything the table renders from actually changed?).
8620
+ const prev = colReg() ? (colReg() as Record<string, any>)[key] : undefined;
8621
+ if (prev !== undefined && columnSpecsEquivalent(prev, spec)) return;
7360
8622
  setColReg({
7361
8623
  ...colReg(),
7362
8624
  [key]: spec
@@ -7377,7 +8639,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7377
8639
 
7378
8640
  <div class={"rdt-column-defs"} style={{ display: "none" }} aria-hidden="true" data-rozie-s-d5dcab4c="">{resolved()}</div>
7379
8641
 
7380
- {<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="">
8642
+ {<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="">
7381
8643
  <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="" />
7382
8644
 
7383
8645
  {<Show when={allLeafColumns().length}><details class={"rdt-colvis"} data-rozie-s-d5dcab4c="">
@@ -7392,23 +8654,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7392
8654
 
7393
8655
 
7394
8656
  {<Show when={local.groupable}><div class={"rdt-group-bar-host"} data-rozie-s-d5dcab4c="">
7395
- {(_props.groupBarSlot ?? _props.slots?.['groupBar'])?.({ grouping: groupingKeys(), groupableColumns: 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>}
7396
- </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="">
8657
+ {(_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>}
8658
+ </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="">
7397
8659
  <thead class={"rdt-thead"} role="rowgroup" data-rozie-s-d5dcab4c="">
7398
8660
  <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="">
7399
- <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="">
8661
+ <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="">
7400
8662
 
7401
8663
 
7402
8664
  {<Show when={isSelectColumn(header().column.id)} fallback={<Show when={isExpanderColumn(header().column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7403
8665
 
7404
8666
  {<Show when={header().column.getCanSort && header().column.getCanSort()} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7405
8667
  <span class={"rdt-header-label"} data-rozie-s-d5dcab4c="">
7406
- {_props.slots?.[`colHeader-${header().column.id}`]?.({ columnId: header().column.id, column: header().column, label: headerLabel(header().column.id) }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ columnId: header().column.id, column: header().column, label: headerLabel(header().column.id) }) ?? rozieDisplay(headerLabel(header().column.id))}
8668
+ {_props.slots?.[`colHeader-${header().column.id}`]?.({ get columnId() { return header().column.id; }, get column() { return header().column; }, get label() { return headerLabel(header().column.id); } }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ get columnId() { return header().column.id; }, get column() { return header().column; }, get label() { return headerLabel(header().column.id); } }) ?? rozieDisplay(headerLabel(header().column.id))}
7407
8669
  </span>
7408
8670
  </span>}><button type="button" class={"rdt-sort-btn"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onHeaderSort(header().column.id, $event); }} data-rozie-s-d5dcab4c="">
7409
8671
 
7410
8672
  <span class={"rdt-header-label"} data-rozie-s-d5dcab4c="">
7411
- {_props.slots?.[`colHeader-${header().column.id}`]?.({ columnId: header().column.id, column: header().column, label: headerLabel(header().column.id) }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ columnId: header().column.id, column: header().column, label: headerLabel(header().column.id) }) ?? rozieDisplay(headerLabel(header().column.id))}
8673
+ {_props.slots?.[`colHeader-${header().column.id}`]?.({ get columnId() { return header().column.id; }, get column() { return header().column; }, get label() { return headerLabel(header().column.id); } }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ get columnId() { return header().column.id; }, get column() { return header().column; }, get label() { return headerLabel(header().column.id); } }) ?? rozieDisplay(headerLabel(header().column.id))}
7412
8674
  </span>
7413
8675
  <span class={"rdt-sort-ind"} aria-hidden="true" data-rozie-s-d5dcab4c="">{rozieDisplay(sortIndicator(header().column.id))}</span>
7414
8676
  </button></Show>}<Popover trigger="click" placement="bottom-end" strategy="fixed" offset={4} data-rozie-s-d5dcab4c="" anchorSlot={() => (<>
@@ -7423,15 +8685,15 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7423
8685
 
7424
8686
  <button type="button" aria-label={rozieAttr('Resize ' + headerLabel(header().column.id))} class={"rdt-resize-handle"} onPointerDown={($event: PointerEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onResizeStart(header().column.id, $event); }} onTouchStart={($event: TouchEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onResizeStart(header().column.id, $event); }} data-rozie-s-d5dcab4c=""><span class={"rdt-resize-grip"} aria-hidden="true" data-rozie-s-d5dcab4c="" /></button>
7425
8687
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7426
- {(_props.selectAllSlot ?? _props.slots?.['selectAll'])?.({ checked: isAllRowsSelected(), indeterminate: isSomeRowsSelected(), toggle: onToggleAllRows }) ?? <Show when={local.selectionMode === 'multiple'}><input type="checkbox" aria-label="Select all rows" class={"rdt-select-all"} checked={isAllRowsSelected()} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleAllRows($event); }} data-rozie-s-d5dcab4c="" /></Show>}
8688
+ {(_props.selectAllSlot ?? _props.slots?.['selectAll'])?.({ get checked() { return isAllRowsSelected(); }, get indeterminate() { return isSomeRowsSelected(); }, get toggle() { return onToggleAllRows; } }) ?? <Show when={local.selectionMode === 'multiple'}><input type="checkbox" aria-label="Select all rows" class={"rdt-select-all"} checked={isAllRowsSelected()} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleAllRows($event); }} data-rozie-s-d5dcab4c="" /></Show>}
7427
8689
  </span></Show>}</th>}</Key>
7428
8690
  </tr>}</Key>
7429
8691
 
7430
8692
  {<Show when={hasAnyFilterableColumn()}><tr class={"rdt-filter-row"} data-rozie-s-d5dcab4c="">
7431
- <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="">
8693
+ <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="">
7432
8694
  {<Show when={isSelectColumn(header().column.id)} fallback={<Show when={isExpanderColumn(header().column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7433
8695
  {<Show when={columnIsFilterable(header().column.id)}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7434
- {_props.slots?.[`filter-${header().column.id}`]?.({ columnId: header().column.id, value: columnFilterValue(header().column.id), uniqueValues: getFacetedUniqueValues(header().column.id), minMax: getFacetedMinMaxValues(header().column.id), setFilter: setColumnFilter }) ?? (_props.filterSlot ?? _props.slots?.['filter'])?.({ columnId: header().column.id, value: columnFilterValue(header().column.id), uniqueValues: getFacetedUniqueValues(header().column.id), minMax: getFacetedMinMaxValues(header().column.id), setFilter: 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="" />}
8696
+ {_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="" />}
7435
8697
  </span></Show>}</span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}</th>}</Key>
7436
8698
  </tr></Show>}</thead>
7437
8699
 
@@ -7439,47 +8701,47 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7439
8701
 
7440
8702
  <Key each={rows() as readonly any[]} by={(row) => row.id}>{(row) => <>
7441
8703
  <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="">
7442
- <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="">
8704
+ <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="">
7443
8705
 
7444
8706
  {<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="">
7445
- {_props.slots?.[`cell-${cell().column.id}`]?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: cell().getValue() }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
8707
+ {_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())}
7446
8708
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7447
- {<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="">
7448
- {_props.slots?.[`editor-${cell().column.id}`]?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, rowIndexOf(row())) }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: 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="" />}
7449
- </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="">
8709
+ {<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="">
8710
+ {_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="" />}
8711
+ </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="">
7450
8712
  <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>
7451
- </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="">
8713
+ </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="">
7452
8714
  <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>
7453
8715
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
7454
- {_props.slots?.[`cell-${cell().column.id}`]?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: cell().getValue() }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
8716
+ {_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())}
7455
8717
  </span>
7456
8718
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{rozieDisplay('(' + groupSubRowCount(row()) + ')')}</span>
7457
8719
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7458
- {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ row: cellSlotRow(row()), checked: rowIsSelected(row()), toggle: e => onToggleRow(row(), e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(row())} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(row(), $event); }} data-rozie-s-d5dcab4c="" />}
8720
+ {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ get row() { return cellSlotRow(row()); }, get checked() { return rowIsSelected(row()); }, toggle: e => onToggleRow(row(), e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(row())} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(row(), $event); }} data-rozie-s-d5dcab4c="" />}
7459
8721
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7460
8722
  {<Show when={rowCanExpand(row())}><button type="button" data-expander="" aria-expanded={!!rowIsExpanded(row())} aria-label={rozieAttr(rowIsExpanded(row()) ? 'Collapse row' : 'Expand row')} class={"rdt-expander"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(row(), $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(row()) ? '▾' : '▸')}</button></Show>}</span></Show>}{<Show when={isFillHandleCell(rowIndexOf(row()), colIndexOf(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>
7461
8723
  </tr>
7462
8724
 
7463
8725
  {<Show when={rowShowsDetail(row())}><tr class={"rdt-detail-row"} role="row" data-detail-row={rozieAttr(row().id)} data-rozie-s-d5dcab4c="">
7464
8726
  <td class={"rdt-detail-cell"} colSpan={rozieAttr(visibleColCount())} data-rozie-s-d5dcab4c="">
7465
- {(_props.detailSlot ?? _props.slots?.['detail'])?.({ row: row().original })}
8727
+ {(_props.detailSlot ?? _props.slots?.['detail'])?.({ get row() { return row().original; } })}
7466
8728
  </td>
7467
8729
  </tr></Show>}</>}</Key>
7468
8730
  </tbody>
7469
8731
  </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="">
7470
- <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="">
8732
+ <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="">
7471
8733
  <thead class={"rdt-thead"} role="rowgroup" data-rozie-s-d5dcab4c="">
7472
8734
  <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="">
7473
8735
 
7474
- {<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="">
8736
+ {<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="">
7475
8737
  {<Show when={isSelectColumn(wh().header.column.id)} fallback={<Show when={isExpanderColumn(wh().header.column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7476
8738
  {<Show when={wh().header.column.getCanSort && wh().header.column.getCanSort()} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7477
8739
  <span class={"rdt-header-label"} data-rozie-s-d5dcab4c="">
7478
- {_props.slots?.[`colHeader-${wh().header.column.id}`]?.({ columnId: wh().header.column.id, column: wh().header.column, label: headerLabel(wh().header.column.id) }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ columnId: wh().header.column.id, column: wh().header.column, label: headerLabel(wh().header.column.id) }) ?? rozieDisplay(headerLabel(wh().header.column.id))}
8740
+ {_props.slots?.[`colHeader-${wh().header.column.id}`]?.({ get columnId() { return wh().header.column.id; }, get column() { return wh().header.column; }, get label() { return headerLabel(wh().header.column.id); } }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ get columnId() { return wh().header.column.id; }, get column() { return wh().header.column; }, get label() { return headerLabel(wh().header.column.id); } }) ?? rozieDisplay(headerLabel(wh().header.column.id))}
7479
8741
  </span>
7480
8742
  </span>}><button type="button" class={"rdt-sort-btn"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onHeaderSort(wh().header.column.id, $event); }} data-rozie-s-d5dcab4c="">
7481
8743
  <span class={"rdt-header-label"} data-rozie-s-d5dcab4c="">
7482
- {_props.slots?.[`colHeader-${wh().header.column.id}`]?.({ columnId: wh().header.column.id, column: wh().header.column, label: headerLabel(wh().header.column.id) }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ columnId: wh().header.column.id, column: wh().header.column, label: headerLabel(wh().header.column.id) }) ?? rozieDisplay(headerLabel(wh().header.column.id))}
8744
+ {_props.slots?.[`colHeader-${wh().header.column.id}`]?.({ get columnId() { return wh().header.column.id; }, get column() { return wh().header.column; }, get label() { return headerLabel(wh().header.column.id); } }) ?? (_props.colHeaderSlot ?? _props.slots?.['colHeader'])?.({ get columnId() { return wh().header.column.id; }, get column() { return wh().header.column; }, get label() { return headerLabel(wh().header.column.id); } }) ?? rozieDisplay(headerLabel(wh().header.column.id))}
7483
8745
  </span>
7484
8746
  <span class={"rdt-sort-ind"} aria-hidden="true" data-rozie-s-d5dcab4c="">{rozieDisplay(sortIndicator(wh().header.column.id))}</span>
7485
8747
  </button></Show>}<Popover trigger="click" placement="bottom-end" strategy="fixed" offset={4} data-rozie-s-d5dcab4c="" anchorSlot={() => (<>
@@ -7493,16 +8755,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7493
8755
  </div></Popover>
7494
8756
  <button type="button" aria-label={rozieAttr('Resize ' + headerLabel(wh().header.column.id))} class={"rdt-resize-handle"} onPointerDown={($event: PointerEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onResizeStart(wh().header.column.id, $event); }} onTouchStart={($event: TouchEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onResizeStart(wh().header.column.id, $event); }} data-rozie-s-d5dcab4c=""><span class={"rdt-resize-grip"} aria-hidden="true" data-rozie-s-d5dcab4c="" /></button>
7495
8757
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7496
- {(_props.selectAllSlot ?? _props.slots?.['selectAll'])?.({ checked: isAllRowsSelected(), indeterminate: isSomeRowsSelected(), toggle: onToggleAllRows }) ?? <Show when={local.selectionMode === 'multiple'}><input type="checkbox" aria-label="Select all rows" class={"rdt-select-all"} checked={isAllRowsSelected()} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleAllRows($event); }} data-rozie-s-d5dcab4c="" /></Show>}
8758
+ {(_props.selectAllSlot ?? _props.slots?.['selectAll'])?.({ get checked() { return isAllRowsSelected(); }, get indeterminate() { return isSomeRowsSelected(); }, get toggle() { return onToggleAllRows; } }) ?? <Show when={local.selectionMode === 'multiple'}><input type="checkbox" aria-label="Select all rows" class={"rdt-select-all"} checked={isAllRowsSelected()} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleAllRows($event); }} data-rozie-s-d5dcab4c="" /></Show>}
7497
8759
  </span></Show>}</th>}</Key>
7498
8760
 
7499
8761
  {<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>
7500
8762
 
7501
8763
  {<Show when={hasAnyFilterableColumn()}><tr class={"rdt-filter-row"} data-rozie-s-d5dcab4c="">
7502
- {<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="">
8764
+ {<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="">
7503
8765
  {<Show when={isSelectColumn(wh().header.column.id)} fallback={<Show when={isExpanderColumn(wh().header.column.id)} fallback={<span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7504
8766
  {<Show when={columnIsFilterable(wh().header.column.id)}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7505
- {_props.slots?.[`filter-${wh().header.column.id}`]?.({ columnId: wh().header.column.id, value: columnFilterValue(wh().header.column.id), uniqueValues: getFacetedUniqueValues(wh().header.column.id), minMax: getFacetedMinMaxValues(wh().header.column.id), setFilter: setColumnFilter }) ?? (_props.filterSlot ?? _props.slots?.['filter'])?.({ columnId: wh().header.column.id, value: columnFilterValue(wh().header.column.id), uniqueValues: getFacetedUniqueValues(wh().header.column.id), minMax: getFacetedMinMaxValues(wh().header.column.id), setFilter: 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="" />}
8767
+ {_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="" />}
7506
8768
  </span></Show>}</span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}</th>}</Key>
7507
8769
  {<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>
7508
8770
 
@@ -7513,33 +8775,39 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7513
8775
  </tr>
7514
8776
 
7515
8777
  <Key each={windowedRows() as readonly any[]} by={(wr) => wr.row.id}>{(wr) => <>
7516
- <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="">
8778
+ <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="">
7517
8779
 
7518
- {<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="">
8780
+ {<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="">
8781
+
7519
8782
 
7520
- {<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="">
7521
- {_props.slots?.[`cell-${cell().column.id}`]?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: cell().getValue() }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
8783
+ {<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="">
8784
+ {_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())}
7522
8785
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7523
- {<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="">
7524
- {_props.slots?.[`editor-${cell().column.id}`]?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, wr().vi.index) }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: 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="" />}
7525
- </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="">
8786
+ {<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="">
8787
+ {_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="" />}
8788
+ </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="">
7526
8789
  <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>
7527
- </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="">
8790
+ </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="">
7528
8791
  <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>
7529
8792
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
7530
- {_props.slots?.[`cell-${cell().column.id}`]?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: cell().getValue() }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
8793
+ {_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())}
7531
8794
  </span>
7532
8795
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{rozieDisplay('(' + groupSubRowCount(wr().row) + ')')}</span>
7533
8796
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7534
- {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ row: cellSlotRow(wr().row), checked: 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="" />}
8797
+ {(_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="" />}
7535
8798
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7536
- {<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>
8799
+ {<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="">
8800
+ {(_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>}
8801
+ </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
8802
+
8803
+ <input class={"rdt-select-placeholder"} type="checkbox" disabled={true} tabIndex={-1} aria-hidden="true" style={{ visibility: "hidden" }} data-rozie-s-d5dcab4c="" />
8804
+ </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>
7537
8805
 
7538
8806
  {<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>
7539
8807
 
7540
8808
  {<Show when={rowShowsDetail(wr().row)}><tr class={"rdt-detail-row"} role="row" data-detail-row={rozieAttr(wr().row.id)} data-rozie-s-d5dcab4c="">
7541
8809
  <td class={"rdt-detail-cell"} colSpan={rozieAttr(windowedColSpan())} data-rozie-s-d5dcab4c="">
7542
- {(_props.detailSlot ?? _props.slots?.['detail'])?.({ row: wr().row.original })}
8810
+ {(_props.detailSlot ?? _props.slots?.['detail'])?.({ get row() { return wr().row.original; } })}
7543
8811
  </td>
7544
8812
  </tr></Show>}</>}</Key>
7545
8813