@svgrid/grid 1.2.23 → 1.2.25

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 (141) hide show
  1. package/dist/SvAutoComplete.svelte +23 -34
  2. package/dist/SvCalendar.svelte +74 -334
  3. package/dist/SvCalendar.svelte.d.ts +5 -23
  4. package/dist/SvCheckBox.svelte +11 -10
  5. package/dist/SvColorInput.svelte +31 -58
  6. package/dist/SvComboBox.svelte +32 -54
  7. package/dist/SvCountryInput.svelte +31 -29
  8. package/dist/SvDateTimePicker.svelte +53 -110
  9. package/dist/SvDateTimePicker.svelte.d.ts +4 -3
  10. package/dist/SvDropDownList.svelte +31 -54
  11. package/dist/SvGauge.svelte +21 -23
  12. package/dist/SvGauge.svelte.d.ts +0 -5
  13. package/dist/SvListBox.svelte +25 -58
  14. package/dist/SvListBox.svelte.d.ts +3 -0
  15. package/dist/SvMaskedInput.svelte +0 -0
  16. package/dist/SvNumberInput.svelte +0 -0
  17. package/dist/SvNumberInput.svelte.d.ts +2 -6
  18. package/dist/SvPasswordInput.svelte +18 -28
  19. package/dist/SvPasswordInput.svelte.d.ts +0 -4
  20. package/dist/SvPhoneInput.svelte +0 -0
  21. package/dist/SvRadioGroup.svelte +14 -37
  22. package/dist/SvRadioGroup.svelte.d.ts +6 -9
  23. package/dist/SvRating.svelte +22 -42
  24. package/dist/SvRating.svelte.d.ts +3 -5
  25. package/dist/SvSlider.svelte +30 -57
  26. package/dist/SvSlider.svelte.d.ts +0 -5
  27. package/dist/SvSwitchButton.svelte +10 -17
  28. package/dist/SvSwitchButton.svelte.d.ts +0 -5
  29. package/dist/SvTabs.svelte +26 -39
  30. package/dist/SvTabs.svelte.d.ts +5 -5
  31. package/dist/SvTagsInput.svelte +17 -28
  32. package/dist/SvTagsInput.svelte.d.ts +0 -5
  33. package/dist/SvTimePicker.svelte +52 -210
  34. package/dist/SvTimePicker.svelte.d.ts +11 -8
  35. package/dist/SvToggleButton.svelte +9 -9
  36. package/dist/SvTree.svelte +30 -109
  37. package/dist/SvTree.svelte.d.ts +9 -11
  38. package/dist/cdn/svgrid.js +7488 -5839
  39. package/dist/cdn/svgrid.svelte-external.js +4691 -3028
  40. package/dist/createAutocomplete.svelte.d.ts +77 -0
  41. package/dist/createAutocomplete.svelte.js +100 -0
  42. package/dist/createCalendar.svelte.d.ts +192 -0
  43. package/dist/createCalendar.svelte.js +402 -0
  44. package/dist/createCheckbox.svelte.d.ts +45 -0
  45. package/dist/createCheckbox.svelte.js +26 -0
  46. package/dist/createColorInput.svelte.d.ts +67 -0
  47. package/dist/createColorInput.svelte.js +100 -0
  48. package/dist/createCombobox.svelte.d.ts +102 -0
  49. package/dist/createCombobox.svelte.js +171 -0
  50. package/dist/createCountryInput.svelte.d.ts +86 -0
  51. package/dist/createCountryInput.svelte.js +126 -0
  52. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  53. package/dist/createDateTimePicker.svelte.js +202 -0
  54. package/dist/createDropdownList.svelte.d.ts +78 -0
  55. package/dist/createDropdownList.svelte.js +117 -0
  56. package/dist/createGauge.svelte.d.ts +56 -0
  57. package/dist/createGauge.svelte.js +49 -0
  58. package/dist/createListbox.svelte.d.ts +79 -0
  59. package/dist/createListbox.svelte.js +117 -0
  60. package/dist/createMaskedInput.svelte.d.ts +30 -0
  61. package/dist/createMaskedInput.svelte.js +62 -0
  62. package/dist/createNumberInput.svelte.d.ts +72 -0
  63. package/dist/createNumberInput.svelte.js +167 -0
  64. package/dist/createPasswordInput.svelte.d.ts +60 -0
  65. package/dist/createPasswordInput.svelte.js +72 -0
  66. package/dist/createPhoneInput.svelte.d.ts +46 -0
  67. package/dist/createPhoneInput.svelte.js +105 -0
  68. package/dist/createRadioGroup.svelte.d.ts +64 -0
  69. package/dist/createRadioGroup.svelte.js +73 -0
  70. package/dist/createRating.svelte.d.ts +70 -0
  71. package/dist/createRating.svelte.js +85 -0
  72. package/dist/createSlider.svelte.d.ts +71 -0
  73. package/dist/createSlider.svelte.js +123 -0
  74. package/dist/createSwitch.svelte.d.ts +43 -0
  75. package/dist/createSwitch.svelte.js +41 -0
  76. package/dist/createTabs.svelte.d.ts +78 -0
  77. package/dist/createTabs.svelte.js +92 -0
  78. package/dist/createTagsInput.svelte.d.ts +65 -0
  79. package/dist/createTagsInput.svelte.js +93 -0
  80. package/dist/createTimePicker.svelte.d.ts +106 -0
  81. package/dist/createTimePicker.svelte.js +248 -0
  82. package/dist/createToggle.svelte.d.ts +43 -0
  83. package/dist/createToggle.svelte.js +39 -0
  84. package/dist/createTree.svelte.d.ts +96 -0
  85. package/dist/createTree.svelte.js +194 -0
  86. package/dist/editor-contract.d.ts +48 -0
  87. package/dist/editor-contract.js +26 -0
  88. package/dist/index.d.ts +24 -0
  89. package/dist/index.js +31 -0
  90. package/package.json +1 -1
  91. package/src/SvAutoComplete.svelte +23 -34
  92. package/src/SvCalendar.svelte +74 -334
  93. package/src/SvCheckBox.svelte +11 -10
  94. package/src/SvColorInput.svelte +31 -58
  95. package/src/SvComboBox.svelte +32 -54
  96. package/src/SvCountryInput.svelte +31 -29
  97. package/src/SvDateTimePicker.svelte +53 -110
  98. package/src/SvDropDownList.svelte +31 -54
  99. package/src/SvGauge.svelte +21 -23
  100. package/src/SvListBox.svelte +25 -58
  101. package/src/SvMaskedInput.svelte +0 -0
  102. package/src/SvNumberInput.svelte +0 -0
  103. package/src/SvPasswordInput.svelte +18 -28
  104. package/src/SvPhoneInput.svelte +0 -0
  105. package/src/SvRadioGroup.svelte +14 -37
  106. package/src/SvRating.svelte +22 -42
  107. package/src/SvSlider.svelte +30 -57
  108. package/src/SvSwitchButton.svelte +10 -17
  109. package/src/SvTabs.svelte +26 -39
  110. package/src/SvTagsInput.svelte +17 -28
  111. package/src/SvTimePicker.svelte +52 -210
  112. package/src/SvToggleButton.svelte +9 -9
  113. package/src/SvTree.svelte +30 -109
  114. package/src/createAutocomplete.svelte.ts +104 -0
  115. package/src/createCalendar.svelte.ts +488 -0
  116. package/src/createCheckbox.svelte.ts +74 -0
  117. package/src/createColorInput.svelte.ts +115 -0
  118. package/src/createCombobox.svelte.ts +158 -0
  119. package/src/createCountryInput.svelte.ts +126 -0
  120. package/src/createDateTimePicker.svelte.ts +216 -0
  121. package/src/createDropdownList.svelte.ts +125 -0
  122. package/src/createGauge.svelte.ts +86 -0
  123. package/src/createListbox.svelte.ts +186 -0
  124. package/src/createMaskedInput.svelte.ts +81 -0
  125. package/src/createNumberInput.svelte.ts +193 -0
  126. package/src/createPasswordInput.svelte.ts +87 -0
  127. package/src/createPhoneInput.svelte.ts +119 -0
  128. package/src/createRadioGroup.svelte.ts +132 -0
  129. package/src/createRating.svelte.ts +145 -0
  130. package/src/createSlider.svelte.ts +160 -0
  131. package/src/createSwitch.svelte.ts +78 -0
  132. package/src/createTabs.svelte.ts +129 -0
  133. package/src/createTagsInput.svelte.ts +98 -0
  134. package/src/createTimePicker.svelte.ts +289 -0
  135. package/src/createToggle.svelte.ts +69 -0
  136. package/src/createTree.svelte.ts +216 -0
  137. package/src/editor-contract.test.ts +32 -0
  138. package/src/editor-contract.ts +60 -0
  139. package/src/index.ts +39 -0
  140. package/src/ui-inputs.test.ts +26 -0
  141. package/src/ui-selection.test.ts +13 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * createTree - the HEADLESS core behind <SvTree>, in the same spirit as
