@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,40 @@
1
+ /**
2
+ * Internal dropdown / listbox control used by the `list` and `chips` cell
3
+ * editors. We deliberately don't reuse the browser-native `<select>` -
4
+ * the native dropdown can't be styled to match the grid theme, picks up
5
+ * the OS color scheme, and on Windows draws the option list with an
6
+ * opaque white system surface that fights the dark theme.
7
+ *
8
+ * Modes:
9
+ * single - picking an option fires `onChange(value)` and `onCommit()`.
10
+ * multi - toggling fires `onChange(values: array)`; the user
11
+ * presses Enter / clicks "Done" to fire `onCommit()`.
12
+ *
13
+ * Keyboard model:
14
+ * ArrowDown / ArrowUp - move highlight (opens the panel if closed).
15
+ * Enter - single: toggle highlighted + commit.
16
+ * multi: toggle highlighted (stay open).
17
+ * Escape - close + onCancel.
18
+ * Tab - commit and let focus escape.
19
+ */
20
+ import type { CellEditorOption } from './editors/cell-editors';
21
+ type Props = {
22
+ options: ReadonlyArray<CellEditorOption>;
23
+ value: unknown;
24
+ multiple?: boolean;
25
+ placeholder?: string;
26
+ /** When provided, the trigger renders as removable chips of the current value(s). */
27
+ renderChipsInTrigger?: boolean;
28
+ /** Add a typeahead input at the top of the popover that filters the
29
+ * option list as the user types. Used by the rich-select editor. */
30
+ searchable?: boolean;
31
+ /** Called with the new full value (scalar for single, array for multi). */
32
+ onChange?: (next: unknown) => void;
33
+ /** Called when the user finalizes the selection (single pick, Enter, blur with selection). */
34
+ onCommit?: () => void;
35
+ /** Called when the user dismisses (Escape, blur with no change). */
36
+ onCancel?: () => void;
37
+ };
38
+ declare const SvGridDropdown: import("svelte").Component<Props, {}, "">;
39
+ type SvGridDropdown = ReturnType<typeof SvGridDropdown>;
40
+ export default SvGridDropdown;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y';
3
+ function attrsToHtml(attrs) {
4
+ return Object.entries(attrs)
5
+ .map(([key, value]) => `${key}="${String(value)}"`)
6
+ .join(' ');
7
+ }
8
+ describe('a11y contract rendering', () => {
9
+ it('builds expected headless markup attributes', () => {
10
+ const root = getGridRootA11yProps();
11
+ const header = getGridHeaderA11yProps({
12
+ sortable: true,
13
+ sortDirection: 'descending',
14
+ });
15
+ const row = getGridRowA11yProps(1);
16
+ const cell = getGridCellA11yProps({
17
+ rowIndex: 1,
18
+ colIndex: 2,
19
+ selected: true,
20
+ });
21
+ const html = [
22
+ `<table ${attrsToHtml(root)}><thead>`,
23
+ `<tr ${attrsToHtml(row)}>`,
24
+ `<th ${attrsToHtml(header)}>Age</th>`,
25
+ `</tr></thead><tbody>`,
26
+ `<tr ${attrsToHtml(row)}><td ${attrsToHtml(cell)}>42</td></tr>`,
27
+ `</tbody></table>`,
28
+ ].join('');
29
+ expect(html).toContain('role="grid"');
30
+ expect(html).toContain('role="columnheader"');
31
+ expect(html).toContain('aria-sort="descending"');
32
+ expect(html).toContain('aria-selected="true"');
33
+ expect(html).toContain('aria-colindex="2"');
34
+ });
35
+ it('creates stable active descendant ids', async () => {
36
+ const { getGridCellDomId } = await import('./a11y');
37
+ expect(getGridCellDomId('svgrid', 3, 4)).toBe('svgrid_cell_3_4');
38
+ });
39
+ });
package/dist/a11y.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ export type GridSortDirection = 'ascending' | 'descending' | 'none' | 'other';
2
+ export type GridColumnA11yInput = {
3
+ sortable?: boolean;
4
+ sortDirection?: GridSortDirection;
5
+ };
6
+ export type GridCellA11yInput = {
7
+ colIndex?: number;
8
+ rowIndex?: number;
9
+ selected?: boolean;
10
+ id?: string;
11
+ };
12
+ export declare function getGridRootA11yProps(input?: {
13
+ activeDescendantId?: string | null;
14
+ rowCount?: number;
15
+ colCount?: number;
16
+ }): {
17
+ readonly 'aria-colcount'?: number | undefined;
18
+ readonly 'aria-rowcount'?: number | undefined;
19
+ readonly 'aria-activedescendant'?: string | undefined;
20
+ readonly role: "grid";
21
+ readonly tabindex: 0;
22
+ };
23
+ export declare function getGridHeaderA11yProps(input?: GridColumnA11yInput): {
24
+ readonly 'aria-sort'?: GridSortDirection | undefined;
25
+ readonly role: "columnheader";
26
+ };
27
+ export declare function getGridRowA11yProps(rowIndex?: number): {
28
+ readonly 'aria-rowindex'?: number | undefined;
29
+ readonly role: "row";
30
+ };
31
+ export declare function getGridCellA11yProps(input?: GridCellA11yInput): {
32
+ readonly 'aria-selected'?: boolean | undefined;
33
+ readonly 'aria-rowindex'?: number | undefined;
34
+ readonly 'aria-colindex'?: number | undefined;
35
+ readonly id?: string | undefined;
36
+ readonly role: "gridcell";
37
+ };
38
+ export declare function getGridCellDomId(baseId: string, rowIndex: number, colIndex: number): string;
package/dist/a11y.js ADDED
@@ -0,0 +1,37 @@
1
+ export function getGridRootA11yProps(input = {}) {
2
+ return {
3
+ role: 'grid',
4
+ tabindex: 0,
5
+ ...(input.activeDescendantId
6
+ ? { 'aria-activedescendant': input.activeDescendantId }
7
+ : {}),
8
+ ...(input.rowCount !== undefined ? { 'aria-rowcount': input.rowCount } : {}),
9
+ ...(input.colCount !== undefined ? { 'aria-colcount': input.colCount } : {}),
10
+ };
11
+ }
12
+ export function getGridHeaderA11yProps(input = {}) {
13
+ const { sortable = false, sortDirection = 'none' } = input;
14
+ return {
15
+ role: 'columnheader',
16
+ ...(sortable ? { 'aria-sort': sortDirection } : {}),
17
+ };
18
+ }
19
+ export function getGridRowA11yProps(rowIndex) {
20
+ return {
21
+ role: 'row',
22
+ ...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
23
+ };
24
+ }
25
+ export function getGridCellA11yProps(input = {}) {
26
+ const { colIndex, rowIndex, selected = false, id } = input;
27
+ return {
28
+ role: 'gridcell',
29
+ ...(id ? { id } : {}),
30
+ ...(colIndex !== undefined ? { 'aria-colindex': colIndex } : {}),
31
+ ...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
32
+ ...(selected ? { 'aria-selected': true } : {}),
33
+ };
34
+ }
35
+ export function getGridCellDomId(baseId, rowIndex, colIndex) {
36
+ return `${baseId}_cell_${rowIndex}_${colIndex}`;
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y';
3
+ describe('a11y helpers', () => {
4
+ it('returns grid role props', () => {
5
+ expect(getGridRootA11yProps({
6
+ activeDescendantId: 'svgrid_cell_0_0',
7
+ rowCount: 10,
8
+ colCount: 5,
9
+ })).toEqual({
10
+ role: 'grid',
11
+ tabindex: 0,
12
+ 'aria-activedescendant': 'svgrid_cell_0_0',
13
+ 'aria-rowcount': 10,
14
+ 'aria-colcount': 5,
15
+ });
16
+ });
17
+ it('returns header sort semantics', () => {
18
+ expect(getGridHeaderA11yProps({
19
+ sortable: true,
20
+ sortDirection: 'ascending',
21
+ })).toEqual({
22
+ role: 'columnheader',
23
+ 'aria-sort': 'ascending',
24
+ });
25
+ });
26
+ it('returns row and cell indices for accessibility metadata', () => {
27
+ expect(getGridRowA11yProps(2)).toEqual({
28
+ role: 'row',
29
+ 'aria-rowindex': 2,
30
+ });
31
+ expect(getGridCellA11yProps({
32
+ id: 'svgrid_cell_2_3',
33
+ rowIndex: 2,
34
+ colIndex: 3,
35
+ selected: true,
36
+ })).toEqual({
37
+ role: 'gridcell',
38
+ id: 'svgrid_cell_2_3',
39
+ 'aria-colindex': 3,
40
+ 'aria-rowindex': 2,
41
+ 'aria-selected': true,
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Helpers for SvGrid column `format` presets (numbers, currency, percent, date patterns).
3
+ */
4
+ /** Map shortcut date patterns to Intl options (merged with caller `options`). */
5
+ export declare function resolveDatePattern(pattern: string | undefined, kind: 'date' | 'datetime'): Intl.DateTimeFormatOptions | undefined;
6
+ type NumericFormatInput = {
7
+ type: 'number' | 'currency' | 'percent';
8
+ locales?: string | string[];
9
+ currency?: string;
10
+ /** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
11
+ valueIsPercentPoints?: boolean;
12
+ options?: Intl.NumberFormatOptions;
13
+ };
14
+ export declare function getDateFormatter(locales: string | string[] | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
15
+ /** Format a numeric value for number / currency / percent column formats. */
16
+ export declare function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string;
17
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Helpers for SvGrid column `format` presets (numbers, currency, percent, date patterns).
3
+ */
4
+ /** Map shortcut date patterns to Intl options (merged with caller `options`). */
5
+ export function resolveDatePattern(pattern, kind) {
6
+ if (!pattern)
7
+ return undefined;
8
+ const p = pattern.trim();
9
+ switch (p) {
10
+ /** Short numeric local date (no weekday). */
11
+ case 'd':
12
+ return kind === 'datetime'
13
+ ? {
14
+ year: 'numeric',
15
+ month: 'numeric',
16
+ day: 'numeric',
17
+ hour: 'numeric',
18
+ minute: 'numeric',
19
+ }
20
+ : { year: 'numeric', month: 'numeric', day: 'numeric' };
21
+ /** Long spelled-out date (+ time if datetime). */
22
+ case 'D':
23
+ return kind === 'datetime'
24
+ ? {
25
+ weekday: 'long',
26
+ year: 'numeric',
27
+ month: 'long',
28
+ day: 'numeric',
29
+ hour: 'numeric',
30
+ minute: 'numeric',
31
+ }
32
+ : { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
33
+ /** ISO-friendly yyyy-mm-dd (with time when datetime). */
34
+ case 'y-m-d':
35
+ return kind === 'datetime'
36
+ ? {
37
+ year: 'numeric',
38
+ month: '2-digit',
39
+ day: '2-digit',
40
+ hour: '2-digit',
41
+ minute: '2-digit',
42
+ hour12: false,
43
+ }
44
+ : { year: 'numeric', month: '2-digit', day: '2-digit' };
45
+ case 'medium':
46
+ return { dateStyle: 'medium', timeStyle: kind === 'datetime' ? 'short' : undefined };
47
+ case 'short':
48
+ return { dateStyle: 'short', timeStyle: kind === 'datetime' ? 'short' : undefined };
49
+ case 'long':
50
+ return { dateStyle: 'long', timeStyle: kind === 'datetime' ? 'short' : undefined };
51
+ default:
52
+ return undefined;
53
+ }
54
+ }
55
+ /**
56
+ * `Intl.NumberFormat` is one of the most expensive constructors in the
57
+ * platform - building one per cell on every scroll makes large grids
58
+ * noticeably stutter. Cache instances by config signature (locale + style
59
+ * + currency + options).
60
+ */
61
+ const numberFormatterCache = new Map();
62
+ function getNumberFormatter(locales, options) {
63
+ // Stable key - for normal column configs the options object is reused
64
+ // (declared in module scope), so this stringifies a small static shape.
65
+ const key = (Array.isArray(locales) ? locales.join(',') : locales ?? '') +
66
+ '|' +
67
+ (options.style ?? '') +
68
+ '|' +
69
+ (options.currency ?? '') +
70
+ '|' +
71
+ (options.minimumFractionDigits ?? '') +
72
+ '|' +
73
+ (options.maximumFractionDigits ?? '') +
74
+ '|' +
75
+ (options.useGrouping ?? '') +
76
+ '|' +
77
+ (options.currencyDisplay ?? '') +
78
+ '|' +
79
+ (options.notation ?? '');
80
+ let fmt = numberFormatterCache.get(key);
81
+ if (!fmt) {
82
+ fmt = new Intl.NumberFormat(locales, options);
83
+ numberFormatterCache.set(key, fmt);
84
+ }
85
+ return fmt;
86
+ }
87
+ /** Cache of `Intl.DateTimeFormat` by `(locale, options)` signature. */
88
+ const dateFormatterCache = new Map();
89
+ export function getDateFormatter(locales, options) {
90
+ const key = (Array.isArray(locales) ? locales.join(',') : locales ?? '') +
91
+ '|' +
92
+ (options.dateStyle ?? '') +
93
+ '|' +
94
+ (options.timeStyle ?? '') +
95
+ '|' +
96
+ (options.year ?? '') +
97
+ '|' +
98
+ (options.month ?? '') +
99
+ '|' +
100
+ (options.day ?? '') +
101
+ '|' +
102
+ (options.hour ?? '') +
103
+ '|' +
104
+ (options.minute ?? '') +
105
+ '|' +
106
+ (options.second ?? '') +
107
+ '|' +
108
+ (options.weekday ?? '') +
109
+ '|' +
110
+ (options.hour12 ?? '');
111
+ let fmt = dateFormatterCache.get(key);
112
+ if (!fmt) {
113
+ fmt = new Intl.DateTimeFormat(locales, options);
114
+ dateFormatterCache.set(key, fmt);
115
+ }
116
+ return fmt;
117
+ }
118
+ /** Format a numeric value for number / currency / percent column formats. */
119
+ export function formatNumericWithConfig(value, config) {
120
+ const n = typeof value === 'number' ? value : Number(value);
121
+ if (!Number.isFinite(n))
122
+ return String(value ?? '');
123
+ const locales = config.locales;
124
+ if (config.type === 'currency') {
125
+ return getNumberFormatter(locales, {
126
+ style: 'currency',
127
+ currency: config.currency ?? 'USD',
128
+ ...config.options,
129
+ }).format(n);
130
+ }
131
+ if (config.type === 'percent') {
132
+ const frac = config.valueIsPercentPoints === true ? n / 100 : n;
133
+ return getNumberFormatter(locales, {
134
+ style: 'percent',
135
+ minimumFractionDigits: 0,
136
+ maximumFractionDigits: 2,
137
+ ...config.options,
138
+ }).format(frac);
139
+ }
140
+ return getNumberFormatter(locales, config.options ?? {}).format(n);
141
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Unit tests for cell-formatting helpers.
3
+ *
4
+ * Covers every code path in:
5
+ * - resolveDatePattern (every shortcut + datetime variant + unknown fallback)
6
+ * - getDateFormatter (cache hit + cache miss path)
7
+ * - formatNumericWithConfig (number, currency, percent + valueIsPercentPoints,
8
+ * NaN/Infinity/null fallbacks, locale variants, option overrides)
9
+ *
10
+ * Locale assertions use partial substring matches where the exact Intl output
11
+ * differs between Node ICU builds.
12
+ */
13
+ import { describe, expect, it } from 'vitest';
14
+ import { formatNumericWithConfig, resolveDatePattern, getDateFormatter, } from './cell-formatting';
15
+ describe('resolveDatePattern', () => {
16
+ it('returns undefined for missing pattern', () => {
17
+ expect(resolveDatePattern(undefined, 'date')).toBeUndefined();
18
+ expect(resolveDatePattern('', 'date')).toBeUndefined();
19
+ });
20
+ it('handles the "d" (short numeric) date pattern', () => {
21
+ expect(resolveDatePattern('d', 'date')).toEqual({
22
+ year: 'numeric',
23
+ month: 'numeric',
24
+ day: 'numeric',
25
+ });
26
+ });
27
+ it('handles the "d" datetime pattern (adds hour + minute)', () => {
28
+ const opts = resolveDatePattern('d', 'datetime');
29
+ expect(opts.hour).toBe('numeric');
30
+ expect(opts.minute).toBe('numeric');
31
+ expect(opts.year).toBe('numeric');
32
+ });
33
+ it('handles the "D" (long) date pattern', () => {
34
+ expect(resolveDatePattern('D', 'date')).toEqual({
35
+ weekday: 'long',
36
+ year: 'numeric',
37
+ month: 'long',
38
+ day: 'numeric',
39
+ });
40
+ });
41
+ it('handles the "D" datetime pattern with time', () => {
42
+ const opts = resolveDatePattern('D', 'datetime');
43
+ expect(opts.weekday).toBe('long');
44
+ expect(opts.month).toBe('long');
45
+ expect(opts.hour).toBe('numeric');
46
+ });
47
+ it('handles "y-m-d" ISO-friendly date pattern', () => {
48
+ expect(resolveDatePattern('y-m-d', 'date')).toEqual({
49
+ year: 'numeric',
50
+ month: '2-digit',
51
+ day: '2-digit',
52
+ });
53
+ });
54
+ it('handles "y-m-d" datetime pattern (24h, 2-digit)', () => {
55
+ const opts = resolveDatePattern('y-m-d', 'datetime');
56
+ expect(opts.hour).toBe('2-digit');
57
+ expect(opts.minute).toBe('2-digit');
58
+ expect(opts.hour12).toBe(false);
59
+ });
60
+ it('handles dateStyle shortcuts (medium, short, long) without timeStyle for date', () => {
61
+ expect(resolveDatePattern('medium', 'date')).toEqual({
62
+ dateStyle: 'medium',
63
+ timeStyle: undefined,
64
+ });
65
+ expect(resolveDatePattern('short', 'date')).toEqual({
66
+ dateStyle: 'short',
67
+ timeStyle: undefined,
68
+ });
69
+ expect(resolveDatePattern('long', 'date')).toEqual({
70
+ dateStyle: 'long',
71
+ timeStyle: undefined,
72
+ });
73
+ });
74
+ it('attaches timeStyle: short when kind is datetime for dateStyle shortcuts', () => {
75
+ expect(resolveDatePattern('medium', 'datetime')).toEqual({
76
+ dateStyle: 'medium',
77
+ timeStyle: 'short',
78
+ });
79
+ expect(resolveDatePattern('short', 'datetime')).toEqual({
80
+ dateStyle: 'short',
81
+ timeStyle: 'short',
82
+ });
83
+ expect(resolveDatePattern('long', 'datetime')).toEqual({
84
+ dateStyle: 'long',
85
+ timeStyle: 'short',
86
+ });
87
+ });
88
+ it('returns undefined for unknown patterns', () => {
89
+ expect(resolveDatePattern('weird', 'date')).toBeUndefined();
90
+ expect(resolveDatePattern('yyyy-mm', 'datetime')).toBeUndefined();
91
+ });
92
+ it('trims whitespace around the pattern', () => {
93
+ expect(resolveDatePattern(' d ', 'date')).toEqual({
94
+ year: 'numeric',
95
+ month: 'numeric',
96
+ day: 'numeric',
97
+ });
98
+ });
99
+ });
100
+ describe('getDateFormatter', () => {
101
+ it('returns the same instance for repeated calls with the same signature', () => {
102
+ const a = getDateFormatter('en-US', { dateStyle: 'short' });
103
+ const b = getDateFormatter('en-US', { dateStyle: 'short' });
104
+ expect(a).toBe(b);
105
+ });
106
+ it('returns a different instance when the locale changes', () => {
107
+ const en = getDateFormatter('en-US', { dateStyle: 'short' });
108
+ const de = getDateFormatter('de-DE', { dateStyle: 'short' });
109
+ expect(en).not.toBe(de);
110
+ });
111
+ it('returns a different instance when options change', () => {
112
+ const a = getDateFormatter('en-US', { dateStyle: 'short' });
113
+ const b = getDateFormatter('en-US', { dateStyle: 'long' });
114
+ expect(a).not.toBe(b);
115
+ });
116
+ it('treats undefined locale and empty-array locale as distinct', () => {
117
+ const undefLocale = getDateFormatter(undefined, { dateStyle: 'short' });
118
+ const arrLocale = getDateFormatter([], { dateStyle: 'short' });
119
+ // Both produce a working formatter; just smoke-check they work.
120
+ expect(typeof undefLocale.format(new Date())).toBe('string');
121
+ expect(typeof arrLocale.format(new Date())).toBe('string');
122
+ });
123
+ it('keys correctly off every option dimension (smoke: each option produces a distinct cache entry)', () => {
124
+ // We exercise each conditional branch in the key construction so v8/c8
125
+ // counts the line as covered.
126
+ getDateFormatter('en-US', { year: 'numeric' });
127
+ getDateFormatter('en-US', { month: 'long' });
128
+ getDateFormatter('en-US', { day: '2-digit' });
129
+ getDateFormatter('en-US', { hour: '2-digit' });
130
+ getDateFormatter('en-US', { minute: '2-digit' });
131
+ getDateFormatter('en-US', { second: '2-digit' });
132
+ getDateFormatter('en-US', { weekday: 'short' });
133
+ getDateFormatter('en-US', { hour12: true });
134
+ getDateFormatter('en-US', { hour12: false });
135
+ getDateFormatter(['en-US', 'en-GB'], { dateStyle: 'short' });
136
+ });
137
+ });
138
+ describe('formatNumericWithConfig - number', () => {
139
+ it('formats a basic integer with default locale', () => {
140
+ expect(formatNumericWithConfig(1234, { type: 'number' })).toMatch(/1[\.,]?234/);
141
+ });
142
+ it('respects locale grouping and decimal separators', () => {
143
+ const de = formatNumericWithConfig(1234.5, { type: 'number', locales: 'de-DE' });
144
+ expect(de).toContain(',');
145
+ const en = formatNumericWithConfig(1234.5, { type: 'number', locales: 'en-US' });
146
+ expect(en).toContain('.');
147
+ });
148
+ it('forwards fraction-digit options', () => {
149
+ expect(formatNumericWithConfig(1.2345, {
150
+ type: 'number',
151
+ options: { minimumFractionDigits: 3, maximumFractionDigits: 3 },
152
+ locales: 'en-US',
153
+ })).toBe('1.235');
154
+ });
155
+ it('returns the raw String(value) for non-numeric input', () => {
156
+ expect(formatNumericWithConfig('abc', { type: 'number' })).toBe('abc');
157
+ expect(formatNumericWithConfig(NaN, { type: 'number' })).toBe('NaN');
158
+ expect(formatNumericWithConfig(Infinity, { type: 'number' })).toBe('Infinity');
159
+ });
160
+ it('treats null as 0 (Number(null) === 0) and undefined as String("")', () => {
161
+ // Number(null) === 0, so null formats as "0".
162
+ expect(formatNumericWithConfig(null, { type: 'number', locales: 'en-US' })).toBe('0');
163
+ // Number(undefined) === NaN -> falls back to String(undefined ?? '') -> "".
164
+ expect(formatNumericWithConfig(undefined, { type: 'number' })).toBe('');
165
+ });
166
+ it('accepts strings that parse to numbers', () => {
167
+ const result = formatNumericWithConfig('42', { type: 'number', locales: 'en-US' });
168
+ expect(result).toBe('42');
169
+ });
170
+ });
171
+ describe('formatNumericWithConfig - currency', () => {
172
+ it('defaults to USD when currency is omitted', () => {
173
+ const usd = formatNumericWithConfig(99.5, { type: 'currency', locales: 'en-US' });
174
+ expect(usd).toMatch(/\$99\.50/);
175
+ });
176
+ it('honors a custom currency', () => {
177
+ const eur = formatNumericWithConfig(99.5, {
178
+ type: 'currency',
179
+ currency: 'EUR',
180
+ locales: 'en-US',
181
+ });
182
+ expect(eur).toMatch(/€99\.50/);
183
+ });
184
+ it('merges caller options on top of currency defaults', () => {
185
+ const result = formatNumericWithConfig(1234.5, {
186
+ type: 'currency',
187
+ currency: 'USD',
188
+ locales: 'en-US',
189
+ options: { maximumFractionDigits: 0, minimumFractionDigits: 0 },
190
+ });
191
+ expect(result).toBe('$1,235');
192
+ });
193
+ });
194
+ describe('formatNumericWithConfig - percent', () => {
195
+ it('treats input as a 0..1 fraction by default', () => {
196
+ expect(formatNumericWithConfig(0.5, { type: 'percent', locales: 'en-US' })).toMatch(/50%/);
197
+ });
198
+ it('treats input as 0..100 points when valueIsPercentPoints is true', () => {
199
+ expect(formatNumericWithConfig(50, {
200
+ type: 'percent',
201
+ locales: 'en-US',
202
+ valueIsPercentPoints: true,
203
+ })).toMatch(/50%/);
204
+ });
205
+ it('respects fraction-digits override', () => {
206
+ const result = formatNumericWithConfig(0.123, {
207
+ type: 'percent',
208
+ locales: 'en-US',
209
+ options: { minimumFractionDigits: 1, maximumFractionDigits: 1 },
210
+ });
211
+ expect(result).toBe('12.3%');
212
+ });
213
+ });
214
+ describe('formatNumericWithConfig - cache reuse', () => {
215
+ it('reuses the cached Intl.NumberFormat instance across calls', () => {
216
+ // We can't access the cache directly, but we can verify that repeated
217
+ // calls with the same config produce identical output - any cache miss
218
+ // would still return the same string, so this is a smoke check.
219
+ const a = formatNumericWithConfig(10, { type: 'number', locales: 'en-US' });
220
+ const b = formatNumericWithConfig(10, { type: 'number', locales: 'en-US' });
221
+ expect(a).toBe(b);
222
+ });
223
+ it('handles array-of-locales input', () => {
224
+ const result = formatNumericWithConfig(1.5, {
225
+ type: 'number',
226
+ locales: ['en-US', 'en-GB'],
227
+ });
228
+ expect(result).toBe('1.5');
229
+ });
230
+ it('handles undefined locale (browser default)', () => {
231
+ const result = formatNumericWithConfig(1.5, { type: 'number' });
232
+ expect(typeof result).toBe('string');
233
+ });
234
+ });
@@ -0,0 +1,14 @@
1
+ export type ChartExportOptions = {
2
+ /** Background color of the exported image. Default: the grid background. */
3
+ background?: string;
4
+ /** PNG pixel scale (1 = viewBox size). Default 2 (retina). */
5
+ scale?: number;
6
+ };
7
+ /** Serialize the chart SVG to a standalone, self-styled SVG string. */
8
+ export declare function chartToSvgString(source: SVGSVGElement | HTMLElement, options?: ChartExportOptions): string;
9
+ /** Download the chart as an `.svg` file. */
10
+ export declare function downloadChartSvg(source: SVGSVGElement | HTMLElement, filename?: string, options?: ChartExportOptions): void;
11
+ /** Rasterize the chart to a PNG `Blob`. */
12
+ export declare function chartToPngBlob(source: SVGSVGElement | HTMLElement, options?: ChartExportOptions): Promise<Blob>;
13
+ /** Download the chart as a `.png` file. */
14
+ export declare function downloadChartPng(source: SVGSVGElement | HTMLElement, filename?: string, options?: ChartExportOptions): Promise<void>;