@rozie-ui/data-table-lit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/DataTable.ts CHANGED
@@ -5,7 +5,7 @@ import { RozieSlotDistributor, createLitControllableProperty, rozieAttr, rozieDi
5
5
  import { ContextProvider, createContext } from '@lit/context';
6
6
  import { repeat } from 'lit/directives/repeat.js';
7
7
  import '@rozie-ui/popover-lit';
8
- import { isSafeKey, wrapAggregationFn, indexDefsById, collectGroupableLeafDefs } from './helpers/columnDefUtils';
8
+ import { isSafeKey, wrapAggregationFn, indexDefsById, editorKindWarning, collectGroupableLeafDefs } from './helpers/columnDefUtils';
9
9
  import { applyUpdater, clamp, focusables } from './helpers/indexMath';
10
10
  import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
11
11
  import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
@@ -37,6 +37,15 @@ import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, m
37
37
  // snapshot (the rete stale-closure anti-pattern — a top-level $computed/useCallback
38
38
  // freezes the table at the empty-initial state on React).
39
39
 
40
+ // C-09's value-equality guard. Imported HERE, in DataTable.rozie's OWN script, because that
41
+ // is where it is USED: a `.rzts` partial's import of a symbol the partial itself never
42
+ // references is dropped as unused, and the host script then reaches a free identifier. That
43
+ // failed at RUNTIME, not at compile time — `ReferenceError: columnSpecsEquivalent is not
44
+ // defined` on the first paste — which is the only way this class of mistake shows up.
45
+ import { columnSpecsEquivalent } from './helpers/columnDefUtils';
46
+
47
+ // The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
48
+
40
49
  const __rozieCtx_data_table_columns = createContext(Symbol.for("rozie:data-table:columns"));
41
50
 
42
51
  interface RozieGroupBarSlotCtx {
@@ -63,9 +72,15 @@ interface RozieDynamicFilterSlotCtx {
63
72
  value: any;
64
73
  uniqueValues: any;
65
74
  minMax: any;
75
+ columnLabel: any;
66
76
  setFilter: any;
67
77
  }
68
78
 
79
+ interface RoziePlaceholderSlotCtx {
80
+ index: any;
81
+ columnId: any;
82
+ }
83
+
69
84
  interface RozieSelectCellSlotCtx {
70
85
  row: any;
71
86
  checked: any;
@@ -86,6 +101,7 @@ interface RozieDynamicEditorSlotCtx {
86
101
  value: any;
87
102
  commit: any;
88
103
  cancel: any;
104
+ columnLabel: any;
89
105
  autofocus: any;
90
106
  }
91
107
 
@@ -104,6 +120,7 @@ interface RozieFilterSlotCtx {
104
120
  value: any;
105
121
  uniqueValues: any;
106
122
  minMax: any;
123
+ columnLabel: any;
107
124
  setFilter: any;
108
125
  }
109
126
 
@@ -121,6 +138,7 @@ interface RozieEditorSlotCtx {
121
138
  value: any;
122
139
  commit: any;
123
140
  cancel: any;
141
+ columnLabel: any;
124
142
  autofocus: any;
125
143
  }
126
144
 
@@ -131,32 +149,30 @@ export default class DataTable extends SignalWatcher(LitElement) {
131
149
  static styles = css`
132
150
  :host{display:contents}
133
151
  :host {
134
- --rdt-font: var(--rozie-data-table-font);
135
- --rdt-color: var(--rozie-data-table-fg);
136
- --rdt-cell-padding: var(--rozie-data-table-cell-padding);
137
- --rdt-border: 1px solid var(--rozie-data-table-border-color);
138
- --rdt-header-bg: var(--rozie-data-table-header-bg);
139
- --rdt-header-weight: var(--rozie-data-table-header-weight);
140
- --rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
141
- --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
142
- --rdt-filter-radius: var(--rozie-data-table-radius);
143
- --rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
144
- --rdt-page-btn-radius: var(--rozie-data-table-radius);
145
- --rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
146
- --rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
147
- --rdt-page-size-radius: var(--rozie-data-table-radius);
148
- --rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
149
- --rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
150
- --rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
151
- --rdt-pin-btn-radius: var(--rozie-data-table-radius);
152
- --rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
153
- --rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color);
154
- --rdt-colvis-summary-radius: var(--rozie-data-table-radius);
155
- --rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color);
156
- --rdt-colvis-menu-radius: var(--rozie-data-table-radius);
157
- --rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg);
158
- --rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow);
159
- --rdt-select-accent: var(--rozie-data-table-select-accent);
152
+ --rdt-color: var(--rozie-data-table-fg, var(--rdt-ds-fg));
153
+ --rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, var(--rdt-ds-select-accent)));
154
+ --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
155
+ --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
156
+ --rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
157
+ --rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
158
+ --rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
159
+ --rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
160
+ --rdt-header-weight: var(--rozie-data-table-header-weight, var(--rdt-ds-header-weight));
161
+ --rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity, var(--rdt-ds-sort-indicator-opacity));
162
+ --rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color, var(--rdt-ds-resize-grip-color));
163
+ --rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active, var(--rozie-data-table-accent, var(--rdt-ds-resize-grip-active)));
164
+ --rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg, var(--rdt-ds-pin-active-bg));
165
+ --rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg, var(--rdt-ds-menu-bg));
166
+ --rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow, var(--rdt-ds-menu-shadow));
167
+ --rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
168
+ --rdt-colvis-menu-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
169
+ --rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
170
+ --rdt-colvis-summary-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
171
+ --rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity, var(--rdt-ds-disabled-opacity));
172
+ --rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
173
+ --rdt-page-btn-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
174
+ --rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
175
+ --rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
160
176
  }
161
177
  .rozie-data-table[data-rozie-s-d5dcab4c] {
162
178
  border-collapse: collapse;
@@ -292,6 +308,16 @@ export default class DataTable extends SignalWatcher(LitElement) {
292
308
  flex-direction: column;
293
309
  gap: var(--rdt-chrome-gap, 0.5rem);
294
310
  }
311
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-skeleton[data-rozie-s-d5dcab4c] {
312
+ /* inline-block on the cell's own line box, exactly like a text value (a block box was 2px
313
+ short of a text row). */
314
+ display: inline-block;
315
+ width: 100%;
316
+ vertical-align: baseline;
317
+ border-radius: 4px;
318
+ background: color-mix(in srgb, currentColor 12%, transparent);
319
+ color: transparent;
320
+ }
295
321
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-toolbar[data-rozie-s-d5dcab4c] {
296
322
  display: flex;
297
323
  gap: var(--rdt-toolbar-gap, 0.5rem);
@@ -330,6 +356,9 @@ export default class DataTable extends SignalWatcher(LitElement) {
330
356
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] {
331
357
  background: var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015));
332
358
  }
359
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
360
+ box-sizing: border-box;
361
+ }
333
362
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
334
363
  padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
335
364
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
@@ -449,6 +478,9 @@ export default class DataTable extends SignalWatcher(LitElement) {
449
478
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-colvis-summary[data-rozie-s-d5dcab4c] {
450
479
  cursor: pointer;
451
480
  font: inherit;
481
+ /* Own the margin too: content-site typography styles bare \`summary\` (VitePress sets
482
+ \`margin: 16px 0\`), which grew the <details> and stretched the toolbar's search input. */
483
+ margin: 0;
452
484
  padding: var(--rdt-colvis-summary-padding, 0.25rem 0.6rem);
453
485
  border: var(--rdt-colvis-summary-border, 1px solid rgba(0, 0, 0, 0.2));
454
486
  border-radius: var(--rdt-colvis-summary-radius, 4px);
@@ -487,6 +519,51 @@ export default class DataTable extends SignalWatcher(LitElement) {
487
519
  text-align: var(--rdt-expander-col-align, center);
488
520
  white-space: nowrap;
489
521
  }
522
+ .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,
523
+ .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,
524
+ .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,
525
+ .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,
526
+ .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,
527
+ .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,
528
+ .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,
529
+ .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 {
530
+ content: '';
531
+ display: inline-block;
532
+ vertical-align: middle;
533
+ }
534
+ .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 {
535
+ width: calc(1 * var(--rdt-tree-indent, 1.25rem));
536
+ }
537
+ .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 {
538
+ width: calc(2 * var(--rdt-tree-indent, 1.25rem));
539
+ }
540
+ .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 {
541
+ width: calc(3 * var(--rdt-tree-indent, 1.25rem));
542
+ }
543
+ .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 {
544
+ width: calc(4 * var(--rdt-tree-indent, 1.25rem));
545
+ }
546
+ .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 {
547
+ width: calc(5 * var(--rdt-tree-indent, 1.25rem));
548
+ }
549
+ .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 {
550
+ width: calc(6 * var(--rdt-tree-indent, 1.25rem));
551
+ }
552
+ .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 {
553
+ width: calc(7 * var(--rdt-tree-indent, 1.25rem));
554
+ }
555
+ .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 {
556
+ width: calc(8 * var(--rdt-tree-indent, 1.25rem));
557
+ }
558
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] {
559
+ user-select: none;
560
+ }
561
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] input[data-rozie-s-d5dcab4c],
562
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] textarea[data-rozie-s-d5dcab4c],
563
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] select[data-rozie-s-d5dcab4c],
564
+ .rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] [contenteditable="true"][data-rozie-s-d5dcab4c] {
565
+ user-select: text;
566
+ }
490
567
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-all[data-rozie-s-d5dcab4c],
491
568
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-row[data-rozie-s-d5dcab4c] {
492
569
  cursor: pointer;
@@ -556,6 +633,10 @@ export default class DataTable extends SignalWatcher(LitElement) {
556
633
  */
557
634
  @property({ type: Object, attribute: 'expanded' }) _expanded_attr: any | boolean = null;
558
635
  private _expandedControllable = createLitControllableProperty<any | boolean>({ host: this, eventName: 'expanded-change', defaultValue: null, initialControlledValue: undefined });
636
+ /**
637
+ * 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.
638
+ */
639
+ @property({ type: Function }) getRowId: ((...args: any[]) => any) | null = null;
559
640
  /**
560
641
  * 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.
561
642
  */
@@ -683,15 +764,27 @@ export default class DataTable extends SignalWatcher(LitElement) {
683
764
  private _rangeAnchor = signal<any>(null);
684
765
  private _rangeFocus = signal<any>(null);
685
766
  private _pasteAnnounce = signal('');
767
+ private _rangeAnnounce = signal('');
686
768
  private _liveAnnounce = signal('');
687
769
  @query('[data-rozie-ref="__rozieRoot"]') private _ref__rozieRoot!: HTMLElement;
688
770
  private __rozieWatchInitial_0 = true;
689
771
  private __rozieWatchInitial_1 = true;
772
+ private __rozieWatchInitial_2 = true;
690
773
  private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { context: __rozieCtx_data_table_columns, initialValue: ((__rozieCtxHost) => ({
691
774
  registerColumn: (id: any, spec: any) => {
692
775
  if (id == null) return;
693
776
  const key = String(id);
694
777
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
778
+ // C-09: skip the state write when the incoming spec is EQUIVALENT to the stored one.
779
+ // `<Column>`'s re-register watch keys on reference-typed props whose documented wiring is
780
+ // an inline literal — a fresh identity every consumer render — so without this guard every
781
+ // render whole-object-replaced `$data.colReg`, which the parent's re-feed watch keys on,
782
+ // which re-rendered the parent, which produced a new identity: a feedback loop, not a slow
783
+ // path. Guarded HERE rather than in the watch because this is the single seam every
784
+ // registration passes through, and because value-equality is the correct test for the
785
+ // registry's own purpose (has anything the table renders from actually changed?).
786
+ const prev = __rozieCtxHost._colReg.value ? (__rozieCtxHost._colReg.value as Record<string, any>)[key] : undefined;
787
+ if (prev !== undefined && columnSpecsEquivalent(prev, spec)) return;
695
788
  __rozieCtxHost._colReg.value = {
696
789
  ...__rozieCtxHost._colReg.value,
697
790
  [key]: spec
@@ -721,6 +814,9 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
721
814
  @queryAssignedElements({ flatten: true }) private _slotDynamicColHeaderElements!: Element[];
722
815
  @state() private _hasSlotDynamicFilter = false;
723
816
  @queryAssignedElements({ flatten: true }) private _slotDynamicFilterElements!: Element[];
817
+ @state() private _hasSlotPlaceholder = false;
818
+ @queryAssignedElements({ slot: 'placeholder', flatten: true }) private _slotPlaceholderElements!: Element[];
819
+ @property({ attribute: false }) placeholder?: (scope: { index: any; columnId: any }) => unknown;
724
820
  @state() private _hasSlotSelectCell = false;
725
821
  @queryAssignedElements({ slot: 'selectCell', flatten: true }) private _slotSelectCellElements!: Element[];
726
822
  @property({ attribute: false }) selectCell?: (scope: { row: any; checked: any; toggle: any }) => unknown;
@@ -736,13 +832,13 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
736
832
  @property({ attribute: false }) colHeader?: (scope: { columnId: any; column: any; label: any }) => unknown;
737
833
  @state() private _hasSlotFilter = false;
738
834
  @queryAssignedElements({ slot: 'filter', flatten: true }) private _slotFilterElements!: Element[];
739
- @property({ attribute: false }) filter?: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; setFilter: any }) => unknown;
835
+ @property({ attribute: false }) filter?: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; columnLabel: any; setFilter: any }) => unknown;
740
836
  @state() private _hasSlotCell = false;
741
837
  @queryAssignedElements({ slot: 'cell', flatten: true }) private _slotCellElements!: Element[];
742
838
  @property({ attribute: false }) cell?: (scope: { columnId: any; column: any; row: any; value: any }) => unknown;
743
839
  @state() private _hasSlotEditor = false;
744
840
  @queryAssignedElements({ slot: 'editor', flatten: true }) private _slotEditorElements!: Element[];
745
- @property({ attribute: false }) editor?: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any }) => unknown;
841
+ @property({ attribute: false }) editor?: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; columnLabel: any; autofocus: any }) => unknown;
746
842
  // Phase 79 Plan 08 (R4) contract for 79-09: the record intake for
747
843
  // record-routed slot fills. 79-09's consumer-side emitSlotFiller
748
844
  // accumulates an object literal onto the SAME `.rozieSlots=${{ ... }}`
@@ -752,7 +848,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
752
848
  // named function-prop / <slot> fallback (AC-9). Attribute
753
849
  // deserialization is disabled — this is a function-valued record,
754
850
  // never reflected to/from an HTML attribute.
755
- @property({ attribute: false }) rozieSlots?: { [key: `colHeader-${string}`]: (scope: { columnId: any; column: any; label: any }) => unknown; [key: `filter-${string}`]: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; setFilter: any }) => unknown; [key: `cell-${string}`]: (scope: { columnId: any; column: any; row: any; value: any }) => unknown; [key: `editor-${string}`]: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any }) => unknown; } & Record<string, (scope: any) => unknown>;
851
+ @property({ attribute: false }) rozieSlots?: { [key: `colHeader-${string}`]: (scope: { columnId: any; column: any; label: any }) => unknown; [key: `filter-${string}`]: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; columnLabel: any; setFilter: any }) => unknown; [key: `cell-${string}`]: (scope: { columnId: any; column: any; row: any; value: any }) => unknown; [key: `editor-${string}`]: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; columnLabel: any; autofocus: any }) => unknown; } & Record<string, (scope: any) => unknown>;
756
852
 
757
853
  private _disconnectCleanups: Array<() => void> = [];
758
854
  // Re-parenting guard: set true once the deferred teardown has actually
@@ -815,6 +911,17 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
815
911
  }
816
912
  }
817
913
 
914
+ {
915
+ const slotEl = this.shadowRoot?.querySelector('slot[name="placeholder"]');
916
+ if (slotEl !== null && slotEl !== undefined) {
917
+ const update = () => { this._hasSlotPlaceholder = this._slotPlaceholderElements.length > 0; };
918
+ slotEl.addEventListener('slotchange', update);
919
+ // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
920
+ this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
921
+ update();
922
+ }
923
+ }
924
+
818
925
  {
819
926
  const slotEl = this.shadowRoot?.querySelector('slot[name="selectCell"]');
820
927
  if (slotEl !== null && slotEl !== undefined) {
@@ -911,6 +1018,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
911
1018
  this._hasSlotSelectAll = Array.from(this.children).some((el) => el.getAttribute('slot') === 'selectAll');
912
1019
  this._hasSlotDynamicColHeader = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
913
1020
  this._hasSlotDynamicFilter = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
1021
+ this._hasSlotPlaceholder = Array.from(this.children).some((el) => el.getAttribute('slot') === 'placeholder');
914
1022
  this._hasSlotSelectCell = Array.from(this.children).some((el) => el.getAttribute('slot') === 'selectCell');
915
1023
  this._hasSlotDynamicCell = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
916
1024
  this._hasSlotDynamicEditor = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
@@ -926,6 +1034,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
926
1034
  firstUpdated(): void {
927
1035
  this._armListeners();
928
1036
 
1037
+ this._disconnectCleanups.push(effect(() => { const __watchVal = (() => this._windowVer.value)(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => this.emitVisibleRangeIfChanged())(); }); }));
929
1038
  this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.globalFilter, this.columnFilters, this.pagination, // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
930
1039
  // changes at runtime (e.g. a server response updates the total) so getPageCount() and the
931
1040
  // Next button availability track the new total.
@@ -939,7 +1048,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
939
1048
  // columnDefs()/tableColumns() build the UNION of both, and reFeed re-passes columns.
940
1049
  // (Consumers memoize the array as with $props.data/$props.sorting; the uncontrolled
941
1050
  // <Column>-children path leaves $props.columns undefined — a stable no-op getter.)
942
- this.columns, this._colReg.value])(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => {
1051
+ this.columns, this._colReg.value])(); untracked(() => { if (this.__rozieWatchInitial_1) { this.__rozieWatchInitial_1 = false; return; } (() => {
943
1052
  // Seed BEFORE the re-feed so the pinning this call may write is part of the state object
944
1053
  // table-core receives on this same tick (rather than landing a frame late and re-ordering
945
1054
  // getVisibleCells() a render after the columns appear). One-shot and self-latching — see
@@ -949,7 +1058,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
949
1058
  this.reFeed();
950
1059
  this.maybeClearHistoryOnExternalSwap();
951
1060
  })(); }); }));
952
- this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.columnFilters, this.globalFilter, this._sortingDefault.value, this._columnFiltersDefault.value, this._globalFilterDefault.value])(); untracked(() => { if (this.__rozieWatchInitial_1) { this.__rozieWatchInitial_1 = false; return; } (() => {
1061
+ this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.columnFilters, this.globalFilter, this._sortingDefault.value, this._columnFiltersDefault.value, this._globalFilterDefault.value])(); untracked(() => { if (this.__rozieWatchInitial_2) { this.__rozieWatchInitial_2 = false; return; } (() => {
953
1062
  const msg = this.buildSortFilterAnnounce();
954
1063
  if (msg) this._liveAnnounce.value = msg;
955
1064
  })(); }); }));
@@ -959,6 +1068,16 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
959
1068
  if (id == null) return;
960
1069
  const key = String(id);
961
1070
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
1071
+ // C-09: skip the state write when the incoming spec is EQUIVALENT to the stored one.
1072
+ // `<Column>`'s re-register watch keys on reference-typed props whose documented wiring is
1073
+ // an inline literal — a fresh identity every consumer render — so without this guard every
1074
+ // render whole-object-replaced `$data.colReg`, which the parent's re-feed watch keys on,
1075
+ // which re-rendered the parent, which produced a new identity: a feedback loop, not a slow
1076
+ // path. Guarded HERE rather than in the watch because this is the single seam every
1077
+ // registration passes through, and because value-equality is the correct test for the
1078
+ // registry's own purpose (has anything the table renders from actually changed?).
1079
+ const prev = __rozieCtxHost._colReg.value ? (__rozieCtxHost._colReg.value as Record<string, any>)[key] : undefined;
1080
+ if (prev !== undefined && columnSpecsEquivalent(prev, spec)) return;
962
1081
  __rozieCtxHost._colReg.value = {
963
1082
  ...__rozieCtxHost._colReg.value,
964
1083
  [key]: spec
@@ -1003,7 +1122,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1003
1122
  // the getter bought nothing. Snapshot the initial data here; setOptions owns updates.
1004
1123
  // currentData() = the bound prop when controlled, else the uncontrolled $data.dataDefault
1005
1124
  // (Phase 51 req-4 — so a committed edit's writeData re-feed is observed either way).
1006
- data: this.currentData(),
1125
+ data: this.feedData(),
1007
1126
  columns: this.tableColumns(),
1008
1127
  state: this.currentState(),
1009
1128
  getCoreRowModel: getCoreRowModel(),
@@ -1018,7 +1137,10 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1018
1137
  // default `!!subRows.length` rule applies (only parents with children expand).
1019
1138
  getExpandedRowModel: getExpandedRowModel(),
1020
1139
  getSubRows: (this.getSubRows || undefined) as any,
1021
- getRowCanExpand: this.expandable === true && this.getSubRows == null ? () => true : undefined,
1140
+ // Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
1141
+ // other row-model fns.
1142
+ getRowId: this.rowIdOption(),
1143
+ getRowCanExpand: this.expandable === true && this.getSubRows == null ? (row: any) => !this.rowIsLazyPlaceholder(row) : undefined,
1022
1144
  onExpandedChange: this.onExpandedChangeCb,
1023
1145
  // Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
1024
1146
  // POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
@@ -1063,7 +1185,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1063
1185
  // Row selection (req-7): enabled unless 'none'; 'single' caps at ≤1
1064
1186
  // (enableMultiRowSelection:false). Select-all scope = filtered rows (TanStack
1065
1187
  // default, D-06 — NOT overridden).
1066
- enableRowSelection: this.selectionMode !== 'none',
1188
+ enableRowSelection: this.rowSelectionOption(),
1067
1189
  enableMultiRowSelection: this.selectionMode === 'multiple',
1068
1190
  // PER-SLICE callbacks (Open-Q1: each maps 1:1 to a slice's r-model + change event,
1069
1191
  // no global onStateChange diff) — hoisted top-level consts, re-passed by the re-feed
@@ -1107,6 +1229,10 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1107
1229
  // NEVER in a render helper (Pitfall 1). Pass the COMPLETE options set (virtual-core's
1108
1230
  // setOptions replaces, not merges). Guarded so the off path executes no virtual-core code.
1109
1231
  if (this.rowsWindowed() && this.virtualizer) {
1232
+ // Rekey any placeholder rows this re-feed just filled (quick 260926, VR probe) BEFORE
1233
+ // handing the virtualizer its fresh options — see migratePlaceholderSizeCache()'s own
1234
+ // comment (virtualization.rzts) for the itemSizeCache/getMeasurements memo root cause.
1235
+ this.migratePlaceholderSizeCache();
1110
1236
  this.virtualizer.setOptions(this.virtualizerOptions());
1111
1237
  this.virtualizer._willUpdate();
1112
1238
  }
@@ -1182,6 +1308,24 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1182
1308
  // cell selectors hang off (the exact idiom proven ×6 by plan 01's probe). Captured here
1183
1309
  // (post-mount) so it is non-null and ROZ123-clean.
1184
1310
  this.gridRoot = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector('.rozie-data-table') : null;
1311
+ // B-02: the click-away detector. `composedPath()` rather than `ev.target` because on Lit the
1312
+ // grid lives in a shadow root and a document-level listener sees the retargeted HOST element,
1313
+ // which is never `gridRoot.contains(...)` — every in-grid click would read as "outside".
1314
+ // B-02: the click-away detector. `composedPath()` rather than `ev.target` because on Lit the
1315
+ // grid lives in a shadow root and a document-level listener sees the retargeted HOST element,
1316
+ // which is never `gridRoot.contains(...)` — every in-grid click would read as "outside".
1317
+ if (typeof document !== 'undefined') {
1318
+ this.docPointerDown = (ev: any) => {
1319
+ if (!this.gridRoot) {
1320
+ this.outsidePointerDown = false;
1321
+ return;
1322
+ }
1323
+ const path = ev && typeof ev.composedPath === 'function' ? ev.composedPath() : null;
1324
+ const inside = path ? path.indexOf(this.gridRoot) !== -1 : !!(ev && ev.target && this.gridRoot.contains && this.gridRoot.contains(ev.target));
1325
+ this.outsidePointerDown = !inside;
1326
+ };
1327
+ document.addEventListener('pointerdown', this.docPointerDown, true);
1328
+ }
1185
1329
  // WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
1186
1330
  // page load AND was non-deterministic on React/Solid (the entry cell may not be
1187
1331
  // committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
@@ -1357,6 +1501,26 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1357
1501
 
1358
1502
  updated(changedProperties: Map<string, unknown>): void {
1359
1503
  this.maybeClearHistoryOnExternalSwap();
1504
+ // visible-range-change backstop (quick 260925-dtl part 2): a re-render from new data can move
1505
+ // the rendered window WITHOUT a window-version bump (measured on React: filling placeholders
1506
+ // shifted the window by two rows and nothing was reported). Deduped, so a render that leaves
1507
+ // the window where it was emits nothing.
1508
+ // visible-range-change backstop (quick 260925-dtl part 2): a re-render from new data can move
1509
+ // the rendered window WITHOUT a window-version bump (measured on React: filling placeholders
1510
+ // shifted the window by two rows and nothing was reported). Deduped, so a render that leaves
1511
+ // the window where it was emits nothing.
1512
+ this.emitVisibleRangeIfChanged();
1513
+ // A-02: the post-render hook is the one place that observes EVERY column-width change on all
1514
+ // six targets — an interactive resize writes `columnSizing` through table-core under
1515
+ // `columnResizeMode: 'onChange'` and never reaches the coarse re-feed watch (which keys on
1516
+ // `$props.columnSizing`, undefined on the uncontrolled path). Self-guarded on the width
1517
+ // signature, so a tick that changed no width costs one loop over the leaf columns.
1518
+ // A-02: the post-render hook is the one place that observes EVERY column-width change on all
1519
+ // six targets — an interactive resize writes `columnSizing` through table-core under
1520
+ // `columnResizeMode: 'onChange'` and never reaches the coarse re-feed watch (which keys on
1521
+ // `$props.columnSizing`, undefined on the uncontrolled path). Self-guarded on the width
1522
+ // signature, so a tick that changed no width costs one loop over the leaf columns.
1523
+ this.remeasureColumnSizes();
1360
1524
  if (!this.table) return;
1361
1525
  // Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
1362
1526
  // $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
@@ -1379,6 +1543,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1379
1543
  if (this.isConnected || this._rozieTornDown) return;
1380
1544
  this._rozieTornDown = true;
1381
1545
  () => {
1546
+ // B-02: drop the document click-away listener (it is attached for the instance's lifetime,
1547
+ // not per edit session — attaching it per session would freeze a stale closure on React).
1548
+ if (this.docPointerDown && typeof document !== 'undefined') {
1549
+ document.removeEventListener('pointerdown', this.docPointerDown, true);
1550
+ this.docPointerDown = null;
1551
+ }
1382
1552
  if (this.virtualizerCleanup) this.virtualizerCleanup();
1383
1553
  // Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
1384
1554
  // No-op when column windowing was off (cleanup stays null).
@@ -1424,7 +1594,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1424
1594
 
1425
1595
  <div class="rdt-column-defs" style="display:none" aria-hidden="true" data-rozie-s-d5dcab4c><slot></slot></div>
1426
1596
 
1427
- ${!!this._invalidMsg.value ? html`<div class="rdt-sr-live" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._invalidMsg.value}</div>` : nothing}${!!this._pasteAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-paste" data-testid="paste-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._pasteAnnounce.value}</div>` : nothing}${!!this._liveAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-sortfilter" data-testid="sortfilter-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._liveAnnounce.value}</div>` : nothing}<div class="rdt-toolbar" data-rozie-s-d5dcab4c>
1597
+ ${!!this._invalidMsg.value ? html`<div class="rdt-sr-live" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._invalidMsg.value}</div>` : nothing}${!!this._pasteAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-paste" data-testid="paste-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._pasteAnnounce.value}</div>` : nothing}${!!this._rangeAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-range" data-testid="range-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._rangeAnnounce.value}</div>` : nothing}${!!this._liveAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-sortfilter" data-testid="sortfilter-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._liveAnnounce.value}</div>` : nothing}<div class="rdt-toolbar" data-rozie-s-d5dcab4c>
1428
1598
  <input class="rdt-global-filter" type="text" role="searchbox" aria-label="Search table" .value=${this.globalFilterValue()} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onGlobalFilterInput($event); }} data-rozie-s-d5dcab4c />