3
+ * `createSvGrid` for the grid: a runes-based state machine (expand/collapse,
4
+ * single-select highlight, cascading tri-state checkboxes, full keyboard) with
5
+ * **prop-getters** you spread onto YOUR OWN markup. The visible tree is
6
+ * flattened into `rows` so roving-focus keyboard nav stays a flat index walk.
7
+ *
8
+ * ```svelte
9
+ * <script lang="ts">
10
+ * import { createTree } from '@svgrid/grid'
11
+ * let selected = $state<string | null>(null)
12
+ * const tree = createTree({ nodes: () => nodes, selected: () => selected, onSelect: (id) => (selected = id) })
13
+ * </script>
14
+ * <div {...tree.treeProps()}>
15
+ * {#each tree.rows as row (row.node.id)}
16
+ * <div {...tree.itemProps(row)}>{row.node.label}</div>
17
+ * {/each}
18
+ * </div>
19
+ * ```
20
+ *
21
+ * The styled <SvTree> is just one renderer over this core. The cascade math
22
+ * (`treeDescendantIds`, `treeCheckState`) is exported as pure functions.
23
+ */
24
+ export type TreeNode = {
25
+ id: string
26
+ label: string
27
+ children?: TreeNode[]
28
+ disabled?: boolean
29
+ }
30
+
31
+ export type CheckState = 'checked' | 'indeterminate' | 'unchecked'
32
+
33
+ /** A single visible (flattened) tree row. */
34
+ export type TreeRow = {
35
+ node: TreeNode
36
+ depth: number
37
+ hasChildren: boolean
38
+ open: boolean
39
+ parentId: string | null
40
+ index: number
41
+ }
42
+
43
+ /** All descendant ids of `node` (pure, depth-first). */
44
+ export function treeDescendantIds(node: TreeNode): string[] {
45
+ const ids: string[] = []
46
+ const walk = (n: TreeNode) => { for (const c of n.children ?? []) { ids.push(c.id); walk(c) } }
47
+ walk(node)
48
+ return ids
49
+ }
50
+
51
+ /** Tri-state check status of `node` given the set of checked ids (pure). */
52
+ export function treeCheckState(node: TreeNode, checked: ReadonlySet<string>): CheckState {
53
+ if (!node.children?.length) return checked.has(node.id) ? 'checked' : 'unchecked'
54
+ const desc = treeDescendantIds(node)
55
+ const on = desc.filter((id) => checked.has(id)).length
56
+ if (on === 0 && !checked.has(node.id)) return 'unchecked'
57
+ if (on === desc.length) return 'checked'
58
+ return 'indeterminate'
59
+ }
60
+
61
+ function findNode(list: ReadonlyArray<TreeNode>, id: string): TreeNode | null {
62
+ for (const n of list) {
63
+ if (n.id === id) return n
64
+ if (n.children) { const f = findNode(n.children, id); if (f) return f }
65
+ }
66
+ return null
67
+ }
68
+
69
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
70
+ export type TreeConfig = {
71
+ nodes: () => ReadonlyArray<TreeNode>
72
+ selected?: () => string | null
73
+ onSelect?: (id: string) => void
74
+ /** Seed the internally-managed expanded set (read once). */
75
+ expandedIds?: () => string[] | undefined
76
+ onToggle?: (id: string, expanded: boolean) => void
77
+ checkable?: () => boolean
78
+ checked?: () => string[]
79
+ onCheck?: (ids: string[]) => void
80
+ ariaLabel?: () => string | undefined
81
+ }
82
+
83
+ export function createTree(config: TreeConfig) {
84
+ const nodes = () => config.nodes()
85
+ const checkable = () => config.checkable?.() ?? false
86
+ const selected = () => config.selected?.() ?? null
87
+
88
+ // Internal expanded set, seeded from the prop once (then self-managed).
89
+ let expanded = $state<Set<string>>(new Set())
90
+ let seeded = false
91
+ $effect(() => {
92
+ if (!seeded) { seeded = true; const ids = config.expandedIds?.(); if (ids) expanded = new Set(ids) }
93
+ })
94
+
95
+ const rows = $derived.by<TreeRow[]>(() => {
96
+ const out: TreeRow[] = []
97
+ const walk = (list: ReadonlyArray<TreeNode>, depth: number, parentId: string | null) => {
98
+ for (const node of list) {
99
+ const hasChildren = !!node.children?.length
100
+ const open = expanded.has(node.id)
101
+ out.push({ node, depth, hasChildren, open, parentId, index: out.length })
102
+ if (hasChildren && open) walk(node.children!, depth + 1, node.id)
103
+ }
104
+ }
105
+ walk(nodes(), 0, null)
106
+ return out
107
+ })
108
+
109
+ let active = $state(0)
110
+ $effect(() => { if (active >= rows.length) active = Math.max(0, rows.length - 1) })
111
+
112
+ const checkedSet = $derived(new Set(config.checked?.() ?? []))
113
+
114
+ // Roving focus target; `focusTick` bumps only on keyboard moves.
115
+ let focusTick = $state(0)
116
+ function setActive(i: number, focus = false) { active = i; if (focus) focusTick++ }
117
+
118
+ const checkStateOf = (node: TreeNode): CheckState => treeCheckState(node, checkedSet)
119
+ const isExpanded = (id: string) => expanded.has(id)
120
+ const isSelected = (id: string) => id === selected()
121
+
122
+ function toggleExpand(node: TreeNode) {
123
+ if (!node.children?.length) return
124
+ const next = new Set(expanded)
125
+ const willOpen = !next.has(node.id)
126
+ willOpen ? next.add(node.id) : next.delete(node.id)
127
+ expanded = next
128
+ config.onToggle?.(node.id, willOpen)
129
+ }
130
+ const toggle = (id: string) => { const n = findNode(nodes(), id); if (n) toggleExpand(n) }
131
+
132
+ function select(node: TreeNode) {
133
+ if (node.disabled) return
134
+ config.onSelect?.(node.id)
135
+ }
136
+ const selectId = (id: string) => { const n = findNode(nodes(), id); if (n) select(n) }
137
+
138
+ function toggleCheck(node: TreeNode) {
139
+ if (node.disabled) return
140
+ const next = new Set(checkedSet)
141
+ const ids = [node.id, ...treeDescendantIds(node)]
142
+ const currentlyOn = checkStateOf(node) === 'checked'
143
+ for (const id of ids) currentlyOn ? next.delete(id) : next.add(id)
144
+ config.onCheck?.([...next])
145
+ }
146
+
147
+ function onKeydown(e: KeyboardEvent) {
148
+ const item = rows[active]
149
+ if (!item) return
150
+ switch (e.key) {
151
+ case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, rows.length - 1), true); break
152
+ case 'ArrowUp': e.preventDefault(); setActive(Math.max(active - 1, 0), true); break
153
+ case 'ArrowRight':
154
+ e.preventDefault()
155
+ if (item.hasChildren && !item.open) toggleExpand(item.node)
156
+ else if (item.hasChildren && item.open) setActive(Math.min(active + 1, rows.length - 1), true)
157
+ break
158
+ case 'ArrowLeft':
159
+ e.preventDefault()
160
+ if (item.hasChildren && item.open) toggleExpand(item.node)
161
+ else if (item.parentId) { const pi = rows.findIndex((r) => r.node.id === item.parentId); if (pi >= 0) setActive(pi, true) }
162
+ break
163
+ case 'Enter': e.preventDefault(); select(item.node); break
164
+ case ' ': e.preventDefault(); checkable() ? toggleCheck(item.node) : select(item.node); break
165
+ case 'Home': e.preventDefault(); setActive(0, true); break
166
+ case 'End': e.preventDefault(); setActive(rows.length - 1, true); break
167
+ }
168
+ }
169
+
170
+ return {
171
+ /** Internally-managed set of expanded node ids. */
172
+ get expanded() { return expanded },
173
+ /** Selected node id (single-select highlight). */
174
+ get selected() { return selected() },
175
+ /** Flattened list of visible rows. */
176
+ get rows() { return rows },
177
+ /** Roving-focus row index. */
178
+ get activeIndex() { return active },
179
+ /** Monotonic counter; changes only on keyboard navigation. */
180
+ get focusTick() { return focusTick },
181
+ isExpanded,
182
+ isSelected,
183
+ checkStateOf,
184
+ toggle,
185
+ toggleExpand,
186
+ select: selectId,
187
+ toggleCheck,
188
+ setActive,
189
+ onKeydown,
190
+ /** Spread onto the tree container element. */
191
+ treeProps: () => ({
192
+ role: 'tree' as const,
193
+ 'aria-label': config.ariaLabel?.(),
194
+ 'aria-multiselectable': checkable(),
195
+ }),
196
+ /** Spread onto the treeitem element for a flattened `row`. */
197
+ itemProps: (row: TreeRow) => {
198
+ const cs = checkable() ? checkStateOf(row.node) : 'unchecked'
199
+ const ariaChecked: 'true' | 'mixed' | 'false' | undefined =
200
+ checkable() ? (cs === 'checked' ? 'true' : cs === 'indeterminate' ? 'mixed' : 'false') : undefined
201
+ return {
202
+ role: 'treeitem' as const,
203
+ 'aria-level': row.depth + 1,
204
+ 'aria-selected': row.node.id === selected(),
205
+ 'aria-expanded': row.hasChildren ? row.open : undefined,
206
+ 'aria-checked': ariaChecked,
207
+ 'data-row': row.index,
208
+ tabindex: row.index === active ? 0 : -1,
209
+ onclick: () => { setActive(row.index); select(row.node) },
210
+ onkeydown: onKeydown,
211
+ }
212
+ },
213
+ }
214
+ }
215
+
216
+ export type Tree = ReturnType<typeof createTree>
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { editorAria, editorErrorId } from './editor-contract'
3
+
4
+ describe('editor-contract', () => {
5
+ it('emits ARIA only for the flags that are set (undefined => attribute omitted)', () => {
6
+ expect(editorAria({})).toEqual({
7
+ 'aria-invalid': undefined,
8
+ 'aria-required': undefined,
9
+ 'aria-describedby': undefined,
10
+ 'aria-label': undefined,
11
+ })
12
+ })
13
+
14
+ it('wires invalid + required + describedby + label', () => {
15
+ const a = editorAria({ id: 'qty', invalid: true, required: true, error: 'Too big', ariaLabel: 'Quantity' })
16
+ expect(a['aria-invalid']).toBe('true')
17
+ expect(a['aria-required']).toBe('true')
18
+ expect(a['aria-describedby']).toBe('qty__error')
19
+ expect(a['aria-label']).toBe('Quantity')
20
+ })
21
+
22
+ it('describedby is only set when there is an error AND an id', () => {
23
+ expect(editorAria({ id: 'qty', error: '' })['aria-describedby']).toBeUndefined()
24
+ expect(editorAria({ error: 'x' })['aria-describedby']).toBeUndefined() // no id
25
+ expect(editorAria({ id: 'qty', error: 'x' })['aria-describedby']).toBe('qty__error')
26
+ })
27
+
28
+ it('editorErrorId', () => {
29
+ expect(editorErrorId('a')).toBe('a__error')
30
+ expect(editorErrorId(undefined)).toBeUndefined()
31
+ })
32
+ })
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The shared "editor contract" for the SvGrid UI kit.
3
+ *
4
+ * Every value-bearing editor (inputs, selects, pickers) accepts this common set
5
+ * of props so forms, validation and accessibility behave IDENTICALLY across the
6
+ * kit - the same spirit as the grid's headless core, applied to the editors.
7
+ * Components spread their own `value` / options / behavior on top of this base.
8
+ *
9
+ * Framework-free (no Svelte, no DOM) so it can be unit-tested and reused.
10
+ */
11
+
12
+ export type EditorSize = 'sm' | 'md' | 'lg'
13
+
14
+ /** Props shared by every value-bearing editor in the kit. */
15
+ export type SvEditorProps = {
16
+ /** Disable interaction + form submission. */
17
+ disabled?: boolean
18
+ /** Read-only: the value is shown but not editable. */
19
+ readonly?: boolean
20
+ /** Required for validation (adds `aria-required` + participates in validity). */
21
+ required?: boolean
22
+ /** Marks the control invalid (`aria-invalid` + error styling). */
23
+ invalid?: boolean
24
+ /** Error message; when set it is announced via `aria-describedby` and shown. */
25
+ error?: string
26
+ /** Control size / density. */
27
+ size?: EditorSize
28
+ /** Form field name; the editor emits a hidden input carrying its value. */
29
+ name?: string
30
+ /** Root/control element id (the error-text id derives from it). */
31
+ id?: string
32
+ /** Accessible name when there is no visible `<label>`. */
33
+ ariaLabel?: string
34
+ }
35
+
36
+ /** Stable DOM id for an editor's error text, for `aria-describedby` wiring. */
37
+ export const editorErrorId = (id?: string): string | undefined => (id ? `${id}__error` : undefined)
38
+
39
+ /** A subset of {@link SvEditorProps} that drives ARIA on the focusable control. */
40
+ export type EditorAriaState = Pick<SvEditorProps, 'id' | 'invalid' | 'required' | 'error' | 'ariaLabel'>
41
+
42
+ /**
43
+ * ARIA attributes for the focusable control of an editor, derived from its state.
44
+ * Spread onto the input/button/combobox:
45
+ * `<input {...editorAria({ id, invalid, required, error, ariaLabel })} />`
46
+ * Undefined values are omitted by Svelte, so unset props add no attributes.
47
+ */
48
+ export function editorAria(state: EditorAriaState): {
49
+ 'aria-invalid': 'true' | undefined
50
+ 'aria-required': 'true' | undefined
51
+ 'aria-describedby': string | undefined
52
+ 'aria-label': string | undefined
53
+ } {
54
+ return {
55
+ 'aria-invalid': state.invalid ? 'true' : undefined,
56
+ 'aria-required': state.required ? 'true' : undefined,
57
+ 'aria-describedby': state.error ? editorErrorId(state.id) : undefined,
58
+ 'aria-label': state.ariaLabel,
59
+ }
60
+ }
package/src/index.ts CHANGED
@@ -73,6 +73,45 @@ export { default as SvGridDropdown } from './SvGridDropdown.svelte'
73
73
  export { default as SvCalendar, type CalendarValue, type CalendarPreset, type CalendarAnimation } from './SvCalendar.svelte'
