@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.
- package/dist/SvAutoComplete.svelte +23 -34
- package/dist/SvCalendar.svelte +74 -334
- package/dist/SvCalendar.svelte.d.ts +5 -23
- package/dist/SvCheckBox.svelte +11 -10
- package/dist/SvColorInput.svelte +31 -58
- package/dist/SvComboBox.svelte +32 -54
- package/dist/SvCountryInput.svelte +31 -29
- package/dist/SvDateTimePicker.svelte +53 -110
- package/dist/SvDateTimePicker.svelte.d.ts +4 -3
- package/dist/SvDropDownList.svelte +31 -54
- package/dist/SvGauge.svelte +21 -23
- package/dist/SvGauge.svelte.d.ts +0 -5
- package/dist/SvListBox.svelte +25 -58
- package/dist/SvListBox.svelte.d.ts +3 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +2 -6
- package/dist/SvPasswordInput.svelte +18 -28
- package/dist/SvPasswordInput.svelte.d.ts +0 -4
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvRadioGroup.svelte +14 -37
- package/dist/SvRadioGroup.svelte.d.ts +6 -9
- package/dist/SvRating.svelte +22 -42
- package/dist/SvRating.svelte.d.ts +3 -5
- package/dist/SvSlider.svelte +30 -57
- package/dist/SvSlider.svelte.d.ts +0 -5
- package/dist/SvSwitchButton.svelte +10 -17
- package/dist/SvSwitchButton.svelte.d.ts +0 -5
- package/dist/SvTabs.svelte +26 -39
- package/dist/SvTabs.svelte.d.ts +5 -5
- package/dist/SvTagsInput.svelte +17 -28
- package/dist/SvTagsInput.svelte.d.ts +0 -5
- package/dist/SvTimePicker.svelte +52 -210
- package/dist/SvTimePicker.svelte.d.ts +11 -8
- package/dist/SvToggleButton.svelte +9 -9
- package/dist/SvTree.svelte +30 -109
- package/dist/SvTree.svelte.d.ts +9 -11
- package/dist/cdn/svgrid.js +7488 -5839
- package/dist/cdn/svgrid.svelte-external.js +4691 -3028
- package/dist/createAutocomplete.svelte.d.ts +77 -0
- package/dist/createAutocomplete.svelte.js +100 -0
- package/dist/createCalendar.svelte.d.ts +192 -0
- package/dist/createCalendar.svelte.js +402 -0
- package/dist/createCheckbox.svelte.d.ts +45 -0
- package/dist/createCheckbox.svelte.js +26 -0
- package/dist/createColorInput.svelte.d.ts +67 -0
- package/dist/createColorInput.svelte.js +100 -0
- package/dist/createCombobox.svelte.d.ts +102 -0
- package/dist/createCombobox.svelte.js +171 -0
- package/dist/createCountryInput.svelte.d.ts +86 -0
- package/dist/createCountryInput.svelte.js +126 -0
- package/dist/createDateTimePicker.svelte.d.ts +83 -0
- package/dist/createDateTimePicker.svelte.js +202 -0
- package/dist/createDropdownList.svelte.d.ts +78 -0
- package/dist/createDropdownList.svelte.js +117 -0
- package/dist/createGauge.svelte.d.ts +56 -0
- package/dist/createGauge.svelte.js +49 -0
- package/dist/createListbox.svelte.d.ts +79 -0
- package/dist/createListbox.svelte.js +117 -0
- package/dist/createMaskedInput.svelte.d.ts +30 -0
- package/dist/createMaskedInput.svelte.js +62 -0
- package/dist/createNumberInput.svelte.d.ts +72 -0
- package/dist/createNumberInput.svelte.js +167 -0
- package/dist/createPasswordInput.svelte.d.ts +60 -0
- package/dist/createPasswordInput.svelte.js +72 -0
- package/dist/createPhoneInput.svelte.d.ts +46 -0
- package/dist/createPhoneInput.svelte.js +105 -0
- package/dist/createRadioGroup.svelte.d.ts +64 -0
- package/dist/createRadioGroup.svelte.js +73 -0
- package/dist/createRating.svelte.d.ts +70 -0
- package/dist/createRating.svelte.js +85 -0
- package/dist/createSlider.svelte.d.ts +71 -0
- package/dist/createSlider.svelte.js +123 -0
- package/dist/createSwitch.svelte.d.ts +43 -0
- package/dist/createSwitch.svelte.js +41 -0
- package/dist/createTabs.svelte.d.ts +78 -0
- package/dist/createTabs.svelte.js +92 -0
- package/dist/createTagsInput.svelte.d.ts +65 -0
- package/dist/createTagsInput.svelte.js +93 -0
- package/dist/createTimePicker.svelte.d.ts +106 -0
- package/dist/createTimePicker.svelte.js +248 -0
- package/dist/createToggle.svelte.d.ts +43 -0
- package/dist/createToggle.svelte.js +39 -0
- package/dist/createTree.svelte.d.ts +96 -0
- package/dist/createTree.svelte.js +194 -0
- package/dist/editor-contract.d.ts +48 -0
- package/dist/editor-contract.js +26 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +31 -0
- package/package.json +1 -1
- package/src/SvAutoComplete.svelte +23 -34
- package/src/SvCalendar.svelte +74 -334
- package/src/SvCheckBox.svelte +11 -10
- package/src/SvColorInput.svelte +31 -58
- package/src/SvComboBox.svelte +32 -54
- package/src/SvCountryInput.svelte +31 -29
- package/src/SvDateTimePicker.svelte +53 -110
- package/src/SvDropDownList.svelte +31 -54
- package/src/SvGauge.svelte +21 -23
- package/src/SvListBox.svelte +25 -58
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +18 -28
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +14 -37
- package/src/SvRating.svelte +22 -42
- package/src/SvSlider.svelte +30 -57
- package/src/SvSwitchButton.svelte +10 -17
- package/src/SvTabs.svelte +26 -39
- package/src/SvTagsInput.svelte +17 -28
- package/src/SvTimePicker.svelte +52 -210
- package/src/SvToggleButton.svelte +9 -9
- package/src/SvTree.svelte +30 -109
- package/src/createAutocomplete.svelte.ts +104 -0
- package/src/createCalendar.svelte.ts +488 -0
- package/src/createCheckbox.svelte.ts +74 -0
- package/src/createColorInput.svelte.ts +115 -0
- package/src/createCombobox.svelte.ts +158 -0
- package/src/createCountryInput.svelte.ts +126 -0
- package/src/createDateTimePicker.svelte.ts +216 -0
- package/src/createDropdownList.svelte.ts +125 -0
- package/src/createGauge.svelte.ts +86 -0
- package/src/createListbox.svelte.ts +186 -0
- package/src/createMaskedInput.svelte.ts +81 -0
- package/src/createNumberInput.svelte.ts +193 -0
- package/src/createPasswordInput.svelte.ts +87 -0
- package/src/createPhoneInput.svelte.ts +119 -0
- package/src/createRadioGroup.svelte.ts +132 -0
- package/src/createRating.svelte.ts +145 -0
- package/src/createSlider.svelte.ts +160 -0
- package/src/createSwitch.svelte.ts +78 -0
- package/src/createTabs.svelte.ts +129 -0
- package/src/createTagsInput.svelte.ts +98 -0
- package/src/createTimePicker.svelte.ts +289 -0
- package/src/createToggle.svelte.ts +69 -0
- package/src/createTree.svelte.ts +216 -0
- package/src/editor-contract.test.ts +32 -0
- package/src/editor-contract.ts +60 -0
- package/src/index.ts +39 -0
- package/src/ui-inputs.test.ts +26 -0
- package/src/ui-selection.test.ts +13 -0
package/dist/SvTagsInput.svelte
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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}
|
|
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
|
|
41
|
+
<button class="sv-tags__x" {...ti.removeProps(i)}>×</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
|
-
{
|
|
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;
|
package/dist/SvTimePicker.svelte
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
export type TimeValue
|
|
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.
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
80
|
+
const { angle, dist } = angleFromEvent(e)
|
|
81
|
+
tp.pointerSelect(angle, dist)
|
|
208
82
|
}
|
|
209
83
|
function onDialPointerMove(e: PointerEvent) {
|
|
210
|
-
if (dragging)
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
260
|
-
<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
|
-
|
|
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
|
|
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
|
|
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
|
|
1
|
+
export type { TimeValue } from './createTimePicker.svelte';
|
|
2
2
|
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
aria-label={ariaLabel}
|
|
31
|
-
{disabled}
|
|
32
|
-
onclick={toggle}
|
|
32
|
+
{...t.buttonProps()}
|
|
33
33
|
>
|
|
34
34
|
{#if children}{@render children()}{/if}
|
|
35
35
|
</button>
|