@rozie-ui/data-table-lit 0.4.0 → 0.5.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/dist/index.d.mts CHANGED
@@ -6,14 +6,14 @@ declare class DataTable extends DataTable_base {
6
6
  static shadowRootOptions: ShadowRootInit;
7
7
  static styles: _$lit.CSSResult;
8
8
  /**
9
- * The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `r-model:data` (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
9
+ * The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
10
10
  * @example
11
11
  * <rozie-data-table .data=${rows} @data-change=${…} .columns=${cols}></rozie-data-table>
12
12
  */
13
13
  _data_attr: any[];
14
14
  private _dataControllable;
15
15
  /**
16
- * Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union).
16
+ * Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
17
17
  */
18
18
  columns: any[];
19
19
  /**
@@ -53,7 +53,7 @@ declare class DataTable extends DataTable_base {
53
53
  */
54
54
  pageCount: number | null;
55
55
  /**
56
- * Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Bind `:expandable="true"` (a bare attr only coerces on Vue+Lit).
56
+ * Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit.
57
57
  */
58
58
  expandable: boolean;
59
59
  /**
@@ -70,7 +70,7 @@ declare class DataTable extends DataTable_base {
70
70
  */
71
71
  groupable: boolean;
72
72
  /**
73
- * `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `r-model:grouping` (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
73
+ * `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
74
74
  */
75
75
  _grouping_attr: any[] | null;
76
76
  private _groupingControllable;
@@ -104,7 +104,7 @@ declare class DataTable extends DataTable_base {
104
104
  */
105
105
  stickyHeader: boolean;
106
106
  /**
107
- * `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` (GA since Phase 63) — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
107
+ * `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
108
108
  */
109
109
  interactionMode: string;
110
110
  /**
@@ -305,6 +305,8 @@ declare class DataTable extends DataTable_base {
305
305
  expandedTouched: boolean;
306
306
  groupingActiveDefault: () => boolean;
307
307
  effectiveColumnPinning: () => any;
308
+ pinSeedApplied: boolean;
309
+ seedColumnPinning: () => void;
308
310
  currentState: () => any;
309
311
  currentData: () => any;
310
312
  buildConfigDef: (c: any) => {
@@ -385,6 +387,7 @@ declare class DataTable extends DataTable_base {
385
387
  onColumnSizingChangeCb: (updater: any) => void;
386
388
  onColumnOrderChangeCb: (updater: any) => void;
387
389
  onColumnPinningChangeCb: (updater: any) => void;
390
+ columnSizingInfoSync: any;
388
391
  onColumnSizingInfoChangeCb: (updater: any) => void;
389
392
  resolveVirtual: () => "rows" | "columns" | "both" | "off";
390
393
  rowsWindowed: () => boolean;
@@ -779,7 +782,7 @@ declare class Column extends Column_base {
779
782
  */
780
783
  filterable: boolean;
781
784
  /**
782
- * Pin side: `''` (unpinned) | `'left'` | `'right'`. Reserved metadata carried into the parent's column pinning state.
785
+ * Initial pin side: `''` (unpinned) | `'left'` | `'right'`. Applied once as the table's starting `columnPinning` state, so `getIsPinned()` reports it and the column joins the matching sticky rail — which also reorders it, since visible cells are ordered `[left-pinned, center, right-pinned]`. Ignored if the consumer has already pinned something; an interactive unpin is never re-applied.
783
786
  */
784
787
  pinned: string;
785
788
  /**
@@ -803,7 +806,7 @@ declare class Column extends Column_base {
803
806
  */
804
807
  editable: boolean;
805
808
  /**
806
- * Built-in editor type for this column: `'text'` | `'number'` | `'select'` | `'checkbox'`. Ignored when a custom `#editor` scoped slot handles the column. Default `'text'`.
809
+ * Editor type for this column: `'text'` | `'number'` | `'select'` | `'checkbox'` (built-in inputs), or `'custom'` to hand rendering to the `#editor` scoped slot (or a per-column `editor-<columnId>` fill). Default `'text'`. **`'custom'` is the gate:** a column left on a built-in type ignores any `#editor` fill and renders the built-in input. Requires `editable: true` — and, for any pointer or keyboard edit entry, `interactionMode="grid"` on the table.
807
810
  */
808
811
  editor: string;
809
812
  /**
package/dist/index.mjs CHANGED
@@ -439,6 +439,34 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
439
439
  left: rail.concat(deduped)
440
440
  };
441
441
  };
442
+ this.pinSeedApplied = false;
443
+ this.seedColumnPinning = () => {
444
+ if (this.pinSeedApplied) return;
445
+ const live = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
446
+ const isRealPin = (id) => id !== this.SELECT_COL_ID && id !== this.EXPANDER_COL_ID;
447
+ if ((live && live.left ? live.left : []).filter(isRealPin).length > 0 || (live && live.right ? live.right : []).filter(isRealPin).length > 0) {
448
+ this.pinSeedApplied = true;
449
+ return;
450
+ }
451
+ const defs = this.columnDefs();
452
+ if (!defs.length) return;
453
+ const left = [];
454
+ const right = [];
455
+ for (let i = 0; i < defs.length; i++) {
456
+ const def = defs[i];
457
+ if (!def) continue;
458
+ if (def.pinned === "left") left.push(def.id);
459
+ else if (def.pinned === "right") right.push(def.id);
460
+ }
461
+ if (!left.length && !right.length) return;
462
+ this.pinSeedApplied = true;
463
+ const seeded = {
464
+ left,
465
+ right
466
+ };
467
+ this._columnPinningDefault.value = seeded;
468
+ this._columnPinningControllable.write(seeded);
469
+ };
442
470
  this.currentState = () => ({
443
471
  sorting: this.sorting != null ? this.sorting : this._sortingDefault.value,
444
472
  globalFilter: this.globalFilter != null ? this.globalFilter : this._globalFilterDefault.value,
@@ -835,9 +863,12 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
835
863
  this.onColumnPinningChangeCb = (updater) => {
836
864
  this.writeColumnPinning(applyUpdater(updater, this.currentState().columnPinning));
837
865
  };
866
+ this.columnSizingInfoSync = null;
838
867
  this.onColumnSizingInfoChangeCb = (updater) => {
839
- const next = applyUpdater(updater, this._columnSizingInfo.value);
840
- this._columnSizingInfo.value = next != null ? next : this._columnSizingInfo.value;
868
+ const next = applyUpdater(updater, this.columnSizingInfoSync != null ? this.columnSizingInfoSync : this._columnSizingInfo.value);
869
+ if (next == null) return;
870
+ this.columnSizingInfoSync = next;
871
+ this._columnSizingInfo.value = next;
841
872
  };
842
873
  this.resolveVirtual = () => {
843
874
  const v = this.virtual;
@@ -2976,6 +3007,11 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
2976
3007
  const tryFocus = () => {
2977
3008
  const el = this.resolveCellEl(String(row), col);
2978
3009
  if (el) {
3010
+ const ae = this.gridRoot && this.gridRoot.getRootNode ? this.gridRoot.getRootNode().activeElement : null;
3011
+ if (ae && ae !== el && this.gridRoot.contains && this.gridRoot.contains(ae) && ae.closest) {
3012
+ const aeCell = ae.closest("[data-grid-cell]");
3013
+ if (aeCell && aeCell !== el) return;
3014
+ }
2979
3015
  el.focus();
2980
3016
  return;
2981
3017
  }
@@ -3596,6 +3632,34 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
3596
3632
  static {
3597
3633
  this.styles = css`
3598
3634
  :host{display:contents}
3635
+ :host {
3636
+ --rdt-font: var(--rozie-data-table-font);
3637
+ --rdt-color: var(--rozie-data-table-fg);
3638
+ --rdt-cell-padding: var(--rozie-data-table-cell-padding);
3639
+ --rdt-border: 1px solid var(--rozie-data-table-border-color);
3640
+ --rdt-header-bg: var(--rozie-data-table-header-bg);
3641
+ --rdt-header-weight: var(--rozie-data-table-header-weight);
3642
+ --rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
3643
+ --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
3644
+ --rdt-filter-radius: var(--rozie-data-table-radius);
3645
+ --rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
3646
+ --rdt-page-btn-radius: var(--rozie-data-table-radius);
3647
+ --rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
3648
+ --rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
3649
+ --rdt-page-size-radius: var(--rozie-data-table-radius);
3650
+ --rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
3651
+ --rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
3652
+ --rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
3653
+ --rdt-pin-btn-radius: var(--rozie-data-table-radius);
3654
+ --rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
3655
+ --rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color);
3656
+ --rdt-colvis-summary-radius: var(--rozie-data-table-radius);
3657
+ --rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color);
3658
+ --rdt-colvis-menu-radius: var(--rozie-data-table-radius);
3659
+ --rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg);
3660
+ --rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow);
3661
+ --rdt-select-accent: var(--rozie-data-table-select-accent);
3662
+ }
3599
3663
  .rozie-data-table[data-rozie-s-d5dcab4c] {
3600
3664
  border-collapse: collapse;
3601
3665
  width: 100%;
@@ -4107,6 +4171,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
4107
4171
  return;
4108
4172
  }
4109
4173
  (() => {
4174
+ this.seedColumnPinning();
4110
4175
  this.reFeed();
4111
4176
  this.maybeClearHistoryOnExternalSwap();
4112
4177
  })();
@@ -4146,6 +4211,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
4146
4211
  }))(this));
4147
4212
  }));
4148
4213
  this._dataDefault.value = this.data || [];
4214
+ this.seedColumnPinning();
4149
4215
  this.table = createTable({
4150
4216
  data: this.currentData(),
4151
4217
  columns: this.tableColumns(),
@@ -4240,6 +4306,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
4240
4306
  }
4241
4307
  if (this.colsWindowed()) {
4242
4308
  if (!(this.gridScrollEl ? this.gridScrollEl.clientWidth : 0)) console.warn("[rozie-data-table] virtual is on for columns but the scroll container has no bounded width; set a CSS width on an ancestor so the column window can be measured");
4309
+ this.bumpWindowVer();
4243
4310
  }
4244
4311
  };
4245
4312
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozie-ui/data-table-lit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "src"
46
46
  ],
47
47
  "dependencies": {
48
- "@rozie/runtime-lit": "0.7.3"
48
+ "@rozie/runtime-lit": "^0.7.4"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "lit": "^3.2",
package/src/Column.ts CHANGED
@@ -34,7 +34,7 @@ export default class Column extends SignalWatcher(LitElement) {
34
34
  */
35
35
  @property({ type: Boolean, reflect: true }) filterable: boolean = false;
36
36
  /**
37
- * Pin side: `''` (unpinned) | `'left'` | `'right'`. Reserved metadata carried into the parent's column pinning state.
37
+ * Initial pin side: `''` (unpinned) | `'left'` | `'right'`. Applied once as the table's starting `columnPinning` state, so `getIsPinned()` reports it and the column joins the matching sticky rail — which also reorders it, since visible cells are ordered `[left-pinned, center, right-pinned]`. Ignored if the consumer has already pinned something; an interactive unpin is never re-applied.
38
38
  */
39
39
  @property({ type: String, reflect: true }) pinned: string = '';
40
40
  /**
@@ -58,7 +58,7 @@ export default class Column extends SignalWatcher(LitElement) {
58
58
  */
59
59
  @property({ type: Boolean, reflect: true }) editable: boolean = false;
60
60
  /**
61
- * Built-in editor type for this column: `'text'` | `'number'` | `'select'` | `'checkbox'`. Ignored when a custom `#editor` scoped slot handles the column. Default `'text'`.
61
+ * Editor type for this column: `'text'` | `'number'` | `'select'` | `'checkbox'` (built-in inputs), or `'custom'` to hand rendering to the `#editor` scoped slot (or a per-column `editor-<columnId>` fill). Default `'text'`. **`'custom'` is the gate:** a column left on a built-in type ignores any `#editor` fill and renders the built-in input. Requires `editable: true` — and, for any pointer or keyboard edit entry, `interactionMode="grid"` on the table.
62
62
  */
63
63
  @property({ type: String, reflect: true }) editor: string = 'text';
64
64
  /**
package/src/DataTable.ts CHANGED
@@ -130,6 +130,34 @@ export default class DataTable extends SignalWatcher(LitElement) {
130
130
 
131
131
  static styles = css`
132
132
  :host{display:contents}
133
+ :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);
160
+ }
133
161
  .rozie-data-table[data-rozie-s-d5dcab4c] {
134
162
  border-collapse: collapse;
135
163
  width: 100%;
@@ -467,14 +495,14 @@ export default class DataTable extends SignalWatcher(LitElement) {
467
495
  `;
468
496
 
469
497
  /**
470
- * The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `r-model:data` (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
498
+ * The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
471
499
  * @example
472
500
  * <rozie-data-table .data=${rows} @data-change=${…} .columns=${cols}></rozie-data-table>
473
501
  */
474
502
  @property({ type: Array, attribute: 'data' }) _data_attr!: any[];
475
503
  private _dataControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'data-change', defaultValue: [], initialControlledValue: undefined });
