@svgrid/grid 2.2.0 → 2.2.1

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 (114) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/GridFooter.svelte +178 -178
  3. package/dist/SvCarousel.svelte +141 -141
  4. package/dist/SvColorInput.svelte +187 -187
  5. package/dist/SvContextMenu.svelte +116 -116
  6. package/dist/SvDrawer.svelte +250 -250
  7. package/dist/SvGrid.svelte +3131 -3131
  8. package/dist/SvGridBoard.svelte +2339 -2339
  9. package/dist/SvGridChart.svelte +1724 -1724
  10. package/dist/SvGridChartPanel.svelte +485 -485
  11. package/dist/SvGridDropdown.svelte +689 -689
  12. package/dist/SvGroupCell.svelte +109 -109
  13. package/dist/SvMenu.svelte +124 -124
  14. package/dist/SvMenuList.svelte +117 -117
  15. package/dist/SvNumberInput.svelte +131 -131
  16. package/dist/SvOtpInput.svelte +158 -158
  17. package/dist/SvPopover.svelte +139 -139
  18. package/dist/SvRowGroupPanel.svelte +170 -170
  19. package/dist/SvScrollArea.svelte +61 -61
  20. package/dist/SvTooltip.svelte +127 -127
  21. package/dist/SvTour.svelte +204 -204
  22. package/dist/SvTree.svelte +377 -377
  23. package/dist/SvTreeSelect.svelte +230 -230
  24. package/dist/cdn/svgrid.js +8 -8
  25. package/dist/cdn/svgrid.svelte-external.js +8 -8
  26. package/dist/chart-export.js +8 -8
  27. package/package.json +10 -10
  28. package/src/FlexRender.svelte +96 -96
  29. package/src/GridFooter.svelte +178 -178
  30. package/src/SvCarousel.svelte +141 -141
  31. package/src/SvColorInput.svelte +187 -187
  32. package/src/SvContextMenu.svelte +116 -116
  33. package/src/SvDrawer.svelte +250 -250
  34. package/src/SvGrid.controller.svelte.ts +3158 -3158
  35. package/src/SvGrid.svelte +3131 -3131
  36. package/src/SvGrid.types.ts +1489 -1489
  37. package/src/SvGridBoard.svelte +2339 -2339
  38. package/src/SvGridChart.svelte +1724 -1724
  39. package/src/SvGridChartPanel.svelte +485 -485
  40. package/src/SvGridDropdown.svelte +689 -689
  41. package/src/SvGroupCell.svelte +109 -109
  42. package/src/SvMenu.svelte +124 -124
  43. package/src/SvMenu.test.ts +97 -97
  44. package/src/SvMenuList.svelte +117 -117
  45. package/src/SvNumberInput.svelte +131 -131
  46. package/src/SvOtpInput.svelte +158 -158
  47. package/src/SvPopover.svelte +139 -139
  48. package/src/SvRowGroupPanel.svelte +170 -170
  49. package/src/SvScrollArea.svelte +61 -61
  50. package/src/SvTooltip.svelte +127 -127
  51. package/src/SvTour.svelte +204 -204
  52. package/src/SvTree.svelte +377 -377
  53. package/src/SvTreeSelect.svelte +230 -230
  54. package/src/a11y.contract.test.ts +49 -49
  55. package/src/a11y.test.ts +59 -59
  56. package/src/a11y.ts +61 -61
  57. package/src/build-api.ts +870 -870
  58. package/src/cell-formatting.ts +169 -169
  59. package/src/cell-render.ts +469 -469
  60. package/src/chart-export.ts +201 -201
  61. package/src/chart.ts +2296 -2296
  62. package/src/collaboration.test.ts +104 -104
  63. package/src/collaboration.ts +167 -167
  64. package/src/core.performance.test.ts +30 -30
  65. package/src/core.ts +1111 -1111
  66. package/src/createAutocomplete.svelte.ts +132 -132
  67. package/src/createCombobox.svelte.ts +179 -179
  68. package/src/createCountryInput.svelte.ts +157 -157
  69. package/src/createDropdownList.svelte.ts +162 -162
  70. package/src/createGrid.svelte.ts +42 -42
  71. package/src/createGrid.test.ts +10 -10
  72. package/src/createGridState.svelte.ts +17 -17
  73. package/src/createMenu.svelte.ts +202 -202
  74. package/src/createPopoverSelect.svelte.ts +206 -206
  75. package/src/createTree.svelte.ts +319 -319
  76. package/src/editing.test.ts +859 -859
  77. package/src/editing.ts +675 -675
  78. package/src/export-data-api.test.ts +126 -126
  79. package/src/export-format.test.ts +107 -107
  80. package/src/export-format.ts +598 -598
  81. package/src/flex-render.ts +3 -3
  82. package/src/index.ts +623 -623
  83. package/src/keyboard.test.ts +59 -59
  84. package/src/keyboard.ts +97 -97
  85. package/src/list-nav.test.ts +49 -49
  86. package/src/list-nav.ts +29 -29
  87. package/src/menus.ts +639 -639
  88. package/src/merge-objects.ts +48 -48
  89. package/src/overlays.test.ts +90 -90
  90. package/src/render-component.ts +28 -28
  91. package/src/selection.test.ts +754 -754
  92. package/src/selection.ts +600 -600
  93. package/src/server-data-source.test.ts +289 -289
  94. package/src/server-data-source.ts +413 -413
  95. package/src/sparkline.test.ts +68 -68
  96. package/src/sparkline.ts +169 -169
  97. package/src/spreadsheet.test.ts +489 -489
  98. package/src/spreadsheet.ts +312 -312
  99. package/src/static-functions.ts +11 -11
  100. package/src/subscribe.ts +38 -38
  101. package/src/svgrid-wrapper.types.ts +501 -501
  102. package/src/svgrid.behavior.test.ts +706 -706
  103. package/src/svgrid.charting.test.ts +527 -527
  104. package/src/svgrid.features.test.ts +157 -157
  105. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  106. package/src/svgrid.wrapper.test.ts +40 -40
  107. package/src/test-setup.ts +62 -62
  108. package/src/themes/index.ts +159 -159
  109. package/src/virtualization/column-virtualizer.test.ts +27 -27
  110. package/src/virtualization/column-virtualizer.ts +30 -30
  111. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  112. package/src/virtualization/types.ts +30 -30
  113. package/src/virtualization/virtualizer.test.ts +47 -47
  114. package/src/virtualization/virtualizer.ts +296 -296
