@svgrid/grid 1.2.22 → 1.2.24

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 (263) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +89 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +409 -0
  8. package/dist/SvCalendar.svelte.d.ts +52 -0
  9. package/dist/SvCheckBox.svelte +82 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +160 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +108 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +125 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +265 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +38 -0
  19. package/dist/SvDropDownList.svelte +120 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +97 -0
  24. package/dist/SvGauge.svelte.d.ts +26 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +89 -0
  35. package/dist/SvListBox.svelte.d.ts +22 -0
  36. package/dist/SvMaskedInput.svelte +66 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +116 -0
  39. package/dist/SvNumberInput.svelte.d.ts +18 -0
  40. package/dist/SvPasswordInput.svelte +100 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +18 -0
  42. package/dist/SvPhoneInput.svelte +87 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +85 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +20 -0
  46. package/dist/SvRating.svelte +91 -0
  47. package/dist/SvRating.svelte.d.ts +18 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +155 -0
  51. package/dist/SvSlider.svelte.d.ts +20 -0
  52. package/dist/SvSwitchButton.svelte +87 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +14 -0
  54. package/dist/SvTabs.svelte +109 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +70 -0
  57. package/dist/SvTagsInput.svelte.d.ts +14 -0
  58. package/dist/SvTimePicker.svelte +201 -0
  59. package/dist/SvTimePicker.svelte.d.ts +30 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +131 -0
  63. package/dist/SvTree.svelte.d.ts +27 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +11674 -5907
  66. package/dist/cdn/svgrid.svelte-external.js +10436 -4671
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/createAutocomplete.svelte.d.ts +77 -0
  76. package/dist/createAutocomplete.svelte.js +100 -0
  77. package/dist/createCalendar.svelte.d.ts +192 -0
  78. package/dist/createCalendar.svelte.js +402 -0
  79. package/dist/createCheckbox.svelte.d.ts +45 -0
  80. package/dist/createCheckbox.svelte.js +26 -0
  81. package/dist/createColorInput.svelte.d.ts +67 -0
  82. package/dist/createColorInput.svelte.js +100 -0
  83. package/dist/createCombobox.svelte.d.ts +102 -0
  84. package/dist/createCombobox.svelte.js +171 -0
  85. package/dist/createCountryInput.svelte.d.ts +86 -0
  86. package/dist/createCountryInput.svelte.js +126 -0
  87. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  88. package/dist/createDateTimePicker.svelte.js +202 -0
  89. package/dist/createDropdownList.svelte.d.ts +78 -0
  90. package/dist/createDropdownList.svelte.js +117 -0
  91. package/dist/createGauge.svelte.d.ts +56 -0
  92. package/dist/createGauge.svelte.js +49 -0
  93. package/dist/createListbox.svelte.d.ts +79 -0
  94. package/dist/createListbox.svelte.js +117 -0
  95. package/dist/createMaskedInput.svelte.d.ts +30 -0
  96. package/dist/createMaskedInput.svelte.js +62 -0
  97. package/dist/createNumberInput.svelte.d.ts +72 -0
  98. package/dist/createNumberInput.svelte.js +167 -0
  99. package/dist/createPasswordInput.svelte.d.ts +60 -0
  100. package/dist/createPasswordInput.svelte.js +72 -0
  101. package/dist/createPhoneInput.svelte.d.ts +46 -0
  102. package/dist/createPhoneInput.svelte.js +105 -0
  103. package/dist/createRadioGroup.svelte.d.ts +64 -0
  104. package/dist/createRadioGroup.svelte.js +73 -0
  105. package/dist/createRating.svelte.d.ts +70 -0
  106. package/dist/createRating.svelte.js +85 -0
  107. package/dist/createSlider.svelte.d.ts +71 -0
  108. package/dist/createSlider.svelte.js +123 -0
  109. package/dist/createSwitch.svelte.d.ts +43 -0
  110. package/dist/createSwitch.svelte.js +41 -0
  111. package/dist/createTabs.svelte.d.ts +78 -0
  112. package/dist/createTabs.svelte.js +92 -0
  113. package/dist/createTagsInput.svelte.d.ts +65 -0
  114. package/dist/createTagsInput.svelte.js +93 -0
  115. package/dist/createTimePicker.svelte.d.ts +106 -0
  116. package/dist/createTimePicker.svelte.js +248 -0
  117. package/dist/createToggle.svelte.d.ts +43 -0
  118. package/dist/createToggle.svelte.js +39 -0
  119. package/dist/createTree.svelte.d.ts +96 -0
  120. package/dist/createTree.svelte.js +194 -0
  121. package/dist/datetime/date-core.d.ts +88 -0
  122. package/dist/datetime/date-core.js +165 -0
  123. package/dist/datetime/date-format.d.ts +39 -0
  124. package/dist/datetime/date-format.js +371 -0
  125. package/dist/datetime/date-restrict.d.ts +23 -0
  126. package/dist/datetime/date-restrict.js +50 -0
  127. package/dist/datetime/date-selection.d.ts +25 -0
  128. package/dist/datetime/date-selection.js +94 -0
  129. package/dist/datetime/mask.d.ts +21 -0
  130. package/dist/datetime/mask.js +90 -0
  131. package/dist/editing.d.ts +1 -1
  132. package/dist/editing.js +7 -0
  133. package/dist/editor-contract.d.ts +48 -0
  134. package/dist/editor-contract.js +26 -0
  135. package/dist/editors/cell-editors.d.ts +2 -2
  136. package/dist/editors/cell-editors.js +3 -1
  137. package/dist/export-format.d.ts +178 -0
  138. package/dist/export-format.js +419 -0
  139. package/dist/hyperformula-adapter.d.ts +4 -0
  140. package/dist/hyperformula-adapter.js +15 -6
  141. package/dist/index.d.ts +60 -0
  142. package/dist/index.js +72 -0
  143. package/dist/list-option.d.ts +10 -0
  144. package/dist/list-option.js +7 -0
  145. package/dist/menus.js +18 -2
  146. package/dist/popover.d.ts +48 -0
  147. package/dist/popover.js +65 -0
  148. package/dist/row-resize.d.ts +4 -4
  149. package/dist/row-resize.js +8 -5
  150. package/dist/selection.d.ts +6 -0
  151. package/dist/selection.js +31 -5
  152. package/dist/server-data-source.d.ts +43 -0
  153. package/dist/server-data-source.js +100 -0
  154. package/dist/sparkline.js +8 -4
  155. package/dist/svgrid-wrapper.types.d.ts +26 -1
  156. package/package.json +6 -1
  157. package/src/GridFooter.svelte +34 -20
  158. package/src/SvAutoComplete.svelte +89 -0
  159. package/src/SvButton.svelte +97 -0
  160. package/src/SvCalendar.svelte +409 -0
  161. package/src/SvCalendar.test.ts +211 -0
  162. package/src/SvCheckBox.svelte +82 -0
  163. package/src/SvColorInput.svelte +160 -0
  164. package/src/SvComboBox.svelte +108 -0
  165. package/src/SvCountryInput.svelte +125 -0
  166. package/src/SvDateTimePicker.svelte +265 -0
  167. package/src/SvDateTimePicker.test.ts +136 -0
  168. package/src/SvDropDownList.svelte +120 -0
  169. package/src/SvForm.svelte +147 -0
  170. package/src/SvGauge.svelte +97 -0
  171. package/src/SvGrid.controller.svelte.ts +74 -8
  172. package/src/SvGrid.css +84 -31
  173. package/src/SvGrid.helpers.ts +41 -12
  174. package/src/SvGrid.svelte +84 -5
  175. package/src/SvGrid.types.ts +33 -0
  176. package/src/SvGridDropdown.svelte +16 -30
  177. package/src/SvListBox.svelte +89 -0
  178. package/src/SvMaskedInput.svelte +66 -0
  179. package/src/SvNumberInput.svelte +116 -0
  180. package/src/SvPasswordInput.svelte +100 -0
  181. package/src/SvPhoneInput.svelte +87 -0
  182. package/src/SvRadioGroup.svelte +85 -0
  183. package/src/SvRating.svelte +91 -0
  184. package/src/SvRepeatButton.svelte +89 -0
  185. package/src/SvSlider.svelte +155 -0
  186. package/src/SvSwitchButton.svelte +87 -0
  187. package/src/SvTabs.svelte +109 -0
  188. package/src/SvTagsInput.svelte +70 -0
  189. package/src/SvTimePicker.svelte +201 -0
  190. package/src/SvTimePicker.test.ts +98 -0
  191. package/src/SvToggleButton.svelte +57 -0
  192. package/src/SvTree.svelte +131 -0
  193. package/src/build-api.ts +115 -0
  194. package/src/cell-render.test.ts +38 -0
  195. package/src/cell-render.ts +43 -0
  196. package/src/clipboard.test.ts +18 -0
  197. package/src/clipboard.ts +24 -1
  198. package/src/collaboration.test.ts +30 -0
  199. package/src/collaboration.ts +6 -0
  200. package/src/core.ts +27 -3
  201. package/src/countries.ts +71 -0
  202. package/src/createAutocomplete.svelte.ts +104 -0
  203. package/src/createCalendar.svelte.ts +488 -0
  204. package/src/createCheckbox.svelte.ts +74 -0
  205. package/src/createColorInput.svelte.ts +115 -0
  206. package/src/createCombobox.svelte.ts +158 -0
  207. package/src/createCountryInput.svelte.ts +126 -0
  208. package/src/createDateTimePicker.svelte.ts +216 -0
  209. package/src/createDropdownList.svelte.ts +125 -0
  210. package/src/createGauge.svelte.ts +86 -0
  211. package/src/createListbox.svelte.ts +186 -0
  212. package/src/createMaskedInput.svelte.ts +81 -0
  213. package/src/createNumberInput.svelte.ts +193 -0
  214. package/src/createPasswordInput.svelte.ts +87 -0
  215. package/src/createPhoneInput.svelte.ts +119 -0
  216. package/src/createRadioGroup.svelte.ts +132 -0
  217. package/src/createRating.svelte.ts +145 -0
  218. package/src/createSlider.svelte.ts +160 -0
  219. package/src/createSwitch.svelte.ts +78 -0
  220. package/src/createTabs.svelte.ts +129 -0
  221. package/src/createTagsInput.svelte.ts +98 -0
  222. package/src/createTimePicker.svelte.ts +289 -0
  223. package/src/createToggle.svelte.ts +69 -0
  224. package/src/createTree.svelte.ts +216 -0
  225. package/src/datetime/date-core.test.ts +217 -0
  226. package/src/datetime/date-core.ts +204 -0
  227. package/src/datetime/date-format.test.ts +121 -0
  228. package/src/datetime/date-format.ts +317 -0
  229. package/src/datetime/date-restrict.ts +60 -0
  230. package/src/datetime/date-selection.test.ts +129 -0
  231. package/src/datetime/date-selection.ts +137 -0
  232. package/src/datetime/mask.test.ts +36 -0
  233. package/src/datetime/mask.ts +84 -0
  234. package/src/editing.test.ts +22 -0
  235. package/src/editing.ts +7 -1
  236. package/src/editor-contract.test.ts +32 -0
  237. package/src/editor-contract.ts +60 -0
  238. package/src/editors/cell-editors.ts +7 -1
  239. package/src/export-data-api.test.ts +126 -0
  240. package/src/export-format.test.ts +107 -0
  241. package/src/export-format.ts +598 -0
  242. package/src/hyperformula-adapter.test.ts +35 -1
  243. package/src/hyperformula-adapter.ts +18 -6
  244. package/src/index.ts +110 -0
  245. package/src/list-option.ts +15 -0
  246. package/src/menus.ts +21 -2
  247. package/src/popover.ts +79 -0
  248. package/src/row-resize.test.ts +25 -0
  249. package/src/row-resize.ts +8 -5
  250. package/src/selection.test.ts +36 -4
  251. package/src/selection.ts +30 -9
  252. package/src/server-data-source.test.ts +201 -0
  253. package/src/server-data-source.ts +148 -0
  254. package/src/sparkline.test.ts +9 -0
  255. package/src/sparkline.ts +9 -4
  256. package/src/svgrid-wrapper.types.ts +28 -1
  257. package/src/svgrid.behavior.test.ts +27 -0
  258. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  259. package/src/ui-buttons.test.ts +105 -0
  260. package/src/ui-composite.test.ts +89 -0
  261. package/src/ui-inputs.test.ts +118 -0
  262. package/src/ui-range.test.ts +61 -0
  263. package/src/ui-selection.test.ts +119 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * createSwitch - the HEADLESS core behind <SvSwitchButton>: a controlled on/off
