bo-grid 0.21.0 → 1.0.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 CHANGED
@@ -1,20 +1,20 @@
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 ~20 KB.
5
- A free alternative to the heavyweight grids that paywall these features.
4
+ realtime cell updates, and virtual scrolling, with a core that gzips to ~32 KB
5
+ (Svelte external; unused exports tree-shake). A free alternative to the heavyweight
6
+ grids that paywall these features.
6
7
 
7
8
  **[Live demo](https://bonguynvan.github.io/bo-grid/)** ·
8
9
  **[API reference](https://bonguynvan.github.io/bo-grid/api.html)** ·
9
10
  **[Benchmarks](./BENCHMARKS.md)** ·
10
11
  **[Roadmap](./ROADMAP.md)**
11
12
 
12
- The demo is a small gallery of nine grid types — a realtime **Trading desk**, a
13
- grouped **Portfolio** with subtotals and pivot, a general-purpose editable
14
- **Spreadsheet**, a live **Order book** depth ladder, a **Correlation** heatmap
15
- matrix, a **Leaderboard** with rank medals and score bars, a **Tree** file
16
- explorer, a drag-to-reorder **Tasks** list, and a **1M-row** trade tape windowed
17
- from a synthetic source — switch between them with the tabs.
13
+ The demo is a gallery of grid types — a realtime **Trading desk**, a grouped
14
+ **Portfolio** with subtotals and pivot, an editable **Spreadsheet**, a live
15
+ **Order book**, a **Correlation** heatmap, a **Dashboard** with in-cell charts, a
16
+ **Wide** 60-column grid, a server-backed **Lazy tree**, and more all on one
17
+ page, each grid lazy-mounting as you scroll (jump between them from the side rail).
18
18
 
19
19
  > **Status: actively developed.** Working: config-driven columns, virtual scroll,
20
20
  > sort (single / multi / controlled), filtering (global, per-column row, header
@@ -37,13 +37,12 @@ from a synthetic source — switch between them with the tabs.
37
37
  | Price | $$$ / dev / year | Free (MIT) |
38
38
  | Sparklines | paid tier | built in |
39
39
  | Realtime cell updates | DIY / complex | built-in primitive |
40
- | Bundle | hundreds of KB | **~20 KB gzip** ([benchmarks](./BENCHMARKS.md)) |
40
+ | Bundle | hundreds of KB | **~32 KB gzip core** ([benchmarks](./BENCHMARKS.md)) |
41
41
  | Svelte | wrapper | native Svelte 5 |
42
42
 
43
- bo-grid ships most of AG Grid's **paid (Enterprise)** features — grouping, pivot,
44
- tree data, master-detail, range selection, Excel export, sparklines — for free.
45
- See the honest **[bo-grid vs AG Grid](./docs/vs-ag-grid.md)** comparison (including
46
- what it doesn't do) to decide.
43
+ bo-grid ships most of the features other grids put behind a **paid (Enterprise)**
44
+ tier — grouping, pivot, tree data, master-detail, range selection, Excel export,
45
+ sparklines for free, and runs in any framework via a [custom element](./docs/frameworks.md).
47
46
 
48
47
  ## Install
49
48
 
@@ -56,7 +55,8 @@ Works with **SvelteKit / SSR** out of the box — `<Grid>` server-renders to HTM
56
55
  without touching `window`/`document`/`localStorage` (a CI gate, `pnpm ssr`,
57
56
  proves it). The package is `sideEffects: false`, so unused exports tree-shake
58
57
  away. See the **[SvelteKit guide](./docs/sveltekit.md)** for `load`-function data,
59
- realtime feeds, and layout persistence.
58
+ server-side / lazy loading, realtime feeds, import helpers, charts, printing, and
59
+ layout persistence.
60
60
 
61
61
  ## Usage
62
62
 
@@ -100,21 +100,26 @@ keep frames smooth.
100
100
 
101
101
  ### React, Vue, Angular & vanilla
102
102
 
103
- bo-grid also ships a framework-agnostic **custom element**. Import it and drive
104
- the whole API through a `config` property:
103
+ bo-grid also ships a framework-agnostic **custom element**, fully typed. Import it
104
+ and drive the whole API through a `config` property:
105
105
 
106
- ```js
107
- import 'bo-grid/element'; // registers <bo-grid>, injects styles
106
+ ```ts
107
+ import { createBoGrid } from 'bo-grid/element'; // registers <bo-grid>, injects styles
108
+ import type { BoGridConfig } from 'bo-grid/element';
108
109
 
109
- const el = document.querySelector('bo-grid');
110
- el.config = { columns, rows, theme: 'dark', height: 520 };
110
+ const config: BoGridConfig = { columns, rows, theme: 'dark', height: 520 };
111
+ document.body.append(createBoGrid(config)); // or set `el.config` on an existing element
111
112
  ```
112
113
 
113
- It works in React, Vue, Angular and plain HTML — see
114
- **[docs/frameworks.md](./docs/frameworks.md)** for per-framework recipes. (Custom
115
- `cell`/`detail` snippets are Svelte-only; use built-in types, `format`, or computed
116
- `value` from other frameworks. Native Svelte users should import `Grid` directly —
117
- smaller, and snippets work.)
114
+ `bo-grid/element` exports `BoGridConfig` (every `<Grid>` prop), a typed
115
+ `BoGridElement`, and the `createBoGrid` helper. `config` is safe to set **after**
116
+ the element attaches (the React `ref` + `useEffect` pattern won't crash). It works
117
+ in React, Vue, Angular and plain HTML see
118
+ **[docs/frameworks.md](./docs/frameworks.md)** for per-framework recipes and
119
+ **[examples/](./examples/)** for runnable, build-free starters. (Svelte `cell`/
120
+ `detail` snippets are Svelte-only; from other frameworks use a column's
121
+ `render(ctx)` hook, built-in types, or `format`. Native Svelte users should import
122
+ `Grid` directly — smaller, and snippets work.)
118
123
 
119
124
  ## Column types
120
125
 
@@ -159,6 +164,52 @@ buttons, links. The snippet receives `{ row, column, value }`:
159
164
  <Grid {rows} {columns} {cell} height={640} />
160
165
  ```
161
166
 
167
+ **From plain JS (React/Vue/vanilla, `<bo-grid>`):** snippets are Svelte-only, so
168
+ give a column a **`render(ctx)`** function instead. Return an `HTMLElement`/`Node`
169
+ (appended as-is — safe) or an HTML **string** (inserted as innerHTML — sanitize
170
+ any untrusted data yourself; prefer a Node for that). `ctx` is `{ value, row,
171
+ column }`. Display only — sort, filter, tooltip, copy and export still use the
172
+ value / `format`:
173
+
174
+ ```js
175
+ { type: 'custom', key: 'trend', header: 'Trend', render: ({ row }) => {
176
+ const el = document.createElement('span');
177
+ el.textContent = row.changePct > 0 ? '▲' : '▼';
178
+ el.style.color = row.changePct > 0 ? 'var(--bo-grid-up)' : 'var(--bo-grid-down)';
179
+ return el;
180
+ } }
181
+ ```
182
+
183
+ ### Tooltips & truncation
184
+
185
+ Long cell values truncate with an **ellipsis** by default. Set `tooltip` on a
186
+ column to reveal the full text in a **styled floating tooltip** on hover (themed,
187
+ instant — not the native `title`). `tooltip: true` shows the formatted value;
188
+ pass a function for custom text built from the whole row (works for any type,
189
+ including `custom`):
190
+
191
+ ```ts
192
+ const columns: ColumnDef[] = [
193
+ { type: 'text', key: 'note', header: 'Notes', width: 220, tooltip: true },
194
+ { type: 'badge', key: 'status', header: 'Status',
195
+ tooltip: (value, row) => `${value} · ${row.role}` }, // custom text
196
+ { type: 'text', key: 'bio', header: 'Bio', flex: 1, wrap: true }, // wrap, no ellipsis
197
+ ];
198
+ ```
199
+
200
+ Set `wrap: true` to let a column wrap onto multiple lines instead of truncating
201
+ (pair with a taller `rowHeight`).
202
+
203
+ **Header tooltips:** `headerTooltip` shows the same styled bubble on the column
204
+ header (describe the metric, units, caveats); add `headerInfo: true` for a small
205
+ ⓘ cue:
206
+
207
+ ```ts
208
+ { type: 'progress', key: 'workload', header: 'Workload', min: 0, max: 100,
209
+ headerTooltip: 'Share of capacity allocated this sprint (0–100%).',
210
+ headerInfo: true }
211
+ ```
212
+
162
213
  ## Conditional formatting
163
214
 
164
215
  Paint analytics cues straight into numeric cells — no custom snippet needed.
@@ -260,13 +311,28 @@ a function for variable per-row heights (in-memory mode):
260
311
  Variable heights use a prefix-sum + binary-search virtualizer, so scrolling stays
261
312
  O(log n). Source mode is uniform-only (unloaded row heights aren't known).
262
313
 
314
+ ### Sizing the grid
315
+
316
+ `height` as a **number** is the scroll viewport's pixel height (the element is
317
+ that plus header/pager/footer chrome). Pass a **CSS string** to size the whole
318
+ element instead and let the viewport auto-fit the space left over — ideal for
319
+ filling a flex/grid cell:
320
+
321
+ ```svelte
322
+ <Grid {rows} {columns} height={640} /> <!-- 640px viewport -->
323
+ <Grid {rows} {columns} height="100%" /> <!-- fill a sized parent -->
324
+ <Grid {rows} {columns} height="80vh" />
325
+ ```
326
+
263
327
  ## Pagination
264
328
 
265
329
  Prefer pages over one long scroll? Set `pageSize` (> 0) for a paged view with a
266
- first/prev/next/last pager; rows still virtualize within each page. In-memory mode.
330
+ first/prev/next/last pager; rows still virtualize within each page. Add
331
+ `pageSizeOptions` for a **rows-per-page dropdown** in the pager (observe changes
332
+ via `onPageSizeChange`). In-memory mode.
267
333
 
268
334
  ```svelte
269
- <Grid {rows} {columns} height={640} pageSize={25} />
335
+ <Grid {rows} {columns} height={640} pageSize={25} pageSizeOptions={[25, 50, 100]} />
270
336
  ```
271
337
 
272
338
  ## Sort & filter
@@ -329,6 +395,22 @@ value(s) across the extended range (editable columns; multi-cell selections tile
329
395
  Edits, paste and fill are **undoable** with <kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd> (redo
330
396
  with <kbd>Ctrl/⌘</kbd>+<kbd>Y</kbd>). A paste or fill undoes as a single step.
331
397
 
398
+ For a read-only / display grid, set `cellSelection={false}` to drop the blue
399
+ range-selection highlight (and fill handle) entirely — clicks pass straight
400
+ through to `onRowClick` / `onCellClick`:
401
+
402
+ ```svelte
403
+ <Grid {rows} {columns} height={640} cellSelection={false} onRowClick={open} />
404
+ ```
405
+
406
+ For a **list-detail / master-detail** layout, drive `selectedRowId` (keyed by
407
+ `getRowId`) to highlight the active row — independent of the checkbox selection:
408
+
409
+ ```svelte
410
+ <Grid {rows} {columns} height={640}
411
+ selectedRowId={active?.id} onRowClick={(r) => (active = r)} />
412
+ ```
413
+
332
414
  When more than one cell is selected, a footer bar shows live **Sum / Avg / Count /
333
415
  Min / Max** over the numeric cells in the range — and it keeps updating as a
334
416
  realtime feed ticks. Choose which stats to show:
@@ -710,17 +792,38 @@ const columns = [
710
792
  ];
711
793
  ```
712
794
 
713
- ## Export
795
+ ## Export & import
714
796
 
715
- CSV export is dependency-free:
797
+ CSV export and import — are dependency-free:
716
798
 
717
799
  ```ts
718
- import { exportCSV, toCSV } from 'bo-grid';
800
+ import { exportCSV, toCSV, parseCSV } from 'bo-grid';
719
801
 
720
802
  exportCSV('tickers.csv', rows, columns); // triggers a download
721
803
  const text = toCSV(rows, columns, { formatted: true }); // or get the string
804
+ const rows = parseCSV(text, columns); // …and back to rows (round-trip)
805
+ ```
806
+
807
+ `parseCSV` is RFC4180-aware (quoted fields, embedded commas/quotes/newlines), maps
808
+ headers to columns, coerces numeric/`date` columns, and stamps `id` + flash fields
809
+ so the result drops straight into `<Grid rows={…}>`. There's also `parseTSV` (tab-
810
+ separated — what Ctrl/⌘+C copies), and for JSON/API data, `rowsFromObjects(objects)`
811
+ / `parseJSON(text)`:
812
+
813
+ ```ts
814
+ import { rowsFromObjects } from 'bo-grid';
815
+ const rows = rowsFromObjects(await (await fetch('/api/rows')).json());
816
+ ```
817
+
818
+ Not sure what you'll get? **`parseRows(text, columns?)`** auto-detects JSON / TSV /
819
+ CSV — perfect for a paste handler:
820
+
821
+ ```svelte
822
+ <div onpaste={(e) => (rows = parseRows(e.clipboardData.getData('text'), columns))}>…</div>
722
823
  ```
723
824
 
825
+ See the **CSV import** demo (CSV / TSV / JSON / Auto-detect).
826
+
724
827
  Excel export loads SheetJS via **dynamic import**, so it lands in its own lazy
725
828
  chunk and never bloats your core bundle. `xlsx` is an **optional peer dependency**
726
829
  — install it only if you use this:
@@ -734,11 +837,23 @@ Sparkline columns are skipped; numeric columns export as raw numbers so
734
837
  spreadsheets can compute on them (pass `{ formatted: true }` for display strings).
735
838
  Ctrl/⌘+C still copies the current selection as TSV.
736
839
 
840
+ **Printing.** The grid virtualizes, so printing it directly drops off-screen rows.
841
+ `printTable(rows, columns, { title })` opens a print window with **all** rows as a
842
+ clean table (Save-as-PDF from the dialog); `toHTMLTable(rows, columns)` returns
843
+ that table as an HTML string to embed. See the **Print** demo.
844
+
845
+ ```ts
846
+ import { printTable } from 'bo-grid';
847
+ printTable(rows, columns, { title: 'Sales report' });
848
+ ```
849
+
737
850
  ## Also exported
738
851
 
739
852
  `Sparkline` component · `drawCandles` / `setupHiDpiCanvas` (draw on your own
740
853
  canvas) · `fmtPrice` / `fmtPercent` / `fmtVolume` / `fmtDate` · `heatColor` ·
741
- `Selection` · `aggregate` · `toCSV` / `exportCSV` / `exportXLSX` / `rowsToMatrix`.
854
+ `Selection` · `aggregate` · `toCSV` / `exportCSV` / `exportXLSX` / `rowsToMatrix` ·
855
+ `parseCSV` / `parseCSVMatrix` / `parseTSV` / `parseJSON` / `parseRows` /
856
+ `rowsFromObjects` · `toHTMLTable` / `printTable`.
742
857
 
743
858
  ## Pivot tables
744
859
 
@@ -1,4 +1,4 @@
1
- import { p as ce, a as xe, v as me, w as _e, n as x, x as _, s as p, y as L, t as N, z as he, j as ke, f as C, h as w, i as u, m as ge, c as h, A as D, e as F, g as t, B as U, k as S, d as ye, l as f, C as we, o as k, D as Ae, u as Oe, q as Se, r as d, b as A } from "./bo-grid.element-DPnHUXMa.js";
1
+ import { p as ce, a as xe, v as me, w as _e, n as x, x as _, s as p, y as L, t as N, z as he, j as ke, f as C, h as w, i as u, m as ge, c as h, A as D, e as F, g as t, B as U, k as S, d as ye, l as f, C as we, o as k, D as Ae, u as Oe, q as Se, r as d, b as A } from "./bo-grid.element-BZGnfKB_.js";
2
2
  var G = k("<option> </option>"), Ne = k('<input class="bo-fm-in svelte-f7i4av" type="number" placeholder="and" aria-label="Upper value"/>'), Ce = k('<select class="bo-fm-op svelte-f7i4av" aria-label="Operator"></select> <input class="bo-fm-in svelte-f7i4av" type="number" placeholder="value" aria-label="Value"/> <!>', 1), Ee = k('<input class="bo-fm-in svelte-f7i4av" type="date" aria-label="End date"/>'), qe = k('<select class="bo-fm-op svelte-f7i4av" aria-label="Operator"></select> <input class="bo-fm-in svelte-f7i4av" type="date" aria-label="Date"/> <!>', 1), Be = k('<label class="bo-fm-opt svelte-f7i4av"><input type="checkbox"/> <span class="svelte-f7i4av"> </span></label>'), De = k('<input class="bo-fm-in svelte-f7i4av" type="search" placeholder="search…" aria-label="Search values"/> <div class="bo-fm-setbar svelte-f7i4av"><button type="button" class="bo-fm-link svelte-f7i4av">All</button> <button type="button" class="bo-fm-link svelte-f7i4av">None</button></div> <div class="bo-fm-list svelte-f7i4av"></div>', 1), Fe = k('<select class="bo-fm-op svelte-f7i4av" aria-label="Operator"></select> <input class="bo-fm-in svelte-f7i4av" type="text" placeholder="filter…" aria-label="Value"/>', 1), ze = k('<div class="bo-filtermenu svelte-f7i4av" role="dialog" tabindex="-1"><div class="bo-fm-head svelte-f7i4av"> </div> <!> <div class="bo-fm-actions svelte-f7i4av"><button class="bo-fm-btn svelte-f7i4av" type="button">Clear</button> <button class="bo-fm-btn bo-fm-apply svelte-f7i4av" type="button">Apply</button></div></div>');
3
3
  const Pe = {
4
4
  hash: "svelte-f7i4av",
@@ -1,4 +1,4 @@
1
- import { p as z, a as S, c as d, s as r, r as l, b as f, e as j, t as g, g as s, d as L, f as P, h as i, i as w, j as T, k as q, l as A, m as E, n as B, u as D, o as k, q as F } from "./bo-grid.element-DPnHUXMa.js";
1
+ import { p as z, a as S, c as d, s as r, r as l, b as f, e as j, t as g, g as s, d as L, f as P, h as i, i as w, j as T, k as q, l as A, m as E, n as B, u as D, o as k, q as F } from "./bo-grid.element-BZGnfKB_.js";
2
2
  var G = k('<label class="bo-tp-opt svelte-7xdno"><input type="checkbox"/> <span class="svelte-7xdno"> </span></label>'), H = k('<div class="bo-toolpanel svelte-7xdno" role="dialog" tabindex="-1" aria-label="Columns"><div class="bo-tp-head svelte-7xdno"><span>Columns</span> <button type="button" class="bo-tp-link svelte-7xdno">Show all</button></div> <input class="bo-tp-search svelte-7xdno" type="search" placeholder="search…" aria-label="Search columns"/> <div class="bo-tp-list svelte-7xdno"></div></div>');
3
3
  const I = {
4
4
  hash: "svelte-7xdno",