@rozie-ui/data-table-solid 0.3.1 → 0.3.2

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/dist/index.cjs CHANGED
@@ -44,6 +44,39 @@ const wrapAggregationFn = (fn) => {
44
44
  }
45
45
  };
46
46
  };
47
+ const collectNestedDefs = (list, out) => {
48
+ if (!Array.isArray(list)) return;
49
+ for (const d of list) {
50
+ if (!d || d.id == null) continue;
51
+ const id = String(d.id);
52
+ if (!(id in out)) out[id] = d;
53
+ if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
54
+ }
55
+ };
56
+ const indexDefsById = (defs) => {
57
+ const out = Object.create(null);
58
+ if (!Array.isArray(defs)) return out;
59
+ for (const d of defs) {
60
+ if (!d || d.id == null) continue;
61
+ out[String(d.id)] = d;
62
+ }
63
+ for (const d of defs) if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
64
+ return out;
65
+ };
66
+ const collectGroupableLeafDefs = (defs) => {
67
+ const out = [];
68
+ if (!Array.isArray(defs)) return out;
69
+ for (const d of defs) {
70
+ if (!d) continue;
71
+ if (Array.isArray(d.columns)) {
72
+ out.push(...collectGroupableLeafDefs(d.columns));
73
+ continue;
74
+ }
75
+ if (d.groupable === false) continue;
76
+ out.push(d);
77
+ }
78
+ return out;
79
+ };
47
80
  //#endregion
48
81
  //#region src/helpers/indexMath.ts
49
82
  const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