3
+ * switch (ARIA `switch`) with full keyboard (Space/Enter toggle, ArrowRight on,
4
+ * ArrowLeft off), exposed as **prop-getters** you spread onto YOUR OWN markup.
5
+ *
6
+ * ```svelte
7
+ * <script lang="ts">
8
+ * import { createSwitch } from '@svgrid/grid'
9
+ * let on = $state(false)
10
+ * const sw = createSwitch({ checked: () => on, onChange: (v) => (on = v) })
11
+ * </script>
12
+ * <button {...sw.switchProps()}><span class="thumb"></span></button>
13
+ * ```
14
+ *
15
+ * The styled <SvSwitchButton> is just one renderer over this core.
16
+ */
17
+ export type SwitchConfig = {
18
+ checked: () => boolean
19
+ onChange?: (checked: boolean) => void
20
+ disabled?: () => boolean
21
+ ariaLabel?: () => string | undefined
22
+ }
23
+
24
+ export type SwitchProps = {
25
+ type: 'button'
26
+ role: 'switch'
27
+ 'aria-checked': boolean
28
+ 'aria-label': string | undefined
29
+ disabled: boolean
30
+ 'data-checked': '' | undefined
31
+ onclick: () => void
32
+ onkeydown: (e: KeyboardEvent) => void
33
+ }
34
+
35
+ export type Switch = {
36
+ /** Current on/off state. */
37
+ readonly checked: boolean
38
+ readonly disabled: boolean
39
+ /** Flip on/off (no-op when disabled). */
40
+ toggle: () => void
41
+ onKeydown: (e: KeyboardEvent) => void
42
+ /** Spread onto the switch element. */
43
+ switchProps: () => SwitchProps
44
+ }
45
+
46
+ export function createSwitch(config: SwitchConfig): Switch {
47
+ const checked = () => config.checked()
48
+ const disabled = () => config.disabled?.() ?? false
49
+
50
+ function toggle() {
51
+ if (disabled()) return
52
+ config.onChange?.(!checked())
53
+ }
54
+
55
+ function onKeydown(e: KeyboardEvent) {
56
+ if (disabled()) return
57
+ if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggle() }
58
+ else if (e.key === 'ArrowRight') { if (!checked()) config.onChange?.(true) }
59
+ else if (e.key === 'ArrowLeft') { if (checked()) config.onChange?.(false) }
60
+ }
61
+
62
+ return {
63
+ get checked() { return checked() },
64
+ get disabled() { return disabled() },
65
+ toggle,
66
+ onKeydown,
67
+ switchProps: () => ({
68
+ type: 'button',
69
+ role: 'switch',
70
+ 'aria-checked': checked(),
71
+ 'aria-label': config.ariaLabel?.(),
72
+ disabled: disabled(),
73
+ 'data-checked': checked() ? '' : undefined,
74
+ onclick: toggle,
75
+ onkeydown: onKeydown,
76
+ }),
77
+ }
78
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * createTabs - the HEADLESS core behind <SvTabs>, in the same spirit as
3
+ * `createSvGrid` for the grid: a runes-based state machine (roving arrow-key
4
+ * focus, automatic/manual activation, full WAI-ARIA wiring) exposed as
5
+ * **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM
6
+ * assumptions - render it however you like.
7
+ *
8
+ * ```svelte
9
+ * <script lang="ts">
10
+ * import { createTabs } from '@svgrid/grid'
11
+ * let value = $state('a')
12
+ * const t = createTabs({ tabs: () => tabs, value: () => value, onChange: (id) => (value = id) })
13
+ * </script>
14
+ * <div {...t.tablistProps()}>
15
+ * {#each tabs as tab}
16
+ * <button {...t.tabProps(tab.id)}>{tab.label}</button>
17
+ * {/each}
18
+ * </div>
19
+ * <div {...t.panelProps(t.activeId)}>...</div>
20
+ * ```
21
+ *
22
+ * The styled <SvTabs> is just one renderer over this core. DOM focus movement is
23
+ * a render concern, so the component watches `focusTick`/`focusId` and calls
24
+ * `.focus()` itself; the core only decides *which* tab should take focus.
25
+ */
26
+ export type TabItem = { id: string; label: string; disabled?: boolean }
27
+
28
+ export type TabsOrientation = 'horizontal' | 'vertical'
29
+ export type TabsActivation = 'automatic' | 'manual'
30
+
31
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
32
+ export type TabsConfig = {
33
+ tabs: () => ReadonlyArray<TabItem>
34
+ value: () => string | undefined
35
+ onChange?: (id: string) => void
36
+ orientation?: () => TabsOrientation
37
+ activation?: () => TabsActivation
38
+ ariaLabel?: () => string | undefined
39
+ }
40
+
41
+ let uid = 0
42
+
43
+ export function createTabs(config: TabsConfig) {
44
+ const id = `sv-tabs-${uid++}`
45
+ const tabs = () => config.tabs()
46
+ const orientation = () => config.orientation?.() ?? 'horizontal'
47
+ const activation = () => config.activation?.() ?? 'automatic'
48
+
49
+ const enabled = $derived(tabs().filter((t) => !t.disabled))
50
+ const active = $derived(config.value() ?? tabs().find((t) => !t.disabled)?.id ?? tabs()[0]?.id ?? '')
51
+
52
+ // Roving focus target. `focusTick` bumps on every keyboard move so the styled
53
+ // renderer can move real DOM focus without stealing it on mount.
54
+ let focusId = $state('')
55
+ let focusTick = $state(0)
56
+ const requestFocus = (tid: string) => { focusId = tid; focusTick++ }
57
+
58
+ const tabId = (tid: string) => `${id}-tab-${tid}`
59
+ const panelId = (tid: string) => `${id}-panel-${tid}`
60
+
61
+ function select(tid: string) {
62
+ const t = tabs().find((x) => x.id === tid)
63
+ if (!t || t.disabled) return
64
+ config.onChange?.(tid)
65
+ }
66
+
67
+ function onKeydown(e: KeyboardEvent) {
68
+ const list = enabled
69
+ if (!list.length) return
70
+ const idx = list.findIndex((t) => t.id === active)
71
+ const fwd = orientation() === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
72
+ const back = orientation() === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
73
+ let next: string | null = null
74
+ if (e.key === fwd) next = list[(idx + 1) % list.length]?.id ?? null
75
+ else if (e.key === back) next = list[(idx - 1 + list.length) % list.length]?.id ?? null
76
+ else if (e.key === 'Home') next = list[0]?.id ?? null
77
+ else if (e.key === 'End') next = list.at(-1)?.id ?? null
78
+ else if ((e.key === 'Enter' || e.key === ' ') && activation() === 'manual') { e.preventDefault(); select(active); return }
79
+ else return
80
+ if (next) {
81
+ e.preventDefault()
82
+ requestFocus(next)
83
+ if (activation() === 'automatic') select(next)
84
+ }
85
+ }
86
+
87
+ return {
88
+ /** Currently selected tab id (falls back to the first enabled tab). */
89
+ get activeId() { return active },
90
+ /** Tab id that should hold DOM focus after the latest keyboard move. */
91
+ get focusId() { return focusId },
92
+ /** Monotonic counter; changes only on keyboard navigation. */
93
+ get focusTick() { return focusTick },
94
+ isActive: (tid: string) => tid === active,
95
+ select,
96
+ onKeydown,
97
+ /** Spread onto the tablist container element. */
98
+ tablistProps: () => ({
99
+ role: 'tablist' as const,
100
+ 'aria-orientation': orientation(),
101
+ 'aria-label': config.ariaLabel?.(),
102
+ }),
103
+ /** Spread onto the tab button for `tid`. */
104
+ tabProps: (tid: string) => {
105
+ const t = tabs().find((x) => x.id === tid)
106
+ const isActive = tid === active
107
+ return {
108
+ role: 'tab' as const,
109
+ id: tabId(tid),
110
+ 'data-tab': tid,
111
+ 'aria-selected': isActive,
112
+ 'aria-controls': panelId(tid),
113
+ tabindex: isActive ? 0 : -1,
114
+ disabled: t?.disabled,
115
+ onclick: () => select(tid),
116
+ onkeydown: onKeydown,
117
+ }
118
+ },
119
+ /** Spread onto the tabpanel element for `tid` (usually the active id). */
120
+ panelProps: (tid: string) => ({
121
+ role: 'tabpanel' as const,
122
+ id: panelId(tid),
123
+ 'aria-labelledby': tabId(tid),
124
+ tabindex: 0,
125
+ }),
126
+ }
127
+ }
128
+
129
+ export type Tabs = ReturnType<typeof createTabs>
@@ -0,0 +1,98 @@
1
+ /**
2
+ * createTagsInput - the HEADLESS core behind <SvTagsInput>: an editable
3
+ * token/chips field (type + Enter/comma to add, Backspace to remove the last,
4
+ * per-chip remove). Owns the draft text + add/remove behavior + keyboard,
5
+ * exposed as **prop-getters** you spread onto YOUR OWN markup. No styles/DOM.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createTagsInput } from '@svgrid/grid'
10
+ * let tags = $state<string[]>([])
11
+ * const ti = createTagsInput({ value: () => tags, onChange: (t) => (tags = t) })
12
+ * </script>
13
+ * <div {...ti.rootProps()}>
14
+ * {#each ti.tags as tag, i (tag + i)}
15
+ * <span {...ti.tagProps(i)}>{tag}<button {...ti.removeProps(i)}>x</button></span>
16
+ * {/each}
17
+ * <input {...ti.inputProps()} />
18
+ * </div>
19
+ * ```
20
+ */
21
+
22
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
23
+ export type TagsInputConfig = {
24
+ value: () => string[]
25
+ onChange?: (tags: string[]) => void
26
+ disabled?: () => boolean
27
+ /** Reject duplicate tags. Default true. */
28
+ unique?: () => boolean
29
+ max?: () => number
30
+ ariaLabel?: () => string | undefined
31
+ }
32
+
33
+ export function createTagsInput(config: TagsInputConfig) {
34
+ const disabled = () => config.disabled?.() ?? false
35
+ const unique = () => config.unique?.() ?? true
36
+ const max = () => config.max?.() ?? Infinity
37
+ const tags = () => config.value()
38
+
39
+ let draft = $state('')
40
+
41
+ function add(raw: string) {
42
+ const tag = raw.trim()
43
+ if (!tag || disabled()) return
44
+ if (tags().length >= max()) return
45
+ if (unique() && tags().includes(tag)) { draft = ''; return }
46
+ config.onChange?.([...tags(), tag])
47
+ draft = ''
48
+ }
49
+ function removeAt(i: number) {
50
+ if (disabled()) return
51
+ config.onChange?.(tags().filter((_, idx) => idx !== i))
52
+ }
53
+ function setDraft(v: string) { draft = v }
54
+ function onInput(e: Event) { draft = (e.currentTarget as HTMLInputElement).value }
55
+ function onKeydown(e: KeyboardEvent) {
56
+ if (disabled()) return
57
+ if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); add(draft) }
58
+ else if (e.key === 'Backspace' && draft === '' && tags().length) { removeAt(tags().length - 1) }
59
+ }
60
+ function onBlur() { add(draft) }
61
+
62
+ return {
63
+ /** Current tags (controlled value). */
64
+ get tags() { return tags() },
65
+ /** Current draft text in the input. */
66
+ get draft() { return draft },
67
+ add,
68
+ removeAt,
69
+ setDraft,
70
+ /** Spread onto the container element. */
71
+ rootProps: () => ({
72
+ role: 'group' as const,
73
+ 'aria-label': config.ariaLabel?.(),
74
+ }),
75
+ /** Spread onto the text <input>. */
76
+ inputProps: () => ({
77
+ type: 'text' as const,
78
+ value: draft,
79
+ disabled: disabled(),
80
+ 'aria-label': config.ariaLabel?.() ?? 'Add tag',
81
+ oninput: onInput,
82
+ onkeydown: onKeydown,
83
+ onblur: onBlur,
84
+ }),
85
+ /** Spread onto the chip wrapper at `index`. */
86
+ tagProps: (index: number) => ({
87
+ 'data-idx': index,
88
+ }),
89
+ /** Spread onto a chip's remove <button> at `index`. */
90
+ removeProps: (index: number) => ({
91
+ type: 'button' as const,
92
+ 'aria-label': `Remove ${tags()[index] ?? ''}`,
93
+ onclick: () => removeAt(index),
94
+ }),
95
+ }
96
+ }
97
+
98
+ export type TagsInput = ReturnType<typeof createTagsInput>
@@ -0,0 +1,289 @@
1
+ /**
2
+ * createTimePicker - the HEADLESS core behind <SvTimePicker>: a runes-based
3
+ * state machine for an analog clock-dial time picker (12/24-hour, minute
4
+ * snapping, hour->minute auto-switch, keyboard) plus the pure dial geometry
5
+ * (tick coordinates + hand endpoint) and **prop-getters** you spread onto YOUR
6
+ * OWN markup. The DOM-bound pieces - pointer capture + `getBoundingClientRect`
7
+ * hit-testing - stay in the styled component; this core exposes `SIZE`/`C` and a
8
+ * pure `pointerSelect(angle, dist)` so the component only has to convert an event
9
+ * into an angle.
10
+ *
11
+ * ```svelte
12
+ * <script lang="ts">
13
+ * import { createTimePicker } from '@svgrid/grid'
14
+ * let value = $state<Date | null>(new Date())
15
+ * const tp = createTimePicker({ value: () => value, onChange: (d) => (value = d) })
16
+ * </script>
17
+ * <button {...tp.segProps('hour')}>{tp.displayHour}</button>:<button {...tp.segProps('minute')}>{tp.mm}</button>
18
+ * ```
19
+ */
20
+ export type TimeValue = Date | string | number | null
21
+
22
+ export type TimeSelection = 'hour' | 'minute'
23
+ export type TimeFormat = '12-hour' | '24-hour'
24
+
25
+ /** A clock-face number and its computed position on the dial. */
26
+ export type DialTick = { label: string; x: number; y: number; value: number; ring: 'outer' | 'inner' }
27
+
28
+ /** Reactive inputs are getters; callbacks are closures. */
29
+ export type TimePickerConfig = {
30
+ /** Date, "HH:MM[:SS]" string, or epoch ms. */
31
+ value: () => TimeValue
32
+ /** Fires with a Date (today's date carrying the picked time). */
33
+ onChange?: (value: Date) => void
34
+ format?: () => TimeFormat
35
+ minuteInterval?: () => number
36
+ /** After picking an hour, jump the dial to minutes. Default true. */
37
+ autoSwitchToMinutes?: () => boolean
38
+ disabled?: () => boolean
39
+ readonly?: () => boolean
40
+ /** Which dial opens first. */
41
+ selection?: () => TimeSelection
42
+ }
43
+
44
+ // --- Dial geometry (pure) -----------------------------------------------------
45
+ const SIZE = 240
46
+ const C = SIZE / 2
47
+ const OUTER = C - 22
48
+ const INNER = C - 58
49
+
50
+ function pointAt(angleDeg: number, r: number) {
51
+ const rad = ((angleDeg - 90) * Math.PI) / 180
52
+ return { x: C + r * Math.cos(rad), y: C + r * Math.sin(rad) }
53
+ }
54
+
55
+ /** Parse a time value to { h, m }, falling back to "now" (pure). */
56
+ export function parseTimeValue(v: TimeValue): { h: number; m: number } {
57
+ if (v == null) { const n = new Date(); return { h: n.getHours(), m: n.getMinutes() } }
58
+ if (typeof v === 'string') {
59
+ const mt = v.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/)
60
+ if (mt) return { h: Math.min(23, +mt[1]!), m: Math.min(59, +mt[2]!) }
61
+ const d = new Date(v)
62
+ if (!isNaN(d.getTime())) return { h: d.getHours(), m: d.getMinutes() }
63
+ const n = new Date(); return { h: n.getHours(), m: n.getMinutes() }
64
+ }
65
+ const d = new Date(v)
66
+ return { h: d.getHours(), m: d.getMinutes() }
67
+ }
68
+
69
+ export function createTimePicker(config: TimePickerConfig) {
70
+ const format = () => config.format?.() ?? '24-hour'
71
+ const minuteInterval = () => config.minuteInterval?.() ?? 1
72
+ const autoSwitchToMinutes = () => config.autoSwitchToMinutes?.() ?? true
73
+ const isDisabled = () => config.disabled?.() ?? false
74
+ const isReadonly = () => config.readonly?.() ?? false
75
+
76
+ const is12 = $derived(format() === '12-hour')
77
+ const isInteractive = $derived(!isDisabled() && !isReadonly())
78
+
79
+ let hours = $state(0)
80
+ let minutes = $state(0)
81
+ let sel = $state<TimeSelection>('hour')
82
+ let seeded = false
83
+ let lastKey = '￿'
84
+ $effect(() => {
85
+ const { h, m } = parseTimeValue(config.value())
86
+ const key = `${h}:${m}`
87
+ if (key !== lastKey) {
88
+ lastKey = key
89
+ hours = h
90
+ minutes = m
91
+ if (!seeded) { seeded = true; sel = config.selection?.() ?? 'hour' }
92
+ }
93
+ })
94
+
95
+ const displayHour = $derived(is12 ? (hours % 12 === 0 ? 12 : hours % 12) : hours)
96
+ const isPm = $derived(hours >= 12)
97
+ const mm = $derived(String(minutes).padStart(2, '0'))
98
+
99
+ const hourTicks = $derived.by<DialTick[]>(() => {
100
+ const ticks: DialTick[] = []
101
+ if (is12) {
102
+ for (let i = 0; i < 12; i++) {
103
+ const h = i === 0 ? 12 : i
104
+ const p = pointAt(i * 30, OUTER)
105
+ ticks.push({ label: String(h), x: p.x, y: p.y, value: h, ring: 'outer' })
106
+ }
107
+ } else {
108
+ for (let i = 0; i < 12; i++) {
109
+ const outer = pointAt(i * 30, OUTER) // 12..23
110
+ const inner = pointAt(i * 30, INNER) // 00..11
111
+ const ho = i + 12
112
+ const hi = i
113
+ ticks.push({ label: String(ho).padStart(2, '0'), x: outer.x, y: outer.y, value: ho, ring: 'outer' })
114
+ ticks.push({ label: hi === 0 ? '00' : String(hi).padStart(2, '0'), x: inner.x, y: inner.y, value: hi, ring: 'inner' })
115
+ }
116
+ }
117
+ return ticks
118
+ })
119
+
120
+ const minuteTicks = $derived.by<DialTick[]>(() =>
121
+ Array.from({ length: 12 }, (_, i) => {
122
+ const v = i * 5
123
+ const p = pointAt(i * 30, OUTER)
124
+ return { label: String(v).padStart(2, '0'), x: p.x, y: p.y, value: v, ring: 'outer' as const }
125
+ }),
126
+ )
127
+
128
+ // Hand endpoint for the current selection.
129
+ const handAngle = $derived(
130
+ sel === 'hour' ? (is12 ? (displayHour % 12) * 30 : (hours % 12) * 30) : minutes * 6,
131
+ )
132
+ const handRadius = $derived(sel === 'hour' && !is12 && hours % 12 === hours && hours < 12 ? INNER : OUTER)
133
+ const handEnd = $derived(pointAt(handAngle, handRadius))
134
+
135
+ // --- Emit + mutators -------------------------------------------------------
136
+ function emit() {
137
+ const d = new Date()
138
+ d.setHours(hours, minutes, 0, 0)
139
+ config.onChange?.(d)
140
+ }
141
+
142
+ const snap = (m: number) => {
143
+ const step = Math.max(1, Math.floor(minuteInterval()))
144
+ return (Math.round(m / step) * step) % 60
145
+ }
146
+
147
+ function setSelection(s: TimeSelection) { sel = s }
148
+
149
+ function setMinute(m: number) {
150
+ minutes = snap(m)
151
+ lastKey = `${hours}:${minutes}`
152
+ emit()
153
+ }
154
+
155
+ function setAmPm(pm: boolean) {
156
+ if (pm === isPm) return
157
+ hours = pm ? (hours % 12) + 12 : hours % 12
158
+ lastKey = `${hours}:${minutes}`
159
+ emit()
160
+ }
161
+
162
+ /** Apply a dial angle (0..360, 12 o'clock = 0) + distance from center. The
163
+ * styled component converts a pointer event into these via the DOM rect. */
164
+ function pointerSelect(angle: number, dist: number) {
165
+ if (!isInteractive) return
166
+ if (sel === 'hour') {
167
+ const idx = Math.round(angle / 30) % 12
168
+ if (is12) {
169
+ const h = idx === 0 ? 12 : idx
170
+ const base = h % 12
171
+ hours = isPm ? base + 12 : base
172
+ } else {
173
+ const inner = dist < (OUTER + INNER) / 2
174
+ hours = inner ? idx : idx + 12
175
+ }
176
+ lastKey = `${hours}:${minutes}`
177
+ emit()
178
+ } else {
179
+ const m = Math.round(angle / 6) % 60
180
+ setMinute(m)
181
+ }
182
+ }
183
+
184
+ /** Called on pointer-up: auto-advance hour -> minute when configured. */
185
+ function endPointer() {
186
+ if (sel === 'hour' && autoSwitchToMinutes()) sel = 'minute'
187
+ }
188
+
189
+ function onKeydown(e: KeyboardEvent) {
190
+ if (!isInteractive) return
191
+ const step = sel === 'minute' ? Math.max(1, Math.floor(minuteInterval())) : 1
192
+ if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
193
+ e.preventDefault()
194
+ if (sel === 'hour') { hours = (hours + 1) % 24 } else { minutes = (minutes + step) % 60 }
195
+ lastKey = `${hours}:${minutes}`; emit()
196
+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
197
+ e.preventDefault()
198
+ if (sel === 'hour') { hours = (hours + 23) % 24 } else { minutes = (minutes - step + 60) % 60 }
199
+ lastKey = `${hours}:${minutes}`; emit()
200
+ } else if (e.key === 'Tab' && !e.shiftKey && sel === 'hour') {
201
+ sel = 'minute'
202
+ }
203
+ }
204
+
205
+ function now() {
206
+ const n = new Date()
207
+ hours = n.getHours(); minutes = n.getMinutes()
208
+ lastKey = `${hours}:${minutes}`
209
+ emit()
210
+ }
211
+
212
+ function isActiveHour(t: DialTick): boolean {
213
+ if (is12) return t.value === displayHour
214
+ return t.value === hours
215
+ }
216
+
217
+ return {
218
+ // Dial geometry constants (the component needs these to map a pointer event
219
+ // to an angle without re-deriving the layout).
220
+ SIZE,
221
+ C,
222
+
223
+ // --- Reactive state -------------------------------------------------------
224
+ get hours() { return hours },
225
+ get minutes() { return minutes },
226
+ get selection() { return sel },
227
+ get is12() { return is12 },
228
+ get isInteractive() { return isInteractive },
229
+ get displayHour() { return displayHour },
230
+ get isPm() { return isPm },
231
+ get mm() { return mm },
232
+ get hourTicks() { return hourTicks },
233
+ get minuteTicks() { return minuteTicks },
234
+ get handEnd() { return handEnd },
235
+
236
+ // --- Derived helpers ------------------------------------------------------
237
+ isActiveHour,
238
+
239
+ // --- Actions --------------------------------------------------------------
240
+ setSelection,
241
+ setMinute,
242
+ setAmPm,
243
+ pointerSelect,
244
+ endPointer,
245
+ onKeydown,
246
+ now,
247
+
248
+ // --- Prop-getters ---------------------------------------------------------
249
+ /** The hour / minute segment button in the header. */
250
+ segProps(which: TimeSelection) {
251
+ return {
252
+ type: 'button' as const,
253
+ disabled: isDisabled(),
254
+ onclick: () => setSelection(which),
255
+ }
256
+ },
257
+ /** An AM / PM toggle button (12-hour mode). */
258
+ ampmProps(pm: boolean) {
259
+ return {
260
+ type: 'button' as const,
261
+ disabled: isDisabled(),
262
+ onclick: () => setAmPm(pm),
263
+ }
264
+ },
265
+ /** The clock dial: ARIA slider semantics + keyboard. Pointer capture stays in
266
+ * the component (it needs the DOM element). */
267
+ dialProps() {
268
+ return {
269
+ role: 'slider' as const,
270
+ tabindex: isDisabled() ? -1 : 0,
271
+ 'aria-label': sel === 'hour' ? 'Hour' : 'Minute',
272
+ 'aria-valuenow': sel === 'hour' ? hours : minutes,
273
+ 'aria-valuemin': 0,
274
+ 'aria-valuemax': sel === 'hour' ? 23 : 59,
275
+ onkeydown: onKeydown,
276
+ }
277
+ },
278
+ /** The Now footer button. */
279
+ nowProps() {
280
+ return {
281
+ type: 'button' as const,
282
+ disabled: isDisabled(),
283
+ onclick: now,
284
+ }
285
+ },
286
+ }
287
+ }
288
+
289
+ export type TimePicker = ReturnType<typeof createTimePicker>
@@ -0,0 +1,69 @@
1
+ /**
2
+ * createToggle - the HEADLESS core behind <SvToggleButton>, in the same spirit
3
+ * as `createSvGrid`: a tiny controlled state machine (a single pressed on/off
4
+ * bit + ARIA) exposed as **prop-getters** you spread onto YOUR OWN markup. No
5
+ * styles, no DOM assumptions.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createToggle } from '@svgrid/grid'
10
+ * let pressed = $state(false)
11
+ * const t = createToggle({ pressed: () => pressed, onChange: (v) => (pressed = v) })
12
+ * </script>
13
+ * <button {...t.buttonProps()}>Bold</button>
14
+ * ```
15
+ *
16
+ * The styled <SvToggleButton> is just one renderer over this core.
17
+ */
18
+
19
+ /** Reactive inputs are passed as getters so the core tracks live prop changes
20
+ * (the same controlled pattern used across the SvGrid UI kit). */
21
+ export type ToggleConfig = {
22
+ pressed: () => boolean
23
+ onChange?: (pressed: boolean) => void
24
+ disabled?: () => boolean
25
+ ariaLabel?: () => string | undefined
26
+ }
27
+
28
+ export type ToggleButtonProps = {
29
+ type: 'button'
30
+ 'aria-pressed': boolean
31
+ 'aria-label': string | undefined
32
+ disabled: boolean
33
+ 'data-pressed': '' | undefined
34
+ onclick: () => void
35
+ }
36
+
37
+ export type Toggle = {
38
+ /** Current pressed state. */
39
+ readonly pressed: boolean
40
+ readonly disabled: boolean
41
+ /** Flip pressed (no-op when disabled). */
42
+ toggle: () => void
43
+ /** Spread onto the button element. */
44
+ buttonProps: () => ToggleButtonProps
45
+ }
46
+
47
+ export function createToggle(config: ToggleConfig): Toggle {
48
+ const pressed = () => config.pressed()
49
+ const disabled = () => config.disabled?.() ?? false
50
+
51
+ function toggle() {
52
+ if (disabled()) return
53
+ config.onChange?.(!pressed())
54
+ }
55
+
56
+ return {
57
+ get pressed() { return pressed() },
58
+ get disabled() { return disabled() },
59
+ toggle,
60
+ buttonProps: () => ({
61
+ type: 'button',
62
+ 'aria-pressed': pressed(),
63
+ 'aria-label': config.ariaLabel?.(),
64
+ disabled: disabled(),
65
+ 'data-pressed': pressed() ? '' : undefined,
66
+ onclick: toggle,
67
+ }),
68
+ }
69
+ }