@svgrid/grid 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.
Files changed (213) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +39 -0
  3. package/dist/FlexRender.svelte +96 -0
  4. package/dist/FlexRender.svelte.d.ts +49 -0
  5. package/dist/SvGrid.svelte +8742 -0
  6. package/dist/SvGrid.svelte.d.ts +381 -0
  7. package/dist/SvGridChart.svelte +653 -0
  8. package/dist/SvGridChart.svelte.d.ts +25 -0
  9. package/dist/SvGridChart.test.d.ts +1 -0
  10. package/dist/SvGridChart.test.js +62 -0
  11. package/dist/SvGridDropdown.svelte +675 -0
  12. package/dist/SvGridDropdown.svelte.d.ts +40 -0
  13. package/dist/a11y.contract.test.d.ts +1 -0
  14. package/dist/a11y.contract.test.js +39 -0
  15. package/dist/a11y.d.ts +38 -0
  16. package/dist/a11y.js +37 -0
  17. package/dist/a11y.test.d.ts +1 -0
  18. package/dist/a11y.test.js +44 -0
  19. package/dist/cell-formatting.d.ts +17 -0
  20. package/dist/cell-formatting.js +141 -0
  21. package/dist/cell-formatting.test.d.ts +1 -0
  22. package/dist/cell-formatting.test.js +234 -0
  23. package/dist/chart-export.d.ts +14 -0
  24. package/dist/chart-export.js +112 -0
  25. package/dist/chart.d.ts +226 -0
  26. package/dist/chart.js +732 -0
  27. package/dist/chart.test.d.ts +1 -0
  28. package/dist/chart.test.js +289 -0
  29. package/dist/collaboration.d.ts +74 -0
  30. package/dist/collaboration.js +98 -0
  31. package/dist/collaboration.test.d.ts +1 -0
  32. package/dist/collaboration.test.js +65 -0
  33. package/dist/conditional-formatting.d.ts +91 -0
  34. package/dist/conditional-formatting.js +170 -0
  35. package/dist/conditional-formatting.test.d.ts +1 -0
  36. package/dist/conditional-formatting.test.js +87 -0
  37. package/dist/core.coverage.test.d.ts +1 -0
  38. package/dist/core.coverage.test.js +186 -0
  39. package/dist/core.d.ts +370 -0
  40. package/dist/core.js +568 -0
  41. package/dist/core.performance.test.d.ts +1 -0
  42. package/dist/core.performance.test.js +29 -0
  43. package/dist/createGrid.svelte.d.ts +6 -0
  44. package/dist/createGrid.svelte.js +17 -0
  45. package/dist/createGrid.test.d.ts +1 -0
  46. package/dist/createGrid.test.js +9 -0
  47. package/dist/createGridState.svelte.d.ts +3 -0
  48. package/dist/createGridState.svelte.js +13 -0
  49. package/dist/editors/cell-editors.d.ts +23 -0
  50. package/dist/editors/cell-editors.js +97 -0
  51. package/dist/editors/cell-editors.test.d.ts +1 -0
  52. package/dist/editors/cell-editors.test.js +75 -0
  53. package/dist/fill-patterns.d.ts +30 -0
  54. package/dist/fill-patterns.js +207 -0
  55. package/dist/fill-patterns.test.d.ts +1 -0
  56. package/dist/fill-patterns.test.js +81 -0
  57. package/dist/filtering/excel-filters.d.ts +26 -0
  58. package/dist/filtering/excel-filters.js +64 -0
  59. package/dist/filtering/excel-filters.test.d.ts +1 -0
  60. package/dist/filtering/excel-filters.test.js +116 -0
  61. package/dist/filtering/locale-filter.test.d.ts +13 -0
  62. package/dist/filtering/locale-filter.test.js +189 -0
  63. package/dist/flex-render.d.ts +1 -0
  64. package/dist/flex-render.js +2 -0
  65. package/dist/flex-render.test.d.ts +1 -0
  66. package/dist/flex-render.test.js +142 -0
  67. package/dist/group-aggregate.test.d.ts +1 -0
  68. package/dist/group-aggregate.test.js +32 -0
  69. package/dist/index.d.ts +29 -0
  70. package/dist/index.js +27 -0
  71. package/dist/keyboard.d.ts +8 -0
  72. package/dist/keyboard.js +91 -0
  73. package/dist/keyboard.test.d.ts +1 -0
  74. package/dist/keyboard.test.js +27 -0
  75. package/dist/merge-objects.d.ts +9 -0
  76. package/dist/merge-objects.js +28 -0
  77. package/dist/named-views.d.ts +42 -0
  78. package/dist/named-views.js +80 -0
  79. package/dist/named-views.test.d.ts +1 -0
  80. package/dist/named-views.test.js +57 -0
  81. package/dist/new-features.test.d.ts +1 -0
  82. package/dist/new-features.test.js +183 -0
  83. package/dist/render-component.d.ts +13 -0
  84. package/dist/render-component.js +14 -0
  85. package/dist/render-component.test.d.ts +1 -0
  86. package/dist/render-component.test.js +41 -0
  87. package/dist/server-data-source.d.ts +76 -0
  88. package/dist/server-data-source.js +83 -0
  89. package/dist/server-data-source.test.d.ts +1 -0
  90. package/dist/server-data-source.test.js +81 -0
  91. package/dist/sparkline.d.ts +56 -0
  92. package/dist/sparkline.js +97 -0
  93. package/dist/sparkline.test.d.ts +1 -0
  94. package/dist/sparkline.test.js +52 -0
  95. package/dist/static-functions.d.ts +1 -0
  96. package/dist/static-functions.js +1 -0
  97. package/dist/subscribe.d.ts +9 -0
  98. package/dist/subscribe.js +31 -0
  99. package/dist/subscribe.test.d.ts +1 -0
  100. package/dist/subscribe.test.js +93 -0
  101. package/dist/sv-grid-scrollbar.d.ts +1 -0
  102. package/dist/sv-grid-scrollbar.js +310 -0
  103. package/dist/svgrid-wrapper.types.d.ts +341 -0
  104. package/dist/svgrid-wrapper.types.js +1 -0
  105. package/dist/svgrid.api-extensions.test.d.ts +14 -0
  106. package/dist/svgrid.api-extensions.test.js +334 -0
  107. package/dist/svgrid.api.test.d.ts +15 -0
  108. package/dist/svgrid.api.test.js +169 -0
  109. package/dist/svgrid.behavior.test.d.ts +15 -0
  110. package/dist/svgrid.behavior.test.js +605 -0
  111. package/dist/svgrid.column-reorder.test.d.ts +15 -0
  112. package/dist/svgrid.column-reorder.test.js +224 -0
  113. package/dist/svgrid.features.test.d.ts +1 -0
  114. package/dist/svgrid.features.test.js +129 -0
  115. package/dist/svgrid.interaction.test.d.ts +9 -0
  116. package/dist/svgrid.interaction.test.js +318 -0
  117. package/dist/svgrid.locale-filtering.test.d.ts +15 -0
  118. package/dist/svgrid.locale-filtering.test.js +261 -0
  119. package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
  120. package/dist/svgrid.new-features.wrapper.test.js +136 -0
  121. package/dist/svgrid.row-pinning.test.d.ts +14 -0
  122. package/dist/svgrid.row-pinning.test.js +250 -0
  123. package/dist/svgrid.set-filter.test.d.ts +17 -0
  124. package/dist/svgrid.set-filter.test.js +268 -0
  125. package/dist/svgrid.wrapper.test.d.ts +1 -0
  126. package/dist/svgrid.wrapper.test.js +33 -0
  127. package/dist/test-setup.d.ts +1 -0
  128. package/dist/test-setup.js +29 -0
  129. package/dist/transaction.test.d.ts +1 -0
  130. package/dist/transaction.test.js +89 -0
  131. package/dist/virtualization/column-virtualizer.d.ts +20 -0
  132. package/dist/virtualization/column-virtualizer.js +19 -0
  133. package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
  134. package/dist/virtualization/column-virtualizer.test.js +25 -0
  135. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
  136. package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
  137. package/dist/virtualization/types.d.ts +28 -0
  138. package/dist/virtualization/types.js +1 -0
  139. package/dist/virtualization/virtualizer.d.ts +13 -0
  140. package/dist/virtualization/virtualizer.js +232 -0
  141. package/dist/virtualization/virtualizer.test.d.ts +1 -0
  142. package/dist/virtualization/virtualizer.test.js +41 -0
  143. package/package.json +69 -0
  144. package/src/FlexRender.svelte +96 -0
  145. package/src/SvGrid.svelte +8742 -0
  146. package/src/SvGridChart.svelte +653 -0
  147. package/src/SvGridChart.test.ts +69 -0
  148. package/src/SvGridDropdown.svelte +675 -0
  149. package/src/a11y.contract.test.ts +49 -0
  150. package/src/a11y.test.ts +59 -0
  151. package/src/a11y.ts +59 -0
  152. package/src/cell-formatting.test.ts +273 -0
  153. package/src/cell-formatting.ts +169 -0
  154. package/src/chart-export.ts +144 -0
  155. package/src/chart.test.ts +311 -0
  156. package/src/chart.ts +934 -0
  157. package/src/collaboration.test.ts +74 -0
  158. package/src/collaboration.ts +161 -0
  159. package/src/conditional-formatting.test.ts +107 -0
  160. package/src/conditional-formatting.ts +239 -0
  161. package/src/core.coverage.test.ts +230 -0
  162. package/src/core.performance.test.ts +30 -0
  163. package/src/core.ts +991 -0
  164. package/src/createGrid.svelte.ts +42 -0
  165. package/src/createGrid.test.ts +10 -0
  166. package/src/createGridState.svelte.ts +17 -0
  167. package/src/editors/cell-editors.test.ts +80 -0
  168. package/src/editors/cell-editors.ts +127 -0
  169. package/src/fill-patterns.test.ts +93 -0
  170. package/src/fill-patterns.ts +229 -0
  171. package/src/filtering/excel-filters.test.ts +131 -0
  172. package/src/filtering/excel-filters.ts +101 -0
  173. package/src/filtering/locale-filter.test.ts +220 -0
  174. package/src/flex-render.test.ts +143 -0
  175. package/src/flex-render.ts +3 -0
  176. package/src/group-aggregate.test.ts +48 -0
  177. package/src/index.ts +159 -0
  178. package/src/keyboard.test.ts +59 -0
  179. package/src/keyboard.ts +97 -0
  180. package/src/merge-objects.ts +48 -0
  181. package/src/named-views.test.ts +66 -0
  182. package/src/named-views.ts +120 -0
  183. package/src/new-features.test.ts +217 -0
  184. package/src/render-component.test.ts +51 -0
  185. package/src/render-component.ts +28 -0
  186. package/src/server-data-source.test.ts +88 -0
  187. package/src/server-data-source.ts +163 -0
  188. package/src/sparkline.test.ts +59 -0
  189. package/src/sparkline.ts +164 -0
  190. package/src/static-functions.ts +11 -0
  191. package/src/subscribe.test.ts +103 -0
  192. package/src/subscribe.ts +38 -0
  193. package/src/sv-grid-scrollbar.ts +347 -0
  194. package/src/svgrid-wrapper.types.ts +382 -0
  195. package/src/svgrid.api-extensions.test.ts +362 -0
  196. package/src/svgrid.api.test.ts +192 -0
  197. package/src/svgrid.behavior.test.ts +657 -0
  198. package/src/svgrid.column-reorder.test.ts +234 -0
  199. package/src/svgrid.features.test.ts +157 -0
  200. package/src/svgrid.interaction.test.ts +355 -0
  201. package/src/svgrid.locale-filtering.test.ts +259 -0
  202. package/src/svgrid.new-features.wrapper.test.ts +164 -0
  203. package/src/svgrid.row-pinning.test.ts +266 -0
  204. package/src/svgrid.set-filter.test.ts +270 -0
  205. package/src/svgrid.wrapper.test.ts +35 -0
  206. package/src/test-setup.ts +37 -0
  207. package/src/transaction.test.ts +100 -0
  208. package/src/virtualization/column-virtualizer.test.ts +27 -0
  209. package/src/virtualization/column-virtualizer.ts +30 -0
  210. package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
  211. package/src/virtualization/types.ts +30 -0
  212. package/src/virtualization/virtualizer.test.ts +47 -0
  213. package/src/virtualization/virtualizer.ts +270 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Server-Side Row Model (SSRM) controller. A single, documented datasource
