@rozie-ui/data-table-lit 0.8.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @rozie-ui/data-table-lit
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cb54e7e: Range fill and border fixes.
8
+ - **Range fill:** with a block of cells selected, `Ctrl`/`Cmd`+`Enter` in a cell editor writes its value to every selected cell in the active cell's column. It works with built-in, drop-in and custom slot editors. Editor slots get `rangeSize` and `commit(value, { toRange: true })`. The fill follows the paste rules: invalid cells are skipped, it's one undo step with one `cell-edit-commit` per changed cell, and it's announced as "N of M cells updated". The value is written as is, not converted to text. The range stays selected while the editor is open and after the fill.
9
+ - **Checkbox ranges:** `Space` (or `Enter`/`F2`) on a checkbox range sets every selected cell to the opposite of the active cell's value.
10
+ - **Border gaps:** the 1px lines of scrolled content beside pinned columns and above the sticky header are gone. The table now uses separate borders, and each cell draws its own lines. Cells also reset their borders, so a host's global `table td { border }` rule no longer draws inside the table.
11
+ - **Pinned cells:** pinned body and filter cells are opaque, and the group-row and filter-row bands still show on them. The new `--rozie-data-table-pinned-bg` token sets their background. It defaults to `#fff` in the base theme, and to the design system's surface colour in the bootstrap, material and shadcn bridges. On a dark or tinted page, set it to the colour behind the table.
12
+ - **Column lines:** the new `columnLines` prop draws column lines plus the outer lines at the start of each row and above the header; they follow the writing direction. Their colour follows the row-line colour unless `--rozie-data-table-column-border-color` is set.
13
+
14
+ ## 0.8.1
15
+
16
+ ### Patch Changes
17
+
18
+ - 3c4b293: README fixes: Angular examples use the `<rozie-data-table>`, `<rozie-column>` and other child-component selectors (the PascalCase tags they showed fail to compile with NG8001), and the grouping and faceting examples no longer nest backticks inside the template string.
19
+
3
20
  ## 0.8.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -441,11 +441,12 @@ import '@rozie-ui/data-table-lit/themes/shadcn.css'; // or material.css, boot
441
441
  | `columnOrder` | `Array` | `[]` | ✓ | | `ColumnOrderState` — `string[]`. A fresh order array on reorder (never an in-place splice). Two-way: fires `reorder-change`. |
442
442
  | `columnPinning` | `Object` | `{…}` | ✓ | | `ColumnPinningState` — `{ left: string[], right: string[] }`. Pinned columns get `position: sticky` + computed offsets. Defaults to `{ left: [], right: [] }`. Two-way: fires `pin-change`. |
443
443
  | `stickyHeader` | `Boolean` | `false` | | | Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container. |
