@rozie-ui/data-table-solid 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -10
- package/dist/index.cjs +3162 -1095
- package/dist/index.d.cts +69 -1
- package/dist/index.d.mts +69 -1
- package/dist/index.mjs +3086 -1019
- package/dist/source/index.jsx +6377 -0
- package/package.json +8 -3
- package/src/Column.tsx +1 -1
- package/src/DataTable.tsx +1233 -135
- package/src/EditorCheckbox.tsx +38 -4
- package/src/EditorDate.tsx +79 -8
- package/src/EditorNumber.tsx +52 -6
- package/src/EditorSelect.tsx +51 -6
- package/src/EditorText.tsx +34 -5
- package/src/FilterNumberRange.tsx +79 -12
- package/src/FilterSelect.tsx +18 -3
- package/src/FilterText.tsx +48 -9
- package/src/GroupBar.tsx +75 -3
- package/src/helpers/columnDefUtils.ts +73 -1
- package/src/helpers/dateValue.ts +55 -0
- package/src/index.ts +1 -1
- package/src/themes/base.css +96 -75
- package/src/themes/bootstrap.css +59 -34
- package/src/themes/material.css +53 -32
- package/src/themes/shadcn.css +58 -33
package/src/FilterSelect.tsx
CHANGED
|
@@ -24,11 +24,15 @@ interface FilterSelectProps {
|
|
|
24
24
|
* The faceted distinct keys for this column (cross-filtered, keys only — no occurrence counts) used to build the `<option>` list.
|
|
25
25
|
*/
|
|
26
26
|
uniqueValues?: any[];
|
|
27
|
+
/**
|
|
28
|
+
* The column's human header, forwarded by the `#filter` slot scope — used as the control's accessible name in place of the internal column id.
|
|
29
|
+
*/
|
|
30
|
+
columnLabel?: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export default function FilterSelect(_props: FilterSelectProps): JSX.Element {
|
|
30
|
-
const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, uniqueValues: (() => [])() as any[] }, _props);
|
|
31
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'uniqueValues']);
|
|
34
|
+
const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, uniqueValues: (() => [])() as any[], columnLabel: '' }, _props);
|
|
35
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'uniqueValues', 'columnLabel']);
|
|
32
36
|
|
|
33
37
|
// The <select> value binding coerced to a string. $props.value is typed `unknown`
|
|
34
38
|
// (opaque slot-scope), which the strict bundled-leaf tsc rejects against the native
|
|
@@ -49,9 +53,20 @@ export default function FilterSelect(_props: FilterSelectProps): JSX.Element {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
57
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
58
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
59
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
60
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
61
|
+
// than invent one.
|
|
62
|
+
function a11yLabel() {
|
|
63
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
64
|
+
return local.columnId;
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
return (
|
|
53
68
|
<>
|
|
54
|
-
<select part="col-filter" aria-label={
|
|
69
|
+
<select part="col-filter" aria-label={rozieAttr(a11yLabel())} class={"rdt-col-filter"} value={selectValue()} onChange={($event: Event & { currentTarget: HTMLSelectElement; target: Element }) => { onChange($event); }} data-rozie-s-d75b42b2="">
|
|
55
70
|
<option value="" data-rozie-s-d75b42b2="">All</option>
|
|
56
71
|
<Key each={local.uniqueValues as readonly any[]} by={(opt) => opt}>{(opt) => <option value={rozieAttr(opt())} data-rozie-s-d75b42b2="">{rozieDisplay(opt())}</option>}</Key>
|
|
57
72
|
</select>
|
package/src/FilterText.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from 'solid-js';
|
|
2
|
-
import { createSignal, mergeProps, splitProps } from 'solid-js';
|
|
2
|
+
import { createEffect, createSignal, mergeProps, on, splitProps, untrack } from 'solid-js';
|
|
3
|
+
import { rozieAttr } from '@rozie/runtime-solid';
|
|
3
4
|
|
|
4
5
|
interface FilterTextProps {
|
|
5
6
|
/**
|
|
@@ -18,30 +19,57 @@ interface FilterTextProps {
|
|
|
18
19
|
* `(columnId, value) => void` — apply the column filter (Enter / blur applies, Escape clears). Null-guarded at call sites.
|
|
19
20
|
*/
|
|
20
21
|
setFilter?: ((...args: any[]) => any) | null;
|
|
22
|
+
/**
|
|
23
|
+
* The column's human header, forwarded by the `#filter` slot scope — used as the control's accessible name in place of the internal column id.
|
|
24
|
+
*/
|
|
25
|
+
columnLabel?: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export default function FilterText(_props: FilterTextProps): JSX.Element {
|
|
24
|
-
const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null }, _props);
|
|
25
|
-
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter']);
|
|
29
|
+
const _merged = mergeProps({ columnId: '', column: null, value: null, setFilter: null, columnLabel: '' }, _props);
|
|
30
|
+
const [local, attrs] = splitProps(_merged, ['columnId', 'column', 'value', 'setFilter', 'columnLabel']);
|
|
26
31
|
|
|
27
32
|
const [draft, setDraft] = createSignal('');
|
|
33
|
+
const [touched, setTouched] = createSignal(false);
|
|
34
|
+
createEffect(on(() => (() => local.value)(), (v) => untrack(() => (() => {
|
|
35
|
+
setTouched(false);
|
|
36
|
+
})()), { defer: true }));
|
|
28
37
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
// ── C-08 + N-04: the draft is DERIVED from `$props.value`, with a `touched` latch ────────
|
|
39
|
+
//
|
|
40
|
+
// C-08 — this component mounts once per rendered filter row and is NOT remounted when the
|
|
41
|
+
// filter changes, and its draft was seeded setup-once. So a programmatic reset (a "Clear
|
|
42
|
+
// filters" button, `setFilter(id, '')` from another control, a consumer writing
|
|
43
|
+
// `columnFilters` directly) moved the real filter while this input went on displaying the old
|
|
44
|
+
// text: the UI stated a filter that was no longer applied. `FilterSelect` was never affected —
|
|
45
|
+
// it reads `$props.value` live through `selectValue()`, which is the pattern adopted here.
|
|
46
|
+
//
|
|
47
|
+
// N-04 — the setup-once seed was ALSO wrong on Angular from the start: the emitter places
|
|
48
|
+
// setup-once statements in the CONSTRUCTOR, where an `input()` signal still returns its
|
|
49
|
+
// DEFAULT, because Angular sets inputs after construction. Measured on the sibling EditorDate,
|
|
50
|
+
// which opened empty on angular for every row while vue and lit seeded correctly. A derived
|
|
51
|
+
// read is correct on all six by construction, with no flash and no per-target branch.
|
|
52
|
+
//
|
|
53
|
+
// `touched` is what stops the live read from overwriting the user mid-type; the watch clears
|
|
54
|
+
// it, so an OUTSIDE change always wins and the input follows it. `@input` never moves
|
|
55
|
+
// `$props.value` (nothing is applied until Enter/blur), so the two can never fight.
|
|
56
|
+
function draftValue() {
|
|
57
|
+
return touched() ? draft() : local.value != null ? String(local.value) : '';
|
|
58
|
+
}
|
|
33
59
|
// Untyped handler param neutralizes to `any`, so reading e.target.value typechecks
|
|
34
60
|
// ×6 (the global-filter idiom). Never inline `$data.x = $event.target.value`.
|
|
35
61
|
function onInput(e: any) {
|
|
36
62
|
setDraft(e && e.target ? e.target.value : '');
|
|
63
|
+
setTouched(true);
|
|
37
64
|
}
|
|
38
65
|
|
|
39
66
|
// setFilter is a Function prop (default null) — guard before calling.
|
|
40
67
|
function applyFilter() {
|
|
41
|
-
local.setFilter && local.setFilter(local.columnId,
|
|
68
|
+
local.setFilter && local.setFilter(local.columnId, draftValue());
|
|
42
69
|
}
|
|
43
70
|
function clearFilter() {
|
|
44
71
|
setDraft('');
|
|
72
|
+
setTouched(false);
|
|
45
73
|
local.setFilter && local.setFilter(local.columnId, '');
|
|
46
74
|
}
|
|
47
75
|
function onKeydown(e: any) {
|
|
@@ -57,9 +85,20 @@ export default function FilterText(_props: FilterTextProps): JSX.Element {
|
|
|
57
85
|
applyFilter();
|
|
58
86
|
}
|
|
59
87
|
|
|
88
|
+
// C-11 — the accessible name must be the column's HUMAN header, not its internal id.
|
|
89
|
+
// `columnId` is a lookup key (`unit_price`, `col_3`); a screen reader announcing it is
|
|
90
|
+
// reading an implementation detail aloud. table-core keeps the authored header on
|
|
91
|
+
// `column.columnDef.header`, which is a string for every declarative `<Column header>`;
|
|
92
|
+
// a header rendered by a function has no static text, so fall back to the id rather
|
|
93
|
+
// than invent one.
|
|
94
|
+
function a11yLabel() {
|
|
95
|
+
if (typeof local.columnLabel === 'string' && local.columnLabel !== '') return local.columnLabel;
|
|
96
|
+
return local.columnId;
|
|
97
|
+
}
|
|
98
|
+
|
|
60
99
|
return (
|
|
61
100
|
<>
|
|
62
|
-
<input part="col-filter" type="text" aria-label={
|
|
101
|
+
<input part="col-filter" type="text" aria-label={rozieAttr(a11yLabel())} class={"rdt-col-filter"} value={draftValue()} onInput={($event: InputEvent & { currentTarget: HTMLInputElement; target: Element }) => { onInput($event); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: Element }) => { onKeydown($event); }} onBlur={($event: FocusEvent & { currentTarget: HTMLInputElement; target: Element }) => { onBlur(); }} data-rozie-s-18cbb44e="" />
|
|
63
102
|
</>
|
|
64
103
|
);
|
|
65
104
|
}
|
package/src/GroupBar.tsx
CHANGED
|
@@ -20,6 +20,18 @@ __rozieInjectStyle('GroupBar-546c469a', `.rdt-group-drop-zone[data-rozie-s-546c4
|
|
|
20
20
|
border-color: var(--rdt-group-drop-zone-border-over, rgba(37, 99, 235, 0.7));
|
|
21
21
|
background: var(--rdt-group-drop-zone-bg-over, rgba(37, 99, 235, 0.08));
|
|
22
22
|
}
|
|
23
|
+
.rdt-group-token-add[data-rozie-s-546c469a] {
|
|
24
|
+
font: inherit;
|
|
25
|
+
color: inherit;
|
|
26
|
+
border: none;
|
|
27
|
+
background: none;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
.rdt-group-token-add[data-rozie-s-546c469a]:focus-visible,
|
|
31
|
+
.rdt-group-drop-zone[data-rozie-s-546c469a] [data-group-token][data-rozie-s-546c469a]:focus-visible {
|
|
32
|
+
outline: var(--rdt-focus-ring, 2px solid #2563eb);
|
|
33
|
+
outline-offset: 1px;
|
|
34
|
+
}
|
|
23
35
|
.rdt-group-drop-hint[data-rozie-s-546c469a] {
|
|
24
36
|
opacity: 0.55;
|
|
25
37
|
font-size: 0.8125em;
|
|
@@ -173,6 +185,66 @@ export default function GroupBar(_props: GroupBarProps): JSX.Element {
|
|
|
173
185
|
function removeKey(key: any) {
|
|
174
186
|
local.applyGrouping && local.applyGrouping(local.grouping.filter((k: any) => k !== key));
|
|
175
187
|
}
|
|
188
|
+
|
|
189
|
+
// ── C-04: the keyboard half of the group bar ────────────────────────────────────────────
|
|
190
|
+
// DECLARED AFTER removeKey ON PURPOSE: toggleKey and onTokenKeydown call it, and the React
|
|
191
|
+
// emitter lowers each to a useCallback whose dependency array is evaluated EAGERLY — declared
|
|
192
|
+
// above removeKey, every React GroupBar died at mount with a TDZ ReferenceError (measured in
|
|
193
|
+
// quick 260922-mkb; the same 87-02 ordering lesson as onEditorDropinKeyDown).
|
|
194
|
+
// Native HTML5 drag-and-drop was the SOLE input path for both adding and reordering, on
|
|
195
|
+
// elements with no tabindex, no role and no click/keydown handler. Remove and Clear were real
|
|
196
|
+
// `<button>`s, so a keyboard user could UNGROUP but could never group or reorder — the bar was
|
|
197
|
+
// a one-way door. HTML5 DnD has no keyboard equivalent by construction, so a parallel,
|
|
198
|
+
// explicitly-keyboard path is the only fix; both halves write through the SAME
|
|
199
|
+
// `applyGrouping` funnel the drop handler uses, so there is one ordering rule, not two.
|
|
200
|
+
//
|
|
201
|
+
// The palette chips become real `<button>`s (Enter/Space for free, focusable for free) rather
|
|
202
|
+
// than spans with `tabindex` + a hand-rolled key handler — the same reasoning that already
|
|
203
|
+
// made Remove and Clear buttons. A `<button draggable="true">` keeps the existing pointer
|
|
204
|
+
// path working unchanged.
|
|
205
|
+
//
|
|
206
|
+
// Reorder is Alt+Arrow on a grouping token, not plain Arrow: the tokens sit in a toolbar that
|
|
207
|
+
// a user also arrows THROUGH, and stealing bare arrows would trap them. Alt is the modifier
|
|
208
|
+
// Windows/macOS list reordering conventionally uses.
|
|
209
|
+
function toggleKey(id: any) {
|
|
210
|
+
if (!id) return;
|
|
211
|
+
if (local.grouping.indexOf(id) !== -1) {
|
|
212
|
+
removeKey(id);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
local.applyGrouping && local.applyGrouping(local.grouping.concat([id]));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Move a grouping key one position left/right. No-op at the ends (never wraps: a wrap would
|
|
219
|
+
// make a held key cycle forever with no signal that the end was reached).
|
|
220
|
+
function moveKey(key: any, delta: any) {
|
|
221
|
+
const cur = local.grouping;
|
|
222
|
+
const from = cur.indexOf(key);
|
|
223
|
+
if (from === -1) return;
|
|
224
|
+
const to = from + delta;
|
|
225
|
+
if (to < 0 || to >= cur.length) return;
|
|
226
|
+
const next = cur.slice();
|
|
227
|
+
next.splice(from, 1);
|
|
228
|
+
next.splice(to, 0, key);
|
|
229
|
+
local.applyGrouping && local.applyGrouping(next);
|
|
230
|
+
}
|
|
231
|
+
function onTokenKeydown(e: any, gk: any) {
|
|
232
|
+
if (!e) return;
|
|
233
|
+
const key = e.key;
|
|
234
|
+
if (key === 'Delete' || key === 'Backspace') {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
removeKey(gk);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!e.altKey) return;
|
|
240
|
+
if (key === 'ArrowLeft') {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
moveKey(gk, -1);
|
|
243
|
+
} else if (key === 'ArrowRight') {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
moveKey(gk, 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
176
248
|
function clearAll() {
|
|
177
249
|
local.clearGrouping && local.clearGrouping();
|
|
178
250
|
}
|
|
@@ -189,12 +261,12 @@ export default function GroupBar(_props: GroupBarProps): JSX.Element {
|
|
|
189
261
|
<>
|
|
190
262
|
<div class={"rdt-group-bar"} data-rozie-s-546c469a="">
|
|
191
263
|
|
|
192
|
-
<Key each={local.groupableColumns as readonly any[]} by={(col) => col.id}>{(col) => <
|
|
264
|
+
<Key each={local.groupableColumns as readonly any[]} by={(col) => col.id}>{(col) => <button type="button" part="group-token" draggable="true" aria-pressed={rozieAttr(local.grouping.indexOf(col().id) !== -1 ? 'true' : 'false')} aria-label={rozieAttr('Group by ' + col().label)} class={"rdt-group-token rdt-group-token-add"} onDragStart={($event: DragEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onChipDragStart($event, col().id); }} onDragEnd={($event: DragEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onDragEnd(); }} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { toggleKey(col().id); }} data-rozie-s-546c469a="">{rozieDisplay(col().label)}</button>}</Key>
|
|
193
265
|
|
|
194
266
|
|
|
195
|
-
<span data-group-drop-zone="" class={"rdt-group-drop-zone" + " " + rozieClass({ 'is-over': isOver() })} onDragOver={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDragOver($event); }} onDragLeave={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDragLeave($event); }} onDrop={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDrop($event); }} data-rozie-s-546c469a="">
|
|
267
|
+
<span data-group-drop-zone="" role="list" aria-label="Active grouping" class={"rdt-group-drop-zone" + " " + rozieClass({ 'is-over': isOver() })} onDragOver={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDragOver($event); }} onDragLeave={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDragLeave($event); }} onDrop={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDrop($event); }} data-rozie-s-546c469a="">
|
|
196
268
|
|
|
197
|
-
{<Show when={!local.grouping.length}><span class={"rdt-group-drop-hint"} data-rozie-s-546c469a="">Drag columns here to group</span></Show>}<Key each={local.grouping as readonly any[]} by={(gk) => gk}>{(gk) => <span part="group-token" data-group-token="" draggable="true" class={"rdt-group-token" + " " + rozieClass({ 'is-drop-target': dragKind() === 'token' && dropKey() === gk() && draggingId() !== gk() })} onDragStart={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onTokenDragStart($event, gk()); }} onDragOver={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onTokenDragOver($event, gk()); }} onDragEnd={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDragEnd(); }} data-rozie-s-546c469a="">
|
|
269
|
+
{<Show when={!local.grouping.length}><span class={"rdt-group-drop-hint"} data-rozie-s-546c469a="">Drag columns here to group</span></Show>}<Key each={local.grouping as readonly any[]} by={(gk) => gk}>{(gk) => <span part="group-token" data-group-token="" draggable="true" role="listitem" aria-label={rozieAttr(labelFor(gk()) + ' grouping, position ' + (local.grouping.indexOf(gk()) + 1) + ' of ' + local.grouping.length + '. Alt+Arrow to reorder, Delete to remove.')} class={"rdt-group-token" + " " + rozieClass({ 'is-drop-target': dragKind() === 'token' && dropKey() === gk() && draggingId() !== gk() })} tabIndex={0} onDragStart={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onTokenDragStart($event, gk()); }} onDragOver={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onTokenDragOver($event, gk()); }} onDragEnd={($event: DragEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onDragEnd(); }} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: Element }) => { onTokenKeydown($event, gk()); }} data-rozie-s-546c469a="">
|
|
198
270
|
{rozieDisplay(labelFor(gk()))}
|
|
199
271
|
<button type="button" aria-label={rozieAttr('Remove ' + labelFor(gk()) + ' grouping')} class={"rdt-group-token-remove"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { removeKey(gk()); }} data-rozie-s-546c469a="">×</button>
|
|
200
272
|
</span>}</Key>
|
|
@@ -108,4 +108,76 @@ const collectGroupableLeafDefs = (defs: any): any[] => {
|
|
|
108
108
|
return out
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
// ── E-03: the `editor` union is FOUR built-ins plus the 'custom' gate ────────────────────
|
|
112
|
+
// `editorTypeOf` falls through to the plain text `<input>` for ANY unrecognised value, so a
|
|
113
|
+
// consumer writing `editor="date"` — a value the comparison page and the root README both
|
|
114
|
+
// advertised as a fifth built-in until the 260910 audit corrected them — got a text box with
|
|
115
|
+
// no error, no warning, and no hint that `EditorDate` is a DROP-IN reached through
|
|
116
|
+
// `editor="custom"` + an `#editor` fill. Same for any typo. The silent degradation was the
|
|
117
|
+
// defect; the docs half is already corrected.
|
|
118
|
+
//
|
|
119
|
+
// Pure and message-returning rather than warning in place, so the contract (which values are
|
|
120
|
+
// accepted, and what a rejected one is told) is unit-testable; `columnBuilders.rzts` owns the
|
|
121
|
+
// latch and the console.warn.
|
|
122
|
+
const EDITOR_KINDS = ['text', 'number', 'select', 'checkbox', 'custom']
|
|
123
|
+
const editorKindWarning = (id: any, editor: any): string | null => {
|
|
124
|
+
if (editor == null) return null
|
|
125
|
+
if (typeof editor === 'string' && EDITOR_KINDS.indexOf(editor) !== -1) return null
|
|
126
|
+
return (
|
|
127
|
+
'[rozie-data-table] column "' + String(id) + '": editor="' + String(editor) + '" is not a ' +
|
|
128
|
+
'built-in editor. The built-ins are text | number | select | checkbox; anything else needs ' +
|
|
129
|
+
'editor="custom" plus an #editor slot fill (that is how <EditorDate> is used). Falling back ' +
|
|
130
|
+
'to the text input.'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// ── C-09: is this column spec EQUIVALENT to the one already registered? ──────────────────
|
|
136
|
+
// `<Column>`'s re-register `$watch` keys on `$props.editorOptions`, `$props.aggregationFn` and
|
|
137
|
+
// `$props.validate` — all reference types. The documented wiring for them is an INLINE literal
|
|
138
|
+
// (`:editorOptions="[{ value: 'a' }, …]"`, `:validate="(v) => v !== ''"`), and an inline
|
|
139
|
+
// literal is a NEW identity on every consumer render. So the watch fired every render,
|
|
140
|
+
// `registerColumn` whole-object-replaced `$data.colReg`, the parent's re-feed watch keys on
|
|
141
|
+
// `$data.colReg`, the re-feed re-rendered the parent, and the next render produced a new
|
|
142
|
+
// identity again: not a slow path, a feedback loop.
|
|
143
|
+
//
|
|
144
|
+
// Equality is by VALUE, with functions compared on identity first and `toString()` second.
|
|
145
|
+
// Source-text comparison is the point rather than a shortcut: an inline arrow re-created each
|
|
146
|
+
// render has a different identity and identical source, which is exactly the case that has to
|
|
147
|
+
// stop churning, while a genuinely different validator has different source and still
|
|
148
|
+
// re-registers. Depth-bounded so a consumer object graph with a cycle cannot hang the compare.
|
|
149
|
+
const columnSpecsEquivalent = (a: any, b: any, depth?: number): boolean => {
|
|
150
|
+
const d = typeof depth === 'number' ? depth : 0
|
|
151
|
+
if (a === b) return true
|
|
152
|
+
if (d > 4) return false
|
|
153
|
+
const ta = typeof a
|
|
154
|
+
const tb = typeof b
|
|
155
|
+
if (ta !== tb) return false
|
|
156
|
+
if (ta === 'function') {
|
|
157
|
+
try {
|
|
158
|
+
return String(a) === String(b)
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (ta === 'number') return a !== a && b !== b // NaN === NaN, for this purpose
|
|
164
|
+
if (a === null || b === null || ta !== 'object') return false
|
|
165
|
+
const aArr = Array.isArray(a)
|
|
166
|
+
if (aArr !== Array.isArray(b)) return false
|
|
167
|
+
if (aArr) {
|
|
168
|
+
if (a.length !== b.length) return false
|
|
169
|
+
for (let i = 0; i < a.length; i++) if (!columnSpecsEquivalent(a[i], b[i], d + 1)) return false
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
const ka = Object.keys(a)
|
|
173
|
+
const kb = Object.keys(b)
|
|
174
|
+
if (ka.length !== kb.length) return false
|
|
175
|
+
for (let i = 0; i < ka.length; i++) {
|
|
176
|
+
const k = ka[i]
|
|
177
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false
|
|
178
|
+
if (!columnSpecsEquivalent(a[k], b[k], d + 1)) return false
|
|
179
|
+
}
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { isSafeKey, wrapAggregationFn, collectNestedDefs, indexDefsById, collectGroupableLeafDefs, EDITOR_KINDS, editorKindWarning, columnSpecsEquivalent }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// dateValue.ts — C-07. The ISO-date coercion `EditorDate` binds its native
|
|
2
|
+
// `<input type="date">` to. Pure and sigil-free (no $props/$data/...), so it is unit-testable
|
|
3
|
+
// and identical across all six leaves (helpers/ is vendored into each one by codegen).
|
|
4
|
+
//
|
|
5
|
+
// `EditorDate` seeded its draft with `String($props.value)` while its own `docs:` string —
|
|
6
|
+
// which ships as JSDoc in every leaf `.d.ts` — promised "String-coerced to an ISO
|
|
7
|
+
// `YYYY-MM-DD` string". `String()` is not that. A native date input accepts ONLY
|
|
8
|
+
// `YYYY-MM-DD` and silently renders BLANK for anything else, so every ordinary way of
|
|
9
|
+
// holding a date in a model produced an empty editor with no error:
|
|
10
|
+
// a `Date` object -> "Mon Sep 21 2026 00:00:00 GMT-0700 (…)" -> blank
|
|
11
|
+
// an ISO datetime string -> "2026-09-21T00:00:00.000Z" -> blank
|
|
12
|
+
// an epoch number -> "1789084800000" -> blank
|
|
13
|
+
// a localised string -> "09/21/2026" -> blank
|
|
14
|
+
// Only a value that was ALREADY `YYYY-MM-DD` worked, i.e. the one case needing no coercion.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Coerce an arbitrary cell value to the `YYYY-MM-DD` string a native date input accepts.
|
|
18
|
+
* Returns `''` for anything that cannot be read as a date — which is what the input shows for
|
|
19
|
+
* "no date", so an unparseable value degrades to empty rather than to a broken control.
|
|
20
|
+
*/
|
|
21
|
+
const toIsoDateString = (v: unknown): string => {
|
|
22
|
+
if (v == null || v === '') return ''
|
|
23
|
+
|
|
24
|
+
// Already the target format.
|
|
25
|
+
if (typeof v === 'string') {
|
|
26
|
+
const s = v.trim()
|
|
27
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s
|
|
28
|
+
// An ISO datetime: take the DATE PART AS WRITTEN. Parsing it and re-formatting from local
|
|
29
|
+
// parts shifts the day for anyone west of UTC ('2026-09-21T00:00:00Z' would render as the
|
|
30
|
+
// 20th in UTC-5), which is the classic off-by-one this branch exists to avoid.
|
|
31
|
+
const iso = /^(\d{4}-\d{2}-\d{2})T/.exec(s)
|
|
32
|
+
if (iso) return iso[1]
|
|
33
|
+
return fromDate(new Date(s))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof v === 'number') return Number.isFinite(v) ? fromDate(new Date(v)) : ''
|
|
37
|
+
if (typeof v === 'object' && typeof (v as Date).getTime === 'function') return fromDate(v as Date)
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format a Date as `YYYY-MM-DD` from its LOCAL parts. Local, not UTC: the input renders a
|
|
43
|
+
* calendar day, and `toISOString()` would show the previous day for anyone west of UTC on a
|
|
44
|
+
* midnight-local value.
|
|
45
|
+
*/
|
|
46
|
+
const fromDate = (d: Date): string => {
|
|
47
|
+
const t = d.getTime()
|
|
48
|
+
if (!Number.isFinite(t)) return ''
|
|
49
|
+
const y = d.getFullYear()
|
|
50
|
+
const m = d.getMonth() + 1
|
|
51
|
+
const day = d.getDate()
|
|
52
|
+
return String(y).padStart(4, '0') + '-' + String(m).padStart(2, '0') + '-' + String(day).padStart(2, '0')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { toIsoDateString }
|
package/src/index.ts
CHANGED
|
@@ -12,5 +12,5 @@ export { default as FilterSelect } from './FilterSelect';
|
|
|
12
12
|
export { default as GroupBar } from './GroupBar';
|
|
13
13
|
export { default as DetailPanel } from './DetailPanel';
|
|
14
14
|
|
|
15
|
-
/** The `$expose` imperative handle received via `ref` — { sortColumn, clearSorting, toggleRowExpanded, expandAll, collapseAll, getExpandedRows, applyGrouping, clearGrouping, getFacetedUniqueValues, getFacetedMinMaxValues, getColumnDefs, toggleAllRows, clearSelection, getSelectedRows, setPage, setRowsPerPage, toggleColumnVisibility, applyColumnOrder, resetColumnSizing, pinColumn, focusCell, getActiveCell, clearActiveCell, getRowIndexRelativeToPage, editCell, commitEditing, editRow, getSelectedRange, cut, undo, redo, canUndo, canRedo, clearHistory }. */
|
|
15
|
+
/** The `$expose` imperative handle received via `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 }. */
|
|
16
16
|
export type { DataTableHandle } from './DataTable';
|
package/src/themes/base.css
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @rozie-ui/data-table — base token reference.
|
|
3
3
|
*
|
|
4
|
-
* This file is the PUBLIC token reference. It
|
|
5
|
-
* tokens
|
|
6
|
-
* scoped `<style>` actually reads. The component reads 82 internal `--rdt-*`
|
|
4
|
+
* This file is the PUBLIC token reference. It wires each of the 18 public
|
|
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-*`
|
|
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
|
*
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
* max-height. It does NOT reach every cosmetic value. 58 of the 82 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
|
-
* row, the per-header column ⋯ menu
|
|
15
|
+
* row, the per-header column ⋯ menu (its panel is a composed @rozie-ui/popover, so its
|
|
16
|
+
* background, border and shadow follow --rozie-popover-bg, --rozie-popover-border and
|
|
17
|
+
* --rozie-popover-shadow, set at the same ancestor), most of the pagination bar (only
|
|
16
18
|
* --rozie-data-table-control-border-color, --rozie-data-table-radius, and
|
|
17
19
|
* --rozie-data-table-disabled-opacity are public — its background, padding, and
|
|
18
20
|
* status-text size are not), and the chrome column widths. Those remain internal
|
|
19
21
|
* and may be renamed; a design system that needs them should set the `--rdt-*`
|
|
20
|
-
* name directly and accept that it is not yet a stable API.
|
|
22
|
+
* name directly and accept that it is not yet a stable API. Those two counts are
|
|
23
|
+
* `<DataTable>`'s own; the sibling `<GroupBar>` reads 9 further internal tokens
|
|
24
|
+
* (`--rdt-group-drop-zone-*`, `--rdt-group-drop-marker`, `--rdt-focus-ring`), none of
|
|
25
|
+
* them public and all of them likewise inline-defaulted. One Lit-specific rule: set
|
|
21
26
|
* the public tokens at or ABOVE the `<rozie-data-table>` element (`:root`, a wrapper, or
|
|
22
27
|
* the element itself), never on the `.rozie-data-table` / `.rozie-data-table-wrap`
|
|
23
28
|
* classes — on that target both of those render inside the component's shadow root,
|
|
@@ -35,96 +40,112 @@
|
|
|
35
40
|
* `.rozie-data-table` / `.rozie-data-table-wrap` element). See
|
|
36
41
|
* themes/shadcn.css, themes/material.css and themes/bootstrap.css for the
|
|
37
42
|
* design-system bridges.
|
|
43
|
+
*
|
|
44
|
+
* WHERE THE DEFAULTS LIVE. Every default below is the FALLBACK of the `var()` that reads the
|
|
45
|
+
* public token, on the table's own elements — not a declaration of the token. Nothing in this
|
|
46
|
+
* file (or in a bridge) sets a `--rozie-data-table-*` token, so a value set on ANY ancestor
|
|
47
|
+
* (`:root`, `.dark`, a wrapper) or on the table itself reaches the table by plain inheritance,
|
|
48
|
+
* and the default applies only while no scope sets it. (A value an element declares for itself
|
|
49
|
+
* always beats one it would inherit, whatever the selector's specificity — which is why a
|
|
50
|
+
* declared default, anywhere on the path to the table, would shadow some ancestor's value.) A
|
|
51
|
+
* design-system bridge is the same mapping with the design system's variable as the fallback,
|
|
52
|
+
* so that variable too is resolved AT the table and follows the nearest theme scope — a
|
|
53
|
+
* `.dark` or `data-bs-theme` wrapper, not only `<html>`. The rule sits under `:where()` (zero
|
|
54
|
+
* specificity) so a bridge's copy wins in either import order; it also names the Lit host
|
|
55
|
+
* element `rozie-data-table`, the only element of that leaf a document stylesheet can reach.
|
|
38
56
|
*/
|
|
39
|
-
.rozie-data-table-wrap,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
:where(.rozie-data-table-wrap, .rozie-data-table, rozie-data-table) {
|
|
58
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
59
|
+
* Wire the PUBLIC tokens onto the INTERNAL `--rdt-*` the component reads, each
|
|
60
|
+
* with its default as the fallback. The public tokens are not read by a CSS var()
|
|
61
|
+
* inside the component's own scoped <style> — that is by design: this block is the
|
|
62
|
+
* public surface, and a bridge re-declares the lines it maps with the design
|
|
63
|
+
* system's variable in place of the default. (The component still renders
|
|
64
|
+
* zero-config without this file via its own inline `var(--rdt-x, <fallback>)`
|
|
65
|
+
* defaults.) A public token read by several lines carries the same default in each.
|
|
66
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
46
67
|
|
|
47
68
|
/* ── palette (the few values most consumers will set) ───────────────────── */
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
--rozie-data-table-
|
|
69
|
+
/* `currentColor` for `color` is the inherited text colour: the table takes its
|
|
70
|
+
container's colour unless the token is set. The accent is read through the two tokens
|
|
71
|
+
that default to it — the selection checkboxes (next line) and the resize grip while
|
|
72
|
+
dragging — so it follows an accent set on any ancestor. */
|
|
73
|
+
--rdt-color: var(--rozie-data-table-fg, currentColor);
|
|
74
|
+
--rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, #0066cc));
|
|
75
|
+
--rdt-border: 1px solid var(--rozie-data-table-border-color, rgba(0, 0, 0, 0.08));
|
|
76
|
+
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, rgba(0, 0, 0, 0.2));
|
|
77
|
+
--rdt-filter-radius: var(--rozie-data-table-radius, 4px);
|
|
53
78
|
|
|
54
79
|
/* ── typography / cell metrics ──────────────────────────────────────────── */
|
|
55
|
-
--rozie-data-table-font
|
|
56
|
-
--rozie-data-table-cell-padding
|
|
80
|
+
--rdt-font: var(--rozie-data-table-font, 14px system-ui, sans-serif);
|
|
81
|
+
--rdt-cell-padding: var(--rozie-data-table-cell-padding, 0.5rem 0.75rem);
|
|
57
82
|
|
|
58
83
|
/* ── header ─────────────────────────────────────────────────────────────── */
|
|
59
84
|
/* OPAQUE (was rgba(0,0,0,0.03)): a translucent header lets the scrolling body bleed
|
|
60
85
|
through in sticky mode. #f7f7f7 ≈ the old 3%-black tint over white, but solid —
|
|
61
86
|
matching the opaque header backgrounds the design-system themes already ship. */
|
|
62
|
-
--rozie-data-table-header-bg
|
|
63
|
-
--rozie-data-table-header-weight
|
|
87
|
+
--rdt-header-bg: var(--rozie-data-table-header-bg, #f7f7f7);
|
|
88
|
+
--rdt-header-weight: var(--rozie-data-table-header-weight, 600);
|
|
64
89
|
|
|
65
90
|
/* ── sort indicator ─────────────────────────────────────────────────────── */
|
|
66
|
-
--rozie-data-table-sort-indicator-opacity
|
|
91
|
+
--rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity, 0.7);
|
|
67
92
|
|
|
68
93
|
/* ── resize handle ──────────────────────────────────────────────────────── */
|
|
69
|
-
|
|
70
|
-
--
|
|
94
|
+
/* The grip while dragging defaults to the accent, resolved at the table. */
|
|
95
|
+
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color, rgba(0, 0, 0, 0.12));
|
|
96
|
+
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active, var(--rozie-data-table-accent, #0066cc));
|
|
71
97
|
|
|
72
98
|
/* ── pin controls ───────────────────────────────────────────────────────── */
|
|
73
|
-
--rozie-data-table-pin-active-bg
|
|
99
|
+
--rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg, rgba(0, 0, 0, 0.1));
|
|
74
100
|
|
|
75
101
|
/* ── column-visibility menu ─────────────────────────────────────────────── */
|
|
76
|
-
--rozie-data-table-menu-bg
|
|
77
|
-
--rozie-data-table-menu-shadow
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
--
|
|
102
|
+
--rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg, #fff);
|
|
103
|
+
--rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow, 0 2px 8px rgba(0, 0, 0, 0.12));
|
|
104
|
+
--rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color, rgba(0, 0, 0, 0.08));
|
|
105
|
+
--rdt-colvis-menu-radius: var(--rozie-data-table-radius, 4px);
|
|
106
|
+
--rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color, rgba(0, 0, 0, 0.2));
|
|
107
|
+
--rdt-colvis-summary-radius: var(--rozie-data-table-radius, 4px);
|
|
81
108
|
|
|
82
109
|
/* ── disabled pagination controls ──────────────────────────────────────── */
|
|
83
|
-
--rozie-data-table-disabled-opacity
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
i.e. `none` (unbounded). Declaring `none` here is the render-neutral
|
|
90
|
-
equivalent of that unset behavior, made explicit and overridable. */
|
|
91
|
-
--rozie-data-table-max-height: none;
|
|
92
|
-
|
|
93
|
-
/* ─────────────────────────────────────────────────────────────────────────
|
|
94
|
-
* Wire the PUBLIC tokens onto the INTERNAL `--rdt-*` the component reads. A
|
|
95
|
-
* bridge only needs to remap the public tokens above; these passthroughs do
|
|
96
|
-
* the rest. (The component still renders zero-config without this file via its
|
|
97
|
-
* own inline `var(--rdt-x, <fallback>)` defaults.)
|
|
98
|
-
* ───────────────────────────────────────────────────────────────────────── */
|
|
99
|
-
--rdt-font: var(--rozie-data-table-font);
|
|
100
|
-
--rdt-color: var(--rozie-data-table-fg);
|
|
101
|
-
--rdt-cell-padding: var(--rozie-data-table-cell-padding);
|
|
102
|
-
--rdt-border: 1px solid var(--rozie-data-table-border-color);
|
|
103
|
-
--rdt-header-bg: var(--rozie-data-table-header-bg);
|
|
104
|
-
--rdt-header-weight: var(--rozie-data-table-header-weight);
|
|
105
|
-
--rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
|
|
106
|
-
|
|
107
|
-
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
108
|
-
--rdt-filter-radius: var(--rozie-data-table-radius);
|
|
109
|
-
--rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
110
|
-
--rdt-page-btn-radius: var(--rozie-data-table-radius);
|
|
111
|
-
--rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
|
|
112
|
-
--rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
113
|
-
--rdt-page-size-radius: var(--rozie-data-table-radius);
|
|
114
|
-
|
|
115
|
-
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
|
|
116
|
-
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
|
|
117
|
-
|
|
118
|
-
--rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
119
|
-
--rdt-pin-btn-radius: var(--rozie-data-table-radius);
|
|
120
|
-
--rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
|
|
110
|
+
--rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity, 0.4);
|
|
111
|
+
--rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color, rgba(0, 0, 0, 0.2));
|
|
112
|
+
--rdt-page-btn-radius: var(--rozie-data-table-radius, 4px);
|
|
113
|
+
--rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color, rgba(0, 0, 0, 0.2));
|
|
114
|
+
--rdt-page-size-radius: var(--rozie-data-table-radius, 4px);
|
|
115
|
+
}
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
/* The same defaults, once more, as private `--rdt-ds-*` values on the document root — for the
|
|
118
|
+
Lit leaf only. Inside a shadow root (and a Lit table nested in another component's shadow
|
|
119
|
+
root is out of reach of every document selector, the host element's included) the
|
|
120
|
+
component's own `:host` wiring reads each public token with `var(--rdt-ds-<token>)` as its
|
|
121
|
+
fallback, and only an INHERITED value can get there. A bridge declares the same names at
|
|
122
|
+
`:root`, which is how its skin reaches a Lit table no selector can match. Internal: not a
|
|
123
|
+
stable API. */
|
|
124
|
+
:where(:root) {
|
|
125
|
+
--rdt-ds-fg: currentColor;
|
|
126
|
+
--rdt-ds-select-accent: #0066cc;
|
|
127
|
+
--rdt-ds-border-color: rgba(0, 0, 0, 0.08);
|
|
128
|
+
--rdt-ds-control-border-color: rgba(0, 0, 0, 0.2);
|
|
129
|
+
--rdt-ds-radius: 4px;
|
|
130
|
+
--rdt-ds-font: 14px system-ui, sans-serif;
|
|
131
|
+
--rdt-ds-cell-padding: 0.5rem 0.75rem;
|
|
132
|
+
--rdt-ds-header-bg: #f7f7f7;
|
|
133
|
+
--rdt-ds-header-weight: 600;
|
|
134
|
+
--rdt-ds-sort-indicator-opacity: 0.7;
|
|
135
|
+
--rdt-ds-resize-grip-color: rgba(0, 0, 0, 0.12);
|
|
136
|
+
--rdt-ds-resize-grip-active: #0066cc;
|
|
137
|
+
--rdt-ds-pin-active-bg: rgba(0, 0, 0, 0.1);
|
|
138
|
+
--rdt-ds-menu-bg: #fff;
|
|
139
|
+
--rdt-ds-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
140
|
+
--rdt-ds-disabled-opacity: 0.4;
|
|
141
|
+
}
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
/* ── virtual scroll (row windowing) ─────────────────────────────────────── */
|
|
144
|
+
/* Read with NO fallback at its one use site (`max-height: var(--rozie-data-table-max-height);`
|
|
145
|
+
on `.rdt-scroll`) — when unset the property is invalid at computed-value time and the rule
|
|
146
|
+
behaves as if `max-height` were never declared there, i.e. `none` (unbounded). No bridge
|
|
147
|
+
maps it, so its default is declared, at zero specificity on the document root: `none` is
|
|
148
|
+
the render-neutral equivalent of that unset behavior, made explicit and overridable. */
|
|
149
|
+
:where(:root) {
|
|
150
|
+
--rozie-data-table-max-height: none;
|
|
130
151
|
}
|