476
504
  /**
477
- * Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union).
505
+ * Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
478
506
  */
479
507
  @property({ type: Array }) columns: any[] = [];
480
508
  /**
@@ -520,7 +548,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
520
548
  */
521
549
  @property({ type: Number, reflect: true }) pageCount: number | null = null;
522
550
  /**
523
- * Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Bind `:expandable="true"` (a bare attr only coerces on Vue+Lit).
551
+ * Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit.
524
552
  */
525
553
  @property({ type: Boolean, reflect: true }) expandable: boolean = false;
526
554
  /**
@@ -537,7 +565,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
537
565
  */
538
566
  @property({ type: Boolean, reflect: true }) groupable: boolean = false;
539
567
  /**
540
- * `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `r-model:grouping` (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
568
+ * `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
541
569
  */
542
570
  @property({ type: Array, attribute: 'grouping' }) _grouping_attr: any[] | null = null;
543
571
  private _groupingControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'grouping-change', defaultValue: null, initialControlledValue: undefined });
@@ -577,7 +605,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
577
605
  */
578
606
  @property({ type: Boolean, reflect: true }) stickyHeader: boolean = false;
579
607
  /**
580
- * `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` (GA since Phase 63) — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
608
+ * `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
581
609
  */
582
610
  @property({ type: String, reflect: true }) interactionMode: string = 'table';