444
+ | `columnLines` | `Boolean` | `false` | | | Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set. |
444
445
  | `interactionMode` | `String` | `"table"` | | | `'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. |
445
446
  | `singleClickEdit` | `Boolean` | `false` | | | Grid mode only. When `true`, a plain click on an **editable** cell opens its editor immediately (single-click-to-edit) instead of just activating the cell. Default `false` keeps click-to-activate (double-click opens the editor). Shift+click (range selection) and clicks on non-editable cells are unaffected. |
446
447
  | `undoable` | `Boolean` | `false` | | | Grid mode. When `true`, every committed data mutation (cell/row edit, paste, fill, cut, clear) becomes one undo step: Ctrl/Cmd+Z undoes, Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z redoes. Default `false` records no history and Ctrl+Z/Y are inert. |
447
448
  | `undoLimit` | `Number` | `100` | | | The maximum number of undo steps retained (oldest evicted past this depth). Only consulted when `undoable` is `true`. |
448
- | `holdView` | `Boolean` | `false` | | | When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`. |
449
+ | `holdView` | `Boolean` | `false` | | | When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`. |
449
450
  | `virtual` | `any` | `false` | | | Opt-in windowing grammar: `false` (default, off — byte-identical to a non-virtual table) \| `true` or `'rows'` (vertical row windowing; `true` is byte-behavior-identical to every existing consumer, zero churn) \| `'columns'` (horizontal column windowing) \| `'both'` (both axes windowed). Row windowing renders only the visible slice of rows inside a bounded `rdt-scroll` container (with leading/trailing spacer rows preserving total scroll height), windowing over the full filtered + sorted (pre-pagination) model and suppressing the client pagination chrome. Column windowing renders only the visible slice of leaf columns inside the same `rdt-scroll` container. An unrecognised string behaves as `false`. |
450
451
  | `estimateRowHeight` | `Number` | `40` | | | Estimated row height (px) — the first-paint seed for the windowing engine before any row has been measured. Only consulted when rows are windowed. When `autoMeasure` is `true`, later renders progressively refine the estimate from measured content; when `autoMeasure` is `false` this remains the explicit override for every render. |
451
452
  | `autoMeasure` | `Boolean` | `false` | | | Opt-in content-driven row-size estimation. When `true`, the windowing engine feeds `estimateSize()` a running mean of measured row heights instead of the fixed `estimateRowHeight` seed, so `getTotalSize()` converges to the true content total on a large table with variable-height rows. Falls back to `estimateRowHeight` before any row has been measured. Default `false` keeps the estimate fixed at `estimateRowHeight` for every render (today's behavior). |
@@ -556,9 +557,9 @@ A misspelled family key does not fail to compile — the emitted type carries a
556
557
  | placeholder | index, columnId |
557
558
  | selectCell | row, checked, toggle |
558
559
  | cell-<id> | columnId, column, row, value |
559
- | editor-<id> | columnId, column, row, value, commit, cancel, columnLabel, autofocus |
560
+ | editor-<id> | columnId, column, row, value, commit, cancel, columnLabel, autofocus, rangeSize |
560
561
  | detail | row |
561
562
  | colHeader | columnId, column, label |
562
563
  | filter | columnId, value, uniqueValues, minMax, columnLabel, setFilter |
563
564
  | cell | columnId, column, row, value |
564
- | editor | columnId, column, row, value, commit, cancel, columnLabel, autofocus |
565
+ | editor | columnId, column, row, value, commit, cancel, columnLabel, autofocus, rangeSize |
package/dist/index.cjs CHANGED
@@ -296,6 +296,28 @@ const tileIndex = (i, lo, hi) => {
296
296
  return lo + k;
297
297
  };
298
298
  //#endregion
299
+ //#region src/helpers/rangeFill.ts
300
+ const rangeFillTargets = (range, activeRow, activeCol) => {
301
+ const single = {
302
+ top: activeRow,
303
+ bottom: activeRow,
304
+ col: activeCol,
305
+ size: 1
306
+ };
307
+ if (!range) return single;
308
+ const top = Math.min(range.r0, range.r1);
309
+ const bottom = Math.max(range.r0, range.r1);
310
+ const left = Math.min(range.c0, range.c1);
311
+ const right = Math.max(range.c0, range.c1);
312
+ if (activeCol < left || activeCol > right || activeRow < top || activeRow > bottom) return single;
313
+ return {
314
+ top,
315
+ bottom,
316
+ col: activeCol,
317
+ size: bottom - top + 1
318
+ };
319
+ };
320
+ //#endregion
299
321
  //#region src/helpers/rowValueUtils.ts
300
322
  const replaceRowValue = (rows, rowIndex, field, value) => {
301
323
  const src = rows || [];
@@ -591,6 +613,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
591
613
  initialControlledValue: void 0
592
614
  });
593
615
  this.stickyHeader = false;
616
+ this.columnLines = false;
594
617
  this.interactionMode = "table";
595
618
  this.singleClickEdit = false;
596
619
  this.undoable = false;
@@ -2702,7 +2725,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
2702
2725
  this._activeColIndex.value = col;
2703
2726
  if (isHeader !== prevIsHeader || col !== prevCol || (isHeader ? movedLevel !== prevLevel : movedRow !== prevRow)) this.focusIntentEpoch = this.focusIntentEpoch + 1;
2704
2727
  if (this.rangeTransition) this.rangeTransition = false;
2705
- else if (this.rangeClickPending && this.rangeClickPending.isHeader === isHeader && this.rangeClickPending.r === movedRow && this.rangeClickPending.c === col) this.rangeClickPending = null;
2728
+ else if (!isHeader && tgt !== cellEl && this._editingRow.value === movedRow && this._editingCol.value === col) {} else if (this.rangeClickPending && this.rangeClickPending.isHeader === isHeader && this.rangeClickPending.r === movedRow && this.rangeClickPending.c === col) this.rangeClickPending = null;
2706
2729
  else {
2707
2730
  this.rangeClickPending = null;
2708
2731
  this.clearRange();
@@ -3466,6 +3489,14 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
3466
3489
  }
3467
3490
  this.applyGridToRange(grid, box.r0, box.c0, "filled");
3468
3491
  };
3492
+ this.fillTargets = () => rangeFillTargets(this.normalizedRange(), this._activeRow.value, this._activeColIndex.value);
3493
+ this.rangeSize = () => this._activeIsHeader.value ? 1 : this.fillTargets().size;
3494
+ this.fillActiveColumn = (value) => {
3495
+ const t = this.fillTargets();
3496
+ const grid = [];
3497
+ for (let i = 0; i < t.size; i++) grid.push([value]);
3498
+ return this.applyGridToRange(grid, t.top, t.col, "updated");
3499
+ };
3469
3500
  this.fillDragging = false;
3470
3501
  this.fillDragMove = null;
3471
3502
  this.fillDragUp = null;
@@ -3892,6 +3923,44 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
3892
3923
  } else if (skipFocusReturn !== true) this.focusCellWhenReady(focusRow, focusCol);
3893
3924
  return true;
3894
3925
  };
3926
+ this.commitEditToRange = (overrideValue = void 0) => {
3927
+ if (this._editingRow.value < 0) return false;
3928
+ if (this.committedThisSession) return false;
3929
+ const colId = this.editingColumnId();
3930
+ if (colId == null) {
3931
+ this.endEdit();
3932
+ return false;
3933
+ }
3934
+ const rawValue = overrideValue !== void 0 ? overrideValue : this._draftValue.value;
3935
+ const err = this.runValidator(colId, this.coerceCellValue(colId, rawValue), this.editingRowOriginal());
3936
+ if (err !== true) {
3937
+ this.setInvalid(err);
3938
+ this.focusEditorWhenReady(this._editingRow.value, this._editingCol.value);
3939
+ return false;
3940
+ }
3941
+ this.setInvalid("");
3942
+ const focusRow = this._editingRow.value;
3943
+ const focusCol = this._editingCol.value;
3944
+ const rowOriginal = this.editingRowOriginal();
3945
+ const rowId = this.editingRowId();
3946
+ this.editTransition = true;
3947
+ this.committedThisSession = true;
3948
+ const result = this.fillActiveColumn(rawValue);
3949
+ this.endEdit();
3950
+ this.editTransition = false;
3951
+ this.rangeClickPending = {
3952
+ isHeader: false,
3953
+ r: focusRow,
3954
+ c: focusCol
3955
+ };
3956
+ if (result && result.changed > 0) this.pendingEditFollow = {
3957
+ rowOriginal,
3958
+ rowId,
3959
+ col: focusCol
3960
+ };
3961
+ else this.focusCellWhenReady(focusRow, focusCol);
3962
+ return true;
3963
+ };
3895
3964
  this.toggleActiveBooleanCell = () => {
3896
3965
  if (this.rowIndexIsGrouped(this._activeRow.value)) return;
3897
3966
  const row = (this._rows.value || [])[this._activeRow.value];
@@ -3903,6 +3972,11 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
3903
3972
  const rowId = row.id;
3904
3973
  const oldValue = this.cellValueAt(this._activeRow.value, this._activeColIndex.value);
3905
3974
  const newValue = !oldValue;
3975
+ if (this.rangeSize() > 1) {
3976
+ this.setInvalid("");
3977
+ this.fillActiveColumn(newValue);
3978
+ return;
3979
+ }
3906
3980
  const err = this.runValidator(colId, newValue, rowOriginal);
3907
3981
  if (err !== true) {
3908
3982
  this.setInvalid(err);
@@ -4133,14 +4207,21 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4133
4207
  this.committedThisSession = false;
4134
4208
  this.rowDraftSync = {};
4135
4209
  this.dropinTabFlush = null;
4210
+ this.rangeCommitIntent = false;
4136
4211
  this.inRowEdit = () => this._editingRowIndex.value != null;
4137
4212
  this.editorValueFor = (colId) => this.inRowEdit() ? this._rowDraft.value ? this._rowDraft.value[colId] : null : this._draftValue.value;
4138
4213
  this.editorCheckedFor = (colId) => !!(this.inRowEdit() ? this._rowDraft.value ? this._rowDraft.value[colId] : null : this._draftValue.value);
4139
- this.editorCommitFor = (colId) => (value) => {
4214
+ this.editorCommitFor = (colId) => (value, options) => {
4140
4215
  if (this.inRowEdit()) {
4141
4216
  this.setRowDraft(colId, value);
4142
4217
  return;
4143
4218
  }
4219
+ const wantsRange = options != null && options.toRange === true || this.rangeCommitIntent;
4220
+ this.rangeCommitIntent = false;
4221
+ if (wantsRange && !this.dropinTabFlush && this.rangeSize() > 1) {
4222
+ this.commitEditToRange(value);
4223
+ return;
4224
+ }
4144
4225
  if (this.dropinTabFlush) {
4145
4226
  this.dropinTabFlush.called = true;
4146
4227
  if (this.commitEdit(value, true)) this.dropinTabFlush.committed = true;
@@ -4212,7 +4293,10 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4212
4293
  }
4213
4294
  return;
4214
4295
  }
4215
- if (key === "Enter") {
4296
+ if (key === "Enter" && (e.ctrlKey || e.metaKey) && this.rangeSize() > 1) {
4297
+ e.preventDefault();
4298
+ this.commitEditToRange(void 0);
4299
+ } else if (key === "Enter") {
4216
4300
  e.preventDefault();
4217
4301
  this.commitEdit(void 0);
4218
4302
  } else if (key === "Tab") {
@@ -4233,8 +4317,18 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4233
4317
  this.cancelEdit();
4234
4318
  }
4235
4319
  };
4320
+ this.onEditorDropinKeyDownCapture = (e) => {
4321
+ if (!e) return;
4322
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
4323
+ this.rangeCommitIntent = true;
4324
+ setTimeout(() => {
4325
+ this.rangeCommitIntent = false;
4326
+ }, 0);
4327
+ }
4328
+ };
4236
4329
  this.onEditorDropinKeyDown = (e) => {
4237
4330
  if (!e) return;
4331
+ this.rangeCommitIntent = false;
4238
4332
  const key = e.key;
4239
4333
  if (this.inRowEdit()) {
4240
4334
  if (key === "Enter") {
@@ -4492,6 +4586,8 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4492
4586
  --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
4493
4587
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
4494
4588
  --rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
4589
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--rdt-ds-column-border-color)));
4590
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--rdt-ds-pinned-bg));
4495
4591
  --rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
4496
4592
  --rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
4497
4593
  --rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
@@ -4513,7 +4609,12 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4513
4609
  --rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
4514
4610
  }
4515
4611
  .rozie-data-table[data-rozie-s-d5dcab4c] {
4516
- border-collapse: collapse;
4612
+ /* SEPARATE, not collapse. With collapsed borders the browser keeps a sticky cell's shared
4613
+ border in the scrolling grid, so a 1px line of scrolled content shows beside pinned
4614
+ columns and above the sticky header. Separate borders travel with their cell; zero
4615
+ spacing keeps lines 1px because each cell draws only its own sides (see the cell rule). */
4616
+ border-collapse: separate;
4617
+ border-spacing: 0;
4517
4618
  width: 100%;
4518
4619
  font: var(--rdt-font, 14px system-ui, sans-serif);
4519
4620
  color: var(--rdt-color, inherit);
@@ -4572,8 +4673,28 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4572
4673
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c] {
4573
4674
  padding: var(--rdt-cell-padding, 0.5rem 0.75rem);
4574
4675
  text-align: left;
4676
+ /* Reset every side, then draw only the row line. Under separate borders each cell owns its
4677
+ lines, and the reset keeps a host's plain \`table td { border }\` rule from drawing a second
4678
+ set inside the table (this class-scoped rule outranks it). */
4679
+ border: 0;
4575
4680
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
4576
4681
  }
4682
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
4683
+ border: 0;
4684
+ }
4685
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c],
4686
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c],
4687
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c],
4688
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
4689
+ border-inline-end: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
4690
+ }
4691
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] tr[data-rozie-s-d5dcab4c] > [data-rozie-s-d5dcab4c]:first-child:not(.rdt-col-spacer[data-rozie-s-d5dcab4c]),
4692
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] tr[data-rozie-s-d5dcab4c] > .rdt-col-spacer[data-rozie-s-d5dcab4c]:first-child + *[data-rozie-s-d5dcab4c] {
4693
+ border-inline-start: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
4694
+ }
4695
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] > tr[data-rozie-s-d5dcab4c]:first-child > .rdt-th[data-rozie-s-d5dcab4c] {
4696
+ border-top: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
4697
+ }
4577
4698
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c] {
4578
4699
  font-weight: var(--rdt-header-weight, 600);
4579
4700
  /* OPAQUE default (was rgba(0,0,0,0.03)): a translucent header lets the scrolling body
@@ -4697,8 +4818,21 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
4697
4818
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
4698
4819
  box-sizing: border-box;
4699
4820
  }
4821
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
4822
+ background-color: var(--rdt-pinned-bg, #fff);
4823
+ }
4824
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-group-header[data-rozie-s-d5dcab4c] > .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
4825
+ background: linear-gradient(var(--rdt-group-header-bg, rgba(0, 0, 0, 0.025)), var(--rdt-group-header-bg, rgba(0, 0, 0, 0.025))), var(--rdt-pinned-bg, #fff);
4826
+ }
4827
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell.rdt-pinned[data-rozie-s-d5dcab4c] {
4828
+ background: linear-gradient(var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015)), var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015))), var(--rdt-pinned-bg, #fff);
4829
+ }
4830
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned.rdt-in-range[data-rozie-s-d5dcab4c] {
4831
+ background: linear-gradient(var(--rdt-range-bg, rgba(37, 99, 235, 0.12)), var(--rdt-range-bg, rgba(37, 99, 235, 0.12))), var(--rdt-pinned-bg, #fff);
4832
+ }
4700
4833
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
4701
4834
  padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
4835
+ border: 0;
4702
4836
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
4703
4837
  }
4704
4838
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] .rdt-col-filter {
@@ -5295,6 +5429,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
5295
5429
  };
5296
5430
  document.addEventListener("pointerdown", this.docPointerDown, true);
5297
5431
  }
5432
+ if (this.gridRoot) this.gridRoot.addEventListener("keydown", this.onEditorDropinKeyDownCapture, true);
5298
5433
  if (this.isWindowed()) this.gridScrollEl = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector(".rdt-scroll") : null;
5299
5434
  if (this.rowsWindowed()) {
5300
5435
  this.virtualizer = new _tanstack_virtual_core.Virtualizer(this.virtualizerOptions());
@@ -5384,6 +5519,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
5384
5519
  document.removeEventListener("pointerdown", this.docPointerDown, true);
5385
5520
  this.docPointerDown = null;
5386
5521
  }
5522
+ if (this.gridRoot) this.gridRoot.removeEventListener("keydown", this.onEditorDropinKeyDownCapture, true);
5387
5523
  if (this.virtualizerCleanup) this.virtualizerCleanup();
5388
5524
  if (this.colVirtualizerCleanup) this.colVirtualizerCleanup();
5389
5525
  this.teardownColRtlWatch();
@@ -5457,7 +5593,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
5457
5593
  <table class="${Object.entries({
5458
5594
  "rozie-data-table": true,
5459
5595
  "rdt-sticky": this.stickyHeader,
5460
- "rdt-col-windowed": this.colsWindowed()
5596
+ "rdt-col-windowed": this.colsWindowed(),
5597
+ "rdt-column-lines": this.columnLines
5461
5598
  }).filter(([, v]) => v).map(([k]) => k).join(" ")}" role=${(0, _rozie_runtime_lit.rozieAttr)(this.tableRole())} aria-rowcount=${(0, _rozie_runtime_lit.rozieAttr)(this.gridAriaRowCount())} aria-colcount=${(0, _rozie_runtime_lit.rozieAttr)(this.gridAriaColCount())} @keydown=${($event) => {
5462
5599
  this.onGridKeyDown($event);
5463
5600
  }} @focusin=${($event) => {
@@ -5792,7 +5929,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
5792
5929
  commit: this.editorCommitFor(cell.column.id),
5793
5930
  cancel: this.editorCancelFor(),
5794
5931
  columnLabel: this.headerLabel(cell.column.id),
5795
- autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)
5932
+ autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index),
5933
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
5796
5934
  }) : lit.html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => {
5797
5935
  try {
5798
5936
  return JSON.stringify({
@@ -5803,7 +5941,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
5803
5941
  commit: this.editorCommitFor(cell.column.id),
5804
5942
  cancel: this.editorCancelFor(),
5805
5943
  columnLabel: this.headerLabel(cell.column.id),
5806
- autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)
5944
+ autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index),
5945
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
5807
5946
  });
5808
5947
  } catch {
5809
5948
  return "{}";
@@ -5817,7 +5956,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
5817
5956
  commit: this.editorCommitFor(cell.column.id),
5818
5957
  cancel: this.editorCancelFor(),
5819
5958
  columnLabel: this.headerLabel(cell.column.id),
5820
- autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)
5959
+ autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index),
5960
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
5821
5961
  }) : lit.html`<slot name="editor" data-rozie-params=${(() => {
5822
5962
  try {
5823
5963
  return JSON.stringify({
@@ -5828,7 +5968,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
5828
5968
  commit: this.editorCommitFor(cell.column.id),
5829
5969
  cancel: this.editorCancelFor(),
5830
5970
  columnLabel: this.headerLabel(cell.column.id),
5831
- autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)
5971
+ autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index),
5972
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
5832
5973
  });
5833
5974
  } catch {
5834
5975
  return "{}";
@@ -5910,7 +6051,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
5910
6051
  </table>
5911
6052
  </div>` : lit.html`<table class="${Object.entries({
5912
6053
  "rozie-data-table": true,
5913
- "rdt-sticky": this.stickyHeader
6054
+ "rdt-sticky": this.stickyHeader,
6055
+ "rdt-column-lines": this.columnLines
5914
6056
  }).filter(([, v]) => v).map(([k]) => k).join(" ")}" role=${(0, _rozie_runtime_lit.rozieAttr)(this.tableRole())} aria-rowcount=${(0, _rozie_runtime_lit.rozieAttr)(this.gridAriaRowCount())} aria-colcount=${(0, _rozie_runtime_lit.rozieAttr)(this.gridAriaColCount())} @keydown=${($event) => {