@@ -933,6 +966,7 @@ function DataTable(_props) {
933
966
  let columnDefsCache = null;
934
967
  let columnDefsCacheColumnsRef = void 0;
935
968
  let columnDefsCacheColRegRef = void 0;
969
+ let columnDefsIndexCache = null;
936
970
  function columnDefs() {
937
971
  const cfg = local.columns || [];
938
972
  const reg = colReg() || {};
@@ -976,8 +1010,13 @@ function DataTable(_props) {
976
1010
  columnDefsCache = out;
977
1011
  columnDefsCacheColumnsRef = local.columns;
978
1012
  columnDefsCacheColRegRef = colReg();
1013
+ columnDefsIndexCache = indexDefsById(out);
979
1014
  return out;
980
1015
  }
1016
+ function defIndex() {
1017
+ columnDefs();
1018
+ return columnDefsIndexCache || Object.create(null);
1019
+ }
981
1020
  const SELECT_COL_ID = "__rdt_select";
982
1021
  const EXPANDER_COL_ID = "__rdt_expander";
983
1022
  function selectionEnabled() {
@@ -1695,9 +1734,9 @@ function DataTable(_props) {
1695
1734
  return "";
1696
1735
  }
1697
1736
  function defFor(colId) {
1698
- const defs = columnDefs();
1699
- for (const d of defs) if (d.id === colId) return d;
1700
- return null;
1737
+ if (colId == null) return null;
1738
+ const d = defIndex()[String(colId)];
1739
+ return d != null ? d : null;
1701
1740
  }
1702
1741
  function visibleCellsFor(row) {
1703
1742
  return rowModelVer() >= 0 ? row.getVisibleCells() : [];
@@ -1890,6 +1929,9 @@ function DataTable(_props) {
1890
1929
  function rowIsGrouped(row) {
1891
1930
  return !!(tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
1892
1931
  }
1932
+ function rowIndexIsGrouped(rowIndex) {
1933
+ return rowIsGrouped((rows() || [])[rowIndex]);
1934
+ }
1893
1935
  function groupingActive() {
1894
1936
  return tick() >= 0 && (currentState().grouping || []).length > 0;
1895
1937
  }
@@ -1903,21 +1945,54 @@ function DataTable(_props) {
1903
1945
  return !!(tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
1904
1946
  }
1905
1947
  function groupSubRowCount(row) {
1906
- return row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
1948
+ if (row && row.getLeafRows) {
1949
+ const leaves = row.getLeafRows();
1950
+ let n = 0;
1951
+ for (let i = 0; i < leaves.length; i++) {
1952
+ const r = leaves[i];
1953
+ if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
1954
+ }
1955
+ return n;
1956
+ }
1957
+ return row && row.subRows ? row.subRows.length : 0;
1958
+ }
1959
+ let groupRowDescriptorCache = null;
1960
+ let groupRowDescriptorCacheVer = void 0;
1961
+ function groupRowDescriptor(row) {
1962
+ const ver = rowModelVer();
1963
+ if (!groupRowDescriptorCache || groupRowDescriptorCacheVer !== ver) {
1964
+ groupRowDescriptorCache = Object.create(null);
1965
+ groupRowDescriptorCacheVer = ver;
1966
+ }
1967
+ const key = String(row.id);
1968
+ let d = groupRowDescriptorCache[key];
1969
+ if (!d) {
1970
+ const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : "";
1971
+ const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
1972
+ d = {
1973
+ isGroupRow: true,
1974
+ groupId: row.id,
1975
+ groupingColumnId,
1976
+ groupingValue,
1977
+ leafCount: groupSubRowCount(row)
1978
+ };
1979
+ groupRowDescriptorCache[key] = d;
1980
+ }
1981
+ return d;
1982
+ }
1983
+ function cellSlotRow(row) {
1984
+ return rowIsGrouped(row) ? groupRowDescriptor(row) : row ? row.original : null;
1907
1985
  }
1908
1986
  function groupingKeys() {
1909
1987
  return currentState().grouping || [];
1910
1988
  }
1911
1989
  function groupableColumns() {
1912
1990
  const out = [];
1913
- const defs = columnDefs();
1914
- for (const d of defs) {
1915
- if (!d || d.groupable === false) continue;
1916
- out.push({
1917
- id: d.id,
1918
- label: d.header != null ? d.header : d.id
1919
- });
1920
- }
1991
+ const defs = collectGroupableLeafDefs(columnDefs());
1992
+ for (const d of defs) out.push({
1993
+ id: d.id,
1994
+ label: d.header != null ? d.header : d.id
1995
+ });
1921
1996
  return out;
1922
1997
  }
1923
1998
  function stopEvent(evt) {
@@ -2943,11 +3018,13 @@ function DataTable(_props) {
2943
3018
  for (let gr = 0; gr < grid.length; gr++) {
2944
3019
  const r = originRow + gr;
2945
3020
  if (r > maxRow) break;
3021
+ const rowGrouped = rowIndexIsGrouped(r);
2946
3022
  const cols = grid[gr] || [];
2947
3023
  for (let gc = 0; gc < cols.length; gc++) {
2948
3024
  const c = originCol + gc;
2949
3025
  if (c > maxCol) break;
2950
3026
  total = total + 1;
3027
+ if (rowGrouped) continue;
2951
3028
  const colId = columnIdAt(r, c);
2952
3029
  if (colId == null || !columnEditable(colId)) continue;
2953
3030
  const rowObj = rowOriginalAt(r);
@@ -3250,11 +3327,13 @@ function DataTable(_props) {
3250
3327
  return cell && cell.column ? cell.column.id : null;
3251
3328
  }
3252
3329
  function isActiveCellEditable() {
3330
+ if (rowIndexIsGrouped(activeRow())) return false;
3253
3331
  const colId = activeCellColumnId();
3254
3332
  return colId != null && columnEditable(colId);
3255
3333
  }
3256
3334
  function isEditing(rowIndex, colIndex) {
3257
3335
  if (editVer() < 0) return false;
3336
+ if (rowIndexIsGrouped(rowIndex)) return false;
3258
3337
  if (editingRowIndex() != null && editingRowIndex() === rowIndex) {
3259
3338
  const colId = columnIdAt(rowIndex, colIndex);
3260
3339
  return colId != null && columnEditable(colId);
@@ -3356,6 +3435,7 @@ function DataTable(_props) {
3356
3435
  return cell ? cell.getValue() : null;
3357
3436
  }
3358
3437
  function beginEdit(rowIndex, colIndex, seed) {
3438
+ if (rowIndexIsGrouped(rowIndex)) return;
3359
3439
  const colId = columnIdAt(rowIndex, colIndex);
3360
3440
  if (colId == null || !columnEditable(colId)) return;
3361
3441
  committedThisSession = false;
@@ -3467,6 +3547,7 @@ function DataTable(_props) {
3467
3547
  return true;
3468
3548
  }
3469
3549
  function toggleActiveBooleanCell() {
3550
+ if (rowIndexIsGrouped(activeRow())) return;
3470
3551
  const colId = columnIdAt(activeRow(), activeColIndex());
3471
3552
  if (colId == null || !columnEditable(colId)) return;
3472
3553
  const row = (rows() || [])[activeRow()];
@@ -3552,6 +3633,7 @@ function DataTable(_props) {
3552
3633
  function beginRowEdit(row) {
3553
3634
  const rowIndex = rowIndexOf(row);
3554
3635
  if (rowIndex < 0) return;
3636
+ if (rowIndexIsGrouped(rowIndex)) return;
3555
3637
  const editable = editableColumnsForRow(rowIndex);
3556
3638
  if (editable.length === 0) return;
3557
3639
  committedThisSession = false;
@@ -4140,7 +4222,7 @@ function DataTable(_props) {
4140
4222
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4141
4223
  columnId: cell().column.id,
4142
4224
  column: cell().column,
4143
- row: row().original,
4225
+ row: cellSlotRow(row()),
4144
4226
  value: cell().getValue()
4145
4227
  }) ?? (0, _rozie_runtime_solid.rozieDisplay)(cell().getValue())}
4146
4228
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Solid_js.Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -4174,7 +4256,7 @@ function DataTable(_props) {
4174
4256
  {(_props.editorSlot ?? _props.slots?.["editor"])?.({
4175
4257
  columnId: cell().column.id,
4176
4258
  column: cell().column,
4177
- row: row().original,
4259
+ row: cellSlotRow(row()),
4178
4260
  value: editorValueFor(cell().column.id),
4179
4261
  commit: editorCommitFor(cell().column.id),
4180
4262
  cancel: editorCancelFor(),
@@ -4188,14 +4270,14 @@ function DataTable(_props) {
4188
4270
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4189
4271
  columnId: cell().column.id,
4190
4272
  column: cell().column,
4191
- row: row().original,
4273
+ row: cellSlotRow(row()),
4192
4274
  value: cell().getValue()
4193
4275
  }) ?? (0, _rozie_runtime_solid.rozieDisplay)(cell().getValue())}
4194
4276
  </span>
4195
4277
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{(0, _rozie_runtime_solid.rozieDisplay)("(" + groupSubRowCount(row()) + ")")}</span>
4196
4278
  </span></Solid_js.Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4197
4279
  {(_props.selectCellSlot ?? _props.slots?.["selectCell"])?.({
4198
- row: row().original,
4280
+ row: cellSlotRow(row()),
4199
4281
  checked: rowIsSelected(row()),
4200
4282
  toggle: (e) => onToggleRow(row(), e)
4201
4283
  }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(row())} onChange={($event) => {
@@ -4336,7 +4418,7 @@ function DataTable(_props) {
4336
4418
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4337
4419
  columnId: cell().column.id,
4338
4420
  column: cell().column,
4339
- row: wr().row.original,
4421
+ row: cellSlotRow(wr().row),
4340
4422
  value: cell().getValue()
4341
4423
  }) ?? (0, _rozie_runtime_solid.rozieDisplay)(cell().getValue())}
4342
4424
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Solid_js.Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -4370,7 +4452,7 @@ function DataTable(_props) {
4370
4452
  {(_props.editorSlot ?? _props.slots?.["editor"])?.({
4371
4453
  columnId: cell().column.id,
4372
4454
  column: cell().column,
4373
- row: wr().row.original,
4455
+ row: cellSlotRow(wr().row),
4374
4456
  value: editorValueFor(cell().column.id),
4375
4457
  commit: editorCommitFor(cell().column.id),
4376
4458
  cancel: editorCancelFor(),
@@ -4384,14 +4466,14 @@ function DataTable(_props) {
4384
4466
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4385
4467
  columnId: cell().column.id,
4386
4468
  column: cell().column,
4387
- row: wr().row.original,
4469
+ row: cellSlotRow(wr().row),
4388
4470
  value: cell().getValue()
4389
4471
  }) ?? (0, _rozie_runtime_solid.rozieDisplay)(cell().getValue())}
4390
4472
  </span>
4391
4473
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{(0, _rozie_runtime_solid.rozieDisplay)("(" + groupSubRowCount(wr().row) + ")")}</span>
4392
4474
  </span></Solid_js.Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4393
4475
  {(_props.selectCellSlot ?? _props.slots?.["selectCell"])?.({
4394
- row: wr().row.original,
4476
+ row: cellSlotRow(wr().row),
4395
4477
  checked: rowIsSelected(wr().row),
4396
4478
  toggle: (e) => onToggleRow(wr().row, e)
4397
4479
  }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(wr().row)} onChange={($event) => {
package/dist/index.mjs CHANGED
@@ -17,6 +17,39 @@ const wrapAggregationFn = (fn) => {
17
17
  }
18
18
  };
19
19
  };
20
+ const collectNestedDefs = (list, out) => {
21
+ if (!Array.isArray(list)) return;
22
+ for (const d of list) {
23
+ if (!d || d.id == null) continue;
24
+ const id = String(d.id);
25
+ if (!(id in out)) out[id] = d;
26
+ if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
27
+ }
28
+ };
29
+ const indexDefsById = (defs) => {
30
+ const out = Object.create(null);
31
+ if (!Array.isArray(defs)) return out;
32
+ for (const d of defs) {
33
+ if (!d || d.id == null) continue;
34
+ out[String(d.id)] = d;
35
+ }
36
+ for (const d of defs) if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
37
+ return out;
38
+ };
39
+ const collectGroupableLeafDefs = (defs) => {
40
+ const out = [];
41
+ if (!Array.isArray(defs)) return out;
42
+ for (const d of defs) {
43
+ if (!d) continue;
44
+ if (Array.isArray(d.columns)) {
45
+ out.push(...collectGroupableLeafDefs(d.columns));
46
+ continue;
47
+ }
48
+ if (d.groupable === false) continue;
49
+ out.push(d);
50
+ }
51
+ return out;
52
+ };
20
53
  //#endregion
21
54
  //#region src/helpers/indexMath.ts
22
55
  const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
@@ -906,6 +939,7 @@ function DataTable(_props) {
906
939
  let columnDefsCache = null;
907
940
  let columnDefsCacheColumnsRef = void 0;
908
941
  let columnDefsCacheColRegRef = void 0;
942
+ let columnDefsIndexCache = null;
909
943
  function columnDefs() {
910
944
  const cfg = local.columns || [];
911
945
  const reg = colReg() || {};
@@ -949,8 +983,13 @@ function DataTable(_props) {
949
983
  columnDefsCache = out;
950
984
  columnDefsCacheColumnsRef = local.columns;
951
985
  columnDefsCacheColRegRef = colReg();
986
+ columnDefsIndexCache = indexDefsById(out);
952
987
  return out;
953
988
  }
989
+ function defIndex() {
990
+ columnDefs();
991
+ return columnDefsIndexCache || Object.create(null);
992
+ }
954
993
  const SELECT_COL_ID = "__rdt_select";
955
994
  const EXPANDER_COL_ID = "__rdt_expander";
956
995
  function selectionEnabled() {
@@ -1668,9 +1707,9 @@ function DataTable(_props) {
1668
1707
  return "";
1669
1708
  }
1670
1709
  function defFor(colId) {
1671
- const defs = columnDefs();
1672
- for (const d of defs) if (d.id === colId) return d;
1673
- return null;
1710
+ if (colId == null) return null;
1711
+ const d = defIndex()[String(colId)];
1712
+ return d != null ? d : null;
1674
1713
  }
1675
1714
  function visibleCellsFor(row) {
1676
1715
  return rowModelVer() >= 0 ? row.getVisibleCells() : [];
@@ -1863,6 +1902,9 @@ function DataTable(_props) {
1863
1902
  function rowIsGrouped(row) {
1864
1903
  return !!(tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
1865
1904
  }
1905
+ function rowIndexIsGrouped(rowIndex) {
1906
+ return rowIsGrouped((rows() || [])[rowIndex]);
1907
+ }
1866
1908
  function groupingActive() {
1867
1909
  return tick() >= 0 && (currentState().grouping || []).length > 0;
1868
1910
  }
@@ -1876,21 +1918,54 @@ function DataTable(_props) {
1876
1918
  return !!(tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
1877
1919
  }
1878
1920
  function groupSubRowCount(row) {
1879
- return row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
1921
+ if (row && row.getLeafRows) {
1922
+ const leaves = row.getLeafRows();
1923
+ let n = 0;
1924
+ for (let i = 0; i < leaves.length; i++) {
1925
+ const r = leaves[i];
1926
+ if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
1927
+ }
1928
+ return n;
1929
+ }
1930
+ return row && row.subRows ? row.subRows.length : 0;
1931
+ }
1932
+ let groupRowDescriptorCache = null;
1933
+ let groupRowDescriptorCacheVer = void 0;
1934
+ function groupRowDescriptor(row) {
1935
+ const ver = rowModelVer();
1936
+ if (!groupRowDescriptorCache || groupRowDescriptorCacheVer !== ver) {
1937
+ groupRowDescriptorCache = Object.create(null);
1938
+ groupRowDescriptorCacheVer = ver;
1939
+ }
1940
+ const key = String(row.id);
1941
+ let d = groupRowDescriptorCache[key];
1942
+ if (!d) {
1943
+ const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : "";
1944
+ const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
1945
+ d = {
1946
+ isGroupRow: true,
1947
+ groupId: row.id,
1948
+ groupingColumnId,
1949
+ groupingValue,
1950
+ leafCount: groupSubRowCount(row)
1951
+ };
1952
+ groupRowDescriptorCache[key] = d;
1953
+ }
1954
+ return d;
1955
+ }
1956
+ function cellSlotRow(row) {
1957
+ return rowIsGrouped(row) ? groupRowDescriptor(row) : row ? row.original : null;
1880
1958
  }
1881
1959
  function groupingKeys() {
1882
1960
  return currentState().grouping || [];
1883
1961
  }
1884
1962
  function groupableColumns() {
1885
1963
  const out = [];
1886
- const defs = columnDefs();
1887
- for (const d of defs) {
1888
- if (!d || d.groupable === false) continue;
1889
- out.push({
1890
- id: d.id,
1891
- label: d.header != null ? d.header : d.id
1892
- });
1893
- }
1964
+ const defs = collectGroupableLeafDefs(columnDefs());
1965
+ for (const d of defs) out.push({
1966
+ id: d.id,
1967
+ label: d.header != null ? d.header : d.id
1968
+ });
1894
1969
  return out;
1895
1970
  }
1896
1971
  function stopEvent(evt) {
@@ -2916,11 +2991,13 @@ function DataTable(_props) {
2916
2991
  for (let gr = 0; gr < grid.length; gr++) {
2917
2992
  const r = originRow + gr;
2918
2993
  if (r > maxRow) break;
2994
+ const rowGrouped = rowIndexIsGrouped(r);
2919
2995
  const cols = grid[gr] || [];
2920
2996
  for (let gc = 0; gc < cols.length; gc++) {
2921
2997
  const c = originCol + gc;
2922
2998
  if (c > maxCol) break;
2923
2999
  total = total + 1;
3000
+ if (rowGrouped) continue;
2924
3001
  const colId = columnIdAt(r, c);
2925
3002
  if (colId == null || !columnEditable(colId)) continue;
2926
3003
  const rowObj = rowOriginalAt(r);
@@ -3223,11 +3300,13 @@ function DataTable(_props) {
3223
3300
  return cell && cell.column ? cell.column.id : null;
3224
3301
  }
3225
3302
  function isActiveCellEditable() {
3303
+ if (rowIndexIsGrouped(activeRow())) return false;
3226
3304
  const colId = activeCellColumnId();
3227
3305
  return colId != null && columnEditable(colId);
3228
3306
  }
3229
3307
  function isEditing(rowIndex, colIndex) {
3230
3308
  if (editVer() < 0) return false;
3309
+ if (rowIndexIsGrouped(rowIndex)) return false;
3231
3310
  if (editingRowIndex() != null && editingRowIndex() === rowIndex) {
3232
3311
  const colId = columnIdAt(rowIndex, colIndex);
3233
3312
  return colId != null && columnEditable(colId);
@@ -3329,6 +3408,7 @@ function DataTable(_props) {
3329
3408
  return cell ? cell.getValue() : null;
3330
3409
  }
3331
3410
  function beginEdit(rowIndex, colIndex, seed) {
3411
+ if (rowIndexIsGrouped(rowIndex)) return;
3332
3412
  const colId = columnIdAt(rowIndex, colIndex);
3333
3413
  if (colId == null || !columnEditable(colId)) return;
3334
3414
  committedThisSession = false;
@@ -3440,6 +3520,7 @@ function DataTable(_props) {
3440
3520
  return true;
3441
3521
  }
3442
3522
  function toggleActiveBooleanCell() {
3523
+ if (rowIndexIsGrouped(activeRow())) return;
3443
3524
  const colId = columnIdAt(activeRow(), activeColIndex());
3444
3525
  if (colId == null || !columnEditable(colId)) return;
3445
3526
  const row = (rows() || [])[activeRow()];
@@ -3525,6 +3606,7 @@ function DataTable(_props) {
3525
3606
  function beginRowEdit(row) {
3526
3607
  const rowIndex = rowIndexOf(row);
3527
3608
  if (rowIndex < 0) return;
3609
+ if (rowIndexIsGrouped(rowIndex)) return;
3528
3610
  const editable = editableColumnsForRow(rowIndex);
3529
3611
  if (editable.length === 0) return;
3530
3612
  committedThisSession = false;
@@ -4113,7 +4195,7 @@ function DataTable(_props) {
4113
4195
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4114
4196
  columnId: cell().column.id,
4115
4197
  column: cell().column,
4116
- row: row().original,
4198
+ row: cellSlotRow(row()),
4117
4199
  value: cell().getValue()
4118
4200
  }) ?? rozieDisplay(cell().getValue())}
4119
4201
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -4147,7 +4229,7 @@ function DataTable(_props) {
4147
4229
  {(_props.editorSlot ?? _props.slots?.["editor"])?.({
4148
4230
  columnId: cell().column.id,
4149
4231
  column: cell().column,
4150
- row: row().original,
4232
+ row: cellSlotRow(row()),
4151
4233
  value: editorValueFor(cell().column.id),
4152
4234
  commit: editorCommitFor(cell().column.id),
4153
4235
  cancel: editorCancelFor(),
@@ -4161,14 +4243,14 @@ function DataTable(_props) {
4161
4243
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4162
4244
  columnId: cell().column.id,
4163
4245
  column: cell().column,
4164
- row: row().original,
4246
+ row: cellSlotRow(row()),
4165
4247
  value: cell().getValue()
4166
4248
  }) ?? rozieDisplay(cell().getValue())}
4167
4249
  </span>
4168
4250
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{rozieDisplay("(" + groupSubRowCount(row()) + ")")}</span>
4169
4251
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4170
4252
  {(_props.selectCellSlot ?? _props.slots?.["selectCell"])?.({
4171
- row: row().original,
4253
+ row: cellSlotRow(row()),
4172
4254
  checked: rowIsSelected(row()),
4173
4255
  toggle: (e) => onToggleRow(row(), e)
4174
4256
  }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(row())} onChange={($event) => {
@@ -4309,7 +4391,7 @@ function DataTable(_props) {
4309
4391
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4310
4392
  columnId: cell().column.id,
4311
4393
  column: cell().column,
4312
- row: wr().row.original,
4394
+ row: cellSlotRow(wr().row),
4313
4395
  value: cell().getValue()
4314
4396
  }) ?? rozieDisplay(cell().getValue())}
4315
4397
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -4343,7 +4425,7 @@ function DataTable(_props) {
4343
4425
  {(_props.editorSlot ?? _props.slots?.["editor"])?.({
4344
4426
  columnId: cell().column.id,
4345
4427
  column: cell().column,
4346
- row: wr().row.original,
4428
+ row: cellSlotRow(wr().row),
4347
4429
  value: editorValueFor(cell().column.id),
4348
4430
  commit: editorCommitFor(cell().column.id),
4349
4431
  cancel: editorCancelFor(),
@@ -4357,14 +4439,14 @@ function DataTable(_props) {
4357
4439
  {(_props.cellSlot ?? _props.slots?.["cell"])?.({
4358
4440
  columnId: cell().column.id,
4359
4441
  column: cell().column,
4360
- row: wr().row.original,
4442
+ row: cellSlotRow(wr().row),
4361
4443
  value: cell().getValue()
4362
4444
  }) ?? rozieDisplay(cell().getValue())}
4363
4445
  </span>
4364
4446
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{rozieDisplay("(" + groupSubRowCount(wr().row) + ")")}</span>
4365
4447
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
4366
4448
  {(_props.selectCellSlot ?? _props.slots?.["selectCell"])?.({
4367
- row: wr().row.original,
4449
+ row: cellSlotRow(wr().row),
4368
4450
  checked: rowIsSelected(wr().row),
4369
4451
  toggle: (e) => onToggleRow(wr().row, e)
4370
4452
  }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(wr().row)} onChange={($event) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozie-ui/data-table-solid",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
package/src/DataTable.tsx CHANGED
@@ -3,7 +3,7 @@ import { Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount, s
3
3
  import { Key } from '@solid-primitives/keyed';
4
4
  import { __rozieInjectStyle, createControllableSignal, parseInlineStyle, rozieAttr, rozieClass, rozieContext, rozieDisplay } from '@rozie/runtime-solid';
5
5
  import Popover from '@rozie-ui/popover-solid';
6
- import { isSafeKey, wrapAggregationFn } from './helpers/columnDefUtils';
6
+ import { isSafeKey, wrapAggregationFn, indexDefsById, collectGroupableLeafDefs } from './helpers/columnDefUtils';
7
7
  import { applyUpdater, clamp, focusables } from './helpers/indexMath';
8
8
  import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
9
9
  import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
@@ -1304,6 +1304,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1304
1304
  let columnDefsCache: any[] | null = null;
1305
1305
  let columnDefsCacheColumnsRef: any = undefined;
1306
1306
  let columnDefsCacheColRegRef: any = undefined;
1307
+ // B1 (quick 260906-afh) — the def-lookup index, populated INSIDE this SAME memo block on
1308
+ // a cache MISS only (never rebuilt per call — a per-call recursive scan would restore the
1309
+ // O(N^2 x M) blowup D-23 removed, T-AFH-03). A top-level `let` lowers to a per-instance
1310
+ // `useRef` on React (verified in the emitted leaf), so this is instance-scoped like
1311
+ // columnDefsCache above.
1312
+ let columnDefsIndexCache: any = null;
1307
1313
  function columnDefs() {
1308
1314
  const cfg = local.columns || [];
1309
1315
  const reg = colReg() || {};
@@ -1352,9 +1358,20 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1352
1358
  columnDefsCache = out;
1353
1359
  columnDefsCacheColumnsRef = local.columns;
1354
1360
  columnDefsCacheColRegRef = colReg();
1361
+ columnDefsIndexCache = indexDefsById(out);
1355
1362
  return out;
1356
1363
  }
1357
1364
 
1365
+ // defIndex: the def-lookup map for the CURRENT columnDefs() array. Calling columnDefs()
1366
+ // first is deliberate — either an O(1) reference-equality cache hit (columnDefsIndexCache
1367
+ // is already fresh) or a miss that repopulates BOTH slots together, so the index can never
1368
+ // go stale relative to the array. Falls back to an empty null-prototype map (never null/
1369
+ // undefined) so a caller can read it unconditionally.
1370
+ function defIndex() {
1371
+ columnDefs();
1372
+ return columnDefsIndexCache || Object.create(null);
1373
+ }
1374
+
1358
1375
  // The constant id of the auto-injected leading checkbox column (D-04). Distinct from
1359
1376
  // any consumer column id (the registry/config guard never produces a leading "__").
1360
1377
  const SELECT_COL_ID = '__rdt_select';
@@ -3034,10 +3051,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3034
3051
 
3035
3052
  // Template helpers reading the resolved column-def metadata by id (plain fns — used
3036
3053
  // in template predicates + interpolation; uniform on all 6, no $computed alias trap).
3054
+ // B1 (quick 260906-afh): reads the O(1) def-lookup index (defIndex(), columnBuilders.rzts)
3055
+ // instead of linearly scanning columnDefs() — which only ever held TOP-LEVEL entries, so a
3056
+ // NESTED group leaf (a `columns:` group child) was invisible to every caller (headerLabel,
3057
+ // columnIsFilterable, editMetaOf/columnEditable/editorTypeOf/editorOptionsOf) and silently
3058
+ // took the "no def" fallback. defIndex() resolves both top-level AND nested leaf ids. This
3059
+ // also removes the per-cell O(N) scan D-23 left in place — defIndex() is memoized in the
3060
+ // SAME cache-invalidation block as columnDefs() itself, never rebuilt per call.
3037
3061
  function defFor(colId: any) {
3038
- const defs = columnDefs();
3039
- for (const d of defs as any) if (d.id === colId) return d;
3040
- return null;
3062
+ if (colId == null) return null;
3063
+ const idx = defIndex();
3064
+ const d = idx[String(colId)];
3065
+ return d != null ? d : null;
3041
3066
  }
3042
3067
  // Per-row visible cells for the body loop. table-core memoizes row objects by id,
3043
3068
  // so a re-pull after a column change (visibility/reorder/pin, or the late <Column>
@@ -3390,6 +3415,16 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3390
3415
  function rowIsGrouped(row: any) {
3391
3416
  return !!(tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
3392
3417
  }
3418
+ // rowIndexIsGrouped (quick 260906-cvo, C1/C2): THE single row-level editability/writability
3419
+ // predicate — a group-header row is a SYNTHETIC row whose `original` is table-core's
3420
+ // `leafRows[0].original` (the same fact cellSlotRow's header comment documents), so any
3421
+ // edit or bulk write resolved through it silently lands on an UNRELATED record. Every edit/
3422
+ // write guard in this component calls THIS predicate — never a re-implemented inline
3423
+ // `getIsGrouped()` check — so the invariant has one definition. Indexes into the SAME
3424
+ // `$data.rows` flattened row list rowIsGrouped reads.
3425
+ function rowIndexIsGrouped(rowIndex: any) {
3426
+ return rowIsGrouped((rows() || [])[rowIndex]);
3427
+ }
3393
3428
  // groupingActive: grouping is currently engaged (a non-empty ordered key list). Drives the
3394
3429
  // data-group-leaf marker so it is ABSENT when ungrouped (byte-identical-off, req-10).
3395
3430
  function groupingActive() {
@@ -3405,22 +3440,100 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3405
3440
  function cellIsAggregated(cellCtx: any) {
3406
3441
  return !!(tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
3407
3442
  }
3408
- // cellIsPlaceholder: a PLACEHOLDER cell on a group-header row — a non-grouped, non-aggregated
3409
- // cell that table-core fills with the FIRST leaf row's value (cell.getValue() leaks e.g.
3410
- // "Services"/"Edsger Dijkstra" onto the group line). Renders BLANK via a dedicated empty
3411
- // template branch so the leaked leaf value never paints. Tick-gated exactly like cellIsGrouped
3412
- // so the group chrome re-derives on a re-pull on the fine-grained targets (Solid/Lit).
3443
+ // cellIsPlaceholder: VERIFIED against @tanstack/table-core@8.21.3 (ColumnGrouping.js:120-121,
3444
+ // quick 260906-afh premise correction). A placeholder cell is a cell IN A GROUPING COLUMN
3445
+ // that is NOT the row's OWN grouping column — `!cell.getIsGrouped() && column.getIsGrouped()`.
3446
+ // That is true both on a group-header row (for every OTHER grouping column) and on every leaf
3447
+ // row (for every grouping column) — it is NOT "never true on a group-header row" and it is NOT
3448
+ // "any non-grouped, non-aggregated cell". The operative consequence for this component: a
3449
+ // placeholder can only EVER be a grouping-column cell, so this branch can never reach a
3450
+ // non-grouping column — it does NOT and CANNOT guard the AGGREGATED-cell `row` leak
3451
+ // (cellSlotRow, below) fixes. Where it DOES apply (e.g. under multi-level grouping, the
3452
+ // OTHER grouping column on a group-header row), table-core still fills cell.getValue() with
3453
+ // the FIRST leaf row's value (cell.getValue() leaks e.g. "Oslo" onto a region-level group
3454
+ // line whose grouping is 'region', not 'city'); this branch renders BLANK via a dedicated
3455
+ // empty template so that leaked value never paints. Tick-gated exactly like cellIsGrouped so
3456
+ // the group chrome re-derives on a re-pull on the fine-grained targets (Solid/Lit).
3413
3457
  function cellIsPlaceholder(cellCtx: any) {
3414
3458
  return !!(tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
3415
3459
  }
3416
- // groupSubRowCount: the number of underlying LEAF RECORDS under a group-header row (the count
3417
- // shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for MULTI-LEVEL
3418
- // grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40 records would show
3419
- // "North (2)". getLeafRows() returns all leaf descendants (the actual record count); keep the
3420
- // subRows fallback for safety. Single-level grouping is unchanged (getLeafRows == subRows when the
3421
- // children are already leaves).
3460
+ // groupSubRowCount: the number of underlying TRUE LEAF RECORDS under a group-header row (the
3461
+ // count shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for
3462
+ // MULTI-LEVEL grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40
3463
+ // records would show "North (2)" off subRows alone. row.getLeafRows() is table-core's
3464
+ // `flattenBy(row.subRows, d => d.subRows)` (ColumnGrouping.js / utils.js, verified against
3465
+ // @tanstack/table-core@8.21.3) — it pushes EVERY node it walks, INCLUDING intermediate group
3466
+ // nodes, so under multi-level grouping it over-counts by exactly the number of sub-group
3467
+ // nodes between the row and its true leaves (C4, quick 260906-cvo: North with 2 city
3468
+ // sub-groups + 3 records reported 5, not 3). `row.getIsGrouped()` is `!!row.groupingColumnId`
3469
+ // — precisely the predicate that separates a group node from a real record — so filter the
3470
+ // flattened list on it rather than hand-rolling a recursive `subRows` walk (getLeafRows()
3471
+ // already flattens; the defect is purely that it does not filter). Single-level grouping is
3472
+ // numerically UNCHANGED: a leaf row has no `subRows`, so no node was ever miscounted there.
3422
3473
  function groupSubRowCount(row: any) {
3423
- return row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
3474
+ if (row && row.getLeafRows) {
3475
+ const leaves = row.getLeafRows();
3476
+ let n = 0;
3477
+ for (let i = 0; i < leaves.length; i++) {
3478
+ const r = leaves[i];
3479
+ if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
3480
+ }
3481
+ return n;
3482
+ }
3483
+ return row && row.subRows ? row.subRows.length : 0;
3484
+ }
3485
+
3486
+ // ── B2 (quick 260906-afh) — group-header row #cell slot descriptor ─────────────────────
3487
+ // PROBLEM: table-core builds a flattened group-header row via
3488
+ // `createRow(table, id, leafRows[0].original, …)` (getGroupedRowModel.js), so `row.original`
3489
+ // on a group row IS the FIRST LEAF's record. The #cell scoped slot's existing 4 call sites
3490
+ // all bound `:row="row.original"` (or `wr.row.original`), so an AGGREGATED cell (a
3491
+ // non-grouping column on a group row — cellIsGrouped()===false, cellIsPlaceholder()===false,
3492
+ // so it takes the #cell r-else branch, same as any ordinary leaf cell) silently handed a
3493
+ // consumer template the WRONG record's fields (T-AFH-05). The grouped cell itself (the
3494
+ // group's own column, .rdt-group-value) has the same #cell binding and the same leak.
3495
+ //
3496
+ // FIX: `cellSlotRow(row)` returns a STABLE GROUP DESCRIPTOR — never `row.original` — for a
3497
+ // group-header row, and `row.original` unchanged for every other row. The descriptor is an
3498
+ // OBJECT, never `null` (T-AFH-06 — a consumer already reading `row.foo` would throw on
3499
+ // Vue/Angular if handed null).
3500
+ //
3501
+ // Descriptor cache (T-AFH-02/T-AFH-04): a null-prototype map keyed on `String(row.id)`,
3502
+ // invalidated wholesale whenever `$data.rowModelVer` (bumped by every refreshRowModel, the
3503
+ // SAME reactive re-derivation discipline visibleCellsFor/rowIsGrouped use) differs from the
3504
+ // generation the map was built for — so the descriptor's REFERENCE IDENTITY is stable
3505
+ // across every cell of one group row within a row-model generation (an unstable reference
3506
+ // would thrash the fine-grained targets, Solid/Lit) and refreshed the moment the grouping
3507
+ // changes. Group row ids are built from consumer grouping VALUES, so the map MUST be
3508
+ // null-prototype — a `__proto__` grouping value must not reach Object.prototype.
3509
+ let groupRowDescriptorCache: any = null;
3510
+ let groupRowDescriptorCacheVer: any = undefined;
3511
+ function groupRowDescriptor(row: any) {
3512
+ const ver = rowModelVer();
3513
+ if (!groupRowDescriptorCache || groupRowDescriptorCacheVer !== ver) {
3514
+ groupRowDescriptorCache = Object.create(null);
3515
+ groupRowDescriptorCacheVer = ver;
3516
+ }
3517
+ const key = String(row.id);
3518
+ let d = groupRowDescriptorCache[key];
3519
+ if (!d) {
3520
+ const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : '';
3521
+ const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
3522
+ d = {
3523
+ isGroupRow: true,
3524
+ groupId: row.id,
3525
+ groupingColumnId,
3526
+ groupingValue,
3527
+ leafCount: groupSubRowCount(row)
3528
+ };
3529
+ groupRowDescriptorCache[key] = d;
3530
+ }
3531
+ return d;
3532
+ }
3533
+ // cellSlotRow: the #cell slot's `:row=` value. A group-header row gets the stable
3534
+ // descriptor above (never the leaked first-leaf record); every other row is unchanged.
3535
+ function cellSlotRow(row: any) {
3536
+ return rowIsGrouped(row) ? groupRowDescriptor(row) : row ? row.original : null;
3424
3537
  }
3425
3538
  // groupingKeys: the live ordered grouping array — slot prop for the headless #groupBar + the
3426
3539
  // default styled-token reflection. Reads currentState() ($props.grouping ?? $data.groupingDefault),
@@ -3428,14 +3541,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
3428
3541
  function groupingKeys() {
3429
3542
  return currentState().grouping || [];
3430
3543
  }
3431
- // groupableColumns: the data columns OFFERED to the headless #groupBar (those whose Column/config
3432
- // `groupable` is not false) — `[{ id, label }]`. Excludes the chrome columns (select/expander are
3433
- // not in columnDefs()). The consumer builds any bar/drag UI from this; the component ships none.
3544
+ // groupableColumns: the data columns OFFERED to the headless #groupBar — `[{ id, label }]`.
3545
+ // Excludes the chrome columns (select/expander are not in columnDefs()). C5+C6 (quick
3546
+ // 260906-cvo): delegates the actual walk to collectGroupableLeafDefs (columnDefUtils.ts) —
3547
+ // a GROUP column (a `columns:` group's own entry) is NEVER offered (it has no accessor and
3548
+ // cannot produce a grouping value, regardless of any `groupable` flag on it), and every
3549
+ // groupable NESTED leaf (a `columns:` group child, opted out via `groupable: false` like any
3550
+ // top-level leaf) IS offered, in declaration order. The consumer builds any bar/drag UI from
3551
+ // this; the component ships none.
3434
3552
  function groupableColumns() {
3435
3553
  const out = [];
3436
- const defs = columnDefs();
3554
+ const defs = collectGroupableLeafDefs(columnDefs());
3437
3555
  for (const d of defs as any) {
3438
- if (!d || d.groupable === false) continue;
3439
3556
  out.push({
3440
3557
  id: d.id,
3441
3558
  label: d.header != null ? d.header : d.id
@@ -4450,11 +4567,18 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
4450
4567
  return;
4451
4568
  }
4452
4569
  // ── C2 (phase 63 wave-8): Enter on a GROUP-HEADER cell toggles that group's collapse/
4453
- // expand (APG treegrid). A group cell is NON-editable (isActiveCellEditable=false, the
4454
- // verified invariant) so it never hits the edit branches above and would otherwise fall to
4455
- // enterControl() — which merely FOCUSES the group-toggle button (requiring a second key).
4456
- // Route it to the SAME onToggleExpand path the chevron uses (group rows ride the expand
4457
- // model) so one Enter toggles the group. Body cells only (a header-active Enter is unchanged);
4570
+ // expand (APG treegrid). C1 (quick 260906-cvo) CORRECTION: the comment here used to claim
4571
+ // "a group cell is non-editable" as "the verified invariant" — that was FALSE AS WRITTEN.
4572
+ // It held only for the grouping column's OWN cell (cellIsGrouped's branch renders before
4573
+ // the editor branch); a non-grouping EDITABLE column's cell on a group row fell straight
4574
+ // through the branch chain to isEditing(...) and opened an editor (the C1 defect this
4575
+ // quick task closed). What is now TRUE, and WHY: isActiveCellEditable() carries an
4576
+ // explicit rowIndexIsGrouped(...) row guard (filterPaginationRowChrome.rzts), so EVERY
4577
+ // editable column's cell on a group row is non-editable — that is what makes the four
4578
+ // edit-entry branches above fall through to this one, not an incidental property of the
4579
+ // grouping column alone. Route it to the SAME onToggleExpand path the chevron uses (group
4580
+ // rows ride the expand model) so one Enter toggles the group. Body cells only (a
4581
+ // header-active Enter is unchanged);
4458
4582
  // ($data.rows || [])[$data.activeRow] is the active flattened row (page-relative non-virtual /
4459
4583
  // full-model virtual — both index $data.rows). Placed BEFORE the reserved enterControl branch.
4460
4584
  else if (key === 'Enter' && !activeIsHeader() && rowIsGrouped((rows() || [])[activeRow()])) {
@@ -5243,6 +5367,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5243
5367
  // rangeToTsv(): serialize the current range to TSV — rows joined by '\n', cells by '\t',
5244
5368
  // reading each cell's value off the visible model by index (cellValueAt). A single active
5245
5369
  // cell (no range) serializes that one cell. Each field is B10-escaped. Pure read — never writes.
5370
+ // C2 (quick 260906-cvo) — DELIBERATE NON-CHANGE: this is a READ, unlike applyGridToRange's
5371
+ // write funnel, and stays UNGUARDED for a group-header row on purpose. cellValueAt reads the
5372
+ // column's DISPLAYED aggregate value on a group row (table-core's cell.getValue()), which is
5373
+ // exactly what the user sees — serializing it is correct behavior, not a leak. Do not "fix"
5374
+ // this asymmetry; it is intentional.
5246
5375
  function rangeToTsv() {
5247
5376
  const box = normalizedRange();
5248
5377
  const r0 = box ? box.r0 : activeRow();
@@ -5296,11 +5425,22 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5296
5425
  for (let gr = 0; gr < grid.length; gr++) {
5297
5426
  const r = originRow + gr;
5298
5427
  if (r > maxRow) break;
5428
+ // C2 (quick 260906-cvo) — hoisted ONCE per ROW (never per cell): a group-header row's
5429
+ // `original` is table-core's `leafRows[0].original` (a synthetic row), so a write
5430
+ // resolved through it silently lands on an unrelated record. This ONE placement closes
5431
+ // pasteRange, cutRange, clearActiveRange AND fillRange — all four build a grid and hand
5432
+ // it to this single funnel, so guarding the four callers individually would be four
5433
+ // chances to miss one.
5434
+ const rowGrouped = rowIndexIsGrouped(r);
5299
5435
  const cols = grid[gr] || [];
5300
5436
  for (let gc = 0; gc < cols.length; gc++) {
5301
5437
  const c = originCol + gc;
5302
5438
  if (c > maxCol) break;
5439
+ // A group-row cell still counts toward `total` — it is a skipped TARGET, exactly like
5440
+ // a non-editable or validator-rejected cell — so the "N of M cells pasted" / "no cells
5441
+ // pasted" announce keeps its existing semantics (never silently under-reports M).
5303
5442
  total = total + 1;
5443
+ if (rowGrouped) continue;
5304
5444
  const colId = columnIdAt(r, c);
5305
5445
  if (colId == null || !columnEditable(colId)) continue;
5306
5446
  const rowObj = rowOriginalAt(r);
@@ -5806,7 +5946,13 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5806
5946
  // isActiveCellEditable: the active cell sits in an editable column AND is a body cell
5807
5947
  // (req-1). Gates the F2/Enter/printable edit-entry branches in onGridKeyDown; a
5808
5948
  // non-editable active cell falls through to the reserved enterControl path.
5949
+ // C1 (quick 260906-cvo) — Layer 1: a group-header row is NEVER editable, regardless of
5950
+ // column. This is the single highest-value guard in the cluster: it closes Shift+F2, the
5951
+ // checkbox toggle, and the F2/Enter/printable edit-entry branches in ONE edit (they are all
5952
+ // `else if` branches gated on this predicate), AND restores fallthrough so Enter on a group
5953
+ // row reaches the group-toggle branch below them instead of opening an editor.
5809
5954
  function isActiveCellEditable() {
5955
+ if (rowIndexIsGrouped(activeRow())) return false;
5810
5956
  const colId = activeCellColumnId();
5811
5957
  return colId != null && columnEditable(colId);
5812
5958
  }
@@ -5824,6 +5970,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5824
5970
  // non-virtual, wr.vi.index virtual).
5825
5971
  function isEditing(rowIndex: any, colIndex: any) {
5826
5972
  if (editVer() < 0) return false;
5973
+ // C1 (quick 260906-cvo) — Layer 6: the ONLY layer whose reachability does not depend on a
5974
+ // keystroke. Editing state is INDEX-based over the visible model, and nothing clears it
5975
+ // when the row model re-derives — a consumer that changes the grouping model WHILE a cell
5976
+ // editor is open can move a group-header row under the index $data.editingRow/
5977
+ // editingRowIndex still holds, rendering the editor on a group row with no beginEdit
5978
+ // involved. Placed AFTER the editVer read (preserves the Svelte/Solid re-derivation
5979
+ // discipline the header comment above documents).
5980
+ if (rowIndexIsGrouped(rowIndex)) return false;
5827
5981
  if (editingRowIndex() != null && editingRowIndex() === rowIndex) {
5828
5982
  const colId = columnIdAt(rowIndex, colIndex);
5829
5983
  return colId != null && columnEditable(colId);
@@ -5998,6 +6152,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5998
6152
  // $data) so a Tab-advance that just setState'd activeRow/Col works on React. Clears any
5999
6153
  // prior invalid state. Focus moves into the editor.
6000
6154
  function beginEdit(rowIndex: any, colIndex: any, seed: any) {
6155
+ // C1 (quick 260906-cvo) — Layer 2: closes the `onGridClick` (singleClickEdit) path, which
6156
+ // calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
6157
+ // does not run on that path at all.
6158
+ if (rowIndexIsGrouped(rowIndex)) return;
6001
6159
  const colId = columnIdAt(rowIndex, colIndex);
6002
6160
  if (colId == null || !columnEditable(colId)) return;
6003
6161
  // A new edit session starts — reset the sync idempotency latch so THIS session's eventual
@@ -6204,6 +6362,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6204
6362
  // editor:'checkbox' columns only (Space/Enter/F2), full-row edit mode is unaffected (the
6205
6363
  // editingRowIndex early return in onGridKeyDown already excludes it).
6206
6364
  function toggleActiveBooleanCell() {
6365
+ // C1 (quick 260906-cvo) — Layer 4: the only write funnel bypassing both beginEdit/
6366
+ // commitEdit and applyGridToRange. Reachable today only through the Layer-1-gated
6367
+ // checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
6368
+ // write ever targets a group row" a property of the write funnels themselves.
6369
+ if (rowIndexIsGrouped(activeRow())) return;
6207
6370
  const colId = columnIdAt(activeRow(), activeColIndex());
6208
6371
  if (colId == null || !columnEditable(colId)) return;
6209
6372
  const rowList = rows() || [];
@@ -6331,6 +6494,11 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
6331
6494
  function beginRowEdit(row: any) {
6332
6495
  const rowIndex = rowIndexOf(row);
6333
6496
  if (rowIndex < 0) return;
6497
+ // C1 (quick 260906-cvo) — Layer 5: a second, independent editing-state funnel that takes a
6498
+ // row OBJECT from its caller. Reachable today only via Shift+F2 (Layer 1), so also
6499
+ // redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
6500
+ // and the natural call site for a future #rowActions "Edit" drop-in button.
6501
+ if (rowIndexIsGrouped(rowIndex)) return;
6334
6502
  const editable = editableColumnsForRow(rowIndex);
6335
6503
  if (editable.length === 0) return;
6336
6504
  // A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
@@ -7199,20 +7367,20 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7199
7367
  <Key each={visibleCellsFor(row()) as readonly any[]} by={(cell) => cell.id}>{(cell) => <td class={"rdt-td" + " " + rozieClass({ 'rdt-select-td': isSelectColumn(cell().column.id), 'rdt-expander-td': isExpanderColumn(cell().column.id), 'rdt-in-range': inRange(rowIndexOf(row()), colIndexOf(row(), cell())), 'rdt-cell-active': isActiveCell(String(rowIndexOf(row())), colIndexOf(row(), cell())) })} role={rozieAttr(cellRole())} data-col={rozieAttr(cell().column.id)} data-grid-cell="" data-row={rozieAttr(rowIndexOf(row()))} data-col-index={rozieAttr(colIndexOf(row(), cell()))} tabIndex={rozieAttr(cellTabindex(String(rowIndexOf(row())), colIndexOf(row(), cell())))} style={parseInlineStyle(bodyCellStyle(row(), cell().column.id))} aria-invalid={rozieAttr(cellAriaInvalid(rowIndexOf(row()), colIndexOf(row(), cell())))} data-in-range={rozieAttr(inRange(rowIndexOf(row()), colIndexOf(row(), cell())) ? 'true' : null)} data-agg-cell={rozieAttr(cellIsAggregated(cell()) ? cell().column.id : null)} data-rozie-s-d5dcab4c="">
7200
7368
 
7201
7369
  {<Show when={isExpanderColumn(cell().column.id)} fallback={<Show when={isSelectColumn(cell().column.id)} fallback={<Show when={cellIsGrouped(cell())} fallback={<Show when={isEditing(rowIndexOf(row()), colIndexOf(row(), cell()))} fallback={<Show when={cellIsPlaceholder(cell())} fallback={<span class={"rdt-cell-value"} data-rozie-s-d5dcab4c="">
7202
- {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: row().original, value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7370
+ {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7203
7371
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7204
7372
  {<Show when={hasEditorSlot(cell().column.id)} fallback={<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<input type="text" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><input type="checkbox" data-editing-cell="" class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
7205
7373
  <Key each={editorOptionsOf(cell().column.id) as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-d5dcab4c="">{rozieDisplay(opt().label)}</option>}</Key>
7206
7374
  </select></Show>}><input type="number" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7207
- {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: row().original, value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, rowIndexOf(row())) })}
7375
+ {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, rowIndexOf(row())) })}
7208
7376
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7209
7377
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(row())} aria-label={rozieAttr(rowIsExpanded(row()) ? 'Collapse group' : 'Expand group')} class={"rdt-expander rdt-group-toggle"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(row(), $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(row()) ? '▾' : '▸')}</button>
7210
7378
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
7211
- {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: row().original, value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7379
+ {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(row()), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7212
7380
  </span>
7213
7381
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{rozieDisplay('(' + groupSubRowCount(row()) + ')')}</span>
7214
7382
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7215
- {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ row: row().original, checked: rowIsSelected(row()), toggle: e => onToggleRow(row(), e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(row())} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(row(), $event); }} data-rozie-s-d5dcab4c="" />}
7383
+ {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ row: cellSlotRow(row()), checked: rowIsSelected(row()), toggle: e => onToggleRow(row(), e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(row())} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(row(), $event); }} data-rozie-s-d5dcab4c="" />}
7216
7384
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7217
7385
  {<Show when={rowCanExpand(row())}><button type="button" data-expander="" aria-expanded={!!rowIsExpanded(row())} aria-label={rozieAttr(rowIsExpanded(row()) ? 'Collapse row' : 'Expand row')} class={"rdt-expander"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(row(), $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(row()) ? '▾' : '▸')}</button></Show>}</span></Show>}{<Show when={isFillHandleCell(rowIndexOf(row()), colIndexOf(row(), cell()))}><span data-fill-handle="" data-testid="fill-handle" aria-hidden="true" class={"rdt-fill-handle"} onPointerDown={($event: PointerEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c="" /></Show>}</td>}</Key>
7218
7386
  </tr>
@@ -7275,20 +7443,20 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7275
7443
  {<Show when={colsWindowed()}><td class={"rdt-col-spacer"} aria-hidden="true" style={parseInlineStyle('width:' + colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c="" /></Show>}<Key each={windowedCells(wr().row) as readonly any[]} by={(cell) => cell.id}>{(cell) => <td class={"rdt-td" + " " + rozieClass({ 'rdt-select-td': isSelectColumn(cell().column.id), 'rdt-expander-td': isExpanderColumn(cell().column.id), 'rdt-in-range': inRange(wr().vi.index, colIndexOf(wr().row, cell())), 'rdt-cell-active': isActiveCell(String(wr().vi.index), colIndexOf(wr().row, cell())) })} role={rozieAttr(cellRole())} data-col={rozieAttr(cell().column.id)} data-grid-cell="" data-row={rozieAttr(wr().vi.index)} data-col-index={rozieAttr(colIndexOf(wr().row, cell()))} tabIndex={rozieAttr(cellTabindex(String(wr().vi.index), colIndexOf(wr().row, cell())))} style={parseInlineStyle(bodyCellStyle(wr().row, cell().column.id))} aria-invalid={rozieAttr(cellAriaInvalid(wr().vi.index, colIndexOf(wr().row, cell())))} data-in-range={rozieAttr(inRange(wr().vi.index, colIndexOf(wr().row, cell())) ? 'true' : null)} data-agg-cell={rozieAttr(cellIsAggregated(cell()) ? cell().column.id : null)} data-rozie-s-d5dcab4c="">
7276
7444
 
7277
7445
  {<Show when={isExpanderColumn(cell().column.id)} fallback={<Show when={isSelectColumn(cell().column.id)} fallback={<Show when={cellIsGrouped(cell())} fallback={<Show when={isEditing(wr().vi.index, colIndexOf(wr().row, cell()))} fallback={<Show when={cellIsPlaceholder(cell())} fallback={<span class={"rdt-cell-value"} data-rozie-s-d5dcab4c="">
7278
- {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: wr().row.original, value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7446
+ {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7279
7447
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7280
7448
  {<Show when={hasEditorSlot(cell().column.id)} fallback={<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<input type="text" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><input type="checkbox" data-editing-cell="" class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLSelectElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="">
7281
7449
  <Key each={editorOptionsOf(cell().column.id) as readonly any[]} by={(opt) => opt.value}>{(opt) => <option value={rozieAttr(opt().value)} data-rozie-s-d5dcab4c="">{rozieDisplay(opt().label)}</option>}</Key>
7282
7450
  </select></Show>}><input type="number" data-editing-cell="" class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7283
- {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: wr().row.original, value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, wr().vi.index) })}
7451
+ {(_props.editorSlot ?? _props.slots?.['editor'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: editorValueFor(cell().column.id), commit: editorCommitFor(cell().column.id), cancel: editorCancelFor(), autofocus: editorAutofocusFor(cell().column.id, wr().vi.index) })}
7284
7452
  </span></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7285
7453
  <button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={rozieAttr(rowIsExpanded(wr().row) ? 'Collapse group' : 'Expand group')} class={"rdt-expander rdt-group-toggle"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(wr().row, $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(wr().row) ? '▾' : '▸')}</button>
7286
7454
  <span class={"rdt-group-value"} data-rozie-s-d5dcab4c="">
7287
- {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: wr().row.original, value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7455
+ {(_props.cellSlot ?? _props.slots?.['cell'])?.({ columnId: cell().column.id, column: cell().column, row: cellSlotRow(wr().row), value: cell().getValue() }) ?? rozieDisplay(cell().getValue())}
7288
7456
  </span>
7289
7457
  <span class={"rdt-group-count"} data-rozie-s-d5dcab4c="">{rozieDisplay('(' + groupSubRowCount(wr().row) + ')')}</span>
7290
7458
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7291
- {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ row: wr().row.original, checked: rowIsSelected(wr().row), toggle: e => onToggleRow(wr().row, e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(wr().row)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(wr().row, $event); }} data-rozie-s-d5dcab4c="" />}
7459
+ {(_props.selectCellSlot ?? _props.slots?.['selectCell'])?.({ row: cellSlotRow(wr().row), checked: rowIsSelected(wr().row), toggle: e => onToggleRow(wr().row, e) }) ?? <input type="checkbox" aria-label="Select row" class={"rdt-select-row"} checked={rowIsSelected(wr().row)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onToggleRow(wr().row, $event); }} data-rozie-s-d5dcab4c="" />}
7292
7460
  </span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
7293
7461
  {<Show when={rowCanExpand(wr().row)}><button type="button" data-expander="" aria-expanded={!!rowIsExpanded(wr().row)} aria-label={rozieAttr(rowIsExpanded(wr().row) ? 'Collapse row' : 'Expand row')} class={"rdt-expander"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onToggleExpand(wr().row, $event); }} data-rozie-s-d5dcab4c="">{rozieDisplay(rowIsExpanded(wr().row) ? '▾' : '▸')}</button></Show>}</span></Show>}{<Show when={isFillHandleCell(wr().vi.index, colIndexOf(wr().row, cell()))}><span data-fill-handle="" data-testid="fill-handle" aria-hidden="true" class={"rdt-fill-handle"} onPointerDown={($event: PointerEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c="" /></Show>}</td>}</Key>
7294
7462
 
@@ -32,4 +32,80 @@ const wrapAggregationFn = (fn: any) => {
32
32
  }
33
33
  }
34
34
 
35
- export { isSafeKey, wrapAggregationFn }
35
+ // ── B1 (quick 260906-afh) — nested group leaf def index ─────────────────────────────
36
+ // LOOKUP-ONLY. This map is never fed to table-core (it does not replace/reshape the
37
+ // columnDefs() array table-core consumes for createTable/setOptions — that stays
38
+ // byte-identical); it exists purely so `defFor(colId)` (columnChrome.rzts) can resolve
39
+ // a NESTED leaf's def (a `columns:` group child) in O(1), the same as a top-level leaf.
40
+ //
41
+ // collectNestedDefs: depth-first walk of a `columns` array (each entry may itself carry
42
+ // a nested `columns` array — 3+ levels deep is supported). Writes `out[String(d.id)] = d`
43
+ // ONLY when the id is ABSENT from `out` — first-write-wins so a caller can pre-seed `out`
44
+ // with the higher-precedence top-level defs and this pass only ever fills GAPS.
45
+ const collectNestedDefs = (list: any, out: any) => {
46
+ if (!Array.isArray(list)) return
47
+ for (const d of list) {
48
+ if (!d || d.id == null) continue
49
+ const id = String(d.id)
50
+ if (!(id in out)) out[id] = d
51
+ if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out)
52
+ }
53
+ }
54
+
55
+ // indexDefsById: builds a null-prototype `{ id: def }` map from a `columnDefs()`-shaped
56
+ // array in TWO passes. Pass 1 registers every TOP-LEVEL def unconditionally — so any id
57
+ // that already resolves at HEAD (a top-level leaf OR a top-level group's OWN id) keeps
58
+ // resolving to the EXACT SAME def, the byte-identical-lookup guarantee for every
59
+ // pre-existing caller. Pass 2 walks each top-level entry's `columns` (if any) via
60
+ // collectNestedDefs, so a nested leaf only fills a GAP — it can never shadow a
61
+ // same-named top-level id (top-level wins regardless of array order). Object.create(null)
62
+ // (T-AFH-01): a consumer column id of "__proto__"/"constructor" cannot pollute
63
+ // Object.prototype through this map — mirrors the existing `byId` T-48-PP guard in
64
+ // columnBuilders.rzts.
65
+ const indexDefsById = (defs: any): any => {
66
+ const out = Object.create(null)
67
+ if (!Array.isArray(defs)) return out
68
+ for (const d of defs) {
69
+ if (!d || d.id == null) continue
70
+ out[String(d.id)] = d
71
+ }
72
+ for (const d of defs) {
73
+ if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out)
74
+ }
75
+ return out
76
+ }
77
+
78
+ // ── C5+C6 (quick 260906-cvo) — groupable-leaf collection for the #groupBar ──────────────
79
+ // C5: buildConfigDef's GROUP branch (columnBuilders.rzts) returns `{ id, header, columns }`
80
+ // with NO `groupable` key and no accessor, so the OLD `groupableColumns()` loop's
81
+ // `d.groupable === false` skip never fired for a group column — it was wrongly OFFERED to
82
+ // the bar even though a group column can never produce a grouping value.
83
+ // C6: that same loop walked TOP-LEVEL `columnDefs()` entries only, so a genuinely groupable
84
+ // NESTED leaf (a `columns:` group child) was never offered at all.
85
+ //
86
+ // collectGroupableLeafDefs: a DEDICATED depth-first walk, NOT a reuse of `indexDefsById`.
87
+ // `indexDefsById` is a LOOKUP index — it deliberately registers group columns too (a group
88
+ // id must keep resolving for `defFor`), and its insertion order is all-top-level-first-then-
89
+ // nested, which would both offer group columns AND hand the group bar an order that does not
90
+ // match the rendered header order. This walk instead: recurses into a `columns:` array
91
+ // WITHOUT ever pushing the group entry itself (closes C5 — a group column has no accessor
92
+ // and cannot be a grouping target, so an explicit `groupable: true` on a group entry is
93
+ // meaningless and is likewise never pushed); pushes every other (leaf) entry unless it opts
94
+ // out via `groupable === false` (closes C6 — nested leaves are now reachable); walks in
95
+ // DECLARATION order, depth-first, matching the rendered header order.
96
+ const collectGroupableLeafDefs = (defs: any): any[] => {
97
+ const out: any[] = []
98
+ if (!Array.isArray(defs)) return out
99
+ for (const d of defs) {
100
+ if (!d) continue
101
+ if (Array.isArray(d.columns)) {
102
+ out.push(...collectGroupableLeafDefs(d.columns))
103
+ continue
104
+ }
105
+ if (d.groupable === false) continue
106
+ out.push(d)
107
+ }
108
+ return out
109
+ }
110
+
111
+ export { isSafeKey, wrapAggregationFn, collectNestedDefs, indexDefsById, collectGroupableLeafDefs }