@@ -1,230 +1,230 @@
1
- <script lang="ts" module>
2
- export type { TreeSelectNode } from './ui-app.types'
3
- </script>
4
-
5
- <script lang="ts">
6
- /**
7
- * SvTreeSelect - a single-select dropdown that shows an indented, collapsible
8
- * tree in its panel (pick a node from a hierarchy). The open / position /
9
- * dismiss / roving / keyboard / ARIA mechanics come from the shared
10
- * `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
11
- * expand/collapse); this component adds the tree flattening + rendering. Covers
12
- * the "cascader / tree select" pattern without depending on SvTree.
13
- */
14
- import type { TreeSelectNode } from './ui-app.types'
15
- import SvField from './SvField.svelte'
16
- import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
17
- import { portalToBody, popIn } from './popover'
18
- import { createPopoverSelect } from './createPopoverSelect.svelte'
19
- import { flattenVisible, findNodePath } from './tree-select'
20
-
21
- type Props = Pick<
22
- SvEditorProps,
23
- 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'
24
- > & {
25
- nodes: ReadonlyArray<TreeSelectNode>
26
- value?: string | number | null
27
- onChange?: (value: string | number) => void
28
- onCommit?: (value: string | number) => void
29
- onCancel?: () => void
30
- placeholder?: string
31
- showPath?: boolean
32
- expandedIds?: ReadonlyArray<string | number>
33
- }
34
-
35
- let {
36
- nodes,
37
- value = $bindable<string | number | null>(null),
38
- onChange,
39
- onCommit,
40
- onCancel,
41
- placeholder = 'Select…',
42
- showPath = false,
43
- expandedIds = [],
44
- disabled = false,
45
- label,
46
- hint,
47
- error,
48
- required = false,
49
- invalid = false,
50
- size = 'md',
51
- id,
52
- name,
53
- dir,
54
- ariaLabel,
55
- }: Props = $props()
56
-
57
- const autoId = nextEditorId('sv-ts')
58
- const uid = $derived(id ?? autoId)
59
-
60
- let expanded = $state(new Set<string | number>(expandedIds))
61
- let triggerEl = $state<HTMLButtonElement | null>(null)
62
- let panelEl = $state<HTMLDivElement | null>(null)
63
- let treeEl = $state<HTMLUListElement | null>(null)
64
-
65
- const rows = $derived(flattenVisible(nodes, expanded))
66
- const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
67
- const triggerText = $derived(
68
- !selectedPath
69
- ? ''
70
- : showPath
71
- ? selectedPath.map((n) => n.label).join(' / ')
72
- : selectedPath[selectedPath.length - 1]!.label,
73
- )
74
-
75
- function setExpanded(v: string | number, on: boolean) {
76
- const next = new Set(expanded)
77
- on ? next.add(v) : next.delete(v)
78
- expanded = next
79
- }
80
- function toggleExpand(v: string | number) {
81
- setExpanded(v, !expanded.has(v))
82
- }
83
- function pick(node: TreeSelectNode) {
84
- if (node.disabled) return
85
- value = node.value // enables bind:value
86
- onChange?.(node.value)
87
- ts.closePanel()
88
- }
89
- function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
90
- if (row.hasChildren) toggleExpand(row.node.value)
91
- else pick(row.node)
92
- }
93
-
94
- const ts = createPopoverSelect({
95
- itemCount: () => rows.length,
96
- disabled: (i) => !!rows[i]?.node.disabled,
97
- onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
98
- getTrigger: () => triggerEl,
99
- getPanel: () => panelEl,
100
- getInitialFocus: () => treeEl,
101
- closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
102
- initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
103
- minWidth: () => 220,
104
- estimatedHeight: () => 300,
105
- onExtraKey: (e, act) => {
106
- const row = rows[act]
107
- if (!row) return false
108
- // "expand" opens the node or steps into the first child; "collapse" closes
109
- // it or jumps to the parent - same fallback as createTree's core.
110
- const expand = () => {
111
- if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
112
- else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
113
- }
114
- const collapse = () => {
115
- if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
116
- else {
117
- let pi = act - 1
118
- while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
119
- if (pi >= 0) ts.active = pi
120
- }
121
- }
122
- if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
123
- if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
124
- return false
125
- },
126
- idPrefix: 'sv-ts',
127
- })
128
- </script>
129
-
130
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
131
- <div class="sv-ts sv-ts--{size}" class:is-disabled={disabled}>
132
- <button
133
- bind:this={triggerEl}
134
- type="button"
135
- class="sv-ts__trigger"
136
- class:is-invalid={invalid}
137
- {disabled}
138
- onclick={() => ts.toggle()}
139
- {...ts.triggerProps('tree')}
140
- {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
141
- >
142
- <span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
143
- <span class="sv-ts__arrow" aria-hidden="true">▾</span>
144
- </button>
145
- {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
146
- </div>
147
- </SvField>
148
-
149
- {#if ts.open}
150
- <div
151
- bind:this={panelEl}
152
- class="sv-ts__panel"
153
- dir={dir}
154
- use:portalToBody
155
- use:popIn={{ up: ts.rect.openUpward }}
156
- style:position="fixed"
157
- style:top={`${ts.rect.top}px`}
158
- style:left={`${ts.rect.left}px`}
159
- style:min-width={`${ts.rect.width}px`}
160
- >
161
- <ul
162
- bind:this={treeEl}
163
- id={ts.panelId}
164
- class="sv-ts__tree"
165
- role="tree"
166
- aria-label={label ?? ariaLabel ?? 'Tree'}
167
- aria-activedescendant={ts.activeDescendant()}
168
- tabindex="0"
169
- style:max-height={`${ts.rect.maxHeight}px`}
170
- onkeydown={ts.onPanelKeydown}
171
- >
172
- {#each rows as row, i (row.node.value)}
173
- <li
174
- class="sv-ts__row"
175
- class:is-active={ts.isActive(i)}
176
- class:is-selected={row.node.value === value}
177
- class:is-disabled={row.node.disabled}
178
- role="treeitem"
179
- aria-selected={row.node.value === value}
180
- aria-expanded={row.hasChildren ? row.expanded : undefined}
181
- style:padding-inline-start={`${8 + row.depth * 16}px`}
182
- onclick={() => onRowClick(row)}
183
- {...ts.itemProps(i)}
184
- >
185
- {#if row.hasChildren}
186
- <span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">▸</span>
187
- {:else}
188
- <span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
189
- {/if}
190
- <span class="sv-ts__label">{row.node.label}</span>
191
- </li>
192
- {/each}
193
- </ul>
194
- </div>
195
- {/if}
196
-
197
- <style>
198
- .sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
199
- .sv-ts.is-disabled { opacity: 0.6; }
200
- .sv-ts__trigger {
201
- flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
202
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
203
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
204
- font: inherit; cursor: pointer; padding: 0 9px;
205
- }
206
- .sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
207
- .sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
208
- .sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
209
- .sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
210
- .sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
211
- .sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
212
- .sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
213
- .sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
214
-
215
- :global(.sv-ts__panel) {
216
- z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
217
- background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
218
- border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
219
- box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
220
- }
221
- :global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
222
- :global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
223
- :global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
224
- :global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
225
- :global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
226
- :global(.sv-ts__chev) { width: 14px; flex: none; color: var(--sg-muted, #64748b); transition: transform 0.12s; font-size: 10px; }
227
- :global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
228
- :global(.sv-ts__chev--leaf) { visibility: hidden; }
229
- :global(.sv-ts__label) { flex: 1; white-space: nowrap; }
230
- </style>
1
+ <script lang="ts" module>
2
+ export type { TreeSelectNode } from './ui-app.types'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvTreeSelect - a single-select dropdown that shows an indented, collapsible
8
+ * tree in its panel (pick a node from a hierarchy). The open / position /
9
+ * dismiss / roving / keyboard / ARIA mechanics come from the shared
10
+ * `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
11
+ * expand/collapse); this component adds the tree flattening + rendering. Covers
12
+ * the "cascader / tree select" pattern without depending on SvTree.
13
+ */
14
+ import type { TreeSelectNode } from './ui-app.types'
15
+ import SvField from './SvField.svelte'
16
+ import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
17
+ import { portalToBody, popIn } from './popover'
18
+ import { createPopoverSelect } from './createPopoverSelect.svelte'
19
+ import { flattenVisible, findNodePath } from './tree-select'
20
+
21
+ type Props = Pick<
22
+ SvEditorProps,
23
+ 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'
24
+ > & {
25
+ nodes: ReadonlyArray<TreeSelectNode>
26
+ value?: string | number | null
27
+ onChange?: (value: string | number) => void
28
+ onCommit?: (value: string | number) => void
29
+ onCancel?: () => void
30
+ placeholder?: string
31
+ showPath?: boolean
32
+ expandedIds?: ReadonlyArray<string | number>
33
+ }
34
+
35
+ let {
36
+ nodes,
37
+ value = $bindable<string | number | null>(null),
38
+ onChange,
39
+ onCommit,
40
+ onCancel,
41
+ placeholder = 'Select…',
42
+ showPath = false,
43
+ expandedIds = [],
44
+ disabled = false,
45
+ label,
46
+ hint,
47
+ error,
48
+ required = false,
49
+ invalid = false,
50
+ size = 'md',
51
+ id,
52
+ name,
53
+ dir,
54
+ ariaLabel,
55
+ }: Props = $props()
56
+
57
+ const autoId = nextEditorId('sv-ts')
58
+ const uid = $derived(id ?? autoId)
59
+
60
+ let expanded = $state(new Set<string | number>(expandedIds))
61
+ let triggerEl = $state<HTMLButtonElement | null>(null)
62
+ let panelEl = $state<HTMLDivElement | null>(null)
63
+ let treeEl = $state<HTMLUListElement | null>(null)
64
+
65
+ const rows = $derived(flattenVisible(nodes, expanded))
66
+ const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
67
+ const triggerText = $derived(
68
+ !selectedPath
69
+ ? ''
70
+ : showPath
71
+ ? selectedPath.map((n) => n.label).join(' / ')
72
+ : selectedPath[selectedPath.length - 1]!.label,
73
+ )
74
+
75
+ function setExpanded(v: string | number, on: boolean) {
76
+ const next = new Set(expanded)
77
+ on ? next.add(v) : next.delete(v)
78
+ expanded = next
79
+ }
80
+ function toggleExpand(v: string | number) {
81
+ setExpanded(v, !expanded.has(v))
82
+ }
83
+ function pick(node: TreeSelectNode) {
84
+ if (node.disabled) return
85
+ value = node.value // enables bind:value
86
+ onChange?.(node.value)
87
+ ts.closePanel()
88
+ }
89
+ function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
90
+ if (row.hasChildren) toggleExpand(row.node.value)
91
+ else pick(row.node)
92
+ }
93
+
94
+ const ts = createPopoverSelect({
95
+ itemCount: () => rows.length,
96
+ disabled: (i) => !!rows[i]?.node.disabled,
97
+ onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
98
+ getTrigger: () => triggerEl,
99
+ getPanel: () => panelEl,
100
+ getInitialFocus: () => treeEl,
101
+ closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
102
+ initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
103
+ minWidth: () => 220,
104
+ estimatedHeight: () => 300,
105
+ onExtraKey: (e, act) => {
106
+ const row = rows[act]
107
+ if (!row) return false
108
+ // "expand" opens the node or steps into the first child; "collapse" closes
109
+ // it or jumps to the parent - same fallback as createTree's core.
110
+ const expand = () => {
111
+ if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
112
+ else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
113
+ }
114
+ const collapse = () => {
115
+ if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
116
+ else {
117
+ let pi = act - 1
118
+ while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
119
+ if (pi >= 0) ts.active = pi
120
+ }
121
+ }
122
+ if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
123
+ if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
124
+ return false
125
+ },
126
+ idPrefix: 'sv-ts',
127
+ })
128
+ </script>
129
+
130
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
131
+ <div class="sv-ts sv-ts--{size}" class:is-disabled={disabled}>
132
+ <button
133
+ bind:this={triggerEl}
134
+ type="button"
135
+ class="sv-ts__trigger"
136
+ class:is-invalid={invalid}
137
+ {disabled}
138
+ onclick={() => ts.toggle()}
139
+ {...ts.triggerProps('tree')}
140
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
141
+ >
142
+ <span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
143
+ <span class="sv-ts__arrow" aria-hidden="true">▾</span>
144
+ </button>
145
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
146
+ </div>
147
+ </SvField>
148
+
149
+ {#if ts.open}
150
+ <div
151
+ bind:this={panelEl}
152
+ class="sv-ts__panel"
153
+ dir={dir}
154
+ use:portalToBody
155
+ use:popIn={{ up: ts.rect.openUpward }}
156
+ style:position="fixed"
157
+ style:top={`${ts.rect.top}px`}
158
+ style:left={`${ts.rect.left}px`}
159
+ style:min-width={`${ts.rect.width}px`}
160
+ >
161
+ <ul
162
+ bind:this={treeEl}
163
+ id={ts.panelId}
164
+ class="sv-ts__tree"
165
+ role="tree"
166
+ aria-label={label ?? ariaLabel ?? 'Tree'}
167
+ aria-activedescendant={ts.activeDescendant()}
168
+ tabindex="0"
169
+ style:max-height={`${ts.rect.maxHeight}px`}
170
+ onkeydown={ts.onPanelKeydown}
171
+ >
172
+ {#each rows as row, i (row.node.value)}
173
+ <li
174
+ class="sv-ts__row"
175
+ class:is-active={ts.isActive(i)}
176
+ class:is-selected={row.node.value === value}
177
+ class:is-disabled={row.node.disabled}
178
+ role="treeitem"
179
+ aria-selected={row.node.value === value}
180
+ aria-expanded={row.hasChildren ? row.expanded : undefined}
181
+ style:padding-inline-start={`${8 + row.depth * 16}px`}
182
+ onclick={() => onRowClick(row)}
183
+ {...ts.itemProps(i)}
184
+ >
185
+ {#if row.hasChildren}
186
+ <span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">▸</span>
187
+ {:else}
188
+ <span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
189
+ {/if}
190
+ <span class="sv-ts__label">{row.node.label}</span>
191
+ </li>
192
+ {/each}
193
+ </ul>
194
+ </div>
195
+ {/if}
196
+
197
+ <style>
198
+ .sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
199
+ .sv-ts.is-disabled { opacity: 0.6; }
200
+ .sv-ts__trigger {
201
+ flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
202
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
203
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
204
+ font: inherit; cursor: pointer; padding: 0 9px;
205
+ }
206
+ .sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
207
+ .sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
208
+ .sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
209
+ .sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
210
+ .sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
211
+ .sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
212
+ .sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
213
+ .sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
214
+
215
+ :global(.sv-ts__panel) {
216
+ z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
217
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
218
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
219
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
220
+ }
221
+ :global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
222
+ :global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
223
+ :global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
224
+ :global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
225
+ :global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
226
+ :global(.sv-ts__chev) { width: 14px; flex: none; color: var(--sg-muted, #64748b); transition: transform 0.12s; font-size: 10px; }
227
+ :global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
228
+ :global(.sv-ts__chev--leaf) { visibility: hidden; }
229
+ :global(.sv-ts__label) { flex: 1; white-space: nowrap; }
230
+ </style>
@@ -1,49 +1,49 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- getGridCellA11yProps,
4
- getGridHeaderA11yProps,
5
- getGridRootA11yProps,
6
- getGridRowA11yProps,
7
- } from './a11y'
8
-
9
- function attrsToHtml(attrs: Record<string, string | number | boolean>) {
10
- return Object.entries(attrs)
11
- .map(([key, value]) => `${key}="${String(value)}"`)
12
- .join(' ')
13
- }
14
-
15
- describe('a11y contract rendering', () => {
16
- it('builds expected headless markup attributes', () => {
17
- const root = getGridRootA11yProps()
18
- const header = getGridHeaderA11yProps({
19
- sortable: true,
20
- sortDirection: 'descending',
21
- })
22
- const row = getGridRowA11yProps(1)
23
- const cell = getGridCellA11yProps({
24
- rowIndex: 1,
25
- colIndex: 2,
26
- selected: true,
27
- })
28
-
29
- const html = [
30
- `<table ${attrsToHtml(root)}><thead>`,
31
- `<tr ${attrsToHtml(row)}>`,
32
- `<th ${attrsToHtml(header)}>Age</th>`,
33
- `</tr></thead><tbody>`,
34
- `<tr ${attrsToHtml(row)}><td ${attrsToHtml(cell)}>42</td></tr>`,
35
- `</tbody></table>`,
36
- ].join('')
37
-
38
- expect(html).toContain('role="grid"')
39
- expect(html).toContain('role="columnheader"')
40
- expect(html).toContain('aria-sort="descending"')
41
- expect(html).toContain('aria-selected="true"')
42
- expect(html).toContain('aria-colindex="2"')
43
- })
44
-
45
- it('creates stable active descendant ids', async () => {
46
- const { getGridCellDomId } = await import('./a11y')
47
- expect(getGridCellDomId('svgrid', 3, 4)).toBe('svgrid_cell_3_4')
48
- })
49
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ getGridCellA11yProps,
4
+ getGridHeaderA11yProps,
5
+ getGridRootA11yProps,
6
+ getGridRowA11yProps,
7
+ } from './a11y'
8
+
9
+ function attrsToHtml(attrs: Record<string, string | number | boolean>) {
10
+ return Object.entries(attrs)
11
+ .map(([key, value]) => `${key}="${String(value)}"`)
12
+ .join(' ')
13
+ }
14
+
15
+ describe('a11y contract rendering', () => {
16
+ it('builds expected headless markup attributes', () => {
17
+ const root = getGridRootA11yProps()
18
+ const header = getGridHeaderA11yProps({
19
+ sortable: true,
20
+ sortDirection: 'descending',
21
+ })
22
+ const row = getGridRowA11yProps(1)
23
+ const cell = getGridCellA11yProps({
24
+ rowIndex: 1,
25
+ colIndex: 2,
26
+ selected: true,
27
+ })
28
+
29
+ const html = [
30
+ `<table ${attrsToHtml(root)}><thead>`,
31
+ `<tr ${attrsToHtml(row)}>`,
32
+ `<th ${attrsToHtml(header)}>Age</th>`,
33
+ `</tr></thead><tbody>`,
34
+ `<tr ${attrsToHtml(row)}><td ${attrsToHtml(cell)}>42</td></tr>`,
35
+ `</tbody></table>`,
36
+ ].join('')
37
+
38
+ expect(html).toContain('role="grid"')
39
+ expect(html).toContain('role="columnheader"')
40
+ expect(html).toContain('aria-sort="descending"')
41
+ expect(html).toContain('aria-selected="true"')
42
+ expect(html).toContain('aria-colindex="2"')
43
+ })
44
+
45
+ it('creates stable active descendant ids', async () => {
46
+ const { getGridCellDomId } = await import('./a11y')
47
+ expect(getGridCellDomId('svgrid', 3, 4)).toBe('svgrid_cell_3_4')
48
+ })
49
+ })