5915
6057
  this.onGridKeyDown($event);
5916
6058
  }} @focusin=${($event) => {
@@ -6222,7 +6364,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
6222
6364
  commit: this.editorCommitFor(cell.column.id),
6223
6365
  cancel: this.editorCancelFor(),
6224
6366
  columnLabel: this.headerLabel(cell.column.id),
6225
- autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))
6367
+ autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)),
6368
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
6226
6369
  }) : lit.html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => {
6227
6370
  try {
6228
6371
  return JSON.stringify({
@@ -6233,7 +6376,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
6233
6376
  commit: this.editorCommitFor(cell.column.id),
6234
6377
  cancel: this.editorCancelFor(),
6235
6378
  columnLabel: this.headerLabel(cell.column.id),
6236
- autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))
6379
+ autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)),
6380
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
6237
6381
  });
6238
6382
  } catch {
6239
6383
  return "{}";
@@ -6247,7 +6391,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
6247
6391
  commit: this.editorCommitFor(cell.column.id),
6248
6392
  cancel: this.editorCancelFor(),
6249
6393
  columnLabel: this.headerLabel(cell.column.id),
6250
- autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))
6394
+ autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)),
6395
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
6251
6396
  }) : lit.html`<slot name="editor" data-rozie-params=${(() => {
6252
6397
  try {
6253
6398
  return JSON.stringify({
@@ -6258,7 +6403,8 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
6258
6403
  commit: this.editorCommitFor(cell.column.id),
6259
6404
  cancel: this.editorCancelFor(),
6260
6405
  columnLabel: this.headerLabel(cell.column.id),
6261
- autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))
6406
+ autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)),
6407
+ rangeSize: this.inRowEdit() ? 1 : this.rangeSize()
6262
6408
  });
6263
6409
  } catch {
6264
6410
  return "{}";
@@ -6521,6 +6667,11 @@ __decorate([(0, lit_decorators_js.property)({
6521
6667
  reflect: true,
6522
6668
  attribute: "sticky-header"
6523
6669
  })], DataTable.prototype, "stickyHeader", void 0);
6670
+ __decorate([(0, lit_decorators_js.property)({
6671
+ type: Boolean,
6672
+ reflect: true,
6673
+ attribute: "column-lines"
6674
+ })], DataTable.prototype, "columnLines", void 0);
6524
6675
  __decorate([(0, lit_decorators_js.property)({
6525
6676
  type: String,
6526
6677
  reflect: true,
package/dist/index.d.cts CHANGED
@@ -20,6 +20,14 @@ interface HoldController {
20
20
  viewState(table: any): ViewState;
21
21
  }
22
22
  //#endregion
23
+ //#region src/helpers/rangeFill.d.ts
24
+ interface FillTargets {
25
+ top: number;
26
+ bottom: number;
27
+ col: number;
28
+ size: number;
29
+ }
30
+ //#endregion
23
31
  //#region src/DataTable.d.ts
24
32
  /** A cell position as integers over the visible (display-order) model. */
25
33
  interface DataTableCellPosition {
@@ -233,6 +241,10 @@ declare class DataTable extends DataTable_base {
233
241
  * Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container.
234
242
  */
235
243
  stickyHeader: boolean;
244
+ /**
245
+ * Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set.
246
+ */
247
+ columnLines: boolean;
236
248
  /**
237
249
  * `'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.
