@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
@@ -3,7 +3,12 @@
3
3
  * SvTagsInput - editable token/chips input. Type + Enter/comma to add,
4
4
  * Backspace to remove the last, click × to remove a chip. Parity: Smart
5
5
  * `smart-tags` / `smart-input` token mode. Emits string[].
6
+ *
7
+ * One styled renderer over the headless `createTagsInput` core (draft +
8
+ * add/remove + keyboard).
6
9
  */
10
+ import { createTagsInput } from './createTagsInput.svelte'
11
+
7
12
  type Props = {
8
13
  value?: string[]
9
14
  onChange?: (tags: string[]) => void
@@ -18,45 +23,29 @@
18
23
 
19
24
  let { value = [], onChange, placeholder = 'Add tag…', disabled = false, unique = true, max = Infinity, name, ariaLabel }: Props = $props()
20
25
 
21
- let draft = $state('')
22
-
23
- function add(raw: string) {
24
- const tag = raw.trim()
25
- if (!tag || disabled) return
26
- if (value.length >= max) return
27
- if (unique && value.includes(tag)) { draft = ''; return }
28
- onChange?.([...value, tag])
29
- draft = ''
30
- }
31
- function removeAt(i: number) {
32
- if (disabled) return
33
- onChange?.(value.filter((_, idx) => idx !== i))
34
- }
35
- function onKeydown(e: KeyboardEvent) {
36
- if (disabled) return
37
- if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); add(draft) }
38
- else if (e.key === 'Backspace' && draft === '' && value.length) { removeAt(value.length - 1) }
39
- }
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
+ })
40
34
  </script>
41
35
 
