@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,87 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvSwitchButton - an on/off sliding switch (ARIA `switch`). Parity: Smart
4
+ * `smart-switch-button`. Controlled via `checked` + `onChange`; keyboard
5
+ * togglable; optional inline on/off labels.
6
+ */
7
+ import { createSwitch } from './createSwitch.svelte'
8
+
9
+ type Props = {
10
+ checked?: boolean
11
+ onChange?: (checked: boolean) => void
12
+ disabled?: boolean
13
+ size?: 'sm' | 'md' | 'lg'
14
+ name?: string
15
+ ariaLabel?: string
16
+ /** Optional labels rendered inside the track. */
17
+ onLabel?: string
18
+ offLabel?: string
19
+ }
20
+
21
+ let {
22
+ checked = false,
23
+ onChange,
24
+ disabled = false,
25
+ size = 'md',
26
+ name,
27
+ ariaLabel,
28
+ onLabel,
29
+ offLabel,
30
+ }: Props = $props()
31
+
32
+ // The styled switch is just a renderer over the headless core.
33
+ const sw = createSwitch({
34
+ checked: () => checked,
35
+ onChange: (v) => onChange?.(v),
36
+ disabled: () => disabled,
37
+ ariaLabel: () => ariaLabel,
38
+ })
39
+ </script>
40
+
41
+ <button
42
+ class="sv-switch sv-switch--{size}"
43
+ class:is-on={checked}
44
+ {...sw.switchProps()}
45
+ >
46
+ <span class="sv-switch__track">
47
+ {#if onLabel || offLabel}
48
+ <span class="sv-switch__txt sv-switch__txt--on">{onLabel ?? ''}</span>
49
+ <span class="sv-switch__txt sv-switch__txt--off">{offLabel ?? ''}</span>
50
+ {/if}
51
+ <span class="sv-switch__thumb"></span>
52
+ </span>
53
+ {#if name}<input type="hidden" {name} value={checked ? 'true' : 'false'} />{/if}
54
+ </button>
55
+
56
+ <style>
57
+ .sv-switch {
58
+ --_accent: var(--sg-accent, #2563eb);
59
+ --_h: 22px; --_w: 40px;
60
+ display: inline-flex; align-items: center; background: none; border: 0; padding: 2px; cursor: pointer;
61
+ }
62
+ .sv-switch--sm { --_h: 18px; --_w: 32px; }
63
+ .sv-switch--lg { --_h: 28px; --_w: 52px; }
64
+ .sv-switch[disabled] { opacity: 0.5; cursor: not-allowed; }
65
+ .sv-switch__track {
66
+ position: relative; width: var(--_w); height: var(--_h); border-radius: 999px;
67
+ background: var(--sg-border, #cbd5e1); transition: background 0.16s;
68
+ display: inline-flex; align-items: center;
69
+ }
70
+ .sv-switch.is-on .sv-switch__track { background: var(--_accent); }
71
+ .sv-switch__thumb {
72
+ position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
73
+ width: calc(var(--_h) - 4px); height: calc(var(--_h) - 4px); border-radius: 50%;
74
+ background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3); transition: left 0.16s;
75
+ }
76
+ .sv-switch.is-on .sv-switch__thumb { left: calc(var(--_w) - var(--_h) + 2px); }
77
+ .sv-switch__txt {
78
+ position: absolute; font-size: 9px; font-weight: 700; color: #fff; text-transform: uppercase;
79
+ top: 50%; transform: translateY(-50%);
80
+ }
81
+ .sv-switch__txt--on { left: 6px; opacity: 0; }
82
+ .sv-switch__txt--off { right: 5px; color: var(--sg-muted, #64748b); }
83
+ .sv-switch.is-on .sv-switch__txt--on { opacity: 1; }
84
+ .sv-switch.is-on .sv-switch__txt--off { opacity: 0; }
85
+ .sv-switch:focus-visible { outline: none; }
86
+ .sv-switch:focus-visible .sv-switch__track { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
87
+ </style>
@@ -0,0 +1,109 @@
1
+ <script lang="ts" module>
2
+ export type { TabItem } from './createTabs.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvTabs - a WAI-ARIA tabs widget with roving tabindex + arrow-key navigation.
8
+ * Parity: Smart `smart-tabs`. Controlled via `value` + `onChange`; the active
9
+ * panel is rendered through the `panel` snippet (receives the active id).
10
+ *
11
+ * The behavior (roving focus, activation, ARIA) lives in the headless
12
+ * `createTabs` core; this component is just one styled renderer over it.
13
+ */
14
+ import type { Snippet } from 'svelte'
15
+ import { createTabs, type TabItem } from './createTabs.svelte'
16
+
17
+ type Props = {
18
+ tabs: ReadonlyArray<TabItem>
19
+ value?: string
20
+ onChange?: (id: string) => void
21
+ orientation?: 'horizontal' | 'vertical'
22
+ /** 'line' underline (default) or 'pill' segmented look. */
23
+ variant?: 'line' | 'pill'
24
+ /** Activate on focus (automatic) vs. on Enter/Space (manual). Default automatic. */
25
+ activation?: 'automatic' | 'manual'
26
+ panel?: Snippet<[string]>
27
+ ariaLabel?: string
28
+ }
29
+
30
+ let {
31
+ tabs,
32
+ value = $bindable(),
33
+ onChange,
34
+ orientation = 'horizontal',
35
+ variant = 'line',
36
+ activation = 'automatic',
37
+ panel,
38
+ ariaLabel,
39
+ }: Props = $props()
40
+
41
+ const tabsCtl = createTabs({
42
+ tabs: () => tabs,
43
+ value: () => value,
44
+ onChange: (id) => { value = id; onChange?.(id) },
45
+ orientation: () => orientation,
46
+ activation: () => activation,
47
+ ariaLabel: () => ariaLabel,
48
+ })
49
+ const active = $derived(tabsCtl.activeId)
50
+
51
+ // DOM focus movement is a render concern: follow the core's roving focus target.
52
+ let tablistEl: HTMLDivElement | null = null
53
+ let lastFocusTick = 0
54
+ $effect(() => {
55
+ if (tabsCtl.focusTick !== lastFocusTick) {
56
+ lastFocusTick = tabsCtl.focusTick
57
+ const fid = tabsCtl.focusId
58
+ queueMicrotask(() => tablistEl?.querySelector<HTMLElement>(`[data-tab="${fid}"]`)?.focus())
59
+ }
60
+ })
61
+ </script>
62
+
63
+ <div class="sv-tabs sv-tabs--{orientation} sv-tabs--{variant}">
64
+ <div bind:this={tablistEl} class="sv-tabs__list" {...tabsCtl.tablistProps()}>
65
+ {#each tabs as tab (tab.id)}
66
+ <button
67
+ type="button"
68
+ class="sv-tabs__tab"
69
+ class:is-active={tab.id === active}
70
+ {...tabsCtl.tabProps(tab.id)}
71
+ >{tab.label}</button>
72
+ {/each}
73
+ </div>
74
+ {#if panel}
75
+ <div class="sv-tabs__panel" {...tabsCtl.panelProps(active)}>
76
+ {@render panel(active)}
77
+ </div>
78
+ {/if}
79
+ </div>
80
+
81
+ <style>
82
+ .sv-tabs { --_accent: var(--sg-accent, #2563eb); display: flex; gap: 0; }
83
+ .sv-tabs--horizontal { flex-direction: column; }
84
+ .sv-tabs--vertical { flex-direction: row; }
85
+ .sv-tabs__list { display: flex; gap: 2px; }
86
+ .sv-tabs--horizontal .sv-tabs__list { border-bottom: 1px solid var(--sg-border, #e2e8f0); }
87
+ .sv-tabs--vertical .sv-tabs__list { flex-direction: column; border-right: 1px solid var(--sg-border, #e2e8f0); }
88
+ .sv-tabs--pill .sv-tabs__list { border: 0; background: var(--sg-header-bg, #f1f5f9); padding: 3px; border-radius: 9px; gap: 3px; }
89
+
90
+ .sv-tabs__tab {
91
+ background: none; border: 0; font: inherit; font-size: 13px; font-weight: 600; color: var(--sg-muted, #64748b);
92
+ padding: 9px 14px; cursor: pointer; white-space: nowrap; position: relative;
93
+ }
94
+ .sv-tabs__tab:hover:not(:disabled):not(.is-active) { color: var(--sg-fg, #0f172a); }
95
+ .sv-tabs__tab:disabled { opacity: 0.45; cursor: not-allowed; }
96
+ .sv-tabs__tab:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: -2px; border-radius: 6px; }
97
+
98
+ .sv-tabs--line .sv-tabs__tab.is-active { color: var(--_accent); }
99
+ .sv-tabs--line .sv-tabs__tab.is-active::after {
100
+ content: ''; position: absolute; left: 8px; right: 8px; height: 2px; background: var(--_accent);
101
+ }
102
+ .sv-tabs--horizontal.sv-tabs--line .sv-tabs__tab.is-active::after { bottom: -1px; }
103
+ .sv-tabs--vertical.sv-tabs--line .sv-tabs__tab.is-active::after { top: 8px; bottom: 8px; left: auto; right: -1px; width: 2px; height: auto; }
104
+
105
+ .sv-tabs--pill .sv-tabs__tab { border-radius: 7px; padding: 7px 14px; }
106
+ .sv-tabs--pill .sv-tabs__tab.is-active { background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
107
+
108
+ .sv-tabs__panel { padding: 16px 2px; outline: none; }
109
+ </style>
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvTagsInput - editable token/chips input. Type + Enter/comma to add,
4
+ * Backspace to remove the last, click × to remove a chip. Parity: Smart
5
+ * `smart-tags` / `smart-input` token mode. Emits string[].
6
+ *
7
+ * One styled renderer over the headless `createTagsInput` core (draft +
8
+ * add/remove + keyboard).
9
+ */
10
+ import { createTagsInput } from './createTagsInput.svelte'
11
+
12
+ type Props = {
13
+ value?: string[]
14
+ onChange?: (tags: string[]) => void
15
+ placeholder?: string
16
+ disabled?: boolean
17
+ /** Reject duplicate tags. Default true. */
18
+ unique?: boolean
19
+ max?: number
20
+ name?: string
21
+ ariaLabel?: string
22
+ }
23
+
24
+ let { value = [], onChange, placeholder = 'Add tag…', disabled = false, unique = true, max = Infinity, name, ariaLabel }: Props = $props()
25
+
26
+ const ti = createTagsInput({
27
+ value: () => value,
28
+ onChange: (t) => onChange?.(t),
29
+ disabled: () => disabled,
30
+ unique: () => unique,
31
+ max: () => max,
32
+ ariaLabel: () => ariaLabel,
33
+ })
34
+ </script>
35
+
36
+ <div class="sv-tags" class:is-disabled={disabled} {...ti.rootProps()}>
37
+ {#each value as tag, i (tag + i)}
38
+ <span class="sv-tags__chip" {...ti.tagProps(i)}>
39
+ <span class="sv-tags__label">{tag}</span>
40
+ {#if !disabled}
41
+ <button class="sv-tags__x" {...ti.removeProps(i)}>&times;</button>
42
+ {/if}
43
+ </span>
44
+ {/each}
45
+ <input
46
+ class="sv-tags__input"
47
+ placeholder={value.length ? '' : placeholder}
48
+ {...ti.inputProps()}
49
+ />
50
+ {#if name}<input type="hidden" {name} value={value.join(',')} />{/if}
51
+ </div>
52
+
53
+ <style>
54
+ .sv-tags {
55
+ --_accent: var(--sg-accent, #2563eb);
56
+ display: flex; flex-wrap: wrap; gap: 5px; align-items: center; width: 280px; min-height: 34px; padding: 4px 6px;
57
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
58
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
59
+ }
60
+ .sv-tags:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
61
+ .sv-tags.is-disabled { opacity: 0.6; }
62
+ .sv-tags__chip {
63
+ display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 4px 0 8px;
64
+ background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent);
65
+ border-radius: 5px; font-size: 12px; font-weight: 600;
66
+ }
67
+ .sv-tags__x { background: none; border: 0; color: inherit; cursor: pointer; font-size: 15px; line-height: 1; padding: 0 2px; opacity: 0.7; }
68
+ .sv-tags__x:hover { opacity: 1; }
69
+ .sv-tags__input { flex: 1; min-width: 80px; border: 0; background: none; outline: none; color: inherit; font: inherit; font-size: 13px; height: 24px; }
70
+ </style>
@@ -0,0 +1,201 @@
1
+ <script lang="ts" module>
2
+ export type { TimeValue } from './createTimePicker.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvTimePicker - an analog clock-dial time picker (parity: Smart
8
+ * `smart-time-picker`). 12/24-hour, minute snapping, hour->minute auto-switch,
9
+ * pointer-drag + keyboard. It is a styled renderer over the headless
10
+ * `createTimePicker` core: the core owns the value math, dial geometry, keyboard
11
+ * and ARIA; this component keeps the DOM-bound pointer capture + hit-testing.
12
+ * All visuals come from `--sg-*` tokens so every theme applies for free.
13
+ */
14
+ import { createTimePicker, type TimeValue, type TimeFormat, type TimeSelection } from './createTimePicker.svelte'
15
+
16
+ type Props = {
17
+ /** Date, "HH:MM[:SS]" string, or epoch ms. */
18
+ value?: TimeValue
19
+ /** Fires with a Date (today's date carrying the picked time). */
20
+ onChange?: (value: Date) => void
21
+ format?: TimeFormat
22
+ minuteInterval?: number
23
+ /** After picking an hour, jump the dial to minutes. Default true. */
24
+ autoSwitchToMinutes?: boolean
25
+ /** Show the Now footer button. */
26
+ footer?: boolean
27
+ disabled?: boolean
28
+ readonly?: boolean
29
+ name?: string
30
+ /** Which dial opens first. */
31
+ selection?: TimeSelection
32
+ }
33
+
34
+ let {
35
+ value = null,
36
+ onChange,
37
+ format = '24-hour',
38
+ minuteInterval = 1,
39
+ autoSwitchToMinutes = true,
40
+ footer = false,
41
+ disabled = false,
42
+ readonly = false,
43
+ name,
44
+ selection = 'hour',
45
+ }: Props = $props()
46
+
47
+ // The headless core owns all state, geometry, keyboard + ARIA. Reactive inputs
48
+ // are getters; callbacks are closures.
49
+ const tp = createTimePicker({
50
+ value: () => value,
51
+ onChange: (d) => onChange?.(d),
52
+ format: () => format,
53
+ minuteInterval: () => minuteInterval,
54
+ autoSwitchToMinutes: () => autoSwitchToMinutes,
55
+ disabled: () => disabled,
56
+ readonly: () => readonly,
57
+ selection: () => selection,
58
+ })
59
+
60
+ // --- Pointer drag on the dial (DOM-bound; stays in the component) -----------
61
+ // The core exposes SIZE/C + a pure `pointerSelect(angle, dist)`; here we only
62
+ // turn a pointer event into that angle via the live element rect.
63
+ let dialEl: SVGSVGElement | null = null
64
+ let dragging = false
65
+
66
+ function angleFromEvent(e: PointerEvent): { angle: number; dist: number } {
67
+ const rect = dialEl!.getBoundingClientRect()
68
+ const scale = tp.SIZE / rect.width
69
+ const x = (e.clientX - rect.left) * scale - tp.C
70
+ const y = (e.clientY - rect.top) * scale - tp.C
71
+ let deg = (Math.atan2(y, x) * 180) / Math.PI + 90
72
+ if (deg < 0) deg += 360
73
+ return { angle: deg, dist: Math.hypot(x, y) }
74
+ }
75
+
76
+ function onDialPointerDown(e: PointerEvent) {
77
+ if (!tp.isInteractive) return
78
+ dragging = true
79
+ dialEl?.setPointerCapture(e.pointerId)
80
+ const { angle, dist } = angleFromEvent(e)
81
+ tp.pointerSelect(angle, dist)
82
+ }
83
+ function onDialPointerMove(e: PointerEvent) {
84
+ if (!dragging) return
85
+ const { angle, dist } = angleFromEvent(e)
86
+ tp.pointerSelect(angle, dist)
87
+ }
88
+ function onDialPointerUp(e: PointerEvent) {
89
+ if (!dragging) return
90
+ dragging = false
91
+ dialEl?.releasePointerCapture(e.pointerId)
92
+ tp.endPointer()
93
+ }
94
+ </script>
95
+
96
+ <div class="sv-tp" class:sv-tp--disabled={disabled} role="group" aria-label="Time picker">
97
+ <div class="sv-tp__head">
98
+ <button class="sv-tp__seg" class:is-active={tp.selection === 'hour'} {...tp.segProps('hour')}>
99
+ {String(tp.displayHour).padStart(2, '0')}
100
+ </button>
101
+ <span class="sv-tp__colon">:</span>
102
+ <button class="sv-tp__seg" class:is-active={tp.selection === 'minute'} {...tp.segProps('minute')}>
103
+ {tp.mm}
104
+ </button>
105
+ {#if tp.is12}
106
+ <div class="sv-tp__ampm">
107
+ <button class:is-active={!tp.isPm} {...tp.ampmProps(false)}>AM</button>
108
+ <button class:is-active={tp.isPm} {...tp.ampmProps(true)}>PM</button>
109
+ </div>
110
+ {/if}
111
+ </div>
112
+
113
+ <svg
114
+ bind:this={dialEl}
115
+ class="sv-tp__dial"
116
+ viewBox={`0 0 ${tp.SIZE} ${tp.SIZE}`}
117
+ width="100%"
118
+ {...tp.dialProps()}
119
+ onpointerdown={onDialPointerDown}
120
+ onpointermove={onDialPointerMove}
121
+ onpointerup={onDialPointerUp}
122
+ onpointercancel={onDialPointerUp}
123
+ >
124
+ <circle class="sv-tp__face" cx={tp.C} cy={tp.C} r={tp.C - 4} />
125
+ <!-- hand -->
126
+ <line class="sv-tp__hand" x1={tp.C} y1={tp.C} x2={tp.handEnd.x} y2={tp.handEnd.y} />
127
+ <circle class="sv-tp__hub" cx={tp.C} cy={tp.C} r="3" />
128
+ <circle class="sv-tp__knob" cx={tp.handEnd.x} cy={tp.handEnd.y} r="15" />
129
+ {#if tp.selection === 'hour'}
130
+ {#each tp.hourTicks as t (t.ring + '-' + t.value)}
131
+ <text class="sv-tp__num" class:is-active={tp.isActiveHour(t)} class:is-inner={t.ring === 'inner'} x={t.x} y={t.y} dominant-baseline="central" text-anchor="middle">{t.label}</text>
132
+ {/each}
133
+ {:else}
134
+ {#each tp.minuteTicks as t (t.value)}
135
+ <text class="sv-tp__num" class:is-active={t.value === tp.minutes} x={t.x} y={t.y} dominant-baseline="central" text-anchor="middle">{t.label}</text>
136
+ {/each}
137
+ {/if}
138
+ </svg>
139
+
140
+ {#if footer}
141
+ <div class="sv-tp__footer">
142
+ <button class="sv-tp__now" {...tp.nowProps()}>Now</button>
143
+ </div>
144
+ {/if}
145
+
146
+ {#if name}<input type="hidden" {name} value={`${String(tp.hours).padStart(2, '0')}:${tp.mm}`} />{/if}
147
+ </div>
148
+
149
+ <style>
150
+ .sv-tp {
151
+ --_accent: var(--sg-accent, #2563eb);
152
+ --_bg: var(--sg-bg, #fff);
153
+ --_fg: var(--sg-fg, #0f172a);
154
+ --_muted: var(--sg-muted, #64748b);
155
+ --_border: var(--sg-border, #e2e8f0);
156
+ --_face: var(--sg-header-bg, #f1f5f9);
157
+ --_radius: var(--sg-radius, 8px);
158
+ display: inline-flex; flex-direction: column; gap: 10px; align-items: center;
159
+ width: 260px; padding: 12px;
160
+ background: var(--_bg); color: var(--_fg);
161
+ border: 1px solid var(--_border); border-radius: calc(var(--_radius) + 4px);
162
+ user-select: none;
163
+ }
164
+ .sv-tp--disabled { opacity: 0.55; pointer-events: none; }
165
+
166
+ .sv-tp__head { display: flex; align-items: center; gap: 2px; font-variant-numeric: tabular-nums; }
167
+ .sv-tp__seg {
168
+ font-size: 30px; font-weight: 700; line-height: 1;
169
+ background: none; border: 0; color: var(--_muted); cursor: pointer;
170
+ padding: 2px 6px; border-radius: var(--_radius);
171
+ }
172
+ .sv-tp__seg.is-active { color: var(--_accent); background: color-mix(in srgb, var(--_accent) 12%, transparent); }
173
+ .sv-tp__colon { font-size: 28px; font-weight: 700; color: var(--_muted); }
174
+ .sv-tp__ampm { display: flex; flex-direction: column; margin-left: 8px; gap: 2px; }
175
+ .sv-tp__ampm button {
176
+ font-size: 11px; font-weight: 700; padding: 3px 8px;
177
+ background: none; border: 1px solid var(--_border); border-radius: 6px; color: var(--_muted); cursor: pointer;
178
+ }
179
+ .sv-tp__ampm button.is-active { background: var(--_accent); color: var(--sg-on-accent, #fff); border-color: var(--_accent); }
180
+
181
+ .sv-tp__dial { touch-action: none; cursor: pointer; outline: none; display: block; width: 100%; height: auto; aspect-ratio: 1; }
182
+ .sv-tp__dial:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; border-radius: 50%; }
183
+ .sv-tp__face { fill: var(--_face); }
184
+ .sv-tp__hand { stroke: var(--_accent); stroke-width: 2; }
185
+ .sv-tp__hub { fill: var(--_accent); }
186
+ .sv-tp__knob { fill: var(--_accent); opacity: 0.9; }
187
+ .sv-tp__num {
188
+ font-size: 13px; fill: var(--_fg); pointer-events: none;
189
+ font-family: inherit;
190
+ }
191
+ .sv-tp__num.is-inner { font-size: 11px; fill: var(--_muted); }
192
+ .sv-tp__num.is-active { fill: var(--sg-on-accent, #fff); font-weight: 700; }
193
+
194
+ .sv-tp__footer { width: 100%; display: flex; justify-content: center; border-top: 1px solid var(--_border); padding-top: 8px; }
195
+ .sv-tp__now {
196
+ padding: 5px 14px; font: inherit; font-size: 12px; font-weight: 600;
197
+ background: none; border: 1px solid var(--_border); border-radius: var(--_radius);
198
+ color: var(--_accent); cursor: pointer;
199
+ }
200
+ .sv-tp__now:hover { background: color-mix(in srgb, var(--_accent) 10%, transparent); }
201
+ </style>
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Component tests for SvTimePicker. jsdom lacks real SVG layout, so pointer-drag
3
+ * geometry is exercised lightly; the header/segment/AM-PM/keyboard logic (where
4
+ * the value math lives) is covered directly.
5
+ */
6
+ import { describe, expect, it } from 'vitest'
7
+ import { mount, unmount, flushSync } from 'svelte'
8
+ import SvTimePicker from './SvTimePicker.svelte'
9
+
10
+ function mountTp(props: Record<string, unknown>) {
11
+ const target = document.createElement('div')
12
+ document.body.appendChild(target)
13
+ const app = mount(SvTimePicker, { target, props: props as any })
14
+ flushSync()
15
+ return { target, destroy: () => { unmount(app); target.remove() } }
16
+ }
17
+
18
+ const segs = (t: HTMLElement) => t.querySelectorAll<HTMLButtonElement>('.sv-tp__seg')
19
+
20
+ describe('SvTimePicker render', () => {
21
+ it('shows the seeded time (24h)', () => {
22
+ const { target, destroy } = mountTp({ value: '14:30' })
23
+ try {
24
+ expect(segs(target)[0]!.textContent!.trim()).toBe('14')
25
+ expect(segs(target)[1]!.textContent!.trim()).toBe('30')
26
+ expect(target.querySelector('.sv-tp__ampm')).toBeNull() // no AM/PM in 24h
27
+ } finally { destroy() }
28
+ })
29
+
30
+ it('shows 12-hour display with AM/PM', () => {
31
+ const { target, destroy } = mountTp({ value: '14:30', format: '12-hour' })
32
+ try {
33
+ expect(segs(target)[0]!.textContent!.trim()).toBe('02') // 14 -> 2 PM
34
+ const ampm = target.querySelectorAll<HTMLButtonElement>('.sv-tp__ampm button')
35
+ expect(ampm[1]!.classList.contains('is-active')).toBe(true) // PM active
36
+ } finally { destroy() }
37
+ })
38
+
39
+ it('renders 24 hour ticks (two rings) in 24h and 12 in 12h', () => {
40
+ const a = mountTp({ value: '10:00' })
41
+ try {
42
+ expect(a.target.querySelectorAll('.sv-tp__num').length).toBe(24)
43
+ } finally { a.destroy() }
44
+ const b = mountTp({ value: '10:00', format: '12-hour' })
45
+ try {
46
+ expect(b.target.querySelectorAll('.sv-tp__num').length).toBe(12)
47
+ } finally { b.destroy() }
48
+ })
49
+ })
50
+
51
+ describe('SvTimePicker interaction', () => {
52
+ it('AM/PM toggle shifts the hour by 12 and emits', () => {
53
+ let got: Date | null = null
54
+ const { target, destroy } = mountTp({ value: '02:15', format: '12-hour', onChange: (d: Date) => (got = d) })
55
+ try {
56
+ const pm = target.querySelectorAll<HTMLButtonElement>('.sv-tp__ampm button')[1]!
57
+ pm.click()
58
+ flushSync()
59
+ expect(got).not.toBeNull()
60
+ expect((got as unknown as Date).getHours()).toBe(14) // 2 AM -> 2 PM
61
+ expect((got as unknown as Date).getMinutes()).toBe(15)
62
+ } finally { destroy() }
63
+ })
64
+
65
+ it('keyboard bumps the selected field', () => {
66
+ let got: Date | null = null
67
+ const { target, destroy } = mountTp({ value: '10:00', selection: 'minute', minuteInterval: 15, onChange: (d: Date) => (got = d) })
68
+ try {
69
+ const dial = target.querySelector<SVGSVGElement>('.sv-tp__dial')!
70
+ dial.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))
71
+ flushSync()
72
+ expect((got as unknown as Date).getMinutes()).toBe(15) // +interval
73
+ } finally { destroy() }
74
+ })
75
+
76
+ it('clicking the minute segment switches the active dial', () => {
77
+ const { target, destroy } = mountTp({ value: '10:00' })
78
+ try {
79
+ segs(target)[1]!.click()
80
+ flushSync()
81
+ expect(segs(target)[1]!.classList.contains('is-active')).toBe(true)
82
+ // minute dial shows 00,05,...,55 (12 labels)
83
+ expect(target.querySelectorAll('.sv-tp__num').length).toBe(12)
84
+ } finally { destroy() }
85
+ })
86
+
87
+ it('Now sets the current time', () => {
88
+ let got: Date | null = null
89
+ const { target, destroy } = mountTp({ value: '00:00', footer: true, onChange: (d: Date) => (got = d) })
90
+ try {
91
+ target.querySelector<HTMLButtonElement>('.sv-tp__now')!.click()
92
+ flushSync()
93
+ expect(got).not.toBeNull()
94
+ const now = new Date()
95
+ expect((got as unknown as Date).getHours()).toBe(now.getHours())
96
+ } finally { destroy() }
97
+ })
98
+ })
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvToggleButton - a button with a pressed on/off state (aria-pressed).
4
+ * Parity: Smart `smart-toggle-button`. Controlled via `pressed` + `onChange`.
5
+ */
6
+ import type { Snippet } from 'svelte'
7
+ import { createToggle } from './createToggle.svelte'
8
+
9
+ type Props = {
10
+ pressed?: boolean
11
+ onChange?: (pressed: boolean) => void
12
+ disabled?: boolean
13
+ size?: 'sm' | 'md' | 'lg'
14
+ ariaLabel?: string
15
+ children?: Snippet
16
+ }
17
+
18
+ let { pressed = false, onChange, disabled = false, size = 'md', ariaLabel, children }: Props = $props()
19
+
20
+ // The styled toggle is just a renderer over the headless core.
21
+ const t = createToggle({
22
+ pressed: () => pressed,
23
+ onChange: (v) => onChange?.(v),
24
+ disabled: () => disabled,
25
+ ariaLabel: () => ariaLabel,
26
+ })
27
+ </script>
28
+
29
+ <button
30
+ class="sv-toggle sv-toggle--{size}"
31
+ class:is-pressed={pressed}
32
+ {...t.buttonProps()}
33
+ >
34
+ {#if children}{@render children()}{/if}
35
+ </button>
36
+
37
+ <style>
38
+ .sv-toggle {
39
+ --_accent: var(--sg-accent, #2563eb);
40
+ --_border: var(--sg-border, #cbd5e1);
41
+ --_radius: var(--sg-radius, 8px);
42
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
43
+ font: inherit; font-weight: 600; line-height: 1;
44
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
45
+ border: 1px solid var(--_border); border-radius: var(--_radius); cursor: pointer; user-select: none;
46
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
47
+ }
48
+ .sv-toggle--sm { padding: 5px 10px; font-size: 12px; }
49
+ .sv-toggle--md { padding: 8px 13px; font-size: 13px; }
50
+ .sv-toggle--lg { padding: 11px 17px; font-size: 15px; }
51
+ .sv-toggle:hover:not([disabled]):not(.is-pressed) { background: var(--sg-row-hover-bg, #f1f5f9); }
52
+ .sv-toggle.is-pressed {
53
+ background: var(--_accent); color: var(--sg-on-accent, #fff); border-color: var(--_accent);
54
+ }
55
+ .sv-toggle[disabled] { opacity: 0.55; cursor: not-allowed; }
56
+ .sv-toggle:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
57
+ </style>