@joewinke/jatui 0.25.0 → 0.27.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/CLAUDE.md +115 -2
- package/dist/actions/keyboardScope.d.ts +58 -0
- package/dist/actions/keyboardScope.js +110 -0
- package/dist/actions/listNav.d.ts +48 -3
- package/dist/actions/listNav.js +267 -77
- package/dist/actions/longpress.d.ts +37 -0
- package/dist/actions/longpress.js +105 -0
- package/dist/actions/stickToBottom.d.ts +66 -0
- package/dist/actions/stickToBottom.js +155 -0
- package/dist/actions/swipe.d.ts +64 -0
- package/dist/actions/swipe.js +179 -0
- package/dist/components/BottomSheet.svelte +280 -0
- package/dist/components/BottomSheet.svelte.d.ts +27 -0
- package/dist/components/ConfirmDialog.svelte +21 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
- package/dist/components/ConfirmModal.svelte +15 -0
- package/dist/components/ConfirmModal.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +18 -1
- package/dist/components/Drawer.svelte.d.ts +9 -1
- package/dist/components/InputDialog.svelte +21 -0
- package/dist/components/InputDialog.svelte.d.ts +11 -1
- package/dist/components/KeyboardShortcutsOverlay.svelte +5 -0
- package/dist/components/Modal.svelte +50 -1
- package/dist/components/Modal.svelte.d.ts +8 -0
- package/dist/components/basic/Empty.svelte +173 -9
- package/dist/components/basic/Empty.svelte.d.ts +50 -1
- package/dist/components/basic/ErrorState.svelte +128 -0
- package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
- package/dist/components/basic/JumpToLatest.svelte +60 -0
- package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
- package/dist/components/basic/PageHeader.svelte +216 -0
- package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
- package/dist/components/basic/ProgressButton.svelte +221 -0
- package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
- package/dist/components/graph/GraphApiBridge.svelte +36 -0
- package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
- package/dist/components/graph/GraphCanvas.svelte +212 -27
- package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
- package/dist/components/graph/api.d.ts +52 -0
- package/dist/components/graph/api.js +66 -0
- package/dist/components/graph/index.d.ts +2 -0
- package/dist/components/graph/index.js +1 -0
- package/dist/components/graph/theme.js +13 -0
- package/dist/components/messaging/MessageItem.svelte +5 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
- package/dist/components/table/DataTable.svelte +607 -72
- package/dist/components/table/DataTable.svelte.d.ts +23 -2
- package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
- package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
- package/dist/components/table/columnStorage.d.ts +29 -0
- package/dist/components/table/columnStorage.js +61 -0
- package/dist/components/table/groupRows.d.ts +72 -0
- package/dist/components/table/groupRows.js +118 -0
- package/dist/components/table/index.d.ts +7 -1
- package/dist/components/table/index.js +3 -0
- package/dist/components/table/types.d.ts +207 -3
- package/dist/index.d.ts +25 -2
- package/dist/index.js +32 -1
- package/dist/utils/ansiToHtml.d.ts +83 -0
- package/dist/utils/ansiToHtml.js +543 -0
- package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
- package/dist/utils/keyboardShortcutRegistry.js +1070 -76
- package/dist/utils/pollBackoff.d.ts +83 -0
- package/dist/utils/pollBackoff.js +151 -0
- package/dist/utils/snapFlick.d.ts +59 -0
- package/dist/utils/snapFlick.js +66 -0
- package/dist/video/advisors/qr.d.ts +1 -1
- package/dist/virtual/createRowWindow.d.ts +75 -0
- package/dist/virtual/createRowWindow.js +129 -0
- package/dist/virtual/index.d.ts +4 -0
- package/dist/virtual/index.js +5 -0
- package/dist/virtual/rowWindowCore.d.ts +202 -0
- package/dist/virtual/rowWindowCore.js +510 -0
- package/package.json +11 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="TData">
|
|
2
|
+
import { tick, untrack } from 'svelte';
|
|
2
3
|
import {
|
|
3
4
|
createTable,
|
|
4
5
|
getCoreRowModel,
|
|
@@ -9,12 +10,18 @@
|
|
|
9
10
|
type RowSelectionState,
|
|
10
11
|
type PaginationState,
|
|
11
12
|
type TableOptionsResolved,
|
|
13
|
+
type Row,
|
|
12
14
|
} from '@tanstack/table-core';
|
|
13
15
|
import { flexRender } from './flexRender.js';
|
|
14
|
-
import type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
import { groupRows, groupPathKey, type GroupedGroupItem, type GroupedRowItem, type GroupLevel } from './groupRows.js';
|
|
17
|
+
import type {
|
|
18
|
+
DataTableProps,
|
|
19
|
+
DataTableRowProps,
|
|
20
|
+
DataTableSortEvent,
|
|
21
|
+
DataTableRenderItem,
|
|
22
|
+
DataTableRowWindow,
|
|
23
|
+
GroupedItem,
|
|
24
|
+
} from './types.js';
|
|
18
25
|
|
|
19
26
|
let {
|
|
20
27
|
data,
|
|
@@ -27,13 +34,51 @@
|
|
|
27
34
|
loadingRows = 5,
|
|
28
35
|
onSelectionChange,
|
|
29
36
|
class: className = '',
|
|
30
|
-
|
|
37
|
+
getRowId,
|
|
38
|
+
sorting = $bindable(),
|
|
39
|
+
initialSorting,
|
|
40
|
+
manualSorting = false,
|
|
41
|
+
onSortingChange,
|
|
42
|
+
rowProps,
|
|
43
|
+
onRowClick,
|
|
44
|
+
headerProps,
|
|
45
|
+
expandedRow,
|
|
46
|
+
isExpanded,
|
|
47
|
+
empty,
|
|
48
|
+
variant = 'zebra',
|
|
49
|
+
tableClass = '',
|
|
50
|
+
api = $bindable(),
|
|
51
|
+
groupBy,
|
|
52
|
+
collapsed = $bindable(),
|
|
53
|
+
onCollapsedChange,
|
|
54
|
+
groupLabel,
|
|
55
|
+
groupHeader,
|
|
56
|
+
stickyGroupHeaders = false,
|
|
57
|
+
stickyHeader = false,
|
|
58
|
+
maxHeight,
|
|
59
|
+
virtual,
|
|
60
|
+
pinnedRowIds,
|
|
61
|
+
ariaGrid = false,
|
|
62
|
+
ariaLabel,
|
|
63
|
+
}: DataTableProps<TData> = $props();
|
|
31
64
|
|
|
32
65
|
// ── State ──────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
66
|
+
// `sorting` is bindable: bound, the caller owns it; unbound, it is local
|
|
67
|
+
// state seeded from `initialSorting`.
|
|
68
|
+
// svelte-ignore state_referenced_locally
|
|
69
|
+
if (sorting === undefined) sorting = initialSorting ?? [];
|
|
70
|
+
// Same contract for the collapsed-group set.
|
|
71
|
+
// svelte-ignore state_referenced_locally
|
|
72
|
+
if (collapsed === undefined) collapsed = new Set();
|
|
73
|
+
const sortState = $derived<SortingState>(sorting ?? []);
|
|
34
74
|
let rowSelection = $state<RowSelectionState>({});
|
|
35
75
|
// svelte-ignore state_referenced_locally
|
|
36
76
|
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize });
|
|
77
|
+
/** Internally-owned expansion (only used when `isExpanded` is not supplied). */
|
|
78
|
+
let expandedIds = $state<Set<string>>(new Set());
|
|
79
|
+
/** The header click that triggered the sort change currently in flight. */
|
|
80
|
+
let pendingSortEvent: DataTableSortEvent | null = null;
|
|
81
|
+
let rootEl: HTMLDivElement | undefined = $state();
|
|
37
82
|
|
|
38
83
|
// Reset to the first page whenever the data set changes.
|
|
39
84
|
//
|
|
@@ -50,7 +95,7 @@
|
|
|
50
95
|
pagination = { pageIndex: 0, pageSize };
|
|
51
96
|
});
|
|
52
97
|
|
|
53
|
-
const checkboxCol: ColumnDef<
|
|
98
|
+
const checkboxCol: ColumnDef<TData, unknown> = {
|
|
54
99
|
id: '__select',
|
|
55
100
|
header: () => null,
|
|
56
101
|
cell: () => null,
|
|
@@ -58,24 +103,37 @@
|
|
|
58
103
|
size: 40,
|
|
59
104
|
};
|
|
60
105
|
|
|
61
|
-
const allColumns = $derived<ColumnDef<
|
|
62
|
-
selectable ? [checkboxCol, ...(columns as ColumnDef<AnyData, unknown>[])] : (columns as ColumnDef<AnyData, unknown>[])
|
|
63
|
-
);
|
|
106
|
+
const allColumns = $derived<ColumnDef<TData, unknown>[]>(selectable ? [checkboxCol, ...columns] : columns);
|
|
64
107
|
|
|
65
108
|
// ── Table instance ─────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
// Created ONCE; every reactive change is pushed in with `setOptions` (R6).
|
|
110
|
+
// It used to be rebuilt in a `$derived.by` on every sort / selection / data
|
|
111
|
+
// change, which threw away TanStack's memoised row models and handed the
|
|
112
|
+
// template brand-new Row objects, so every row re-rendered on every poll.
|
|
113
|
+
//
|
|
114
|
+
// Row-model factories are created once too: TanStack caches the model a
|
|
115
|
+
// factory builds on first use, so a fresh factory per update would be ignored.
|
|
116
|
+
const coreRowModel = getCoreRowModel<TData>();
|
|
117
|
+
const sortedRowModel = getSortedRowModel<TData>();
|
|
118
|
+
const paginationRowModel = getPaginationRowModel<TData>();
|
|
119
|
+
|
|
120
|
+
function buildOptions(): TableOptionsResolved<TData> {
|
|
121
|
+
return {
|
|
68
122
|
data,
|
|
69
123
|
columns: allColumns,
|
|
70
124
|
state: {
|
|
71
|
-
sorting,
|
|
125
|
+
sorting: sortState,
|
|
72
126
|
rowSelection,
|
|
73
127
|
...(paginated ? { pagination } : {}),
|
|
74
128
|
},
|
|
129
|
+
getRowId: getRowId ? (row: TData) => getRowId(row) : undefined,
|
|
75
130
|
enableRowSelection: selectable,
|
|
76
131
|
enableSorting: sortable,
|
|
132
|
+
manualSorting,
|
|
77
133
|
onSortingChange: (updater) => {
|
|
78
|
-
|
|
134
|
+
const next = typeof updater === 'function' ? updater(sortState) : updater;
|
|
135
|
+
sorting = next;
|
|
136
|
+
onSortingChange?.(next, pendingSortEvent ?? { field: next[0]?.id ?? '', shiftKey: false });
|
|
79
137
|
},
|
|
80
138
|
onRowSelectionChange: (updater) => {
|
|
81
139
|
rowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
|
@@ -86,53 +144,351 @@
|
|
|
86
144
|
}
|
|
87
145
|
: () => {},
|
|
88
146
|
onStateChange: () => {},
|
|
89
|
-
getCoreRowModel:
|
|
90
|
-
getSortedRowModel: sortable ?
|
|
91
|
-
getPaginationRowModel: paginated ?
|
|
147
|
+
getCoreRowModel: coreRowModel,
|
|
148
|
+
getSortedRowModel: sortable && !manualSorting ? sortedRowModel : undefined,
|
|
149
|
+
getPaginationRowModel: paginated ? paginationRowModel : undefined,
|
|
92
150
|
renderFallbackValue: '',
|
|
93
151
|
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const table = createTable(untrack(buildOptions));
|
|
155
|
+
/** The row-model mode the cached `_get*RowModel`s were built for. */
|
|
156
|
+
let modelMode = '';
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Pushes the current props/state into the one instance. Its value is a fresh
|
|
160
|
+
* token per change: everything that reads the table goes through `t()` (or
|
|
161
|
+
* reads this), so it re-runs exactly when the table's inputs moved.
|
|
162
|
+
*/
|
|
163
|
+
const tableRev = $derived.by(() => {
|
|
164
|
+
const opts = buildOptions();
|
|
165
|
+
const mode = `${sortable && !manualSorting}|${paginated}`;
|
|
166
|
+
if (mode !== modelMode) {
|
|
167
|
+
// TanStack caches the sorted / paginated model on first use and never
|
|
168
|
+
// re-reads the option, so a flag flip must drop the cache.
|
|
169
|
+
table._getSortedRowModel = undefined;
|
|
170
|
+
table._getPaginationRowModel = undefined;
|
|
171
|
+
modelMode = mode;
|
|
172
|
+
}
|
|
173
|
+
// createTable takes a fully-resolved state — unlike the framework adapters it
|
|
174
|
+
// does not fill in defaults, so feature state we never touch (columnPinning,
|
|
175
|
+
// columnVisibility, …) would land undefined and getHeaderGroups() dies reading
|
|
176
|
+
// columnPinning.left. Merge the instance defaults under our reactive slices.
|
|
177
|
+
table.setOptions((prev) => ({
|
|
100
178
|
...prev,
|
|
101
|
-
|
|
179
|
+
...opts,
|
|
180
|
+
state: { ...table.initialState, ...opts.state },
|
|
102
181
|
}));
|
|
103
|
-
return
|
|
182
|
+
return {};
|
|
104
183
|
});
|
|
105
184
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
185
|
+
/** The table, read through `tableRev` so the caller re-runs when it changes. */
|
|
186
|
+
function t() {
|
|
187
|
+
void tableRev;
|
|
188
|
+
return table;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const headerGroups = $derived(t().getHeaderGroups());
|
|
192
|
+
const rows = $derived(t().getRowModel().rows);
|
|
193
|
+
const pageCount = $derived(t().getPageCount());
|
|
194
|
+
const canPrevPage = $derived(t().getCanPreviousPage());
|
|
195
|
+
const canNextPage = $derived(t().getCanNextPage());
|
|
196
|
+
|
|
197
|
+
const tableClasses = $derived(
|
|
198
|
+
[variant === 'unstyled' ? '' : variant === 'plain' ? 'table' : 'table table-zebra', 'w-full', tableClass]
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.join(' ')
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
/** The table owns expansion: a row click / Enter / Space toggles the detail row. */
|
|
204
|
+
const internalExpand = $derived(!!expandedRow && !isExpanded);
|
|
111
205
|
|
|
112
206
|
$effect(() => {
|
|
113
207
|
if (!selectable || !onSelectionChange) return;
|
|
114
|
-
const selected =
|
|
208
|
+
const selected = t().getSelectedRowModel().rows.map((r) => r.original);
|
|
115
209
|
onSelectionChange(selected);
|
|
116
210
|
});
|
|
117
211
|
|
|
212
|
+
// ── Render list ────────────────────────────────────────────────────────
|
|
213
|
+
// Everything the body draws, in order: group headers, data rows and the
|
|
214
|
+
// detail row under each expanded row. One entry = one `<tr>`, which is what
|
|
215
|
+
// the row window counts and measures.
|
|
216
|
+
const GROUP_KEY = '\u0000group:';
|
|
217
|
+
const DETAIL_KEY = '\u0000detail:';
|
|
218
|
+
|
|
219
|
+
type Entry =
|
|
220
|
+
| { kind: 'group'; key: string; item: GroupedGroupItem<TData>; pathKey: string }
|
|
221
|
+
| { kind: 'row'; key: string; item: GroupedRowItem<TData>; row: Row<TData> }
|
|
222
|
+
| { kind: 'detail'; key: string; row: Row<TData> };
|
|
223
|
+
|
|
224
|
+
const grouped = $derived(!!groupBy && groupBy.length > 0);
|
|
225
|
+
|
|
226
|
+
const entries = $derived.by<Entry[]>(() => {
|
|
227
|
+
const list = rows;
|
|
228
|
+
const out: Entry[] = [];
|
|
229
|
+
const pushRow = (row: Row<TData>, item: GroupedRowItem<TData>) => {
|
|
230
|
+
out.push({ kind: 'row', key: row.id, item, row });
|
|
231
|
+
if (rowIsExpanded(row)) out.push({ kind: 'detail', key: DETAIL_KEY + row.id, row });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (!grouped) {
|
|
235
|
+
const n = list.length;
|
|
236
|
+
list.forEach((row, i) => {
|
|
237
|
+
const position = n === 1 ? 'only' : i === 0 ? 'first' : i === n - 1 ? 'last' : 'middle';
|
|
238
|
+
pushRow(row, { kind: 'row', row: row.original, id: row.id, depth: 0, groupPath: [], position });
|
|
239
|
+
});
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Group the TanStack rows themselves (not their originals) so duplicate
|
|
244
|
+
// originals stay distinct, then hand callers originals in the items.
|
|
245
|
+
const levels: GroupLevel<Row<TData>>[] = groupBy!.map((level) => ({
|
|
246
|
+
key: (r) => level.key(r.original),
|
|
247
|
+
compare: level.compare,
|
|
248
|
+
showHeader: level.showHeader
|
|
249
|
+
? (k, rs) => level.showHeader!(k, rs.map((r) => r.original))
|
|
250
|
+
: undefined,
|
|
251
|
+
}));
|
|
252
|
+
const items = groupRows(list, levels, {
|
|
253
|
+
getRowId: (r) => r.id,
|
|
254
|
+
collapsed: collapsed ?? new Set(),
|
|
255
|
+
label: groupLabel,
|
|
256
|
+
});
|
|
257
|
+
for (const it of items) {
|
|
258
|
+
if (it.kind === 'group') {
|
|
259
|
+
const pathKey = groupPathKey(it.path);
|
|
260
|
+
out.push({
|
|
261
|
+
kind: 'group',
|
|
262
|
+
key: GROUP_KEY + pathKey,
|
|
263
|
+
pathKey,
|
|
264
|
+
item: { ...it, rows: it.rows.map((r) => r.original) },
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
pushRow(it.row, { ...it, row: it.row.original });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/** Group-header indices per depth, outermost first — the window's sticky levels. */
|
|
274
|
+
const stickyLevels = $derived.by<number[][]>(() => {
|
|
275
|
+
const levels: number[][] = [];
|
|
276
|
+
entries.forEach((e, i) => {
|
|
277
|
+
if (e.kind !== 'group') return;
|
|
278
|
+
(levels[e.item.depth] ??= []).push(i);
|
|
279
|
+
});
|
|
280
|
+
for (let d = 0; d < levels.length; d++) levels[d] ??= [];
|
|
281
|
+
return levels;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Windowing (grid mode) ──────────────────────────────────────────────
|
|
285
|
+
function renderItem(e: Entry): DataTableRenderItem<TData> {
|
|
286
|
+
if (e.kind === 'detail') return { kind: 'detail', row: e.row.original, id: e.row.id };
|
|
287
|
+
return e.item as GroupedItem<TData>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function estimate(e: Entry | undefined): number {
|
|
291
|
+
if (!e) return 40;
|
|
292
|
+
const est = virtual?.estimateRowSize;
|
|
293
|
+
if (typeof est === 'function') return est(renderItem(e));
|
|
294
|
+
if (e.kind === 'group') return 36;
|
|
295
|
+
if (e.kind === 'detail') return 120;
|
|
296
|
+
return typeof est === 'number' ? est : 40;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Read once: the window must be created during component init.
|
|
300
|
+
const virtualAtMount = untrack(() => virtual);
|
|
301
|
+
const win: DataTableRowWindow | null = virtualAtMount
|
|
302
|
+
? virtualAtMount.createRowWindow({
|
|
303
|
+
count: () => entries.length,
|
|
304
|
+
getKey: (i) => entries[i]?.key ?? String(i),
|
|
305
|
+
estimateSize: (i) => estimate(entries[i]),
|
|
306
|
+
overscan: virtualAtMount.overscan,
|
|
307
|
+
get threshold() {
|
|
308
|
+
return virtual?.threshold ?? 0;
|
|
309
|
+
},
|
|
310
|
+
scrollRoot: virtualAtMount.scrollRoot ?? 'auto',
|
|
311
|
+
scrollPaddingStart: virtualAtMount.scrollPaddingStart,
|
|
312
|
+
pinned: () => {
|
|
313
|
+
const keys: string[] = [];
|
|
314
|
+
for (const id of pinnedRowIds ?? []) keys.push(id, DETAIL_KEY + id);
|
|
315
|
+
return keys;
|
|
316
|
+
},
|
|
317
|
+
stickyIndices: () => (stickyGroupHeaders ? stickyLevels : []),
|
|
318
|
+
})
|
|
319
|
+
: null;
|
|
320
|
+
|
|
321
|
+
const noopAction = (_node: HTMLElement, _key?: string) => ({ update() {}, destroy() {} });
|
|
322
|
+
const measureAction = win ? win.measure : noopAction;
|
|
323
|
+
const anchorAction = win ? win.anchor : noopAction;
|
|
324
|
+
|
|
325
|
+
type Slot = { entry: Entry; index: number; spacers: number[] };
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Spacer rows that stand in for `skipped` undrawn entries totalling `height`
|
|
329
|
+
* px. The COUNT keeps `nth-child` parity with the unwindowed table, so zebra
|
|
330
|
+
* striping does not flip as the slice scrolls: an odd gap gets one spacer, an
|
|
331
|
+
* even one gets two.
|
|
332
|
+
*/
|
|
333
|
+
function spacersFor(skipped: number, height: number): number[] {
|
|
334
|
+
if (skipped <= 0) return height > 0 ? [height, 0] : [];
|
|
335
|
+
return skipped % 2 === 1 ? [height] : [height, 0];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const slots = $derived.by<Slot[]>(() => {
|
|
339
|
+
if (!win) return entries.map((entry, index) => ({ entry, index, spacers: [] }));
|
|
340
|
+
const out: Slot[] = [];
|
|
341
|
+
let prev = -1;
|
|
342
|
+
for (const it of win.items) {
|
|
343
|
+
const entry = entries[it.index];
|
|
344
|
+
// The window can trail the entry list by one flush; skip strays.
|
|
345
|
+
if (!entry || entry.key !== it.key) continue;
|
|
346
|
+
const height = prev < 0 ? win.paddingTop : it.gapBefore;
|
|
347
|
+
out.push({ entry, index: it.index, spacers: spacersFor(it.index - prev - 1, height) });
|
|
348
|
+
prev = it.index;
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ── ARIA ───────────────────────────────────────────────────────────────
|
|
354
|
+
const ariaRows = $derived(ariaGrid || !!win);
|
|
355
|
+
const tableRole = $derived(ariaGrid ? (grouped ? 'treegrid' : 'grid') : undefined);
|
|
356
|
+
/** 1-based row index, counting the header rows. */
|
|
357
|
+
function ariaRowIndex(index: number): number | undefined {
|
|
358
|
+
return ariaRows ? index + 1 + headerGroups.length : undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Imperative api ─────────────────────────────────────────────────────
|
|
362
|
+
api = {
|
|
363
|
+
orderedRowIds: () =>
|
|
364
|
+
grouped && !paginated
|
|
365
|
+
? entries.filter((e) => e.kind === 'row').map((e) => e.key)
|
|
366
|
+
: t().getPrePaginationRowModel().rows.map((r) => r.id),
|
|
367
|
+
scrollToRow: async (id, align = 'auto') => {
|
|
368
|
+
if (paginated) {
|
|
369
|
+
const index = t().getPrePaginationRowModel().rows.findIndex((r) => r.id === id);
|
|
370
|
+
if (index < 0) return;
|
|
371
|
+
const pageIndex = Math.floor(index / pagination.pageSize);
|
|
372
|
+
if (pageIndex !== pagination.pageIndex) {
|
|
373
|
+
pagination = { ...pagination, pageIndex };
|
|
374
|
+
await tick();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (win) {
|
|
378
|
+
win.scrollToKey(id, { align });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const el = rootEl?.querySelector<HTMLElement>(`tr[data-row-id="${CSS.escape(id)}"]`);
|
|
382
|
+
el?.scrollIntoView({ block: align === 'auto' ? 'nearest' : align });
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
118
386
|
function sortDir(headerId: string): 'asc' | 'desc' | false {
|
|
119
|
-
const s =
|
|
387
|
+
const s = sortState.find((item) => item.id === headerId);
|
|
120
388
|
return s ? (s.desc ? 'desc' : 'asc') : false;
|
|
121
389
|
}
|
|
122
390
|
|
|
123
|
-
|
|
391
|
+
type AnyHeader = ReturnType<typeof table.getHeaderGroups>[0]['headers'][0];
|
|
392
|
+
|
|
393
|
+
function toggleSort(header: AnyHeader, e: MouseEvent | KeyboardEvent) {
|
|
394
|
+
pendingSortEvent = { field: header.column.id, shiftKey: e.shiftKey };
|
|
395
|
+
try {
|
|
396
|
+
header.column.getToggleSortingHandler()?.(e);
|
|
397
|
+
} finally {
|
|
398
|
+
pendingSortEvent = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function onHeaderKeydown(header: AnyHeader, e: KeyboardEvent) {
|
|
403
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
toggleSort(header, e);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderHeader(header: AnyHeader) {
|
|
124
409
|
return flexRender(header.column.columnDef.header, header.getContext());
|
|
125
410
|
}
|
|
126
411
|
|
|
127
|
-
function renderCell(cell: ReturnType<
|
|
412
|
+
function renderCell(cell: ReturnType<Row<TData>['getVisibleCells']>[0]) {
|
|
128
413
|
return flexRender(cell.column.columnDef.cell, cell.getContext());
|
|
129
414
|
}
|
|
415
|
+
|
|
416
|
+
// ── Row helpers ────────────────────────────────────────────────────────
|
|
417
|
+
function isSelected(row: Row<TData>): boolean {
|
|
418
|
+
void tableRev;
|
|
419
|
+
return row.getIsSelected();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function rowIsExpanded(row: Row<TData>): boolean {
|
|
423
|
+
if (!expandedRow) return false;
|
|
424
|
+
return isExpanded ? isExpanded(row.original) : expandedIds.has(row.id);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function toggleExpanded(id: string) {
|
|
428
|
+
const next = new Set(expandedIds);
|
|
429
|
+
if (next.has(id)) next.delete(id);
|
|
430
|
+
else next.add(id);
|
|
431
|
+
expandedIds = next;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function toggleGroup(pathKey: string) {
|
|
435
|
+
const next = new Set(collapsed ?? []);
|
|
436
|
+
if (next.has(pathKey)) next.delete(pathKey);
|
|
437
|
+
else next.add(pathKey);
|
|
438
|
+
collapsed = next;
|
|
439
|
+
onCollapsedChange?.(next);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Clicks on these inside a row belong to the control, not to the row. */
|
|
443
|
+
const INTERACTIVE = 'input, button, a, select, textarea, label, [contenteditable]:not([contenteditable="false"])';
|
|
444
|
+
|
|
445
|
+
function isInteractiveTarget(e: Event): boolean {
|
|
446
|
+
const target = e.target as Element | null;
|
|
447
|
+
const tr = e.currentTarget as Element;
|
|
448
|
+
const hit = target?.closest?.(INTERACTIVE);
|
|
449
|
+
return !!hit && hit !== tr && tr.contains(hit);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function onRowClickInternal(row: Row<TData>, rp: DataTableRowProps, e: MouseEvent) {
|
|
453
|
+
if (isInteractiveTarget(e)) return;
|
|
454
|
+
rp.onclick?.(e);
|
|
455
|
+
onRowClick?.(row.original, e);
|
|
456
|
+
if (internalExpand) toggleExpanded(row.id);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function onRowKeydown(row: Row<TData>, e: KeyboardEvent) {
|
|
460
|
+
if (!internalExpand || e.target !== e.currentTarget) return;
|
|
461
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
462
|
+
e.preventDefault();
|
|
463
|
+
toggleExpanded(row.id);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Normalise `rowProps().attrs`: booleans become `""`/omitted (aria-* stay "true"/"false"). */
|
|
467
|
+
function rowAttrs(rp: DataTableRowProps): Record<string, string | number | undefined> {
|
|
468
|
+
const out: Record<string, string | number | undefined> = {};
|
|
469
|
+
for (const [k, v] of Object.entries(rp.attrs ?? {})) {
|
|
470
|
+
if (typeof v === 'boolean') out[k] = k.startsWith('aria-') ? String(v) : v ? '' : undefined;
|
|
471
|
+
else out[k] = v;
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const hasRowHandlers = $derived(!!rowProps || !!onRowClick || internalExpand);
|
|
477
|
+
|
|
478
|
+
// ── Scroll container ───────────────────────────────────────────────────
|
|
479
|
+
// The default wrapper scrolls horizontally, which also makes it the sticky
|
|
480
|
+
// containing block — a sticky header would stick to a box that never scrolls
|
|
481
|
+
// vertically. With sticky on and no `maxHeight`, clip instead (no scroll
|
|
482
|
+
// container); with `maxHeight`, the wrapper becomes the real scroller.
|
|
483
|
+
const sticky = $derived(stickyHeader || stickyGroupHeaders);
|
|
484
|
+
const scrollerClass = $derived(maxHeight ? 'dt-scroller' : sticky ? 'dt-clip' : 'overflow-x-auto');
|
|
485
|
+
let theadHeight = $state(0);
|
|
130
486
|
</script>
|
|
131
487
|
|
|
132
|
-
<div class="datatable-root {className}">
|
|
488
|
+
<div class="datatable-root {className}" bind:this={rootEl}>
|
|
133
489
|
{#if loading}
|
|
134
490
|
<div class="overflow-x-auto">
|
|
135
|
-
<table class=
|
|
491
|
+
<table class={tableClasses}>
|
|
136
492
|
<thead>
|
|
137
493
|
<tr>
|
|
138
494
|
{#each allColumns as _}
|
|
@@ -154,19 +510,30 @@
|
|
|
154
510
|
</table>
|
|
155
511
|
</div>
|
|
156
512
|
{:else}
|
|
157
|
-
<div
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
513
|
+
<div
|
|
514
|
+
class={scrollerClass}
|
|
515
|
+
style:max-height={maxHeight}
|
|
516
|
+
style:--dt-thead-h={sticky ? `${stickyHeader ? theadHeight : 0}px` : undefined}
|
|
517
|
+
>
|
|
518
|
+
<table
|
|
519
|
+
class={tableClasses}
|
|
520
|
+
class:dt-sticky-head={stickyHeader}
|
|
521
|
+
role={tableRole}
|
|
522
|
+
aria-label={ariaLabel}
|
|
523
|
+
aria-multiselectable={ariaGrid && selectable ? 'true' : undefined}
|
|
524
|
+
aria-rowcount={ariaRows ? entries.length + headerGroups.length : undefined}
|
|
525
|
+
>
|
|
526
|
+
<thead bind:offsetHeight={theadHeight}>
|
|
527
|
+
{#each headerGroups as headerGroup, hi}
|
|
528
|
+
<tr aria-rowindex={ariaRows ? hi + 1 : undefined}>
|
|
162
529
|
{#each headerGroup.headers as header}
|
|
163
530
|
{#if header.column.id === '__select'}
|
|
164
531
|
<th class="dt-select-cell">
|
|
165
532
|
<input
|
|
166
533
|
type="checkbox"
|
|
167
534
|
class="checkbox checkbox-sm"
|
|
168
|
-
checked={
|
|
169
|
-
indeterminate={
|
|
535
|
+
checked={t().getIsAllRowsSelected()}
|
|
536
|
+
indeterminate={t().getIsSomeRowsSelected()}
|
|
170
537
|
aria-label="Select all rows"
|
|
171
538
|
onchange={() => table.toggleAllRowsSelected()}
|
|
172
539
|
/>
|
|
@@ -175,10 +542,15 @@
|
|
|
175
542
|
{@const isSortable = sortable && header.column.getCanSort()}
|
|
176
543
|
{@const dir = sortDir(header.column.id)}
|
|
177
544
|
{@const rendered = renderHeader(header)}
|
|
545
|
+
{@const hp = headerProps?.(header.column.id)}
|
|
178
546
|
<th
|
|
179
547
|
style:width={header.getSize() !== 150 ? `${header.getSize()}px` : undefined}
|
|
548
|
+
class={hp?.class}
|
|
180
549
|
class:dt-th-sortable={isSortable}
|
|
181
|
-
|
|
550
|
+
tabindex={isSortable ? 0 : undefined}
|
|
551
|
+
onclick={isSortable ? (e) => toggleSort(header, e) : undefined}
|
|
552
|
+
onkeydown={isSortable ? (e) => onHeaderKeydown(header, e) : undefined}
|
|
553
|
+
oncontextmenu={hp?.oncontextmenu}
|
|
182
554
|
aria-sort={dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : undefined}
|
|
183
555
|
>
|
|
184
556
|
<span class="dt-th-label">
|
|
@@ -209,44 +581,127 @@
|
|
|
209
581
|
{/each}
|
|
210
582
|
</thead>
|
|
211
583
|
|
|
212
|
-
<tbody>
|
|
584
|
+
<tbody use:anchorAction>
|
|
213
585
|
{#if rows.length === 0}
|
|
214
586
|
<tr>
|
|
215
|
-
|
|
587
|
+
{#if empty}
|
|
588
|
+
<td colspan={allColumns.length} class="dt-empty-custom">{@render empty()}</td>
|
|
589
|
+
{:else}
|
|
590
|
+
<td colspan={allColumns.length} class="dt-empty">No data</td>
|
|
591
|
+
{/if}
|
|
216
592
|
</tr>
|
|
217
593
|
{:else}
|
|
218
|
-
{#each
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
594
|
+
{#each slots as slot (slot.entry.key)}
|
|
595
|
+
{#each slot.spacers as h}
|
|
596
|
+
<tr class="dt-spacer" aria-hidden="true" style:height="{h}px"></tr>
|
|
597
|
+
{/each}
|
|
598
|
+
{@const entry = slot.entry}
|
|
599
|
+
{#if entry.kind === 'group'}
|
|
600
|
+
{@const g = entry.item}
|
|
601
|
+
{@const pathKey = entry.pathKey}
|
|
602
|
+
<tr
|
|
603
|
+
class="dt-group-row"
|
|
604
|
+
class:dt-group-sticky={stickyGroupHeaders}
|
|
605
|
+
data-group-key={pathKey}
|
|
606
|
+
data-group-depth={g.depth}
|
|
607
|
+
style:--dt-group-depth={g.depth}
|
|
608
|
+
aria-rowindex={ariaRowIndex(slot.index)}
|
|
609
|
+
aria-level={ariaGrid ? g.depth + 1 : undefined}
|
|
610
|
+
aria-expanded={ariaGrid ? !g.collapsed : undefined}
|
|
611
|
+
use:measureAction={entry.key}
|
|
612
|
+
>
|
|
613
|
+
{#if groupHeader}
|
|
614
|
+
{@render groupHeader(g, {
|
|
615
|
+
toggle: () => toggleGroup(pathKey),
|
|
616
|
+
colspan: allColumns.length,
|
|
617
|
+
collapsed: g.collapsed,
|
|
618
|
+
})}
|
|
232
619
|
{:else}
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
|
|
620
|
+
<th colspan={allColumns.length} scope="rowgroup" class="dt-group-cell">
|
|
621
|
+
<button
|
|
622
|
+
type="button"
|
|
623
|
+
class="dt-group-toggle"
|
|
624
|
+
aria-expanded={!g.collapsed}
|
|
625
|
+
onclick={() => toggleGroup(pathKey)}
|
|
626
|
+
>
|
|
627
|
+
<svg class="dt-group-chevron" class:dt-group-chevron-open={!g.collapsed} width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
628
|
+
<path d="M4.5 2.5L8 6L4.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
629
|
+
</svg>
|
|
630
|
+
<span class="dt-group-label">{g.label}</span>
|
|
631
|
+
<span class="dt-group-count">{g.count}</span>
|
|
632
|
+
</button>
|
|
633
|
+
</th>
|
|
241
634
|
{/if}
|
|
242
|
-
|
|
243
|
-
|
|
635
|
+
</tr>
|
|
636
|
+
{:else if entry.kind === 'row'}
|
|
637
|
+
{@const row = entry.row}
|
|
638
|
+
{@const rp = rowProps?.(row.original, entry.item) ?? {}}
|
|
639
|
+
{@const attrs = rowAttrs(rp)}
|
|
640
|
+
{@const expanded = rowIsExpanded(row)}
|
|
641
|
+
{@const selected = isSelected(row)}
|
|
642
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
643
|
+
<tr
|
|
644
|
+
{...attrs}
|
|
645
|
+
class={rp.class}
|
|
646
|
+
class:dt-row-selected={selected}
|
|
647
|
+
class:dt-row-clickable={!!rp.onclick || !!onRowClick || internalExpand}
|
|
648
|
+
class:dt-row-expanded={expanded}
|
|
649
|
+
data-row-id={getRowId ? row.id : undefined}
|
|
650
|
+
aria-expanded={expandedRow ? expanded : undefined}
|
|
651
|
+
aria-selected={ariaGrid && selectable ? selected : undefined}
|
|
652
|
+
aria-level={ariaGrid && grouped ? entry.item.depth + 1 : undefined}
|
|
653
|
+
aria-rowindex={ariaRowIndex(slot.index)}
|
|
654
|
+
tabindex={internalExpand ? (attrs.tabindex ?? 0) : attrs.tabindex}
|
|
655
|
+
onclick={hasRowHandlers ? (e) => onRowClickInternal(row, rp, e) : undefined}
|
|
656
|
+
onkeydown={internalExpand ? (e) => onRowKeydown(row, e) : undefined}
|
|
657
|
+
oncontextmenu={rp.oncontextmenu}
|
|
658
|
+
ondragover={rp.ondragover}
|
|
659
|
+
ondrop={rp.ondrop}
|
|
660
|
+
use:measureAction={entry.key}
|
|
661
|
+
>
|
|
662
|
+
{#each row.getVisibleCells() as cell}
|
|
663
|
+
{#if cell.column.id === '__select'}
|
|
664
|
+
<td class="dt-select-cell">
|
|
665
|
+
<input
|
|
666
|
+
type="checkbox"
|
|
667
|
+
class="checkbox checkbox-sm"
|
|
668
|
+
checked={selected}
|
|
669
|
+
disabled={!row.getCanSelect()}
|
|
670
|
+
aria-label="Select row"
|
|
671
|
+
onchange={() => row.toggleSelected()}
|
|
672
|
+
/>
|
|
673
|
+
</td>
|
|
674
|
+
{:else}
|
|
675
|
+
{@const rendered = renderCell(cell)}
|
|
676
|
+
<td>
|
|
677
|
+
{#if rendered.type === 'snippet'}
|
|
678
|
+
{@render rendered.snippet(rendered.props)}
|
|
679
|
+
{:else}
|
|
680
|
+
{rendered.value}
|
|
681
|
+
{/if}
|
|
682
|
+
</td>
|
|
683
|
+
{/if}
|
|
684
|
+
{/each}
|
|
685
|
+
</tr>
|
|
686
|
+
{:else if expandedRow}
|
|
687
|
+
{@const row = entry.row}
|
|
688
|
+
<tr
|
|
689
|
+
class="dt-expanded-detail"
|
|
690
|
+
data-detail-for={getRowId ? row.id : undefined}
|
|
691
|
+
aria-rowindex={ariaRowIndex(slot.index)}
|
|
692
|
+
use:measureAction={entry.key}
|
|
693
|
+
>
|
|
694
|
+
<td colspan={allColumns.length}>{@render expandedRow(row.original, { colspan: allColumns.length })}</td>
|
|
695
|
+
</tr>
|
|
696
|
+
{/if}
|
|
244
697
|
{/each}
|
|
698
|
+
{#if win && win.paddingBottom > 0}
|
|
699
|
+
<tr class="dt-spacer" aria-hidden="true" style:height="{win.paddingBottom}px"></tr>
|
|
700
|
+
{/if}
|
|
245
701
|
{/if}
|
|
246
702
|
</tbody>
|
|
247
703
|
</table>
|
|
248
704
|
</div>
|
|
249
|
-
|
|
250
705
|
{#if paginated && pageCount > 1}
|
|
251
706
|
<div class="dt-pagination">
|
|
252
707
|
<span class="dt-page-info">
|
|
@@ -327,6 +782,14 @@
|
|
|
327
782
|
background-color: color-mix(in oklch, var(--color-primary) 6%, transparent) !important;
|
|
328
783
|
}
|
|
329
784
|
|
|
785
|
+
.dt-row-clickable {
|
|
786
|
+
cursor: pointer;
|
|
787
|
+
}
|
|
788
|
+
.dt-row-clickable:focus-visible {
|
|
789
|
+
outline: 2px solid var(--color-primary);
|
|
790
|
+
outline-offset: -2px;
|
|
791
|
+
}
|
|
792
|
+
|
|
330
793
|
.dt-select-cell {
|
|
331
794
|
width: 2.5rem;
|
|
332
795
|
text-align: center;
|
|
@@ -381,4 +844,76 @@
|
|
|
381
844
|
@media (prefers-reduced-motion: reduce) {
|
|
382
845
|
.skel-cell { animation: none; }
|
|
383
846
|
}
|
|
847
|
+
|
|
848
|
+
/* ── Grid mode ─────────────────────────────────────────────────────── */
|
|
849
|
+
.dt-scroller {
|
|
850
|
+
overflow: auto;
|
|
851
|
+
}
|
|
852
|
+
/* Sticky without maxHeight: no scroll container, so sticky cells stick to
|
|
853
|
+
the page's scroller. Horizontal overflow is clipped instead of scrolled. */
|
|
854
|
+
.dt-clip {
|
|
855
|
+
overflow-x: clip;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.dt-sticky-head thead th {
|
|
859
|
+
position: sticky;
|
|
860
|
+
top: 0;
|
|
861
|
+
z-index: 3;
|
|
862
|
+
background-color: var(--color-base-100);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.dt-spacer {
|
|
866
|
+
border: 0;
|
|
867
|
+
background: transparent !important;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.dt-group-sticky > :global(th),
|
|
871
|
+
.dt-group-sticky > :global(td) {
|
|
872
|
+
position: sticky;
|
|
873
|
+
top: calc(var(--dt-thead-h, 0px) + var(--dt-group-depth, 0) * var(--dt-group-row-h, 2.25rem));
|
|
874
|
+
z-index: 2;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.dt-group-cell {
|
|
878
|
+
text-align: left;
|
|
879
|
+
padding-left: calc(var(--jatui-space-base, 8px) + var(--dt-group-depth, 0) * var(--dt-group-indent, 1rem));
|
|
880
|
+
background-color: var(--color-base-200);
|
|
881
|
+
font-weight: 600;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.dt-group-toggle {
|
|
885
|
+
display: inline-flex;
|
|
886
|
+
align-items: center;
|
|
887
|
+
gap: var(--jatui-space-sm, 4px);
|
|
888
|
+
padding: 0;
|
|
889
|
+
border: 0;
|
|
890
|
+
background: none;
|
|
891
|
+
color: inherit;
|
|
892
|
+
font: inherit;
|
|
893
|
+
cursor: pointer;
|
|
894
|
+
border-radius: var(--jatui-radius-selector, 0.375rem);
|
|
895
|
+
}
|
|
896
|
+
.dt-group-toggle:focus-visible {
|
|
897
|
+
outline: 2px solid var(--color-primary);
|
|
898
|
+
outline-offset: 2px;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.dt-group-chevron {
|
|
902
|
+
flex-shrink: 0;
|
|
903
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
904
|
+
transition: transform var(--motion-fast, 150ms) var(--motion-glide, ease-out);
|
|
905
|
+
}
|
|
906
|
+
.dt-group-chevron-open {
|
|
907
|
+
transform: rotate(90deg);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.dt-group-count {
|
|
911
|
+
font-weight: 400;
|
|
912
|
+
font-size: 0.75rem;
|
|
913
|
+
color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
@media (prefers-reduced-motion: reduce) {
|
|
917
|
+
.dt-group-chevron { transition: none; }
|
|
918
|
+
}
|
|
384
919
|
</style>
|