583
611
  /**
@@ -912,6 +940,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
912
940
  // (Consumers memoize the array as with $props.data/$props.sorting; the uncontrolled
913
941
  // <Column>-children path leaves $props.columns undefined — a stable no-op getter.)
914
942
  this.columns, this._colReg.value])(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => {
943
+ // Seed BEFORE the re-feed so the pinning this call may write is part of the state object
944
+ // table-core receives on this same tick (rather than landing a frame late and re-ordering
945
+ // getVisibleCells() a render after the columns appear). One-shot and self-latching — see
946
+ // seedColumnPinning() in stateAssembly.rzts. This is the `<Column>`-children entry point:
947
+ // those register during mount, so the $onMount call below can find zero columns.
948
+ this.seedColumnPinning();
915
949
  this.reFeed();
916
950
  this.maybeClearHistoryOnExternalSwap();
917
951
  })(); }); }));
@@ -945,6 +979,19 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
945
979
  // a one-way `:data`) has a base array to whole-array-replace. currentData() then sources
946
980
  // the bound prop when controlled, this fallback otherwise.
947
981
  this._dataDefault.value = this.data || [];
982
+ // Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
983
+ // reads currentState() below, so the first row model table-core builds is already correctly
984
+ // ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
985
+ // re-ordering a render later. Covers the `:columns` config-array form, which is available
986
+ // now; `<Column>` children register during mount and are covered by the re-feed watch's own
987
+ // call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
988
+ // Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
989
+ // reads currentState() below, so the first row model table-core builds is already correctly
990
+ // ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
991
+ // re-ordering a render later. Covers the `:columns` config-array form, which is available
992
+ // now; `<Column>` children register during mount and are covered by the re-feed watch's own
993
+ // call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
994
+ this.seedColumnPinning();
948
995
  // Build the table instance HERE so the closures below capture the live `table`.
949
996
  // Build the table instance HERE so the closures below capture the live `table`.
950
997
  this.table = createTable({
@@ -1267,6 +1314,31 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1267
1314
  if (!w) {
1268
1315
  console.warn('[rozie-data-table] virtual is on for columns but the scroll container has no bounded width; set a CSS width on an ancestor so the column window can be measured');
1269
1316
  }
1317
+ // COLUMN-AXIS FIRST-WINDOW BUMP (quick 260908-vcy). The mount-kick above fires right
1318
+ // after `new Virtualizer(...)` — BEFORE colVirtualizer has measured, and before the
1319
+ // spacer effects even exist. Any fine-grained subscriber created inside that measure
1320
+ // gap takes colPadLeft()/colPadRight()'s `if (!items.length) return 0` early return,
1321
+ // which reads ONLY windowVer + editVer and never reaches the live path's reads
1322
+ // (forcedColumns()/columnSize()/column state). Its dependency set is therefore those
1323
+ // two counters alone — and without this bump nothing ever touches them again, so the
1324
+ // subscriber stays frozen at 0 for the lifetime of the component. Measured on Svelte:
1325
+ // the FIRST header row's trailing spacer froze at 0px while its siblings (created
1326
+ // after measurement, on the live path) tracked correctly; under table-layout:fixed the
1327
+ // first row alone establishes column widths, so that one cell collapsed the whole
1328
+ // table's scrollWidth from 9000 to 1350. The five other targets re-render wholesale
1329
+ // (Solid re-creates these rows), which masked it.
1330
+ //
1331
+ // The ROW axis has always been safe here only by accident: remeasureWindow() above
1332
+ // already routes through bumpWindowVer() on this same post-first-frame callback. This
1333
+ // is the column axis's missing equivalent — not a Svelte workaround (the emitted
1334
+ // Svelte binding is a correct reactive template_effect; the dependency set is what was
1335
+ // wrong), so it is fixed once, in the shared engine's consumer, for all six targets.
1336
+ //
1337
+ // Routed through bumpWindowVer() rather than a raw `$data.windowVer = $data.windowVer + 1`
1338
+ // to keep 87-10's microtask coalescing AND to avoid manufacturing the ROZ138
1339
+ // false positive that a second in-body counter self-read would trip (see the
1340
+ // mount-kick's own comment above).
1341
+ this.bumpWindowVer();
1270
1342
  }
1271
1343
  };
1272
1344
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));else setTimeout(afterFirstFrame, 0);
@@ -1287,12 +1359,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
1287
1359
  this.maybeClearHistoryOnExternalSwap();
1288
1360
  if (!this.table) return;
1289
1361
  // Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
1290
- // $data.dataDefault) so a committed edit re-feeds on Lit whether or not r-model:data is
1291
- // bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
1362
+ // $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
1363
+ // `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
1292
1364
  // identical length) still re-feeds.
1293
1365
  // Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
1294
- // $data.dataDefault) so a committed edit re-feeds on Lit whether or not r-model:data is
1295
- // bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
1366
+ // $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
1367
+ // `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
1296
1368
  // identical length) still re-feeds.
1297
1369
  const d = this.currentData() || [];
1298
1370
  if (d === this.lastData && d.length === this.lastDataLen) return;
@@ -1733,6 +1805,96 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1733
1805
  };
1734
1806
  };
1735
1807
 
1808
+ // seedColumnPinning(): apply the per-column `pinned` declaration ('left' | 'right') — from the
1809
+ // `:columns` config array or a `<Column pinned="...">` child — as the INITIAL columnPinning
1810
+ // state, exactly once.
1811
+ //
1812
+ // Until now that field was carried onto every ColumnDef by columnBuilders.rzts and then read by
1813
+ // nobody: `column.getIsPinned()` returned false for a column declared `pinned="left"`, so the
1814
+ // documented per-column API was inert while `r-model:columnPinning` was the only mechanism that
1815
+ // actually pinned anything.
1816
+ //
1817
+ // Three invariants, each load-bearing:
1818
+ // 1. ONE-SHOT, LATCHED ON THE OUTCOME — never merely on "we ran". The latch flips only when
1819
+ // a seed is actually applied, or when a consumer pin is observed; a call that finds no
1820
+ // declared pins leaves it DOWN and will look again on the next re-feed. Latching on the
1821
+ // first call that saw any column at all was wrong, and Lit proved it: `<Column>` children
1822
+ // register INCREMENTALLY there, one re-feed per child, so the first call sees only
1823
+ // `name` — no pins declared yet — and a run-once latch would disable the seed
1824
+ // permanently before `amount pinned="right"` ever registered. The other five targets
1825
+ // batch all children into a single flush, which hid the race entirely. Once a seed HAS
1826
+ // been applied the latch holds, so a subsequent interactive UNPIN is never re-seeded —
1827
+ // which is the whole reason this is not folded into effectiveColumnPinning().
1828
+ // 2. ONLY WHEN NOTHING IS PINNED YET. Note this is NOT an `$props.columnPinning == null`
1829
+ // check: the prop carries `default: () => ({ left: [], right: [] })`, so it is never null
1830
+ // and a bound-vs-unbound test is impossible here. The meaningful question is whether the
1831
+ // consumer has actually expressed a pin — if they have, they own the slice and the
1832
+ // per-column declaration must not fight it; if the state is empty, `pinned` IS the
1833
+ // initial state and applying it is the whole point.
1834
+ // 3. NO CHROME IDS. The select/expander rail is injected by effectiveColumnPinning() on read
1835
+ // and stripped by writeColumnPinning() on write; the persisted state must never contain
1836
+ // them. Seeding only real column ids keeps that discipline.
1837
+ //
1838
+ // Writes BOTH the uncontrolled holder and `$model.columnPinning` so a consumer with a two-way
1839
+ // binding sees the pinning that is actually in effect rather than a stale empty object — but
1840
+ // does NOT emit `pin-change`: this is initial state derived from the consumer's own markup, not
1841
+ // a user action, and firing a change event for it on mount would be noise.
1842
+ //
1843
+ // Called from $onMount (covers the `:columns` config-array form, known at mount) AND from the
1844
+ // re-feed watch (covers `<Column>` children, which register during mount, and a runtime
1845
+ // `:columns` swap that arrives before any pin interaction). The `!defs.length` bail is what
1846
+ // makes the mount call harmless when columns are not known yet — the latch stays down and the
1847
+ // next re-feed seeds instead.
1848
+ pinSeedApplied = false;
1849
+
1850
+ seedColumnPinning = (): void => {
1851
+ if (this.pinSeedApplied) return;
1852
+ // Invariant 2: bail if the consumer already pinned something (either an initial bound value
1853
+ // or an interaction that beat us here). Read the same source order effectiveColumnPinning()
1854
+ // does, minus the chrome rail it injects on read — the rail is structural, not a consumer pin.
1855
+ const live = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
1856
+ const isRealPin = (id: string) => id !== this.SELECT_COL_ID && id !== this.EXPANDER_COL_ID;
1857
+ const pinnedAlready = (live && live.left ? live.left : []).filter(isRealPin).length > 0 || (live && live.right ? live.right : []).filter(isRealPin).length > 0;
1858
+ if (pinnedAlready) {
1859
+ this.pinSeedApplied = true;
1860
+ return;
1861
+ }
1862
+ const defs = this.columnDefs();
1863
+ if (!defs.length) return;
1864
+ const left: string[] = [];
1865
+ const right: string[] = [];
1866
+ for (let i = 0; i < defs.length; i++) {
1867
+ const def = defs[i];
1868
+ if (!def) continue;
1869
+ if (def.pinned === 'left') left.push(def.id);else if (def.pinned === 'right') right.push(def.id);
1870
+ }
1871
+ // Invariant 1: no declared pins in the column set we can currently SEE is not evidence there
1872
+ // are none — on Lit the set is still filling in. Leave the latch down and try again on the
1873
+ // next re-feed; the only exits are "seeded" (below) and "consumer already pinned" (above).
1874
+ if (!left.length && !right.length) return;
1875
+ this.pinSeedApplied = true;
1876
+ // Nothing was pinned (invariant 2 above), so this is a plain assignment, not a merge — the
1877
+ // only ids that could be in `live` are the structural chrome rail, which must never be
1878
+ // persisted (invariant 3). Writes the uncontrolled holder AND the two-way model so both
1879
+ // binding modes observe the pinning that is actually in effect; deliberately does NOT route
1880
+ // through writeColumnPinning(), whose job is to emit `pin-change` for real user actions.
1881
+ // Annotated `any` for the same reason currentState() is (see its comment below): the
1882
+ // uncontrolled holder's initial value is the empty literal `{ left: [], right: [] }`, so the
1883
+ // emitted per-target state holder infers `{ left: never[]; right: never[] }` and a genuinely
1884
+ // populated `{ left: string[]; right: string[] }` will not assign to it under the strict
1885
+ // bundled-leaf tsc (react TS2345 on setColumnPinningDefault, solid TS2769, lit TS2322 — one
1886
+ // per target, all the same never[] inference). The runtime shape is correct; this is the
1887
+ // established idiom here rather than widening three per-leaf strict baselines for it.
1888
+ // writeColumnPinning() never hits this because its payload flows through `strip()`, which
1889
+ // returns `any[]`.
1890
+ const seeded: any = {
1891
+ left,
1892
+ right
1893
+ };
1894
+ this._columnPinningDefault.value = seeded;
1895
+ this._columnPinningControllable.write(seeded);
1896
+ };
1897
+
1736
1898
  // Assemble the live state object from bound r-model slices (?? uncontrolled fallback).
1737
1899
  // All NINE slices are wired (each ?? its own $data.<slice>Default). table-core reads
1738
1900
  // this whole object as `state`. Return type annotated `any`: the inferred object-literal
@@ -2467,9 +2629,43 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
2467
2629
  this.writeColumnPinning(applyUpdater(updater, this.currentState().columnPinning));
2468
2630
  };
2469
2631
 
2632
+ // columnSizingInfo is the ONLY slice table-core drives as a rapid, multi-call gesture: a resize
2633
+ // drag fires this updater once on pointerdown (seeding startOffset/startSize/isResizingColumn)
2634
+ // and then again on EVERY mousemove, each call reading the values the previous one wrote.
2635
+ //
2636
+ // That read-your-own-last-write requirement is what makes a plain `$data` read wrong here on
2637
+ // React. These callbacks are handed to createTable in $onMount, so on React the closure binds a
2638
+ // MOUNT-TIME `$data.columnSizingInfo` and never sees its own writes. Measured on the shipped
2639
+ // leaf (quick 260909, instrumented build): pointerdown correctly wrote
2640
+ // `{ startOffset: 163, isResizingColumn: 'col0' }`, and then every single mousemove still saw
2641
+ // `startOffset: null, isResizingColumn: false` — so it computed deltaOffset from 0 instead of
2642
+ // 163 (179, 195, 211… instead of 16, 32, 48…) AND wrote `isResizingColumn: false` back, wiping
2643
+ // the gesture on the first move. Net effect: column resizing did nothing at all on React while
2644
+ // working on the other five. It was masked because the one VR case that drives a real drag was
2645
+ // `test.fixme`'d on React as a presumed harness limitation (87-05 Task 3) — a real CDP drag
2646
+ // with intermediate steps fails there too, which is what proved it a product bug.
2647
+ //
2648
+ // Why not just read `currentState().columnSizingInfo` like the eleven siblings above: that
2649
+ // idiom relies on the re-feed $watch re-passing fresh callbacks into setOptions, and that watch
2650
+ // fires on data/column changes — NOT on every render, and never mid-gesture. It is sufficient
2651
+ // for one-shot updaters (a sort toggle, a selection click) and insufficient here, where
2652
+ // table-core issues a burst of updaters between any two re-feeds.
2653
+ //
2654
+ // Fix: a synchronous module-scope mirror, written on the same tick as the state. Reads prefer
2655
+ // the mirror, so each updater in a gesture composes on the previous one's result regardless of
2656
+ // when the host framework commits. Behaviourally identical on the five targets whose state read
2657
+ // was already synchronous (mirror and state agree), and the slice is transient gesture state
2658
+ // owned entirely by table-core — nothing else writes it — so the mirror cannot drift.
2659
+ // Same trick as the `committedThisSession` commit latch (editCellLifecycle.rzts), for the same
2660
+ // reason: a sync latch survives React's async window.
2661
+ columnSizingInfoSync: any = null;
2662
+
2470
2663
  onColumnSizingInfoChangeCb = (updater: any) => {
2471
- const next = applyUpdater(updater, this._columnSizingInfo.value);
2472
- this._columnSizingInfo.value = next != null ? next : this._columnSizingInfo.value;
2664
+ const base = this.columnSizingInfoSync != null ? this.columnSizingInfoSync : this._columnSizingInfo.value;
2665
+ const next = applyUpdater(updater, base);
2666
+ if (next == null) return;
2667
+ this.columnSizingInfoSync = next;
2668
+ this._columnSizingInfo.value = next;
2473
2669
  };
2474
2670
 
2475
2671
  // ══ Vertical row windowing (phase 53, req-1/2/3/6/9/10) — the virtual-core bridge ════════
@@ -6938,6 +7134,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6938
7134
  const tryFocus = () => {
6939
7135
  const el = this.resolveCellEl(String(row), col);
6940
7136
  if (el) {
7137
+ // B-05 / F-07: do NOT stomp focus a later interaction has already placed on a DIFFERENT
7138
+ // grid cell. This poll runs up to 30 frames after commit/cancel, so without this guard a
7139
+ // user (or a test) who moves to the next cell in that window gets yanked BACK to the cell
7140
+ // they just left — and because the steal depends on rAF timing it presents as a flake that
7141
+ // rotates across targets rather than as a reproducible bug. Measured 2026-09-12: the
7142
+ // data-table-dropins editor case failed with the active cell pinned to the PREVIOUS column
7143
+ // (want 0/2, active 0/1; want 0/1, active 0/0) with no editor open.
7144
+ // focusEditorWhenReady already carries the equivalent guard against the same class of
7145
+ // theft; this is its missing twin. Compare the OWNING CELL (not node identity) so a
7146
+ // node-replacing re-render on Solid still resolves a genuinely dropped focus as recoverable.
7147
+ const ae = this.gridRoot && this.gridRoot.getRootNode ? this.gridRoot.getRootNode().activeElement : null;
7148
+ if (ae && ae !== el && this.gridRoot.contains && this.gridRoot.contains(ae) && ae.closest) {
7149
+ const aeCell = ae.closest('[data-grid-cell]');
7150
+ if (aeCell && aeCell !== el) return;
7151
+ }
6941
7152
  el.focus();
6942
7153
  return;
6943
7154
  }