74
74
  export { default as SvTimePicker, type TimeValue } from './SvTimePicker.svelte'
75
75
  export { default as SvDateTimePicker, type DateTimeValue } from './SvDateTimePicker.svelte'
76
+ // UI kit - shared editor contract (common props + a11y wiring for every editor)
77
+ export { editorAria, editorErrorId, type SvEditorProps, type EditorSize, type EditorAriaState } from './editor-contract'
78
+ // UI kit - headless cores (state + prop-getters you render yourself, like createSvGrid)
79
+ export {
80
+ createListbox,
81
+ toSelectedArray,
82
+ type Listbox,
83
+ type ListboxConfig,
84
+ type ListboxValue,
85
+ type ListboxRootProps,
86
+ type OptionProps,
87
+ } from './createListbox.svelte'
88
+ // Selection family cores
89
+ export { createCombobox, type Combobox, type ComboboxConfig, type ComboboxValue } from './createCombobox.svelte'
90
+ export { createDropdownList, type DropdownList, type DropdownListConfig, type DropdownValue } from './createDropdownList.svelte'
91
+ export { createAutocomplete, type Autocomplete, type AutocompleteConfig } from './createAutocomplete.svelte'
92
+ export { createTagsInput, type TagsInput, type TagsInputConfig } from './createTagsInput.svelte'
93
+ export { createCountryInput, type CountryInput, type CountryInputConfig } from './createCountryInput.svelte'
94
+ // Text-input family cores
95
+ export { createNumberInput, type NumberInputConfig, type NumberInputCore } from './createNumberInput.svelte'
96
+ export { createMaskedInput, type MaskedInputConfig, type MaskedInputCore } from './createMaskedInput.svelte'
97
+ export { createPhoneInput, type PhoneInputConfig, type PhoneInputCore } from './createPhoneInput.svelte'
98
+ export { createColorInput, normalizeHex, type ColorInputConfig, type ColorInputCore } from './createColorInput.svelte'
99
+ export { createPasswordInput, passwordStrength, STRENGTH_LABELS, type PasswordInputConfig, type PasswordInputCore } from './createPasswordInput.svelte'
100
+ // Buttons & toggles family cores
101
+ export { createToggle, type Toggle, type ToggleConfig, type ToggleButtonProps } from './createToggle.svelte'
102
+ export { createSwitch, type Switch, type SwitchConfig, type SwitchProps } from './createSwitch.svelte'
103
+ export { createCheckbox, type Checkbox, type CheckboxConfig, type CheckboxBoxProps } from './createCheckbox.svelte'
104
+ export { createRadioGroup, type RadioGroup, type RadioGroupConfig, type RadioProps, type RadioGroupProps } from './createRadioGroup.svelte'
105
+ export { createRating, type Rating, type RatingConfig, type RatingFill, type RatingRootProps, type RatingStarProps } from './createRating.svelte'
106
+ // Date & time family cores
107
+ export { createCalendar, normalizeCalendarValue, type Calendar, type CalendarConfig, type CalendarDayState, type CalendarNavDir, type CalendarNameFormat, type DisplayMode } from './createCalendar.svelte'
108
+ export { createTimePicker, parseTimeValue, type TimePicker, type TimePickerConfig, type TimeSelection, type TimeFormat, type DialTick } from './createTimePicker.svelte'
109
+ export { createDateTimePicker, type DateTimePicker, type DateTimePickerConfig, type DropDownDisplayMode, type DateTimeTab } from './createDateTimePicker.svelte'
110
+ // Layout & range family cores
111
+ export { createTabs, type Tabs, type TabsConfig, type TabsOrientation, type TabsActivation } from './createTabs.svelte'
112
+ export { createTree, treeDescendantIds, treeCheckState, type Tree, type TreeConfig, type TreeRow, type CheckState } from './createTree.svelte'
113
+ export { createSlider, type Slider, type SliderConfig, type SliderValue, type SliderOrientation, type SliderThumb, type SliderPoint } from './createSlider.svelte'
114
+ export { createGauge, type Gauge, type GaugeConfig, type GaugePoint } from './createGauge.svelte'
76
115
  // UI kit - Group B (buttons & toggles)