42
- <div class="sv-tags" class:is-disabled={disabled} role="group" aria-label={ariaLabel}>
36
+ <div class="sv-tags" class:is-disabled={disabled} {...ti.rootProps()}>
43
37
  {#each value as tag, i (tag + i)}
44
- <span class="sv-tags__chip">
38
+ <span class="sv-tags__chip" {...ti.tagProps(i)}>
45
39
  <span class="sv-tags__label">{tag}</span>
46
40
  {#if !disabled}
47
- <button type="button" class="sv-tags__x" aria-label={`Remove ${tag}`} onclick={() => removeAt(i)}>&times;</button>
41
+ <button class="sv-tags__x" {...ti.removeProps(i)}>&times;</button>
48
42
  {/if}
49
43
  </span>
50
44
  {/each}
51
45
  <input
52
46
  class="sv-tags__input"
53
- type="text"
54
- bind:value={draft}
55
47
  placeholder={value.length ? '' : placeholder}
56
- {disabled}
57
- aria-label={ariaLabel ?? 'Add tag'}
58
- onkeydown={onKeydown}
59
- onblur={() => add(draft)}
48
+ {...ti.inputProps()}
60
49
  />
61
50
  {#if name}<input type="hidden" {name} value={value.join(',')} />{/if}
62
51
  </div>
@@ -1,8 +1,3 @@
1
- /**
2
- * SvTagsInput - editable token/chips input. Type + Enter/comma to add,
3
- * Backspace to remove the last, click × to remove a chip. Parity: Smart
4
- * `smart-tags` / `smart-input` token mode. Emits string[].
5
- */
6
1
  type Props = {
7
2
  value?: string[];
8
3
  onChange?: (tags: string[]) => void;
@@ -1,20 +1,24 @@
1
1
  <script lang="ts" module>
2
- export type TimeValue = Date | string | number | null
2
+ export type { TimeValue } from './createTimePicker.svelte'
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
6
  /**
7
7
  * SvTimePicker - an analog clock-dial time picker (parity: Smart
8
8
  * `smart-time-picker`). 12/24-hour, minute snapping, hour->minute auto-switch,
9
- * pointer-drag + keyboard. Works standalone and as the SvGrid `time` editor.
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.
10
12
  * All visuals come from `--sg-*` tokens so every theme applies for free.
11
13
  */
14
+ import { createTimePicker, type TimeValue, type TimeFormat, type TimeSelection } from './createTimePicker.svelte'
15
+
12
16
  type Props = {
13
17
  /** Date, "HH:MM[:SS]" string, or epoch ms. */
14
18
  value?: TimeValue
15
19
  /** Fires with a Date (today's date carrying the picked time). */
16
20
  onChange?: (value: Date) => void
17
- format?: '12-hour' | '24-hour'
21
+ format?: TimeFormat
18
22
  minuteInterval?: number
19
23
  /** After picking an hour, jump the dial to minutes. Default true. */
20
24
  autoSwitchToMinutes?: boolean
@@ -24,7 +28,7 @@
24
28
  readonly?: boolean
25
29
  name?: string
26
30
  /** Which dial opens first. */
27
- selection?: 'hour' | 'minute'
31
+ selection?: TimeSelection
28
32
  }
29
33
 
30
34
  let {
@@ -40,224 +44,68 @@
40
44
  selection = 'hour',
41
45
  }: Props = $props()
42
46
 
43
- const is12 = $derived(format === '12-hour')
44
- const isInteractive = $derived(!disabled && !readonly)
45
-
46
- function parseValue(v: TimeValue): { h: number; m: number } {
47
- if (v == null) { const n = new Date(); return { h: n.getHours(), m: n.getMinutes() } }
48
- if (typeof v === 'string') {
49
- const mt = v.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/)
50
- if (mt) return { h: Math.min(23, +mt[1]!), m: Math.min(59, +mt[2]!) }
51
- const d = new Date(v)
52
- if (!isNaN(d.getTime())) return { h: d.getHours(), m: d.getMinutes() }
53
- const n = new Date(); return { h: n.getHours(), m: n.getMinutes() }
54
- }
55
- const d = new Date(v)
56
- return { h: d.getHours(), m: d.getMinutes() }
57
- }
58
-
59
- let hours = $state(0)
60
- let minutes = $state(0)
61
- let sel = $state<'hour' | 'minute'>('hour')
62
- let seeded = false
63
- let lastKey = '￿'
64
- $effect(() => {
65
- const { h, m } = parseValue(value)
66
- const key = `${h}:${m}`
67
- if (key !== lastKey) {
68
- lastKey = key
69
- hours = h
70
- minutes = m
71
- if (!seeded) { seeded = true; sel = selection }
72
- }
73
- })
74
-
75
- const displayHour = $derived(is12 ? (hours % 12 === 0 ? 12 : hours % 12) : hours)
76
- const isPm = $derived(hours >= 12)
77
- const mm = $derived(String(minutes).padStart(2, '0'))
78
-
79
- // --- Dial geometry ---
80
- const SIZE = 240
81
- const C = SIZE / 2
82
- const OUTER = C - 22
83
- const INNER = C - 58
84
-
85
- function pointAt(angleDeg: number, r: number) {
86
- const rad = ((angleDeg - 90) * Math.PI) / 180
87
- return { x: C + r * Math.cos(rad), y: C + r * Math.sin(rad) }
88
- }
89
-
90
- type Tick = { label: string; x: number; y: number; value: number; ring: 'outer' | 'inner' }
91
-
92
- const hourTicks = $derived.by<Tick[]>(() => {
93
- const ticks: Tick[] = []
94
- if (is12) {
95
- for (let i = 0; i < 12; i++) {
96
- const h = i === 0 ? 12 : i
97
- const p = pointAt(i * 30, OUTER)
98
- ticks.push({ label: String(h), x: p.x, y: p.y, value: h, ring: 'outer' })
99
- }
100
- } else {
101
- for (let i = 0; i < 12; i++) {
102
- const outer = pointAt(i * 30, OUTER) // 12..23
103
- const inner = pointAt(i * 30, INNER) // 00..11
104
- const ho = i + 12
105
- const hi = i
106
- ticks.push({ label: String(ho).padStart(2, '0'), x: outer.x, y: outer.y, value: ho, ring: 'outer' })
107
- ticks.push({ label: hi === 0 ? '00' : String(hi).padStart(2, '0'), x: inner.x, y: inner.y, value: hi, ring: 'inner' })
108
- }
109
- }
110
- return ticks
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,
111
58
  })
112
59
 
113
- const minuteTicks = $derived.by<Tick[]>(() =>
114
- Array.from({ length: 12 }, (_, i) => {
115
- const v = i * 5
116
- const p = pointAt(i * 30, OUTER)
117
- return { label: String(v).padStart(2, '0'), x: p.x, y: p.y, value: v, ring: 'outer' as const }
118
- }),
119
- )
120
-
121
- // Hand endpoint for the current selection.
122
- const handAngle = $derived(
123
- sel === 'hour'
124
- ? (is12 ? (displayHour % 12) * 30 : (hours % 12) * 30)
125
- : minutes * 6,
126
- )
127
- const handRadius = $derived(sel === 'hour' && !is12 && hours % 12 === hours && hours < 12 ? INNER : OUTER)
128
- const handEnd = $derived(pointAt(handAngle, handRadius))
129
-
130
- // --- Emit ---
131
- function emit() {
132
- const d = new Date()
133
- d.setHours(hours, minutes, 0, 0)
134
- onChange?.(d)
135
- }
136
-
137
- function setHourFromDisplay(h12OrH: number, ring: 'outer' | 'inner') {
138
- if (is12) {
139
- // Preserve AM/PM.
140
- const base = h12OrH % 12
141
- hours = isPm ? base + 12 : base
142
- } else {
143
- hours = ring === 'inner' ? h12OrH : h12OrH
144
- }
145
- lastKey = `${hours}:${minutes}`
146
- emit()
147
- if (autoSwitchToMinutes) sel = 'minute'
148
- }
149
-
150
- function snap(m: number) {
151
- const step = Math.max(1, Math.floor(minuteInterval))
152
- return (Math.round(m / step) * step) % 60
153
- }
154
-
155
- function setMinute(m: number) {
156
- minutes = snap(m)
157
- lastKey = `${hours}:${minutes}`
158
- emit()
159
- }
160
-
161
- function setAmPm(pm: boolean) {
162
- if (pm === isPm) return
163
- hours = pm ? (hours % 12) + 12 : hours % 12
164
- lastKey = `${hours}:${minutes}`
165
- emit()
166
- }
167
-
168
- // --- Pointer drag on the dial ---
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.
169
63
  let dialEl: SVGSVGElement | null = null
170
64
  let dragging = false
171
65
 
172
66
  function angleFromEvent(e: PointerEvent): { angle: number; dist: number } {
173
67
  const rect = dialEl!.getBoundingClientRect()
174
- const scale = SIZE / rect.width
175
- const x = (e.clientX - rect.left) * scale - C
176
- const y = (e.clientY - rect.top) * scale - C
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
177
71
  let deg = (Math.atan2(y, x) * 180) / Math.PI + 90
178
72
  if (deg < 0) deg += 360
179
73
  return { angle: deg, dist: Math.hypot(x, y) }
180
74
  }
181
75
 
182
- function applyPointer(e: PointerEvent) {
183
- if (!isInteractive) return
184
- const { angle, dist } = angleFromEvent(e)
185
- if (sel === 'hour') {
186
- const idx = Math.round(angle / 30) % 12
187
- if (is12) {
188
- const h = idx === 0 ? 12 : idx
189
- const base = h % 12
190
- hours = isPm ? base + 12 : base
191
- } else {
192
- const inner = dist < (OUTER + INNER) / 2
193
- hours = inner ? idx : idx + 12
194
- }
195
- lastKey = `${hours}:${minutes}`
196
- emit()
197
- } else {
198
- const m = Math.round(angle / 6) % 60
199
- setMinute(m)
200
- }
201
- }
202
-
203
76
  function onDialPointerDown(e: PointerEvent) {
204
- if (!isInteractive) return
77
+ if (!tp.isInteractive) return
205
78
  dragging = true
206
79
  dialEl?.setPointerCapture(e.pointerId)
207
- applyPointer(e)
80
+ const { angle, dist } = angleFromEvent(e)
81
+ tp.pointerSelect(angle, dist)
208
82
  }
209
83
  function onDialPointerMove(e: PointerEvent) {
210
- if (dragging) applyPointer(e)
84
+ if (!dragging) return
85
+ const { angle, dist } = angleFromEvent(e)
86
+ tp.pointerSelect(angle, dist)
211
87
  }
212
88
  function onDialPointerUp(e: PointerEvent) {
213
89
  if (!dragging) return
214
90
  dragging = false
215
91
  dialEl?.releasePointerCapture(e.pointerId)
216
- if (sel === 'hour' && autoSwitchToMinutes) sel = 'minute'
217
- }
218
-
219
- function onKeydown(e: KeyboardEvent) {
220
- if (!isInteractive) return
221
- const step = sel === 'minute' ? Math.max(1, Math.floor(minuteInterval)) : 1
222
- if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
223
- e.preventDefault()
224
- if (sel === 'hour') { hours = (hours + 1) % 24 } else { minutes = (minutes + step) % 60 }
225
- lastKey = `${hours}:${minutes}`; emit()
226
- } else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
227
- e.preventDefault()
228
- if (sel === 'hour') { hours = (hours + 23) % 24 } else { minutes = (minutes - step + 60) % 60 }
229
- lastKey = `${hours}:${minutes}`; emit()
230
- } else if (e.key === 'Tab' && !e.shiftKey && sel === 'hour') {
231
- sel = 'minute'
232
- }
233
- }
234
-
235
- function now() {
236
- const n = new Date()
237
- hours = n.getHours(); minutes = n.getMinutes()
238
- lastKey = `${hours}:${minutes}`
239
- emit()
240
- }
241
-
242
- function isActiveHour(t: Tick): boolean {
243
- if (is12) return t.value === displayHour
244
- return t.value === hours
92
+ tp.endPointer()
245
93
  }
246
94
  </script>
247
95
 
248
96
  <div class="sv-tp" class:sv-tp--disabled={disabled} role="group" aria-label="Time picker">
249
97
  <div class="sv-tp__head">
250
- <button type="button" class="sv-tp__seg" class:is-active={sel === 'hour'} onclick={() => (sel = 'hour')} disabled={disabled}>
251
- {String(displayHour).padStart(2, '0')}
98
+ <button class="sv-tp__seg" class:is-active={tp.selection === 'hour'} {...tp.segProps('hour')}>
99
+ {String(tp.displayHour).padStart(2, '0')}
252
100
  </button>
253
101
  <span class="sv-tp__colon">:</span>
254
- <button type="button" class="sv-tp__seg" class:is-active={sel === 'minute'} onclick={() => (sel = 'minute')} disabled={disabled}>
255
- {mm}
102
+ <button class="sv-tp__seg" class:is-active={tp.selection === 'minute'} {...tp.segProps('minute')}>
103
+ {tp.mm}
256
104
  </button>
257
- {#if is12}
105
+ {#if tp.is12}
258
106
  <div class="sv-tp__ampm">
259
- <button type="button" class:is-active={!isPm} onclick={() => setAmPm(false)} disabled={disabled}>AM</button>
260
- <button type="button" class:is-active={isPm} onclick={() => setAmPm(true)} disabled={disabled}>PM</button>
107
+ <button class:is-active={!tp.isPm} {...tp.ampmProps(false)}>AM</button>
108
+ <button class:is-active={tp.isPm} {...tp.ampmProps(true)}>PM</button>
261
109
  </div>
262
110
  {/if}
263
111
  </div>
@@ -265,43 +113,37 @@
265
113
  <svg
266
114
  bind:this={dialEl}
267
115
  class="sv-tp__dial"
268
- viewBox={`0 0 ${SIZE} ${SIZE}`}
116
+ viewBox={`0 0 ${tp.SIZE} ${tp.SIZE}`}
269
117
  width="100%"
270
- role="slider"
271
- tabindex={disabled ? -1 : 0}
272
- aria-label={sel === 'hour' ? 'Hour' : 'Minute'}
273
- aria-valuenow={sel === 'hour' ? hours : minutes}
274
- aria-valuemin="0"
275
- aria-valuemax={sel === 'hour' ? 23 : 59}
118
+ {...tp.dialProps()}
276
119
  onpointerdown={onDialPointerDown}
277
120
  onpointermove={onDialPointerMove}
278
121
  onpointerup={onDialPointerUp}
279
122
  onpointercancel={onDialPointerUp}
280
- onkeydown={onKeydown}
281
123
  >
282
- <circle class="sv-tp__face" cx={C} cy={C} r={C - 4} />
124
+ <circle class="sv-tp__face" cx={tp.C} cy={tp.C} r={tp.C - 4} />
283
125
  <!-- hand -->
284
- <line class="sv-tp__hand" x1={C} y1={C} x2={handEnd.x} y2={handEnd.y} />
285
- <circle class="sv-tp__hub" cx={C} cy={C} r="3" />
286
- <circle class="sv-tp__knob" cx={handEnd.x} cy={handEnd.y} r="15" />
287
- {#if sel === 'hour'}
288
- {#each hourTicks as t (t.ring + '-' + t.value)}
289
- <text class="sv-tp__num" class:is-active={isActiveHour(t)} class:is-inner={t.ring === 'inner'} x={t.x} y={t.y} dominant-baseline="central" text-anchor="middle">{t.label}</text>
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>
290
132
  {/each}
291
133
  {:else}
292
- {#each minuteTicks as t (t.value)}
293
- <text class="sv-tp__num" class:is-active={t.value === minutes} x={t.x} y={t.y} dominant-baseline="central" text-anchor="middle">{t.label}</text>
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>
294
136
  {/each}
295
137
  {/if}
296
138
  </svg>
297
139
 
298
140
  {#if footer}
299
141
  <div class="sv-tp__footer">
300
- <button type="button" class="sv-tp__now" onclick={now} disabled={disabled}>Now</button>
142
+ <button class="sv-tp__now" {...tp.nowProps()}>Now</button>
301
143
  </div>
302
144
  {/if}
303
145
 
304
- {#if name}<input type="hidden" {name} value={`${String(hours).padStart(2, '0')}:${mm}`} />{/if}
146
+ {#if name}<input type="hidden" {name} value={`${String(tp.hours).padStart(2, '0')}:${tp.mm}`} />{/if}
305
147
  </div>
306
148
 
307
149
  <style>
@@ -1,16 +1,19 @@
1
- export type TimeValue = Date | string | number | null;
1
+ export type { TimeValue } from './createTimePicker.svelte';
2
2
  /**
3
- * SvTimePicker - an analog clock-dial time picker (parity: Smart
4
- * `smart-time-picker`). 12/24-hour, minute snapping, hour->minute auto-switch,
5
- * pointer-drag + keyboard. Works standalone and as the SvGrid `time` editor.
6
- * All visuals come from `--sg-*` tokens so every theme applies for free.
7
- */
3
+ * SvTimePicker - an analog clock-dial time picker (parity: Smart
4
+ * `smart-time-picker`). 12/24-hour, minute snapping, hour->minute auto-switch,
5
+ * pointer-drag + keyboard. It is a styled renderer over the headless
6
+ * `createTimePicker` core: the core owns the value math, dial geometry, keyboard
7
+ * and ARIA; this component keeps the DOM-bound pointer capture + hit-testing.
8
+ * All visuals come from `--sg-*` tokens so every theme applies for free.
9
+ */
10
+ import { type TimeValue, type TimeFormat, type TimeSelection } from './createTimePicker.svelte';
8
11
  type Props = {
9
12
  /** Date, "HH:MM[:SS]" string, or epoch ms. */
10
13
  value?: TimeValue;
11
14
  /** Fires with a Date (today's date carrying the picked time). */
12
15
  onChange?: (value: Date) => void;
13
- format?: '12-hour' | '24-hour';
16
+ format?: TimeFormat;
14
17
  minuteInterval?: number;
15
18
  /** After picking an hour, jump the dial to minutes. Default true. */
16
19
  autoSwitchToMinutes?: boolean;
@@ -20,7 +23,7 @@ type Props = {
20
23
  readonly?: boolean;
21
24
  name?: string;
22
25
  /** Which dial opens first. */
23
- selection?: 'hour' | 'minute';
26
+ selection?: TimeSelection;
24
27
  };
25
28
  declare const SvTimePicker: import("svelte").Component<Props, {}, "">;
26
29
  type SvTimePicker = ReturnType<typeof SvTimePicker>;
@@ -4,6 +4,7 @@
4
4
  * Parity: Smart `smart-toggle-button`. Controlled via `pressed` + `onChange`.
5
5
  */
6
6
  import type { Snippet } from 'svelte'
7
+ import { createToggle } from './createToggle.svelte'
7
8
 
8
9
  type Props = {
9
10
  pressed?: boolean
@@ -16,20 +17,19 @@
16
17
 
17
18
  let { pressed = false, onChange, disabled = false, size = 'md', ariaLabel, children }: Props = $props()
18
19
 
19
- function toggle() {
20
- if (disabled) return
21
- onChange?.(!pressed)
22
- }
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
+ })
23
27
  </script>
24
28
 
25
29
  <button
26
- type="button"
27
30
  class="sv-toggle sv-toggle--{size}"
28
31
  class:is-pressed={pressed}
29
- aria-pressed={pressed}
30
- aria-label={ariaLabel}
31
- {disabled}
32
- onclick={toggle}
32
+ {...t.buttonProps()}
33
33
  >
34
34
  {#if children}{@render children()}{/if}
35
35
  </button>