@svgrid/grid 3.0.1 → 3.0.2

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 (77) hide show
  1. package/dist/GridFooter.svelte +14 -5
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/GridMenus.svelte +18 -10
  4. package/dist/GridMenus.svelte.d.ts +2 -1
  5. package/dist/SvGrid.controller.svelte.d.ts +17 -0
  6. package/dist/SvGrid.controller.svelte.js +128 -14
  7. package/dist/SvGrid.css +79 -10
  8. package/dist/SvGrid.svelte +191 -186
  9. package/dist/SvGrid.types.d.ts +42 -1
  10. package/dist/SvGridChart.svelte +571 -41
  11. package/dist/SvGridChart.svelte.d.ts +63 -1
  12. package/dist/SvGridChartPanel.svelte +169 -21
  13. package/dist/SvGridChartView.svelte +9 -0
  14. package/dist/SvGridChartView.svelte.d.ts +7 -0
  15. package/dist/SvGroupCell.svelte +28 -3
  16. package/dist/SvGroupCell.svelte.d.ts +8 -0
  17. package/dist/SvRowGroupPanel.svelte +20 -4
  18. package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
  19. package/dist/ai.d.ts +11 -2
  20. package/dist/ai.js +51 -7
  21. package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
  22. package/dist/cdn/{GridMenus-DFB6iwF8.js → GridMenus-M2XP9t0_.js} +152 -150
  23. package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
  24. package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
  25. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
  26. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
  27. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-CxfakxR0.js} +5 -3
  28. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-Dm_YEx9L.js} +5 -3
  29. package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
  30. package/dist/cdn/chart-CKJRCPOk.js +1652 -0
  31. package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
  32. package/dist/cdn/{src-C8QjSaq-.js → src-BJ2opd0j.js} +3608 -3414
  33. package/dist/cdn/{src-D0hO2eYN.js → src-DNbz94Ct.js} +8617 -8423
  34. package/dist/cdn/svgrid.js +16 -15
  35. package/dist/cdn/svgrid.svelte-external.js +16 -15
  36. package/dist/cell-formatting.d.ts +7 -0
  37. package/dist/cell-formatting.js +7 -1
  38. package/dist/chart-export.d.ts +23 -2
  39. package/dist/chart-export.js +80 -6
  40. package/dist/chart.d.ts +410 -18
  41. package/dist/chart.js +824 -24
  42. package/dist/grid-icons.d.ts +49 -0
  43. package/dist/grid-icons.js +86 -0
  44. package/dist/index.d.ts +3 -2
  45. package/dist/index.js +3 -2
  46. package/package.json +11 -11
  47. package/src/GridFooter.svelte +14 -5
  48. package/src/GridMenus.svelte +18 -10
  49. package/src/SvGrid.controller.svelte.ts +146 -13
  50. package/src/SvGrid.css +79 -10
  51. package/src/SvGrid.svelte +191 -186
  52. package/src/SvGrid.types.ts +42 -1
  53. package/src/SvGridChart.svelte +571 -41
  54. package/src/SvGridChart.test.ts +243 -1
  55. package/src/SvGridChartPanel.svelte +169 -21
  56. package/src/SvGridChartView.svelte +9 -0
  57. package/src/SvGroupCell.svelte +28 -3
  58. package/src/SvRowGroupPanel.svelte +20 -4
  59. package/src/ai.test.ts +60 -1
  60. package/src/ai.ts +59 -6
  61. package/src/cell-formatting.ts +7 -1
  62. package/src/chart-export.test.ts +112 -0
  63. package/src/chart-export.ts +76 -8
  64. package/src/chart.test.ts +830 -1
  65. package/src/chart.ts +1053 -32
  66. package/src/grid-icons.test.ts +46 -0
  67. package/src/grid-icons.ts +184 -0
  68. package/src/icon-overrides.grid.test.ts +188 -0
  69. package/src/icon-seam.test.ts +100 -0
  70. package/src/index.ts +19 -0
  71. package/src/svgrid.charting.test.ts +120 -0
  72. package/dist/cdn/GridMenus-CYzOvmNQ.js +0 -600
  73. package/dist/cdn/SvGridChart-BEJmNNx9.js +0 -1203
  74. package/dist/cdn/SvGridChart-BwVos976.js +0 -1202
  75. package/dist/cdn/SvGridChartPanel-BOknOkrP.js +0 -561
  76. package/dist/cdn/SvGridChartPanel-D2PjII4O.js +0 -585
  77. package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The icon catalogue as data: the name list and the glyph table have to agree
