@opendata-ai/openchart-svelte 2.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 (66) hide show
  1. package/README.md +94 -0
  2. package/dist/Chart.svelte +132 -0
  3. package/dist/Chart.svelte.d.ts +20 -0
  4. package/dist/Chart.svelte.d.ts.map +1 -0
  5. package/dist/DataTable.svelte +132 -0
  6. package/dist/DataTable.svelte.d.ts +19 -0
  7. package/dist/DataTable.svelte.d.ts.map +1 -0
  8. package/dist/Graph.svelte +119 -0
  9. package/dist/Graph.svelte.d.ts +22 -0
  10. package/dist/Graph.svelte.d.ts.map +1 -0
  11. package/dist/ThemeProvider.svelte +27 -0
  12. package/dist/ThemeProvider.svelte.d.ts +11 -0
  13. package/dist/ThemeProvider.svelte.d.ts.map +1 -0
  14. package/dist/Visualization.svelte +35 -0
  15. package/dist/Visualization.svelte.d.ts +12 -0
  16. package/dist/Visualization.svelte.d.ts.map +1 -0
  17. package/dist/__tests__/Chart.test.js +108 -0
  18. package/dist/__tests__/DataTable.test.js +101 -0
  19. package/dist/__tests__/Graph.test.js +104 -0
  20. package/dist/__tests__/ThemeContext.test.js +89 -0
  21. package/dist/__tests__/composables.test.js +84 -0
  22. package/dist/__tests__/useTableState.test.js +105 -0
  23. package/dist/composables/useChart.svelte.d.ts +41 -0
  24. package/dist/composables/useChart.svelte.d.ts.map +1 -0
  25. package/dist/composables/useChart.svelte.js +73 -0
  26. package/dist/composables/useDarkMode.svelte.d.ts +15 -0
  27. package/dist/composables/useDarkMode.svelte.d.ts.map +1 -0
  28. package/dist/composables/useDarkMode.svelte.js +47 -0
  29. package/dist/composables/useGraph.svelte.d.ts +53 -0
  30. package/dist/composables/useGraph.svelte.d.ts.map +1 -0
  31. package/dist/composables/useGraph.svelte.js +67 -0
  32. package/dist/composables/useTable.svelte.d.ts +30 -0
  33. package/dist/composables/useTable.svelte.d.ts.map +1 -0
  34. package/dist/composables/useTable.svelte.js +59 -0
  35. package/dist/composables/useTableState.svelte.d.ts +45 -0
  36. package/dist/composables/useTableState.svelte.d.ts.map +1 -0
  37. package/dist/composables/useTableState.svelte.js +58 -0
  38. package/dist/context.d.ts +21 -0
  39. package/dist/context.d.ts.map +1 -0
  40. package/dist/context.js +28 -0
  41. package/dist/index.d.ts +25 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/types.d.ts +60 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +7 -0
  47. package/package.json +65 -0
  48. package/src/Chart.svelte +132 -0
  49. package/src/DataTable.svelte +132 -0
  50. package/src/Graph.svelte +119 -0
  51. package/src/ThemeProvider.svelte +27 -0
  52. package/src/Visualization.svelte +35 -0
  53. package/src/__tests__/Chart.test.ts +134 -0
  54. package/src/__tests__/DataTable.test.ts +126 -0
  55. package/src/__tests__/Graph.test.ts +130 -0
  56. package/src/__tests__/ThemeContext.test.ts +106 -0
  57. package/src/__tests__/composables.test.ts +101 -0
  58. package/src/__tests__/useTableState.test.ts +134 -0
  59. package/src/composables/useChart.svelte.ts +108 -0
  60. package/src/composables/useDarkMode.svelte.ts +51 -0
  61. package/src/composables/useGraph.svelte.ts +115 -0
  62. package/src/composables/useTable.svelte.ts +85 -0
  63. package/src/composables/useTableState.svelte.ts +78 -0
  64. package/src/context.ts +35 -0
  65. package/src/index.ts +46 -0
  66. package/src/types.ts +78 -0