238
250
  */
@@ -250,7 +262,7 @@ declare class DataTable extends DataTable_base {
250
262
  */
251
263
  undoLimit: number;
252
264
  /**
253
- * When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
265
+ * When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
254
266
  */
255
267
  holdView: boolean;
256
268
  /**
@@ -388,10 +400,13 @@ declare class DataTable extends DataTable_base {
388
400
  column: Column$1<any, unknown>;
389
401
  row: any;
390
402
  value: any;
391
- commit: (value: any) => void;
403
+ commit: (value: any, options?: {
404
+ toRange?: boolean;
405
+ }) => void;
392
406
  cancel: () => void;
393
407
  columnLabel: string;
394
408
  autofocus: boolean;
409
+ rangeSize: number;
395
410
  }) => unknown;
396
411
  rozieSlots?: {
397
412
  [key: `colHeader-${string}`]: (scope: {
@@ -418,10 +433,13 @@ declare class DataTable extends DataTable_base {
418
433
  column: Column$1<any, unknown>;
419
434
  row: any;
420
435
  value: any;
421
- commit: (value: any) => void;
436
+ commit: (value: any, options?: {
437
+ toRange?: boolean;
438
+ }) => void;
422
439
  cancel: () => void;
423
440
  columnLabel: string;
424
441
  autofocus: boolean;
442
+ rangeSize: number;
425
443
  }) => unknown;
426
444
  } & Record<string, (scope: any) => unknown>;
427
445
  private _disconnectCleanups;
@@ -845,6 +863,13 @@ declare class DataTable extends DataTable_base {
845
863
  cutRange: () => void;
846
864
  clearActiveRange: () => void;
847
865
  fillRange: (sourceBox: any, endCell: any) => void;
866
+ fillTargets: () => FillTargets;
867
+ rangeSize: () => number;
868
+ fillActiveColumn: (value: any) => {
869
+ wrote: number;
870
+ changed: number;
871
+ total: number;
872
+ };
848
873
  fillDragging: boolean;
849
874
  fillDragMove: any;
850
875
  fillDragUp: any;
@@ -896,6 +921,7 @@ declare class DataTable extends DataTable_base {
896
921
  editorAutofocusFor: (colId: any, rowIndex: any) => boolean;
897
922
  coerceCellValue: (colId: any, raw: any) => any;
898
923
  commitEdit: (overrideValue?: any, skipFocusReturn?: boolean) => boolean;
924
+ commitEditToRange: (overrideValue?: any) => boolean;
899
925
  toggleActiveBooleanCell: () => void;
900
926
  cancelEdit: () => void;
901
927
  editableColumnsForRow: (rowIndex: any) => any[];
@@ -919,16 +945,18 @@ declare class DataTable extends DataTable_base {
919
945
  called: boolean;
920
946
  committed: boolean;
921
947
  } | null;
948
+ rangeCommitIntent: boolean;
922
949
  inRowEdit: () => boolean;
923
950
  editorValueFor: (colId: any) => any;
924
951
  editorCheckedFor: (colId: any) => boolean;
925
- editorCommitFor: (colId: any) => (value: any) => void;
952
+ editorCommitFor: (colId: any) => (value: any, options: any) => void;
926
953
  editorCancelFor: () => () => void;
927
954
  onCellEditorInput: (colId: any, evt: any) => void;
928
955
  onCellEditorCheckbox: (colId: any, evt: any) => void;
929
956
  setRowDraft: (colId: any, value: any) => void;
930
957
  rowEditTab: (target: any, backward: any) => void;
931
958
  onEditorKeyDown: (e: any) => void;
959
+ onEditorDropinKeyDownCapture: (e: any) => void;
932
960
  onEditorDropinKeyDown: (e: any) => void;
933
961
  onEditorBlur: (e: any) => void;
934
962
  editCell: (rowIndex: number, colIndex: number) => void;
package/dist/index.d.mts CHANGED
@@ -20,6 +20,14 @@ interface HoldController {
20
20
  viewState(table: any): ViewState;
21
21
  }
22
22
  //#endregion
23
+ //#region src/helpers/rangeFill.d.ts
24
+ interface FillTargets {
25
+ top: number;
26
+ bottom: number;
27
+ col: number;
28
+ size: number;
29
+ }
30
+ //#endregion
23
31
  //#region src/DataTable.d.ts
24
32
  /** A cell position as integers over the visible (display-order) model. */
25
33
  interface DataTableCellPosition {
@@ -233,6 +241,10 @@ declare class DataTable extends DataTable_base {
233
241
  * Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container.
234
242
  */
235
243
  stickyHeader: boolean;
244
+ /**
245
+ * Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set.
246
+ */
247
+ columnLines: boolean;
236
248
  /**
237
249
  * `'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.
238
250
  */
@@ -250,7 +262,7 @@ declare class DataTable extends DataTable_base {
250
262
  */
251
263
  undoLimit: number;
252
264
  /**
253
- * When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
265
+ * When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
254
266
  */
255
267
  holdView: boolean;
256
268
  /**
@@ -388,10 +400,13 @@ declare class DataTable extends DataTable_base {
388
400
  column: Column$1<any, unknown>;
389
401
  row: any;
390
402
  value: any;
391
- commit: (value: any) => void;
403
+ commit: (value: any, options?: {
404
+ toRange?: boolean;
405
+ }) => void;
392
406
  cancel: () => void;
393
407
  columnLabel: string;
394
408
  autofocus: boolean;
409
+ rangeSize: number;
395
410
  }) => unknown;
396
411
  rozieSlots?: {
397
412
  [key: `colHeader-${string}`]: (scope: {
@@ -418,10 +433,13 @@ declare class DataTable extends DataTable_base {
418
433
  column: Column$1<any, unknown>;
419
434
  row: any;
420
435
  value: any;
421
- commit: (value: any) => void;
436
+ commit: (value: any, options?: {
437
+ toRange?: boolean;
438
+ }) => void;
422
439
  cancel: () => void;
423
440
  columnLabel: string;
424
441
  autofocus: boolean;
442
+ rangeSize: number;
425
443
  }) => unknown;
426
444
  } & Record<string, (scope: any) => unknown>;
427
445
  private _disconnectCleanups;
@@ -845,6 +863,13 @@ declare class DataTable extends DataTable_base {
845
863
  cutRange: () => void;
846
864
  clearActiveRange: () => void;
847
865
  fillRange: (sourceBox: any, endCell: any) => void;
866
+ fillTargets: () => FillTargets;
867
+ rangeSize: () => number;
868
+ fillActiveColumn: (value: any) => {
869
+ wrote: number;
870
+ changed: number;
871
+ total: number;
872
+ };
848
873
  fillDragging: boolean;
849
874
  fillDragMove: any;
850
875
  fillDragUp: any;
@@ -896,6 +921,7 @@ declare class DataTable extends DataTable_base {
896
921
  editorAutofocusFor: (colId: any, rowIndex: any) => boolean;
897
922
  coerceCellValue: (colId: any, raw: any) => any;
898
923
  commitEdit: (overrideValue?: any, skipFocusReturn?: boolean) => boolean;
924
+ commitEditToRange: (overrideValue?: any) => boolean;
899
925
  toggleActiveBooleanCell: () => void;
900
926
  cancelEdit: () => void;
901
927
  editableColumnsForRow: (rowIndex: any) => any[];
@@ -919,16 +945,18 @@ declare class DataTable extends DataTable_base {
919
945
  called: boolean;
920
946
  committed: boolean;
921
947
  } | null;
948
+ rangeCommitIntent: boolean;
922
949
  inRowEdit: () => boolean;
923
950
  editorValueFor: (colId: any) => any;
924
951
  editorCheckedFor: (colId: any) => boolean;
925
- editorCommitFor: (colId: any) => (value: any) => void;
952
+ editorCommitFor: (colId: any) => (value: any, options: any) => void;
926
953
  editorCancelFor: () => () => void;
927
954
  onCellEditorInput: (colId: any, evt: any) => void;
928
955
  onCellEditorCheckbox: (colId: any, evt: any) => void;
929
956
  setRowDraft: (colId: any, value: any) => void;
930
957
  rowEditTab: (target: any, backward: any) => void;
931
958
  onEditorKeyDown: (e: any) => void;
959
+ onEditorDropinKeyDownCapture: (e: any) => void;
932
960
  onEditorDropinKeyDown: (e: any) => void;
933
961
  onEditorBlur: (e: any) => void;
934
962
  editCell: (rowIndex: number, colIndex: number) => void;