1429
1599
 
1430
1600
  ${this.allLeafColumns().length ? html`<details class="rdt-colvis" data-rozie-s-d5dcab4c>
@@ -1443,11 +1613,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1443
1613
  ${repeat<any>(this.groupingKeys(), (gk, _idx) => gk, (gk, _idx) => html`<span class="rdt-group-token" data-group-token="" data-rozie-s-d5dcab4c>${rozieDisplay(gk)}</span>`)}
1444
1614
  </slot>`}
1445
1615
  </div>` : nothing}${this.isWindowed() ? html`<div class="rdt-scroll" style=${rozieStyle(this.rowsWindowed() && this.maxHeight ? 'max-height:' + this.maxHeight + ';overflow:auto;--rozie-data-table-max-height:' + this.maxHeight : 'overflow:auto')} data-rozie-s-d5dcab4c>
1446
- <table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader, 'rdt-col-windowed': this.colsWindowed() }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
1616
+ <table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader, 'rdt-col-windowed': this.colsWindowed() }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
1447
1617
  <thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
1448
1618
  ${repeat<any>(this._headerGroups.value, (hg, hgLevel) => hg.id, (hg, hgLevel) => html`<tr class="rdt-tr" role="row" aria-rowindex=${rozieAttr(hgLevel + 1)} data-rozie-s-d5dcab4c>
1449
1619
 
1450
- ${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}${repeat<any>(this.windowedHeadersFor(hg, hgLevel), (wh, _idx) => wh.header.id, (wh, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(wh.header.column.id), 'rdt-expander-th': this.isExpanderColumn(wh.header.column.id), 'rdt-th-resizing': this.columnIsResizing(wh.header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, wh.header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" 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(this.headerColIndexOf(hg, wh.header))} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, wh.header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(wh.header.column.id))} style=${rozieStyle(this.thStyle(wh.header))} data-rozie-s-d5dcab4c>
1620
+ ${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}${repeat<any>(this.windowedHeadersFor(hg, hgLevel), (wh, _idx) => wh.header.id, (wh, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(wh.header.column.id), 'rdt-expander-th': this.isExpanderColumn(wh.header.column.id), 'rdt-pinned': this.columnPinSide(wh.header.column.id) !== '', 'rdt-th-resizing': this.columnIsResizing(wh.header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, wh.header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" 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(this.headerColIndexOf(hg, wh.header))} aria-colindex=${rozieAttr(this.headerLeafStart(hg, wh.header) + 1)} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, wh.header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(wh.header.column.id))} style=${rozieStyle(this.thStyle(wh.header, wh.width))} data-rozie-s-d5dcab4c>
1451
1621
  ${this.isSelectColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1452
1622
  ${this.selectAll !== undefined ? this.selectAll({checked: this.isAllRowsSelected(), indeterminate: this.isSomeRowsSelected(), toggle: this.onToggleAllRows}) : html`<slot name="selectAll" data-rozie-params=${(() => { try { return JSON.stringify({checked: this.isAllRowsSelected(), indeterminate: this.isSomeRowsSelected()}); } catch { return '{}'; } })()} @rozie-select-all-toggle=${($event: CustomEvent) => ((this.onToggleAllRows) as (...args: any[]) => any)($event.detail)}>
1453
1623
  ${this.selectionMode === 'multiple' ? html`<input class="rdt-select-all" type="checkbox" aria-label="Select all rows" ?checked=${this.isAllRowsSelected()} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleAllRows($event); }} data-rozie-s-d5dcab4c />` : nothing}</slot>`}
@@ -1478,11 +1648,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1478
1648
  ${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}</tr>`)}
1479
1649
 
