@svgrid/grid 1.2.21 → 1.2.23
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/GridFooter.svelte +34 -20
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/SvAutoComplete.svelte +100 -0
- package/dist/SvAutoComplete.svelte.d.ts +16 -0
- package/dist/SvButton.svelte +97 -0
- package/dist/SvButton.svelte.d.ts +23 -0
- package/dist/SvCalendar.svelte +669 -0
- package/dist/SvCalendar.svelte.d.ts +70 -0
- package/dist/SvCheckBox.svelte +81 -0
- package/dist/SvCheckBox.svelte.d.ts +19 -0
- package/dist/SvColorInput.svelte +187 -0
- package/dist/SvColorInput.svelte.d.ts +15 -0
- package/dist/SvComboBox.svelte +130 -0
- package/dist/SvComboBox.svelte.d.ts +15 -0
- package/dist/SvCountryInput.svelte +123 -0
- package/dist/SvCountryInput.svelte.d.ts +14 -0
- package/dist/SvDateTimePicker.svelte +322 -0
- package/dist/SvDateTimePicker.svelte.d.ts +37 -0
- package/dist/SvDropDownList.svelte +143 -0
- package/dist/SvDropDownList.svelte.d.ts +15 -0
- package/dist/SvForm.svelte +147 -0
- package/dist/SvForm.svelte.d.ts +29 -0
- package/dist/SvGauge.svelte +99 -0
- package/dist/SvGauge.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +16 -2
- package/dist/SvGrid.controller.svelte.js +63 -8
- package/dist/SvGrid.css +84 -31
- package/dist/SvGrid.helpers.d.ts +4 -0
- package/dist/SvGrid.helpers.js +40 -12
- package/dist/SvGrid.svelte +84 -5
- package/dist/SvGrid.types.d.ts +36 -0
- package/dist/SvGridDropdown.svelte +16 -30
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvListBox.svelte +122 -0
- package/dist/SvListBox.svelte.d.ts +19 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvMaskedInput.svelte.d.ts +15 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +22 -0
- package/dist/SvPasswordInput.svelte +110 -0
- package/dist/SvPasswordInput.svelte.d.ts +22 -0
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvPhoneInput.svelte.d.ts +19 -0
- package/dist/SvRadioGroup.svelte +108 -0
- package/dist/SvRadioGroup.svelte.d.ts +23 -0
- package/dist/SvRating.svelte +111 -0
- package/dist/SvRating.svelte.d.ts +20 -0
- package/dist/SvRepeatButton.svelte +89 -0
- package/dist/SvRepeatButton.svelte.d.ts +21 -0
- package/dist/SvSlider.svelte +182 -0
- package/dist/SvSlider.svelte.d.ts +25 -0
- package/dist/SvSwitchButton.svelte +94 -0
- package/dist/SvSwitchButton.svelte.d.ts +19 -0
- package/dist/SvTabs.svelte +122 -0
- package/dist/SvTabs.svelte.d.ts +26 -0
- package/dist/SvTagsInput.svelte +81 -0
- package/dist/SvTagsInput.svelte.d.ts +19 -0
- package/dist/SvTimePicker.svelte +359 -0
- package/dist/SvTimePicker.svelte.d.ts +27 -0
- package/dist/SvToggleButton.svelte +57 -0
- package/dist/SvToggleButton.svelte.d.ts +16 -0
- package/dist/SvTree.svelte +210 -0
- package/dist/SvTree.svelte.d.ts +29 -0
- package/dist/build-api.js +78 -1
- package/dist/cdn/svgrid.js +10022 -5904
- package/dist/cdn/svgrid.svelte-external.js +8531 -4429
- package/dist/cell-render.d.ts +4 -0
- package/dist/cell-render.js +35 -1
- package/dist/clipboard.js +19 -1
- package/dist/collaboration.d.ts +2 -0
- package/dist/collaboration.js +4 -0
- package/dist/core.d.ts +22 -1
- package/dist/countries.d.ts +15 -0
- package/dist/countries.js +61 -0
- package/dist/datetime/date-core.d.ts +88 -0
- package/dist/datetime/date-core.js +165 -0
- package/dist/datetime/date-format.d.ts +39 -0
- package/dist/datetime/date-format.js +371 -0
- package/dist/datetime/date-restrict.d.ts +23 -0
- package/dist/datetime/date-restrict.js +50 -0
- package/dist/datetime/date-selection.d.ts +25 -0
- package/dist/datetime/date-selection.js +94 -0
- package/dist/datetime/mask.d.ts +21 -0
- package/dist/datetime/mask.js +90 -0
- package/dist/editing.d.ts +1 -1
- package/dist/editing.js +7 -0
- package/dist/editors/cell-editors.d.ts +2 -2
- package/dist/editors/cell-editors.js +3 -1
- package/dist/export-format.d.ts +178 -0
- package/dist/export-format.js +419 -0
- package/dist/hyperformula-adapter.d.ts +4 -0
- package/dist/hyperformula-adapter.js +15 -6
- package/dist/index.d.ts +36 -0
- package/dist/index.js +41 -0
- package/dist/list-option.d.ts +10 -0
- package/dist/list-option.js +7 -0
- package/dist/menus.js +18 -2
- package/dist/popover.d.ts +48 -0
- package/dist/popover.js +65 -0
- package/dist/row-resize.d.ts +4 -4
- package/dist/row-resize.js +8 -5
- package/dist/selection.d.ts +6 -0
- package/dist/selection.js +31 -5
- package/dist/server-data-source.d.ts +43 -0
- package/dist/server-data-source.js +100 -0
- package/dist/sparkline.js +8 -4
- package/dist/svgrid-wrapper.types.d.ts +26 -1
- package/package.json +6 -1
- package/src/GridFooter.svelte +34 -20
- package/src/SvAutoComplete.svelte +100 -0
- package/src/SvButton.svelte +97 -0
- package/src/SvCalendar.svelte +669 -0
- package/src/SvCalendar.test.ts +211 -0
- package/src/SvCheckBox.svelte +81 -0
- package/src/SvColorInput.svelte +187 -0
- package/src/SvComboBox.svelte +130 -0
- package/src/SvCountryInput.svelte +123 -0
- package/src/SvDateTimePicker.svelte +322 -0
- package/src/SvDateTimePicker.test.ts +136 -0
- package/src/SvDropDownList.svelte +143 -0
- package/src/SvForm.svelte +147 -0
- package/src/SvGauge.svelte +99 -0
- package/src/SvGrid.controller.svelte.ts +74 -8
- package/src/SvGrid.css +84 -31
- package/src/SvGrid.helpers.ts +41 -12
- package/src/SvGrid.svelte +84 -5
- package/src/SvGrid.types.ts +33 -0
- package/src/SvGridDropdown.svelte +16 -30
- package/src/SvListBox.svelte +122 -0
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +110 -0
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +108 -0
- package/src/SvRating.svelte +111 -0
- package/src/SvRepeatButton.svelte +89 -0
- package/src/SvSlider.svelte +182 -0
- package/src/SvSwitchButton.svelte +94 -0
- package/src/SvTabs.svelte +122 -0
- package/src/SvTagsInput.svelte +81 -0
- package/src/SvTimePicker.svelte +359 -0
- package/src/SvTimePicker.test.ts +98 -0
- package/src/SvToggleButton.svelte +57 -0
- package/src/SvTree.svelte +210 -0
- package/src/build-api.ts +115 -0
- package/src/cell-render.test.ts +38 -0
- package/src/cell-render.ts +43 -0
- package/src/clipboard.test.ts +18 -0
- package/src/clipboard.ts +24 -1
- package/src/collaboration.test.ts +30 -0
- package/src/collaboration.ts +6 -0
- package/src/core.ts +27 -3
- package/src/countries.ts +71 -0
- package/src/datetime/date-core.test.ts +217 -0
- package/src/datetime/date-core.ts +204 -0
- package/src/datetime/date-format.test.ts +121 -0
- package/src/datetime/date-format.ts +317 -0
- package/src/datetime/date-restrict.ts +60 -0
- package/src/datetime/date-selection.test.ts +129 -0
- package/src/datetime/date-selection.ts +137 -0
- package/src/datetime/mask.test.ts +36 -0
- package/src/datetime/mask.ts +84 -0
- package/src/editing.test.ts +22 -0
- package/src/editing.ts +7 -1
- package/src/editors/cell-editors.ts +7 -1
- package/src/export-data-api.test.ts +126 -0
- package/src/export-format.test.ts +107 -0
- package/src/export-format.ts +598 -0
- package/src/hyperformula-adapter.test.ts +35 -1
- package/src/hyperformula-adapter.ts +18 -6
- package/src/index.ts +71 -0
- package/src/list-option.ts +15 -0
- package/src/menus.ts +21 -2
- package/src/popover.ts +79 -0
- package/src/row-resize.test.ts +25 -0
- package/src/row-resize.ts +8 -5
- package/src/selection.test.ts +36 -4
- package/src/selection.ts +30 -9
- package/src/server-data-source.test.ts +201 -0
- package/src/server-data-source.ts +148 -0
- package/src/sparkline.test.ts +9 -0
- package/src/sparkline.ts +9 -4
- package/src/svgrid-wrapper.types.ts +28 -1
- package/src/svgrid.behavior.test.ts +27 -0
- package/src/svgrid.new-features.wrapper.test.ts +27 -1
- package/src/ui-buttons.test.ts +105 -0
- package/src/ui-composite.test.ts +89 -0
- package/src/ui-inputs.test.ts +92 -0
- package/src/ui-range.test.ts +61 -0
- package/src/ui-selection.test.ts +106 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type TimeValue = Date | string | number | null
|
|
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. Works standalone and as the SvGrid `time` editor.
|
|
10
|
+
* All visuals come from `--sg-*` tokens so every theme applies for free.
|
|
11
|
+
*/
|
|
12
|
+
type Props = {
|
|
13
|
+
/** Date, "HH:MM[:SS]" string, or epoch ms. */
|
|
14
|
+
value?: TimeValue
|
|
15
|
+
/** Fires with a Date (today's date carrying the picked time). */
|
|
16
|
+
onChange?: (value: Date) => void
|
|
17
|
+
format?: '12-hour' | '24-hour'
|
|
18
|
+
minuteInterval?: number
|
|
19
|
+
/** After picking an hour, jump the dial to minutes. Default true. */
|
|
20
|
+
autoSwitchToMinutes?: boolean
|
|
21
|
+
/** Show the Now footer button. */
|
|
22
|
+
footer?: boolean
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
readonly?: boolean
|
|
25
|
+
name?: string
|
|
26
|
+
/** Which dial opens first. */
|
|
27
|
+
selection?: 'hour' | 'minute'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
value = null,
|
|
32
|
+
onChange,
|
|
33
|
+
format = '24-hour',
|
|
34
|
+
minuteInterval = 1,
|
|
35
|
+
autoSwitchToMinutes = true,
|
|
36
|
+
footer = false,
|
|
37
|
+
disabled = false,
|
|
38
|
+
readonly = false,
|
|
39
|
+
name,
|
|
40
|
+
selection = 'hour',
|
|
41
|
+
}: Props = $props()
|
|
42
|
+
|
|
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
|
|
111
|
+
})
|
|
112
|
+
|
|
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 ---
|
|
169
|
+
let dialEl: SVGSVGElement | null = null
|
|
170
|
+
let dragging = false
|
|
171
|
+
|
|
172
|
+
function angleFromEvent(e: PointerEvent): { angle: number; dist: number } {
|
|
173
|
+
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
|
|
177
|
+
let deg = (Math.atan2(y, x) * 180) / Math.PI + 90
|
|
178
|
+
if (deg < 0) deg += 360
|
|
179
|
+
return { angle: deg, dist: Math.hypot(x, y) }
|
|
180
|
+
}
|
|
181
|
+
|
|
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
|
+
function onDialPointerDown(e: PointerEvent) {
|
|
204
|
+
if (!isInteractive) return
|
|
205
|
+
dragging = true
|
|
206
|
+
dialEl?.setPointerCapture(e.pointerId)
|
|
207
|
+
applyPointer(e)
|
|
208
|
+
}
|
|
209
|
+
function onDialPointerMove(e: PointerEvent) {
|
|
210
|
+
if (dragging) applyPointer(e)
|
|
211
|
+
}
|
|
212
|
+
function onDialPointerUp(e: PointerEvent) {
|
|
213
|
+
if (!dragging) return
|
|
214
|
+
dragging = false
|
|
215
|
+
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
|
|
245
|
+
}
|
|
246
|
+
</script>
|
|
247
|
+
|
|
248
|
+
<div class="sv-tp" class:sv-tp--disabled={disabled} role="group" aria-label="Time picker">
|
|
249
|
+
<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')}
|
|
252
|
+
</button>
|
|
253
|
+
<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}
|
|
256
|
+
</button>
|
|
257
|
+
{#if is12}
|
|
258
|
+
<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>
|
|
261
|
+
</div>
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<svg
|
|
266
|
+
bind:this={dialEl}
|
|
267
|
+
class="sv-tp__dial"
|
|
268
|
+
viewBox={`0 0 ${SIZE} ${SIZE}`}
|
|
269
|
+
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}
|
|
276
|
+
onpointerdown={onDialPointerDown}
|
|
277
|
+
onpointermove={onDialPointerMove}
|
|
278
|
+
onpointerup={onDialPointerUp}
|
|
279
|
+
onpointercancel={onDialPointerUp}
|
|
280
|
+
onkeydown={onKeydown}
|
|
281
|
+
>
|
|
282
|
+
<circle class="sv-tp__face" cx={C} cy={C} r={C - 4} />
|
|
283
|
+
<!-- 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>
|
|
290
|
+
{/each}
|
|
291
|
+
{: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>
|
|
294
|
+
{/each}
|
|
295
|
+
{/if}
|
|
296
|
+
</svg>
|
|
297
|
+
|
|
298
|
+
{#if footer}
|
|
299
|
+
<div class="sv-tp__footer">
|
|
300
|
+
<button type="button" class="sv-tp__now" onclick={now} disabled={disabled}>Now</button>
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
|
|
304
|
+
{#if name}<input type="hidden" {name} value={`${String(hours).padStart(2, '0')}:${mm}`} />{/if}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<style>
|
|
308
|
+
.sv-tp {
|
|
309
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
310
|
+
--_bg: var(--sg-bg, #fff);
|
|
311
|
+
--_fg: var(--sg-fg, #0f172a);
|
|
312
|
+
--_muted: var(--sg-muted, #64748b);
|
|
313
|
+
--_border: var(--sg-border, #e2e8f0);
|
|
314
|
+
--_face: var(--sg-header-bg, #f1f5f9);
|
|
315
|
+
--_radius: var(--sg-radius, 8px);
|
|
316
|
+
display: inline-flex; flex-direction: column; gap: 10px; align-items: center;
|
|
317
|
+
width: 260px; padding: 12px;
|
|
318
|
+
background: var(--_bg); color: var(--_fg);
|
|
319
|
+
border: 1px solid var(--_border); border-radius: calc(var(--_radius) + 4px);
|
|
320
|
+
user-select: none;
|
|
321
|
+
}
|
|
322
|
+
.sv-tp--disabled { opacity: 0.55; pointer-events: none; }
|
|
323
|
+
|
|
324
|
+
.sv-tp__head { display: flex; align-items: center; gap: 2px; font-variant-numeric: tabular-nums; }
|
|
325
|
+
.sv-tp__seg {
|
|
326
|
+
font-size: 30px; font-weight: 700; line-height: 1;
|
|
327
|
+
background: none; border: 0; color: var(--_muted); cursor: pointer;
|
|
328
|
+
padding: 2px 6px; border-radius: var(--_radius);
|
|
329
|
+
}
|
|
330
|
+
.sv-tp__seg.is-active { color: var(--_accent); background: color-mix(in srgb, var(--_accent) 12%, transparent); }
|
|
331
|
+
.sv-tp__colon { font-size: 28px; font-weight: 700; color: var(--_muted); }
|
|
332
|
+
.sv-tp__ampm { display: flex; flex-direction: column; margin-left: 8px; gap: 2px; }
|
|
333
|
+
.sv-tp__ampm button {
|
|
334
|
+
font-size: 11px; font-weight: 700; padding: 3px 8px;
|
|
335
|
+
background: none; border: 1px solid var(--_border); border-radius: 6px; color: var(--_muted); cursor: pointer;
|
|
336
|
+
}
|
|
337
|
+
.sv-tp__ampm button.is-active { background: var(--_accent); color: var(--sg-on-accent, #fff); border-color: var(--_accent); }
|
|
338
|
+
|
|
339
|
+
.sv-tp__dial { touch-action: none; cursor: pointer; outline: none; display: block; width: 100%; height: auto; aspect-ratio: 1; }
|
|
340
|
+
.sv-tp__dial:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; border-radius: 50%; }
|
|
341
|
+
.sv-tp__face { fill: var(--_face); }
|
|
342
|
+
.sv-tp__hand { stroke: var(--_accent); stroke-width: 2; }
|
|
343
|
+
.sv-tp__hub { fill: var(--_accent); }
|
|
344
|
+
.sv-tp__knob { fill: var(--_accent); opacity: 0.9; }
|
|
345
|
+
.sv-tp__num {
|
|
346
|
+
font-size: 13px; fill: var(--_fg); pointer-events: none;
|
|
347
|
+
font-family: inherit;
|
|
348
|
+
}
|
|
349
|
+
.sv-tp__num.is-inner { font-size: 11px; fill: var(--_muted); }
|
|
350
|
+
.sv-tp__num.is-active { fill: var(--sg-on-accent, #fff); font-weight: 700; }
|
|
351
|
+
|
|
352
|
+
.sv-tp__footer { width: 100%; display: flex; justify-content: center; border-top: 1px solid var(--_border); padding-top: 8px; }
|
|
353
|
+
.sv-tp__now {
|
|
354
|
+
padding: 5px 14px; font: inherit; font-size: 12px; font-weight: 600;
|
|
355
|
+
background: none; border: 1px solid var(--_border); border-radius: var(--_radius);
|
|
356
|
+
color: var(--_accent); cursor: pointer;
|
|
357
|
+
}
|
|
358
|
+
.sv-tp__now:hover { background: color-mix(in srgb, var(--_accent) 10%, transparent); }
|
|
359
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TimeValue = Date | string | number | null;
|
|
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
|
+
*/
|
|
8
|
+
type Props = {
|
|
9
|
+
/** Date, "HH:MM[:SS]" string, or epoch ms. */
|
|
10
|
+
value?: TimeValue;
|
|
11
|
+
/** Fires with a Date (today's date carrying the picked time). */
|
|
12
|
+
onChange?: (value: Date) => void;
|
|
13
|
+
format?: '12-hour' | '24-hour';
|
|
14
|
+
minuteInterval?: number;
|
|
15
|
+
/** After picking an hour, jump the dial to minutes. Default true. */
|
|
16
|
+
autoSwitchToMinutes?: boolean;
|
|
17
|
+
/** Show the Now footer button. */
|
|
18
|
+
footer?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
readonly?: boolean;
|
|
21
|
+
name?: string;
|
|
22
|
+
/** Which dial opens first. */
|
|
23
|
+
selection?: 'hour' | 'minute';
|
|
24
|
+
};
|
|
25
|
+
declare const SvTimePicker: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type SvTimePicker = ReturnType<typeof SvTimePicker>;
|
|
27
|
+
export default SvTimePicker;
|
|
@@ -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
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
pressed?: boolean
|
|
10
|
+
onChange?: (pressed: boolean) => void
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
size?: 'sm' | 'md' | 'lg'
|
|
13
|
+
ariaLabel?: string
|
|
14
|
+
children?: Snippet
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { pressed = false, onChange, disabled = false, size = 'md', ariaLabel, children }: Props = $props()
|
|
18
|
+
|
|
19
|
+
function toggle() {
|
|
20
|
+
if (disabled) return
|
|
21
|
+
onChange?.(!pressed)
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="sv-toggle sv-toggle--{size}"
|
|
28
|
+
class:is-pressed={pressed}
|
|
29
|
+
aria-pressed={pressed}
|
|
30
|
+
aria-label={ariaLabel}
|
|
31
|
+
{disabled}
|
|
32
|
+
onclick={toggle}
|
|
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>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvToggleButton - a button with a pressed on/off state (aria-pressed).
|
|
3
|
+
* Parity: Smart `smart-toggle-button`. Controlled via `pressed` + `onChange`.
|
|
4
|
+
*/
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
type Props = {
|
|
7
|
+
pressed?: boolean;
|
|
8
|
+
onChange?: (pressed: boolean) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
};
|
|
14
|
+
declare const SvToggleButton: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type SvToggleButton = ReturnType<typeof SvToggleButton>;
|
|
16
|
+
export default SvToggleButton;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type TreeNode = {
|
|
3
|
+
id: string
|
|
4
|
+
label: string
|
|
5
|
+
children?: TreeNode[]
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
/**
|
|
12
|
+
* SvTree - a WAI-ARIA tree view. Expand/collapse, single-select highlight, and
|
|
13
|
+
* optional cascading tri-state checkboxes. Parity: Smart `smart-tree`. Keyboard:
|
|
14
|
+
* up/down move, left collapse/parent, right expand/child, Enter/Space select.
|
|
15
|
+
* The visible tree is flattened so roving-focus keyboard nav stays simple.
|
|
16
|
+
*/
|
|
17
|
+
type Props = {
|
|
18
|
+
nodes: ReadonlyArray<TreeNode>
|
|
19
|
+
/** Selected node id (single-select highlight). */
|
|
20
|
+
selected?: string | null
|
|
21
|
+
onSelect?: (id: string) => void
|
|
22
|
+
/** Initially/controlled expanded ids. */
|
|
23
|
+
expandedIds?: string[]
|
|
24
|
+
onToggle?: (id: string, expanded: boolean) => void
|
|
25
|
+
/** Show cascading checkboxes; `checked` is the set of checked ids. */
|
|
26
|
+
checkable?: boolean
|
|
27
|
+
checked?: string[]
|
|
28
|
+
onCheck?: (ids: string[]) => void
|
|
29
|
+
ariaLabel?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
nodes,
|
|
34
|
+
selected = null,
|
|
35
|
+
onSelect,
|
|
36
|
+
expandedIds,
|
|
37
|
+
onToggle,
|
|
38
|
+
checkable = false,
|
|
39
|
+
checked = [],
|
|
40
|
+
onCheck,
|
|
41
|
+
ariaLabel,
|
|
42
|
+
}: Props = $props()
|
|
43
|
+
|
|
44
|
+
// Internal expanded set, seeded from the prop once (then self-managed unless
|
|
45
|
+
// the prop identity changes).
|
|
46
|
+
let expanded = $state<Set<string>>(new Set())
|
|
47
|
+
let seededExp = false
|
|
48
|
+
$effect(() => {
|
|
49
|
+
if (!seededExp) { seededExp = true; if (expandedIds) expanded = new Set(expandedIds) }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
type Flat = { node: TreeNode; depth: number; hasChildren: boolean; open: boolean; parentId: string | null }
|
|
53
|
+
|
|
54
|
+
const flat = $derived.by<Flat[]>(() => {
|
|
55
|
+
const out: Flat[] = []
|
|
56
|
+
const walk = (list: ReadonlyArray<TreeNode>, depth: number, parentId: string | null) => {
|
|
57
|
+
for (const node of list) {
|
|
58
|
+
const hasChildren = !!node.children?.length
|
|
59
|
+
const open = expanded.has(node.id)
|
|
60
|
+
out.push({ node, depth, hasChildren, open, parentId })
|
|
61
|
+
if (hasChildren && open) walk(node.children!, depth + 1, node.id)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
walk(nodes, 0, null)
|
|
65
|
+
return out
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
let active = $state(0)
|
|
69
|
+
$effect(() => { if (active >= flat.length) active = Math.max(0, flat.length - 1) })
|
|
70
|
+
|
|
71
|
+
const checkedSet = $derived(new Set(checked))
|
|
72
|
+
|
|
73
|
+
function descendantIds(node: TreeNode): string[] {
|
|
74
|
+
const ids: string[] = []
|
|
75
|
+
const walk = (n: TreeNode) => { for (const c of n.children ?? []) { ids.push(c.id); walk(c) } }
|
|
76
|
+
walk(node)
|
|
77
|
+
return ids
|
|
78
|
+
}
|
|
79
|
+
function checkState(node: TreeNode): 'checked' | 'indeterminate' | 'unchecked' {
|
|
80
|
+
if (!node.children?.length) return checkedSet.has(node.id) ? 'checked' : 'unchecked'
|
|
81
|
+
const desc = descendantIds(node)
|
|
82
|
+
const on = desc.filter((id) => checkedSet.has(id)).length
|
|
83
|
+
if (on === 0 && !checkedSet.has(node.id)) return 'unchecked'
|
|
84
|
+
if (on === desc.length) return 'checked'
|
|
85
|
+
return 'indeterminate'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toggleExpand(node: TreeNode) {
|
|
89
|
+
if (!node.children?.length) return
|
|
90
|
+
const next = new Set(expanded)
|
|
91
|
+
const willOpen = !next.has(node.id)
|
|
92
|
+
willOpen ? next.add(node.id) : next.delete(node.id)
|
|
93
|
+
expanded = next
|
|
94
|
+
onToggle?.(node.id, willOpen)
|
|
95
|
+
}
|
|
96
|
+
function select(node: TreeNode) {
|
|
97
|
+
if (node.disabled) return
|
|
98
|
+
onSelect?.(node.id)
|
|
99
|
+
}
|
|
100
|
+
function toggleCheck(node: TreeNode) {
|
|
101
|
+
if (node.disabled) return
|
|
102
|
+
const next = new Set(checkedSet)
|
|
103
|
+
const ids = [node.id, ...descendantIds(node)]
|
|
104
|
+
const currentlyOn = checkState(node) === 'checked'
|
|
105
|
+
for (const id of ids) currentlyOn ? next.delete(id) : next.add(id)
|
|
106
|
+
onCheck?.([...next])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onKeydown(e: KeyboardEvent) {
|
|
110
|
+
const item = flat[active]
|
|
111
|
+
if (!item) return
|
|
112
|
+
switch (e.key) {
|
|
113
|
+
case 'ArrowDown': e.preventDefault(); active = Math.min(active + 1, flat.length - 1); focusActive(); break
|
|
114
|
+
case 'ArrowUp': e.preventDefault(); active = Math.max(active - 1, 0); focusActive(); break
|
|
115
|
+
case 'ArrowRight':
|
|
116
|
+
e.preventDefault()
|
|
117
|
+
if (item.hasChildren && !item.open) toggleExpand(item.node)
|
|
118
|
+
else if (item.hasChildren && item.open) { active = Math.min(active + 1, flat.length - 1); focusActive() }
|
|
119
|
+
break
|
|
120
|
+
case 'ArrowLeft':
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
if (item.hasChildren && item.open) toggleExpand(item.node)
|
|
123
|
+
else if (item.parentId) { const pi = flat.findIndex((f) => f.node.id === item.parentId); if (pi >= 0) { active = pi; focusActive() } }
|
|
124
|
+
break
|
|
125
|
+
case 'Enter': e.preventDefault(); select(item.node); break
|
|
126
|
+
case ' ': e.preventDefault(); checkable ? toggleCheck(item.node) : select(item.node); break
|
|
127
|
+
case 'Home': e.preventDefault(); active = 0; focusActive(); break
|
|
128
|
+
case 'End': e.preventDefault(); active = flat.length - 1; focusActive(); break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let treeEl: HTMLDivElement | null = null
|
|
133
|
+
function focusActive() {
|
|
134
|
+
queueMicrotask(() => treeEl?.querySelector<HTMLElement>(`[data-row="${active}"]`)?.focus())
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<div bind:this={treeEl} class="sv-tree" role="tree" aria-label={ariaLabel} aria-multiselectable={checkable}>
|
|
139
|
+
{#each flat as item, i (item.node.id)}
|
|
140
|
+
{@const cs = checkable ? checkState(item.node) : 'unchecked'}
|
|
141
|
+
<div
|
|
142
|
+
class="sv-tree__row"
|
|
143
|
+
class:is-selected={item.node.id === selected}
|
|
144
|
+
class:is-disabled={item.node.disabled}
|
|
145
|
+
role="treeitem"
|
|
146
|
+
aria-level={item.depth + 1}
|
|
147
|
+
aria-selected={item.node.id === selected}
|
|
148
|
+
aria-expanded={item.hasChildren ? item.open : undefined}
|
|
149
|
+
aria-checked={checkable ? cs === 'checked' ? 'true' : cs === 'indeterminate' ? 'mixed' : 'false' : undefined}
|
|
150
|
+
data-row={i}
|
|
151
|
+
tabindex={i === active ? 0 : -1}
|
|
152
|
+
style:padding-left={`${item.depth * 18 + 6}px`}
|
|
153
|
+
onclick={() => { active = i; select(item.node) }}
|
|
154
|
+
onkeydown={onKeydown}
|
|
155
|
+
>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
class="sv-tree__twist"
|
|
159
|
+
class:is-open={item.open}
|
|
160
|
+
class:is-leaf={!item.hasChildren}
|
|
161
|
+
tabindex="-1"
|
|
162
|
+
aria-hidden={!item.hasChildren}
|
|
163
|
+
onclick={(e) => { e.stopPropagation(); toggleExpand(item.node) }}
|
|
164
|
+
>
|
|
165
|
+
{#if item.hasChildren}
|
|
166
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
|
167
|
+
{/if}
|
|
168
|
+
</button>
|
|
169
|
+
{#if checkable}
|
|
170
|
+
<button type="button" class="sv-tree__check is-{cs}" tabindex="-1" aria-hidden="true" onclick={(e) => { e.stopPropagation(); toggleCheck(item.node) }}>
|
|
171
|
+
{#if cs === 'checked'}<svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
172
|
+
{:else if cs === 'indeterminate'}<svg viewBox="0 0 16 16"><path d="M4 8h8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" /></svg>{/if}
|
|
173
|
+
</button>
|
|
174
|
+
{/if}
|
|
175
|
+
<span class="sv-tree__label">{item.node.label}</span>
|
|
176
|
+
</div>
|
|
177
|
+
{/each}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
.sv-tree {
|
|
182
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
183
|
+
width: 260px; padding: 4px; user-select: none;
|
|
184
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
185
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 8px);
|
|
186
|
+
font-size: 13px;
|
|
187
|
+
}
|
|
188
|
+
.sv-tree__row {
|
|
189
|
+
display: flex; align-items: center; gap: 4px; height: 30px; padding-right: 8px;
|
|
190
|
+
border-radius: 6px; cursor: pointer; outline: none;
|
|
191
|
+
}
|
|
192
|
+
.sv-tree__row:hover:not(.is-disabled) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
193
|
+
.sv-tree__row.is-selected { background: color-mix(in srgb, var(--_accent) 15%, transparent); color: var(--_accent); font-weight: 600; }
|
|
194
|
+
.sv-tree__row.is-disabled { opacity: 0.45; cursor: not-allowed; }
|
|
195
|
+
.sv-tree__row:focus-visible { box-shadow: inset 0 0 0 2px var(--sg-focus-ring, var(--_accent)); }
|
|
196
|
+
.sv-tree__twist {
|
|
197
|
+
display: grid; place-items: center; width: 18px; height: 18px; flex: none;
|
|
198
|
+
background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; transition: transform 0.12s;
|
|
199
|
+
}
|
|
200
|
+
.sv-tree__twist.is-open { transform: rotate(90deg); }
|
|
201
|
+
.sv-tree__twist.is-leaf { visibility: hidden; }
|
|
202
|
+
.sv-tree__check {
|
|
203
|
+
display: grid; place-items: center; width: 16px; height: 16px; flex: none; padding: 0;
|
|
204
|
+
border: 1.5px solid var(--sg-border, #cbd5e1); border-radius: 4px; background: var(--sg-input-bg, #fff);
|
|
205
|
+
color: var(--sg-on-accent, #fff); cursor: pointer;
|
|
206
|
+
}
|
|
207
|
+
.sv-tree__check.is-checked, .sv-tree__check.is-indeterminate { background: var(--_accent); border-color: var(--_accent); }
|
|
208
|
+
.sv-tree__check svg { width: 100%; height: 100%; }
|
|
209
|
+
.sv-tree__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
210
|
+
</style>
|