@rozie-ui/data-table-solid 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/DataTable.tsx CHANGED
@@ -8,6 +8,7 @@ import { columnRuleKeys } from './helpers/columnRuleUtils';
8
8
  import { applyUpdater, clamp, focusables } from './helpers/indexMath';
9
9
  import { resolveFollow, sameIdSequence } from './helpers/activeFollow';
10
10
  import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
11
+ import { rangeFillTargets } from './helpers/rangeFill';
11
12
  import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
12
13
  import { createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel,
13
14
  // Faceted filtering (phase 50 reqs 8-9, D-03). All three are supplied UNCONDITIONALLY
@@ -131,6 +132,8 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
131
132
  --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
132
133
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
133
134
  --rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
135
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--rdt-ds-column-border-color)));
136
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--rdt-ds-pinned-bg));
134
137
  --rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
135
138
  --rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
136
139
  --rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
@@ -152,7 +155,12 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
152
155
  --rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
153
156
  }
154
157
  .rozie-data-table[data-rozie-s-d5dcab4c] {
155
- border-collapse: collapse;
158
+ /* SEPARATE, not collapse. With collapsed borders the browser keeps a sticky cell's shared
159
+ border in the scrolling grid, so a 1px line of scrolled content shows beside pinned
160
+ columns and above the sticky header. Separate borders travel with their cell; zero
161
+ spacing keeps lines 1px because each cell draws only its own sides (see the cell rule). */
162
+ border-collapse: separate;
163
+ border-spacing: 0;
156
164
  width: 100%;
157
165
  font: var(--rdt-font, 14px system-ui, sans-serif);
158
166
  color: var(--rdt-color, inherit);
@@ -211,8 +219,28 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
211
219
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c] {
212
220
  padding: var(--rdt-cell-padding, 0.5rem 0.75rem);
213
221
  text-align: left;
222
+ /* Reset every side, then draw only the row line. Under separate borders each cell owns its
223
+ lines, and the reset keeps a host's plain \`table td { border }\` rule from drawing a second
224
+ set inside the table (this class-scoped rule outranks it). */
225
+ border: 0;
214
226
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
215
227
  }