1480
1650
  ${this.hasAnyFilterableColumn() ? html`<tr class="rdt-filter-row" data-rozie-s-d5dcab4c>
1481
- ${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}${repeat<any>(this.windowedHeadersFor(this._headerGroups.value[this._headerGroups.value.length - 1], this._headerGroups.value.length - 1), (wh, _idx) => wh.header.id, (wh, _idx) => html`<th class="rdt-filter-cell" role="presentation" data-col=${rozieAttr(wh.header.column.id)} style=${rozieStyle(this.pinStyle(wh.header.column.id))} data-rozie-s-d5dcab4c>
1651
+ ${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}${repeat<any>(this.windowedHeadersFor(this._headerGroups.value[this._headerGroups.value.length - 1], this._headerGroups.value.length - 1), (wh, _idx) => wh.header.id, (wh, _idx) => html`<th class="${Object.entries({ "rdt-filter-cell": true, 'rdt-pinned': this.columnPinSide(wh.header.column.id) !== '' }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="presentation" data-col=${rozieAttr(wh.header.column.id)} style=${rozieStyle(this.pinStyle(wh.header.column.id))} data-rozie-s-d5dcab4c>
1482
1652
  ${this.isSelectColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : this.isExpanderColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
1483
1653
  ${this.columnIsFilterable(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1484
- ${this.rozieSlots?.[`filter-${wh.header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${wh.header.column.id}`]!({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="${`filter-${wh.header.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1485
- ${this.filter !== undefined ? this.filter({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="filter" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1654
+ ${this.rozieSlots?.[`filter-${wh.header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${wh.header.column.id}`]!({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), columnLabel: this.headerLabel(wh.header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="${`filter-${wh.header.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), columnLabel: this.headerLabel(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1655
+ ${this.filter !== undefined ? this.filter({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), columnLabel: this.headerLabel(wh.header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="filter" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), columnLabel: this.headerLabel(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1486
1656
  <input class="rdt-col-filter" type="text" aria-label=${rozieAttr('Filter ' + this.headerLabel(wh.header.column.id))} .value=${this.columnFilterValue(wh.header.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onColumnFilterInput(wh.header.column.id, $event); }} @click=${($event: MouseEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.stopEvent($event); }} data-rozie-s-d5dcab4c />
1487
1657
  </slot>`}
1488
1658
  </slot>`}
@@ -1496,11 +1666,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1496
1666
  </tr>
1497
1667
 
1498
1668
  ${repeat<any>(this.windowedRows(), (wr, _idx) => wr.row.id, (wr, _idx) => html`
1499
- <tr class="${Object.entries({ "rdt-tr": true, 'rdt-group-header': this.rowIsGrouped(wr.row), 'rdt-row-pinned': wr.pinned }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="row" data-row=${rozieAttr(wr.vi.index)} aria-rowindex=${rozieAttr(this.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(this.rowIsGrouped(wr.row) ? wr.row.id : null)} data-group-leaf=${rozieAttr(this.groupingActive() && !this.rowIsGrouped(wr.row) ? wr.row.id : null)} aria-expanded=${rozieAttr(this.rowIsGrouped(wr.row) ? !!this.rowIsExpanded(wr.row) : null)} aria-selected=${rozieAttr(this.selectionMode !== 'none' ? !!this.rowIsSelected(wr.row) : null)} aria-level=${rozieAttr(this.groupingActive() ? wr.row.depth + 1 : null)} data-rozie-s-d5dcab4c>
1669
+ <tr class="${Object.entries({ "rdt-tr": true, 'rdt-group-header': this.rowIsGrouped(wr.row), 'rdt-row-pinned': wr.pinned, 'rdt-placeholder-row': this.rowIsLazyPlaceholder(wr.row) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="row" data-row=${rozieAttr(wr.vi.index)} aria-rowindex=${rozieAttr(this.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(this.rowIsGrouped(wr.row) ? wr.row.id : null)} data-group-leaf=${rozieAttr(this.groupingActive() && !this.rowIsGrouped(wr.row) ? wr.row.id : null)} aria-expanded=${rozieAttr(this.rowIsGrouped(wr.row) ? !!this.rowIsExpanded(wr.row) : null)} aria-selected=${rozieAttr(this.selectionMode !== 'none' ? !!this.rowIsSelected(wr.row) : null)} aria-level=${rozieAttr(this.groupingActive() ? wr.row.depth + 1 : null)} aria-busy=${rozieAttr(this.rowIsLazyPlaceholder(wr.row) ? 'true' : null)} data-rozie-s-d5dcab4c>
1500
1670
 
1501
- ${this.colsWindowed() ? html`<td class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>` : nothing}${repeat<any>(this.windowedCells(wr.row), (cell, _idx) => cell.id, (cell, _idx) => html`<td class="${Object.entries({ "rdt-td": true, 'rdt-select-td': this.isSelectColumn(cell.column.id), 'rdt-expander-td': this.isExpanderColumn(cell.column.id), 'rdt-in-range': this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)), 'rdt-cell-active': this.isActiveCell(String(wr.vi.index), this.colIndexOf(wr.row, cell)) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.cellRole())} data-col=${rozieAttr(cell.column.id)} data-grid-cell="" data-row=${rozieAttr(wr.vi.index)} data-col-index=${rozieAttr(this.colIndexOf(wr.row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(wr.vi.index), this.colIndexOf(wr.row, cell)))} style=${rozieStyle(this.bodyCellStyle(wr.row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(wr.vi.index, this.colIndexOf(wr.row, cell)))} data-in-range=${rozieAttr(this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
1671
+ ${this.colsWindowed() ? html`<td class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>` : nothing}${repeat<any>(this.windowedCells(wr.row), (cell, _idx) => cell.id, (cell, _idx) => html`<td class="${Object.entries({ "rdt-td": true, 'rdt-select-td': this.isSelectColumn(cell.column.id), 'rdt-expander-td': this.isExpanderColumn(cell.column.id), 'rdt-pinned': this.columnPinSide(cell.column.id) !== '', 'rdt-in-range': this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)), 'rdt-cell-active': this.isActiveCell(String(wr.vi.index), this.colIndexOf(wr.row, cell)) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.cellRole())} data-col=${rozieAttr(cell.column.id)} data-grid-cell="" data-row=${rozieAttr(wr.vi.index)} data-col-index=${rozieAttr(this.colIndexOf(wr.row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(wr.vi.index), this.colIndexOf(wr.row, cell)))} style=${rozieStyle(this.bodyCellStyle(wr.row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(wr.vi.index, this.colIndexOf(wr.row, cell)))} aria-colindex=${rozieAttr(this.colIndexOf(wr.row, cell) + 1)} aria-selected=${rozieAttr(this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)) ? 'true' : null)} data-in-range=${rozieAttr(this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
1502
1672
 
1503
- ${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1673
+
1674
+ ${this.rowIsLazyPlaceholder(wr.row) && this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1675
+
1676
+ <input class="rdt-select-placeholder" type="checkbox" disabled="" tabindex="-1" aria-hidden="true" style="visibility:hidden" data-rozie-s-d5dcab4c />
1677
+ </span>` : this.rowIsLazyPlaceholder(wr.row) && this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : this.rowIsLazyPlaceholder(wr.row) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1678
+ ${this.placeholder !== undefined ? this.placeholder({index: wr.vi.index, columnId: cell.column.id}) : html`<slot name="placeholder" data-rozie-params=${(() => { try { return JSON.stringify({index: wr.vi.index, columnId: cell.column.id}); } catch { return '{}'; } })()}>
1679
+ <span class="rdt-skeleton" aria-hidden="true" data-rozie-s-d5dcab4c>&#8203;</span>
1680
+ </slot>`}
1681
+ </span>` : this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1504
1682
  ${this.rowCanExpand(wr.row) ? html`<button class="rdt-expander" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(wr.row)} aria-label=${rozieAttr(this.rowIsExpanded(wr.row) ? 'Collapse row' : 'Expand row')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(wr.row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(wr.row) ? '▾' : '▸')}</button>` : nothing}</span>` : this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1505
1683
  ${this.selectCell !== undefined ? this.selectCell({row: this.cellSlotRow(wr.row), checked: this.rowIsSelected(wr.row), toggle: e => this.onToggleRow(wr.row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: this.cellSlotRow(wr.row), checked: this.rowIsSelected(wr.row)}); } catch { return '{}'; } })()} @rozie-select-cell-toggle=${($event: CustomEvent) => ((e => this.onToggleRow(wr.row, e)) as (...args: any[]) => any)($event.detail)}>
1506
1684
  <input class="rdt-select-row" type="checkbox" aria-label="Select row" ?checked=${this.rowIsSelected(wr.row)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleRow(wr.row, $event); }} data-rozie-s-d5dcab4c />
@@ -1514,15 +1692,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1514
1692
  </span>
1515
1693
  <span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(wr.row) + ')')}</span>
1516
1694
  </span>` : this.isEditing(wr.vi.index, this.colIndexOf(wr.row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1517
- ${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
1518
- ${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
1519
- </select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1520
- ${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
1521
- ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
1522
- <input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />
1695
+ ${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
1696
+ ${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} ?selected=${opt.value === this.editorValueFor(cell.column.id)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
1697
+ </select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c>
1698
+ ${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
1699
+ ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
1700
+ <input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />
1523
1701
  </slot>`}
1524
1702
  </slot>`}
1525
- </span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
1703
+ </span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
1526
1704
  ${this.rozieSlots?.[`cell-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`cell-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="${`cell-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>
1527
1705
  ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1528
1706
  </slot>`}
@@ -1541,10 +1719,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1541
1719
  </tr>
1542
1720
  </tbody>
1543
1721
  </table>
1544
- </div>` : html`<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
1722
+ </div>` : html`<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
1545
1723
  <thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
1546
1724
  ${repeat<any>(this._headerGroups.value, (hg, hgLevel) => hg.id, (hg, hgLevel) => html`<tr class="rdt-tr" role="row" aria-rowindex=${rozieAttr(hgLevel + 1)} data-rozie-s-d5dcab4c>
1547
- ${repeat<any>(hg.headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(header.column.id), 'rdt-expander-th': this.isExpanderColumn(header.column.id), 'rdt-th-resizing': this.columnIsResizing(header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" 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(this.headerColIndexOf(hg, header))} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(header.column.id))} style=${rozieStyle(this.thStyle(header))} data-rozie-s-d5dcab4c>
1725
+ ${repeat<any>(hg.headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(header.column.id), 'rdt-expander-th': this.isExpanderColumn(header.column.id), 'rdt-pinned': this.columnPinSide(header.column.id) !== '', 'rdt-th-resizing': this.columnIsResizing(header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" 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(this.headerColIndexOf(hg, header))} aria-colindex=${rozieAttr(this.headerLeafStart(hg, header) + 1)} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(header.column.id))} style=${rozieStyle(this.thStyle(header))} data-rozie-s-d5dcab4c>
1548
1726
 
1549
1727
 
1550
1728
  ${this.isSelectColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
@@ -1580,11 +1758,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1580
1758
  </tr>`)}
1581
1759
 
1582
1760
  ${this.hasAnyFilterableColumn() ? html`<tr class="rdt-filter-row" data-rozie-s-d5dcab4c>
1583
- ${repeat<any>(this._headerGroups.value[this._headerGroups.value.length - 1].headers, (header, _idx) => header.id, (header, _idx) => html`<th class="rdt-filter-cell" role="presentation" style=${rozieStyle(this.pinStyle(header.column.id))} data-rozie-s-d5dcab4c>
1761
+ ${repeat<any>(this._headerGroups.value[this._headerGroups.value.length - 1].headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-filter-cell": true, 'rdt-pinned': this.columnPinSide(header.column.id) !== '' }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="presentation" style=${rozieStyle(this.pinStyle(header.column.id))} data-rozie-s-d5dcab4c>
1584
1762
  ${this.isSelectColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : this.isExpanderColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
1585
1763
  ${this.columnIsFilterable(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1586
- ${this.rozieSlots?.[`filter-${header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${header.column.id}`]!({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="${`filter-${header.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id)}); } catch { return '{}'; } })()} @rozie-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1587
- ${this.filter !== undefined ? this.filter({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="filter" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1764
+ ${this.rozieSlots?.[`filter-${header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${header.column.id}`]!({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), columnLabel: this.headerLabel(header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="${`filter-${header.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), columnLabel: this.headerLabel(header.column.id)}); } catch { return '{}'; } })()} @rozie-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1765
+ ${this.filter !== undefined ? this.filter({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), columnLabel: this.headerLabel(header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="filter" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), columnLabel: this.headerLabel(header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
1588
1766
  <input class="rdt-col-filter" type="text" aria-label=${rozieAttr('Filter ' + this.headerLabel(header.column.id))} .value=${this.columnFilterValue(header.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onColumnFilterInput(header.column.id, $event); }} @click=${($event: MouseEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.stopEvent($event); }} data-rozie-s-d5dcab4c />
1589
1767
  </slot>`}
1590
1768
  </slot>`}
@@ -1595,7 +1773,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1595
1773
 
1596
1774
  ${repeat<any>(this._rows.value, (row, _idx) => row.id, (row, _idx) => html`
1597
1775
  <tr class="${Object.entries({ "rdt-tr": true, 'rdt-group-header': this.rowIsGrouped(row) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="row" data-depth=${rozieAttr(row.depth)} aria-rowindex=${rozieAttr(this.bodyAriaRowIndex(row))} data-group-header=${rozieAttr(this.rowIsGrouped(row) ? row.id : null)} data-group-leaf=${rozieAttr(this.groupingActive() && !this.rowIsGrouped(row) ? row.id : null)} aria-expanded=${rozieAttr(this.rowIsGrouped(row) ? !!this.rowIsExpanded(row) : null)} aria-selected=${rozieAttr(this.selectionMode !== 'none' ? !!this.rowIsSelected(row) : null)} aria-level=${rozieAttr(this.groupingActive() ? row.depth + 1 : null)} data-rozie-s-d5dcab4c>
1598
- ${repeat<any>(this.visibleCellsFor(row), (cell, _idx) => cell.id, (cell, _idx) => html`<td class="${Object.entries({ "rdt-td": true, 'rdt-select-td': this.isSelectColumn(cell.column.id), 'rdt-expander-td': this.isExpanderColumn(cell.column.id), 'rdt-in-range': this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)), 'rdt-cell-active': this.isActiveCell(String(this.rowIndexOf(row)), this.colIndexOf(row, cell)) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.cellRole())} data-col=${rozieAttr(cell.column.id)} data-grid-cell="" data-row=${rozieAttr(this.rowIndexOf(row))} data-col-index=${rozieAttr(this.colIndexOf(row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(this.rowIndexOf(row)), this.colIndexOf(row, cell)))} style=${rozieStyle(this.bodyCellStyle(row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(this.rowIndexOf(row), this.colIndexOf(row, cell)))} data-in-range=${rozieAttr(this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
1776
+ ${repeat<any>(this.visibleCellsFor(row), (cell, _idx) => cell.id, (cell, _idx) => html`<td class="${Object.entries({ "rdt-td": true, 'rdt-select-td': this.isSelectColumn(cell.column.id), 'rdt-expander-td': this.isExpanderColumn(cell.column.id), 'rdt-pinned': this.columnPinSide(cell.column.id) !== '', 'rdt-in-range': this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)), 'rdt-cell-active': this.isActiveCell(String(this.rowIndexOf(row)), this.colIndexOf(row, cell)) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.cellRole())} data-col=${rozieAttr(cell.column.id)} data-grid-cell="" data-row=${rozieAttr(this.rowIndexOf(row))} data-col-index=${rozieAttr(this.colIndexOf(row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(this.rowIndexOf(row)), this.colIndexOf(row, cell)))} style=${rozieStyle(this.bodyCellStyle(row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(this.rowIndexOf(row), this.colIndexOf(row, cell)))} aria-colindex=${rozieAttr(this.colIndexOf(row, cell) + 1)} aria-selected=${rozieAttr(this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? 'true' : null)} data-in-range=${rozieAttr(this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
1599
1777
 
1600
1778
  ${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1601
1779
  ${this.rowCanExpand(row) ? html`<button class="rdt-expander" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(row)} aria-label=${rozieAttr(this.rowIsExpanded(row) ? 'Collapse row' : 'Expand row')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(row) ? '▾' : '▸')}</button>` : nothing}</span>` : this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
@@ -1611,15 +1789,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1611
1789
  </span>
1612
1790
  <span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(row) + ')')}</span>
1613
1791
  </span>` : this.isEditing(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1614
- ${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
1615
- ${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
1616
- </select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1617
- ${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
1618
- ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
1619
- <input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />
1792
+ ${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
1793
+ ${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} ?selected=${opt.value === this.editorValueFor(cell.column.id)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
1794
+ </select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c>
1795
+ ${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
1796
+ ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
1797
+ <input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />
1620
1798
  </slot>`}
1621
1799
  </slot>`}
1622
- </span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
1800
+ </span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
1623
1801
  ${this.rozieSlots?.[`cell-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`cell-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="${`cell-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>
1624
1802
  ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1625
1803
  </slot>`}
@@ -1639,10 +1817,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1639
1817
  </span>
1640
1818
  <button class="rdt-page-btn rdt-page-next" type="button" ?disabled=${!this.canNextPage()} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onNextPage(); }} data-rozie-s-d5dcab4c>Next</button>
1641
1819
  <select class="rdt-page-size" aria-label="Rows per page" .value=${this.pageSize()} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onPageSizeChange($event); }} data-rozie-s-d5dcab4c>
1642
- <option value=${10} data-rozie-s-d5dcab4c>10</option>
1643
- <option value=${25} data-rozie-s-d5dcab4c>25</option>
1644
- <option value=${50} data-rozie-s-d5dcab4c>50</option>
1645
- <option value=${100} data-rozie-s-d5dcab4c>100</option>
1820
+ <option value=${10} ?selected=${10 === this.pageSize()} data-rozie-s-d5dcab4c>10</option>
1821
+ <option value=${25} ?selected=${25 === this.pageSize()} data-rozie-s-d5dcab4c>25</option>
1822
+ <option value=${50} ?selected=${50 === this.pageSize()} data-rozie-s-d5dcab4c>50</option>
1823
+ <option value=${100} ?selected=${100 === this.pageSize()} data-rozie-s-d5dcab4c>100</option>
1646
1824
  </select>
1647
1825
  </div>` : nothing}</div>
1648
1826
  `;
@@ -1694,6 +1872,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1694
1872
 
1695
1873
  remeasureDisposed = false;
1696
1874
 
1875
+ // D-19 (quick 260923-rrr): "the user scrolled to the end" — recorded only when the scroll
1876
+ // offset actually MOVED (a growth-driven onChange leaves it where it was), with the row count
1877
+ // and offset at that moment. See scheduleRemeasure() in virtualization.rzts.
1878
+ scrollEndPinned: boolean = false;
1879
+
1880
+ scrollEndPinnedCount: number = -1;
1881
+
1882
+ scrollEndPinnedTop: number = -1;
1883
+
1697
1884
  // ── Grid interaction-mode constants + DOM root (phase 49, REQ-2/6) ────────────────────
1698
1885
  // Fixed PageUp/PageDown row step (D-06). Phase 53 swaps this for the visible-window size
1699
1886
  // via the same focusActiveCell() scroll-into-view seam — kept a top-level const so that
@@ -1707,6 +1894,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1707
1894
  // computed/template binding (ROZ123).
1708
1895
  gridRoot: any = null;
1709
1896
 
1897
+ // ── B-02: "did the user just click OUTSIDE the grid?" ──────────────────────────────────
1898
+ // Set by the document-level pointerdown listener registered in $onMount, consumed by
1899
+ // onEditorBlur's null-relatedTarget branch (editorBindings.rzts), which explains in full why
1900
+ // the decision has to come from here and be routed back there. In short: a null relatedTarget
1901
+ // on its own cannot tell a click onto a non-focusable page region (the edit must commit) from
1902
+ // an editor being recycled out of the virtual window (it must NOT), and a document listener
1903
+ // cannot commit for itself on React because it closes over the render that attached it — the
1904
+ // draft it would write is the one from editor-open. A plain module-let is hoisted to a
1905
+ // per-instance `useRef` there, so it is read FRESH from the template-bound blur handler.
1906
+ // Capture phase, so a stopPropagation anywhere in the page cannot hide the click from us.
1907
+ outsidePointerDown = false;
1908
+
1909
+ docPointerDown: any = null;
1910
+
1710
1911
  // Echo-guard: while WE are writing a slice back, the re-feed watcher must not re-enter
1711
1912
  // the funnel. A counter (not a boolean) so nested writes are safe.
1712
1913
  programmatic = 0;
@@ -1948,6 +2149,56 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1948
2149
  // ({ id?, header, columns: [...] }) maps to a multi-level header GROUP column
1949
2150
  // whose children are built recursively (B12 — grouped/multi-level column headers).
1950
2151
  // Returns null for an unusable entry (no id/field, unsafe key, empty group).
2152
+ /**
2153
+ * A-01 — translate the public per-column `width` into table-core's `size`.
2154
+ *
2155
+ * `width` was written onto the ColumnDef in both build branches and read by NOTHING:
2156
+ * table-core resolves column width from `size` (a NUMBER of px), which only the two
2157
+ * chrome columns ever set. So the documented API was completely inert — the exact
2158
+ * sibling of the inert `pinned` that 0.5.0 closed.
2159
+ *
2160
+ * `size` is numeric px by construction, so only a number or a px string can map onto
2161
+ * it; any other CSS length (`12rem`, `20%`, `auto`) has no numeric px value at build
2162
+ * time and is deliberately NOT guessed — it returns null and the column keeps
2163
+ * table-core's default. The `width` prop's docs say exactly this.
2164
+ *
2165
+ * Returns null (not undefined) for "no usable width" so callers can test `!= null`
2166
+ * without tripping over a legitimate 0.
2167
+ */
2168
+ parseWidthToSize = (w: any) => {
2169
+ if (typeof w === 'number') return Number.isFinite(w) && w > 0 ? w : null;
2170
+ if (typeof w !== 'string') return null;
2171
+ const t = w.trim();
2172
+ if (t === '') return null;
2173
+ const m = /^(\d+(?:\.\d+)?)(px)?$/i.exec(t);
2174
+ if (!m) return null;
2175
+ const n = Number.parseFloat(m[1]);
2176
+ return Number.isFinite(n) && n > 0 ? n : null;
2177
+ };
2178
+
2179
+ // ── E-03 ────────────────────────────────────────────────────────────────────────────────
2180
+ // The union check itself is `editorKindWarning` in helpers/columnDefUtils.ts (pure, so the
2181
+ // contract is unit-testable). Here we own the two things that are not pure: WHEN it runs and
2182
+ // how often it speaks.
2183
+ //
2184
+ // WHEN: at column-BUILD time, not in `editorTypeOf`, which is called per cell per render (D-23).
2185
+ // HOW OFTEN: latched per `id:value`, so a rebuild — the memo misses whenever the column set
2186
+ // changes identity — cannot turn one misconfiguration into a console flood.
2187
+ // Unguarded console.warn, matching the D-07/D-08 virtual-misconfig warns in DataTable.rozie:
2188
+ // a process.env guard is not bundler-portable across six targets, and always-warn-on-misconfig
2189
+ // is the established call in this component.
2190
+ editorWarned = Object.create(null);
2191
+
2192
+ checkEditorKind = (id: any, editor: any) => {
2193
+ const msg = editorKindWarning(id, editor);
2194
+ if (!msg) return editor;
2195
+ const key = id + '\u0000' + String(editor);
2196
+ if (this.editorWarned[key]) return editor;
2197
+ this.editorWarned[key] = true;
2198
+ console.warn(msg);
2199
+ return editor;
2200
+ };
2201
+
1951
2202
  buildConfigDef = (c: any) => {
1952
2203
  if (!c) return null;
1953
2204
  // Grouped (multi-level) header column: an entry carrying a `columns` array. table-core's
@@ -2003,11 +2254,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2003
2254
  aggregationFn: wrapAggregationFn(c.aggregationFn),
2004
2255
  pinned: c.pinned != null ? c.pinned : '',
2005
2256
  width: c.width != null ? c.width : '',
2257
+ // A-01 — `width` alone is inert; table-core sizes from `size`. Spread so an
2258
+ // unusable width leaves `size` ABSENT rather than explicitly undefined, which
2259
+ // would override a consumer's `defaultColumn.size`.
2260
+ ...(this.parseWidthToSize(c.width) != null ? {
2261
+ size: this.parseWidthToSize(c.width)
2262
+ } : {}),
2006
2263
  // Editable-cell config (Phase 51) → ColumnDef.meta, the table-core per-column
2007
2264
  // metadata carrier the display↔editor branch + runValidator read. Off by default.
2008
2265
  meta: {
2009
2266
  editable: c.editable === true,
2010
- editor: c.editor != null ? c.editor : 'text',
2267
+ editor: c.editor != null ? this.checkEditorKind(id, c.editor) : 'text',
2011
2268
  editorOptions: c.editorOptions != null ? c.editorOptions : [],
2012
2269
  validate: typeof c.validate === 'function' ? c.validate : null
2013
2270
  }
@@ -2092,10 +2349,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2092
2349
  aggregationFn: wrapAggregationFn(spec.aggregationFn),
2093
2350
  pinned: spec.pinned != null ? spec.pinned : '',
2094
2351
  width: spec.width != null ? spec.width : '',
2352
+ // A-01 — see buildConfigDef; same mapping for the <Column> registry branch.
2353
+ ...(this.parseWidthToSize(spec.width) != null ? {
2354
+ size: this.parseWidthToSize(spec.width)
2355
+ } : {}),
2095
2356
  // Editable-cell config (Phase 51) → ColumnDef.meta from the <Column> registry spec.
2096
2357
  meta: {
2097
2358
  editable: spec.editable === true,
2098
- editor: spec.editor != null ? spec.editor : 'text',
2359
+ editor: spec.editor != null ? this.checkEditorKind(id, spec.editor) : 'text',
2099
2360
  editorOptions: spec.editorOptions != null ? spec.editorOptions : [],
2100
2361
  validate: typeof spec.validate === 'function' ? spec.validate : null
2101
2362
  }
@@ -2777,9 +3038,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2777
3038
  }
2778
3039
  }
2779
3040
  // Active cell's column (D-10): required or focusActiveCell()'s [data-col-index="N"]
2780
- // querySelector cannot resolve for an off-window active cell. Body cells only — a header-
2781
- // active cell has no column-axis rendering dependency here (headers window separately).
2782
- if (!this._activeIsHeader.value && this._activeColIndex.value >= 0 && out.indexOf(this._activeColIndex.value) === -1) {
3041
+ // querySelector cannot resolve for an off-window active cell.
3042
+ //
3043
+ // B-03: this used to be body-cells-only, on the stated grounds that "a header-active cell
3044
+ // has no column-axis rendering dependency here (headers window separately)". That is false —
3045
+ // `windowedHeadersFor()` slices the header row from `windowedColIndices()`, the very set this
3046
+ // function feeds, so a header cell has EXACTLY the same dependency a body cell does. With the
3047
+ // active header column unforced, focusing a leaf header in a 60-column windowed grid and
3048
+ // pressing End left `resolveCellEl` with nothing to find: focus froze on the old `<th>` while
3049
+ // the roving model moved on, and `cellTabindex` handed `tabindex="0"` to a cell that does not
3050
+ // exist — so the grid had no tab stop at all. The gate is gone; the column is forced for
3051
+ // whichever axis the active cell is on.
3052
+ if (this._activeColIndex.value >= 0 && out.indexOf(this._activeColIndex.value) === -1) {
2783
3053
  out.push(this._activeColIndex.value);
2784
3054
  }
2785
3055
  // Editing column (D-10): required or an open editor unmounts mid-keystroke when a
@@ -2856,21 +3126,65 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2856
3126
  // committed yet), so the rAF pass is what observes React's recycled rows.
2857
3127
  // Each pass only OBSERVES + measures the live window; measureElement is idempotent on an
2858
3128
  // already-observed node, so running both is cheap and loop-free.
3129
+ // Upper bound on the frames scheduleRemeasure() will wait for the framework to commit a recycled
3130
+ // window (N-05). Measured commit lag is 1 frame on React/Angular; 10 is generous headroom.
3131
+ REMEASURE_MAX_FRAMES = 10;
3132
+
2859
3133
  scheduleRemeasure = () => {
2860
3134
  // 87-13: never arm a new sweep after teardown — an onChange can still fire from a
2861
3135
  // virtualizer whose own ResizeObserver cleanup has run but whose queued tick was already
2862
3136
  // in flight.
2863
3137
  if (this.remeasureDisposed) return;
3138
+ // D-19 (quick 260923-rrr): remember whether the view is at the END — but only when the scroll
3139
+ // offset MOVED since the last record. A measurement-driven onChange (rows growing) arrives
3140
+ // with the offset unchanged, and it is exactly that growth that moves the view off the end,
3141
+ // so it must not clear the pin. (virtual-core's isScrolling cannot tell the two apart: it
3142
+ // stays true for 150ms after the user's scroll, spanning the growth.)
3143
+ if (this.virtualizer && this.gridScrollEl && !this.virtualizer.scrollState && this.gridScrollEl.scrollTop !== this.scrollEndPinnedTop) {
3144
+ this.scrollEndPinnedTop = this.gridScrollEl.scrollTop;
3145
+ // Judged on virtual-core's MODEL (total size − viewport − offset), not DOM geometry: its own
3146
+ // above-viewport adjustments move scrollTop before the framework commits the matching
3147
+ // spacer, so the DOM transiently reads "not at the end" for a view that is (measured on
3148
+ // Angular: a 12px adjustment cleared a DOM-judged pin and left the view 12px short).
3149
+ this.scrollEndPinned = this.virtualizer.getVirtualDistanceFromEnd() <= 1;
3150
+ this.scrollEndPinnedCount = this.windowSource().length;
3151
+ }
2864
3152
  if (this.remeasurePending) return;
2865
3153
  this.remeasurePending = true;
2866
3154
  let ranMicro = false;
2867
3155
  const microPass = () => {
2868
3156
  this.remeasureWindow();
2869
3157
  };
3158
+ // N-05 (quick 260923-rrr): the rAF pass keys on the OUTCOME, not on "a frame went by". React
3159
+ // and Angular commit the recycled window AFTER that first rAF, so a single pass measured the
3160
+ // OLD rows and the new ones were only handed to measureElement when virtual-core's 150ms
3161
+ // scrolling-ended tick fired an onChange — the user saw the un-adjusted layout, then a 1–3 row
3162
+ // jump (measured). While the committed <tr> set does not yet cover the virtualizer's window,
3163
+ // re-run next frame, bounded so a window that can never be covered cannot spin forever.
3164
+ let rafAttempts = 0;
2870
3165
  const rafPass = () => {
2871
3166
  this.remeasureRaf = null;
3167
+ const covered = this.remeasureWindow();
3168
+ rafAttempts = rafAttempts + 1;
3169
+ if (!covered && rafAttempts < this.REMEASURE_MAX_FRAMES && !this.remeasureDisposed && typeof requestAnimationFrame === 'function') {
3170
+ this.remeasureRaf = requestAnimationFrame(rafPass);
3171
+ return;
3172
+ }
3173
+ // D-19: keep a user who scrolled to the END at the end. When rows at/above the viewport
3174
+ // measure taller than their estimate, virtual-core compensates with a scroll adjustment — but
3175
+ // on React/Angular that adjustment is written BEFORE the framework commits the taller spacer,
3176
+ // so the browser clamps it to the OLD maximum and it is lost (measured: the view ended
3177
+ // 593–1025px above the bottom on React, 30px on Angular). This pass runs after the commit, so
3178
+ // the real maximum is known. Not when the row count changed: appending rows must not
3179
+ // auto-follow them.
3180
+ if (this.scrollEndPinned && this.gridScrollEl && !this.remeasureDisposed && this.windowSource().length === this.scrollEndPinnedCount) {
3181
+ const maxTop = this.gridScrollEl.scrollHeight - this.gridScrollEl.clientHeight;
3182
+ if (maxTop - this.gridScrollEl.scrollTop > 1) {
3183
+ this.gridScrollEl.scrollTop = maxTop;
3184
+ this.scrollEndPinnedTop = this.gridScrollEl.scrollTop;
3185
+ }
3186
+ }
2872
3187
  this.remeasurePending = false;
2873
- this.remeasureWindow();
2874
3188
  };
2875
3189
  if (typeof queueMicrotask !== 'undefined') {
2876
3190
  ranMicro = true;
@@ -2904,9 +3218,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2904
3218
  // unioned into the windowed slice when it scrolls off-window and its height is subtracted from
2905
3219
  // the appropriate spacer so the total stays exactly getTotalSize() (the 51-01-proven mechanism).
2906
3220
  // This is the data-table half of the D-05 windowing.rzts pin-extension hook (listbox provides none).
3221
+ //
3222
+ // B-06 widens this from "the editing row" to "the row that must not recycle out of the
3223
+ // window", which now includes the ACTIVE row in grid mode. The active cell is the grid's ONLY
3224
+ // `tabindex="0"` — `cellTabindex()` returns 0 for it and -1 for every other cell — so once a
3225
+ // wheel-scroll carried it out of the window the grid had NO tab stop at all: Tab could not
3226
+ // re-enter it, and the roving model still pointed at a cell with no DOM node, so
3227
+ // `resolveCellEl` returned null and a subsequent arrow key moved state with nothing to focus.
3228
+ // The column axis has forced the active-cell COLUMN since D-10 (`forcedColumns`); this is the
3229
+ // row-axis twin of that decision, and it costs at most one extra rendered row, in flow, with
3230
+ // its height subtracted from the spacer by the same 51-01 mechanism the editing pin uses.
3231
+ //
3232
+ // `$props.interactionMode` is read directly rather than through `isGrid()` (gridFocusNav.rzts):
3233
+ // that partial is imported AFTER this one, and this file's header documents the
3234
+ // forward-cross-partial-reference TDZ on React that the 87-02 lesson established. A prop read
3235
+ // has no such hazard.
2907
3236
  pinnedEditIndex = () => {
2908
3237
  if (this._editingRow.value >= 0) return this._editingRow.value;
2909
3238
  if (this._editingRowIndex.value != null) return this._editingRowIndex.value;
3239
+ if (this.interactionMode === 'grid' && !this._activeIsHeader.value && this._activeRow.value >= 0) return this._activeRow.value;
2910
3240
  return -1;
2911
3241
  };
2912
3242
 
@@ -2934,8 +3264,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2934
3264
  // cover unmount — virtualizer/gridRoot are assigned in $onMount and never nulled — so this
2935
3265
  // is the only thing standing between a pass queued pre-unmount and refineRowEstimate()'s
2936
3266
  // setOptions/scrollTop/$data.windowVer writes against a torn-down instance.
2937
- if (this.remeasureDisposed) return;
2938
- if (!this.virtualizer || !this.gridRoot) return;
3267
+ if (this.remeasureDisposed) return true;
3268
+ if (!this.virtualizer || !this.gridRoot) return true;
2939
3269
  // Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
2940
3270
  // exclusively during scrollToIndex / scrollToOffset (the D-12 scroll-then-focus seam) and
2941
3271
  // null for ordinary user/scrollTop-driven scrolling (verified virtual-core@3.17.1: set in
@@ -2943,9 +3273,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2943
3273
  // resizeItem nudge the offset and starve the scroll target (the Solid off-window focus
2944
3274
  // regression); the next settled onChange re-measures the stable window. Manual-scroll
2945
3275
  // recycling (the CR-01 case) has scrollState === null, so it measures normally.
2946
- if (this.virtualizer.scrollState) return;
3276
+ if (this.virtualizer.scrollState) return true;
2947
3277
  const trs = this.gridRoot.querySelectorAll('tbody.rdt-tbody > tr[data-index]');
2948
- for (const tr of trs as any) this.virtualizer.measureElement(tr);
3278
+ const rendered = new Set();
3279
+ for (const tr of trs as any) {
3280
+ this.virtualizer.measureElement(tr);
3281
+ rendered.add(tr.getAttribute('data-index'));
3282
+ }
3283
+ // N-05: did the committed rows cover the virtualizer's CURRENT window? False while the
3284
+ // framework has not yet committed the recycled set (see scheduleRemeasure).
3285
+ let covered = true;
3286
+ const items = this.virtualizer.getVirtualItems();
3287
+ for (let i = 0; i < items.length; i++) {
3288
+ if (!rendered.has(String(items[i].index))) {
3289
+ covered = false;
3290
+ break;
3291
+ }
3292
+ }
2949
3293
  // D-15 (Phase 87 87-07): trigger the shared engine's post-measurement fold + hysteresis
2950
3294
  // re-feed via the afterRowRemeasure mutable-let hook (DataTable.rozie's own script,
2951
3295
  // assigned to windowing.rzts's refineRowEstimate()) — NEVER a direct call from this file
@@ -2954,6 +3298,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2954
3298
  // documents for columnVirtualizerOptions()). Inert (no-op) until the host assigns it; the
2955
3299
  // off/pre-mount path never reaches this line since virtualizer is null there.
2956
3300
  if (this.afterRowRemeasure) this.afterRowRemeasure();
3301
+ return covered;
2957
3302
  };
2958
3303
 
2959
3304
  // D-04: this shell exports ONLY the impure, data-table-specific host pieces. The pure windowing
@@ -2962,6 +3307,148 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2962
3307
  // `@rozie-ui/headless-core/windowing.rzts` via bare specifier — the P0-proven cross-package inline
2963
3308
  // path that DISSOLVES the partial into the leaf (a re-export-from THROUGH this shell would survive as
2964
3309
  // a runtime import, not inline — verified). The math closes over these host symbols by convention.
3310
+ // ── Lazy rows (quick 260925-dtl part 2, oinbox dogfooding) ─────────────────────────────────────
3311
+ // `virtual` + `manual` + `rowCount`: the row space is rowCount long and `data` may be SPARSE —
3312
+ // an `undefined`/`null` entry (or anything past data.length) is a row not loaded yet. Such holes
3313
+ // are fed to table-core as placeholder sentinels (one cached object per index, so a re-feed does
3314
+ // not churn them) that render as placeholder rows and can't be selected, expanded, edited or
3315
+ // activated. ONLY the table-core feed is padded: every write-back derives from currentData(), the
3316
+ // consumer's own array, so a sentinel can never leak into `data`.
3317
+ LAZY_PLACEHOLDER_KEY = '__rdtLazyPlaceholder';
3318
+
3319
+ // LAZY_PLACEHOLDER_ROW_ID_PREFIX: the table-core row-id table.rowIdOption() (DataTable.rozie)
3320
+ // assigns to a placeholder ('__rdt_ph_' + index — no consumer row to ask, so it keys by
3321
+ // position). Shared here (not re-literaled) because migratePlaceholderSizeCache() below and
3322
+ // rowIdOption() must agree byte-for-byte on the prefix or the migration's key scan silently
3323
+ // finds nothing.
3324
+ LAZY_PLACEHOLDER_ROW_ID_PREFIX = '__rdt_ph_';
3325
+
3326
+ lazyPlaceholders: Record<number, any> = {};
3327
+
3328
+ lazyRowsActive = () => this.rowsWindowed() && !!this.manual && this.rowCount != null && this.rowCount > 0;
3329
+
3330
+ isLazyPlaceholder = (orig: any): boolean => !!orig && orig[this.LAZY_PLACEHOLDER_KEY] === true;
3331
+
3332
+ rowIsLazyPlaceholder = (row: any): boolean => !!row && this.isLazyPlaceholder(row.original);
3333
+
3334
+ feedData = () => {
3335
+ const d: any[] = this.currentData() || [];
3336
+ if (!this.lazyRowsActive()) return d;
3337
+ const n = Math.max(d.length, Number(this.rowCount));
3338
+ const out: any[] = new Array(n);
3339
+ for (let i = 0; i < n; i++) {
3340
+ const v = d[i];
3341
+ if (v != null) {
3342
+ out[i] = v;
3343
+ } else {
3344
+ if (!this.lazyPlaceholders[i]) this.lazyPlaceholders[i] = {
3345
+ [this.LAZY_PLACEHOLDER_KEY]: true,
3346
+ index: i
3347
+ };
3348
+ out[i] = this.lazyPlaceholders[i];
3349
+ }
3350
+ }
3351
+ return out;
3352
+ };
3353
+
3354
+ // migratePlaceholderSizeCache() (quick 260926, VR probe on data-table-lazy [react]): a filled
3355
+ // placeholder's row id changes ('__rdt_ph_' + i -> the real getRowId id), and virtual-core's
3356
+ // getItemKey re-keys by it — but virtual-core's itemSizeCache (dist/esm/index.js@3.17.1) is a
3357
+ // bare Map keyed by the OLD key, so the new key has no entry. The next getMeasurements() recompute
3358
+ // then estimates that row at $props.estimateRowHeight instead of its already-known measured size;
3359
+ // the CR-01 sweep re-measures it moments later and each delta fires one applyScrollAdjustment,
3360
+ // walking the window's anchor off by their sum (measured: 9 rows x -4px = -36px, row 5000 -> 4999
3361
+ // on React — the other 5 targets happened to keep reading the stale pre-refeed measurements array
3362
+ // and never saw the estimate at all). Fix: carry the cached size across the rekey, keyed off
3363
+ // table-core's OWN freshly-assigned id (table.getCoreRowModel().rows[i].id, NOT a locally
3364
+ // recomputed getRowId() call — must be byte-identical to what virtualItemKey(i) reads next) so
3365
+ // the cache is already correct before the caller's virtualizer.setOptions()/_willUpdate() re-feed.
3366
+ // Invalidating the memo needs BOTH strategies below, not just one: getMeasurements() is
3367
+ // memoized on [getMeasurementOptions(), itemSizeCacheVersion] on 3.15+ (confirmed from the
3368
+ // installed source; the same fact windowing.rzts's refineRowEstimate() comment documents) —
3369
+ // but our peer range is `^3`, and versions <=3.14 have NO itemSizeCacheVersion at all and key
3370
+ // the SAME memo on `this.itemSizeCache` (Map IDENTITY) instead — verified against the 3.14.0
3371
+ // tarball, whose own resizeItem() invalidates by reassigning `this.itemSizeCache = new Map(...)`,
3372
+ // never by bumping a version counter. Mutating the Map in place would be silently invisible to
3373
+ // that memo on <=3.14 (a stray `itemSizeCacheVersion++` there just creates a NaN-valued own
3374
+ // property nobody reads). So this reassigns the Map (a public field on every 3.x we support —
3375
+ // harmless on 3.15+, whose memo body re-reads `this.itemSizeCache` fresh) AND bumps the version
3376
+ // counter only where it actually exists. Scans the CACHE's own placeholder-prefixed keys
3377
+ // (bounded by rows actually measured so far), never rowCount (unbounded — 10,000+ here).
3378
+ migratePlaceholderSizeCache = (): void => {
3379
+ if (!this.virtualizer || !this.virtualizer.itemSizeCache || !this.table) return;
3380
+ const cache = this.virtualizer.itemSizeCache;
3381
+ const coreRows = this.table.getCoreRowModel().rows;
3382
+ let migrated = false;
3383
+ for (const oldKey of Array.from(cache.keys()) as any) {
3384
+ if (typeof oldKey !== 'string' || oldKey.indexOf(this.LAZY_PLACEHOLDER_ROW_ID_PREFIX) !== 0) continue;
3385
+ const i = Number(oldKey.slice(this.LAZY_PLACEHOLDER_ROW_ID_PREFIX.length));
3386
+ const row = coreRows[i];
3387
+ if (!row || this.rowIsLazyPlaceholder(row) || row.id === oldKey) continue;
3388
+ cache.set(row.id, cache.get(oldKey));
3389
+ cache.delete(oldKey);
3390
+ migrated = true;
3391
+ }
3392
+ if (!migrated) return;
3393
+ this.virtualizer.itemSizeCache = new Map(cache);
3394
+ if (typeof this.virtualizer.itemSizeCacheVersion === 'number') this.virtualizer.itemSizeCacheVersion++;
3395
+ };
3396
+
3397
+ // `visible-range-change { start, end }` — the RENDERED row window (overscan included), `end`
3398
+ // exclusive, over the full row space. Driven by the window-version bump every virtualizer change
3399
+ // routes through, deduped here so a scroll that doesn't move the window emits nothing.
3400
+ lastRangeStart = -1;
3401
+
3402
+ lastRangeEnd = -1;
3403
+
3404
+ emitVisibleRangeIfChanged = () => {
3405
+ if (!this.rowsWindowed() || !this.virtualizer) return;
3406
+ const items = this.virtualizer.getVirtualItems();
3407
+ if (!items.length) return;
3408
+ const start = items[0].index;
3409
+ const end = items[items.length - 1].index + 1;
3410
+ if (start === this.lastRangeStart && end === this.lastRangeEnd) return;
3411
+ this.lastRangeStart = start;
3412
+ this.lastRangeEnd = end;
3413
+ this.dispatchEvent(new CustomEvent("visible-range-change", {
3414
+ detail: {
3415
+ start,
3416
+ end
3417
+ },
3418
+ bubbles: true,
3419
+ composed: true
3420
+ }));
3421
+ };
3422
+
3423
+ // scrollToRow(index, options?) — quick 260927-a2r (oinbox dogfooding): plain scroll-into-view
3424
+ // for a windowed row, forwarding TanStack virtual-core's own ScrollToOptions ({ align?,
3425
+ // behavior? }) UNCHANGED to virtualizer.scrollToIndex — the SAME call focusActiveCell's D-12
3426
+ // off-window scroll-then-focus path (gridFocusNav.rzts) already drives, exposed here as a
3427
+ // standalone public verb rather than invented plumbing. INDEPENDENT of grid-mode focus
3428
+ // (feedback_no_false_equivalent_options / the todo's own open question): never reads or
3429
+ // writes $data.activeRow/activeColIndex, never emits activecell-change, and is callable in
3430
+ // EITHER interactionMode ('table' or 'grid') — a scroll, not a focus move. No-op when rows
3431
+ // are not windowed or the row virtualizer has not been constructed yet — mirrors the
3432
+ // isGrid()-gated no-op precedent on focusCell/clearActiveCell (gridActiveCellVerbs.rzts) for
3433
+ // the identical reason: the capability this verb drives isn't active. `index` is coerced
3434
+ // (T-260927A2R-01) — never range-clamped here: virtual-core's own scrollToIndex already
3435
+ // clamps to [0, options.count-1] internally (verified in @tanstack/virtual-core source).
3436
+ scrollToRow = (index: any, options?: {
3437
+ align?: 'start' | 'center' | 'end' | 'auto';
3438
+ behavior?: 'auto' | 'smooth' | 'instant';
3439
+ }): void => {
3440
+ if (!this.rowsWindowed() || !this.virtualizer) return;
3441
+ this.virtualizer.scrollToIndex(Math.trunc(Number(index)) || 0, options || undefined);
3442
+ };
3443
+
3444
+ // getScrollElement() — quick 260927-a2r: returns the DOM node the row AND column
3445
+ // virtualizer(s) observe/scroll (the `.rdt-scroll` div captured post-mount in
3446
+ // DataTable.rozie's own $onMount, D-03) — the supported replacement for a consumer reaching
3447
+ // into the internal `.rdt-scroll` class selector. Returns null when nothing is windowed:
3448
+ // gridScrollEl is only ever assigned inside the isWindowed() guard, so no extra gate is
3449
+ // needed here beyond that natural null.
3450
+ getScrollElement = (): any => this.gridScrollEl;
3451
+
2965
3452
  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
2966
3453
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
2967
3454
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
@@ -3343,6 +3830,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3343
3830
  // makes the eventual render land strictly AFTER this fold + re-feed has fully landed, on
3344
3831
  // every target (the 87-10 fix for the confirmed Solid-specific rendering gap).
3345
3832
  this.bumpWindowVer();
3833
+ // D-19 (quick 260923-rrr): a re-feed changes every unmeasured row's size, but — unlike a
3834
+ // measurement — it does not go through virtual-core's onChange, so the host's post-commit
3835
+ // pass (which restores an at-the-end view the growth moved off) never ran for it. Schedule it.
3836
+ this.scheduleRemeasure();
3346
3837
  };
3347
3838
 
3348
3839
  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
@@ -3741,6 +4232,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3741
4232
  return '';
3742
4233
  };
3743
4234
 
4235
+ // The table-core `getRowId` option (quick 260925-rew). Without it table-core keys
4236
+ // rowSelection / expanded by position, so a row inserted above a selected one moves the
4237
+ // selection onto another row. The prop is captured into a local so the null check narrows
4238
+ // inside the arrow (TS2721 on the Vue leaf otherwise), and String() because table-core
4239
+ // compares ids as strings.
4240
+ rowIdOption = () => {
4241
+ const f = this.getRowId;
4242
+ // A lazy placeholder has no consumer row to ask — key it by its index (see feedData()).
4243
+ // Prefix shared with migratePlaceholderSizeCache() (virtualization.rzts) — see its own comment.
4244
+ return f ? (originalRow: any, index: any, parent: any) => this.isLazyPlaceholder(originalRow) ? this.LAZY_PLACEHOLDER_ROW_ID_PREFIX + originalRow.index : String(f(originalRow, index, parent)) : undefined;
4245
+ };
4246
+
4247
+ // table-core `enableRowSelection`: off for selectionMode 'none'; with lazy rows a placeholder
4248
+ // (a row not loaded yet) is never selectable, so select-all takes the loaded rows only.
4249
+ rowSelectionOption = () => {
4250
+ if (this.selectionMode === 'none') return false;
4251
+ return this.lazyRowsActive() ? (row: any) => !this.rowIsLazyPlaceholder(row) : true;
4252
+ };
4253
+
3744
4254
  // Push fresh options into table-core + re-pull the row model. Extracted so BOTH the
3745
4255
  // re-feed $watch (above) and the Lit data-change $onUpdate (below) call it.
3746
4256
  reFeed = () => {
@@ -3753,10 +4263,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3753
4263
  // below (`maybeClearHistoryOnExternalSwap`), which runs on all six targets.
3754
4264
  this.table.setOptions((prev: any) => ({
3755
4265
  ...prev,
3756
- data: this.currentData(),
4266
+ data: this.feedData(),
3757
4267
  columns: this.tableColumns(),
3758
4268
  state: this.currentState(),
3759
- enableRowSelection: this.selectionMode !== 'none',
4269
+ enableRowSelection: this.rowSelectionOption(),
3760
4270
  enableMultiRowSelection: this.selectionMode === 'multiple',
3761
4271
  // Re-pass the server-side page-count sources (#2) so a RUNTIME rowCount/pageCount change
3762
4272
  // takes effect: setOptions REPLACES via `...prev`, which holds the value captured at
@@ -3769,7 +4279,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3769
4279
  // onExpandedChange callback must re-capture fresh currentState each cycle, F6).
3770
4280
  getExpandedRowModel: getExpandedRowModel(),
3771
4281
  getSubRows: (this.getSubRows || undefined) as any,
3772
- getRowCanExpand: this.expandable === true && this.getSubRows == null ? () => true : undefined,
4282
+ // Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
4283
+ // other row-model fns.
4284
+ getRowId: this.rowIdOption(),
4285
+ getRowCanExpand: this.expandable === true && this.getSubRows == null ? (row: any) => !this.rowIsLazyPlaceholder(row) : undefined,
3773
4286
  onExpandedChange: this.onExpandedChangeCb,
3774
4287
  // Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
3775
4288
  // POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
@@ -4109,9 +4622,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4109
4622
  // read header.getSize() — the group-aware size (see headerWidth's own comment). pinStyle still
4110
4623
  // keys off the plain column id (pinning is a leaf-column-only concern; a grouped header never
4111
4624
  // pins as a unit in this codebase's model).
4112
- thStyle = (header: any) => {
4625
+ // A-03: `widthPx` is the in-window leaf-width sum windowedHeadersFor() computed alongside the
4626
+ // clamped colspan. When it is present it WINS over headerWidth(), which reports
4627
+ // Header.getSize() — every descendant leaf, in-window or not — and so over-declares a straddling
4628
+ // group by exactly the leaves the colspan already excluded. Null on the unwindowed path and from
4629
+ // the non-windowed header body, which both keep headerWidth() verbatim.
4630
+ // `widthPx` carries an EXPLICIT `= null` default for the same reason setRangeFocus's anchor
4631
+ // params do: the emitted signature types a declared param as required, and the non-windowed
4632
+ // header body calls `thStyle(header)` with one argument — TS2554 on the strict leaf typecheck.
4633
+ thStyle = (header: any, widthPx = null) => {
4113
4634
  let s = '';
4114
- const w = this.headerWidth(header);
4635
+ const w = widthPx != null && widthPx > 0 ? widthPx + 'px' : this.headerWidth(header);
4115
4636
  if (w) s += 'width:' + w + ';';
4116
4637
  s += this.pinStyle(header && header.column ? header.column.id : null, 2);
4117
4638
  return s;
@@ -4497,7 +5018,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4497
5018
  if (this.selectAllBox) this.selectAllBox.indeterminate = this.isSomeRowsSelected() && !this.isAllRowsSelected();
4498
5019
  };
4499
5020
 
4500
- // The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
4501
5021
  // Imperative handle (consumer-callable). Each verb is a PRE-DECLARED top-level
4502
5022
  // `const` (the canonical $expose contract — `$expose({ name })` references a
4503
5023
  // binding ALREADY in scope; an INLINE-defined verb `$expose({ name: () => {} })`
@@ -4618,6 +5138,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4618
5138
  // headerColIndexOf: a header cell's position in its header group's leaf headers.
4619
5139
  headerColIndexOf = (hg: any, header: any) => (hg && hg.headers ? hg.headers : []).indexOf(header);
4620
5140
 
5141
+ // E-05 — a header's ABSOLUTE LEAF-column start index, which is NOT the same as its position
5142
+ // in the group's header array: a grouped header occupies ONE array slot but spans colSpan leaf
5143
+ // columns, so positions and leaf indices diverge the moment a header group exists. Walk the
5144
+ // group accumulating colSpan to the target. Window-independent by construction (a header's
5145
+ // leaf start is a full-model property), so both the windowed grid header and the plain
5146
+ // non-grid header can share it.
5147
+ headerLeafStart = (hg: any, header: any) => {
5148
+ const list = hg && hg.headers ? hg.headers : [];
5149
+ let leaf = 0;
5150
+ for (let i = 0; i < list.length; i++) {
5151
+ if (list[i] === header) return leaf;
5152
+ const h = list[i];
5153
+ leaf = leaf + (h && h.colSpan > 1 ? h.colSpan : 1);
5154
+ }
5155
+ return -1;
5156
+ };
5157
+
4621
5158
  // ── C1 (phase 63 wave-6) absolute-index bridge ─────────────────────────────────────────
4622
5159
  // The PUBLIC active-cell rowIndex (focusCell/getActiveCell/activecell-change) is the ABSOLUTE
4623
5160
  // display-order position in getPrePaginationRowModel().rows (filter+sort+expand applied, BEFORE
@@ -4703,7 +5240,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4703
5240
  // post-mount root. Uniform on all six, shadow-safe (the query runs from inside the
4704
5241
  // component's own scope). rowKey is the literal '__header' or a String(integer index) and
4705
5242
  // colIndex is an integer — NO consumer string is interpolated into the selector (T-49-01).
4706
- resolveCellEl = (rowKey: any, colIndex: any, level = null) => {
5243
+ // `level` is ANNOTATED, not just defaulted: an untyped `level = null` infers the param type as
5244
+ // `null`, so every header call site passing a real level (`lvl`, activeHeaderLevel) was a
5245
+ // TS2345 under the strict leaf typecheck (quick 260922-mkb — B-03 added one more such call).
5246
+ resolveCellEl = (rowKey: any, colIndex: any, level: number | null = null) => {
4707
5247
  if (!this.gridRoot) return null;
4708
5248
  // B12: a grouped multi-level header has MULTIPLE cells sharing data-row="__header" at the
4709
5249
  // same data-col-index across levels (parent vs leaf). Disambiguate header lookups by the
@@ -4763,10 +5303,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4763
5303
  // both are out, and the ONE existing poll below is reused unchanged — it already resolves the
4764
5304
  // target cell by BOTH (row, col) via resolveCellEl(String(r), c), so a cell that becomes
4765
5305
  // reachable via the column axis needs no new poll logic (per the plan's own key_links note).
5306
+ // B-03: the COLUMN axis applies to HEADER cells too. This guard used to be `!header &&
5307
+ // (rowOut || colOut)`, so a header-active move to an off-window column never scrolled it in:
5308
+ // `resolveCellEl` returned null, focus froze on the old `<th>` while the state and the ring
5309
+ // ran away to a column with no DOM node, and `cellTabindex` then reported 0 for an unrendered
5310
+ // cell — leaving the grid with ZERO `tabindex="0"` cells and no way back in with Tab. Header
5311
+ // rows genuinely have no ROW-axis window (headers are not row-virtualized), so the row half
5312
+ // stays body-only; only the column half widens.
4766
5313
  const rowOut = this.rowsWindowed() && this.virtualizer && this.rowIsOutsideWindow(r);
4767
5314
  const colOut = this.colsWindowed() && this.colVirtualizer && this.colIsOutsideWindow(c);
4768
- if (!header && (rowOut || colOut)) {
4769
- if (rowOut) this.virtualizer.scrollToIndex(r, {
5315
+ const rowScroll = !header && rowOut;
5316
+ if (rowScroll || colOut) {
5317
+ if (rowScroll) this.virtualizer.scrollToIndex(r, {
4770
5318
  align: 'center'
4771
5319
  });
4772
5320
  if (colOut) this.colVirtualizer.scrollToIndex(c, {
@@ -4790,7 +5338,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4790
5338
  // A newer focus intent superseded this poll — abort WITHOUT focusing (the user has since
4791
5339
  // navigated / clicked elsewhere; re-focusing this off-window target would yank focus back).
4792
5340
  if (this.focusIntentEpoch !== myEpoch) return;
4793
- const el = this.resolveCellEl(String(r), c);
5341
+ // B-03: resolve with the SAME key the synchronous path below uses, so the poll can
5342
+ // find a header cell. `String(r)` alone addresses body rows only.
5343
+ const el = this.resolveCellEl(header ? '__header' : String(r), c, header ? lvl : null);
4794
5344
  if (el) {
4795
5345
  el.focus();
4796
5346
  return;
@@ -4822,8 +5372,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4822
5372
  // HTMLElement ARIA-reflected property (`Element.ariaRowCount: string`), so a same-named method
4823
5373
  // becomes a class field that shadows it on Lit → TS2416 cascades to EVERY @property decorator
4824
5374
  // (the `valueOf`/`nodeType` inherited-DOM-member collision class, authoring playbook §6).
5375
+ // A-04: under `manual` the filtered row model is NOT the total — table-core holds only the
5376
+ // page the consumer handed it, so getFilteredRowModel().rows.length is the PAGE SIZE. The
5377
+ // server-side total lives in the props the consumer passes for exactly this purpose
5378
+ // (`rowCount`, else `pageCount`), which is also table-core's own precedence for getPageCount()
5379
+ // (explicit pageCount wins, else ⌈rowCount / pageSize⌉). Taking them here keeps aria-rowcount
5380
+ // consistent with bodyAriaRowIndex, which already offsets by pageIndex * pageSize and would
5381
+ // otherwise hand a screen reader an index LARGER than the advertised count on any page past
5382
+ // the first. `pageCount * pageSize` is an upper bound (the last page is usually partial) —
5383
+ // an over-count is the safe direction: an aria-rowindex can never exceed it.
5384
+ // With `manual` and NEITHER count set, table-core pins getCanNextPage() to false, so pageIndex
5385
+ // stays 0, the offset is 0 and page-slice-as-total is self-consistent; we leave it alone.
4825
5386
  totalRowCount = () => {
4826
5387
  if (!this.table) return (this._rows.value || []).length;
5388
+ if (this.manual === true) {
5389
+ if (this.rowCount != null) return this.rowCount;
5390
+ if (this.pageCount != null) return this.pageCount * this.pageSize();
5391
+ }
4827
5392
  const fm = this.table.getFilteredRowModel();
4828
5393
  return fm && fm.rows ? fm.rows.length : (this._rows.value || []).length;
4829
5394
  };
@@ -4857,6 +5422,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4857
5422
  // `headerRowCount() + wr.vi.index + 1` inline (wr.vi.index is already the absolute full-model index).
4858
5423
  bodyAriaRowIndex = (row: any) => this.headerRowCount() + this.rowIndexOf(row) + this.ariaPageOffset() + 1;
4859
5424
 
5425
+ // ── E-05: the COLUMN-axis twin of gridAriaRowCount / bodyAriaRowIndex ────────────────
5426
+ // WAI-ARIA: when aria-colcount is set, every cell must carry an aria-colindex and the count
5427
+ // must equal the largest index any cell declares. The row axis has had this since #13; the
5428
+ // column axis had NOTHING repo-wide (0 occurrences of aria-colcount/aria-colindex), which
5429
+ // matters most under column windowing — the rendered <td>s are a WINDOW, so without an
5430
+ // explicit index a screen reader counts the window and announces "column 3 of 6" while the
5431
+ // user sits in absolute column 41.
5432
+ //
5433
+ // The index space is `visibleCellsFor(row)` / the header group's leaf headers — the same one
5434
+ // `colIndexOf` and `data-col-index` already use, INCLUDING the auto-injected select/expander
5435
+ // chrome columns (they are real columns in that list). `visibleColCount()` counts the same
5436
+ // list, so count and indices are mutually consistent by construction.
5437
+ //
5438
+ // NB named gridAriaColCount, NOT ariaColCount: `Element.ariaColCount` / `.ariaColIndex` are
5439
+ // inherited ARIA-reflected properties, so a same-named method becomes a Lit class field that
5440
+ // shadows them → TS2416 cascading to every @property decorator. Exactly the rename discipline
5441
+ // totalRowCount / gridAriaRowCount / bodyAriaRowIndex already document above.
5442
+ gridAriaColCount = () => this.visibleColCount();
5443
+
4860
5444
  // Column count = the visible cell list length (uniform header+body in a flat grid). Reads
4861
5445
  // $data.rows (reactive) so it is fine-grained-correct on Solid/Lit; falls back to the
4862
5446
  // header leaf count when there are no body rows.
@@ -5117,6 +5701,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5117
5701
  // Enter/F2 → enter interaction mode: focus the active cell's FIRST interactive control
5118
5702
  // (D-07 — uniform for header sort buttons and body controls; Enter does NOT sort directly).
5119
5703
  // No-op (stay in navigation mode) if the cell has no focusable control.
5704
+ // True when the active cell holds its own focusable controls (a link, a button, a drop-in
5705
+ // widget). Enter then ENTERS them (enterControl) rather than activating the row (row-activate).
5706
+ activeCellHasControls = () => focusables(this.currentCellEl()).length > 0;
5707
+
5120
5708
  enterControl = () => {
5121
5709
  const cellEl = this.currentCellEl();
5122
5710
  const list = focusables(cellEl);
@@ -5209,7 +5797,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5209
5797
  // nextRow/nextCol locals and FALLS THROUGH to the shared focus seam (like Ctrl+Home/End). ──
5210
5798
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && !this._activeIsHeader.value && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
5211
5799
  e.preventDefault();
5212
- if (key === 'ArrowUp') this.extendRange(-this._activeRow.value, 0);else if (key === 'ArrowDown') this.extendRange(this.bodyRowCount() - 1 - this._activeRow.value, 0);else if (key === 'ArrowLeft') this.extendRange(0, -this._activeColIndex.value);else this.extendRange(0, this.visibleColCount() - 1 - this._activeColIndex.value);
5800
+ // B-04: measured from the range's FOCUS corner, which is what extendRange moves — not
5801
+ // from the active cell, which a drag-select or Ctrl+A leaves somewhere else entirely.
5802
+ if (key === 'ArrowUp') this.extendRange(-this.rangeFocusRow(), 0);else if (key === 'ArrowDown') this.extendRange(this.bodyRowCount() - 1 - this.rangeFocusRow(), 0);else if (key === 'ArrowLeft') this.extendRange(0, -this.rangeFocusCol());else this.extendRange(0, this.visibleColCount() - 1 - this.rangeFocusCol());
5213
5803
  return;
5214
5804
  } else if ((e.ctrlKey || e.metaKey) && !this._activeIsHeader.value && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
5215
5805
  e.preventDefault();
@@ -5307,11 +5897,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5307
5897
  // gated by clipboardActiveAllowed() (== !activeIsHeader) so a header-active Ctrl+C/Ctrl+V
5308
5898
  // falls through to NATIVE behavior — never preventDefault'd, never a silent body mutation
5309
5899
  // (copyRange/pasteRange also self-guard; the verb guard is what plan 63-09's Cut reuses). ──
5310
- else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed()) {
5900
+ // B-07: `clipboardWriteAvailable()` / `clipboardReadAvailable()` are part of the BRANCH
5901
+ // CONDITION, not a guard inside the verb. preventDefault ran first and copyRange/pasteRange
5902
+ // then returned early when `navigator.clipboard` was absent — an insecure `http://` origin, a
5903
+ // permissions policy, an older embedded webview — so the browser's own copy/paste was
5904
+ // suppressed and nothing replaced it: Ctrl+C did nothing whatsoever. Falling through leaves
5905
+ // the native default in charge, which is the only correct behaviour when we cannot act.
5906
+ else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed() && this.clipboardWriteAvailable()) {
5311
5907
  e.preventDefault();
5312
5908
  this.copyRange();
5313
5909
  return;
5314
- } else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed()) {
5910
+ } else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed() && this.clipboardReadAvailable()) {
5315
5911
  e.preventDefault();
5316
5912
  this.pasteRange();
5317
5913
  return;
@@ -5321,7 +5917,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5321
5917
  // Ctrl+C/Ctrl+V (clipboardActiveAllowed) so a header-active Ctrl+X falls through to NATIVE cut
5322
5918
  // and never silently clears a body cell (cutRange also self-guards). Placed beside the C/V
5323
5919
  // shortcuts, BEFORE the printable-key edit-entry branch (which excludes ctrl/meta). ──
5324
- else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed()) {
5920
+ // B-07 applies to Cut too, and gating it on WRITE availability is deliberate: a cut that
5921
+ // cannot copy is data destruction with nothing on the clipboard to paste back.
5922
+ else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed() && this.clipboardWriteAvailable()) {
5325
5923
  e.preventDefault();
5326
5924
  this.cutRange();
5327
5925
  return;
@@ -5373,6 +5971,44 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5373
5971
  if (!this._activeIsHeader.value) this.selectAllBody();
5374
5972
  return;
5375
5973
  }
5974
+ // ── B-15: the APG grid pattern's row/column selection keys, and Escape in NAVIGATION mode.
5975
+ // All three were simply absent. They are placed HERE, before every `key === ' '` branch
5976
+ // below, because the boolean in-place toggle matches a bare `' '` without excluding
5977
+ // modifiers — so a Shift+Space on a checkbox cell would have toggled the checkbox instead
5978
+ // of selecting the row, and Ctrl+Space likewise.
5979
+ //
5980
+ // Shift+Space selects the ROW (table-core's own rowSelection slice, through the row's
5981
+ // toggleSelected — the same funnel the per-row checkbox uses, so the change event and the
5982
+ // two-way model behave identically). A no-op when selectionMode is 'none', which is the
5983
+ // honest behaviour: there is no row selection to make.
5984
+ //
5985
+ // Ctrl+Space selects the COLUMN as a full-height CELL RANGE rather than inventing a third
5986
+ // selection concept — the same corners Ctrl+A and shift+arrow drive, so Ctrl+C / Ctrl+X /
5987
+ // Delete / the fill handle all act on it unchanged.
5988
+ //
5989
+ // Escape in navigation mode collapses the range. In-control mode already consumed Escape at
5990
+ // the top of this handler; in navigation mode it did nothing at all, leaving a user who had
5991
+ // shift-arrowed out a rectangle with no keyboard way to drop it short of an arrow key, which
5992
+ // also MOVES. Only preventDefault'd when there is a range to clear, so a consumer's own
5993
+ // Escape handling on a table with no selection is untouched.
5994
+ else if (key === ' ' && e.shiftKey && !e.ctrlKey && !e.metaKey && !this._activeIsHeader.value) {
5995
+ e.preventDefault();
5996
+ if (this.selectionMode !== 'none') {
5997
+ const selRow = (this._rows.value || [])[this._activeRow.value];
5998
+ if (selRow && selRow.toggleSelected) selRow.toggleSelected(!this.rowIsSelected(selRow));
5999
+ }
6000
+ return;
6001
+ } else if (key === ' ' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !this._activeIsHeader.value) {
6002
+ e.preventDefault();
6003
+ this.selectActiveColumn();
6004
+ return;
6005
+ } else if (key === 'Escape') {
6006
+ if (this.rangeActive || this._rangeAnchor.value != null) {
6007
+ e.preventDefault();
6008
+ this.clearRange();
6009
+ }
6010
+ return;
6011
+ }
5376
6012
  // ── Full-row edit entry (phase 51 req-6 / D-06) — Shift+F2 on an editable active cell puts
5377
6013
  // EVERY editable cell in the active row into edit at once. Tested BEFORE the plain F2 branch
5378
6014
  // (a Shift+F2 must NOT fall through to single-cell F2). Shift+F2 was chosen for the lowest
@@ -5452,6 +6088,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5452
6088
  // rAF poll must not steal focus back after the user has already ArrowDown'd to another row.
5453
6089
  this.recoverGridFocus(String(grpRow), grpCol, null, true);
5454
6090
  return;
6091
+ }
6092
+ // ── row-activate (quick 260925-dtl): Enter on a NON-editable body cell with no controls of
6093
+ // its own opens the row (a list-style "open this item"). Reached only after every edit-entry
6094
+ // and group-toggle branch above has declined; a cell WITH controls keeps entering them.
6095
+ else if (key === 'Enter' && !this._activeIsHeader.value && !this.activeCellHasControls()) {
6096
+ e.preventDefault();
6097
+ this.activateRowAt(this._activeRow.value, 'keyboard');
6098
+ return;
5455
6099
  } else if (key === 'Enter' || key === 'F2') {
5456
6100
  e.preventDefault();
5457
6101
  this.enterControl();
@@ -5550,11 +6194,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5550
6194
  // focusin that follows a Shift+Click (rangeClickPending): @mousedown already set the range
5551
6195
  // BEFORE this focusin fires, and a focusin carries no reliable shiftKey, so the @mousedown
5552
6196
  // path owns the shift case and flags it here so the collapse is skipped.
6197
+ // B-11: `rangeClickPending` carries the CELL the Shift+Click landed on, not a bare boolean.
6198
+ // A boolean leaks: Shift+Click on the cell that ALREADY holds focus fires no focusin at all,
6199
+ // so nothing consumed the flag, and the next ordinary navigation click (or Tab) hit this
6200
+ // branch instead and skipped its collapse — leaving a stale rectangle painted while the
6201
+ // active cell moved away. Matching on the coordinates makes a leak self-healing: a focusin on
6202
+ // any OTHER cell falls through to clearRange() and clears the flag on the way past.
5553
6203
  if (this.rangeTransition) {
5554
6204
  this.rangeTransition = false;
5555
- } else if (this.rangeClickPending) {
5556
- this.rangeClickPending = false;
6205
+ } else if (this.rangeClickPending && this.rangeClickPending.isHeader === isHeader && this.rangeClickPending.r === movedRow && this.rangeClickPending.c === col) {
6206
+ this.rangeClickPending = null;
5557
6207
  } else {
6208
+ this.rangeClickPending = null;
5558
6209
  this.clearRange();
5559
6210
  }
5560
6211
  // The cell box (not an inner control) receiving focus = navigation mode.
@@ -5592,13 +6243,26 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5592
6243
  this._activeIsHeader.value = false;
5593
6244
  this._activeRow.value = row;
5594
6245
  this._activeColIndex.value = col;
5595
- this.rangeClickPending = true;
6246
+ // B-11: arm the skip for THIS cell only (see syncActiveFromEvent). Body cell → isHeader
6247
+ // false; the header path never reaches here (rowAttr === '__header' returned above).
6248
+ this.rangeClickPending = {
6249
+ isHeader: false,
6250
+ r: row,
6251
+ c: col
6252
+ };
5596
6253
  return;
5597
6254
  }
5598
- // §6 plain mousedown → begin a document-level drag-select anchored at this cell. The mousedown's
5599
- // native focusin commits the ACTIVE cell to (row,col); beginRangeDrag's first cross-cell
5600
- // pointermove paints the range via setRangeFocus (anchored at the active cell). A mousedown with
5601
- // no move collapses to a single active cell (no range).
6255
+ // B-16: a mousedown INSIDE the cell that is currently in edit belongs to the editor — it is
6256
+ // the user placing a caret or drag-selecting TEXT. Starting a cell drag-select from it paints
6257
+ // a grid range over the very cell being typed into, and (with `undoable`) a drag that leaves
6258
+ // the cell then lets Delete/Ctrl+X act on a rectangle the user was never selecting. The
6259
+ // editor owns the gesture; return before any document listener is attached.
6260
+ if (this.isEditing(row, col)) return;
6261
+ // §6 plain mousedown → begin a document-level drag-select anchored at this cell. The anchor is
6262
+ // passed EXPLICITLY into setRangeFocus by beginRangeDrag (B-01) rather than seeded from the
6263
+ // active cell, which is still the PRE-mousedown one at this point (native focus is a default
6264
+ // action that runs after this handler). A mousedown with no move collapses to a single active
6265
+ // cell (no range).
5602
6266
  this.beginRangeDrag(row, col);
5603
6267
  };
5604
6268
 
@@ -5655,7 +6319,57 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5655
6319
  // editor). A plain click on an EDITABLE body cell opens its editor via the SAME beginEdit funnel;
5656
6320
  // shift+click (range extend) and non-editable cells are unaffected. Same closest/parse/header-skip
5657
6321
  // /finite guards as onGridDblClick. isGrid()-gated so 'table' mode never runs it.
6322
+ // ── row-activate (quick 260925-dtl, oinbox dogfooding) ─────────────────────────────────────
6323
+ // `row-activate { row, index, trigger }`: `row` = the original data object, `index` = its
6324
+ // position in the rendered model ($data.rows — page-relative, or the full model when virtual).
6325
+ // Group-header rows never activate; detail rows carry no data-row so a click there never
6326
+ // resolves a row.
6327
+ activateRowAt = (index: any, trigger: any) => {
6328
+ const row = (this._rows.value || [])[index];
6329
+ if (!row || this.rowIsGrouped(row) || this.rowIsLazyPlaceholder(row)) return;
6330
+ this.dispatchEvent(new CustomEvent("row-activate", {
6331
+ detail: {
6332
+ row: row.original,
6333
+ index,
6334
+ trigger
6335
+ },
6336
+ bubbles: true,
6337
+ composed: true
6338
+ }));
6339
+ };
6340
+
6341
+ // Controls a click must not activate THROUGH — the selection checkbox, the expander, links,
6342
+ // drop-in widgets and an open editor. Matched only INSIDE the clicked cell, so a host page that
6343
+ // happens to wrap the table in one of these does not silence every row.
6344
+ ROW_ACTIVATE_CONTROLS = 'input, button, a[href], select, textarea, label, summary, [contenteditable=""], [contenteditable="true"], [role="button"], [role="checkbox"], [role="link"], [data-editing-cell]';
6345
+
6346
+ // Both interaction modes: a plain click on a body data cell, outside any control. Not a
6347
+ // modified click (shift/ctrl/meta are selection gestures), not the end of a drag-select, and
6348
+ // not a singleClickEdit gesture on an editable grid cell (that click opens the editor instead).
6349
+ maybeActivateRowFromClick = (e: any) => {
6350
+ if (!e || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
6351
+ if (this.rangeDragMoved) return;
6352
+ const tgt = e.target;
6353
+ if (!tgt || !tgt.closest) return;
6354
+ const cellEl = tgt.closest('[data-row]');
6355
+ if (!cellEl) return;
6356
+ const control = tgt.closest(this.ROW_ACTIVATE_CONTROLS);
6357
+ if (control && cellEl.contains(control)) return;
6358
+ const rowAttr = cellEl.getAttribute('data-row');
6359
+ if (rowAttr == null || rowAttr === '__header') return;
6360
+ const index = parseInt(rowAttr, 10);
6361
+ if (!Number.isFinite(index)) return;
6362
+ if (this.isGrid() && this.singleClickEdit) {
6363
+ const colAttr = cellEl.getAttribute('data-col-index');
6364
+ const col = colAttr == null ? NaN : parseInt(colAttr, 10);
6365
+ const colId = Number.isFinite(col) ? this.columnIdAt(index, col) : null;
6366
+ if (colId != null && this.columnEditable(colId)) return;
6367
+ }
6368
+ this.activateRowAt(index, 'click');
6369
+ };
6370
+
5658
6371
  onGridClick = (e: any) => {
6372
+ this.maybeActivateRowFromClick(e);
5659
6373
  if (!this.isGrid() || !e) return;
5660
6374
  if (!this.singleClickEdit) return;
5661
6375
  if (e.shiftKey) return;
@@ -5851,12 +6565,26 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5851
6565
  // UNCHANGED (span = the header's own colSpan, or 1) when columns are not windowed — the
5852
6566
  // off-path the template's `:colspan="wh.span > 1 ? wh.span : null"` binding preserves
5853
6567
  // byte-identically (a bare colspan="1" would change the emitted markup on the unwindowed path).
6568
+ //
6569
+ // A-03: the emitted entry also carries the WIDTH of the in-window leaves, because the colspan
6570
+ // clamp above is only half the job. `thStyle` declared `Header.getSize()` — the sum of ALL
6571
+ // descendant leaves — against a colspan clamped to the leaves actually rendered. Under
6572
+ // `table-layout: fixed` a first-row spanning cell's declared width is divided across the
6573
+ // columns it spans, and only the first row establishes widths, so a group straddling the window
6574
+ // edge inflated every column it still rendered: measured on "Group A" (cols 10-14 @150px,
6575
+ // getSize() 750), each rendered group column went 150 -> 187.5 -> 250 -> 375 -> 750px as the
6576
+ // group scrolled out, and scrollWidth with it, 9000 -> 9600. Identical on all five light-DOM
6577
+ // targets. This is the SAME getSize()-vs-colspan mismatch the earlier Column.getSize() ->
6578
+ // Header.getSize() fix cured for the fully-in-window case, inverted: that one was a 5x
6579
+ // COLLAPSE, this is a 5x EXPANSION for the partial case. Summing the same leaves the clamp
6580
+ // counted keeps the declared width and the colspan describing the same set by construction.
5854
6581
  windowedHeadersFor = (hg: any, hgLevel: any) => {
5855
6582
  const headers = hg && hg.headers || [];
5856
6583
  if (!this.colsWindowed()) {
5857
6584
  return headers.map((h: any) => ({
5858
6585
  header: h,
5859
- span: h && h.colSpan > 1 ? h.colSpan : 1
6586
+ span: h && h.colSpan > 1 ? h.colSpan : 1,
6587
+ width: null
5860
6588
  }));
5861
6589
  }
5862
6590
  const idx = this.windowedColIndices();
@@ -5871,10 +6599,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5871
6599
  const end = cursor + leafCount;
5872
6600
  cursor = end;
5873
6601
  let span = 0;
5874
- for (let c = start; c < end; c++) if (idxSet[c]) span = span + 1;
6602
+ let width = 0;
6603
+ for (let c = start; c < end; c++) if (idxSet[c]) {
6604
+ span = span + 1;
6605
+ width = width + this.columnSize(c);
6606
+ }
5875
6607
  if (span > 0) out.push({
5876
6608
  header: h,
5877
- span
6609
+ span,
6610
+ width
5878
6611
  });
5879
6612
  }
5880
6613
  return out;
@@ -5887,6 +6620,37 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5887
6620
  // (byte-identical).
5888
6621
  windowedColSpan = () => this.colsWindowed() ? this.windowedColIndices().length + 2 : this.visibleColCount();
5889
6622
 
6623
+ // ── A-02: a column RESIZE must invalidate the column virtualizer's measurement cache ──────
6624
+ // `setOptions` is not enough, and this is a property of virtual-core, not of how we call it.
6625
+ // `getMeasurements()` is memoized on `[getMeasurementOptions(), itemSizeCacheVersion]`, and
6626
+ // `getMeasurementOptions()` is itself memoized on count / paddingStart / scrollMargin /
6627
+ // getItemKey / enabled / lanes / laneAssignmentMode — `estimateSize` is NOT among them. The
6628
+ // column axis feeds its widths ENTIRELY through `estimateSize: (i) => columnSize(i)` (it never
6629
+ // calls `measureElement`), so after a resize the memo returns the array built from the OLD
6630
+ // widths: measured on a 60x150px grid, dragging one column +1200px left `scrollWidth`
6631
+ // oscillating 9000 <-> 10200 as that column entered and left the window, and at scrollLeft 6000
6632
+ // the window started at index 37 where fresh geometry predicts 29. `measure()` is the only API
6633
+ // that bumps `itemSizeCacheVersion`, and it appeared ZERO times in this package or in
6634
+ // headless-core. Same defect for `resetColumnSizing()` and any programmatic `columnSizing` write.
6635
+ //
6636
+ // Guarded on a SIGNATURE rather than called unconditionally. `columnResizeMode` is 'onChange',
6637
+ // so `writeColumnSizing` fires on every pointermove of a drag; `measure()` clears the size cache
6638
+ // and calls `notify(false)`, which bumps `$data.windowVer` and therefore re-renders. Hashing the
6639
+ // widths (order-sensitive, so a pure A<->B swap that preserves the sum still trips it) makes the
6640
+ // steady state a ~N-integer-op no-op and guarantees the notify cannot feed itself: the second
6641
+ // pass sees an unchanged signature and returns.
6642
+ lastColSizeSig = 0;
6643
+
6644
+ remeasureColumnSizes = () => {
6645
+ if (!this.colsWindowed() || !this.colVirtualizer || !this.colVirtualizer.measure) return;
6646
+ const n = this.columnCount();
6647
+ let sig = n;
6648
+ for (let i = 0; i < n; i++) sig = Math.imul(sig, 31) + this.columnSize(i) | 0;
6649
+ if (sig === this.lastColSizeSig) return;
6650
+ this.lastColSizeSig = sig;
6651
+ this.colVirtualizer.measure();
6652
+ };
6653
+
5890
6654
  // remeasureColumnWindow() (Phase 87 87-04): push a fresh column count/size into the SECOND
5891
6655
  // (horizontal) virtualizer on every row-model refresh — mirrors the row axis's
5892
6656
  // virtualizer.setOptions(virtualizerOptions()) + _willUpdate() re-feed in refreshRowModel, so a
@@ -5900,6 +6664,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5900
6664
  remeasureColumnWindow = () => {
5901
6665
  if (!this.colsWindowed() || !this.colVirtualizer) return;
5902
6666
  this.colVirtualizer.setOptions(this.columnVirtualizerOptions());
6667
+ this.remeasureColumnSizes();
5903
6668
  this.colVirtualizer._willUpdate();
5904
6669
  };
5905
6670
 
@@ -5936,8 +6701,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5936
6701
 
5937
6702
  // rangeClickPending: set by onGridMouseDown on a Shift+Click (the range is set off the
5938
6703
  // pointer event's shiftKey BEFORE the cell's focusin fires); the follow-up focusin reads it
5939
- // to SKIP the range-collapse (a focusin carries no reliable shiftKey). Reset on consumption.
5940
- rangeClickPending = false;
6704
+ // to SKIP the range-collapse (a focusin carries no reliable shiftKey).
6705
+ // B-11: it holds the CELL `{ isHeader, r, c }` the Shift+Click landed on, not a boolean, and
6706
+ // is null when idle. Shift+Click on the already-focused cell fires no focusin, so a boolean
6707
+ // was left armed and the NEXT ordinary click/Tab consumed it and skipped its own collapse,
6708
+ // stranding a painted rectangle. Coordinate-matched, that leak is self-healing: a focusin on
6709
+ // any other cell clears it and collapses normally. Cleared on BOTH branches of the consume.
6710
+ rangeClickPending: any = null;
5941
6711
 
5942
6712
  // B19: a SYNCHRONOUS mirror of "a range currently exists" — extendRange/setRangeFocus set it
5943
6713
  // true, clearRange/clampRange-to-empty set it false. clearRange is invoked TWICE in one plain-
@@ -6008,7 +6778,27 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6008
6778
  // pre-write value (ROZ138). extendRange/setRangeFocus thread the just-computed locals through
6009
6779
  // here so the emitted payload matches the write. The single call site keeps the count
6010
6780
  // predictable (React multi-emit dedup, D-07). One-way notification.
6781
+ // B-10 — announce the selection alongside the emit. This is the SINGLE site every range
6782
+ // mutation funnels through (shift+arrow via extendRange, shift+click and drag-select and
6783
+ // fill-drag via setRangeFocus, Ctrl+A via selectAllBody), so one announce here covers all of
6784
+ // them; guarding the callers individually would be five chances to miss one.
6785
+ //
6786
+ // Writes $data.rangeAnnounce directly rather than importing clipboardFill's announce(): that
6787
+ // helper drives pasteAnnounce, and a range move must not overwrite a paste/validation message
6788
+ // the user has not heard yet (hence the separate key + region). `polite` + `aria-atomic` means
6789
+ // a fast drag replaces the pending text instead of queueing every intermediate rectangle.
6790
+ rangeSummary = (anchor: any, focus: any) => {
6791
+ if (!anchor || !focus) return '';
6792
+ const rows = Math.abs(focus.rowIndex - anchor.rowIndex) + 1;
6793
+ const cols = Math.abs(focus.colIndex - anchor.colIndex) + 1;
6794
+ if (rows === 1 && cols === 1) return '1 cell selected';
6795
+ const rowPart = rows + (rows === 1 ? ' row' : ' rows');
6796
+ const colPart = cols + (cols === 1 ? ' column' : ' columns');
6797
+ return rowPart + ' by ' + colPart + ' selected, ' + rows * cols + ' cells';
6798
+ };
6799
+
6011
6800
  emitRangeChange = (anchor: any, focus: any) => {
6801
+ this._rangeAnnounce.value = this.rangeSummary(anchor, focus);
6012
6802
  this.dispatchEvent(new CustomEvent("range-change", {
6013
6803
  detail: {
6014
6804
  anchor,
@@ -6068,14 +6858,39 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6068
6858
  }
6069
6859
  };
6070
6860
 
6071
- // setRangeFocus(rIdx, cIdx): set the moving corner to an explicit cell (Shift+Click),
6072
- // seeding the anchor from the active cell when no range exists yet. Clamped to bounds.
6073
- // Emits range-change from this single site.
6074
- setRangeFocus = (rIdx: any, cIdx: any) => {
6861
+ // setRangeFocus(rIdx, cIdx, anchorR, anchorC): set the moving corner to an explicit cell
6862
+ // (Shift+Click, or a drag-select's pointermove), seeding the anchor when no range exists yet.
6863
+ // Clamped to bounds. Emits range-change from this single site.
6864
+ //
6865
+ // B-01: the anchor seed is EXPLICIT when the caller knows it. Seeding it from
6866
+ // `$data.activeRow`/`$data.activeColIndex` is only correct if that state is live at the moment
6867
+ // the seed happens — and inside a drag-select it is not on React, where the document
6868
+ // pointermove closure captures the render-scoped consts from the render that ran BEFORE the
6869
+ // mousedown. Native focus moves as mousedown's DEFAULT ACTION, after handlers return, so at
6870
+ // `beginRangeDrag` time the active cell is still the PRE-mousedown one and React can never
6871
+ // show that closure a newer value. Dragging (5,2)→(7,3) on a fresh grid anchored at (0,0) and
6872
+ // selected the whole rectangle from there — which Ctrl+X and Delete then destroy.
6873
+ // The other five targets read `$data.activeRow` LIVE inside `move` (by then focusin has synced
6874
+ // it), so they were accidentally correct; taking the anchor from the caller removes the read
6875
+ // entirely and makes all six correct for the same reason. Falls back to the active cell when no
6876
+ // explicit anchor is passed — Shift+Click, where "the anchor is where you already were" IS the
6877
+ // active cell and the read happens synchronously in the same handler.
6878
+ // The two anchor params carry EXPLICIT `= null` defaults, and that is load-bearing, not style:
6879
+ // the emitted per-target signature types every declared param as REQUIRED, so the Shift+Click
6880
+ // call site's two-argument `setRangeFocus(row, col)` fails the strict leaf typecheck with
6881
+ // TS2554 "Expected 4 arguments, but got 2" on react and solid. Same rule the `focusActiveCell`
6882
+ // comment in gridKeydownHandlers records for its own optional params.
6883
+ setRangeFocus = (rIdx: any, cIdx: any, anchorR = null, anchorC = null) => {
6075
6884
  const maxRow = this.bodyRowCount() - 1;
6076
6885
  const maxCol = this.visibleColCount() - 1;
6077
6886
  if (maxRow < 0 || maxCol < 0) return;
6078
6887
  let anchor = this._rangeAnchor.value;
6888
+ if (!anchor && anchorR != null && anchorC != null) {
6889
+ anchor = {
6890
+ rowIndex: clamp(Math.trunc(Number(anchorR)) || 0, 0, maxRow),
6891
+ colIndex: clamp(Math.trunc(Number(anchorC)) || 0, 0, maxCol)
6892
+ };
6893
+ }
6079
6894
  if (!anchor) anchor = {
6080
6895
  rowIndex: this._activeRow.value,
6081
6896
  colIndex: this._activeColIndex.value
@@ -6116,6 +6931,54 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6116
6931
  this.emitRangeChange(anchor, focus);
6117
6932
  };
6118
6933
 
6934
+ // ── B-04: where does a range EXTENSION measure its delta from? ──────────────────────────
6935
+ // `extendRange(dRow, dCol)` applies its delta to the range's FOCUS corner, not to the active
6936
+ // cell. For shift+arrow the two always agree (extendRange writes the active cell to the new
6937
+ // focus on every step), but two paths deliberately move the focus corner WITHOUT syncing the
6938
+ // active cell: a drag-select (`setRangeFocus`, which leaves the active cell on the mousedown
6939
+ // anchor) and Ctrl+A (`selectAllBody`, which sets both corners outright). After either,
6940
+ // Ctrl/Cmd+Shift+Arrow computed "distance from the active cell to the edge" and then added it
6941
+ // to a DIFFERENT cell. Drag (5,2)->(7,3) then Ctrl+Shift+ArrowUp extended to row 2 instead of
6942
+ // row 0; Ctrl+Shift+ArrowLeft to column 1 instead of column 0 — an extend-to-edge that stops
6943
+ // short of the edge by exactly the gap between the two.
6944
+ //
6945
+ // These report the corner extendRange will actually move, so the delta and its target are the
6946
+ // same cell by construction. No range yet -> the active cell IS the focus corner.
6947
+ rangeFocusRow = () => {
6948
+ const f = this._rangeFocus.value;
6949
+ return f && f.rowIndex != null ? f.rowIndex : this._activeRow.value;
6950
+ };
6951
+
6952
+ rangeFocusCol = () => {
6953
+ const f = this._rangeFocus.value;
6954
+ return f && f.colIndex != null ? f.colIndex : this._activeColIndex.value;
6955
+ };
6956
+
6957
+ // B-15: the APG grid pattern's Ctrl+Space — select the COLUMN containing the focused cell.
6958
+ // Expressed as a full-height rectangle over the SAME range corners shift+arrow and Ctrl+A
6959
+ // drive, so Ctrl+C / Ctrl+X / Delete / the fill handle all act on it with no new machinery
6960
+ // and no second selection concept for a consumer to reason about. Emits through the single
6961
+ // emitRangeChange funnel (fresh corners, never a $data re-read). No-op on an empty grid, and
6962
+ // header-active is gated OUT by the caller, exactly as selectAllBody is.
6963
+ selectActiveColumn = () => {
6964
+ const maxRow = this.bodyRowCount() - 1;
6965
+ const maxCol = this.visibleColCount() - 1;
6966
+ if (maxRow < 0 || maxCol < 0) return;
6967
+ const c = clamp(Math.trunc(Number(this._activeColIndex.value)) || 0, 0, maxCol);
6968
+ const anchor = {
6969
+ rowIndex: 0,
6970
+ colIndex: c
6971
+ };
6972
+ const focus = {
6973
+ rowIndex: maxRow,
6974
+ colIndex: c
6975
+ };
6976
+ this._rangeAnchor.value = anchor;
6977
+ this._rangeFocus.value = focus;
6978
+ this.rangeActive = true;
6979
+ this.emitRangeChange(anchor, focus);
6980
+ };
6981
+
6119
6982
  // clearRange(): drop the rectangle (a non-shift navigation / edit-entry collapses any
6120
6983
  // range back to a single active cell). Cheap no-op when no range is set (the guard keeps a
6121
6984
  // plain navigation with no active range from emitting). B19: when a range DID exist, emit
@@ -6250,6 +7113,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6250
7113
 
6251
7114
  // copyRange(): write the current range as TSV to the clipboard (async). No-op when the
6252
7115
  // async Clipboard API is unavailable (older/insecure contexts) — a copy is best-effort.
7116
+ // B-07: is the async Clipboard API actually usable? Ctrl+C / Ctrl+X / Ctrl+V used to
7117
+ // `e.preventDefault()` FIRST and only then call a verb that self-guards on
7118
+ // `navigator.clipboard`, so on any page where the API is absent — an insecure `http://` origin,
7119
+ // a permissions policy, an older embedded webview — the browser's OWN copy/cut/paste was
7120
+ // suppressed and nothing took its place. The keystroke did nothing at all. These predicates
7121
+ // move the check into the keymap's branch CONDITION (gridKeydownHandlers.rzts), so an
7122
+ // unavailable API means the branch does not match and the native default runs. Separate
7123
+ // read/write predicates because a context can grant one and not the other.
7124
+ clipboardWriteAvailable = () => typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.writeText === 'function';
7125
+
7126
+ clipboardReadAvailable = () => typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.readText === 'function';
7127
+
6253
7128
  copyRange = () => {
6254
7129
  // B11: never copy from a header-active state (the reusable clipboard guard).
6255
7130
  if (!this.clipboardActiveAllowed()) return;
@@ -6268,15 +7143,33 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6268
7143
  // running fresh array (replaceRowValue) and record the committed cell. After the walk: ONE
6269
7144
  // writeData (the single r-model:data write), ONE cell-edit-commit per COMMITTED cell, and the
6270
7145
  // N-of-M aria-live announce. Returns { wrote, total }.
6271
- applyGridToRange = (grid: any, originRow: any, originCol: any) => {
7146
+ // B-09 — `verb` names the OPERATION in the aria-live summary. This funnel serves paste, cut,
7147
+ // range-delete AND fill, so hardcoding "pasted" made a Cut announce "3 of 3 cells pasted" and a
7148
+ // Delete announce the same — actively wrong for a screen-reader user, who has no other signal
7149
+ // that the destructive operation they invoked actually happened. Defaults to 'pasted' so the
7150
+ // paste path's wording is unchanged.
7151
+ applyGridToRange = (grid: any, originRow: any, originCol: any, verb: any) => {
7152
+ const opVerb = typeof verb === 'string' && verb !== '' ? verb : 'pasted';
6272
7153
  const maxRow = this.bodyRowCount() - 1;
6273
7154
  const maxCol = this.visibleColCount() - 1;
6274
7155
  if (maxRow < 0 || maxCol < 0) return {
6275
7156
  wrote: 0,
7157
+ changed: 0,
6276
7158
  total: 0
6277
7159
  };
7160
+ // B-08: `applied` and `committed` are DIFFERENT counts and used to be one.
7161
+ // applied — targets whose value now equals what was pasted, INCLUDING the ones that
7162
+ // already did. This is the announce's numerator: from the user's point of
7163
+ // view a cell that already held the pasted value was pasted successfully, and
7164
+ // reporting "3 of 5" for a 5-cell paste where 2 matched would be a lie.
7165
+ // committed — targets whose value actually CHANGED. This drives the write, the per-cell
7166
+ // `cell-edit-commit` emits and (through writeData) the undo step.
7167
+ // Conflating them meant an unchanged cell emitted an edit event and recorded an undo entry:
7168
+ // a 1->5 fill fired 5 commits when 4 cells already held the value, and a Delete over an
7169
+ // already-empty cell pushed an undo step that undoes nothing. Consumers persisting on
7170
+ // `cell-edit-commit` were issuing writes for data that had not moved.
6278
7171
  let total = 0;
6279
- let wrote = 0;
7172
+ let applied = 0;
6280
7173
  const committed = [];
6281
7174
  // Build the fresh data array incrementally so the whole paste is ONE writeData.
6282
7175
  let next = this.currentData();
@@ -6290,15 +7183,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6290
7183
  // it to this single funnel, so guarding the four callers individually would be four
6291
7184
  // chances to miss one.
6292
7185
  const rowGrouped = this.rowIndexIsGrouped(r);
7186
+ // Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (virtual+manual+rowCount, a row not
7187
+ // loaded yet) has no real underlying record — rowOriginalAt(r) returns the placeholder
7188
+ // sentinel ({__rdtLazyPlaceholder:true,index:i}, virtualization.rzts), which is never IN
7189
+ // currentData() (only the consumer's own array is), so sourceIndexOfRow's data.indexOf(...)
7190
+ // is always -1 and falls back to the VISIBLE index — either a no-op write with a FABRICATED
7191
+ // cell-edit-commit, or, inside a sparse hole, the hole gets clobbered with a partial row via
7192
+ // replaceRowValue. Hoisted per-ROW exactly like rowGrouped above (the SAME single placement
7193
+ // closes pasteRange/cutRange/clearActiveRange/fillRange all at once).
7194
+ const rowPlaceholder = this.rowIsLazyPlaceholder((this._rows.value || [])[r]);
6293
7195
  const cols = grid[gr] || [];
6294
7196
  for (let gc = 0; gc < cols.length; gc++) {
6295
7197
  const c = originCol + gc;
6296
7198
  if (c > maxCol) break;
6297
- // A group-row cell still counts toward `total` — it is a skipped TARGET, exactly like
6298
- // a non-editable or validator-rejected cell — so the "N of M cells pasted" / "no cells
6299
- // pasted" announce keeps its existing semantics (never silently under-reports M).
7199
+ // A group-row / placeholder-row cell still counts toward `total` — it is a skipped
7200
+ // TARGET, exactly like a non-editable or validator-rejected cell — so the "N of M cells
7201
+ // pasted" / "no cells pasted" announce keeps its existing semantics (never silently
7202
+ // under-reports M).
6300
7203
  total = total + 1;
6301
- if (rowGrouped) continue;
7204
+ if (rowGrouped || rowPlaceholder) continue;
6302
7205
  const colId = this.columnIdAt(r, c);
6303
7206
  if (colId == null || !this.columnEditable(colId)) continue;
6304
7207
  const rowObj = this.rowOriginalAt(r);
@@ -6312,6 +7215,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6312
7215
  const field = this.fieldOfColId(colId);
6313
7216
  const srcIndex = this.sourceIndexOfRow(r);
6314
7217
  const oldValue = rowObj ? rowObj[field] : null;
7218
+ applied = applied + 1;
7219
+ // B-08: the cell already holds this value — it counts as applied, but there is nothing
7220
+ // to write, nothing to emit and nothing to undo.
7221
+ if (oldValue === value) continue;
6315
7222
  next = replaceRowValue(next, srcIndex, field, value);
6316
7223
  committed.push({
6317
7224
  rowId: this.rowIdAt(r),
@@ -6319,26 +7226,41 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6319
7226
  oldValue,
6320
7227
  newValue: value
6321
7228
  });
6322
- wrote = wrote + 1;
6323
7229
  }
6324
7230
  }
6325
- if (wrote > 0) {
7231
+ if (committed.length > 0) {
6326
7232
  this.editTransition = true;
6327
7233
  this.writeData(next);
6328
7234
  this.editTransition = false;
6329
7235
  // One cell-edit-commit per COMMITTED cell (the per-cell event contract, D-03).
6330
- for (let i = 0; i < committed.length; i++) this.dispatchEvent(new CustomEvent("cell-edit-commit", {
6331
- detail: committed[i],
6332
- bubbles: true,
6333
- composed: true
6334
- }));
7236
+ // A-08: each emit is guarded INDIVIDUALLY. These listeners are consumer code, and a single
7237
+ // throwing one used to abort the loop — so on a 40-cell paste, cells 0..k notified and
7238
+ // k+1..39 silently never did, leaving a consumer persisting on this event with a partial
7239
+ // write of a model that had already been fully updated. Measured while building this: a
7240
+ // throwing VALIDATOR is NOT the reachable vehicle here — `runValidator`
7241
+ // (editCellLifecycle.rzts) already catches and reports 'Invalid value' — so the listener
7242
+ // is the first-party code that actually escapes.
7243
+ for (let i = 0; i < committed.length; i++) {
7244
+ try {
7245
+ this.dispatchEvent(new CustomEvent("cell-edit-commit", {
7246
+ detail: committed[i],
7247
+ bubbles: true,
7248
+ composed: true
7249
+ }));
7250
+ } catch (err: any) {
7251
+ 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);
7252
+ }
7253
+ }
6335
7254
  }
6336
7255
  // WR-02: announce the N-of-M summary only when at least one cell was written. When the paste
6337
7256
  // targeted real cells but every one was skipped (validation-failed / non-editable), announce a
6338
7257
  // distinct validation-failed message instead of a misleading "0 of M cells pasted".
6339
- if (wrote > 0) this.announce(wrote + ' of ' + total + ' cells pasted');else if (total > 0) this.announce('No cells pasted — ' + total + ' cells were invalid or read-only');
7258
+ if (applied > 0) this.announce(applied + ' of ' + total + ' cells ' + opVerb);else if (total > 0) this.announce('No cells ' + opVerb + ' — ' + total + ' cells were invalid or read-only');
7259
+ // `wrote` stays the ANNOUNCED count (targets satisfied), not the written one, so every
7260
+ // existing caller and assertion keeps its meaning; `changed` is the new, narrower number.
6340
7261
  return {
6341
- wrote,
7262
+ wrote: applied,
7263
+ changed: committed.length,
6342
7264
  total
6343
7265
  };
6344
7266
  };
@@ -6386,14 +7308,26 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6386
7308
  return;
6387
7309
  }
6388
7310
  if (!p || !p.then) return;
7311
+ // A-08: the two failure modes are separated. A REJECTED clipboard read (permission denied,
7312
+ // insecure context, an empty selection) is a legitimate silent no-op — that is the rejection
7313
+ // handler, the second argument. Anything the fulfilment handler throws is FIRST-PARTY: a
7314
+ // consumer's row accessor, a `data`-model setter, or a `cell-edit-commit` listener (which
7315
+ // `applyGridToRange` now also guards per-emit). A single trailing `.catch(() => {})`
7316
+ // swallowed both, so consumer code failing inside a paste produced no error, no warning and
7317
+ // nothing in the console to search for. NOT a column validator: `runValidator` already
7318
+ // catches a throwing one and reports 'Invalid value' — checked rather than assumed.
6389
7319
  p.then((text: any) => {
6390
- const grid = parseTsv(text);
6391
- if (!grid.length) return;
6392
- // C3: tile the clipboard block to fill the destination range (single→range fill,
6393
- // smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
6394
- const tiled = tileGridToBox(grid, destBox);
6395
- this.applyGridToRange(tiled, anchorRow, anchorCol);
6396
- }).catch(() => {});
7320
+ try {
7321
+ const grid = parseTsv(text);
7322
+ if (!grid.length) return;
7323
+ // C3: tile the clipboard block to fill the destination range (single→range fill,
7324
+ // smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
7325
+ const tiled = tileGridToBox(grid, destBox);
7326
+ this.applyGridToRange(tiled, anchorRow, anchorCol, 'pasted');
7327
+ } catch (err: any) {
7328
+ console.error('[rozie-data-table] paste failed: a row accessor, the data model setter or other ' + 'consumer code threw. The paste was abandoned.', err);
7329
+ }
7330
+ }, () => {/* clipboard read rejected (permission / insecure context) — silent by design */});
6397
7331
  };
6398
7332
 
6399
7333
  // cutRange(): C3 Cut — copy the current range to the clipboard (rangeToTsv — the SAME escaped
@@ -6427,7 +7361,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6427
7361
  for (let c = c0; c <= c1; c++) cols.push('');
6428
7362
  grid.push(cols);
6429
7363
  }
6430
- this.applyGridToRange(grid, r0, c0);
7364
+ this.applyGridToRange(grid, r0, c0, 'cut');
6431
7365
  };
6432
7366
 
6433
7367
  // clearActiveRange(): the §7 (260709-3qt) Delete/Backspace clear — cutRange() MINUS the clipboard
@@ -6453,7 +7387,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6453
7387
  for (let c = c0; c <= c1; c++) cols.push('');
6454
7388
  grid.push(cols);
6455
7389
  }
6456
- this.applyGridToRange(grid, r0, c0);
7390
+ this.applyGridToRange(grid, r0, c0, 'cleared');
6457
7391
  };
6458
7392
 
6459
7393
  // fillRange(sourceBox): drag-fill (D-04 — VALUE-COPY ONLY, no series detection). B7: the fill
@@ -6508,7 +7442,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6508
7442
  }
6509
7443
  grid.push(cols);
6510
7444
  }
6511
- this.applyGridToRange(grid, box.r0, box.c0);
7445
+ this.applyGridToRange(grid, box.r0, box.c0, 'filled');
6512
7446
  };
6513
7447
 
6514
7448
  // onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
@@ -6796,10 +7730,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6796
7730
  this.rangeDragging = false;
6797
7731
  };
6798
7732
 
6799
- // beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell. The
6800
- // mousedown's native focus/focusin already committed the ACTIVE cell to (anchorR, anchorC), so
6801
- // setRangeFocus (which seeds the anchor from the ACTIVE cell) spans mousedown-cell→pointer-cell —
6802
- // we NEVER write $data.rangeAnchor directly (it is React-stale, ROZ138). rangeDragMoved starts
7733
+ // beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell.
7734
+ // B-01: the anchor is handed to setRangeFocus EXPLICITLY, never left to be seeded from the
7735
+ // active cell. The mousedown's native focus is a DEFAULT ACTION that runs after this handler
7736
+ // returns, so at this point $data.activeRow/activeColIndex still hold the PRE-mousedown cell;
7737
+ // on React the pointermove closure below is frozen at that render and can never observe the
7738
+ // post-focusin value, so the whole gesture anchored at wherever the user happened to be
7739
+ // (a fresh grid: (0,0)). We still never WRITE $data.rangeAnchor directly (React-stale,
7740
+ // ROZ138) — setRangeFocus owns that write. rangeDragMoved starts
6803
7741
  // false and flips true only once the pointer reaches a DIFFERENT cell, so a mousedown-with-no-move
6804
7742
  // leaves a single active cell + no range (a normal click). lastCell dedups the many pointermove
6805
7743
  // events per cell (setRangeFocus emits range-change — only extend on a NEW cell, mirroring fillDrag's
@@ -6824,7 +7762,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6824
7762
  if (cell && (cell.r !== lastCell.r || cell.c !== lastCell.c)) {
6825
7763
  lastCell = cell;
6826
7764
  this.rangeDragMoved = true;
6827
- this.setRangeFocus(cell.r, cell.c);
7765
+ this.setRangeFocus(cell.r, cell.c, anchorR, anchorC);
6828
7766
  }
6829
7767
  };
6830
7768
  const up = () => {
@@ -6862,6 +7800,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6862
7800
  // row reaches the group-toggle branch below them instead of opening an editor.
6863
7801
  isActiveCellEditable = () => {
6864
7802
  if (this.rowIndexIsGrouped(this._activeRow.value)) return false;
7803
+ // A lazy placeholder (a row not loaded yet) has nothing to edit (quick 260925-dtl part 2).
7804
+ if (this.rowIsLazyPlaceholder((this._rows.value || [])[this._activeRow.value])) return false;
6865
7805
  const colId = this.activeCellColumnId();
6866
7806
  return colId != null && this.columnEditable(colId);
6867
7807
  };
@@ -7096,6 +8036,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7096
8036
  // calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
7097
8037
  // does not run on that path at all.
7098
8038
  if (this.rowIndexIsGrouped(rowIndex)) return;
8039
+ if (this.rowIsLazyPlaceholder((this._rows.value || [])[rowIndex])) return;
7099
8040
  const colId = this.columnIdAt(rowIndex, colIndex);
7100
8041
  if (colId == null || !this.columnEditable(colId)) return;
7101
8042
  // A new edit session starts — reset the sync idempotency latch so THIS session's eventual
@@ -7106,6 +8047,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7106
8047
  // editor clears any row-edit state so isEditing never resolves both modes for one cell.
7107
8048
  this._editingRowIndex.value = null;
7108
8049
  this._rowDraft.value = {};
8050
+ this.rowDraftSync = {};
7109
8051
  this._editingRow.value = rowIndex;
7110
8052
  this._editingCol.value = colIndex;
7111
8053
  this._draftValue.value = seed != null ? seed : this.cellValueAt(rowIndex, colIndex);
@@ -7178,6 +8120,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7178
8120
  endRowEdit = () => {
7179
8121
  this._editingRowIndex.value = null;
7180
8122
  this._rowDraft.value = {};
8123
+ this.rowDraftSync = {};
7181
8124
  this._invalidMsg.value = '';
7182
8125
  this._activeInControl.value = false;
7183
8126
  this._editVer.value = this._editVer.value + 1;
@@ -7206,7 +8149,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7206
8149
  // other editor type commits the value verbatim. Idempotent for the #editor drop-in path
7207
8150
  // (an already-numeric override passes through; an explicit null stays null).
7208
8151
  coerceCellValue = (colId: any, raw: any) => {
7209
- if (this.editorTypeOf(colId) !== 'number') return raw;
8152
+ const kind = this.editorTypeOf(colId);
8153
+ // ── C-13 ────────────────────────────────────────────────────────────────────────────
8154
+ // This funnel exists so "no mixed/garbage types ever reach the model" (B9, T-63-03-01), and
8155
+ // it covered ONE of the four built-in editor types. Every path that writes without opening
8156
+ // the built-in control — paste, Cut, Delete/Backspace over a range, a fill-drag — arrives
8157
+ // here with a plain TSV STRING, so a `checkbox` column took `''` from a Delete and the
8158
+ // literal text `'true'` from a paste, straight into a field the consumer declared boolean.
8159
+ // `select` is deliberately NOT coerced: its model type IS the option string, so a string is
8160
+ // already correct there, and an out-of-range option is a validation question (C-03's class),
8161
+ // not a type one.
8162
+ //
8163
+ // Recognised spellings both ways; ANYTHING else — including '' and unrecognised text —
8164
+ // becomes `false`. That is lossy by construction and deterministic by design: the
8165
+ // alternative is letting a non-boolean reach a boolean field, which is the defect. It is
8166
+ // documented in the editing page's emptied-cell table.
8167
+ if (kind === 'checkbox') {
8168
+ if (typeof raw === 'boolean') return raw;
8169
+ if (raw == null) return false;
8170
+ if (typeof raw === 'number') return raw !== 0;
8171
+ return /^\s*(true|1|yes|y|on)\s*$/i.test(String(raw));
8172
+ }
8173
+ if (kind !== 'number') return raw;
7210
8174
  if (raw == null) return null;
7211
8175
  if (typeof raw === 'number') return Number.isNaN(raw) ? null : raw;
7212
8176
  const s = String(raw).trim();
@@ -7330,11 +8294,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7330
8294
  // checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
7331
8295
  // write ever targets a group row" a property of the write funnels themselves.
7332
8296
  if (this.rowIndexIsGrouped(this._activeRow.value)) return;
7333
- const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
7334
- if (colId == null || !this.columnEditable(colId)) return;
7335
8297
  const rowList = this._rows.value || [];
7336
8298
  const row = rowList[this._activeRow.value];
7337
8299
  if (!row) return;
8300
+ // Debug 260926-dtl-edit-guard: defense-in-depth, matching the redundant rowIndexIsGrouped
8301
+ // check just above (Layer 4 is reachable ONLY through the Layer-1-gated checkbox branch in
8302
+ // onGridKeyDown, whose isActiveCellEditable() already excludes a placeholder row — this makes
8303
+ // "no write ever targets a placeholder row" a property of the write funnel itself, not just of
8304
+ // its one caller).
8305
+ if (this.rowIsLazyPlaceholder(row)) return;
8306
+ const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
8307
+ if (colId == null || !this.columnEditable(colId)) return;
7338
8308
  const rowOriginal = row.original;
7339
8309
  const rowId = row.id;
7340
8310
  const oldValue = this.cellValueAt(this._activeRow.value, this._activeColIndex.value);
@@ -7471,6 +8441,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7471
8441
  // redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
7472
8442
  // and the natural call site for a future #rowActions "Edit" drop-in button.
7473
8443
  if (this.rowIndexIsGrouped(rowIndex)) return;
8444
+ // Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (a row not loaded yet, virtual+manual+
8445
+ // rowCount) has nothing to edit — beginEdit (editCellLifecycle.rzts) already carries this
8446
+ // guard for single-cell entry; this full-row twin was missing it, so the public editRow()
8447
+ // verb (and, redundantly with isActiveCellEditable's own placeholder check, Shift+F2) could
8448
+ // seed a row editor from a placeholder's undefined fields and commit through the same
8449
+ // sourceIndexOfRow fallback bug applyGridToRange's placeholder skip closes.
8450
+ if (this.rowIsLazyPlaceholder((this._rows.value || [])[rowIndex])) return;
7474
8451
  const editable = this.editableColumnsForRow(rowIndex);
7475
8452
  if (editable.length === 0) return;
7476
8453
  // A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
@@ -7490,6 +8467,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7490
8467
  draft[ec.colId] = orig ? orig[ec.field] : null;
7491
8468
  }
7492
8469
  this._rowDraft.value = draft;
8470
+ this.rowDraftSync = draft;
7493
8471
  this._editingRowIndex.value = rowIndex;
7494
8472
  this._activeInControl.value = true;
7495
8473
  this._editVer.value = this._editVer.value + 1;
@@ -7520,7 +8498,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7520
8498
  const r = rowList[rowIndex];
7521
8499
  const rowOriginal = r ? r.original : null;
7522
8500
  const rowId = r ? r.id : null;
7523
- const draft = this._rowDraft.value || {};
8501
+ // C-05: the SYNC mirror, not `$data.rowDraft` — see rowDraftSync in DataTable.rozie.
8502
+ const draft = this.rowDraftSync || {};
7524
8503
  // Validate every edited column FIRST (D-01: a single failure blocks the whole row commit).
7525
8504
  // B3 (Rule 1): coerce each draft by the column's editor type BEFORE validation + write — a
7526
8505
  // 'number' editor must commit a real Number/null, never the raw editor STRING (the single-cell
@@ -7716,6 +8695,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7716
8695
  // A top-level `let` (React hoists to useRef → persists).
7717
8696
  committedThisSession = false;
7718
8697
 
8698
+ // rowDraftSync (quick 260922-mkb, C-05): a SYNCHRONOUS mirror of `$data.rowDraft`, written at
8699
+ // every rowDraft write and read by commitRow. A custom #editor drop-in stages its value with
8700
+ // `commit(v)` → setRowDraft in the SAME keydown that then commits the row (the host's drop-in
8701
+ // wrapper runs in the bubble phase right after the drop-in's own Enter handler). On React
8702
+ // `$data.rowDraft` is a `useState` read, so commitRow in that same tick saw the pre-write draft,
8703
+ // found no change and wrote nothing. The built-in editors never hit this only because they stage
8704
+ // on every `input` event, long before Enter. Same top-level-`let` technique as
8705
+ // committedThisSession above; on the five synchronous targets it always equals $data.rowDraft.
8706
+ rowDraftSync: Record<string, any> = {};
8707
+
8708
+ // dropinTabFlush (quick 260922-mkb, C-10): non-null only for the duration of the drop-in
8709
+ // wrapper's Tab flush (see onEditorDropinKeyDown). While set, a drop-in's `commit(v)` records
8710
+ // that it ran and whether it committed, and commits WITHOUT the focus return (the Tab handler
8711
+ // moves focus on to the next editor itself).
8712
+ dropinTabFlush: {
8713
+ called: boolean;
8714
+ committed: boolean;
8715
+ } | null = null;
8716
+
7719
8717
  // ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
7720
8718
  // In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
7721
8719
  // (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
@@ -7739,6 +8737,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7739
8737
  this.setRowDraft(colId, value);
7740
8738
  return;
7741
8739
  }
8740
+ // C-10: during the drop-in wrapper's Tab flush, record the outcome and skip the focus
8741
+ // return — the Tab handler advances focus to the next editor itself.
8742
+ if (this.dropinTabFlush) {
8743
+ this.dropinTabFlush.called = true;
8744
+ // STICKY: only a successful commit may set the outcome. A re-entrant commit within the
8745
+ // same flush (any blur the write itself provokes) is latched to false by commitEdit and
8746
+ // must not clear it. Measured: before the Solid slot-fill fix this re-entry was real.
8747
+ if (this.commitEdit(value, true)) this.dropinTabFlush.committed = true;
8748
+ return;
8749
+ }
7742
8750
  this.commitEdit(value);
7743
8751
  };
7744
8752
 
@@ -7776,10 +8784,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7776
8784
  // setRowDraft: write ONE key into a FRESH rowDraft object (whole-object replace — an
7777
8785
  // in-place mutation is silently dropped on React/Solid; the family immutable rule).
7778
8786
  setRowDraft = (colId: any, value: any) => {
7779
- const src = this._rowDraft.value || {};
8787
+ // C-05: build from the SYNC mirror, so two same-tick writes (and a same-tick commitRow) never
8788
+ // see a React-stale `$data.rowDraft`.
8789
+ const src = this.rowDraftSync || {};
7780
8790
  const next = {};
7781
8791
  for (const k in src) next[k] = src[k];
7782
8792
  next[colId] = value;
8793
+ this.rowDraftSync = next;
7783
8794
  this._rowDraft.value = next;
7784
8795
  };
7785
8796
 
@@ -7869,6 +8880,105 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7869
8880
  }
7870
8881
  };
7871
8882
 
8883
+ // DECLARED AFTER onEditorKeyDown ON PURPOSE. It delegates to it, and the React emitter lowers
8884
+ // both to `useCallback`s whose DEPENDENCY ARRAY is evaluated EAGERLY at declaration time — so
8885
+ // with this one first, its deps array referenced `onEditorKeyDown` before the const existed and
8886
+ // every React cell in the family died with `ReferenceError: Cannot access 'ze' before
8887
+ // initialization` at mount. That is the 87-02 useCallback-deps-array ordering lesson, and it is
8888
+ // invisible on the other five targets. Keep this block below.
8889
+ // ── C-10 + C-05: the grid keymap for a CUSTOM #editor drop-in ───────────────────────────
8890
+ // The built-in editors bind `onEditorKeyDown` directly, so Tab commits-and-advances and, in
8891
+ // row mode, Enter commits the row. A drop-in binds nothing of ours: it is consumer markup
8892
+ // inside the `#editor` slot. Its own keydown handler covers Enter and Escape (the drop-in
8893
+ // contract) and stops there, and the keydown then bubbles to the table root where
8894
+ // `onGridKeyDown` early-returns because an editor is open. Result:
8895
+ // C-10 — Tab in a drop-in did not commit and advance. It fell through to NATIVE tab order
8896
+ // and walked focus out of the grid, leaving the editor open behind it.
8897
+ // C-05 — in FULL-ROW mode a drop-in's `commit(v)` is draft-only BY DESIGN (committing per
8898
+ // cell would split the row into N writes and N events), and the row commits only
8899
+ // through handlers bound on the built-in inputs — which a row of drop-ins does not
8900
+ // have. So Shift+F2, documented as "Enter commits the whole row in one
8901
+ // `r-model:data` write", wrote a draft and nothing else, `editingRowIndex` stayed
8902
+ // set, and `onGridKeyDown`'s early-return left arrow nav dead. The only exits were
8903
+ // Escape (reverting the whole row) and the imperative verb.
8904
+ //
8905
+ // Bound on the `display: contents` span that wraps the slot, so it sees the bubbled event
8906
+ // from whatever the consumer rendered — including a bespoke editor that never heard of this
8907
+ // component. The host owning the GRID keymap while the drop-in owns its own CONTROL is the
8908
+ // same division the built-in path already has.
8909
+ //
8910
+ // Row mode ignores `defaultPrevented` for Enter/Escape on purpose: a drop-in preventDefaults
8911
+ // Enter to mean "do not insert a newline", not "the row is handled". Its own handler has
8912
+ // already run (target phase before bubble), so the draft is staged by the time we commit.
8913
+ // Single-cell mode honours `defaultPrevented` for Tab, so a drop-in that genuinely implements
8914
+ // its own Tab (a multi-field editor) keeps it.
8915
+ //
8916
+ // Single-cell Tab FLUSHES the drop-in before advancing (quick 260922-mkb). A drop-in owns its
8917
+ // draft: the host's `draftValue` still holds the value the editor opened with, so routing Tab
8918
+ // straight into onEditorKeyDown committed that stale value, opened the next cell's editor, and
8919
+ // then the old drop-in's unmount blur ran `commit(typed)` against the NOW-editing cell — the
8920
+ // typed text landed in the NEXT column (measured: `status=Zeta` from a Name edit, on four
8921
+ // targets). Instead the handler blurs the drop-in while its own cell is still the editing one,
8922
+ // so the drop-in's blur-commit (the drop-in contract) writes the right cell, and only then
8923
+ // advances. `dropinTabFlush` tells editorCommitFor to report the outcome and skip the focus
8924
+ // return. A drop-in that does not commit on blur (EditorCheckbox commits on change, so it holds
8925
+ // nothing pending) falls back to the host-draft path, which is exactly right in that case.
8926
+ onEditorDropinKeyDown = (e: any) => {
8927
+ if (!e) return;
8928
+ const key = e.key;
8929
+ if (this.inRowEdit()) {
8930
+ if (key === 'Enter') {
8931
+ e.preventDefault();
8932
+ this.commitRow();
8933
+ return;
8934
+ }
8935
+ if (key === 'Escape') {
8936
+ e.preventDefault();
8937
+ this.cancelRow();
8938
+ return;
8939
+ }
8940
+ if (key === 'Tab') {
8941
+ e.preventDefault();
8942
+ this.rowEditTab(e.target, e.shiftKey);
8943
+ return;
8944
+ }
8945
+ return;
8946
+ }
8947
+ if (key !== 'Tab' || e.defaultPrevented) return;
8948
+ e.preventDefault();
8949
+ const fromRow = this._editingRow.value;
8950
+ const fromCol = this._editingCol.value;
8951
+ const target = e.shiftKey ? this.prevEditableCell(fromRow, fromCol) : this.nextEditableCell(fromRow, fromCol);
8952
+ const el = e.target;
8953
+ // Move focus to the drop-in's OWN cell rather than calling blur(): that still fires the
8954
+ // drop-in's blur-commit, but keeps focus inside the grid instead of dropping it to <body>.
8955
+ const ownCell = el && el.closest ? el.closest('[data-grid-cell]') : null;
8956
+ const flush = {
8957
+ called: false,
8958
+ committed: false
8959
+ };
8960
+ this.dropinTabFlush = flush;
8961
+ if (ownCell && ownCell.focus) ownCell.focus();else if (el && el.blur) el.blur();
8962
+ this.dropinTabFlush = null;
8963
+ if (!flush.called) {
8964
+ this.onEditorKeyDown(e);
8965
+ return;
8966
+ }
8967
+ // A validation failure keeps the editor open: give the drop-in its focus back.
8968
+ if (!flush.committed) {
8969
+ if (el && el.focus) el.focus();
8970
+ return;
8971
+ }
8972
+ if (target) {
8973
+ this._activeRow.value = target.row;
8974
+ this._activeColIndex.value = target.col;
8975
+ this.beginEdit(target.row, target.col, null);
8976
+ } else {
8977
+ // B5 twin: no editable cell in the Tab direction — keep focus INSIDE the grid.
8978
+ this.focusCellWhenReady(fromRow, fromCol);
8979
+ }
8980
+ };
8981
+
7872
8982
  // onEditorBlur: commit on a genuine click/focus-away (D-01 — an invalid value keeps the
7873
8983
  // editor open via commitEdit's reject path). SKIP when:
7874
8984
  // - editTransition is set (a synchronous commit/cancel teardown is unmounting the editor), or
@@ -7905,11 +9015,52 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7905
9015
  }
7906
9016
  if (this._editingRow.value < 0 || this.editTransition) return;
7907
9017
  const next = e ? e.relatedTarget : null;
7908
- // A null relatedTarget is an unmount-blur (the editor left the DOM) or a focus drop the
7909
- // keyboard path owns; committing here would double-count (WR-04: the OLD editor's blur on
7910
- // a Tab-advance fires with a TRANSIENT null relatedTarget while it unmounts). Keep the
7911
- // conservative null=skip behavior.
7912
- if (next == null) return;
9018
+ const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
9019
+ // ── B-02 ────────────────────────────────────────────────────────────────────────────
9020
+ // A null relatedTarget used to return unconditionally. That is correct for the WR-04 case
9021
+ // the comment cited — a Tab-advance fires the OLD editor's blur with a TRANSIENT null
9022
+ // relatedTarget while it unmounts, and committing there double-counts — but it is NOT the
9023
+ // only shape. Clicking any non-focusable region of the page (the body, a heading, whitespace)
9024
+ // also blurs the editor with a null relatedTarget, and that returned WITHOUT committing and
9025
+ // WITHOUT endEdit(): the typed value was silently discarded, the editor stayed open, and
9026
+ // `$data.editingRow` stayed >= 0 — which gridKeydownHandlers' `if ($data.editingRow >= 0)
9027
+ // return` reads as "an editor owns the keyboard", so arrows/Enter/F2/Home/Ctrl+C were all
9028
+ // dead until the user double-clicked somewhere else. There is no document-level click-away
9029
+ // listener anywhere in the family to catch it afterwards.
9030
+ //
9031
+ // The two shapes ARE separable, with the discriminator the branch below already uses: on a
9032
+ // Tab-advance the keyboard path has already moved the editing pair to the NEXT cell, so the
9033
+ // blurring editor's own cell no longer matches ($data.editingRow/editingCol), while a
9034
+ // click-away leaves the pair still sitting on it. The row-edit branch twenty lines above
9035
+ // reaches the same conclusion by the same reasoning and commits on null; this is that branch's
9036
+ // single-cell twin.
9037
+ if (next == null) {
9038
+ // Not a click-away unless a pointerdown OUTSIDE the grid just said so. `outsidePointerDown`
9039
+ // is the discriminator, and nothing weaker works: an editor recycled out of the virtual
9040
+ // window (or replaced by Solid's keyed-row reconcile) blurs with a null relatedTarget too,
9041
+ // and `closest()` still resolves an owning `<td>` inside a DETACHED subtree, so every
9042
+ // synchronous DOM test reads it as a click-away — measured, it closed the pinned editor in
9043
+ // data-table-edit's "open editor + index-based range survive virtualization recycle" case on
9044
+ // solid. Deferring a frame does not help either: by then the node is reattached, focus sits
9045
+ // on <body>, and the state is indistinguishable from a real click-away.
9046
+ //
9047
+ // The flag is a plain module-let set by the document pointerdown listener in DataTable.rozie
9048
+ // (NOT a $data field, and NOT a closure over one): a document listener is attached once, so
9049
+ // on React any `$data` it closes over is frozen at attach time, and a commit driven from
9050
+ // that closure would write the draft as of editor-OPEN and lose everything typed. Routing
9051
+ // the decision back through THIS handler — template-bound, so React re-binds it every
9052
+ // render — keeps the commit on the always-fresh path. Consume it here so an unmount blur
9053
+ // arriving later can never inherit it.
9054
+ if (!this.outsidePointerDown) return;
9055
+ this.outsidePointerDown = false;
9056
+ if (!fromCell) return;
9057
+ if (fromCell.getAttribute('data-row') !== String(this._editingRow.value) || fromCell.getAttribute('data-col-index') !== String(this._editingCol.value)) return;
9058
+ // Genuine click-away onto a non-focusable region outside the grid → commit + close (D-01's
9059
+ // reject path keeps the editor open on an invalid value, exactly as for a click onto another
9060
+ // cell). skipFocusReturn: the user deliberately put focus elsewhere; do not yank it back.
9061
+ this.commitEdit(undefined, true);
9062
+ return;
9063
+ }
7913
9064
  // Focus moving OUTSIDE the grid (a click into another widget) → commit (D-01 reject keeps
7914
9065
  // the editor open on an invalid value).
7915
9066
  if (!(this.gridRoot && this.gridRoot.contains && this.gridRoot.contains(next))) {
@@ -7920,7 +9071,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
7920
9071
  // keyboard handler already committed) from a genuine click-away to ANOTHER grid cell
7921
9072
  // (which must commit + close so the grid is not wedged with an open editor).
7922
9073
  const nextCell = next.closest ? next.closest('[data-grid-cell]') : null;
7923
- const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
7924
9074
  // Same cell (an inner control / the editing cell itself on an Enter focus-return) → a
7925
9075
  // controlled move; skip. Also skip when either cell can't be resolved (an unmounting
7926
9076
  // editor has no owning cell — the Tab-advance remount-blur path, never a click-away).