bo-grid 0.8.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.
Files changed (51) hide show
  1. package/README.md +254 -27
  2. package/dist/bo-grid.FilterMenu-BHI6rILc.js +154 -0
  3. package/dist/bo-grid.ToolPanel-C3u-4YKc.js +34 -0
  4. package/dist/bo-grid.element-DPnHUXMa.js +6623 -0
  5. package/dist/bo-grid.element.js +4 -0
  6. package/dist/charts/BarChart.svelte +50 -0
  7. package/dist/charts/BarChart.svelte.d.ts +16 -0
  8. package/dist/charts/DonutChart.svelte +54 -0
  9. package/dist/charts/DonutChart.svelte.d.ts +18 -0
  10. package/dist/charts/Legend.svelte +47 -0
  11. package/dist/charts/Legend.svelte.d.ts +12 -0
  12. package/dist/charts/LineChart.svelte +59 -0
  13. package/dist/charts/LineChart.svelte.d.ts +14 -0
  14. package/dist/charts/StackedBarChart.svelte +56 -0
  15. package/dist/charts/StackedBarChart.svelte.d.ts +18 -0
  16. package/dist/charts/chart-math.d.ts +57 -0
  17. package/dist/charts/chart-math.js +174 -0
  18. package/dist/charts/index.d.ts +8 -0
  19. package/dist/charts/index.js +11 -0
  20. package/dist/charts/palette.d.ts +4 -0
  21. package/dist/charts/palette.js +14 -0
  22. package/dist/format/format.d.ts +6 -0
  23. package/dist/format/format.js +41 -0
  24. package/dist/grid/Cell.svelte +247 -8
  25. package/dist/grid/Cell.svelte.d.ts +6 -0
  26. package/dist/grid/FilterMenu.svelte +7 -0
  27. package/dist/grid/Grid.svelte +307 -85
  28. package/dist/grid/Grid.svelte.d.ts +19 -0
  29. package/dist/grid/GroupRow.svelte +5 -2
  30. package/dist/grid/Pager.svelte +4 -0
  31. package/dist/grid/RowMenu.svelte +65 -2
  32. package/dist/grid/ToolPanel.svelte +5 -0
  33. package/dist/grid/column.d.ts +133 -0
  34. package/dist/grid/column.js +133 -4
  35. package/dist/grid/colvirt.d.ts +15 -0
  36. package/dist/grid/colvirt.js +43 -0
  37. package/dist/grid/export.d.ts +33 -0
  38. package/dist/grid/export.js +158 -2
  39. package/dist/grid/filtering.d.ts +5 -2
  40. package/dist/grid/filtering.js +5 -4
  41. package/dist/grid/grouping.d.ts +30 -0
  42. package/dist/grid/grouping.js +33 -0
  43. package/dist/grid/print.d.ts +17 -0
  44. package/dist/grid/print.js +55 -0
  45. package/dist/grid/theme.d.ts +15 -0
  46. package/dist/grid/theme.js +78 -0
  47. package/dist/grid/tree.d.ts +19 -7
  48. package/dist/grid/tree.js +16 -11
  49. package/dist/index.d.ts +7 -5
  50. package/dist/index.js +6 -4
  51. package/package.json +13 -2