77
116
  export { default as SvButton } from './SvButton.svelte'
78
117
  export { default as SvRepeatButton } from './SvRepeatButton.svelte'
@@ -14,6 +14,32 @@ function mnt(Comp: any, props: Record<string, unknown>) {
14
14
  return { target, destroy: () => { unmount(app); target.remove() } }
15
15
  }
16
16
 
17
+ describe('SvNumberInput validation a11y (editor contract)', () => {
18
+ it('wires aria-invalid + aria-required + aria-describedby -> error text', () => {
19
+ const { target, destroy } = mnt(SvNumberInput, { value: 5, id: 'qty', invalid: true, required: true, error: 'Too big' })
20
+ try {
21
+ const input = target.querySelector<HTMLInputElement>('.sv-num__input')!
22
+ expect(input.getAttribute('aria-invalid')).toBe('true')
23
+ expect(input.getAttribute('aria-required')).toBe('true')
24
+ expect(input.getAttribute('aria-describedby')).toBe('qty__error')
25
+ const err = target.querySelector('#qty__error')!
26
+ expect(err.getAttribute('role')).toBe('alert')
27
+ expect(err.textContent).toBe('Too big')
28
+ expect(target.querySelector('.sv-num.is-invalid')).toBeTruthy()
29
+ } finally { destroy() }
30
+ })
31
+
32
+ it('adds no validation attributes when the props are unset', () => {
33
+ const { target, destroy } = mnt(SvNumberInput, { value: 1 })
34
+ try {
35
+ const input = target.querySelector<HTMLInputElement>('.sv-num__input')!
36
+ expect(input.getAttribute('aria-invalid')).toBeNull()
37
+ expect(input.getAttribute('aria-describedby')).toBeNull()
38
+ expect(target.querySelector('.sv-num.is-invalid')).toBeNull()
39
+ } finally { destroy() }
40
+ })
41
+ })
42
+
17
43
  describe('SvNumberInput', () => {
18
44
  it('clamps to max on commit', () => {
19
45
  let got: number | null | undefined
@@ -39,6 +39,19 @@ describe('SvListBox', () => {
39
39
  expect(got).toEqual(['a', 'b'])
40
40
  } finally { b.destroy() }
41
41
  })
42
+
43
+ it('posts its value(s) via hidden inputs when `name` is set (multi -> repeated name)', () => {
44
+ const m = mnt(SvListBox, { options: OPTS, value: ['a', 'c'], multiple: true, name: 'tags' })
45
+ try {
46
+ const hidden = Array.from(m.target.querySelectorAll<HTMLInputElement>('input[type="hidden"][name="tags"]'))
47
+ expect(hidden.map((h) => h.value)).toEqual(['a', 'c'])
48
+ } finally { m.destroy() }
49
+ const s = mnt(SvListBox, { options: OPTS, value: 'b', name: 'choice' })
50
+ try {
51
+ const hidden = Array.from(s.target.querySelectorAll<HTMLInputElement>('input[type="hidden"][name="choice"]'))
52
+ expect(hidden.map((h) => h.value)).toEqual(['b'])
53
+ } finally { s.destroy() }
54
+ })
42
55
  })
43
56
 
44
57
  describe('SvDropDownList', () => {