228
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
229
+ border: 0;
230
+ }
231
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c],
232
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c],
233
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c],
234
+ .rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
235
+ border-inline-end: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
236
+ }
237
+ .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]),
238
+ .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] {
239
+ border-inline-start: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
240
+ }
241
+ .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] {
242
+ border-top: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
243
+ }
216
244
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c] {
217
245
  font-weight: var(--rdt-header-weight, 600);
218
246
  /* OPAQUE default (was rgba(0,0,0,0.03)): a translucent header lets the scrolling body
@@ -315,8 +343,21 @@ __rozieInjectStyle('DataTable-d5dcab4c', `[data-rozie-s-d5dcab4c]:host {
315
343
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
316
344
  box-sizing: border-box;
317
345
  }
346
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
347
+ background-color: var(--rdt-pinned-bg, #fff);
348
+ }
349
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-group-header[data-rozie-s-d5dcab4c] > .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
350
+ 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);
351
+ }
352
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell.rdt-pinned[data-rozie-s-d5dcab4c] {
353
+ 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);
354
+ }
355
+ .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned.rdt-in-range[data-rozie-s-d5dcab4c] {
356
+ 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);
357
+ }
318
358
  .rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
319
359
  padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
360
+ border: 0;
320
361
  border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
321
362
  }
322
363
  .rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] .rdt-col-filter {
@@ -542,7 +583,9 @@ interface FilterSlotCtx { columnId: string; value: any; uniqueValues: any[]; min
542
583
 
543
584
  interface CellSlotCtx { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; }
544
585
 
545
- interface EditorSlotCtx { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any) => void; cancel: () => void; columnLabel: string; autofocus: boolean; }
586
+ interface EditorSlotCtx { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any, options?: {
587
+ toRange?: boolean;
588
+ }) => void; cancel: () => void; columnLabel: string; autofocus: boolean; rangeSize: number; }
546
589
 
547
590
  interface DataTableProps {
548
591
  /**
@@ -659,6 +702,10 @@ interface DataTableProps {
659
702
  * Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container.
660
703
  */
661
704
  stickyHeader?: boolean;
705
+ /**
706
+ * 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. Each line takes 1px inside the cell, so outside column windowing a column renders 1px wider than without lines; pinned and windowed columns keep their declared width.
707
+ */
708
+ columnLines?: boolean;
662
709
  /**
663
710
  * `'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.
664
711
  */
@@ -676,11 +723,11 @@ interface DataTableProps {
676
723
  */
677
724
  undoLimit?: number;
678
725
  /**
679
- * 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()`.
726
+ * 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()`.
680
727
  */
681
728
  holdView?: boolean;
682
729
  /**
683
- * 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`.
730
+ * 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`. Choose it when the table mounts: changing `virtual` afterwards is not supported (remount the table instead).
684
731
  */
685
732
  virtual?: boolean | string;
686
733
  /**
@@ -724,7 +771,9 @@ interface DataTableProps {
724
771
  filterSlot?: (ctx: FilterSlotCtx) => JSX.Element;
725
772
  cellSlot?: (ctx: CellSlotCtx) => JSX.Element;
726
773
  editorSlot?: (ctx: EditorSlotCtx) => JSX.Element;
727
- slots?: { [key: `colHeader-${string}`]: ((ctx: { columnId: string; column: TableColumn<any, unknown>; label: string }) => JSX.Element) | undefined; [key: `filter-${string}`]: ((ctx: { columnId: string; value: any; uniqueValues: any[]; minMax: [number, number] | null; columnLabel: string; setFilter: (columnId: string, value: any) => void }) => JSX.Element) | undefined; [key: `cell-${string}`]: ((ctx: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any }) => JSX.Element) | undefined; [key: `editor-${string}`]: ((ctx: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any) => void; cancel: () => void; columnLabel: string; autofocus: boolean }) => JSX.Element) | undefined; [key: string]: ((...args: any[]) => JSX.Element) | undefined; };
774
+ slots?: { [key: `colHeader-${string}`]: ((ctx: { columnId: string; column: TableColumn<any, unknown>; label: string }) => JSX.Element) | undefined; [key: `filter-${string}`]: ((ctx: { columnId: string; value: any; uniqueValues: any[]; minMax: [number, number] | null; columnLabel: string; setFilter: (columnId: string, value: any) => void }) => JSX.Element) | undefined; [key: `cell-${string}`]: ((ctx: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any }) => JSX.Element) | undefined; [key: `editor-${string}`]: ((ctx: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any, options?: {
775
+ toRange?: boolean;
776
+ }) => void; cancel: () => void; columnLabel: string; autofocus: boolean; rangeSize: number }) => JSX.Element) | undefined; [key: string]: ((...args: any[]) => JSX.Element) | undefined; };
728
777
  ref?: (h: DataTableHandle) => void;
729
778
  }
730
779
 
@@ -770,8 +819,8 @@ export interface DataTableHandle {
770
819
  }
771
820
 
772
821
  export default function DataTable(_props: DataTableProps): JSX.Element {
773
- const _merged = mergeProps({ columns: (() => [])() as any[], selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getRowId: null, getSubRows: null, groupable: false, stickyHeader: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, holdView: false, virtual: false, estimateRowHeight: 40, autoMeasure: false, maxHeight: '' }, _props);
774
- const [local, attrs] = splitProps(_merged, ['data', 'columns', 'selectionMode', 'sorting', 'globalFilter', 'columnFilters', 'pagination', 'manual', 'rowCount', 'pageCount', 'expandable', 'expanded', 'getRowId', 'getSubRows', 'groupable', 'grouping', 'rowSelection', 'columnVisibility', 'columnSizing', 'columnOrder', 'columnPinning', 'stickyHeader', 'interactionMode', 'singleClickEdit', 'undoable', 'undoLimit', 'holdView', 'virtual', 'estimateRowHeight', 'autoMeasure', 'maxHeight', 'children', 'ref', 'onActivecellChange', 'onCellEditCommit', 'onExpandChange', 'onFilterChange', 'onGroupChange', 'onHistoryChange', 'onPageChange', 'onPinChange', 'onRangeChange', 'onReorderChange', 'onResizeChange', 'onRowActivate', 'onRowEditCommit', 'onSelectionChange', 'onSortChange', 'onVisibilityChange', 'onViewStateChange', 'onVisibleRangeChange', 'groupBarSlot', 'selectAllSlot', 'placeholderSlot', 'selectCellSlot', 'detailSlot', 'colHeaderSlot', 'filterSlot', 'cellSlot', 'editorSlot', 'slots']);
822
+ const _merged = mergeProps({ columns: (() => [])() as any[], selectionMode: 'none', manual: false, rowCount: null, pageCount: null, expandable: false, getRowId: null, getSubRows: null, groupable: false, stickyHeader: false, columnLines: false, interactionMode: 'table', singleClickEdit: false, undoable: false, undoLimit: 100, holdView: false, virtual: false, estimateRowHeight: 40, autoMeasure: false, maxHeight: '' }, _props);
823
+ const [local, attrs] = splitProps(_merged, ['data', 'columns', 'selectionMode', 'sorting', 'globalFilter', 'columnFilters', 'pagination', 'manual', 'rowCount', 'pageCount', 'expandable', 'expanded', 'getRowId', 'getSubRows', 'groupable', 'grouping', 'rowSelection', 'columnVisibility', 'columnSizing', 'columnOrder', 'columnPinning', 'stickyHeader', 'columnLines', 'interactionMode', 'singleClickEdit', 'undoable', 'undoLimit', 'holdView', 'virtual', 'estimateRowHeight', 'autoMeasure', 'maxHeight', 'children', 'ref', 'onActivecellChange', 'onCellEditCommit', 'onExpandChange', 'onFilterChange', 'onGroupChange', 'onHistoryChange', 'onPageChange', 'onPinChange', 'onRangeChange', 'onReorderChange', 'onResizeChange', 'onRowActivate', 'onRowEditCommit', 'onSelectionChange', 'onSortChange', 'onVisibilityChange', 'onViewStateChange', 'onVisibleRangeChange', 'groupBarSlot', 'selectAllSlot', 'placeholderSlot', 'selectCellSlot', 'detailSlot', 'colHeaderSlot', 'filterSlot', 'cellSlot', 'editorSlot', 'slots']);
775
824
  const resolved = () => local.children;
776
825
  onMount(() => { local.ref?.({ sortColumn, clearSorting, toggleRowExpanded, expandAll, collapseAll, getExpandedRows, applyGrouping, clearGrouping, getFacetedUniqueValues, getFacetedMinMaxValues, getColumnDefs, toggleAllRows, clearSelection, getSelectedRows, setPage, setRowsPerPage, toggleColumnVisibility, applyColumnOrder, resetColumnSizing, pinColumn, focusCell, getActiveCell, clearActiveCell, scrollToRow, getScrollElement, getRowIndexRelativeToPage, editCell, commitEditing, editRow, getSelectedRange, cut, undo, redo, canUndo, canRedo, clearHistory, refreshView, getViewState }); });
777
826
 
@@ -1157,6 +1206,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1157
1206
  };
1158
1207
  document.addEventListener('pointerdown', docPointerDown, true);
1159
1208
  }
1209
+ // Range fill: a CAPTURE-phase keydown on the grid root runs before any editor's own keydown
1210
+ // handler — built-in, drop-in or slotted (on Lit the slotted editor's composed path still
1211
+ // passes through the grid root). Registered here in script rather than as a template
1212
+ // `.capture` binding so it is the same code on all six targets. It reads no reactive state
1213
+ // (a mount-time closure would see stale state on React); the commit checks the range.
1214
+ if (gridRoot) gridRoot.addEventListener('keydown', onEditorDropinKeyDownCapture, true);
1160
1215
  // WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
1161
1216
  // page load AND was non-deterministic on React/Solid (the entry cell may not be
1162
1217
  // committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
@@ -1292,6 +1347,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
1292
1347
  document.removeEventListener('pointerdown', docPointerDown, true);
1293
1348
  docPointerDown = null;
1294
1349
  }
1350
+ if (gridRoot) gridRoot.removeEventListener('keydown', onEditorDropinKeyDownCapture, true);
1295
1351
  if (virtualizerCleanup) virtualizerCleanup();
1296
1352
  // Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
1297
1353
  // No-op when column windowing was off (cleanup stays null).
@@ -5840,6 +5896,10 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
5840
5896
  // any OTHER cell falls through to clearRange() and clears the flag on the way past.
5841
5897
  if (rangeTransition) {
5842
5898
  rangeTransition = false;
5899
+ } else if (!isHeader && tgt !== cellEl && editingRow() === movedRow && editingCol() === col) {
5900
+ // Focus moving INTO the open editor of this cell (a built-in input, or a slot editor that
5901
+ // focuses itself) is not navigation: keep the range so the editor can commit to it with
5902
+ // Ctrl/Cmd+Enter.
5843
5903
  } else if (rangeClickPending && rangeClickPending.isHeader === isHeader && rangeClickPending.r === movedRow && rangeClickPending.c === col) {
5844
5904
  rangeClickPending = null;
5845
5905
  } else {
@@ -7207,6 +7267,28 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
7207
7267
  applyGridToRange(grid, box.r0, box.c0, 'filled');
7208
7268
  }
7209
7269
 
7270
+ // ══ Range fill (Ctrl/Cmd+Enter from an editor; Space on a checkbox range) ════════════════
7271
+ // rangeSize(): how many cells a range fill would write — the range's rows in the ACTIVE
7272
+ // cell's column, or 1 with no range. Exposed to editor slots as `rangeSize` so a custom
7273
+ // editor can say "Apply to 24 cells".
7274
+ function fillTargets() {
7275
+ return rangeFillTargets(normalizedRange(), activeRow(), activeColIndex());
7276
+ }
7277
+ function rangeSize() {
7278
+ return activeIsHeader() ? 1 : fillTargets().size;
7279
+ }
7280
+
7281
+ // fillActiveColumn(value): write ONE value down the active cell's column for every row of the
7282
+ // range, through the same funnel as paste and fill (skip rule, one undo step, one
7283
+ // cell-edit-commit per changed cell, "N of M cells updated"). The value is written AS IS — a
7284
+ // one-column grid of the same value, never stringified — so an array or object survives.
7285
+ function fillActiveColumn(value: any) {
7286
+ const t = fillTargets();
7287
+ const grid = [];
7288
+ for (let i = 0; i < t.size; i++) grid.push([value]);
7289
+ return applyGridToRange(grid, t.top, t.col, 'updated');
7290
+ }
7291
+
7210
7292
  // onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
7211
7293
  // range's bottom-right cell; a pointer drag extends the range (reusing setRangeFocus off the
7212
7294
  // cell under the pointer) and, on release, value-fills the dragged rectangle. Kept minimal:
@@ -8029,6 +8111,62 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
8029
8111
  return true;
8030
8112
  }
8031
8113
 
8114
+ // commitEditToRange(overrideValue): the Ctrl/Cmd+Enter commit. Writes the editor's value to
8115
+ // every selected cell in the editing cell's column through fillActiveColumn (the paste/fill
8116
+ // funnel: skip rule, one undo step, one cell-edit-commit per changed cell, "N of M cells
8117
+ // updated"), then closes the editor and returns focus to the active cell WITHOUT clearing the
8118
+ // range. The value is checked against the editing cell first: if the cell being edited rejects
8119
+ // it, the editor stays open with the error, exactly as a single-cell commit does.
8120
+ function commitEditToRange(overrideValue = undefined) {
8121
+ if (editingRow() < 0) return false;
8122
+ if (committedThisSession) return false;
8123
+ const colId = editingColumnId();
8124
+ if (colId == null) {
8125
+ endEdit();
8126
+ return false;
8127
+ }
8128
+ const rawValue = overrideValue !== undefined ? overrideValue : draftValue();
8129
+ const err = runValidator(colId, coerceCellValue(colId, rawValue), editingRowOriginal());
8130
+ if (err !== true) {
8131
+ setInvalid(err);
8132
+ focusEditorWhenReady(editingRow(), editingCol());
8133
+ return false;
8134
+ }
8135
+ setInvalid('');
8136
+ const focusRow = editingRow();
8137
+ const focusCol = editingCol();
8138
+ const rowOriginal = editingRowOriginal();
8139
+ const rowId = editingRowId();
8140
+ // Same teardown guards as commitEdit: the unmount blur of the editor must not commit again.
8141
+ editTransition = true;
8142
+ committedThisSession = true;
8143
+ const result = fillActiveColumn(rawValue);
8144
+ endEdit();
8145
+ editTransition = false;
8146
+ // Keep the range when focus comes back to the active cell (the selection stays put after a
8147
+ // fill, as in a spreadsheet). Coordinate-matched, like a Shift+Click: if focus lands anywhere
8148
+ // else first (the user clicks another cell, or the focus return gives up), that focusin
8149
+ // clears the range as usual instead of inheriting a stale "keep" flag.
8150
+ rangeClickPending = {
8151
+ isHeader: false,
8152
+ r: focusRow,
8153
+ c: focusCol
8154
+ };
8155
+ if (result && result.changed > 0) {
8156
+ // A write can re-sort or re-filter the rows (holdView off), so follow the active cell's
8157
+ // RECORD rather than its old index — the same B23 path commitEdit uses. A re-order also
8158
+ // clears the range, so the coordinate match above only keeps it when nothing moved.
8159
+ pendingEditFollow = {
8160
+ rowOriginal,
8161
+ rowId,
8162
+ col: focusCol
8163
+ };
8164
+ } else {
8165
+ focusCellWhenReady(focusRow, focusCol);
8166
+ }
8167
+ return true;
8168
+ }
8169
+
8032
8170
  // toggleActiveBooleanCell (design doc 2026-07-05, Change 1): the spreadsheet-standard
8033
8171
  // single-keystroke boolean toggle. Flips the ACTIVE cell's value and commits it through the
8034
8172
  // EXACT SAME write funnel commitEdit uses (replaceRowValue → writeData → single $emit) but
@@ -8057,6 +8195,14 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
8057
8195
  const rowId = row.id;
8058
8196
  const oldValue = cellValueAt(activeRow(), activeColIndex());
8059
8197
  const newValue = !oldValue;
8198
+ // With a range selected, set every selected cell in this column to the active cell's new
8199
+ // value (one value for all, not a per-cell flip) through the paste/fill funnel: invalid cells
8200
+ // skipped, one undo step, one cell-edit-commit per changed cell, "N of M cells updated".
8201
+ if (rangeSize() > 1) {
8202
+ setInvalid('');
8203
+ fillActiveColumn(newValue);
8204
+ return;
8205
+ }
8060
8206
  // D-01: same discipline as commitEdit — a rejecting validator blocks the toggle. There is
8061
8207
  // no editor to keep open here, so the toggle simply does not apply (no model write).
8062
8208
  const err = runValidator(colId, newValue, rowOriginal);
@@ -8454,6 +8600,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
8454
8600
  committed: boolean;
8455
8601
  } | null = null;
8456
8602
 
8603
+ // rangeCommitIntent: set by the drop-in wrapper's CAPTURE-phase keydown when Ctrl/Cmd+Enter is
8604
+ // pressed while a range is selected, so the slot editor's own commit — which runs during that
8605
+ // same keypress — goes to the whole range. Cleared by the bubble-phase handler and by a
8606
+ // setTimeout(0), so an editor that does not commit during the keypress falls back to one cell.
8607
+ let rangeCommitIntent = false;
8608
+
8457
8609
  // ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
8458
8610
  // In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
8459
8611
  // (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
@@ -8477,11 +8629,19 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
8477
8629
  // reverts the cell (single) or the whole row (row mode). Factory-bound per columnId so the
8478
8630
  // row-mode commit targets the right draft key.
8479
8631
  function editorCommitFor(colId: any) {
8480
- return (value: any) => {
8632
+ return (value: any, options: any) => {
8481
8633
  if (inRowEdit()) {
8482
8634
  setRowDraft$local(colId, value);
8483
8635
  return;
8484
8636
  }
8637
+ // Range fill: an explicit `commit(v, { toRange: true })`, or any commit made during a
8638
+ // Ctrl/Cmd+Enter keypress (rangeCommitIntent), writes v to every selected cell in this column.
8639
+ const wantsRange = options != null && options.toRange === true || rangeCommitIntent;
8640
+ rangeCommitIntent = false;
8641
+ if (wantsRange && !dropinTabFlush && rangeSize() > 1) {
8642
+ commitEditToRange(value);
8643
+ return;
8644
+ }
8485
8645
  // C-10: during the drop-in wrapper's Tab flush, record the outcome and skip the focus
8486
8646
  // return — the Tab handler advances focus to the next editor itself.
8487
8647
  if (dropinTabFlush) {
@@ -8593,7 +8753,12 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
8593
8753
  }
8594
8754
  return;
8595
8755
  }
8596
- if (key === 'Enter') {
8756
+ if (key === 'Enter' && (e.ctrlKey || e.metaKey) && rangeSize() > 1) {
8757
+ // Ctrl/Cmd+Enter with a range selected: write the draft to every selected cell in this
8758
+ // column. Plain Enter (below) still commits the active cell only.
8759
+ e.preventDefault();
8760
+ commitEditToRange(undefined);
8761
+ } else if (key === 'Enter') {
8597
8762
  e.preventDefault();
8598
8763
  commitEdit(undefined);
8599
8764
  } else if (key === 'Tab') {
@@ -8681,8 +8846,23 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
8681
8846
  // advances. `dropinTabFlush` tells editorCommitFor to report the outcome and skip the focus
8682
8847
  // return. A drop-in that does not commit on blur (EditorCheckbox commits on change, so it holds
8683
8848
  // nothing pending) falls back to the host-draft path, which is exactly right in that case.
8849
+ // Capture-phase keydown on the grid root (registered at mount): runs BEFORE any editor's own
8850
+ // keydown handler. Ctrl/Cmd+Enter marks the commit an editor makes during this keypress as a
8851
+ // range commit; editorCommitFor decides whether a range is actually selected. The mark expires
8852
+ // after this event, so a commit that comes later (on blur, after an async step) writes one
8853
+ // cell, as usual. Reads NO reactive state: it is a mount-time closure (stale on React).
8854
+ function onEditorDropinKeyDownCapture(e: any) {
8855
+ if (!e) return;
8856
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
8857
+ rangeCommitIntent = true;
8858
+ setTimeout(() => {
8859
+ rangeCommitIntent = false;
8860
+ }, 0);
8861
+ }
8862
+ }
8684
8863
  function onEditorDropinKeyDown(e: any) {
8685
8864
  if (!e) return;
8865
+ rangeCommitIntent = false;
8686
8866
  const key = e.key;
8687
8867
  if (inRowEdit()) {
8688
8868
  if (key === 'Enter') {
@@ -9209,7 +9389,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
9209
9389
 
9210
9390
  {<Show when={local.groupable}><div class={"rdt-group-bar-host"} data-rozie-s-d5dcab4c="">
9211
9391
  {(_props.groupBarSlot ?? _props.slots?.['groupBar'])?.({ get grouping() { return groupingKeys(); }, get groupableColumns() { return groupableColumns(); }, applyGrouping, clearGrouping }) ?? <Key each={groupingKeys() as readonly any[]} by={(gk) => gk}>{(gk) => <span class={"rdt-group-token"} data-group-token="" data-rozie-s-d5dcab4c="">{rozieDisplay(gk())}</span>}</Key>}
9212
- </div></Show>}{<Show when={isWindowed()} fallback={<table aria-rowcount={rozieAttr(gridAriaRowCount())} aria-colcount={rozieAttr(gridAriaColCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
9392
+ </div></Show>}{<Show when={isWindowed()} fallback={<table aria-rowcount={rozieAttr(gridAriaRowCount())} aria-colcount={rozieAttr(gridAriaColCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader, 'rdt-column-lines': local.columnLines })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
9213
9393
  <thead class={"rdt-thead"} role="rowgroup" data-rozie-s-d5dcab4c="">
9214
9394
  <Key each={headerGroups() as readonly any[]} by={(hg) => hg.id}>{(hg, hgLevel) => <tr class={"rdt-tr"} role="row" aria-rowindex={rozieAttr(hgLevel() + 1)} data-rozie-s-d5dcab4c="">
9215
9395
  <Key each={hg().headers as readonly any[]} by={(header) => header.id}>{(header) => <th class={"rdt-th" + " " + rozieClass({ 'rdt-select-th': isSelectColumn(header().column.id), 'rdt-expander-th': isExpanderColumn(header().column.id), 'rdt-pinned': columnPinSide(header().column.id) !== '', 'rdt-th-resizing': columnIsResizing(header().column.id), 'rdt-cell-active': isActiveCell('__header', headerColIndexOf(hg(), header()), hgLevel()) })} role="columnheader" data-col={rozieAttr(header().column.id)} data-grid-cell="" data-row="__header" data-header-level={rozieAttr(hgLevel())} colSpan={rozieAttr(header().colSpan > 1 ? header().colSpan : null)} data-col-index={rozieAttr(headerColIndexOf(hg(), header()))} aria-colindex={rozieAttr(headerLeafStart(hg(), header()) + 1)} tabIndex={rozieAttr(cellTabindex('__header', headerColIndexOf(hg(), header()), hgLevel()))} aria-sort={rozieAttr(ariaSortFor(header().column.id))} style={parseInlineStyle(thStyle(header()))} data-rozie-s-d5dcab4c="">
@@ -9261,7 +9441,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
9261
9441
  {_props.slots?.[`cell-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return cell().getValue(); } }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return cell().getValue(); } }) ?? rozieDisplay(cell().getValue())}
9262
9442
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
9263
9443
  {<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<Show when={editorTypeOf(cell().column.id) === 'custom'} fallback={<input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><span style={{ display: "contents" }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c="">
9264
- {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
9444
+ {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); }, get rangeSize() { return inRowEdit() ? 1 : rangeSize(); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(row()); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, rowIndexOf(row())); }, get rangeSize() { return inRowEdit() ? 1 : rangeSize(); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
9265
9445
  </span></Show>}><input type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} ref={(el) => { rozieSelectValue(el as HTMLSelectElement, () => (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="">
9266
9446
  <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>
9267
9447
  </select></Show>}><input type="number" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -9283,7 +9463,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
9283
9463
  </tr></Show>}</>}</Key>
9284
9464
  </tbody>
9285
9465
  </table>}><div class={"rdt-scroll"} style={parseInlineStyle(rowsWindowed() && local.maxHeight ? 'max-height:' + local.maxHeight + ';overflow:auto;--rozie-data-table-max-height:' + local.maxHeight : 'overflow:auto')} data-rozie-s-d5dcab4c="">
9286
- <table aria-rowcount={rozieAttr(gridAriaRowCount())} aria-colcount={rozieAttr(gridAriaColCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader, 'rdt-col-windowed': colsWindowed() })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
9466
+ <table aria-rowcount={rozieAttr(gridAriaRowCount())} aria-colcount={rozieAttr(gridAriaColCount())} class={"rozie-data-table" + " " + rozieClass({ 'rdt-sticky': local.stickyHeader, 'rdt-col-windowed': colsWindowed(), 'rdt-column-lines': local.columnLines })} role={rozieAttr(tableRole())} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridKeyDown($event); }} onFocusIn={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { syncActiveFromEvent($event); }} onFocusOut={($event: FocusEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridFocusOut($event); }} onMouseDown={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridMouseDown($event); }} onDblClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridDblClick($event); }} onClick={($event: MouseEvent & { currentTarget: HTMLTableElement; target: Element }) => { onGridClick($event); }} data-rozie-s-d5dcab4c="">
9287
9467
  <thead class={"rdt-thead"} role="rowgroup" data-rozie-s-d5dcab4c="">
9288
9468
  <Key each={headerGroups() as readonly any[]} by={(hg) => hg.id}>{(hg, hgLevel) => <tr class={"rdt-tr"} role="row" aria-rowindex={rozieAttr(hgLevel() + 1)} data-rozie-s-d5dcab4c="">
9289
9469
 
@@ -9338,7 +9518,7 @@ export default function DataTable(_props: DataTableProps): JSX.Element {
9338
9518
  {_props.slots?.[`cell-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return cell().getValue(); } }) ?? (_props.cellSlot ?? _props.slots?.['cell'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return cell().getValue(); } }) ?? rozieDisplay(cell().getValue())}
9339
9519
  </span>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="" /></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
9340
9520
  {<Show when={editorTypeOf(cell().column.id) === 'number'} fallback={<Show when={editorTypeOf(cell().column.id) === 'select'} fallback={<Show when={editorTypeOf(cell().column.id) === 'checkbox'} fallback={<Show when={editorTypeOf(cell().column.id) === 'custom'} fallback={<input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}><span style={{ display: "contents" }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c="">
9341
- {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
9521
+ {_props.slots?.[`editor-${cell().column.id}`]?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); }, get rangeSize() { return inRowEdit() ? 1 : rangeSize(); } }) ?? (_props.editorSlot ?? _props.slots?.['editor'])?.({ get columnId() { return cell().column.id; }, get column() { return cell().column; }, get row() { return cellSlotRow(wr().row); }, get value() { return editorValueFor(cell().column.id); }, get commit() { return editorCommitFor(cell().column.id); }, get cancel() { return editorCancelFor(); }, get columnLabel() { return headerLabel(cell().column.id); }, get autofocus() { return editorAutofocusFor(cell().column.id, wr().vi.index); }, get rangeSize() { return inRowEdit() ? 1 : rangeSize(); } }) ?? <input type="text" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" />}
9342
9522
  </span></Show>}><input type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} checked={editorCheckedFor(cell().column.id)} onChange={($event: Event & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorCheckbox(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}><select data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} ref={(el) => { rozieSelectValue(el as HTMLSelectElement, () => (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="">
9343
9523
  <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>
9344
9524
  </select></Show>}><input type="number" data-editing-cell="" data-builtin-editor="" aria-invalid={rozieAttr(invalidMsg() ? 'true' : null)} class={"rdt-cell-editor"} value={editorValueFor(cell().column.id)} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onCellEditorInput(cell().column.id, $event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorKeyDown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onEditorBlur($event); }} data-rozie-s-d5dcab4c="" /></Show>}</span></Show>}><span style={{ display: "contents" }} data-rozie-s-d5dcab4c="">
@@ -0,0 +1,22 @@
1
+ // rangeFill.ts — which cells a range fill writes. Pure, sigil-free.
2
+ //
3
+ // A range fill (Ctrl/Cmd+Enter in an editor, or Space on a checkbox range) writes one value
4
+ // down the ACTIVE cell's column for every row the range covers. Other columns of a
5
+ // multi-column range are left alone: one typed value rarely makes sense in several columns.
6
+ // With no range, or an active cell outside it, the fill is the active cell alone (size 1).
7
+
8
+ export interface FillRange { r0: number; r1: number; c0: number; c1: number }
9
+ export interface FillTargets { top: number; bottom: number; col: number; size: number }
10
+
11
+ const rangeFillTargets = (range: FillRange | null, activeRow: number, activeCol: number): FillTargets => {
12
+ const single = { top: activeRow, bottom: activeRow, col: activeCol, size: 1 }
13
+ if (!range) return single
14
+ const top = Math.min(range.r0, range.r1)
15
+ const bottom = Math.max(range.r0, range.r1)
16
+ const left = Math.min(range.c0, range.c1)
17
+ const right = Math.max(range.c0, range.c1)
18
+ if (activeCol < left || activeCol > right || activeRow < top || activeRow > bottom) return single
19
+ return { top, bottom, col: activeCol, size: bottom - top + 1 }
20
+ }
21
+
22
+ export { rangeFillTargets }
@@ -1,15 +1,15 @@
1
1
  /*
2
2
  * @rozie-ui/data-table — base token reference.
3
3
  *
4
- * This file is the PUBLIC token reference. It wires each of the 18 public
4
+ * This file is the PUBLIC token reference. It wires each of the 20 public
5
5
  * `--rozie-data-table-*` tokens, with its default, onto the short internal `--rdt-*`
6
- * token the component's scoped `<style>` actually reads. The component reads 82 internal `--rdt-*`
6
+ * token the component's scoped `<style>` actually reads. The component reads 84 internal `--rdt-*`
7
7
  * tokens in total, each with an inline `var(--rdt-x, <fallback>)` default, so the
8
8
  * table renders ZERO-CONFIG without importing this file.
9
9
  *
10
10
  * Scope, stated plainly: remapping the public tokens re-skins the table's primary
11
11
  * surfaces — type, borders, background, header, row, cell padding, accent, and the
12
- * max-height. It does NOT reach every cosmetic value. 58 of the 82 internal tokens
12
+ * max-height. It does NOT reach every cosmetic value. 58 of the 84 internal tokens
13
13
  * have no public counterpart today, including the grid active-cell ring, the range
14
14
  * highlight, the fill handle, the invalid-cell outline, the group tokens, the filter
15
15
  * row, the per-header column ⋯ menu (its panel is a composed @rozie-ui/popover, so its
@@ -27,7 +27,7 @@
27
27
  * the element itself), never on the `.rozie-data-table` / `.rozie-data-table-wrap`
28
28
  * classes — on that target both of those render inside the component's shadow root,
29
29
  * where a document stylesheet cannot reach them. The tokens themselves inherit across
30
- * the boundary, and the component carries the mapping internally, so every one of the 18
30
+ * the boundary, and the component carries the mapping internally, so every one of the 20
31
31
  * works there.
32
32
  *
33
33
  * NOTE the split: the STRUCTURAL rules — `border-collapse`, the keyed-r-for
@@ -75,6 +75,12 @@
75
75
  --rdt-border: 1px solid var(--rozie-data-table-border-color, rgba(0, 0, 0, 0.08));
76
76
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, rgba(0, 0, 0, 0.2));
77
77
  --rdt-filter-radius: var(--rozie-data-table-radius, 4px);
78
+ /* Column lines are off unless the `columnLines` prop is set. Their colour follows the
79
+ row-line colour unless the column token is set, so one variable themes both. */
80
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, rgba(0, 0, 0, 0.08)));
81
+ /* Pinned body cells are opaque so scrolled content never shows through them. Set this
82
+ to the surface colour behind the table. */
83
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, #fff);
78
84
 