3
+ * with each other and with the filter-operator catalogue.
4
+ *
5
+ * The third test here is the one that earns its keep. Operator icons are
6
+ * data-driven (`filterOperatorOptions[].iconName`), and an operator naming an
7
+ * icon the grid has no case for renders an empty box rather than failing -
8
+ * which is exactly how `op-between` shipped blank. `GridIconName` makes that a
9
+ * compile error now, and this makes it a test failure too, so it also catches
10
+ * an operator added through a path that skips type-checking.
11
+ */
12
+ import { describe, expect, it } from 'vitest'
13
+ import { GRID_ICON_GLYPHS, GRID_ICON_NAMES, type GridIconName } from './grid-icons'
14
+ import { filterOperatorOptions } from './filter-operators'
15
+
16
+ describe('grid icon catalogue', () => {
17
+ it('lists every name exactly once', () => {
18
+ const dupes = GRID_ICON_NAMES.filter((n, i) => GRID_ICON_NAMES.indexOf(n) !== i)
19
+ expect(dupes, `duplicated icon names: ${dupes.join(', ')}`).toEqual([])
20
+ })
21
+
22
+ it('only maps glyphs for names in the catalogue', () => {
23
+ const orphans = Object.keys(GRID_ICON_GLYPHS).filter(
24
+ (k) => !GRID_ICON_NAMES.includes(k as GridIconName),
25
+ )
26
+ expect(orphans, `glyphs for unknown icons: ${orphans.join(', ')}`).toEqual([])
27
+ })
28
+
29
+ it('has a name for every filter operator icon', () => {
30
+ const missing = filterOperatorOptions
31
+ .map((o) => o.iconName)
32
+ .filter((n) => !GRID_ICON_NAMES.includes(n))
33
+ expect(missing, `operators naming an icon the grid cannot draw: ${missing.join(', ')}`).toEqual(
34
+ [],
35
+ )
36
+ })
37
+
38
+ it('keeps the pin icons separate from the operator icons they look like', () => {
39
+ // Pin used to render `op-startsWith` / `op-greaterThan` / `x` directly, so
40
+ // overriding a filter operator silently repainted the pin menu. Same
41
+ // default glyphs, different names.
42
+ for (const name of ['pin-left', 'pin-right', 'unpin'] as const) {
43
+ expect(GRID_ICON_NAMES).toContain(name)
44
+ }
45
+ })
46
+ })
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Grid chrome icons. Every glyph `<SvGrid>` draws for its own UI - sort
3
+ * indicators, the filter funnel, expanders, the pager arrows, tool-panel
4
+ * buttons - has a name here, and the `icons` prop replaces any subset of them.
5
+ *
6
+ * This mirrors `grid-messages.ts`: a name-keyed map with built-in defaults,
7
+ * where an unset name keeps the default. A partial map is all a consumer ever
8
+ * needs, so `icons` is safe to pass without knowing the full catalogue. That
9
+ * matters more than it looks - a single catch-all snippet would make the
10
+ * consumer responsible for every name the grid can ask for, and missing one
11
+ * renders an empty box. The `op-between` comment in SvGrid.svelte records what
12
+ * that looks like when it ships.
13
+ *
14
+ * Deliberately NOT in here:
15
+ * - the scrollbar stepper arrows. `sv-grid-scrollbar.ts` is a custom element
16
+ * that builds its SVG with `createElementNS` inside a shadow root, on
17
+ * purpose, so it stays compatible with strict CSP / Trusted Types. A Svelte
18
+ * snippet cannot render there. Theme it with the `--sg-scrollbar-*` tokens.
19
+ * - the checkbox tick and its indeterminate dash. Both are `::after`
20
+ * pseudo-elements drawn from borders (SvGrid.css), a form-control
21
+ * affordance rather than an icon, and they render once per visible row.
22
+ * - conditional formatting's `iconSet`. Those are value-driven data glyphs a
23
+ * consumer already chooses; a different concept that shares the word.
24
+ */
25
+ import type { Snippet } from 'svelte'
26
+
27
+ /**
28
+ * Every icon `<SvGrid>` draws for its own chrome. Pass any subset of these to
29
+ * the `icons` prop to swap the built-in glyph for your own markup.
30
+ */
31
+ export type GridIconName =
32
+ // ---- Header + column menu ----
33
+ | 'sort'
34
+ | 'sort-asc'
35
+ | 'sort-desc'
36
+ | 'filter'
37
+ | 'menu'
38
+ | 'group'
39
+ | 'x'
40
+ | 'chevron-down'
41
+ | 'chevron-right'
42
+ | 'autosize'
43
+ | 'columns'
44
+ | 'reset'
45
+ // ---- Filter operators (mirror `iconName` in filter-operators.ts) ----
46
+ | 'op-contains'
47
+ | 'op-notContains'
48
+ | 'op-equals'
49
+ | 'op-notEquals'
50
+ | 'op-startsWith'
51
+ | 'op-endsWith'
52
+ | 'op-regex'
53
+ | 'op-in'
54
+ | 'op-notIn'
55
+ | 'op-greaterThan'
56
+ | 'op-lessThan'
57
+ | 'op-between'
58
+ | 'op-isBlank'
59
+ | 'op-isNotBlank'
60
+ // ---- Pinning. Their defaults borrow the operator glyphs above, but they get
61
+ // their own names so overriding a filter operator does not silently repaint
62
+ // the pin menu. ----
63
+ | 'pin-left'
64
+ | 'pin-right'
65
+ | 'unpin'
66
+ // ---- Toolbar + overlays ----
67
+ | 'search'
68
+ | 'tool-panel'
69
+ | 'chart'
70
+ | 'advanced-filter'
71
+ | 'column-group-caret'
72
+ // ---- Glyph icons: the built-in is a character, not a path ----
73
+ | 'close'
74
+ | 'clear'
75
+ | 'remove'
76
+ | 'row-number'
77
+ | 'pinned-row-top'
78
+ | 'pinned-row-bottom'
79
+ | 'find-prev'
80
+ | 'find-next'
81
+ | 'move-up'
82
+ | 'move-down'
83
+ | 'group-add'
84
+ | 'page-first'
85
+ | 'page-prev'
86
+ | 'page-next'
87
+ | 'page-last'
88
+ | 'page-size-caret'
89
+ | 'drag-handle'
90
+ | 'breadcrumb-separator'
91
+
92
+ /**
93
+ * Consumer icon overrides, keyed by name. Any subset; names you leave out keep
94
+ * their built-in glyph, so `icons` never has to be exhaustive.
95
+ */
96
+ export type GridIcons = Partial<Record<GridIconName, Snippet>>
97
+
98
+ /**
99
+ * Icons whose built-in form is a character rather than an SVG path. Kept as
100
+ * data so `<SvGrid>`, its footer and the standalone `SvRowGroupPanel` render
101
+ * the same defaults instead of three copies drifting apart.
102
+ *
103
+ * Each is the exact character that shipped inline before, so a grid that sets
104
+ * no `icons` renders byte-for-byte what it always did.
105
+ */
106
+ export const GRID_ICON_GLYPHS: Partial<Record<GridIconName, string>> = {
107
+ close: '✕', // multiplication X
108
+ clear: '✕',
109
+ remove: '×', // multiplication sign
110
+ 'row-number': '#',
111
+ 'pinned-row-top': '↑', // upwards arrow
112
+ 'pinned-row-bottom': '↓', // downwards arrow
113
+ 'find-prev': '↑',
114
+ 'find-next': '↓',
115
+ 'move-up': '↑',
116
+ 'move-down': '↓',
117
+ 'group-add': '⊞', // squared plus
118
+ 'page-first': '⇤', // leftwards arrow to bar
119
+ 'page-prev': '‹', // single left-pointing angle quote
120
+ 'page-next': '›', // single right-pointing angle quote
121
+ 'page-last': '⇥', // rightwards arrow to bar
122
+ 'page-size-caret': '▾', // black down-pointing small triangle
123
+ 'drag-handle': '⠿', // braille pattern dots-123456, the grip
124
+ 'breadcrumb-separator': '›',
125
+ }
126
+
127
+ /**
128
+ * Every icon name, in catalogue order. Exported for tests and docs generation;
129
+ * the grid itself never reads it, so it costs a consumer's bundle nothing.
130
+ */
131
+ export const GRID_ICON_NAMES: ReadonlyArray<GridIconName> = [
132
+ 'sort',
133
+ 'sort-asc',
134
+ 'sort-desc',
135
+ 'filter',
136
+ 'menu',
137
+ 'group',
138
+ 'x',
139
+ 'chevron-down',
140
+ 'chevron-right',
141
+ 'autosize',
142
+ 'columns',
143
+ 'reset',
144
+ 'op-contains',
145
+ 'op-notContains',
146
+ 'op-equals',
147
+ 'op-notEquals',
148
+ 'op-startsWith',
149
+ 'op-endsWith',
150
+ 'op-regex',
151
+ 'op-in',
152
+ 'op-notIn',
153
+ 'op-greaterThan',
154
+ 'op-lessThan',
155
+ 'op-between',
156
+ 'op-isBlank',
157
+ 'op-isNotBlank',
158
+ 'pin-left',
159
+ 'pin-right',
160
+ 'unpin',
161
+ 'search',
162
+ 'tool-panel',
163
+ 'chart',
164
+ 'advanced-filter',
165
+ 'column-group-caret',
166
+ 'close',
167
+ 'clear',
168
+ 'remove',
169
+ 'row-number',
170
+ 'pinned-row-top',
171
+ 'pinned-row-bottom',
172
+ 'find-prev',
173
+ 'find-next',
174
+ 'move-up',
175
+ 'move-down',
176
+ 'group-add',
177
+ 'page-first',
178
+ 'page-prev',
179
+ 'page-next',
180
+ 'page-last',
181
+ 'page-size-caret',
182
+ 'drag-handle',
183
+ 'breadcrumb-separator',
184
+ ]
@@ -0,0 +1,188 @@
1
+ /**
2
+ * DOM: the `icons` prop replaces the grid's own glyphs, one name at a time.
3
+ *
4
+ * The contract these lock in:
5
+ * - per-name fallback. Overriding one icon must not blank the others; that
6
+ * cliff is the whole reason `icons` is a map and not a single snippet.
7
+ * - an override reaches the lazily-imported column menu, not just the parts
8
+ * of the grid that render eagerly.
9
+ * - overriding a filter operator does NOT repaint the pin menu, which shared
10
+ * its glyphs before this feature.
11
+ * - defaults are untouched when `icons` is absent.
12
+ *
13
+ * jsdom does not compute `transform`, so the rotation contract is asserted as
14
+ * the class hook the CSS keys off (`.sv-grid-icon` on the wrapper, as a direct
15
+ * child of the toggle) rather than as a computed style. The rotation itself is
16
+ * browser-only verification.
17
+ */
18
+ import { afterEach, describe, expect, it, vi } from 'vitest'
19
+ import { createRawSnippet, flushSync, mount, unmount } from 'svelte'
20
+ import SvGrid from './SvGrid.svelte'
21
+ import {
22
+ createCoreRowModel,
23
+ createFilteredRowModel,
24
+ createPaginatedRowModel,
25
+ createSortedRowModel,
26
+ sortFns,
27
+ tableFeatures,
28
+ rowSortingFeature,
29
+ } from './index'
30
+ import type { ColumnDef } from './index'
31
+
32
+ type Row = { id: number; name: string }
33
+ const features = tableFeatures({ rowSortingFeature })
34
+ const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 160 }]
35
+ const data: Row[] = Array.from({ length: 12 }, (_, i) => ({ id: i, name: `n${i}` }))
36
+
37
+ const tick = () => new Promise<void>((r) => queueMicrotask(r))
38
+
39
+ /** A marker icon that is trivially findable in the DOM. */
40
+ const mark = (id: string) =>
41
+ createRawSnippet(() => ({
42
+ render: () => `<svg data-icon="${id}"><path d="M0 0" /></svg>`,
43
+ }))
44
+
45
+ function mountGrid(props: Record<string, unknown>) {
46
+ const target = document.createElement('div')
47
+ document.body.appendChild(target)
48
+ const app = mount(SvGrid, {
49
+ target,
50
+ props: {
51
+ data,
52
+ columns: cols,
53
+ features,
54
+ _rowModels: {
55
+ coreRowModel: createCoreRowModel(),
56
+ filteredRowModel: createFilteredRowModel(),
57
+ sortedRowModel: createSortedRowModel(sortFns),
58
+ paginatedRowModel: createPaginatedRowModel(),
59
+ },
60
+ containerHeight: 240,
61
+ virtualization: false,
62
+ ...props,
63
+ } as never,
64
+ })
65
+ return { target, app, destroy: () => { unmount(app); target.remove() } }
66
+ }
67
+
68
+ let cleanup: (() => void) | null = null
69
+ afterEach(() => { cleanup?.(); cleanup = null })
70
+
71
+ describe('icons prop', () => {
72
+ it('renders the built-in glyphs when no icons are passed', async () => {
73
+ const { target, destroy } = mountGrid({ showColumnMenu: true })
74
+ cleanup = destroy
75
+ await tick()
76
+ // The header menu button draws a real <svg> from the built-in catalogue.
77
+ const menuBtn = target.querySelector('.sv-grid-col-menu-btn')
78
+ expect(menuBtn).not.toBeNull()
79
+ expect(menuBtn!.querySelector('svg.sv-grid-icon')).not.toBeNull()
80
+ expect(target.querySelector('[data-icon]')).toBeNull()
81
+ })
82
+
83
+ it('replaces one icon and leaves the rest alone', async () => {
84
+ const { target, destroy } = mountGrid({
85
+ showColumnMenu: true,
86
+ showFilterRow: true,
87
+ icons: { menu: mark('m') },
88
+ })
89
+ cleanup = destroy
90
+ await tick()
91
+
92
+ const menuBtn = target.querySelector('.sv-grid-col-menu-btn')!
93
+ // The override is there...
94
+ expect(menuBtn.querySelector('[data-icon="m"]')).not.toBeNull()
95
+ // ...and the built-in it replaced is not drawn alongside it.
96
+ expect(menuBtn.querySelector('svg.sv-grid-icon')).toBeNull()
97
+ // An unset name still renders its built-in. This is the assertion that a
98
+ // single catch-all snippet could not satisfy.
99
+ expect(target.querySelectorAll('svg.sv-grid-icon').length).toBeGreaterThan(0)
100
+ })
101
+
102
+ it('wraps an override in the icon box the CSS keys off', async () => {
103
+ const { target, destroy } = mountGrid({ showColumnMenu: true, icons: { menu: mark('m') } })
104
+ cleanup = destroy
105
+ await tick()
106
+ const wrapper = target.querySelector('[data-icon="m"]')!.parentElement!
107
+ expect(wrapper.classList.contains('sv-grid-icon')).toBe(true)
108
+ expect(wrapper.classList.contains('sv-grid-icon-custom')).toBe(true)
109
+ })
110
+
111
+ it('reaches the lazily-imported column menu', async () => {
112
+ const { target, destroy } = mountGrid({
113
+ showColumnMenu: true,
114
+ icons: { 'sort-asc': mark('sa') },
115
+ })
116
+ cleanup = destroy
117
+ await tick()
118
+ ;(target.querySelector('.sv-grid-col-menu-btn') as HTMLElement).click()
119
+ // GridMenus arrives through a dynamic import(), so poll rather than
120
+ // guessing a tick count.
121
+ await vi.waitFor(() => {
122
+ expect(target.querySelector('[data-icon="sa"]')).not.toBeNull()
123
+ })
124
+ })
125
+
126
+ it('does not repaint the pin menu when a filter operator is overridden', async () => {
127
+ const { target, destroy } = mountGrid({
128
+ showColumnMenu: true,
129
+ // The pin section is hidden while columns are virtualized, and that
130
+ // defaults to on. This is COLUMN virtualization, not the row
131
+ // `virtualization: false` the shared helper sets.
132
+ columnVirtualization: false,
133
+ icons: { 'op-startsWith': mark('sw') },
134
+ })
135
+ cleanup = destroy
136
+ await tick()
137
+ ;(target.querySelector('.sv-grid-col-menu-btn') as HTMLElement).click()
138
+
139
+ // Assert the item is really there before asserting about its icon -
140
+ // a conditional check here would pass just as happily if the pin menu
141
+ // stopped rendering at all.
142
+ const pinLeft = await vi.waitFor(() => {
143
+ const el = [...target.querySelectorAll('.sv-grid-menu-item')].find((e) =>
144
+ e.textContent?.includes('Pin to left'),
145
+ )
146
+ expect(el).toBeTruthy()
147
+ return el!
148
+ })
149
+
150
+ // Pin borrowed `op-startsWith` before this feature; it must not follow
151
+ // that override now.
152
+ expect(pinLeft.querySelector('[data-icon="sw"]')).toBeNull()
153
+ expect(pinLeft.querySelector('svg.sv-grid-icon')).not.toBeNull()
154
+ })
155
+
156
+ it('overrides the pager arrows through GridFooter', async () => {
157
+ const { target, destroy } = mountGrid({
158
+ showPagination: true,
159
+ pageSize: 5,
160
+ icons: { 'page-next': mark('pn') },
161
+ })
162
+ cleanup = destroy
163
+ await tick()
164
+ const hit = target.querySelector('[data-icon="pn"]')
165
+ expect(hit).not.toBeNull()
166
+ // Still a real pager button, and its label is untouched: icons and
167
+ // localeText are independent axes.
168
+ const btn = hit!.closest('.sv-grid-pagination-btn')
169
+ expect(btn).not.toBeNull()
170
+ expect(btn!.getAttribute('aria-label')).toBe('Next page')
171
+ })
172
+
173
+ it('picks up icons set after mount through api.setOption', async () => {
174
+ let api: { setOption: (k: string, v: unknown) => void } | undefined
175
+ const { target, destroy } = mountGrid({
176
+ showColumnMenu: true,
177
+ onApiReady: (a: typeof api) => (api = a),
178
+ })
179
+ cleanup = destroy
180
+ await tick()
181
+ expect(target.querySelector('[data-icon="rt"]')).toBeNull()
182
+
183
+ api!.setOption('icons', { menu: mark('rt') })
184
+ flushSync()
185
+ await tick()
186
+ expect(target.querySelector('[data-icon="rt"]')).not.toBeNull()
187
+ })
188
+ })
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Source scan: every chrome icon goes through the `icon` snippet.
3
+ *
4
+ * The `icons` prop is only worth having if it covers the chrome, and coverage
5
+ * is the kind of thing that rots one commit at a time - somebody adds a toolbar
6
+ * button with an inline `<svg>`, it works, nobody notices it ignores `icons`
7
+ * until a consumer reports a stray glyph in a re-skinned grid. This is the
8
+ * check that turns that into a red test instead.
9
+ *
10
+ * It reads source rather than DOM on purpose: a rendering test can only cover
11
+ * the states it happens to mount, and the icons most likely to be missed are
12
+ * the ones behind a rarely-exercised branch.
13
+ */
14
+ import { readFileSync } from 'node:fs'
15
+ import { join } from 'node:path'
16
+ import { describe, expect, it } from 'vitest'
17
+ import { GRID_ICON_GLYPHS, GRID_ICON_NAMES, type GridIconName } from './grid-icons'
18
+
19
+ const SRC = join(process.cwd(), 'src')
20
+ const read = (f: string) => readFileSync(join(SRC, f), 'utf8')
21
+
22
+ /** Files that render grid chrome and therefore must go through the seam. */
23
+ const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte', 'SvRowGroupPanel.svelte']
24
+
25
+ /**
26
+ * Drop the regions allowed to contain raw icon markup: the `icon` snippet's own
27
+ * body (which IS the built-in set), the `ic` snippet in SvRowGroupPanel, and
28
+ * every `<script>` block.
29
+ */
30
+ function stripAllowedRegions(src: string): string {
31
+ let out = src.replace(/<script[\s\S]*?<\/script>/g, '')
32
+ // The built-in catalogue: from the snippet header to its closing tag.
33
+ out = out.replace(/\{#snippet icon\(name: GridIconName\)\}[\s\S]*?\n\{\/snippet\}/g, '')
34
+ out = out.replace(/\{#snippet ic\(name: GridIconName\)\}[\s\S]*?\{\/snippet\}/g, '')
35
+ return out
36
+ }
37
+
38
+ describe('icon seam', () => {
39
+ it('leaves no inline <svg> outside the built-in catalogue', () => {
40
+ // Data visualisation is not chrome: these draw values, not affordances, and
41
+ // there is nothing for a consumer's icon set to say about them.
42
+ const ALLOWED = ['sv-grid-sparkline']
43
+
44
+ const offenders: string[] = []
45
+ for (const file of CHROME) {
46
+ const body = stripAllowedRegions(read(file))
47
+ for (const [i, line] of body.split('\n').entries()) {
48
+ if (!line.includes('<svg')) continue
49
+ if (ALLOWED.some((a) => body.split('\n').slice(i, i + 3).join(' ').includes(a))) continue
50
+ offenders.push(`${file}:${i + 1} ${line.trim().slice(0, 80)}`)
51
+ }
52
+ }
53
+ expect(
54
+ offenders,
55
+ `Inline <svg> outside the icon snippet - route it through {@render icon("...")} ` +
56
+ `and add a name to grid-icons.ts:\n ${offenders.join('\n ')}`,
57
+ ).toEqual([])
58
+ })
59
+
60
+ it('leaves no glyph character used as an icon', () => {
61
+ // Every character the catalogue owns. If one of these appears as an
62
+ // element's text content outside the snippet, it is an icon that bypassed
63
+ // the seam.
64
+ const glyphs = [...new Set(Object.values(GRID_ICON_GLYPHS))].filter((g) => g !== '#')
65
+ const offenders: string[] = []
66
+ for (const file of CHROME) {
67
+ const body = stripAllowedRegions(read(file))
68
+ for (const [i, line] of body.split('\n').entries()) {
69
+ for (const g of glyphs) {
70
+ // `>GLYPH<` is the shape of a glyph rendered as element content.
71
+ if (new RegExp(`>\\s*${escapeRe(g)}\\s*<`).test(line)) {
72
+ offenders.push(`${file}:${i + 1} ${JSON.stringify(g)} in ${line.trim().slice(0, 70)}`)
73
+ }
74
+ }
75
+ }
76
+ }
77
+ expect(
78
+ offenders,
79
+ `Glyph rendered directly instead of through the icon seam:\n ${offenders.join('\n ')}`,
80
+ ).toEqual([])
81
+ })
82
+
83
+ it('only renders names the catalogue defines', () => {
84
+ const unknown: string[] = []
85
+ for (const file of CHROME) {
86
+ const src = read(file)
87
+ for (const m of src.matchAll(/\{@render ic(?:on)?\(\s*["']([a-zA-Z-]+)["']\s*\)\}/g)) {
88
+ const name = m[1] as GridIconName
89
+ if (!GRID_ICON_NAMES.includes(name)) unknown.push(`${file}: ${name}`)
90
+ }
91
+ }
92
+ expect(unknown, `Icon names with no entry in grid-icons.ts:\n ${unknown.join('\n ')}`).toEqual(
93
+ [],
94
+ )
95
+ })
96
+ })
97
+
98
+ function escapeRe(s: string): string {
99
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
100
+ }
package/src/index.ts CHANGED
@@ -536,6 +536,12 @@ export {
536
536
  buildChart,
537
537
  formatChartValue,
538
538
  rowsToChartSpec,
539
+ rowsToBoxSpec,
540
+ rowsToDirectSpec,
541
+ boxStats,
542
+ sliceChartWindow,
543
+ chartScales,
544
+ ordinalDateTicks,
539
545
  niceScale,
540
546
  niceLogScale,
541
547
  linearTrend,
@@ -550,7 +556,13 @@ export {
550
556
  type ChartSpec,
551
557
  type ChartSeries,
552
558
  type ChartGeometry,
559
+ type ChartScales,
553
560
  type ChartBar,
561
+ type ChartCandle,
562
+ type ChartBox,
563
+ type ChartErrorBar,
564
+ type BoxStats,
565
+ type OhlcBar,
554
566
  type ChartLine,
555
567
  type ChartLinePoint,
556
568
  type ChartPieSlice,
@@ -581,6 +593,7 @@ export {
581
593
  downloadChartSvg,
582
594
  chartToPngBlob,
583
595
  downloadChartPng,
596
+ chartCsvExportable,
584
597
  chartSpecToCsv,
585
598
  downloadChartCsv,
586
599
  type ChartExportOptions,
@@ -693,6 +706,12 @@ export {
693
706
  resolveGridMessages,
694
707
  type GridMessages,
695
708
  } from './grid-messages'
709
+ export {
710
+ GRID_ICON_GLYPHS,
711
+ GRID_ICON_NAMES,
712
+ type GridIconName,
713
+ type GridIcons,
714
+ } from './grid-icons'
696
715
  export {
697
716
  insertGroupFooters,
698
717
  buildAutoGroupColumns,