@@ -1,4 +1,4 @@
1
- import { formatCell, isNumeric } from './column';
1
+ import { formatCell, isNumeric, cellValue } from './column';
2
2
  function rawValue(col, v) {
3
3
  if (col.type === 'date')
4
4
  return formatCell(col, v); // epoch ms isn't useful raw
@@ -19,7 +19,10 @@ export function rowsToMatrix(rows, columns, opts = {}) {
19
19
  if (opts.header !== false)
20
20
  matrix.push(cols.map((c) => c.header));
21
21
  for (const row of rows) {
22
- matrix.push(cols.map((c) => (opts.formatted ? formatCell(c, row[c.key], row) : rawValue(c, row[c.key]))));
22
+ matrix.push(cols.map((c) => {
23
+ const v = cellValue(c, row);
24
+ return opts.formatted ? formatCell(c, v, row) : rawValue(c, v);
25
+ }));
23
26
  }
24
27
  return matrix;
25
28
  }
@@ -46,3 +49,156 @@ export function download(filename, content, mime = 'text/csv;charset=utf-8') {
46
49
  export function exportCSV(filename, rows, columns, opts = {}) {
47
50
  download(filename, toCSV(rows, columns, opts));
48
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
+ }
@@ -35,7 +35,10 @@ export declare function emptyFilter(kind: FilterKind): ColumnFilter;
35
35
  export declare function isFilterActive(f: ColumnFilter | undefined | null): boolean;
36
36
  /** Does one cell value satisfy one filter? An inactive filter passes everything. */
37
37
  export declare function matchesFilter(value: unknown, f: ColumnFilter): boolean;
38
+ /** Resolve a column's value for filtering — `row[key]` by default; Grid passes a
39
+ computed-aware resolver so computed columns filter on their derived value. */
40
+ export type ValueResolver = (row: GridRow, key: string) => unknown;
38
41
  /** AND across every active per-column filter. */
39
- export declare function passesFilters(row: GridRow, filters: Record<string, ColumnFilter>): boolean;
42
+ export declare function passesFilters(row: GridRow, filters: Record<string, ColumnFilter>, valueOf?: ValueResolver): boolean;
40
43
  /** Sorted unique string values for a column — the set-filter checklist. */
41
- export declare function distinctValues(rows: readonly GridRow[], key: string): string[];
44
+ export declare function distinctValues(rows: readonly GridRow[], key: string, valueOf?: ValueResolver): string[];
@@ -89,19 +89,20 @@ export function matchesFilter(value, f) {
89
89
  }
90
90
  return true; // unreachable fallback
91
91
  }
92
+ const byKey = (row, key) => row[key];
92
93
  /** AND across every active per-column filter. */
93
- export function passesFilters(row, filters) {
94
+ export function passesFilters(row, filters, valueOf = byKey) {
94
95
  for (const key in filters) {
95
96
  const f = filters[key];
96
- if (isFilterActive(f) && !matchesFilter(row[key], f))
97
+ if (isFilterActive(f) && !matchesFilter(valueOf(row, key), f))
97
98
  return false;
98
99
  }
99
100
  return true;
100
101
  }
101
102
  /** Sorted unique string values for a column — the set-filter checklist. */
102
- export function distinctValues(rows, key) {
103
+ export function distinctValues(rows, key, valueOf = byKey) {
103
104
  const seen = new Set();
104
105
  for (const row of rows)
105
- seen.add(String(row[key] ?? ''));
106
+ seen.add(String(valueOf(row, key) ?? ''));
106
107
  return [...seen].sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
107
108
  }
@@ -10,6 +10,19 @@ export interface GroupNode {
10
10
  rows: GridRow[];
11
11
  count: number;
12
12
  collapsed: boolean;
13
+ /** Server-provided aggregate display strings, keyed by column key (lazy groups).
14
+ When present, the group header shows these instead of computing from `rows`. */
15
+ aggText?: Record<string, string>;
16
+ }
17
+ /** A server-side group summary (lazy grouping): the header data without the leaf
18
+ rows, which load on expand via `loadGroup`. */
19
+ export interface LazyGroup {
20
+ key: string;
21
+ /** Header label (defaults to `key`). */
22
+ label?: string;
23
+ count?: number;
24
+ /** Preformatted aggregate strings keyed by column key, shown in the header. */
25
+ agg?: Record<string, string>;
13
26
  }
14
27
  export type VisualRow = {
15
28
  kind: 'data';
@@ -19,6 +32,9 @@ export type VisualRow = {
19
32
  } | {
20
33
  kind: 'group';
21
34
  group: GroupNode;
35
+ } | {
36
+ kind: 'treeloading';
37
+ depth: number;
22
38
  };
23
39
  /**
24
40
  * Flatten data rows into the visual row list the grid renders: a stream of
@@ -36,3 +52,17 @@ export declare function buildFlatRows(rows: GridRow[], groupBy: string[], collap
36
52
  * nearest preceding header at each depth until the depth-0 group is found.
37
53
  */
38
54
  export declare function activeGroupsAt(flat: VisualRow[], idx: number): GroupNode[];
55
+ /** How lazy grouping reads expand/loading state and the rows loaded per group. */
56
+ export interface LazyGroupAccess {
57
+ isExpanded: (key: string) => boolean;
58
+ /** Loaded leaf rows for a group, or undefined when not yet loaded. */
59
+ rowsOf: (key: string) => readonly GridRow[] | undefined;
60
+ isLoading: (key: string) => boolean;
61
+ }
62
+ /**
63
+ * Flatten server-side group summaries into visual rows: a group header per group,
64
+ * then — when expanded — its loaded leaf rows, or a single `treeloading`
65
+ * placeholder while they load. Aggregates come from the summary (not computed).
66
+ * Pure; unit-tested.
67
+ */
68
+ export declare function buildLazyGroupRows(groups: readonly LazyGroup[], access: LazyGroupAccess): VisualRow[];
@@ -60,3 +60,36 @@ export function activeGroupsAt(flat, idx) {
60
60
  }
61
61
  return [...found.values()].sort((a, b) => a.depth - b.depth);
62
62
  }
63
+ /**
64
+ * Flatten server-side group summaries into visual rows: a group header per group,
65
+ * then — when expanded — its loaded leaf rows, or a single `treeloading`
66
+ * placeholder while they load. Aggregates come from the summary (not computed).
67
+ * Pure; unit-tested.
68
+ */
69
+ export function buildLazyGroupRows(groups, access) {
70
+ const out = [];
71
+ for (const g of groups) {
72
+ const expanded = access.isExpanded(g.key);
73
+ out.push({
74
+ kind: 'group',
75
+ group: {
76
+ path: g.key,
77
+ depth: 0,
78
+ value: g.label ?? g.key,
79
+ rows: [],
80
+ count: g.count ?? 0,
81
+ collapsed: !expanded,
82
+ aggText: g.agg,
83
+ },
84
+ });
85
+ if (!expanded)
86
+ continue;
87
+ const loaded = access.rowsOf(g.key);
88
+ if (loaded)
89
+ for (const row of loaded)
90
+ out.push({ kind: 'data', row });
91
+ else if (access.isLoading(g.key))
92
+ out.push({ kind: 'treeloading', depth: 1 });
93
+ }
94
+ return out;
95
+ }
@@ -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
+ }
@@ -35,3 +35,18 @@ export interface GridTheme {
35
35
  export declare function themeVars(theme: GridTheme): string;
36
36
  export declare const darkTheme: GridTheme;
37
37
  export declare const lightTheme: GridTheme;
38
+ export declare const highContrastDark: GridTheme;
39
+ export declare const highContrastLight: GridTheme;
40
+ export declare const midnightTheme: GridTheme;
41
+ export declare const terminalTheme: GridTheme;
42
+ /** All built-in presets, keyed by name (handy for a theme picker). */
43
+ export declare const themePresets: {
44
+ dark: GridTheme;
45
+ light: GridTheme;
46
+ 'high-contrast-dark': GridTheme;
47
+ 'high-contrast-light': GridTheme;
48
+ midnight: GridTheme;
49
+ terminal: GridTheme;
50
+ };
51
+ /** Built-in preset name. */
52
+ export type ThemePreset = keyof typeof themePresets;
@@ -64,3 +64,81 @@ export const lightTheme = {
64
64
  selBorder: '#6366f1',
65
65
  scheme: 'light',
66
66
  };
67
+ // High-contrast dark (accessibility): pure black, white text, strong borders and
68
+ // vivid status colours — comfortably exceeds WCAG AA, toward AAA.
69
+ export const highContrastDark = {
70
+ bg: '#000000',
71
+ headerBg: '#0a0a0a',
72
+ rowA: '#000000',
73
+ rowB: '#0a0a0a',
74
+ rowHover: '#1c1c1c',
75
+ text: '#ffffff',
76
+ textDim: '#c8c8c8',
77
+ border: 'rgba(255,255,255,0.34)',
78
+ up: '#00e676',
79
+ down: '#ff5252',
80
+ amber: '#ffd740',
81
+ selFill: 'rgba(255,255,255,0.20)',
82
+ selBorder: '#ffffff',
83
+ scheme: 'dark',
84
+ };
85
+ // High-contrast light (accessibility): white, near-black text, strong borders.
86
+ export const highContrastLight = {
87
+ bg: '#ffffff',
88
+ headerBg: '#eeeeee',
89
+ rowA: '#ffffff',
90
+ rowB: '#f5f5f5',
91
+ rowHover: '#e3e3e3',
92
+ text: '#000000',
93
+ textDim: '#383838',
94
+ border: 'rgba(0,0,0,0.42)',
95
+ up: '#007a36',
96
+ down: '#c20000',
97
+ amber: '#7a5c00',
98
+ selFill: 'rgba(0,0,0,0.10)',
99
+ selBorder: '#000000',
100
+ scheme: 'light',
101
+ };
102
+ // Midnight: a deep navy/indigo dark theme — a calmer, "premium" alternative.
103
+ export const midnightTheme = {
104
+ bg: '#0f172a',
105
+ headerBg: '#0b1120',
106
+ rowA: '#0f172a',
107
+ rowB: '#121d35',
108
+ rowHover: '#1e293b',
109
+ text: '#e2e8f0',
110
+ textDim: '#94a3b8',
111
+ border: 'rgba(148,163,184,0.16)',
112
+ up: '#34d399',
113
+ down: '#fb7185',
114
+ amber: '#fbbf24',
115
+ selFill: 'rgba(129,140,248,0.22)',
116
+ selBorder: '#818cf8',
117
+ scheme: 'dark',
118
+ };
119
+ // Terminal: green phosphor on near-black — a retro fintech/console look.
120
+ export const terminalTheme = {
121
+ bg: '#0a0f0a',
122
+ headerBg: '#0d140d',
123
+ rowA: '#0a0f0a',
124
+ rowB: '#0e160e',
125
+ rowHover: '#16241a',
126
+ text: '#4ade80',
127
+ textDim: '#3f9e60',
128
+ border: 'rgba(74,222,128,0.20)',
129
+ up: '#4ade80',
130
+ down: '#f87171',
131
+ amber: '#fde047',
132
+ selFill: 'rgba(74,222,128,0.16)',
133
+ selBorder: '#4ade80',
134
+ scheme: 'dark',
135
+ };
136
+ /** All built-in presets, keyed by name (handy for a theme picker). */
137
+ export const themePresets = {
138
+ dark: darkTheme,
139
+ light: lightTheme,
140
+ 'high-contrast-dark': highContrastDark,
141
+ 'high-contrast-light': highContrastLight,
142
+ midnight: midnightTheme,
143
+ terminal: terminalTheme,
144
+ };
@@ -1,12 +1,24 @@
1
1
  import type { GridRow } from './column';
2
2
  import type { VisualRow } from './grouping';
3
- /** Resolve a row's children (undefined/empty = leaf). */
3
+ /** Resolve a row's children (undefined/empty = leaf). Sync. */
4
4
  export type GetChildren = (row: GridRow) => GridRow[] | undefined;
5
+ /** How the flattener reads a tree: which rows have children, which are expanded,
6
+ the children currently available (sync result or async cache), and which
7
+ expanded nodes are still loading. */
8
+ export interface TreeAccess {
9
+ /** Children available now (sync result or loaded cache); undefined = not loaded. */
10
+ childrenOf: (row: GridRow) => readonly GridRow[] | undefined;
11
+ /** Cheap predicate: does this row have children? (Drives the expand chevron.) */
12
+ hasChildren: (row: GridRow) => boolean;
13
+ isExpanded: (row: GridRow) => boolean;
14
+ /** Whether an expanded row's children are still loading (async trees). */
15
+ isLoading?: (row: GridRow) => boolean;
16
+ }
5
17
  /**
6
- * Flatten a tree of rows into the visible, depth-tagged data rows the grid
7
- * renders. Pre-order DFS: each node is emitted, then — if it has children and
8
- * `isExpanded` returns true — its children, one level deeper. Collapsed or leaf
9
- * nodes contribute only themselves. Pure: no row values are read, so a realtime
10
- * tick never rebuilds this list.
18
+ * Flatten a tree of rows into the visible, depth-tagged rows the grid renders.
19
+ * Pre-order DFS: each node is emitted, then — if it has children and is expanded
20
+ * — its loaded children one level deeper, or a single `treeloading` placeholder
21
+ * while they load. Pure: no row values are read, so a realtime tick never rebuilds
22
+ * this list.
11
23
  */
12
- export declare function buildTreeRows(roots: readonly GridRow[], getChildren: GetChildren, isExpanded: (row: GridRow) => boolean): VisualRow[];
24
+ export declare function buildTreeRows(roots: readonly GridRow[], access: TreeAccess): VisualRow[];
package/dist/grid/tree.js CHANGED
@@ -1,19 +1,24 @@
1
1
  /**
2
- * Flatten a tree of rows into the visible, depth-tagged data rows the grid
3
- * renders. Pre-order DFS: each node is emitted, then — if it has children and
4
- * `isExpanded` returns true — its children, one level deeper. Collapsed or leaf
5
- * nodes contribute only themselves. Pure: no row values are read, so a realtime
6
- * tick never rebuilds this list.
2
+ * Flatten a tree of rows into the visible, depth-tagged rows the grid renders.
3
+ * Pre-order DFS: each node is emitted, then — if it has children and is expanded
4
+ * — its loaded children one level deeper, or a single `treeloading` placeholder
5
+ * while they load. Pure: no row values are read, so a realtime tick never rebuilds
6
+ * this list.
7
7
  */
8
- export function buildTreeRows(roots, getChildren, isExpanded) {
8
+ export function buildTreeRows(roots, access) {
9
+ const { childrenOf, hasChildren, isExpanded, isLoading } = access;
9
10
  const out = [];
10
11
  const walk = (nodes, depth) => {
11
12
  for (const row of nodes) {
12
- const children = getChildren(row);
13
- const hasChildren = !!children && children.length > 0;
14
- out.push({ kind: 'data', row, depth, hasChildren });
15
- if (hasChildren && isExpanded(row))
16
- walk(children, depth + 1);
13
+ const has = hasChildren(row);
14
+ out.push({ kind: 'data', row, depth, hasChildren: has });
15
+ if (has && isExpanded(row)) {
16
+ const children = childrenOf(row);
17
+ if (children && children.length > 0)
18
+ walk(children, depth + 1);
19
+ else if (isLoading?.(row))
20
+ out.push({ kind: 'treeloading', depth: depth + 1 });
21
+ }
17
22
  }
18
23
  };
19
24
  walk(roots, 0);
package/dist/index.d.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  export { default as Grid } from './grid/Grid.svelte';
2
2
  export { default as Sparkline } from './sparkline/Sparkline.svelte';
3
- export type { ColumnDef, Align, GridRow, SortDir, SortState, CellEditEvent } from './grid/column';
3
+ export type { LazyGroup } from './grid/grouping';
4
+ export type { ColumnDef, Align, GridRow, SortDir, SortState, CellEditEvent, BadgeTone, DataBarConfig, IconRule, ColorScaleConfig, } from './grid/column';
4
5
  export type { AggKind, AggResult } from './grid/aggregate';
5
6
  export type { ColumnFilter, FilterKind, TextOp, NumberOp, DateOp } from './grid/filtering';
6
- export { fmtPrice, fmtPercent, fmtVolume, fmtDate } from './format/format';
7
+ export { fmtPrice, fmtPercent, fmtVolume, fmtDate, fmtCurrency, relativeTime } from './format/format';
7
8
  export type { DateStyle } from './format/format';
8
9
  export { aggregate } from './grid/aggregate';
9
10
  export { heatColor } from './grid/heatmap';
10
11
  export { pivot } from './grid/pivot';
11
12
  export type { PivotConfig, PivotResult } from './grid/pivot';
12
- export { themeVars, darkTheme, lightTheme } from './grid/theme';
13
- export type { GridTheme } from './grid/theme';
13
+ export { themeVars, darkTheme, lightTheme, highContrastDark, highContrastLight, midnightTheme, terminalTheme, themePresets, } from './grid/theme';
14
+ export type { GridTheme, ThemePreset } from './grid/theme';
14
15
  export { drawCandles, setupHiDpiCanvas } from './sparkline/sparkline-render';
15
- export { toCSV, exportCSV } from './grid/export';
16
+ export { toCSV, exportCSV, parseCSV, parseCSVMatrix, parseTSV, parseJSON, parseRows, rowsFromObjects, } from './grid/export';
16
17
  export { exportXLSX } from './grid/export-xlsx';
17
18
  export type { ExportOptions } from './grid/export';
19
+ export { toHTMLTable, printTable, escapeHTML } from './grid/print';
18
20
  export { createArraySource } from './grid/source';
19
21
  export type { RowSource, RowRange, RowSourceParams, RowSourceResult, ArraySourceOptions, } from './grid/source';
20
22
  export type { Candle } from './types';
package/dist/index.js CHANGED
@@ -8,17 +8,19 @@
8
8
  export { default as Grid } from './grid/Grid.svelte';
9
9
  export { default as Sparkline } from './sparkline/Sparkline.svelte';
10
10
  // Value formatters (handy when building custom cell content)
11
- export { fmtPrice, fmtPercent, fmtVolume, fmtDate } from './format/format';
11
+ export { fmtPrice, fmtPercent, fmtVolume, fmtDate, fmtCurrency, relativeTime } from './format/format';
12
12
  // Standalone helpers
13
13
  export { aggregate } from './grid/aggregate';
14
14
  export { heatColor } from './grid/heatmap';
15
15
  export { pivot } from './grid/pivot';
16
16
  // Theming
17
- export { themeVars, darkTheme, lightTheme } from './grid/theme';
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.8.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.",
@@ -33,6 +33,14 @@
33
33
  "svelte": "./dist/index.js",
34
34
  "default": "./dist/index.js"
35
35
  },
36
+ "./charts": {
37
+ "types": "./dist/charts/index.d.ts",
38
+ "svelte": "./dist/charts/index.js",
39
+ "default": "./dist/charts/index.js"
40
+ },
41
+ "./element": {
42
+ "default": "./dist/bo-grid.element.js"
43
+ },
36
44
  "./package.json": "./package.json"
37
45
  },
38
46
  "files": [
@@ -53,12 +61,15 @@
53
61
  "demo:build": "vite build",
54
62
  "pages:build": "vite build --base=/bo-grid/ --outDir demo-dist",
55
63
  "preview": "vite preview",
56
- "package": "svelte-package -i src/lib -o dist && node scripts/clean-dist.mjs",
64
+ "package": "svelte-package -i src/lib -o dist && node scripts/clean-dist.mjs && pnpm run build:wc",
65
+ "build:wc": "vite build --config vite.wc.config.ts",
57
66
  "prepublishOnly": "pnpm run package",
58
67
  "check": "svelte-check --tsconfig ./tsconfig.json",
59
68
  "size": "vite build && node scripts/size-check.mjs",
60
69
  "size:lib": "vite build --config vite.lib.config.ts && node scripts/size-lib.mjs",
61
70
  "smoke": "vite build --base=./ --outDir demo-dist && node scripts/smoke.mjs",
71
+ "smoke:wc": "node scripts/wc-smoke.mjs",
72
+ "check:examples": "node scripts/check-examples.mjs",
62
73
  "ssr": "node scripts/ssr.mjs",
63
74
  "bench": "node scripts/bench.mjs",
64
75
  "test": "vitest run",