bo-grid 0.21.0 → 0.25.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 ~31 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 switch between
17
+ them with the tabs.
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 | **~31 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
 
@@ -111,7 +111,8 @@ el.config = { columns, rows, theme: 'dark', height: 520 };
111
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
114
+ **[docs/frameworks.md](./docs/frameworks.md)** for per-framework recipes and
115
+ **[examples/](./examples/)** for runnable, build-free starters. (Custom
115
116
  `cell`/`detail` snippets are Svelte-only; use built-in types, `format`, or computed
116
117
  `value` from other frameworks. Native Svelte users should import `Grid` directly —
117
118
  smaller, and snippets work.)
@@ -710,17 +711,38 @@ const columns = [
710
711
  ];
711
712
  ```
712
713
 
713
- ## Export
714
+ ## Export & import
714
715
 
715
- CSV export is dependency-free:
716
+ CSV export and import — are dependency-free:
716
717
 
717
718
  ```ts
718
- import { exportCSV, toCSV } from 'bo-grid';
719
+ import { exportCSV, toCSV, parseCSV } from 'bo-grid';
719
720
 
720
721
  exportCSV('tickers.csv', rows, columns); // triggers a download
721
722
  const text = toCSV(rows, columns, { formatted: true }); // or get the string
723
+ const rows = parseCSV(text, columns); // …and back to rows (round-trip)
722
724
  ```
723
725
 
726
+ `parseCSV` is RFC4180-aware (quoted fields, embedded commas/quotes/newlines), maps
727
+ headers to columns, coerces numeric/`date` columns, and stamps `id` + flash fields
728
+ so the result drops straight into `<Grid rows={…}>`. There's also `parseTSV` (tab-
729
+ separated — what Ctrl/⌘+C copies), and for JSON/API data, `rowsFromObjects(objects)`
730
+ / `parseJSON(text)`:
731
+
732
+ ```ts
733
+ import { rowsFromObjects } from 'bo-grid';
734
+ const rows = rowsFromObjects(await (await fetch('/api/rows')).json());
735
+ ```
736
+
737
+ Not sure what you'll get? **`parseRows(text, columns?)`** auto-detects JSON / TSV /
738
+ CSV — perfect for a paste handler:
739
+
740
+ ```svelte
741
+ <div onpaste={(e) => (rows = parseRows(e.clipboardData.getData('text'), columns))}>…</div>
742
+ ```
743
+
744
+ See the **CSV import** demo (CSV / TSV / JSON / Auto-detect).
745
+
724
746
  Excel export loads SheetJS via **dynamic import**, so it lands in its own lazy
725
747
  chunk and never bloats your core bundle. `xlsx` is an **optional peer dependency**
726
748
  — install it only if you use this:
@@ -734,11 +756,23 @@ Sparkline columns are skipped; numeric columns export as raw numbers so
734
756
  spreadsheets can compute on them (pass `{ formatted: true }` for display strings).
735
757
  Ctrl/⌘+C still copies the current selection as TSV.
736
758
 
759
+ **Printing.** The grid virtualizes, so printing it directly drops off-screen rows.
760
+ `printTable(rows, columns, { title })` opens a print window with **all** rows as a
761
+ clean table (Save-as-PDF from the dialog); `toHTMLTable(rows, columns)` returns
762
+ that table as an HTML string to embed. See the **Print** demo.
763
+
764
+ ```ts
765
+ import { printTable } from 'bo-grid';
766
+ printTable(rows, columns, { title: 'Sales report' });
767
+ ```
768
+
737
769
  ## Also exported
738
770
 
739
771
  `Sparkline` component · `drawCandles` / `setupHiDpiCanvas` (draw on your own
740
772
  canvas) · `fmtPrice` / `fmtPercent` / `fmtVolume` / `fmtDate` · `heatColor` ·
741
- `Selection` · `aggregate` · `toCSV` / `exportCSV` / `exportXLSX` / `rowsToMatrix`.
773
+ `Selection` · `aggregate` · `toCSV` / `exportCSV` / `exportXLSX` / `rowsToMatrix` ·
774
+ `parseCSV` / `parseCSVMatrix` / `parseTSV` / `parseJSON` / `parseRows` /
775
+ `rowsFromObjects` · `toHTMLTable` / `printTable`.
742
776
 
743
777
  ## Pivot tables
744
778
 
@@ -16,4 +16,37 @@ export declare function toCSV(rows: readonly GridRow[], columns: readonly Column
16
16
  /** Trigger a browser download of text content. No-op outside the browser. */
17
17
  export declare function download(filename: string, content: string, mime?: string): void;
18
18
  export declare function exportCSV(filename: string, rows: readonly GridRow[], columns: readonly ColumnDef[], opts?: ExportOptions): void;
19
+ /** Parse RFC4180 CSV text into a 2-D string matrix. Pure; inverse of `rowsToMatrix`. */
20
+ export declare function parseCSVMatrix(text: string): string[][];
21
+ /**
22
+ * Parse CSV text into grid rows. The first line is the header (mapped to columns
23
+ * by `header` or `key`); numeric columns coerce to numbers, `date` columns to
24
+ * epoch ms. Rows get `id` + flash fields so they're `GridRow`-ready. Inverse of
25
+ * `toCSV` — round-trips. Pure; unit-tested.
26
+ */
27
+ export declare function parseCSV(text: string, columns?: readonly ColumnDef[]): GridRow[];
28
+ /** Parse TAB-separated text into grid rows (same mapping as `parseCSV`). Handy for
29
+ spreadsheet/clipboard data — `Ctrl/⌘+C` copies the selection as TSV. */
30
+ export declare function parseTSV(text: string, columns?: readonly ColumnDef[]): GridRow[];
31
+ /**
32
+ * Adapt plain objects (e.g. a JSON API response) to grid rows: stamp `id` (the
33
+ * object's own `id`, else the index) and `flashSeq`/`flashDir` if absent, keeping
34
+ * all fields. The cheapest path from `await res.json()` to `<Grid rows>`.
35
+ */
36
+ export declare function rowsFromObjects(objects: readonly Record<string, unknown>[]): GridRow[];
37
+ /** Parse a JSON array of objects into grid rows (`JSON.parse` + `rowsFromObjects`).
38
+ Throws on invalid JSON or a non-array top level. */
39
+ export declare function parseJSON(text: string): GridRow[];
40
+ /**
41
+ * Smart import: detect the format of `text` — a JSON array, TSV, or CSV — and
42
+ * parse it into grid rows. Ideal for a paste handler where the source is unknown:
43
+ *
44
+ * el.addEventListener('paste', (e) => {
45
+ * rows = parseRows(e.clipboardData.getData('text'), columns);
46
+ * });
47
+ *
48
+ * Leading `[` → JSON (falls through to delimited if it isn't valid JSON); a tab in
49
+ * the first line → TSV; otherwise CSV. Pure; unit-tested.
50
+ */
51
+ export declare function parseRows(text: string, columns?: readonly ColumnDef[]): GridRow[];
19
52
  export {};
@@ -49,3 +49,156 @@ export function download(filename, content, mime = 'text/csv;charset=utf-8') {
49
49
  export function exportCSV(filename, rows, columns, opts = {}) {
50
50
  download(filename, toCSV(rows, columns, opts));
51
51
  }
52
+ /**
53
+ * Parse delimited text (CSV/TSV) into a 2-D string matrix — handles quoted fields
54
+ * with embedded delimiters, doubled quotes and newlines, and CRLF or LF endings.
55
+ */
56
+ function parseDelimited(text, delim) {
57
+ const rows = [];
58
+ let row = [];
59
+ let field = '';
60
+ let inQuotes = false;
61
+ let i = 0;
62
+ const endField = () => {
63
+ row.push(field);
64
+ field = '';
65
+ };
66
+ const endRow = () => {
67
+ endField();
68
+ rows.push(row);
69
+ row = [];
70
+ };
71
+ while (i < text.length) {
72
+ const c = text[i];
73
+ if (inQuotes) {
74
+ if (c === '"') {
75
+ if (text[i + 1] === '"') {
76
+ field += '"';
77
+ i += 2;
78
+ }
79
+ else {
80
+ inQuotes = false;
81
+ i++;
82
+ }
83
+ }
84
+ else {
85
+ field += c;
86
+ i++;
87
+ }
88
+ continue;
89
+ }
90
+ if (c === '"') {
91
+ inQuotes = true;
92
+ i++;
93
+ }
94
+ else if (c === delim) {
95
+ endField();
96
+ i++;
97
+ }
98
+ else if (c === '\n') {
99
+ endRow();
100
+ i++;
101
+ }
102
+ else if (c === '\r') {
103
+ i++; // CRLF: the \n ends the row
104
+ }
105
+ else {
106
+ field += c;
107
+ i++;
108
+ }
109
+ }
110
+ if (field !== '' || row.length > 0)
111
+ endRow(); // trailing line without a newline
112
+ return rows;
113
+ }
114
+ /** Parse RFC4180 CSV text into a 2-D string matrix. Pure; inverse of `rowsToMatrix`. */
115
+ export function parseCSVMatrix(text) {
116
+ return parseDelimited(text, ',');
117
+ }
118
+ // Header-row matrix → grid rows: map each header to a column (by `header` or
119
+ // `key`), coerce numeric columns to numbers and `date` columns to epoch ms, leave
120
+ // blanks/unparseable as-is, and stamp id + flash fields. Drops blank lines.
121
+ function matrixToRows(matrix, columns) {
122
+ const m = matrix.filter((r) => !(r.length === 1 && r[0] === ''));
123
+ if (m.length < 1)
124
+ return [];
125
+ const headers = m[0];
126
+ const cols = headers.map((h) => columns.find((c) => c.header === h || c.key === h));
127
+ return m.slice(1).map((cells, i) => {
128
+ const row = { id: i, flashSeq: 0, flashDir: 'up' };
129
+ headers.forEach((h, c) => {
130
+ const col = cols[c];
131
+ const raw = cells[c] ?? '';
132
+ const key = col ? col.key : h;
133
+ if (raw === '') {
134
+ row[key] = '';
135
+ }
136
+ else if (col?.type === 'date') {
137
+ const ms = Date.parse(raw);
138
+ row[key] = Number.isFinite(ms) ? ms : raw;
139
+ }
140
+ else if (col && isNumeric(col)) {
141
+ const n = Number(raw);
142
+ row[key] = Number.isFinite(n) ? n : raw;
143
+ }
144
+ else {
145
+ row[key] = raw;
146
+ }
147
+ });
148
+ return row;
149
+ });
150
+ }
151
+ /**
152
+ * Parse CSV text into grid rows. The first line is the header (mapped to columns
153
+ * by `header` or `key`); numeric columns coerce to numbers, `date` columns to
154
+ * epoch ms. Rows get `id` + flash fields so they're `GridRow`-ready. Inverse of
155
+ * `toCSV` — round-trips. Pure; unit-tested.
156
+ */
157
+ export function parseCSV(text, columns = []) {
158
+ return matrixToRows(parseDelimited(text, ','), columns);
159
+ }
160
+ /** Parse TAB-separated text into grid rows (same mapping as `parseCSV`). Handy for
161
+ spreadsheet/clipboard data — `Ctrl/⌘+C` copies the selection as TSV. */
162
+ export function parseTSV(text, columns = []) {
163
+ return matrixToRows(parseDelimited(text, '\t'), columns);
164
+ }
165
+ /**
166
+ * Adapt plain objects (e.g. a JSON API response) to grid rows: stamp `id` (the
167
+ * object's own `id`, else the index) and `flashSeq`/`flashDir` if absent, keeping
168
+ * all fields. The cheapest path from `await res.json()` to `<Grid rows>`.
169
+ */
170
+ export function rowsFromObjects(objects) {
171
+ return objects.map((o, i) => ({ id: i, flashSeq: 0, flashDir: 'up', ...o }));
172
+ }
173
+ /** Parse a JSON array of objects into grid rows (`JSON.parse` + `rowsFromObjects`).
174
+ Throws on invalid JSON or a non-array top level. */
175
+ export function parseJSON(text) {
176
+ const data = JSON.parse(text);
177
+ if (!Array.isArray(data))
178
+ throw new Error('parseJSON: expected a JSON array of objects');
179
+ return rowsFromObjects(data);
180
+ }
181
+ /**
182
+ * Smart import: detect the format of `text` — a JSON array, TSV, or CSV — and
183
+ * parse it into grid rows. Ideal for a paste handler where the source is unknown:
184
+ *
185
+ * el.addEventListener('paste', (e) => {
186
+ * rows = parseRows(e.clipboardData.getData('text'), columns);
187
+ * });
188
+ *
189
+ * Leading `[` → JSON (falls through to delimited if it isn't valid JSON); a tab in
190
+ * the first line → TSV; otherwise CSV. Pure; unit-tested.
191
+ */
192
+ export function parseRows(text, columns = []) {
193
+ if (text.trimStart().startsWith('[')) {
194
+ try {
195
+ return parseJSON(text);
196
+ }
197
+ catch {
198
+ // Not valid JSON after all — treat it as delimited text below.
199
+ }
200
+ }
201
+ const nl = text.indexOf('\n');
202
+ const firstLine = nl === -1 ? text : text.slice(0, nl);
203
+ return firstLine.includes('\t') ? parseTSV(text, columns) : parseCSV(text, columns);
204
+ }
@@ -0,0 +1,17 @@
1
+ import type { ColumnDef, GridRow } from './column';
2
+ /** Escape a string for safe HTML text/attribute interpolation. */
3
+ export declare function escapeHTML(s: string): string;
4
+ /**
5
+ * Render rows to a semantic `<table>` HTML string (formatted via the column
6
+ * formatters; numeric columns right-aligned; sparkline/custom columns skipped).
7
+ * Values are HTML-escaped. Pure; unit-tested. Embed it, or use `printTable`.
8
+ */
9
+ export declare function toHTMLTable(rows: readonly GridRow[], columns: readonly ColumnDef[]): string;
10
+ /**
11
+ * Open a print window with all rows as a clean table and trigger the print
12
+ * dialog. No-op outside the browser (or if a popup is blocked). For PDFs, users
13
+ * "Save as PDF" from the print dialog.
14
+ */
15
+ export declare function printTable(rows: readonly GridRow[], columns: readonly ColumnDef[], opts?: {
16
+ title?: string;
17
+ }): void;
@@ -0,0 +1,55 @@
1
+ import { formatCell, cellValue, isNumeric } from './column';
2
+ const ESC = {
3
+ '&': '&amp;',
4
+ '<': '&lt;',
5
+ '>': '&gt;',
6
+ '"': '&quot;',
7
+ "'": '&#39;',
8
+ };
9
+ /** Escape a string for safe HTML text/attribute interpolation. */
10
+ export function escapeHTML(s) {
11
+ return s.replace(/[&<>"']/g, (c) => ESC[c]);
12
+ }
13
+ /**
14
+ * Render rows to a semantic `<table>` HTML string (formatted via the column
15
+ * formatters; numeric columns right-aligned; sparkline/custom columns skipped).
16
+ * Values are HTML-escaped. Pure; unit-tested. Embed it, or use `printTable`.
17
+ */
18
+ export function toHTMLTable(rows, columns) {
19
+ const cols = columns.filter((c) => c.type !== 'sparkline' && c.type !== 'custom');
20
+ const align = (c) => (isNumeric(c) ? ' style="text-align:right"' : '');
21
+ const head = cols.map((c) => `<th${align(c)}>${escapeHTML(c.header)}</th>`).join('');
22
+ const body = rows
23
+ .map((row) => '<tr>' +
24
+ cols.map((c) => `<td${align(c)}>${escapeHTML(formatCell(c, cellValue(c, row), row))}</td>`).join('') +
25
+ '</tr>')
26
+ .join('');
27
+ return `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
28
+ }
29
+ const PRINT_CSS = `
30
+ *{box-sizing:border-box}
31
+ body{font:12px -apple-system,system-ui,sans-serif;color:#111;margin:24px}
32
+ h1{font-size:16px;margin:0 0 12px}
33
+ table{border-collapse:collapse;width:100%}
34
+ th,td{padding:4px 8px;border:1px solid #ddd;white-space:nowrap;text-align:left}
35
+ thead th{background:#f4f4f4;font-weight:600}
36
+ tbody tr:nth-child(even){background:#fafafa}
37
+ @media print{body{margin:0}}`;
38
+ /**
39
+ * Open a print window with all rows as a clean table and trigger the print
40
+ * dialog. No-op outside the browser (or if a popup is blocked). For PDFs, users
41
+ * "Save as PDF" from the print dialog.
42
+ */
43
+ export function printTable(rows, columns, opts = {}) {
44
+ if (typeof window === 'undefined' || typeof window.open !== 'function')
45
+ return;
46
+ const win = window.open('', '_blank');
47
+ if (!win)
48
+ return; // popup blocked
49
+ const title = opts.title ?? 'Grid';
50
+ win.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${escapeHTML(title)}</title>` +
51
+ `<style>${PRINT_CSS}</style></head><body><h1>${escapeHTML(title)}</h1>` +
52
+ `${toHTMLTable(rows, columns)}` +
53
+ `<script>window.onload=function(){window.print()}<\/script></body></html>`);
54
+ win.document.close();
55
+ }
package/dist/index.d.ts CHANGED
@@ -13,9 +13,10 @@ export type { PivotConfig, PivotResult } from './grid/pivot';
13
13
  export { themeVars, darkTheme, lightTheme, highContrastDark, highContrastLight, midnightTheme, terminalTheme, themePresets, } from './grid/theme';
14
14
  export type { GridTheme, ThemePreset } from './grid/theme';
15
15
  export { drawCandles, setupHiDpiCanvas } from './sparkline/sparkline-render';
16
- export { toCSV, exportCSV } from './grid/export';
16
+ export { toCSV, exportCSV, parseCSV, parseCSVMatrix, parseTSV, parseJSON, parseRows, rowsFromObjects, } from './grid/export';
17
17
  export { exportXLSX } from './grid/export-xlsx';
18
18
  export type { ExportOptions } from './grid/export';
19
+ export { toHTMLTable, printTable, escapeHTML } from './grid/print';
19
20
  export { createArraySource } from './grid/source';
20
21
  export type { RowSource, RowRange, RowSourceParams, RowSourceResult, ArraySourceOptions, } from './grid/source';
21
22
  export type { Candle } from './types';
package/dist/index.js CHANGED
@@ -17,8 +17,10 @@ export { pivot } from './grid/pivot';
17
17
  export { themeVars, darkTheme, lightTheme, highContrastDark, highContrastLight, midnightTheme, terminalTheme, themePresets, } from './grid/theme';
18
18
  // Sparkline canvas primitives (draw candlesticks on your own canvas)
19
19
  export { drawCandles, setupHiDpiCanvas } from './sparkline/sparkline-render';
20
- // Export (CSV is dependency-free; XLSX dynamic-imports the optional `xlsx` peer)
21
- export { toCSV, exportCSV } from './grid/export';
20
+ // Export + import (CSV is dependency-free; XLSX dynamic-imports the optional `xlsx` peer)
21
+ export { toCSV, exportCSV, parseCSV, parseCSVMatrix, parseTSV, parseJSON, parseRows, rowsFromObjects, } from './grid/export';
22
22
  export { exportXLSX } from './grid/export-xlsx';
23
+ // Printable / export HTML (renders ALL rows, unlike the virtualized grid)
24
+ export { toHTMLTable, printTable, escapeHTML } from './grid/print';
23
25
  // Server-side / windowed data source
24
26
  export { createArraySource } from './grid/source';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bo-grid",
3
- "version": "0.21.0",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Tiny, fast Svelte 5 data grid: canvas sparklines, batched realtime cell updates, and virtual scrolling. A free, fintech-focused alternative to heavyweight grids.",
@@ -69,6 +69,7 @@
69
69
  "size:lib": "vite build --config vite.lib.config.ts && node scripts/size-lib.mjs",
70
70
  "smoke": "vite build --base=./ --outDir demo-dist && node scripts/smoke.mjs",
71
71
  "smoke:wc": "node scripts/wc-smoke.mjs",
72
+ "check:examples": "node scripts/check-examples.mjs",
72
73
  "ssr": "node scripts/ssr.mjs",
73
74
  "bench": "node scripts/bench.mjs",
74
75
  "test": "vitest run",