3
+ * contract for grids whose data lives on the server - the "I have a million
4
+ * rows in a database" case. The consumer implements ONE async `getRows`
5
+ * function; this controller owns the request lifecycle (sort, filter, page),
6
+ * de-dupes/races, and pushes results back through `onChange`.
7
+ *
8
+ * It's headless and framework-agnostic on purpose: wire `setSort` /
9
+ * `setFilter` / `setPage` to the grid's controlled callbacks, and render the
10
+ * grid from the `{ rows, total, loading }` it hands you. See the demo.
11
+ */
12
+ export type ServerSortModel = Array<{
13
+ id: string;
14
+ desc: boolean;
15
+ }>;
16
+ export type ServerFilterModel = {
17
+ /** Free-text global search. */
18
+ global?: string;
19
+ /**
20
+ * Per-column filters, keyed by column id. `value` (+ `valueTo`) carry the
21
+ * operator-style filter; `selectedValues` carries a facet/checklist
22
+ * selection (set-filter). Either or both may be present.
23
+ */
24
+ columns?: Record<string, {
25
+ operator: string;
26
+ value: string;
27
+ valueTo?: string;
28
+ selectedValues?: string[];
29
+ }>;
30
+ };
31
+ export type ServerRequest = {
32
+ /** Zero-based index of the first row wanted (inclusive). */
33
+ startRow: number;
34
+ /** Index just past the last row wanted (exclusive). */
35
+ endRow: number;
36
+ pageIndex: number;
37
+ pageSize: number;
38
+ sortModel: ServerSortModel;
39
+ filterModel: ServerFilterModel;
40
+ };
41
+ export type ServerResult<TData> = {
42
+ rows: ReadonlyArray<TData>;
43
+ /** Total row count after filtering (for the pager). */
44
+ rowCount: number;
45
+ };
46
+ export type ServerDataSource<TData> = {
47
+ getRows(request: ServerRequest): Promise<ServerResult<TData>>;
48
+ };
49
+ export type ServerState<TData> = {
50
+ rows: ReadonlyArray<TData>;
51
+ total: number;
52
+ loading: boolean;
53
+ error: unknown;
54
+ pageIndex: number;
55
+ pageSize: number;
56
+ pageCount: number;
57
+ sortModel: ServerSortModel;
58
+ filterModel: ServerFilterModel;
59
+ };
60
+ export type ServerController<TData> = {
61
+ /** Re-fetch the current page (e.g. after a mutation). */
62
+ refresh(): void;
63
+ setSort(sortModel: ServerSortModel): void;
64
+ setFilter(filterModel: ServerFilterModel): void;
65
+ setPage(pageIndex: number): void;
66
+ setPageSize(pageSize: number): void;
67
+ getState(): ServerState<TData>;
68
+ /** Stop accepting in-flight responses (call on unmount). */
69
+ dispose(): void;
70
+ };
71
+ export type ServerControllerOptions<TData> = {
72
+ pageSize?: number;
73
+ /** Called whenever any of `rows` / `total` / `loading` / page changes. */
74
+ onChange: (state: ServerState<TData>) => void;
75
+ };
76
+ export declare function createServerDataSource<TData>(source: ServerDataSource<TData>, options: ServerControllerOptions<TData>): ServerController<TData>;
@@ -0,0 +1,83 @@
1
+ export function createServerDataSource(source, options) {
2
+ const state = {
3
+ rows: [],
4
+ total: 0,
5
+ loading: false,
6
+ error: null,
7
+ pageIndex: 0,
8
+ pageSize: options.pageSize ?? 50,
9
+ pageCount: 1,
10
+ sortModel: [],
11
+ filterModel: {},
12
+ };
13
+ // Monotonic request id: only the latest fetch is allowed to land, so a slow
14
+ // response for an old sort/filter can't clobber a newer one.
15
+ let requestSeq = 0;
16
+ let disposed = false;
17
+ const emit = () => {
18
+ state.pageCount = Math.max(1, Math.ceil(state.total / state.pageSize));
19
+ options.onChange({ ...state });
20
+ };
21
+ async function fetchPage() {
22
+ if (disposed)
23
+ return;
24
+ const id = ++requestSeq;
25
+ state.loading = true;
26
+ state.error = null;
27
+ emit();
28
+ const startRow = state.pageIndex * state.pageSize;
29
+ try {
30
+ const result = await source.getRows({
31
+ startRow,
32
+ endRow: startRow + state.pageSize,
33
+ pageIndex: state.pageIndex,
34
+ pageSize: state.pageSize,
35
+ sortModel: state.sortModel,
36
+ filterModel: state.filterModel,
37
+ });
38
+ if (disposed || id !== requestSeq)
39
+ return; // stale
40
+ state.rows = result.rows;
41
+ state.total = result.rowCount;
42
+ state.loading = false;
43
+ emit();
44
+ }
45
+ catch (err) {
46
+ if (disposed || id !== requestSeq)
47
+ return;
48
+ state.rows = [];
49
+ state.error = err;
50
+ state.loading = false;
51
+ emit();
52
+ }
53
+ }
54
+ return {
55
+ refresh: fetchPage,
56
+ setSort(sortModel) {
57
+ state.sortModel = sortModel;
58
+ state.pageIndex = 0;
59
+ void fetchPage();
60
+ },
61
+ setFilter(filterModel) {
62
+ state.filterModel = filterModel;
63
+ state.pageIndex = 0;
64
+ void fetchPage();
65
+ },
66
+ setPage(pageIndex) {
67
+ const clamped = Math.max(0, pageIndex);
68
+ if (clamped === state.pageIndex)
69
+ return;
70
+ state.pageIndex = clamped;
71
+ void fetchPage();
72
+ },
73
+ setPageSize(pageSize) {
74
+ state.pageSize = Math.max(1, pageSize);
75
+ state.pageIndex = 0;
76
+ void fetchPage();
77
+ },
78
+ getState: () => ({ ...state }),
79
+ dispose() {
80
+ disposed = true;
81
+ },
82
+ };
83
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createServerDataSource } from './server-data-source';
3
+ function makeSource(total = 250, delay = 0) {
4
+ const all = Array.from({ length: total }, (_, i) => ({ id: i }));
5
+ return {
6
+ calls: 0,
7
+ async getRows(req) {
8
+ this.calls += 1;
9
+ if (delay)
10
+ await new Promise((r) => setTimeout(r, delay));
11
+ // server applies sort: reverse when desc on 'id'
12
+ let rows = all;
13
+ if (req.sortModel[0]?.id === 'id' && req.sortModel[0]?.desc)
14
+ rows = [...all].reverse();
15
+ return { rows: rows.slice(req.startRow, req.endRow), rowCount: total };
16
+ },
17
+ };
18
+ }
19
+ const flush = () => new Promise((r) => setTimeout(r, 0));
20
+ describe('createServerDataSource', () => {
21
+ it('fetches a page and reports total + pageCount', async () => {
22
+ const src = makeSource(250);
23
+ const states = [];
24
+ const ctl = createServerDataSource(src, { pageSize: 50, onChange: (s) => states.push(s) });
25
+ ctl.refresh();
26
+ await flush();
27
+ const last = ctl.getState();
28
+ expect(last.rows).toHaveLength(50);
29
+ expect(last.total).toBe(250);
30
+ expect(last.pageCount).toBe(5);
31
+ expect(last.loading).toBe(false);
32
+ // emitted a loading=true then loading=false
33
+ expect(states.some((s) => s.loading)).toBe(true);
34
+ });
35
+ it('setPage fetches the right slice', async () => {
36
+ const src = makeSource(250);
37
+ const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => { } });
38
+ ctl.setPage(2);
39
+ await flush();
40
+ expect(ctl.getState().rows[0]).toEqual({ id: 100 });
41
+ });
42
+ it('setSort resets to page 0 and re-fetches', async () => {
43
+ const src = makeSource(250);
44
+ const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => { } });
45
+ ctl.setPage(2);
46
+ await flush();
47
+ ctl.setSort([{ id: 'id', desc: true }]);
48
+ await flush();
49
+ const s = ctl.getState();
50
+ expect(s.pageIndex).toBe(0);
51
+ expect(s.rows[0]).toEqual({ id: 249 }); // reversed
52
+ });
53
+ it('ignores a stale response when a newer request supersedes it', async () => {
54
+ // slow source: first call (page 0) resolves AFTER the second (page 1)
55
+ const all = Array.from({ length: 100 }, (_, i) => ({ id: i }));
56
+ let call = 0;
57
+ const src = {
58
+ getRows: vi.fn(async (req) => {
59
+ call += 1;
60
+ const delay = call === 1 ? 40 : 5; // first call is slow
61
+ await new Promise((r) => setTimeout(r, delay));
62
+ return { rows: all.slice(req.startRow, req.endRow), rowCount: 100 };
63
+ }),
64
+ };
65
+ const ctl = createServerDataSource(src, { pageSize: 10, onChange: () => { } });
66
+ ctl.refresh(); // page 0 (slow)
67
+ ctl.setPage(3); // page 30 (fast) - should win
68
+ await new Promise((r) => setTimeout(r, 80));
69
+ expect(ctl.getState().rows[0]).toEqual({ id: 30 });
70
+ });
71
+ it('dispose stops further updates', async () => {
72
+ const src = makeSource(50, 20);
73
+ const onChange = vi.fn();
74
+ const ctl = createServerDataSource(src, { pageSize: 10, onChange });
75
+ ctl.refresh();
76
+ ctl.dispose();
77
+ await new Promise((r) => setTimeout(r, 40));
78
+ // only the synchronous loading=true emit happened before dispose
79
+ expect(ctl.getState().rows).toHaveLength(0);
80
+ });
81
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Sparkline geometry. Pure functions that turn an array of numbers into the
3
+ * SVG primitives a tiny in-cell chart needs - no DOM, so it is trivially
4
+ * unit-testable and the `<SvGrid>` render component just paints the result.
5
+ *
6
+ * Four chart types, mirroring the de-facto enterprise-grid set:
7
+ * - `line` a single polyline
8
+ * - `area` a polyline plus a filled area down to the baseline
9
+ * - `bar` one column per value, scaled from the dataset min..max
10
+ * - `winloss` fixed-height up/down bars (sign only), e.g. W/L streaks
11
+ */
12
+ export type SparklineType = 'line' | 'area' | 'bar' | 'winloss';
13
+ export type SparklineConfig = {
14
+ type?: SparklineType;
15
+ /** Stroke (line/area) or positive fill (bar/winloss). */
16
+ color?: string;
17
+ /** Fill for negative bars / losses. Defaults to a red. */
18
+ negativeColor?: string;
19
+ width?: number;
20
+ height?: number;
21
+ /** Fix the value scale instead of deriving it from the row's own data. */
22
+ min?: number;
23
+ max?: number;
24
+ lineWidth?: number;
25
+ /** Draw a dot on the last point (line/area). Default true. */
26
+ lastPoint?: boolean;
27
+ };
28
+ export type SparklineBar = {
29
+ x: number;
30
+ y: number;
31
+ w: number;
32
+ h: number;
33
+ negative: boolean;
34
+ };
35
+ export type SparklineGeometry = {
36
+ width: number;
37
+ height: number;
38
+ type: SparklineType;
39
+ color: string;
40
+ negativeColor: string;
41
+ lineWidth: number;
42
+ /** `line` / `area`: the polyline through every point. */
43
+ linePath: string;
44
+ /** `area`: closed path filled down to the baseline. */
45
+ areaPath: string;
46
+ /** `bar` / `winloss`: one rect per value. */
47
+ bars: SparklineBar[];
48
+ /** `line` / `area`: the final point, for the end-cap dot. */
49
+ lastPoint: {
50
+ x: number;
51
+ y: number;
52
+ } | null;
53
+ };
54
+ /** Coerce loose cell values (arrays, comma strings) into a number array. */
55
+ export declare function toSparklineValues(value: unknown): number[];
56
+ export declare function buildSparkline(values: number[], cfg?: SparklineConfig): SparklineGeometry | null;
@@ -0,0 +1,97 @@
1
+ const DEFAULTS = {
2
+ width: 88,
3
+ height: 22,
4
+ color: 'var(--sg-accent, #2563eb)',
5
+ negativeColor: '#ef4444',
6
+ lineWidth: 1.5,
7
+ };
8
+ /** Coerce loose cell values (arrays, comma strings) into a number array. */
9
+ export function toSparklineValues(value) {
10
+ if (Array.isArray(value)) {
11
+ return value.map((v) => Number(v)).filter((n) => Number.isFinite(n));
12
+ }
13
+ if (typeof value === 'string' && value.trim()) {
14
+ return value
15
+ .split(/[,\s]+/)
16
+ .map((v) => Number(v))
17
+ .filter((n) => Number.isFinite(n));
18
+ }
19
+ return [];
20
+ }
21
+ export function buildSparkline(values, cfg = {}) {
22
+ if (!values.length)
23
+ return null;
24
+ const width = cfg.width ?? DEFAULTS.width;
25
+ const height = cfg.height ?? DEFAULTS.height;
26
+ const type = cfg.type ?? 'line';
27
+ const color = cfg.color ?? DEFAULTS.color;
28
+ const negativeColor = cfg.negativeColor ?? DEFAULTS.negativeColor;
29
+ const lineWidth = cfg.lineWidth ?? DEFAULTS.lineWidth;
30
+ const pad = Math.ceil(lineWidth) + 1;
31
+ const innerW = Math.max(1, width - pad * 2);
32
+ const innerH = Math.max(1, height - pad * 2);
33
+ const base = { width, height, type, color, negativeColor, lineWidth };
34
+ if (type === 'winloss') {
35
+ const n = values.length;
36
+ const gap = n > 1 ? Math.min(2, innerW / n / 3) : 0;
37
+ const slot = innerW / n;
38
+ const barW = Math.max(1, slot - gap);
39
+ const mid = height / 2;
40
+ const h = innerH / 2;
41
+ const bars = values.map((v, i) => {
42
+ const x = pad + i * slot + (slot - barW) / 2;
43
+ if (v > 0)
44
+ return { x, y: mid - h, w: barW, h, negative: false };
45
+ if (v < 0)
46
+ return { x, y: mid, w: barW, h, negative: true };
47
+ return { x, y: mid - 1, w: barW, h: 2, negative: false };
48
+ });
49
+ return { ...base, linePath: '', areaPath: '', bars, lastPoint: null };
50
+ }
51
+ const min = cfg.min ?? Math.min(...values);
52
+ const max = cfg.max ?? Math.max(...values);
53
+ const span = max - min || 1;
54
+ if (type === 'bar') {
55
+ const n = values.length;
56
+ const gap = n > 1 ? Math.min(2, innerW / n / 3) : 0;
57
+ const slot = innerW / n;
58
+ const barW = Math.max(1, slot - gap);
59
+ // Bars grow from a zero baseline when the data spans zero, else from min.
60
+ const baseVal = min < 0 && max > 0 ? 0 : min;
61
+ const baseY = pad + innerH - ((baseVal - min) / span) * innerH;
62
+ const bars = values.map((v, i) => {
63
+ const x = pad + i * slot + (slot - barW) / 2;
64
+ const vy = pad + innerH - ((v - min) / span) * innerH;
65
+ const top = Math.min(vy, baseY);
66
+ const h = Math.max(1, Math.abs(vy - baseY));
67
+ return { x, y: top, w: barW, h, negative: v < baseVal };
68
+ });
69
+ return { ...base, linePath: '', areaPath: '', bars, lastPoint: null };
70
+ }
71
+ // line / area
72
+ const n = values.length;
73
+ const stepX = n > 1 ? innerW / (n - 1) : 0;
74
+ const pts = values.map((v, i) => ({
75
+ x: pad + i * stepX,
76
+ y: pad + innerH - ((v - min) / span) * innerH,
77
+ }));
78
+ const linePath = pts
79
+ .map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`)
80
+ .join(' ');
81
+ const first = pts[0];
82
+ const last = pts[pts.length - 1];
83
+ const baseY = pad + innerH;
84
+ const areaPath = type === 'area'
85
+ ? `${linePath} L${round(last.x)},${round(baseY)} L${round(first.x)},${round(baseY)} Z`
86
+ : '';
87
+ return {
88
+ ...base,
89
+ linePath,
90
+ areaPath,
91
+ bars: [],
92
+ lastPoint: cfg.lastPoint === false ? null : { x: last.x, y: last.y },
93
+ };
94
+ }
95
+ function round(n) {
96
+ return Math.round(n * 100) / 100;
97
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildSparkline, toSparklineValues } from './sparkline';
3
+ describe('toSparklineValues', () => {
4
+ it('passes through number arrays and drops non-finite', () => {
5
+ expect(toSparklineValues([1, 2, NaN, 3])).toEqual([1, 2, 3]);
6
+ });
7
+ it('parses comma / space separated strings', () => {
8
+ expect(toSparklineValues('1, 2 3,4')).toEqual([1, 2, 3, 4]);
9
+ });
10
+ it('returns [] for empty / unusable input', () => {
11
+ expect(toSparklineValues(null)).toEqual([]);
12
+ expect(toSparklineValues('')).toEqual([]);
13
+ expect(toSparklineValues({})).toEqual([]);
14
+ });
15
+ });
16
+ describe('buildSparkline', () => {
17
+ it('returns null for empty data', () => {
18
+ expect(buildSparkline([])).toBeNull();
19
+ });
20
+ it('builds a line path through every point', () => {
21
+ const g = buildSparkline([0, 5, 10], { type: 'line', width: 100, height: 20 });
22
+ expect(g.type).toBe('line');
23
+ expect(g.linePath.startsWith('M')).toBe(true);
24
+ // three points => one M + two L commands
25
+ expect((g.linePath.match(/[ML]/g) ?? []).length).toBe(3);
26
+ expect(g.lastPoint).not.toBeNull();
27
+ });
28
+ it('closes the area path back to the baseline', () => {
29
+ const g = buildSparkline([1, 2, 3], { type: 'area' });
30
+ expect(g.areaPath.endsWith('Z')).toBe(true);
31
+ });
32
+ it('emits one bar per value and flags negatives', () => {
33
+ const g = buildSparkline([-2, 4, -1], { type: 'bar' });
34
+ expect(g.bars).toHaveLength(3);
35
+ expect(g.bars[0].negative).toBe(true);
36
+ expect(g.bars[1].negative).toBe(false);
37
+ });
38
+ it('winloss uses sign only, equal-height up/down bars', () => {
39
+ const g = buildSparkline([1, -1, 1], { type: 'winloss', height: 20 });
40
+ expect(g.bars).toHaveLength(3);
41
+ expect(g.bars[0].negative).toBe(false);
42
+ expect(g.bars[1].negative).toBe(true);
43
+ // up and down bars are the same height (sign-only chart)
44
+ expect(g.bars[0].h).toBeCloseTo(g.bars[1].h);
45
+ });
46
+ it('honors a fixed min/max scale', () => {
47
+ const a = buildSparkline([5, 5, 5], { type: 'line', min: 0, max: 10 });
48
+ // flat series on a 0..10 scale sits at the vertical middle, not the top
49
+ const ys = a.linePath.match(/,(-?\d+(\.\d+)?)/g).map((s) => Number(s.slice(1)));
50
+ expect(new Set(ys).size).toBe(1);
51
+ });
52
+ });
@@ -0,0 +1 @@
1
+ export { createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, filterFns, sortFns, tableFeatures, } from './core';
@@ -0,0 +1 @@
1
+ export { createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, filterFns, sortFns, tableFeatures, } from './core';
@@ -0,0 +1,9 @@
1
+ import type { RowData, SvGrid, TableFeatures } from './core';
2
+ /**
3
+ * Fine-grained subscription to `table.store` using `useSelector` with shallow
4
+ * comparison. Call from `<script>` so the selector is contextually typed.
5
+ */
6
+ export declare function subscribeGrid<TFeatures extends TableFeatures, TData extends RowData, TSelected>(grid: SvGrid<TData>, selector: (state: Record<string, any>) => TSelected): {
7
+ readonly current: TSelected;
8
+ };
9
+ export declare const subscribeSvGrid: typeof subscribeGrid;
@@ -0,0 +1,31 @@
1
+ const shallowCompare = (a, b) => {
2
+ if (Object.is(a, b))
3
+ return true;
4
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
5
+ return false;
6
+ const aObj = a;
7
+ const bObj = b;
8
+ const aKeys = Object.keys(aObj);
9
+ const bKeys = Object.keys(bObj);
10
+ if (aKeys.length !== bKeys.length)
11
+ return false;
12
+ return aKeys.every((key) => Object.is(aObj[key], bObj[key]));
13
+ };
14
+ /**
15
+ * Fine-grained subscription to `table.store` using `useSelector` with shallow
16
+ * comparison. Call from `<script>` so the selector is contextually typed.
17
+ */
18
+ export function subscribeGrid(grid, selector) {
19
+ let current = selector(grid.store.state);
20
+ grid.store.subscribe(() => {
21
+ const next = selector(grid.store.state);
22
+ if (!shallowCompare(current, next))
23
+ current = next;
24
+ });
25
+ return {
26
+ get current() {
27
+ return current;
28
+ },
29
+ };
30
+ }
31
+ export const subscribeSvGrid = subscribeGrid;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Unit tests for the grid-store subscribe helper.
3
+ *
4
+ * The function attaches a selector to grid.store and shallow-compares each
5
+ * emitted state, updating the returned `current` getter only when the
6
+ * selection actually changed.
7
+ */
8
+ import { describe, expect, it } from 'vitest';
9
+ import { subscribeGrid, subscribeSvGrid } from './subscribe';
10
+ /**
11
+ * Minimal hand-rolled stand-in for `grid.store` that mimics the TanStack
12
+ * Store shape `subscribeGrid` reads: `state` getter + `subscribe(listener)`
13
+ * + `setState(updater)` that fires the listeners.
14
+ */
15
+ function makeFakeStore(initial) {
16
+ let state = initial;
17
+ const listeners = new Set();
18
+ const store = {
19
+ get state() {
20
+ return state;
21
+ },
22
+ subscribe(listener) {
23
+ listeners.add(listener);
24
+ return () => listeners.delete(listener);
25
+ },
26
+ setState(updater) {
27
+ state = updater(state);
28
+ for (const l of listeners)
29
+ l();
30
+ },
31
+ };
32
+ return store;
33
+ }
34
+ describe('subscribeGrid', () => {
35
+ it('starts with the current selected value', () => {
36
+ const store = makeFakeStore({ sorting: ['name'], rowSelection: { '0': true } });
37
+ const grid = { store };
38
+ const sub = subscribeGrid(grid, (s) => s.sorting);
39
+ expect(sub.current).toEqual(['name']);
40
+ });
41
+ it('updates the selected value when the store fires a change', () => {
42
+ const store = makeFakeStore({ sorting: ['name'] });
43
+ const grid = { store };
44
+ const sub = subscribeGrid(grid, (s) => s.sorting);
45
+ store.setState((prev) => ({ ...prev, sorting: ['age'] }));
46
+ expect(sub.current).toEqual(['age']);
47
+ });
48
+ it('keeps the previous reference when shallow-compare reports equal', () => {
49
+ const store = makeFakeStore({ sel: { a: true } });
50
+ const grid = { store };
51
+ const sub = subscribeGrid(grid, (s) => s.sel);
52
+ const initial = sub.current;
53
+ // A new object with the same key/value content - shallow-equal to the prev.
54
+ store.setState(() => ({ sel: { a: true } }));
55
+ expect(sub.current).toBe(initial);
56
+ });
57
+ it('replaces the reference when a key value changes', () => {
58
+ const store = makeFakeStore({ sel: { a: true } });
59
+ const grid = { store };
60
+ const sub = subscribeGrid(grid, (s) => s.sel);
61
+ const initial = sub.current;
62
+ store.setState(() => ({ sel: { a: false } }));
63
+ expect(sub.current).not.toBe(initial);
64
+ expect(sub.current).toEqual({ a: false });
65
+ });
66
+ it('treats a different key set as a change', () => {
67
+ const store = makeFakeStore({ sel: { a: true } });
68
+ const grid = { store };
69
+ const sub = subscribeGrid(grid, (s) => s.sel);
70
+ store.setState(() => ({ sel: { a: true, b: true } }));
71
+ expect(sub.current).toEqual({ a: true, b: true });
72
+ });
73
+ it('treats primitive selections (numbers, strings) correctly', () => {
74
+ const store = makeFakeStore({ count: 1 });
75
+ const grid = { store };
76
+ const sub = subscribeGrid(grid, (s) => s.count);
77
+ expect(sub.current).toBe(1);
78
+ store.setState(() => ({ count: 1 }));
79
+ expect(sub.current).toBe(1);
80
+ store.setState(() => ({ count: 2 }));
81
+ expect(sub.current).toBe(2);
82
+ });
83
+ it('treats null and undefined as inequal to objects (early-return shallowCompare branch)', () => {
84
+ const store = makeFakeStore({ sel: { a: true } });
85
+ const grid = { store };
86
+ const sub = subscribeGrid(grid, (s) => s.sel);
87
+ store.setState(() => ({ sel: null }));
88
+ expect(sub.current).toBeNull();
89
+ });
90
+ it('exports the subscribeSvGrid alias as the same function', () => {
91
+ expect(subscribeSvGrid).toBe(subscribeGrid);
92
+ });
93
+ });
@@ -0,0 +1 @@
1
+ export {};