79
85
  /* ── typography / cell metrics ──────────────────────────────────────────── */
80
86
  --rdt-font: var(--rozie-data-table-font, 14px system-ui, sans-serif);
@@ -125,6 +131,8 @@
125
131
  --rdt-ds-fg: currentColor;
126
132
  --rdt-ds-select-accent: #0066cc;
127
133
  --rdt-ds-border-color: rgba(0, 0, 0, 0.08);
134
+ --rdt-ds-column-border-color: var(--rdt-ds-border-color);
135
+ --rdt-ds-pinned-bg: #fff;
128
136
  --rdt-ds-control-border-color: rgba(0, 0, 0, 0.2);
129
137
  --rdt-ds-radius: 4px;
130
138
  --rdt-ds-font: 14px system-ui, sans-serif;
@@ -35,6 +35,8 @@ rozie-data-table {
35
35
  --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--bs-border-color, #dee2e6));
36
36
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--bs-border-color, #dee2e6));
37
37
  --rdt-filter-radius: var(--rozie-data-table-radius, var(--bs-border-radius, 0.375rem));
38
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--bs-border-color, #dee2e6)));
39
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--bs-body-bg, #fff));
38
40
  --rdt-header-bg: var(--rozie-data-table-header-bg, var(--bs-tertiary-bg, #f8f9fa));
39
41
  --rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color, var(--bs-border-color, #dee2e6));
40
42
  --rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active, var(--rozie-data-table-accent, var(--bs-primary, #0d6efd)));
@@ -60,6 +62,7 @@ rozie-data-table {
60
62
  --rdt-ds-fg: var(--bs-body-color, #212529);
61
63
  --rdt-ds-select-accent: var(--bs-primary, #0d6efd);
62
64
  --rdt-ds-border-color: var(--bs-border-color, #dee2e6);
65
+ --rdt-ds-pinned-bg: var(--bs-body-bg, #fff);
63
66
  --rdt-ds-control-border-color: var(--bs-border-color, #dee2e6);
64
67
  --rdt-ds-radius: var(--bs-border-radius, 0.375rem);
65
68
  --rdt-ds-header-bg: var(--bs-tertiary-bg, #f8f9fa);
@@ -33,6 +33,8 @@ rozie-data-table {
33
33
  --rdt-color: var(--rozie-data-table-fg, var(--md-sys-color-on-surface, #1d1b20));
34
34
  --rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, var(--md-sys-color-primary, #6750a4)));
35
35
  --rdt-border: 1px solid var(--rozie-data-table-border-color, var(--md-sys-color-outline-variant, #cac4d0));
36
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--md-sys-color-outline-variant, #cac4d0)));
37
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--md-sys-color-surface, #fef7ff));
36
38
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--md-sys-color-outline, #79747e));
37
39
  --rdt-header-bg: var(--rozie-data-table-header-bg, var(--md-sys-color-surface-container-highest, #e6e0e9));
38
40
  --rdt-header-weight: var(--rozie-data-table-header-weight, 500);
@@ -55,6 +57,7 @@ rozie-data-table {
55
57
  --rdt-ds-fg: var(--md-sys-color-on-surface, #1d1b20);
56
58
  --rdt-ds-select-accent: var(--md-sys-color-primary, #6750a4);
57
59
  --rdt-ds-border-color: var(--md-sys-color-outline-variant, #cac4d0);
60
+ --rdt-ds-pinned-bg: var(--md-sys-color-surface, #fef7ff);
58
61
  --rdt-ds-control-border-color: var(--md-sys-color-outline, #79747e);
59
62
  --rdt-ds-radius: 4px;
60
63
  --rdt-ds-header-bg: var(--md-sys-color-surface-container-highest, #e6e0e9);
@@ -39,6 +39,8 @@ rozie-data-table {
39
39
  --rdt-color: var(--rozie-data-table-fg, hsl(var(--foreground, 222.2 47.4% 11.2%)));
40
40
  --rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, hsl(var(--primary, 222.2 47.4% 11.2%))));
41
41
  --rdt-border: 1px solid var(--rozie-data-table-border-color, hsl(var(--border, 214.3 31.8% 91.4%)));
42
+ --rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, hsl(var(--border, 214.3 31.8% 91.4%))));
43
+ --rdt-pinned-bg: var(--rozie-data-table-pinned-bg, hsl(var(--background, 0 0% 100%)));
42
44
  --rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, hsl(var(--border, 214.3 31.8% 91.4%)));
43
45
  --rdt-filter-radius: var(--rozie-data-table-radius, var(--radius, 0.5rem));
44
46
  --rdt-header-bg: var(--rozie-data-table-header-bg, hsl(var(--muted, 210 40% 96.1%)));
@@ -65,6 +67,7 @@ rozie-data-table {
65
67
  --rdt-ds-fg: hsl(var(--foreground, 222.2 47.4% 11.2%));
66
68
  --rdt-ds-select-accent: hsl(var(--primary, 222.2 47.4% 11.2%));
67
69
  --rdt-ds-border-color: hsl(var(--border, 214.3 31.8% 91.4%));
70
+ --rdt-ds-pinned-bg: hsl(var(--background, 0 0% 100%));
68
71
  --rdt-ds-control-border-color: hsl(var(--border, 214.3 31.8% 91.4%));
69
72
  --rdt-ds-radius: var(--radius, 0.5rem);
70
73
  --rdt-ds-header-bg: hsl(var(--muted, 210 40% 96.1%));