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/README.md
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# bo-grid
|
|
2
2
|
|
|
3
3
|
Tiny, fast **Svelte 5** data grid for fintech UIs — canvas sparklines, batched
|
|
4
|
-
realtime cell updates, and virtual scrolling in a package that gzips to
|
|
4
|
+
realtime cell updates, and virtual scrolling in a package that gzips to ~20 KB.
|
|
5
5
|
A free alternative to the heavyweight grids that paywall these features.
|
|
6
6
|
|
|
7
7
|
**[Live demo](https://bonguynvan.github.io/bo-grid/)** ·
|
|
8
|
-
**[API reference](https://bonguynvan.github.io/bo-grid/api.html)**
|
|
8
|
+
**[API reference](https://bonguynvan.github.io/bo-grid/api.html)** ·
|
|
9
|
+
**[Benchmarks](./BENCHMARKS.md)**
|
|
9
10
|
|
|
10
|
-
The demo is a small gallery of
|
|
11
|
+
The demo is a small gallery of nine grid types — a realtime **Trading desk**, a
|
|
11
12
|
grouped **Portfolio** with subtotals and pivot, a general-purpose editable
|
|
12
13
|
**Spreadsheet**, a live **Order book** depth ladder, a **Correlation** heatmap
|
|
13
|
-
matrix, a **Leaderboard** with rank medals and score bars,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
>
|
|
18
|
-
>
|
|
14
|
+
matrix, a **Leaderboard** with rank medals and score bars, a **Tree** file
|
|
15
|
+
explorer, a drag-to-reorder **Tasks** list, and a **1M-row** trade tape windowed
|
|
16
|
+
from a synthetic source — switch between them with the tabs.
|
|
17
|
+
|
|
18
|
+
> **Status: v0.2.** Working: config-driven columns, virtual scroll, client
|
|
19
|
+
> single/multi/controlled sort + global & per-column filters, multi-cell selection
|
|
20
|
+
> + live range aggregation, row grouping (nested, collapsible, sticky headers,
|
|
21
|
+
> live subtotals), pivot tables, tree data, master-detail, a server-side
|
|
19
22
|
> `RowSource` for huge datasets, CSV/Excel export, drag-to-reorder and
|
|
20
|
-
> drag-to-resize columns, pinned columns,
|
|
21
|
-
> copy/paste, sparklines,
|
|
22
|
-
>
|
|
23
|
-
>
|
|
23
|
+
> drag-to-resize columns, pinned columns (left/right), row selection, pagination,
|
|
24
|
+
> inline editing with validation, type-to-edit, clipboard copy/paste, sparklines,
|
|
25
|
+
> realtime flash, heatmaps, theming, and full keyboard a11y. **SSR/SvelteKit-safe.**
|
|
26
|
+
> Unit tests (Vitest), type-check, a headless mount smoke-test, an SSR render
|
|
27
|
+
> check, and library + demo bundle-size budgets all run in CI. A formal WCAG audit
|
|
24
28
|
> is the main thing left — see the roadmap.
|
|
25
29
|
|
|
26
30
|
## Why
|
|
@@ -30,9 +34,14 @@ tape windowed from a synthetic source — switch between them with the tabs.
|
|
|
30
34
|
| Price | $$$ / dev / year | Free (MIT) |
|
|
31
35
|
| Sparklines | paid tier | built in |
|
|
32
36
|
| Realtime cell updates | DIY / complex | built-in primitive |
|
|
33
|
-
| Bundle |
|
|
37
|
+
| Bundle | hundreds of KB | **~20 KB gzip** ([benchmarks](./BENCHMARKS.md)) |
|
|
34
38
|
| Svelte | wrapper | native Svelte 5 |
|
|
35
39
|
|
|
40
|
+
bo-grid ships most of AG Grid's **paid (Enterprise)** features — grouping, pivot,
|
|
41
|
+
tree data, master-detail, range selection, Excel export, sparklines — for free.
|
|
42
|
+
See the honest **[bo-grid vs AG Grid](./docs/vs-ag-grid.md)** comparison (including
|
|
43
|
+
what it doesn't do) to decide.
|
|
44
|
+
|
|
36
45
|
## Install
|
|
37
46
|
|
|
38
47
|
```sh
|
|
@@ -40,6 +49,12 @@ npm i bo-grid
|
|
|
40
49
|
# peer dependency: svelte@^5
|
|
41
50
|
```
|
|
42
51
|
|
|
52
|
+
Works with **SvelteKit / SSR** out of the box — `<Grid>` server-renders to HTML
|
|
53
|
+
without touching `window`/`document`/`localStorage` (a CI gate, `pnpm ssr`,
|
|
54
|
+
proves it). The package is `sideEffects: false`, so unused exports tree-shake
|
|
55
|
+
away. See the **[SvelteKit guide](./docs/sveltekit.md)** for `load`-function data,
|
|
56
|
+
realtime feeds, and layout persistence.
|
|
57
|
+
|
|
43
58
|
## Usage
|
|
44
59
|
|
|
45
60
|
```svelte
|
|
@@ -112,6 +127,15 @@ a function for variable per-row heights (in-memory mode):
|
|
|
112
127
|
Variable heights use a prefix-sum + binary-search virtualizer, so scrolling stays
|
|
113
128
|
O(log n). Source mode is uniform-only (unloaded row heights aren't known).
|
|
114
129
|
|
|
130
|
+
## Pagination
|
|
131
|
+
|
|
132
|
+
Prefer pages over one long scroll? Set `pageSize` (> 0) for a paged view with a
|
|
133
|
+
first/prev/next/last pager; rows still virtualize within each page. In-memory mode.
|
|
134
|
+
|
|
135
|
+
```svelte
|
|
136
|
+
<Grid {rows} {columns} height={640} pageSize={25} />
|
|
137
|
+
```
|
|
138
|
+
|
|
115
139
|
## Sort & filter
|
|
116
140
|
|
|
117
141
|
Click a column header to sort (asc → desc → off). **Shift-click** additional
|
|
@@ -175,7 +199,8 @@ columns (and `rowClass`) but are display-only:
|
|
|
175
199
|
|
|
176
200
|
Set `rowSelection` for a leading checkbox column — whole-row selection keyed by
|
|
177
201
|
`row.id`, so it survives sorting and filtering (unlike the positional cell
|
|
178
|
-
selection above). The header checkbox selects/clears all matching rows
|
|
202
|
+
selection above). The header checkbox selects/clears all matching rows, and
|
|
203
|
+
<kbd>Space</kbd> toggles the focused row from the keyboard.
|
|
179
204
|
`onRowSelectionChange` reports the selected ids:
|
|
180
205
|
|
|
181
206
|
```svelte
|
|
@@ -269,8 +294,10 @@ client-only, so it's not applied in source mode.
|
|
|
269
294
|
|
|
270
295
|
## Column reorder
|
|
271
296
|
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
Pass `onRowReorder(from, to)` to enable **drag-to-reorder rows** via a handle in
|
|
298
|
+
the first column — reorder your own `rows` array in the callback (flat, unsorted,
|
|
299
|
+
in-memory lists). Drag any column header to reorder columns; pass `persistKey` to
|
|
300
|
+
remember the user's order across reloads (saved to `localStorage`):
|
|
274
301
|
|
|
275
302
|
```svelte
|
|
276
303
|
<Grid {rows} {columns} persistKey="watchlist" height={640} />
|
|
@@ -283,8 +310,9 @@ grip to reset it to its default width. Resizing a fit-to-width (`flex`) column
|
|
|
283
310
|
pins it to the dragged width and lets its neighbours absorb the difference. The
|
|
284
311
|
same `persistKey` remembers widths across reloads.
|
|
285
312
|
|
|
286
|
-
|
|
287
|
-
|
|
313
|
+
Bound a column's draggable range with `minWidth` / `maxWidth`. Resizing is on by
|
|
314
|
+
default. Turn it off for the whole grid with `resizable={false}`, or per column
|
|
315
|
+
with `resizable: false` (handy for a fixed action column):
|
|
288
316
|
|
|
289
317
|
```svelte
|
|
290
318
|
<Grid {rows} {columns} resizable={false} height={640} />
|
|
@@ -312,6 +340,33 @@ const columns = [
|
|
|
312
340
|
];
|
|
313
341
|
```
|
|
314
342
|
|
|
343
|
+
## Tree data
|
|
344
|
+
|
|
345
|
+
Pass `getChildren` to render hierarchical rows — `rows` become the roots, and each
|
|
346
|
+
node gets an indented first column with an expand chevron when it has children:
|
|
347
|
+
|
|
348
|
+
```svelte
|
|
349
|
+
<Grid {rows} {columns} height={520} getChildren={(r) => r.children} />
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
In tree mode the grid renders the tree directly (filter/sort/group/paginate are
|
|
353
|
+
not applied to it). Nodes are keyboard-accessible: **→** expands a collapsed node,
|
|
354
|
+
**←** collapses an expanded one, and rows expose `aria-level` / `aria-expanded`.
|
|
355
|
+
|
|
356
|
+
## Master-detail
|
|
357
|
+
|
|
358
|
+
Pass a `detail` snippet to render an expandable panel under each row — the grid
|
|
359
|
+
adds a leading expand toggle and virtualizes the expanded heights (`detailHeight`,
|
|
360
|
+
default 160). In-memory mode:
|
|
361
|
+
|
|
362
|
+
```svelte
|
|
363
|
+
<Grid {rows} {columns} height={640} detailHeight={120} detail={rowDetail} />
|
|
364
|
+
|
|
365
|
+
{#snippet rowDetail({ row })}
|
|
366
|
+
<div class="detail">…anything about {row.name}…</div>
|
|
367
|
+
{/snippet}
|
|
368
|
+
```
|
|
369
|
+
|
|
315
370
|
## Per-row styling
|
|
316
371
|
|
|
317
372
|
Return a class from `rowClass` to style rows by their data (e.g. red/green book
|
|
@@ -326,15 +381,35 @@ levels). Rows live inside the grid, so target the class with `:global(...)`:
|
|
|
326
381
|
</style>
|
|
327
382
|
```
|
|
328
383
|
|
|
384
|
+
For per-column styling, a column's `cellClass` (static or `(value, row)`
|
|
385
|
+
conditional) and `headerClass` add classes to that column's cells/header:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
{ type: 'number', key: 'pnl', header: 'P&L',
|
|
389
|
+
cellClass: (v) => (Number(v) < 0 ? 'loss' : 'gain'), headerClass: 'num-head' }
|
|
390
|
+
```
|
|
391
|
+
|
|
329
392
|
`onRowClick(row, event)` fires when a row is activated by click or <kbd>Enter</kbd>
|
|
330
393
|
on the focused cell — wire it to open a detail view or navigate.
|
|
331
394
|
|
|
395
|
+
Pass `rowMenu(row)` to add a **right-click menu** of row actions; each item runs
|
|
396
|
+
its `onSelect` and the menu closes (also on outside-click or <kbd>Esc</kbd>). It
|
|
397
|
+
is keyboard-accessible: the <kbd>ContextMenu</kbd> key (or
|
|
398
|
+
<kbd>Shift</kbd>+<kbd>F10</kbd>) opens it at the focused cell.
|
|
399
|
+
|
|
400
|
+
```svelte
|
|
401
|
+
<Grid {rows} {columns} height={640}
|
|
402
|
+
rowMenu={(r) => [{ label: 'Delete', onSelect: () => remove(r.id) }]} />
|
|
403
|
+
```
|
|
404
|
+
|
|
332
405
|
## Inline editing
|
|
333
406
|
|
|
334
407
|
Mark a column `editable: true`. Double-click a cell (or press <kbd>Enter</kbd> on
|
|
335
408
|
the focused cell) to edit; <kbd>Enter</kbd>/blur commits, <kbd>Esc</kbd> cancels.
|
|
336
|
-
|
|
337
|
-
|
|
409
|
+
Or just start typing — a printable key on a focused editable cell opens the
|
|
410
|
+
editor seeded with that character (Excel-style type-to-edit). The grid is
|
|
411
|
+
controlled, so it reports the change via `onCellEdit` — update your own row data
|
|
412
|
+
there:
|
|
338
413
|
|
|
339
414
|
```svelte
|
|
340
415
|
<Grid
|
|
@@ -354,17 +429,27 @@ applies to paste too):
|
|
|
354
429
|
{ type: 'number', key: 'qty', header: 'Qty', editable: true, validate: (v) => v >= 0 }
|
|
355
430
|
```
|
|
356
431
|
|
|
432
|
+
Give an editable column `options` to edit it via a dropdown instead of a text
|
|
433
|
+
input (enum/status columns):
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
{ type: 'text', key: 'status', header: 'Status', editable: true,
|
|
437
|
+
options: ['New', 'Active', 'Closed'] }
|
|
438
|
+
```
|
|
439
|
+
|
|
357
440
|
## Pinned columns
|
|
358
441
|
|
|
359
|
-
Set `pinned: true` on a column to keep it visible while the rest
|
|
360
|
-
horizontally
|
|
361
|
-
|
|
442
|
+
Set `pinned: true` (or `'left'`) on a column to keep it visible while the rest
|
|
443
|
+
scroll horizontally; `pinned: 'right'` sticks it to the right edge (e.g. an
|
|
444
|
+
actions or total column). Opt-in: with no pinned columns the grid stays
|
|
445
|
+
fit-to-width (no horizontal scroll).
|
|
362
446
|
|
|
363
447
|
```ts
|
|
364
448
|
const columns = [
|
|
365
449
|
{ type: 'text', key: 'symbol', header: 'Symbol', width: 132, pinned: true },
|
|
366
450
|
{ type: 'price', key: 'price', header: 'Price', width: 88, pinned: true },
|
|
367
|
-
// …wider columns scroll under the pinned ones
|
|
451
|
+
// …wider columns scroll under the pinned ones…
|
|
452
|
+
{ type: 'number', key: 'pnl', header: 'P&L', width: 96, pinned: 'right' },
|
|
368
453
|
];
|
|
369
454
|
```
|
|
370
455
|
|
package/dist/grid/Cell.svelte
CHANGED
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
selected = false,
|
|
14
14
|
focused = false,
|
|
15
15
|
pinned = false,
|
|
16
|
-
|
|
16
|
+
pinSide = 'left',
|
|
17
|
+
pinOffset = 0,
|
|
17
18
|
width,
|
|
18
19
|
alt = false,
|
|
19
20
|
editing = false,
|
|
21
|
+
seed = null,
|
|
20
22
|
colIndex,
|
|
21
23
|
cellId,
|
|
22
24
|
cellSnippet,
|
|
25
|
+
tree,
|
|
26
|
+
dragHandle,
|
|
23
27
|
onCellDown,
|
|
24
28
|
onCellEnter,
|
|
25
29
|
onCellClick,
|
|
@@ -34,14 +38,22 @@
|
|
|
34
38
|
selected?: boolean;
|
|
35
39
|
focused?: boolean;
|
|
36
40
|
pinned?: boolean;
|
|
37
|
-
|
|
41
|
+
pinSide?: 'left' | 'right';
|
|
42
|
+
pinOffset?: number;
|
|
38
43
|
/** Fixed pixel width (pinned/horizontal-scroll mode). */
|
|
39
44
|
width?: number;
|
|
40
45
|
alt?: boolean;
|
|
41
46
|
editing?: boolean;
|
|
47
|
+
/** Type-to-edit seed: when set, the editor opens pre-filled with this string
|
|
48
|
+
(the character that triggered the edit) instead of the current value. */
|
|
49
|
+
seed?: string | null;
|
|
42
50
|
colIndex?: number;
|
|
43
51
|
cellId?: string;
|
|
44
52
|
cellSnippet?: Snippet<[{ row: GridRow; column: ColumnDef; value: unknown }]>;
|
|
53
|
+
/** Tree-data gutter for the first column: indent + expand chevron. */
|
|
54
|
+
tree?: { depth: number; hasChildren: boolean; expanded: boolean; onToggle: () => void };
|
|
55
|
+
/** Drag-to-reorder handle for the first column (HTML5 draggable grip). */
|
|
56
|
+
dragHandle?: { onStart: () => void; onEnd: () => void };
|
|
45
57
|
onCellDown?: (r: number, c: number, e: PointerEvent) => void;
|
|
46
58
|
onCellEnter?: (r: number, c: number, e: PointerEvent) => void;
|
|
47
59
|
onCellClick?: (r: number, c: number, e: MouseEvent) => void;
|
|
@@ -53,7 +65,13 @@
|
|
|
53
65
|
let cancelled = false;
|
|
54
66
|
function focusSelect(node: HTMLInputElement) {
|
|
55
67
|
node.focus();
|
|
56
|
-
|
|
68
|
+
// Type-to-edit: keep the seeded character and put the caret after it;
|
|
69
|
+
// otherwise select the whole value so the next keystroke replaces it.
|
|
70
|
+
if (seed != null) node.setSelectionRange(node.value.length, node.value.length);
|
|
71
|
+
else node.select();
|
|
72
|
+
}
|
|
73
|
+
function focusEl(node: HTMLElement) {
|
|
74
|
+
node.focus();
|
|
57
75
|
}
|
|
58
76
|
function onEditKey(e: KeyboardEvent) {
|
|
59
77
|
e.stopPropagation(); // keep arrows/Enter in the input, not the grid
|
|
@@ -78,12 +96,22 @@
|
|
|
78
96
|
// though the key is only known at runtime.
|
|
79
97
|
const value = $derived(row[col.key]);
|
|
80
98
|
const kind = $derived(col.type === 'text' ? 'text' : col.type === 'sparkline' ? 'spark' : 'num');
|
|
99
|
+
// Optional per-column cell class (static string or value/row function).
|
|
100
|
+
const extraClass = $derived(
|
|
101
|
+
typeof col.cellClass === 'function' ? (col.cellClass(value, row) ?? '') : (col.cellClass ?? ''),
|
|
102
|
+
);
|
|
103
|
+
// Native tooltip of the full value (opt-in via column `tooltip`).
|
|
104
|
+
const tip = $derived(
|
|
105
|
+
col.tooltip && col.type !== 'sparkline' && col.type !== 'custom'
|
|
106
|
+
? formatCell(col, value, row)
|
|
107
|
+
: undefined,
|
|
108
|
+
);
|
|
81
109
|
|
|
82
110
|
function cellStyle(): string {
|
|
83
111
|
let s = width != null ? `flex:0 0 ${width}px;width:${width}px;` : colStyle(col);
|
|
84
112
|
if (col.type === 'heatmap') s += `background:${heatColor(Number(value), col.min, col.max)};`;
|
|
85
113
|
if (pinned) {
|
|
86
|
-
s += `position:sticky
|
|
114
|
+
s += `position:sticky;${pinSide}:${pinOffset}px;z-index:1;`;
|
|
87
115
|
// Pinned cells must be opaque to cover scrolled content. Heatmap already
|
|
88
116
|
// set a background; otherwise match the (alternating) row colour.
|
|
89
117
|
if (col.type !== 'heatmap') s += `background:var(${alt ? '--bo-row-a' : '--bo-row-b'});`;
|
|
@@ -95,7 +123,7 @@
|
|
|
95
123
|
<!-- Keyboard interaction is handled at the grid level (arrow nav via aria-activedescendant + Enter); this cell click is a pointer affordance. -->
|
|
96
124
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
97
125
|
<span
|
|
98
|
-
class="c {kind}"
|
|
126
|
+
class="c {kind} {extraClass}"
|
|
99
127
|
class:dim={col.type === 'volume'}
|
|
100
128
|
class:pos={col.type === 'percent' && Number(value) >= 0}
|
|
101
129
|
class:neg={col.type === 'percent' && Number(value) < 0}
|
|
@@ -105,6 +133,7 @@
|
|
|
105
133
|
role="gridcell"
|
|
106
134
|
tabindex="-1"
|
|
107
135
|
id={cellId}
|
|
136
|
+
title={tip}
|
|
108
137
|
aria-colindex={colIndex}
|
|
109
138
|
aria-selected={selected}
|
|
110
139
|
onpointerdown={(e) => onCellDown?.(r, c, e)}
|
|
@@ -112,10 +141,57 @@
|
|
|
112
141
|
onclick={(e) => onCellClick?.(r, c, e)}
|
|
113
142
|
ondblclick={() => onCellDblClick?.(r, c)}
|
|
114
143
|
>
|
|
115
|
-
{#if
|
|
116
|
-
<
|
|
144
|
+
{#if dragHandle}
|
|
145
|
+
<span
|
|
146
|
+
class="drag-handle"
|
|
147
|
+
role="button"
|
|
148
|
+
tabindex="-1"
|
|
149
|
+
aria-label="Drag to reorder row"
|
|
150
|
+
draggable="true"
|
|
151
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
152
|
+
ondragstart={() => dragHandle.onStart()}
|
|
153
|
+
ondragend={() => dragHandle.onEnd()}
|
|
154
|
+
>⠿</span>
|
|
155
|
+
{/if}
|
|
156
|
+
{#if tree}
|
|
157
|
+
<span class="tree-gutter" style="padding-left:{tree.depth * 16}px">
|
|
158
|
+
{#if tree.hasChildren}
|
|
159
|
+
<button
|
|
160
|
+
class="tree-toggle"
|
|
161
|
+
type="button"
|
|
162
|
+
aria-expanded={tree.expanded}
|
|
163
|
+
aria-label="Toggle children"
|
|
164
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
165
|
+
onclick={(e) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
tree.onToggle();
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{tree.expanded ? '▾' : '▸'}
|
|
171
|
+
</button>
|
|
172
|
+
{:else}
|
|
173
|
+
<span class="tree-leaf"></span>
|
|
174
|
+
{/if}
|
|
175
|
+
</span>
|
|
176
|
+
{/if}
|
|
177
|
+
{#if editing && col.options && col.options.length > 0}
|
|
178
|
+
<select
|
|
117
179
|
class="bo-edit"
|
|
118
180
|
value={String(value ?? '')}
|
|
181
|
+
use:focusEl
|
|
182
|
+
onkeydown={onEditKey}
|
|
183
|
+
onblur={onEditBlur}
|
|
184
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
185
|
+
onclick={(e) => e.stopPropagation()}
|
|
186
|
+
>
|
|
187
|
+
{#each col.options as opt (opt)}
|
|
188
|
+
<option value={opt}>{opt}</option>
|
|
189
|
+
{/each}
|
|
190
|
+
</select>
|
|
191
|
+
{:else if editing}
|
|
192
|
+
<input
|
|
193
|
+
class="bo-edit"
|
|
194
|
+
value={seed ?? String(value ?? '')}
|
|
119
195
|
use:focusSelect
|
|
120
196
|
onkeydown={onEditKey}
|
|
121
197
|
onblur={onEditBlur}
|
|
@@ -128,13 +204,13 @@
|
|
|
128
204
|
{:else if col.type === 'sparkline'}
|
|
129
205
|
<Sparkline candles={candlesOf(row, col.sparkKey)} />
|
|
130
206
|
{:else if col.type === 'text'}
|
|
131
|
-
<strong>{formatCell(col, value)}</strong>{#if col.sub}<em>{row[col.sub]}</em>{/if}
|
|
207
|
+
<strong>{formatCell(col, value, row)}</strong>{#if col.sub}<em>{row[col.sub]}</em>{/if}
|
|
132
208
|
{:else if col.flash}
|
|
133
209
|
{#key row.flashSeq}
|
|
134
|
-
<span class="flash {row.flashDir}">{formatCell(col, value)}</span>
|
|
210
|
+
<span class="flash {row.flashDir}">{formatCell(col, value, row)}</span>
|
|
135
211
|
{/key}
|
|
136
212
|
{:else}
|
|
137
|
-
{formatCell(col, value)}
|
|
213
|
+
{formatCell(col, value, row)}
|
|
138
214
|
{/if}
|
|
139
215
|
</span>
|
|
140
216
|
|
|
@@ -171,6 +247,44 @@
|
|
|
171
247
|
.spark {
|
|
172
248
|
overflow: visible;
|
|
173
249
|
}
|
|
250
|
+
/* Tree-data gutter: indent + expand chevron, before the cell content. */
|
|
251
|
+
.tree-gutter {
|
|
252
|
+
display: inline-flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
flex: none;
|
|
255
|
+
}
|
|
256
|
+
.tree-toggle {
|
|
257
|
+
width: 18px;
|
|
258
|
+
height: 18px;
|
|
259
|
+
padding: 0;
|
|
260
|
+
font-size: 10px;
|
|
261
|
+
line-height: 1;
|
|
262
|
+
color: var(--bo-text-dim);
|
|
263
|
+
background: transparent;
|
|
264
|
+
border: 0;
|
|
265
|
+
border-radius: 4px;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
.tree-toggle:hover {
|
|
269
|
+
color: var(--bo-text);
|
|
270
|
+
background: var(--bo-row-hover);
|
|
271
|
+
}
|
|
272
|
+
.tree-leaf {
|
|
273
|
+
display: inline-block;
|
|
274
|
+
width: 18px;
|
|
275
|
+
}
|
|
276
|
+
.drag-handle {
|
|
277
|
+
flex: none;
|
|
278
|
+
margin-right: 4px;
|
|
279
|
+
font-size: 12px;
|
|
280
|
+
line-height: 1;
|
|
281
|
+
color: var(--bo-text-dim);
|
|
282
|
+
cursor: grab;
|
|
283
|
+
user-select: none;
|
|
284
|
+
}
|
|
285
|
+
.drag-handle:active {
|
|
286
|
+
cursor: grabbing;
|
|
287
|
+
}
|
|
174
288
|
.bo-edit {
|
|
175
289
|
width: 100%;
|
|
176
290
|
height: 100%;
|
|
@@ -8,11 +8,15 @@ type $$ComponentProps = {
|
|
|
8
8
|
selected?: boolean;
|
|
9
9
|
focused?: boolean;
|
|
10
10
|
pinned?: boolean;
|
|
11
|
-
|
|
11
|
+
pinSide?: 'left' | 'right';
|
|
12
|
+
pinOffset?: number;
|
|
12
13
|
/** Fixed pixel width (pinned/horizontal-scroll mode). */
|
|
13
14
|
width?: number;
|
|
14
15
|
alt?: boolean;
|
|
15
16
|
editing?: boolean;
|
|
17
|
+
/** Type-to-edit seed: when set, the editor opens pre-filled with this string
|
|
18
|
+
(the character that triggered the edit) instead of the current value. */
|
|
19
|
+
seed?: string | null;
|
|
16
20
|
colIndex?: number;
|
|
17
21
|
cellId?: string;
|
|
18
22
|
cellSnippet?: Snippet<[{
|
|
@@ -20,6 +24,18 @@ type $$ComponentProps = {
|
|
|
20
24
|
column: ColumnDef;
|
|
21
25
|
value: unknown;
|
|
22
26
|
}]>;
|
|
27
|
+
/** Tree-data gutter for the first column: indent + expand chevron. */
|
|
28
|
+
tree?: {
|
|
29
|
+
depth: number;
|
|
30
|
+
hasChildren: boolean;
|
|
31
|
+
expanded: boolean;
|
|
32
|
+
onToggle: () => void;
|
|
33
|
+
};
|
|
34
|
+
/** Drag-to-reorder handle for the first column (HTML5 draggable grip). */
|
|
35
|
+
dragHandle?: {
|
|
36
|
+
onStart: () => void;
|
|
37
|
+
onEnd: () => void;
|
|
38
|
+
};
|
|
23
39
|
onCellDown?: (r: number, c: number, e: PointerEvent) => void;
|
|
24
40
|
onCellEnter?: (r: number, c: number, e: PointerEvent) => void;
|
|
25
41
|
onCellClick?: (r: number, c: number, e: MouseEvent) => void;
|