bo-grid 0.1.0 → 0.2.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 +110 -25
- package/dist/grid/Cell.svelte +124 -10
- package/dist/grid/Cell.svelte.d.ts +17 -1
- package/dist/grid/Grid.svelte +398 -31
- package/dist/grid/Grid.svelte.d.ts +39 -0
- package/dist/grid/Pager.svelte +59 -0
- package/dist/grid/Pager.svelte.d.ts +9 -0
- package/dist/grid/RowMenu.svelte +66 -0
- package/dist/grid/RowMenu.svelte.d.ts +12 -0
- package/dist/grid/column.d.ts +28 -5
- package/dist/grid/column.js +7 -5
- package/dist/grid/export.js +1 -1
- package/dist/grid/grouping.d.ts +2 -0
- package/dist/grid/pin.d.ts +10 -6
- package/dist/grid/pin.js +38 -15
- package/dist/grid/sizing.d.ts +2 -2
- package/dist/grid/sizing.js +4 -3
- package/dist/grid/tree.d.ts +12 -0
- package/dist/grid/tree.js +21 -0
- package/package.json +3 -1
package/dist/grid/Grid.svelte
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { Selection } from './selection.svelte';
|
|
15
15
|
import { aggregate, type AggKind, type AggResult } from './aggregate';
|
|
16
16
|
import { buildFlatRows, activeGroupsAt, type VisualRow, type GroupNode } from './grouping';
|
|
17
|
+
import { buildTreeRows } from './tree';
|
|
17
18
|
import { moveIndex } from './reorder';
|
|
18
19
|
import { parseClipboard, isSingleCell } from './clipboard';
|
|
19
20
|
import { applyWidths, clampWidth, isResizable, type WidthMap } from './sizing';
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
import Cell from './Cell.svelte';
|
|
23
24
|
import GroupRow from './GroupRow.svelte';
|
|
24
25
|
import AggregationBar from './AggregationBar.svelte';
|
|
26
|
+
import Pager from './Pager.svelte';
|
|
27
|
+
import RowMenu from './RowMenu.svelte';
|
|
25
28
|
|
|
26
29
|
let {
|
|
27
30
|
rows,
|
|
@@ -48,6 +51,19 @@
|
|
|
48
51
|
onCellClick,
|
|
49
52
|
pinnedRows = [],
|
|
50
53
|
filterRow = false,
|
|
54
|
+
emptyMessage = 'No matching rows',
|
|
55
|
+
loading = false,
|
|
56
|
+
rowMenu,
|
|
57
|
+
detail,
|
|
58
|
+
detailHeight = 160,
|
|
59
|
+
getChildren,
|
|
60
|
+
onRowReorder,
|
|
61
|
+
pageSize = 0,
|
|
62
|
+
page,
|
|
63
|
+
onPageChange,
|
|
64
|
+
onColumnReorder,
|
|
65
|
+
onColumnResize,
|
|
66
|
+
ariaLabel,
|
|
51
67
|
cell,
|
|
52
68
|
}: {
|
|
53
69
|
rows: GridRow[];
|
|
@@ -99,6 +115,40 @@
|
|
|
99
115
|
/** Show a per-column filter input row under the header. Rows must match every
|
|
100
116
|
non-empty column filter (AND). In-memory mode only. Default false. */
|
|
101
117
|
filterRow?: boolean;
|
|
118
|
+
/** Message shown when there are no rows. Default 'No matching rows'. */
|
|
119
|
+
emptyMessage?: string;
|
|
120
|
+
/** Show a loading overlay over the grid (for consumer-driven async work in
|
|
121
|
+
in-memory mode; source mode shows skeleton rows automatically). */
|
|
122
|
+
loading?: boolean;
|
|
123
|
+
/** Right-click row menu. Return the items for a row; an empty array shows no
|
|
124
|
+
menu. Each item runs `onSelect` and closes the menu. */
|
|
125
|
+
rowMenu?: (row: GridRow) => Array<{ label: string; onSelect: () => void }>;
|
|
126
|
+
/** Master-detail: render an expandable detail panel under a row. Adds a
|
|
127
|
+
leading expand-toggle column. In-memory mode only (overrides rowHeight). */
|
|
128
|
+
detail?: Snippet<[{ row: GridRow }]>;
|
|
129
|
+
/** Height (px) of the expanded detail panel. Default 160. */
|
|
130
|
+
detailHeight?: number;
|
|
131
|
+
/** Tree data: return a row's children (undefined/empty = leaf). When set,
|
|
132
|
+
`rows` are the roots; the grid renders an indented, expandable tree.
|
|
133
|
+
In-memory mode; filter/sort/group/paginate are not applied to the tree. */
|
|
134
|
+
getChildren?: (row: GridRow) => GridRow[] | undefined;
|
|
135
|
+
/** Enable drag-to-reorder rows via a handle in the first column. Called with
|
|
136
|
+
the from/to indices (into the visible rows) on drop — reorder your own
|
|
137
|
+
`rows` in here. Flat, unsorted, in-memory lists only. */
|
|
138
|
+
onRowReorder?: (fromIndex: number, toIndex: number) => void;
|
|
139
|
+
/** Rows per page. When > 0 (in-memory mode), shows a pager instead of one
|
|
140
|
+
long scroll; rows still virtualize within a page. Default 0 (off). */
|
|
141
|
+
pageSize?: number;
|
|
142
|
+
/** Controlled current page (0-based). Omit for uncontrolled paging. */
|
|
143
|
+
page?: number;
|
|
144
|
+
/** Called with the new page index when the pager is used. */
|
|
145
|
+
onPageChange?: (page: number) => void;
|
|
146
|
+
/** Called with the new column-key order after a header drag-reorder. */
|
|
147
|
+
onColumnReorder?: (keys: string[]) => void;
|
|
148
|
+
/** Called with a column key + new width after a drag-resize. */
|
|
149
|
+
onColumnResize?: (key: string, width: number) => void;
|
|
150
|
+
/** Accessible name for the grid (`aria-label` on the `role="grid"` root). */
|
|
151
|
+
ariaLabel?: string;
|
|
102
152
|
filter?: string;
|
|
103
153
|
groupBy?: string[];
|
|
104
154
|
aggregations?: AggKind[];
|
|
@@ -135,9 +185,13 @@
|
|
|
135
185
|
const sel = new Selection();
|
|
136
186
|
let dragging = $state(false);
|
|
137
187
|
let editing = $state<{ r: number; c: number } | null>(null);
|
|
188
|
+
// When editing was opened by typing a character (type-to-edit), the editor
|
|
189
|
+
// seeds its input with it; null means edit the existing value (dblclick/Enter).
|
|
190
|
+
let editSeed = $state<string | null>(null);
|
|
138
191
|
|
|
139
|
-
function startEdit(r: number, c: number) {
|
|
192
|
+
function startEdit(r: number, c: number, seed: string | null = null) {
|
|
140
193
|
if (!isEditable(cols[c]) || !dataAt(r)) return;
|
|
194
|
+
editSeed = seed;
|
|
141
195
|
editing = { r, c };
|
|
142
196
|
}
|
|
143
197
|
// Coerce + validate a raw string for cell (r,c) and emit onCellEdit.
|
|
@@ -161,6 +215,7 @@
|
|
|
161
215
|
|
|
162
216
|
function commitEdit(r: number, c: number, raw: string) {
|
|
163
217
|
editing = null;
|
|
218
|
+
editSeed = null;
|
|
164
219
|
writeCell(r, c, raw);
|
|
165
220
|
}
|
|
166
221
|
|
|
@@ -181,9 +236,30 @@
|
|
|
181
236
|
// Whole-row selection (opt-in), keyed by row id so it survives sort/filter.
|
|
182
237
|
// Plain Set + a version counter for reactivity (same pattern as `collapsed`).
|
|
183
238
|
const SEL_W = 40; // checkbox column width (px)
|
|
239
|
+
const EXP_W = 30; // master-detail expand column width (px)
|
|
184
240
|
const selectedRows = new Set<string | number>();
|
|
185
241
|
let selRowsVersion = $state(0);
|
|
242
|
+
|
|
243
|
+
// Master-detail expansion (opt-in via `detail`), keyed by row id.
|
|
244
|
+
const expandedRows = new Set<string | number>();
|
|
245
|
+
let expVersion = $state(0);
|
|
246
|
+
const expandable = $derived(!!detail && !source);
|
|
247
|
+
|
|
248
|
+
// Leading fixed columns: expand toggle (if any) then checkbox (if any).
|
|
186
249
|
const selOffset = $derived(rowSelection ? 1 : 0);
|
|
250
|
+
const expOffset = $derived(expandable ? 1 : 0);
|
|
251
|
+
const leadCols = $derived(selOffset + expOffset); // count, for aria indices
|
|
252
|
+
const leadPx = $derived(selOffset * SEL_W + expOffset * EXP_W); // px, for sticky offsets
|
|
253
|
+
|
|
254
|
+
function isExpanded(id: string | number): boolean {
|
|
255
|
+
expVersion; // track
|
|
256
|
+
return expandedRows.has(id);
|
|
257
|
+
}
|
|
258
|
+
function toggleExpand(id: string | number): void {
|
|
259
|
+
if (expandedRows.has(id)) expandedRows.delete(id);
|
|
260
|
+
else expandedRows.add(id);
|
|
261
|
+
expVersion++;
|
|
262
|
+
}
|
|
187
263
|
|
|
188
264
|
function isRowSelected(id: string | number): boolean {
|
|
189
265
|
selRowsVersion; // track
|
|
@@ -237,24 +313,38 @@
|
|
|
237
313
|
|
|
238
314
|
// Pinned columns sit to the right of the (also-sticky) checkbox column, so
|
|
239
315
|
// their sticky-left offsets shift by SEL_W when row selection is on.
|
|
316
|
+
// Sticky position for a pinned column: left columns offset past the (also
|
|
317
|
+
// sticky) checkbox column; right columns offset from the right edge.
|
|
318
|
+
function pinStick(ci: number): string {
|
|
319
|
+
const inf = layout.info[ci];
|
|
320
|
+
if (inf.side === 'right') return `position:sticky;right:${inf.right}px;`;
|
|
321
|
+
return `position:sticky;left:${inf.left + leadPx}px;`;
|
|
322
|
+
}
|
|
240
323
|
function headStyle(ci: number): string {
|
|
241
324
|
if (!pinned) return colStyle(cols[ci]);
|
|
242
325
|
const inf = layout.info[ci];
|
|
243
326
|
let s = `flex:0 0 ${inf.width}px;width:${inf.width}px;`;
|
|
244
|
-
if (inf.pinned) s +=
|
|
327
|
+
if (inf.pinned) s += `${pinStick(ci)}z-index:5;background:var(--bo-header-bg);`;
|
|
245
328
|
return s;
|
|
246
329
|
}
|
|
247
330
|
function cellWidthStyle(ci: number): string {
|
|
248
331
|
if (!pinned) return colStyle(cols[ci]);
|
|
249
332
|
const inf = layout.info[ci];
|
|
250
333
|
let s = `flex:0 0 ${inf.width}px;width:${inf.width}px;`;
|
|
251
|
-
if (inf.pinned) s +=
|
|
334
|
+
if (inf.pinned) s += `${pinStick(ci)}z-index:1;background:var(--bo-bg);`;
|
|
252
335
|
return s;
|
|
253
336
|
}
|
|
254
|
-
// The leading checkbox column: a fixed-width flex item, sticky-left
|
|
255
|
-
// grid scrolls horizontally (pinned mode).
|
|
337
|
+
// The leading checkbox column: a fixed-width flex item, sticky-left (past the
|
|
338
|
+
// expand column, if any) when the grid scrolls horizontally (pinned mode).
|
|
256
339
|
function selCellStyle(header: boolean): string {
|
|
257
340
|
let s = `flex:0 0 ${SEL_W}px;width:${SEL_W}px;`;
|
|
341
|
+
if (pinned)
|
|
342
|
+
s += `position:sticky;left:${expOffset * EXP_W}px;z-index:${header ? 6 : 2};background:var(--bo-${header ? 'header-bg' : 'bg'});`;
|
|
343
|
+
return s;
|
|
344
|
+
}
|
|
345
|
+
// The leading expand-toggle column (master-detail), sticky at the far left.
|
|
346
|
+
function expandCellStyle(header: boolean): string {
|
|
347
|
+
let s = `flex:0 0 ${EXP_W}px;width:${EXP_W}px;`;
|
|
258
348
|
if (pinned)
|
|
259
349
|
s += `position:sticky;left:0;z-index:${header ? 6 : 2};background:var(--bo-${header ? 'header-bg' : 'bg'});`;
|
|
260
350
|
return s;
|
|
@@ -301,6 +391,7 @@
|
|
|
301
391
|
/* storage unavailable — order still applies this session */
|
|
302
392
|
}
|
|
303
393
|
}
|
|
394
|
+
onColumnReorder?.(next.map((i) => columns[i].key));
|
|
304
395
|
}
|
|
305
396
|
|
|
306
397
|
// ---- Column resizing -------------------------------------------------------
|
|
@@ -331,7 +422,7 @@
|
|
|
331
422
|
});
|
|
332
423
|
});
|
|
333
424
|
|
|
334
|
-
let resize: { key: string; startX: number; startW: number } | null = null;
|
|
425
|
+
let resize: { key: string; startX: number; startW: number; min?: number; max?: number } | null = null;
|
|
335
426
|
let justResized = false;
|
|
336
427
|
|
|
337
428
|
function startResize(ci: number, e: PointerEvent) {
|
|
@@ -340,22 +431,24 @@
|
|
|
340
431
|
e.stopPropagation();
|
|
341
432
|
const headCell = (e.currentTarget as HTMLElement).closest('.h') as HTMLElement | null;
|
|
342
433
|
const startW = headCell ? headCell.getBoundingClientRect().width : layout.info[ci].width;
|
|
343
|
-
resize = { key: cols[ci].key, startX: e.clientX, startW };
|
|
434
|
+
resize = { key: cols[ci].key, startX: e.clientX, startW, min: cols[ci].minWidth, max: cols[ci].maxWidth };
|
|
344
435
|
window.addEventListener('pointermove', onResizeMove);
|
|
345
436
|
window.addEventListener('pointerup', onResizeUp);
|
|
346
437
|
}
|
|
347
438
|
function onResizeMove(e: PointerEvent) {
|
|
348
439
|
if (!resize) return;
|
|
349
|
-
const w = clampWidth(resize.startW + (e.clientX - resize.startX));
|
|
440
|
+
const w = clampWidth(resize.startW + (e.clientX - resize.startX), resize.min, resize.max);
|
|
350
441
|
widths = { ...widths, [resize.key]: w };
|
|
351
442
|
}
|
|
352
443
|
function onResizeUp() {
|
|
353
444
|
if (!resize) return;
|
|
445
|
+
const { key } = resize;
|
|
354
446
|
resize = null;
|
|
355
447
|
justResized = true; // swallow the click that ends this drag (no sort toggle)
|
|
356
448
|
window.removeEventListener('pointermove', onResizeMove);
|
|
357
449
|
window.removeEventListener('pointerup', onResizeUp);
|
|
358
450
|
persistWidths();
|
|
451
|
+
onColumnResize?.(key, widths[key]);
|
|
359
452
|
}
|
|
360
453
|
/** Double-click a resize grip to clear the override and restore the default. */
|
|
361
454
|
function resetWidth(ci: number, e: MouseEvent) {
|
|
@@ -421,18 +514,75 @@
|
|
|
421
514
|
if (colF.length > 0) {
|
|
422
515
|
r = r.filter((row) => colF.every(([k, v]) => String(row[k] ?? '').toLowerCase().includes(v)));
|
|
423
516
|
}
|
|
424
|
-
if (s.length > 0)
|
|
517
|
+
if (s.length > 0) {
|
|
518
|
+
const colOf = (k: string) => allCols.find((c) => c.key === k);
|
|
519
|
+
r = [...r].sort((a, b) => compareBySorts(a, b, s, colOf));
|
|
520
|
+
}
|
|
425
521
|
return r;
|
|
426
522
|
});
|
|
427
523
|
});
|
|
428
524
|
|
|
525
|
+
// Pagination (in-memory only): slice the view into pages; rows still
|
|
526
|
+
// virtualize within a page. Off when pageSize <= 0.
|
|
527
|
+
const paged = $derived(pageSize > 0 && !source);
|
|
528
|
+
let internalPage = $state(0);
|
|
529
|
+
const currentPage = $derived(page ?? internalPage); // controlled by `page` prop, else internal
|
|
530
|
+
function setPage(p: number): void {
|
|
531
|
+
if (page === undefined) internalPage = p; // uncontrolled: own the state
|
|
532
|
+
onPageChange?.(p); // always notify
|
|
533
|
+
}
|
|
534
|
+
const pageCount = $derived(paged ? Math.max(1, Math.ceil(view.length / pageSize)) : 1);
|
|
535
|
+
// Keep the page in range when the view shrinks (filter/sort changes).
|
|
536
|
+
$effect(() => {
|
|
537
|
+
const max = pageCount - 1;
|
|
538
|
+
untrack(() => {
|
|
539
|
+
if (currentPage > max) setPage(max);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
const pageRows = $derived(
|
|
543
|
+
paged ? view.slice(currentPage * pageSize, currentPage * pageSize + pageSize) : view,
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const treeData = $derived(!!getChildren && !source);
|
|
547
|
+
|
|
548
|
+
// Drag-to-reorder rows (flat, unsorted, in-memory only). The handle lives in
|
|
549
|
+
// the first cell; the dragged/drop indices are tracked in component state.
|
|
550
|
+
const reorderable = $derived(
|
|
551
|
+
!!onRowReorder && !source && !treeData && groupBy.length === 0 && pageSize <= 0,
|
|
552
|
+
);
|
|
553
|
+
let dragRowVr = $state(-1);
|
|
554
|
+
let dropRowVr = $state(-1);
|
|
555
|
+
function onRowDrop() {
|
|
556
|
+
if (dragRowVr >= 0 && dropRowVr >= 0 && dragRowVr !== dropRowVr) {
|
|
557
|
+
onRowReorder?.(dragRowVr, dropRowVr);
|
|
558
|
+
}
|
|
559
|
+
dragRowVr = -1;
|
|
560
|
+
dropRowVr = -1;
|
|
561
|
+
}
|
|
562
|
+
|
|
429
563
|
const flat = $derived.by<VisualRow[]>(() => {
|
|
430
|
-
|
|
564
|
+
if (treeData && getChildren) {
|
|
565
|
+
const roots = rows;
|
|
566
|
+
expVersion; // track expand/collapse
|
|
567
|
+
return untrack(() =>
|
|
568
|
+
buildTreeRows(roots, getChildren, (r) => expandedRows.has(getRowId(r))),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const v = pageRows;
|
|
431
572
|
const gb = groupBy;
|
|
432
573
|
collapsedVersion;
|
|
433
574
|
return untrack(() => buildFlatRows(v, gb, collapsed));
|
|
434
575
|
});
|
|
435
576
|
|
|
577
|
+
function goToPage(p: number): void {
|
|
578
|
+
const next = Math.max(0, Math.min(p, pageCount - 1));
|
|
579
|
+
if (next === currentPage) return;
|
|
580
|
+
setPage(next);
|
|
581
|
+
sel.clear();
|
|
582
|
+
editing = null;
|
|
583
|
+
if (viewportEl) viewportEl.scrollTop = 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
436
586
|
// Header select-all state over the in-memory rows (source mode can't enumerate
|
|
437
587
|
// unloaded ids, so the header checkbox is disabled there).
|
|
438
588
|
const selectAll = $derived.by(() => {
|
|
@@ -483,22 +633,32 @@
|
|
|
483
633
|
// a prefix-sum model (in-memory only — source mode can't know unloaded heights).
|
|
484
634
|
const baseH = $derived(typeof rowHeight === 'number' && rowHeight > 0 ? rowHeight : ROW_H);
|
|
485
635
|
const variable = $derived(typeof rowHeight === 'function' && !source);
|
|
636
|
+
// Use a prefix-sum height model when row heights vary: a `rowHeight` function,
|
|
637
|
+
// or master-detail expansion (expanded rows are taller by `detailHeight`).
|
|
638
|
+
const useHeights = $derived(variable || expandable);
|
|
486
639
|
const heights = $derived.by<number[] | null>(() => {
|
|
487
|
-
if (!
|
|
488
|
-
const fn = rowHeight as (row: GridRow, index: number) => number;
|
|
640
|
+
if (!useHeights) return null;
|
|
641
|
+
const fn = variable ? (rowHeight as (row: GridRow, index: number) => number) : null;
|
|
642
|
+
expVersion; // track expansion changes
|
|
489
643
|
const arr = new Array<number>(flat.length);
|
|
490
644
|
let di = 0;
|
|
491
645
|
for (let i = 0; i < flat.length; i++) {
|
|
492
646
|
const it = flat[i];
|
|
493
|
-
|
|
647
|
+
if (it.kind !== 'data') {
|
|
648
|
+
arr[i] = baseH;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
let h = fn ? Math.max(1, fn(it.row, di++)) : baseH;
|
|
652
|
+
if (expandable && expandedRows.has(getRowId(it.row))) h += detailHeight;
|
|
653
|
+
arr[i] = h;
|
|
494
654
|
}
|
|
495
655
|
return arr;
|
|
496
656
|
});
|
|
497
|
-
const hm = $derived(
|
|
657
|
+
const hm = $derived(useHeights && heights ? variableHeights(heights) : uniformHeights(rowCount, baseH));
|
|
498
658
|
|
|
499
659
|
const total = $derived(hm.total);
|
|
500
660
|
const rowWidthStyle = $derived(
|
|
501
|
-
pinned ? `width:${layout.totalWidth +
|
|
661
|
+
pinned ? `width:${layout.totalWidth + leadPx}px;right:auto;` : '',
|
|
502
662
|
);
|
|
503
663
|
const visibleCount = $derived(Math.ceil(height / baseH) + OVERSCAN * 2);
|
|
504
664
|
const start = $derived(Math.max(0, hm.indexAt(scrollTop) - OVERSCAN));
|
|
@@ -510,7 +670,7 @@
|
|
|
510
670
|
|
|
511
671
|
type RenderItem =
|
|
512
672
|
| { vr: number; kind: 'group'; group: GroupNode }
|
|
513
|
-
| { vr: number; kind: 'data'; row: GridRow }
|
|
673
|
+
| { vr: number; kind: 'data'; row: GridRow; depth?: number; hasChildren?: boolean }
|
|
514
674
|
| { vr: number; kind: 'skeleton' };
|
|
515
675
|
|
|
516
676
|
const renderItems = $derived.by<RenderItem[]>(() => {
|
|
@@ -526,7 +686,7 @@
|
|
|
526
686
|
const item = flat[vr];
|
|
527
687
|
if (!item) continue;
|
|
528
688
|
if (item.kind === 'group') out.push({ vr, kind: 'group', group: item.group });
|
|
529
|
-
else out.push({ vr, kind: 'data', row: item.row });
|
|
689
|
+
else out.push({ vr, kind: 'data', row: item.row, depth: item.depth, hasChildren: item.hasChildren });
|
|
530
690
|
}
|
|
531
691
|
}
|
|
532
692
|
return out;
|
|
@@ -598,6 +758,29 @@
|
|
|
598
758
|
if (dragging) sel.extendTo(r, c);
|
|
599
759
|
}
|
|
600
760
|
|
|
761
|
+
// Right-click row menu (floating).
|
|
762
|
+
let menu = $state<{ x: number; y: number; items: Array<{ label: string; onSelect: () => void }> } | null>(null);
|
|
763
|
+
function openRowMenu(row: GridRow, e: MouseEvent) {
|
|
764
|
+
if (!rowMenu) return;
|
|
765
|
+
const items = rowMenu(row);
|
|
766
|
+
if (items.length === 0) return;
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
menu = { x: e.clientX, y: e.clientY, items };
|
|
769
|
+
}
|
|
770
|
+
$effect(() => {
|
|
771
|
+
if (!menu) return;
|
|
772
|
+
const close = () => (menu = null);
|
|
773
|
+
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && (menu = null);
|
|
774
|
+
window.addEventListener('pointerdown', close);
|
|
775
|
+
window.addEventListener('keydown', onKey);
|
|
776
|
+
window.addEventListener('blur', close);
|
|
777
|
+
return () => {
|
|
778
|
+
window.removeEventListener('pointerdown', close);
|
|
779
|
+
window.removeEventListener('keydown', onKey);
|
|
780
|
+
window.removeEventListener('blur', close);
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
|
|
601
784
|
function onCellClicked(r: number, c: number, e: MouseEvent) {
|
|
602
785
|
if (!onCellClick) return;
|
|
603
786
|
const row = dataAt(r);
|
|
@@ -636,7 +819,7 @@
|
|
|
636
819
|
const cells: string[] = [];
|
|
637
820
|
for (let c = b.c0; c <= cEnd; c++) {
|
|
638
821
|
const col = cols[c];
|
|
639
|
-
cells.push(col.type === 'sparkline' || col.type === 'custom' ? '' : formatCell(col, row[col.key]));
|
|
822
|
+
cells.push(col.type === 'sparkline' || col.type === 'custom' ? '' : formatCell(col, row[col.key], row));
|
|
640
823
|
}
|
|
641
824
|
lines.push(cells.join('\t'));
|
|
642
825
|
}
|
|
@@ -708,6 +891,28 @@
|
|
|
708
891
|
}
|
|
709
892
|
}
|
|
710
893
|
}
|
|
894
|
+
// Space toggles selection of the focused row (when the checkbox column is on),
|
|
895
|
+
// so keyboard users can tick rows without reaching for the mouse.
|
|
896
|
+
if (e.key === ' ' && rowSelection && sel.focus && !editing) {
|
|
897
|
+
const row = dataAt(sel.focus.r);
|
|
898
|
+
if (row) {
|
|
899
|
+
e.preventDefault();
|
|
900
|
+
toggleRow(getRowId(row));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// Type-to-edit (Excel-style): a printable key on a focused editable text/number
|
|
905
|
+
// cell opens the editor seeded with that character. Select-column editors keep
|
|
906
|
+
// their Enter-to-open behavior.
|
|
907
|
+
if (!mod && !e.altKey && e.key.length === 1 && e.key !== ' ' && sel.focus && !editing) {
|
|
908
|
+
const f = sel.focus;
|
|
909
|
+
const col = cols[f.c];
|
|
910
|
+
if (isEditable(col) && !(col.options && col.options.length) && dataAt(f.r)) {
|
|
911
|
+
e.preventDefault();
|
|
912
|
+
startEdit(f.r, f.c, e.key);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
711
916
|
if (mod && e.key.toLowerCase() === 'a') {
|
|
712
917
|
e.preventDefault();
|
|
713
918
|
sel.selectAll(rowCount, cols.length);
|
|
@@ -727,6 +932,36 @@
|
|
|
727
932
|
sel.clear();
|
|
728
933
|
return;
|
|
729
934
|
}
|
|
935
|
+
// Open the row menu from the keyboard (ContextMenu key / Shift+F10).
|
|
936
|
+
if (rowMenu && sel.focus && (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10'))) {
|
|
937
|
+
const row = dataAt(sel.focus.r);
|
|
938
|
+
const items = row ? rowMenu(row) : [];
|
|
939
|
+
if (items.length > 0) {
|
|
940
|
+
e.preventDefault();
|
|
941
|
+
const rect = document.getElementById(activeId ?? '')?.getBoundingClientRect();
|
|
942
|
+
menu = { x: rect?.left ?? 0, y: rect?.bottom ?? 0, items };
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Tree nav: ArrowRight expands a collapsed node, ArrowLeft collapses an
|
|
947
|
+
// expanded one (treegrid pattern); otherwise arrows move normally.
|
|
948
|
+
if (treeData && sel.focus && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) {
|
|
949
|
+
const item = flat[sel.focus.r];
|
|
950
|
+
if (item?.kind === 'data' && item.hasChildren) {
|
|
951
|
+
const id = getRowId(item.row);
|
|
952
|
+
const open = expandedRows.has(id);
|
|
953
|
+
if (e.key === 'ArrowRight' && !open) {
|
|
954
|
+
e.preventDefault();
|
|
955
|
+
toggleExpand(id);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (e.key === 'ArrowLeft' && open) {
|
|
959
|
+
e.preventDefault();
|
|
960
|
+
toggleExpand(id);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
730
965
|
// Home/End (row, or whole grid with Ctrl/⌘); PageUp/PageDown by a viewport page.
|
|
731
966
|
const f = sel.focus;
|
|
732
967
|
if (f && (e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown')) {
|
|
@@ -777,8 +1012,9 @@
|
|
|
777
1012
|
role="grid"
|
|
778
1013
|
tabindex="0"
|
|
779
1014
|
id={gid}
|
|
1015
|
+
aria-label={ariaLabel}
|
|
780
1016
|
aria-rowcount={rowCount + 1}
|
|
781
|
-
aria-colcount={cols.length +
|
|
1017
|
+
aria-colcount={cols.length + leadCols}
|
|
782
1018
|
aria-multiselectable="true"
|
|
783
1019
|
aria-activedescendant={activeId}
|
|
784
1020
|
style={themeStyle}
|
|
@@ -787,6 +1023,7 @@
|
|
|
787
1023
|
>
|
|
788
1024
|
{#if headerGroups}
|
|
789
1025
|
<div class="head-groups" aria-hidden="true" bind:this={groupHeadEl} style={pinned ? 'overflow:hidden;' : ''}>
|
|
1026
|
+
{#if expandable}<span class="expandcell" style={expandCellStyle(true)}></span>{/if}
|
|
790
1027
|
{#if rowSelection}<span class="selcell" style={selCellStyle(true)}></span>{/if}
|
|
791
1028
|
{#each headerGroups as g, gi (gi)}
|
|
792
1029
|
<span class="hg" class:empty={!g.label} style="flex:0 0 {g.width}px;width:{g.width}px;">{g.label}</span>
|
|
@@ -794,8 +1031,11 @@
|
|
|
794
1031
|
</div>
|
|
795
1032
|
{/if}
|
|
796
1033
|
<div class="head" role="row" aria-rowindex={1} bind:this={headEl} style={pinned ? 'overflow:hidden;' : ''}>
|
|
1034
|
+
{#if expandable}
|
|
1035
|
+
<span class="expandcell selhead" role="columnheader" aria-colindex={1} style={expandCellStyle(true)}></span>
|
|
1036
|
+
{/if}
|
|
797
1037
|
{#if rowSelection}
|
|
798
|
-
<span class="selcell selhead" role="columnheader" aria-colindex={1} style={selCellStyle(true)}>
|
|
1038
|
+
<span class="selcell selhead" role="columnheader" aria-colindex={1 + expOffset} style={selCellStyle(true)}>
|
|
799
1039
|
<input
|
|
800
1040
|
type="checkbox"
|
|
801
1041
|
class="rowcheck"
|
|
@@ -810,7 +1050,7 @@
|
|
|
810
1050
|
{/if}
|
|
811
1051
|
{#each cols as col, ci (ci)}
|
|
812
1052
|
<button
|
|
813
|
-
class="h"
|
|
1053
|
+
class="h {col.headerClass ?? ''}"
|
|
814
1054
|
class:right={isNumeric(col) || col.align === 'right'}
|
|
815
1055
|
class:sortable={isSortable(col)}
|
|
816
1056
|
class:dragging={ci === dragSrc}
|
|
@@ -818,7 +1058,7 @@
|
|
|
818
1058
|
style={headStyle(ci)}
|
|
819
1059
|
type="button"
|
|
820
1060
|
role="columnheader"
|
|
821
|
-
aria-colindex={ci + 1 +
|
|
1061
|
+
aria-colindex={ci + 1 + leadCols}
|
|
822
1062
|
draggable="true"
|
|
823
1063
|
aria-sort={isSortable(col) && sortInfo(col.key)
|
|
824
1064
|
? sortInfo(col.key)?.dir === 'asc'
|
|
@@ -873,6 +1113,7 @@
|
|
|
873
1113
|
|
|
874
1114
|
{#if filterRow && !source}
|
|
875
1115
|
<div class="filter-row" role="row" bind:this={filterRowEl} style={pinned ? 'overflow:hidden;' : ''}>
|
|
1116
|
+
{#if expandable}<span class="expandcell" style={expandCellStyle(false)}></span>{/if}
|
|
876
1117
|
{#if rowSelection}<span class="selcell" style={selCellStyle(false)}></span>{/if}
|
|
877
1118
|
{#each cols as col, ci (ci)}
|
|
878
1119
|
<span class="fr-cell" style={cellWidthStyle(ci)}>
|
|
@@ -901,6 +1142,7 @@
|
|
|
901
1142
|
<div class="pinned-top">
|
|
902
1143
|
{#each pinnedRows as prow, pi (getRowId(prow))}
|
|
903
1144
|
<div class="row pinrow {rowClass?.(prow) ?? ''}" role="row" aria-hidden="true" style="height:{baseH}px;{rowWidthStyle}">
|
|
1145
|
+
{#if expandable}<span class="expandcell" style={expandCellStyle(false)}></span>{/if}
|
|
904
1146
|
{#if rowSelection}<span class="selcell" style={selCellStyle(false)}></span>{/if}
|
|
905
1147
|
{#each cols as col, ci (ci)}
|
|
906
1148
|
<Cell
|
|
@@ -908,11 +1150,12 @@
|
|
|
908
1150
|
row={prow}
|
|
909
1151
|
r={-1 - pi}
|
|
910
1152
|
c={ci}
|
|
911
|
-
colIndex={ci + 1 +
|
|
1153
|
+
colIndex={ci + 1 + leadCols}
|
|
912
1154
|
cellId={`${gid}-pin${pi}-c${ci}`}
|
|
913
1155
|
cellSnippet={cell}
|
|
914
1156
|
pinned={pinned && layout.info[ci].pinned}
|
|
915
|
-
|
|
1157
|
+
pinSide={layout.info[ci].side ?? 'left'}
|
|
1158
|
+
pinOffset={layout.info[ci].side === 'right' ? layout.info[ci].right : layout.info[ci].left + leadPx}
|
|
916
1159
|
width={pinned ? layout.info[ci].width : undefined}
|
|
917
1160
|
/>
|
|
918
1161
|
{/each}
|
|
@@ -921,7 +1164,13 @@
|
|
|
921
1164
|
</div>
|
|
922
1165
|
{/if}
|
|
923
1166
|
{#if rowCount === 0 && !controller?.loading}
|
|
924
|
-
<div class="empty">
|
|
1167
|
+
<div class="empty">{emptyMessage}</div>
|
|
1168
|
+
{/if}
|
|
1169
|
+
{#if loading}
|
|
1170
|
+
<div class="loading-overlay" aria-busy="true" aria-live="polite">
|
|
1171
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
1172
|
+
<span class="loading-label">Loading…</span>
|
|
1173
|
+
</div>
|
|
925
1174
|
{/if}
|
|
926
1175
|
{#if stickyGroups.length > 0}
|
|
927
1176
|
<div class="sticky">
|
|
@@ -932,15 +1181,17 @@
|
|
|
932
1181
|
{/each}
|
|
933
1182
|
</div>
|
|
934
1183
|
{/if}
|
|
935
|
-
<div class="spacer" style="height:{total}px;{pinned ? `width:${layout.totalWidth +
|
|
1184
|
+
<div class="spacer" style="height:{total}px;{pinned ? `width:${layout.totalWidth + leadPx}px;` : ''}">
|
|
936
1185
|
{#each renderItems as item (item.vr)}
|
|
937
1186
|
{#if item.kind === 'group'}
|
|
938
1187
|
<div class="grouprow" style="top:{hm.offsetOf(item.vr)}px;height:{hm.heightOf(item.vr)}px;{rowWidthStyle}">
|
|
1188
|
+
{#if expandable}<span class="expandcell" aria-hidden="true" style={expandCellStyle(false)}></span>{/if}
|
|
939
1189
|
{#if rowSelection}<span class="selcell" aria-hidden="true" style={selCellStyle(false)}></span>{/if}
|
|
940
1190
|
<GroupRow group={item.group} columns={cols} onToggle={toggleGroup} rowIndex={item.vr + 2} />
|
|
941
1191
|
</div>
|
|
942
1192
|
{:else if item.kind === 'skeleton'}
|
|
943
1193
|
<div class="row skeleton" role="row" aria-rowindex={item.vr + 2} aria-hidden="true" style="top:{hm.offsetOf(item.vr)}px;height:{hm.heightOf(item.vr)}px;{rowWidthStyle}">
|
|
1194
|
+
{#if expandable}<span class="expandcell" style={expandCellStyle(false)}></span>{/if}
|
|
944
1195
|
{#if rowSelection}<span class="selcell" style={selCellStyle(false)}></span>{/if}
|
|
945
1196
|
{#each cols as col, ci (ci)}
|
|
946
1197
|
<span class="c" style={cellWidthStyle(ci)}><span class="skelbar"></span></span>
|
|
@@ -949,7 +1200,24 @@
|
|
|
949
1200
|
{:else}
|
|
950
1201
|
<!-- Row activation is keyboard-accessible at the grid level: Enter on the focused cell fires onRowClick (focus is via aria-activedescendant). -->
|
|
951
1202
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
952
|
-
<div class="row {rowClass?.(item.row) ?? ''}" class:alt={item.vr % 2 === 1} class:rowsel={rowSelection && isRowSelected(getRowId(item.row))} class:clickable={!!onRowClick} role="row" tabindex="-1" aria-rowindex={item.vr + 2} aria-selected={rowSelection ? isRowSelected(getRowId(item.row)) : undefined} style="top:{hm.offsetOf(item.vr)}px;height:{hm.heightOf(item.vr)}px;{rowWidthStyle}" onclick={(e) => onRowClick?.(item.row, e)}>
|
|
1203
|
+
<div class="row {rowClass?.(item.row) ?? ''}" class:alt={item.vr % 2 === 1} class:rowsel={rowSelection && isRowSelected(getRowId(item.row))} class:clickable={!!onRowClick} class:droptarget={reorderable && dropRowVr === item.vr && dragRowVr !== item.vr} role="row" tabindex="-1" aria-rowindex={item.vr + 2} aria-selected={rowSelection ? isRowSelected(getRowId(item.row)) : undefined} aria-level={treeData ? (item.depth ?? 0) + 1 : undefined} aria-expanded={treeData && item.hasChildren ? isExpanded(getRowId(item.row)) : undefined} style="top:{hm.offsetOf(item.vr)}px;height:{expandable ? baseH : hm.heightOf(item.vr)}px;{rowWidthStyle}" onclick={(e) => onRowClick?.(item.row, e)} oncontextmenu={(e) => openRowMenu(item.row, e)} ondragover={reorderable ? (e) => { if (dragRowVr < 0) return; e.preventDefault(); dropRowVr = item.vr; } : undefined} ondrop={reorderable ? (e) => { e.preventDefault(); onRowDrop(); } : undefined}>
|
|
1204
|
+
{#if expandable}
|
|
1205
|
+
<span class="expandcell" style={expandCellStyle(false)}>
|
|
1206
|
+
<button
|
|
1207
|
+
class="expand-toggle"
|
|
1208
|
+
type="button"
|
|
1209
|
+
aria-expanded={isExpanded(getRowId(item.row))}
|
|
1210
|
+
aria-label="Toggle detail"
|
|
1211
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
1212
|
+
onclick={(e) => {
|
|
1213
|
+
e.stopPropagation();
|
|
1214
|
+
toggleExpand(getRowId(item.row));
|
|
1215
|
+
}}
|
|
1216
|
+
>
|
|
1217
|
+
{isExpanded(getRowId(item.row)) ? '▾' : '▸'}
|
|
1218
|
+
</button>
|
|
1219
|
+
</span>
|
|
1220
|
+
{/if}
|
|
953
1221
|
{#if rowSelection}
|
|
954
1222
|
<span class="selcell" style={selCellStyle(false)}>
|
|
955
1223
|
<input
|
|
@@ -969,30 +1237,52 @@
|
|
|
969
1237
|
row={item.row}
|
|
970
1238
|
r={item.vr}
|
|
971
1239
|
c={ci}
|
|
972
|
-
colIndex={ci + 1 +
|
|
1240
|
+
colIndex={ci + 1 + leadCols}
|
|
973
1241
|
cellId={`${gid}-r${item.vr}-c${ci}`}
|
|
974
1242
|
cellSnippet={cell}
|
|
975
1243
|
selected={sel.contains(item.vr, ci)}
|
|
976
1244
|
focused={sel.isFocus(item.vr, ci)}
|
|
977
1245
|
pinned={pinned && layout.info[ci].pinned}
|
|
978
|
-
|
|
1246
|
+
pinSide={layout.info[ci].side ?? 'left'}
|
|
1247
|
+
pinOffset={layout.info[ci].side === 'right' ? layout.info[ci].right : layout.info[ci].left + leadPx}
|
|
979
1248
|
width={pinned ? layout.info[ci].width : undefined}
|
|
980
1249
|
alt={item.vr % 2 === 1}
|
|
981
1250
|
editing={editing?.r === item.vr && editing?.c === ci}
|
|
1251
|
+
seed={editing?.r === item.vr && editing?.c === ci ? editSeed : null}
|
|
1252
|
+
tree={treeData && ci === 0
|
|
1253
|
+
? {
|
|
1254
|
+
depth: item.depth ?? 0,
|
|
1255
|
+
hasChildren: item.hasChildren ?? false,
|
|
1256
|
+
expanded: isExpanded(getRowId(item.row)),
|
|
1257
|
+
onToggle: () => toggleExpand(getRowId(item.row)),
|
|
1258
|
+
}
|
|
1259
|
+
: undefined}
|
|
1260
|
+
dragHandle={reorderable && ci === 0
|
|
1261
|
+
? { onStart: () => (dragRowVr = item.vr), onEnd: () => (dragRowVr = -1) }
|
|
1262
|
+
: undefined}
|
|
982
1263
|
{onCellDown}
|
|
983
1264
|
{onCellEnter}
|
|
984
1265
|
onCellClick={onCellClick ? onCellClicked : undefined}
|
|
985
1266
|
onCellDblClick={startEdit}
|
|
986
1267
|
onEditCommit={(raw) => commitEdit(item.vr, ci, raw)}
|
|
987
|
-
onEditCancel={() =>
|
|
1268
|
+
onEditCancel={() => {
|
|
1269
|
+
editing = null;
|
|
1270
|
+
editSeed = null;
|
|
1271
|
+
}}
|
|
988
1272
|
/>
|
|
989
1273
|
{/each}
|
|
990
1274
|
</div>
|
|
1275
|
+
{#if expandable && detail && isExpanded(getRowId(item.row))}
|
|
1276
|
+
<div class="row-detail" style="top:{hm.offsetOf(item.vr) + baseH}px;height:{detailHeight}px;{rowWidthStyle}">
|
|
1277
|
+
{@render detail({ row: item.row })}
|
|
1278
|
+
</div>
|
|
1279
|
+
{/if}
|
|
991
1280
|
{/if}
|
|
992
1281
|
{/each}
|
|
993
1282
|
</div>
|
|
994
1283
|
{#if footerCells}
|
|
995
|
-
<div class="footer" role="row" style={pinned ? `width:${layout.totalWidth +
|
|
1284
|
+
<div class="footer" role="row" style={pinned ? `width:${layout.totalWidth + leadPx}px;` : ''}>
|
|
1285
|
+
{#if expandable}<span class="expandcell" aria-hidden="true" style={expandCellStyle(false)}></span>{/if}
|
|
996
1286
|
{#if rowSelection}<span class="selcell" aria-hidden="true" style={selCellStyle(false)}></span>{/if}
|
|
997
1287
|
{#each cols as col, ci (ci)}
|
|
998
1288
|
<span class="fcell" class:right={isNumeric(col)} style={cellWidthStyle(ci)}>
|
|
@@ -1004,6 +1294,14 @@
|
|
|
1004
1294
|
</div>
|
|
1005
1295
|
|
|
1006
1296
|
<AggregationBar result={agg} kinds={aggregations} />
|
|
1297
|
+
|
|
1298
|
+
{#if paged}
|
|
1299
|
+
<Pager page={currentPage} {pageCount} total={view.length} onGoto={goToPage} />
|
|
1300
|
+
{/if}
|
|
1301
|
+
|
|
1302
|
+
{#if menu}
|
|
1303
|
+
<RowMenu x={menu.x} y={menu.y} items={menu.items} onClose={() => (menu = null)} />
|
|
1304
|
+
{/if}
|
|
1007
1305
|
</div>
|
|
1008
1306
|
|
|
1009
1307
|
<style>
|
|
@@ -1199,6 +1497,40 @@
|
|
|
1199
1497
|
color: var(--bo-text-dim);
|
|
1200
1498
|
font-size: 13px;
|
|
1201
1499
|
}
|
|
1500
|
+
.loading-overlay {
|
|
1501
|
+
position: sticky;
|
|
1502
|
+
top: 0;
|
|
1503
|
+
left: 0;
|
|
1504
|
+
z-index: 7;
|
|
1505
|
+
display: flex;
|
|
1506
|
+
align-items: center;
|
|
1507
|
+
justify-content: center;
|
|
1508
|
+
gap: 10px;
|
|
1509
|
+
height: 100%;
|
|
1510
|
+
margin-bottom: -100%; /* overlay without consuming scroll height */
|
|
1511
|
+
color: var(--bo-text-dim);
|
|
1512
|
+
font-size: 13px;
|
|
1513
|
+
background: color-mix(in srgb, var(--bo-bg) 70%, transparent);
|
|
1514
|
+
backdrop-filter: blur(1px);
|
|
1515
|
+
}
|
|
1516
|
+
.spinner {
|
|
1517
|
+
width: 18px;
|
|
1518
|
+
height: 18px;
|
|
1519
|
+
border: 2px solid var(--bo-border);
|
|
1520
|
+
border-top-color: var(--bo-sel-border);
|
|
1521
|
+
border-radius: 50%;
|
|
1522
|
+
animation: bo-spin 0.7s linear infinite;
|
|
1523
|
+
}
|
|
1524
|
+
@keyframes bo-spin {
|
|
1525
|
+
to {
|
|
1526
|
+
transform: rotate(360deg);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1530
|
+
.spinner {
|
|
1531
|
+
animation: none;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1202
1534
|
.spacer {
|
|
1203
1535
|
position: relative;
|
|
1204
1536
|
width: 100%;
|
|
@@ -1244,6 +1576,9 @@
|
|
|
1244
1576
|
.row.clickable {
|
|
1245
1577
|
cursor: pointer;
|
|
1246
1578
|
}
|
|
1579
|
+
.row.droptarget {
|
|
1580
|
+
box-shadow: inset 0 2px 0 var(--bo-sel-border);
|
|
1581
|
+
}
|
|
1247
1582
|
|
|
1248
1583
|
/* Pinned top rows: stick to the top of the viewport above the scroll. */
|
|
1249
1584
|
.pinned-top {
|
|
@@ -1289,6 +1624,38 @@
|
|
|
1289
1624
|
justify-content: flex-end;
|
|
1290
1625
|
}
|
|
1291
1626
|
|
|
1627
|
+
/* Master-detail: leading expand-toggle column + detail panel. */
|
|
1628
|
+
.expandcell {
|
|
1629
|
+
display: flex;
|
|
1630
|
+
align-items: center;
|
|
1631
|
+
justify-content: center;
|
|
1632
|
+
flex: 0 0 auto;
|
|
1633
|
+
}
|
|
1634
|
+
.expand-toggle {
|
|
1635
|
+
width: 20px;
|
|
1636
|
+
height: 20px;
|
|
1637
|
+
padding: 0;
|
|
1638
|
+
font-size: 11px;
|
|
1639
|
+
line-height: 1;
|
|
1640
|
+
color: var(--bo-text-dim);
|
|
1641
|
+
background: transparent;
|
|
1642
|
+
border: 0;
|
|
1643
|
+
border-radius: 4px;
|
|
1644
|
+
cursor: pointer;
|
|
1645
|
+
}
|
|
1646
|
+
.expand-toggle:hover {
|
|
1647
|
+
color: var(--bo-text);
|
|
1648
|
+
background: var(--bo-row-hover);
|
|
1649
|
+
}
|
|
1650
|
+
.row-detail {
|
|
1651
|
+
position: absolute;
|
|
1652
|
+
left: 0;
|
|
1653
|
+
overflow: auto;
|
|
1654
|
+
background: var(--bo-row-a);
|
|
1655
|
+
border-bottom: 0.5px solid var(--bo-border);
|
|
1656
|
+
box-shadow: inset 0 1px 0 var(--bo-border);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1292
1659
|
/* Leading checkbox column (row selection). */
|
|
1293
1660
|
.selcell {
|
|
1294
1661
|
display: flex;
|