@@ -0,0 +1,108 @@
1
+ import { cleanup, render, waitFor } from '@testing-library/svelte';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import Chart from '../Chart.svelte';
4
+ // ---------------------------------------------------------------------------
5
+ // Test data
6
+ // ---------------------------------------------------------------------------
7
+ const lineSpec = {
8
+ type: 'line',
9
+ data: [
10
+ { date: '2020-01-01', value: 10, country: 'US' },
11
+ { date: '2021-01-01', value: 40, country: 'US' },
12
+ { date: '2020-01-01', value: 15, country: 'UK' },
13
+ { date: '2021-01-01', value: 35, country: 'UK' },
14
+ ],
15
+ encoding: {
16
+ x: { field: 'date', type: 'temporal' },
17
+ y: { field: 'value', type: 'quantitative' },
18
+ color: { field: 'country', type: 'nominal' },
19
+ },
20
+ chrome: {
21
+ title: 'GDP Growth',
22
+ subtitle: 'US vs UK over time',
23
+ source: 'World Bank',
24
+ },
25
+ };
26
+ const barSpec = {
27
+ type: 'bar',
28
+ data: [
29
+ { name: 'A', value: 10 },
30
+ { name: 'B', value: 30 },
31
+ { name: 'C', value: 20 },
32
+ ],
33
+ encoding: {
34
+ x: { field: 'value', type: 'quantitative' },
35
+ y: { field: 'name', type: 'nominal' },
36
+ },
37
+ chrome: {
38
+ title: 'Updated Title',
39
+ },
40
+ };
41
+ // ---------------------------------------------------------------------------
42
+ // Helper: render Chart and wait for SVG to appear ($effect is deferred)
43
+ // ---------------------------------------------------------------------------
44
+ async function renderChart(props) {
45
+ const result = render(Chart, { props });
46
+ await waitFor(() => {
47
+ expect(result.container.querySelector('svg')).not.toBeNull();
48
+ });
49
+ return result;
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Tests
53
+ // ---------------------------------------------------------------------------
54
+ afterEach(() => {
55
+ cleanup();
56
+ });
57
+ describe('<Chart />', () => {
58
+ it('renders an SVG element', async () => {
59
+ const { container } = await renderChart({ spec: lineSpec });
60
+ const svg = container.querySelector('svg');
61
+ expect(svg).not.toBeNull();
62
+ expect(svg?.getAttribute('class')).toBe('viz-chart');
63
+ });
64
+ it('renders chrome text elements', async () => {
65
+ const { container } = await renderChart({ spec: lineSpec });
66
+ const title = container.querySelector('.viz-title');
67
+ expect(title).not.toBeNull();
68
+ expect(title?.textContent).toBe('GDP Growth');
69
+ const subtitle = container.querySelector('.viz-subtitle');
70
+ expect(subtitle?.textContent).toBe('US vs UK over time');
71
+ const source = container.querySelector('.viz-source');
72
+ expect(source?.textContent).toBe('World Bank');
73
+ });
74
+ it('spec changes trigger re-render', async () => {
75
+ const { container, rerender } = await renderChart({ spec: lineSpec });
76
+ const titleBefore = container.querySelector('.viz-title');
77
+ expect(titleBefore?.textContent).toBe('GDP Growth');
78
+ await rerender({ spec: barSpec });
79
+ await waitFor(() => {
80
+ expect(container.querySelector('.viz-title')?.textContent).toBe('Updated Title');
81
+ });
82
+ });
83
+ it('unmounting cleans up chart instance', async () => {
84
+ const { container, unmount } = await renderChart({ spec: lineSpec });
85
+ const svgBefore = container.querySelector('svg');
86
+ expect(svgBefore).not.toBeNull();
87
+ unmount();
88
+ expect(container.querySelector('svg')).toBeNull();
89
+ });
90
+ it('className prop passes through to wrapper div', async () => {
91
+ const { container } = await renderChart({ spec: lineSpec, class: 'my-chart' });
92
+ const wrapper = container.firstElementChild;
93
+ expect(wrapper?.className).toContain('my-chart');
94
+ });
95
+ it('renders with dark mode option', async () => {
96
+ const { container } = await renderChart({ spec: lineSpec, darkMode: 'force' });
97
+ const svg = container.querySelector('svg');
98
+ expect(svg).not.toBeNull();
99
+ });
100
+ it('style prop passes through to wrapper div', async () => {
101
+ const { container } = await renderChart({
102
+ spec: lineSpec,
103
+ style: 'border: 1px solid red',
104
+ });
105
+ const wrapper = container.firstElementChild;
106
+ expect(wrapper?.style.border).toBe('1px solid red');
107
+ });
108
+ });
@@ -0,0 +1,101 @@
1
+ import { cleanup, render, waitFor } from '@testing-library/svelte';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import DataTable from '../DataTable.svelte';
4
+ // ---------------------------------------------------------------------------
5
+ // Test data
6
+ // ---------------------------------------------------------------------------
7
+ const tableSpec = {
8
+ type: 'table',
9
+ data: [
10
+ { name: 'Alice', age: 30, city: 'Portland' },
11
+ { name: 'Bob', age: 25, city: 'Seattle' },
12
+ { name: 'Charlie', age: 35, city: 'Denver' },
13
+ ],
14
+ columns: [
15
+ { key: 'name', label: 'Name' },
16
+ { key: 'age', label: 'Age' },
17
+ { key: 'city', label: 'City' },
18
+ ],
19
+ chrome: { title: 'People Table' },
20
+ };
21
+ const updatedSpec = {
22
+ type: 'table',
23
+ data: [
24
+ { x: 1, y: 2 },
25
+ { x: 3, y: 4 },
26
+ ],
27
+ columns: [
28
+ { key: 'x', label: 'X Value' },
29
+ { key: 'y', label: 'Y Value' },
30
+ ],
31
+ chrome: { title: 'Updated Table' },
32
+ };
33
+ // ---------------------------------------------------------------------------
34
+ // Helper: render DataTable and wait for the table to mount ($effect is deferred)
35
+ // ---------------------------------------------------------------------------
36
+ async function renderTable(props) {
37
+ const result = render(DataTable, { props });
38
+ await waitFor(() => {
39
+ expect(result.container.querySelector('table')).not.toBeNull();
40
+ });
41
+ return result;
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Tests
45
+ // ---------------------------------------------------------------------------
46
+ afterEach(() => {
47
+ cleanup();
48
+ });
49
+ describe('<DataTable />', () => {
50
+ it('renders a table', async () => {
51
+ const { container } = await renderTable({ spec: tableSpec });
52
+ const table = container.querySelector('table');
53
+ expect(table).not.toBeNull();
54
+ });
55
+ it('renders correct number of columns', async () => {
56
+ const { container } = await renderTable({ spec: tableSpec });
57
+ const headers = container.querySelectorAll('thead th');
58
+ expect(headers.length).toBe(3);
59
+ });
60
+ it('renders correct number of rows', async () => {
61
+ const { container } = await renderTable({ spec: tableSpec });
62
+ const rows = container.querySelectorAll('tbody tr');
63
+ expect(rows.length).toBe(3);
64
+ });
65
+ it('spec changes trigger re-render', async () => {
66
+ const { container, rerender } = await renderTable({ spec: tableSpec });
67
+ const titleBefore = container.querySelector('.viz-table-title');
68
+ expect(titleBefore?.textContent).toBe('People Table');
69
+ await rerender({ spec: updatedSpec });
70
+ await waitFor(() => {
71
+ expect(container.querySelector('.viz-table-title')?.textContent).toBe('Updated Table');
72
+ });
73
+ const headersAfter = container.querySelectorAll('thead th');
74
+ expect(headersAfter.length).toBe(2);
75
+ });
76
+ it('unmounting cleans up', async () => {
77
+ const { container, unmount } = await renderTable({ spec: tableSpec });
78
+ const tableBefore = container.querySelector('table');
79
+ expect(tableBefore).not.toBeNull();
80
+ unmount();
81
+ expect(container.querySelector('table')).toBeNull();
82
+ });
83
+ it('className passes through', async () => {
84
+ const { container } = await renderTable({ spec: tableSpec, class: 'my-table' });
85
+ const wrapper = container.firstElementChild;
86
+ expect(wrapper?.className).toContain('my-table');
87
+ });
88
+ it('renders with dark mode option', async () => {
89
+ const { container } = await renderTable({ spec: tableSpec, darkMode: 'force' });
90
+ const table = container.querySelector('table');
91
+ expect(table).not.toBeNull();
92
+ });
93
+ it('style prop passes through to wrapper div', async () => {
94
+ const { container } = await renderTable({
95
+ spec: tableSpec,
96
+ style: 'border: 1px solid red',
97
+ });
98
+ const wrapper = container.firstElementChild;
99
+ expect(wrapper?.style.border).toBe('1px solid red');
100
+ });
101
+ });
@@ -0,0 +1,104 @@
1
+ import { cleanup, render, waitFor } from '@testing-library/svelte';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import Graph from '../Graph.svelte';
4
+ // ---------------------------------------------------------------------------
5
+ // Test data
6
+ // ---------------------------------------------------------------------------
7
+ const basicSpec = {
8
+ type: 'graph',
9
+ nodes: [
10
+ { id: 'a', label: 'Node A' },
11
+ { id: 'b', label: 'Node B' },
12
+ { id: 'c', label: 'Node C' },
13
+ ],
14
+ edges: [
15
+ { source: 'a', target: 'b' },
16
+ { source: 'b', target: 'c' },
17
+ ],
18
+ chrome: {
19
+ title: 'Test Graph',
20
+ subtitle: 'A simple test graph',
21
+ },
22
+ };
23
+ const updatedSpec = {
24
+ type: 'graph',
25
+ nodes: [
26
+ { id: 'x', label: 'Node X' },
27
+ { id: 'y', label: 'Node Y' },
28
+ ],
29
+ edges: [{ source: 'x', target: 'y' }],
30
+ chrome: {
31
+ title: 'Updated Graph',
32
+ },
33
+ };
34
+ // ---------------------------------------------------------------------------
35
+ // Helper: render Graph and wait for canvas to mount ($effect is deferred)
36
+ // ---------------------------------------------------------------------------
37
+ async function renderGraph(props) {
38
+ const result = render(Graph, { props });
39
+ await waitFor(() => {
40
+ expect(result.container.querySelector('canvas')).not.toBeNull();
41
+ });
42
+ return result;
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // Tests
46
+ // ---------------------------------------------------------------------------
47
+ afterEach(() => {
48
+ cleanup();
49
+ });
50
+ describe('<Graph />', () => {
51
+ it('renders a container div', async () => {
52
+ const { container } = await renderGraph({ spec: basicSpec });
53
+ const wrapper = container.firstElementChild;
54
+ expect(wrapper).not.toBeNull();
55
+ expect(wrapper.tagName.toLowerCase()).toBe('div');
56
+ });
57
+ it('mounts graph instance with canvas element', async () => {
58
+ const { container } = await renderGraph({ spec: basicSpec });
59
+ const canvas = container.querySelector('canvas');
60
+ expect(canvas).not.toBeNull();
61
+ });
62
+ it('renders chrome text elements', async () => {
63
+ const { container } = await renderGraph({ spec: basicSpec });
64
+ const title = container.querySelector('.viz-title');
65
+ expect(title).not.toBeNull();
66
+ expect(title?.textContent).toBe('Test Graph');
67
+ const subtitle = container.querySelector('.viz-subtitle');
68
+ expect(subtitle?.textContent).toBe('A simple test graph');
69
+ });
70
+ it('spec changes trigger re-render', async () => {
71
+ const { container, rerender } = await renderGraph({ spec: basicSpec });
72
+ const titleBefore = container.querySelector('.viz-title');
73
+ expect(titleBefore?.textContent).toBe('Test Graph');
74
+ await rerender({ spec: updatedSpec });
75
+ await waitFor(() => {
76
+ expect(container.querySelector('.viz-title')?.textContent).toBe('Updated Graph');
77
+ });
78
+ });
79
+ it('unmounting cleans up graph instance', async () => {
80
+ const { container, unmount } = await renderGraph({ spec: basicSpec });
81
+ const canvasBefore = container.querySelector('canvas');
82
+ expect(canvasBefore).not.toBeNull();
83
+ unmount();
84
+ expect(container.querySelector('canvas')).toBeNull();
85
+ });
86
+ it('className prop passes through to wrapper div', async () => {
87
+ const { container } = await renderGraph({ spec: basicSpec, class: 'my-graph' });
88
+ const wrapper = container.firstElementChild;
89
+ expect(wrapper?.className).toContain('my-graph');
90
+ });
91
+ it('style prop passes through to wrapper div', async () => {
92
+ const { container } = await renderGraph({
93
+ spec: basicSpec,
94
+ style: 'border: 1px solid red',
95
+ });
96
+ const wrapper = container.firstElementChild;
97
+ expect(wrapper?.style.border).toBe('1px solid red');
98
+ });
99
+ it('renders with dark mode option', async () => {
100
+ const { container } = await renderGraph({ spec: basicSpec, darkMode: 'force' });
101
+ const canvas = container.querySelector('canvas');
102
+ expect(canvas).not.toBeNull();
103
+ });
104
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tests for VizThemeProvider context propagation.
3
+ *
4
+ * Tests that theme and dark mode contexts are correctly provided to
5
+ * descendant Chart components via the VizThemeProvider.
6
+ *
7
+ * Since testing context requires Svelte components that consume context,
8
+ * we test through the Chart component which reads context via
9
+ * getVizTheme() and getVizDarkMode().
10
+ */
11
+ import { cleanup, render, waitFor } from '@testing-library/svelte';
12
+ import { afterEach, describe, expect, it } from 'vitest';
13
+ import Chart from '../Chart.svelte';
14
+ import { DARK_MODE_KEY, THEME_KEY } from '../context.js';
15
+ // ---------------------------------------------------------------------------
16
+ // Test data
17
+ // ---------------------------------------------------------------------------
18
+ const minimalSpec = {
19
+ type: 'bar',
20
+ data: [
21
+ { name: 'A', value: 10 },
22
+ { name: 'B', value: 20 },
23
+ ],
24
+ encoding: {
25
+ x: { field: 'value', type: 'quantitative' },
26
+ y: { field: 'name', type: 'nominal' },
27
+ },
28
+ };
29
+ // ---------------------------------------------------------------------------
30
+ // Tests
31
+ // ---------------------------------------------------------------------------
32
+ afterEach(() => {
33
+ cleanup();
34
+ });
35
+ describe('Theme context', () => {
36
+ it('Chart renders without context (graceful fallback)', async () => {
37
+ const { container } = render(Chart, { props: { spec: minimalSpec } });
38
+ await waitFor(() => {
39
+ expect(container.querySelector('svg')).not.toBeNull();
40
+ });
41
+ });
42
+ it('Chart receives theme from context', async () => {
43
+ const theme = {
44
+ colors: { categorical: ['#ff0000', '#00ff00', '#0000ff'] },
45
+ };
46
+ const { container } = render(Chart, {
47
+ props: { spec: minimalSpec },
48
+ context: new Map([[THEME_KEY, () => theme]]),
49
+ });
50
+ await waitFor(() => {
51
+ expect(container.querySelector('svg')).not.toBeNull();
52
+ });
53
+ });
54
+ it('Chart receives dark mode from context', async () => {
55
+ const { container } = render(Chart, {
56
+ props: { spec: minimalSpec },
57
+ context: new Map([[DARK_MODE_KEY, () => 'force']]),
58
+ });
59
+ await waitFor(() => {
60
+ expect(container.querySelector('svg')).not.toBeNull();
61
+ });
62
+ });
63
+ it('explicit theme prop overrides context theme', async () => {
64
+ const contextTheme = {
65
+ colors: { categorical: ['#111'] },
66
+ };
67
+ const propTheme = {
68
+ colors: { categorical: ['#222'] },
69
+ };
70
+ // When both context and prop are provided, prop should take priority.
71
+ // We just verify it renders successfully (the override logic is in the component).
72
+ const { container } = render(Chart, {
73
+ props: { spec: minimalSpec, theme: propTheme },
74
+ context: new Map([[THEME_KEY, () => contextTheme]]),
75
+ });
76
+ await waitFor(() => {
77
+ expect(container.querySelector('svg')).not.toBeNull();
78
+ });
79
+ });
80
+ it('explicit darkMode prop overrides context darkMode', async () => {
81
+ const { container } = render(Chart, {
82
+ props: { spec: minimalSpec, darkMode: 'off' },
83
+ context: new Map([[DARK_MODE_KEY, () => 'force']]),
84
+ });
85
+ await waitFor(() => {
86
+ expect(container.querySelector('svg')).not.toBeNull();
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Tests for useDarkMode composable.
3
+ *
4
+ * Since useDarkMode uses Svelte 5 runes ($state, $effect), it needs to run
5
+ * in a Svelte component context. We test by rendering a Chart component with
6
+ * dark mode options and verifying it renders correctly.
7
+ *
8
+ * Direct unit testing of rune-based composables outside a component context
9
+ * is limited in Svelte 5, so we test the observable behavior through components.
10
+ */
11
+ import { cleanup, render, waitFor } from '@testing-library/svelte';
12
+ import { afterEach, describe, expect, it, vi } from 'vitest';
13
+ import Chart from '../Chart.svelte';
14
+ // ---------------------------------------------------------------------------
15
+ // Test data
16
+ // ---------------------------------------------------------------------------
17
+ const barSpec = {
18
+ type: 'bar',
19
+ data: [
20
+ { name: 'A', value: 10 },
21
+ { name: 'B', value: 30 },
22
+ ],
23
+ encoding: {
24
+ x: { field: 'value', type: 'quantitative' },
25
+ y: { field: 'name', type: 'nominal' },
26
+ },
27
+ };
28
+ // ---------------------------------------------------------------------------
29
+ // Tests
30
+ // ---------------------------------------------------------------------------
31
+ afterEach(() => {
32
+ cleanup();
33
+ });
34
+ describe('dark mode rendering', () => {
35
+ it('renders without dark mode', async () => {
36
+ const { container } = render(Chart, { props: { spec: barSpec } });
37
+ await waitFor(() => {
38
+ expect(container.querySelector('svg')).not.toBeNull();
39
+ });
40
+ });
41
+ it('renders with dark mode off', async () => {
42
+ const { container } = render(Chart, {
43
+ props: { spec: barSpec, darkMode: 'off' },
44
+ });
45
+ await waitFor(() => {
46
+ expect(container.querySelector('svg')).not.toBeNull();
47
+ });
48
+ });
49
+ it('renders with dark mode forced', async () => {
50
+ const { container } = render(Chart, {
51
+ props: { spec: barSpec, darkMode: 'force' },
52
+ });
53
+ await waitFor(() => {
54
+ expect(container.querySelector('svg')).not.toBeNull();
55
+ });
56
+ });
57
+ it('renders with dark mode auto', async () => {
58
+ const spy = vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
59
+ matches: query === '(prefers-color-scheme: dark)',
60
+ media: query,
61
+ addEventListener: vi.fn(),
62
+ removeEventListener: vi.fn(),
63
+ }));
64
+ const { container } = render(Chart, {
65
+ props: { spec: barSpec, darkMode: 'auto' },
66
+ });
67
+ await waitFor(() => {
68
+ expect(container.querySelector('svg')).not.toBeNull();
69
+ });
70
+ spy.mockRestore();
71
+ });
72
+ it('switching dark mode re-renders chart', async () => {
73
+ const { container, rerender } = render(Chart, {
74
+ props: { spec: barSpec, darkMode: 'off' },
75
+ });
76
+ await waitFor(() => {
77
+ expect(container.querySelector('svg')).not.toBeNull();
78
+ });
79
+ await rerender({ spec: barSpec, darkMode: 'force' });
80
+ await waitFor(() => {
81
+ expect(container.querySelector('svg')).not.toBeNull();
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Tests for useTableState composable.
3
+ *
4
+ * Since useTableState uses $state runes, it requires running within a Svelte
5
+ * reactive context. We test by verifying the returned object's behavior
6
+ * directly since $state creates reactive getters/setters that work
7
+ * synchronously outside of $effect tracking.
8
+ */
9
+ import { describe, expect, it } from 'vitest';
10
+ import { useTableState } from '../composables/useTableState.svelte.js';
11
+ // ---------------------------------------------------------------------------
12
+ // Tests
13
+ // ---------------------------------------------------------------------------
14
+ describe('useTableState', () => {
15
+ // -----------------------------------------------------------------------
16
+ // Default initial state
17
+ // -----------------------------------------------------------------------
18
+ it('initializes with default values when no options provided', () => {
19
+ const state = useTableState();
20
+ expect(state.sort).toBeNull();
21
+ expect(state.search).toBe('');
22
+ expect(state.page).toBe(0);
23
+ });
24
+ // -----------------------------------------------------------------------
25
+ // Custom initial state
26
+ // -----------------------------------------------------------------------
27
+ it('initializes with provided sort state', () => {
28
+ const state = useTableState({
29
+ sort: { column: 'name', direction: 'asc' },
30
+ });
31
+ expect(state.sort).toEqual({ column: 'name', direction: 'asc' });
32
+ });
33
+ it('initializes with provided search string', () => {
34
+ const state = useTableState({ search: 'hello' });
35
+ expect(state.search).toBe('hello');
36
+ });
37
+ it('initializes with provided page number', () => {
38
+ const state = useTableState({ page: 3 });
39
+ expect(state.page).toBe(3);
40
+ });
41
+ // -----------------------------------------------------------------------
42
+ // State setters
43
+ // -----------------------------------------------------------------------
44
+ it('setSort updates the sort state', () => {
45
+ const state = useTableState();
46
+ state.setSort({ column: 'age', direction: 'desc' });
47
+ expect(state.sort).toEqual({ column: 'age', direction: 'desc' });
48
+ });
49
+ it('setSort can clear sort by passing null', () => {
50
+ const state = useTableState({
51
+ sort: { column: 'name', direction: 'asc' },
52
+ });
53
+ expect(state.sort).toEqual({ column: 'name', direction: 'asc' });
54
+ state.setSort(null);
55
+ expect(state.sort).toBeNull();
56
+ });
57
+ it('setSearch updates the search query', () => {
58
+ const state = useTableState();
59
+ state.setSearch('filter text');
60
+ expect(state.search).toBe('filter text');
61
+ });
62
+ it('setPage updates the current page', () => {
63
+ const state = useTableState();
64
+ state.setPage(5);
65
+ expect(state.page).toBe(5);
66
+ });
67
+ // -----------------------------------------------------------------------
68
+ // resetState
69
+ // -----------------------------------------------------------------------
70
+ it('resetState restores default initial values', () => {
71
+ const state = useTableState();
72
+ // Change all values
73
+ state.setSort({ column: 'name', direction: 'asc' });
74
+ state.setSearch('filter text');
75
+ state.setPage(5);
76
+ expect(state.sort).toEqual({ column: 'name', direction: 'asc' });
77
+ expect(state.search).toBe('filter text');
78
+ expect(state.page).toBe(5);
79
+ // Reset
80
+ state.resetState();
81
+ expect(state.sort).toBeNull();
82
+ expect(state.search).toBe('');
83
+ expect(state.page).toBe(0);
84
+ });
85
+ it('resetState restores custom initial values', () => {
86
+ const initialState = {
87
+ sort: { column: 'age', direction: 'desc' },
88
+ search: 'initial',
89
+ page: 1,
90
+ };
91
+ const state = useTableState(initialState);
92
+ // Change all values
93
+ state.setSort(null);
94
+ state.setSearch('changed');
95
+ state.setPage(5);
96
+ expect(state.sort).toBeNull();
97
+ expect(state.search).toBe('changed');
98
+ expect(state.page).toBe(5);
99
+ // Reset to initial
100
+ state.resetState();
101
+ expect(state.sort).toEqual({ column: 'age', direction: 'desc' });
102
+ expect(state.search).toBe('initial');
103
+ expect(state.page).toBe(1);
104
+ });
105
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * useChart: composable for manual chart lifecycle control.
3
+ *
4
+ * Returns a Svelte action function for use with `use:chart` directive
5
+ * and exposes the chart instance and compiled layout.
6
+ *
7
+ * Usage:
8
+ * ```svelte
9
+ * <script>
10
+ * const { action, chart, layout } = useChart(spec);
11
+ * </script>
12
+ * <div use:action></div>
13
+ * ```
14
+ *
15
+ * Uses .svelte.ts extension so runes ($state, $effect) work outside
16
+ * .svelte components.
17
+ */
18
+ import type { ChartLayout, ChartSpec, GraphSpec } from '@opendata-ai/openchart-core';
19
+ import { type ChartInstance, type MountOptions } from '@opendata-ai/openchart-vanilla';
20
+ export interface UseChartOptions {
21
+ /** Theme overrides. */
22
+ theme?: MountOptions['theme'];
23
+ /** Dark mode setting. */
24
+ darkMode?: MountOptions['darkMode'];
25
+ /** Data point click handler. */
26
+ onDataPointClick?: MountOptions['onDataPointClick'];
27
+ /** Enable responsive resizing. Defaults to true. */
28
+ responsive?: boolean;
29
+ }
30
+ export interface UseChartReturn {
31
+ /** Svelte action to attach to a container div. */
32
+ action: (node: HTMLElement) => {
33
+ destroy: () => void;
34
+ };
35
+ /** The chart instance (null until mounted). */
36
+ readonly chart: ChartInstance | null;
37
+ /** The current compiled layout (null until mounted). */
38
+ readonly layout: ChartLayout | null;
39
+ }
40
+ export declare function useChart(spec: () => ChartSpec | GraphSpec, options?: () => UseChartOptions | undefined): UseChartReturn;
41
+ //# sourceMappingURL=useChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useChart.svelte.d.ts","sourceRoot":"","sources":["../../src/composables/useChart.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACrF,OAAO,EAAE,KAAK,aAAa,EAAe,KAAK,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGpG,MAAM,WAAW,eAAe;IAC9B,uBAAuB;IACvB,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IAC9B,yBAAyB;IACzB,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;IACpC,gCAAgC;IAChC,gBAAgB,CAAC,EAAE,YAAY,CAAC,kBAAkB,CAAC,CAAC;IACpD,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK;QAAE,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACvD,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CACrC;AAED,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,SAAS,GAAG,SAAS,EACjC,OAAO,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,GAC1C,cAAc,CA8DhB"}