@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
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
export type DateTimeValue
|
|
2
|
+
export type { DateTimeValue } from './createDateTimePicker.svelte'
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
/**
|
|
7
7
|
* SvDateTimePicker - a text input (formatted via the date-format token engine)
|
|
8
8
|
* plus a dropdown that composes SvCalendar + SvTimePicker behind DATE / TIME
|
|
9
|
-
* tabs. Parity target: Smart `smart-date-time-picker`.
|
|
10
|
-
* the
|
|
11
|
-
*
|
|
9
|
+
* tabs. Parity target: Smart `smart-date-time-picker`. It is a styled renderer
|
|
10
|
+
* over the headless `createDateTimePicker` core (value math, parse/format,
|
|
11
|
+
* clamp, dropdown open/tab state, sub-picker wiring); the component keeps the
|
|
12
|
+
* render-only concerns - the portalled popover positioning (via popover.ts), DOM
|
|
13
|
+
* refs, and outside-click / reposition listeners - so the dropdown is never
|
|
14
|
+
* clipped by the grid's scroll container.
|
|
12
15
|
*/
|
|
13
16
|
import SvCalendar from './SvCalendar.svelte'
|
|
14
17
|
import SvTimePicker from './SvTimePicker.svelte'
|
|
15
18
|
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
19
|
+
import { createDateTimePicker, type DateTimeValue, type DropDownDisplayMode } from './createDateTimePicker.svelte'
|
|
20
|
+
import type { DateLike } from './datetime/date-core'
|
|
18
21
|
|
|
19
22
|
type Props = {
|
|
20
23
|
value?: DateTimeValue
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
hourFormat?: '12-hour' | '24-hour'
|
|
40
43
|
minuteInterval?: number
|
|
41
44
|
/** Which tabs the dropdown shows. */
|
|
42
|
-
dropDownDisplayMode?:
|
|
45
|
+
dropDownDisplayMode?: DropDownDisplayMode
|
|
43
46
|
/** Up/down spinner buttons that bump the value by `stepMinutes`. */
|
|
44
47
|
spinButtons?: boolean
|
|
45
48
|
stepMinutes?: number
|
|
@@ -74,66 +77,27 @@
|
|
|
74
77
|
animate = false,
|
|
75
78
|
}: Props = $props()
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
// The headless core owns value math, parse/format, clamping, the dropdown
|
|
81
|
+
// open/tab state and the sub-picker wiring. Reactive inputs are getters;
|
|
82
|
+
// callbacks are closures.
|
|
83
|
+
const dtp = createDateTimePicker({
|
|
84
|
+
value: () => value,
|
|
85
|
+
onChange: (d) => onChange?.(d),
|
|
86
|
+
onCommit: (d) => onCommit?.(d),
|
|
87
|
+
onCancel: () => onCancel?.(),
|
|
88
|
+
formatString: () => formatString,
|
|
89
|
+
min: () => min,
|
|
90
|
+
max: () => max,
|
|
91
|
+
nullable: () => nullable,
|
|
92
|
+
locale: () => locale,
|
|
93
|
+
dropDownDisplayMode: () => dropDownDisplayMode,
|
|
94
|
+
spinButtons: () => spinButtons,
|
|
95
|
+
stepMinutes: () => stepMinutes,
|
|
96
|
+
disabled: () => disabled,
|
|
97
|
+
readonly: () => readonly,
|
|
92
98
|
})
|
|
93
99
|
|
|
94
|
-
// ---
|
|
95
|
-
function commit(next: Date | null) {
|
|
96
|
-
const clamped = next ? clampDate(next, minD, maxD) : null
|
|
97
|
-
current = clamped
|
|
98
|
-
lastKey = clamped ? String(clamped.getTime()) : 'null'
|
|
99
|
-
text = clamped ? formatDate(clamped, formatString, locale) : ''
|
|
100
|
-
onChange?.(clamped)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function commitFromText() {
|
|
104
|
-
const raw = text.trim()
|
|
105
|
-
if (!raw) {
|
|
106
|
-
if (nullable) commit(null)
|
|
107
|
-
else text = current ? formatDate(current, formatString, locale) : ''
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
const parsed = parseDate(raw, formatString, locale, 2029, current ?? new Date())
|
|
111
|
-
if (parsed) commit(parsed)
|
|
112
|
-
else text = current ? formatDate(current, formatString, locale) : '' // revert on bad input
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function bump(deltaMin: number) {
|
|
116
|
-
const base = current ?? new Date()
|
|
117
|
-
commit(new Date(base.getTime() + deltaMin * 60000))
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// --- Sub-picker wiring ---
|
|
121
|
-
function onCalendarChange(dates: Date[]) {
|
|
122
|
-
const day = dates[0]
|
|
123
|
-
if (!day) return
|
|
124
|
-
commit(withTime(day, current ?? startOfDay(day)))
|
|
125
|
-
// Date-only picker: a single pick finalizes the value.
|
|
126
|
-
if (dropDownDisplayMode === 'calendar') { open = false; onCommit?.(current) }
|
|
127
|
-
else if (dropDownDisplayMode === 'both') tab = 'time'
|
|
128
|
-
}
|
|
129
|
-
function onTimeChange(t: Date) {
|
|
130
|
-
const base = current ?? new Date()
|
|
131
|
-
commit(withTime(base, t))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// --- Dropdown (portalled) ---
|
|
135
|
-
let open = $state(false)
|
|
136
|
-
let tab = $state<'date' | 'time'>('date')
|
|
100
|
+
// --- Portalled dropdown positioning (DOM-bound; stays in the component) -----
|
|
137
101
|
let triggerEl = $state<HTMLDivElement | null>(null)
|
|
138
102
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
139
103
|
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
@@ -142,17 +106,11 @@
|
|
|
142
106
|
if (!triggerEl) return
|
|
143
107
|
panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 360, minWidth: 300 })
|
|
144
108
|
}
|
|
145
|
-
function openPanel() {
|
|
146
|
-
if (!isInteractive || open) return
|
|
147
|
-
tab = dropDownDisplayMode === 'time' ? 'time' : 'date'
|
|
148
|
-
open = true
|
|
149
|
-
updatePos()
|
|
150
|
-
}
|
|
151
|
-
function closePanel() { open = false }
|
|
152
|
-
function toggle() { (open ? closePanel : openPanel)() }
|
|
153
109
|
|
|
110
|
+
// Reposition on open + while open (scroll/resize), and dismiss on outside click.
|
|
154
111
|
$effect(() => {
|
|
155
|
-
if (!open) return
|
|
112
|
+
if (!dtp.open) return
|
|
113
|
+
updatePos()
|
|
156
114
|
const reposition = () => updatePos()
|
|
157
115
|
window.addEventListener('scroll', reposition, true)
|
|
158
116
|
window.addEventListener('resize', reposition)
|
|
@@ -160,7 +118,7 @@
|
|
|
160
118
|
const t = e.target as Node | null
|
|
161
119
|
if (!t) return
|
|
162
120
|
if (triggerEl?.contains(t) || panelEl?.contains(t)) return
|
|
163
|
-
closePanel()
|
|
121
|
+
dtp.closePanel()
|
|
164
122
|
}
|
|
165
123
|
document.addEventListener('pointerdown', onDown, true)
|
|
166
124
|
return () => {
|
|
@@ -170,59 +128,44 @@
|
|
|
170
128
|
}
|
|
171
129
|
})
|
|
172
130
|
|
|
173
|
-
function onInputKeydown(e: KeyboardEvent) {
|
|
174
|
-
if (e.key === 'Enter') { e.preventDefault(); commitFromText(); closePanel(); onCommit?.(current) }
|
|
175
|
-
else if (e.key === 'Escape') { closePanel(); onCancel?.() }
|
|
176
|
-
else if (e.key === 'ArrowDown' && (e.altKey || !text)) { e.preventDefault(); openPanel() }
|
|
177
|
-
else if (spinButtons && e.key === 'ArrowUp') { e.preventDefault(); bump(stepMinutes) }
|
|
178
|
-
else if (spinButtons && e.key === 'ArrowDown') { e.preventDefault(); bump(-stepMinutes) }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
131
|
function focusOpen(node: HTMLInputElement) {
|
|
182
132
|
if (!autoOpen) return
|
|
183
133
|
node.focus()
|
|
184
|
-
openPanel()
|
|
134
|
+
dtp.openPanel()
|
|
185
135
|
}
|
|
186
136
|
|
|
187
137
|
function onRootFocusOut(e: FocusEvent) {
|
|
188
138
|
const next = e.relatedTarget as Node | null
|
|
189
139
|
// Ignore focus moving within the field or into the portalled panel.
|
|
190
140
|
if (next && (triggerEl?.contains(next) || panelEl?.contains(next))) return
|
|
191
|
-
onCommit?.(current)
|
|
141
|
+
onCommit?.(dtp.current)
|
|
192
142
|
}
|
|
193
|
-
|
|
194
|
-
const showDateTab = $derived(dropDownDisplayMode !== 'time')
|
|
195
|
-
const showTimeTab = $derived(dropDownDisplayMode !== 'calendar')
|
|
196
143
|
</script>
|
|
197
144
|
|
|
198
145
|
<div class="sv-dtp" class:sv-dtp--disabled={disabled} onfocusout={onRootFocusOut}>
|
|
199
146
|
<div class="sv-dtp__field" bind:this={triggerEl}>
|
|
200
147
|
{#if spinButtons}
|
|
201
148
|
<div class="sv-dtp__spin">
|
|
202
|
-
<button
|
|
203
|
-
<button
|
|
149
|
+
<button {...dtp.spinProps(1)}>▲</button>
|
|
150
|
+
<button {...dtp.spinProps(-1)}>▼</button>
|
|
204
151
|
</div>
|
|
205
152
|
{/if}
|
|
206
153
|
<input
|
|
207
154
|
class="sv-dtp__input"
|
|
208
|
-
|
|
209
|
-
bind:value={text}
|
|
155
|
+
bind:value={dtp.text}
|
|
210
156
|
{placeholder}
|
|
211
|
-
{
|
|
212
|
-
{readonly}
|
|
213
|
-
onblur={commitFromText}
|
|
214
|
-
onkeydown={onInputKeydown}
|
|
157
|
+
{...dtp.inputProps()}
|
|
215
158
|
use:focusOpen
|
|
216
159
|
/>
|
|
217
|
-
{#if nullable && current && isInteractive}
|
|
218
|
-
<button
|
|
160
|
+
{#if nullable && dtp.current && dtp.isInteractive}
|
|
161
|
+
<button class="sv-dtp__clear" {...dtp.clearProps()}>×</button>
|
|
219
162
|
{/if}
|
|
220
|
-
<button
|
|
163
|
+
<button class="sv-dtp__toggle" {...dtp.toggleProps()}>
|
|
221
164
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></svg>
|
|
222
165
|
</button>
|
|
223
166
|
</div>
|
|
224
167
|
|
|
225
|
-
{#if open}
|
|
168
|
+
{#if dtp.open}
|
|
226
169
|
<div
|
|
227
170
|
class="sv-dtp__panel"
|
|
228
171
|
bind:this={panelEl}
|
|
@@ -233,16 +176,16 @@
|
|
|
233
176
|
role="dialog"
|
|
234
177
|
aria-label="Choose date and time"
|
|
235
178
|
>
|
|
236
|
-
{#if showDateTab && showTimeTab}
|
|
179
|
+
{#if dtp.showDateTab && dtp.showTimeTab}
|
|
237
180
|
<div class="sv-dtp__tabs" role="tablist">
|
|
238
|
-
<button
|
|
239
|
-
<button
|
|
181
|
+
<button class:is-active={dtp.tab === 'date'} {...dtp.tabProps('date')}>DATE</button>
|
|
182
|
+
<button class:is-active={dtp.tab === 'time'} {...dtp.tabProps('time')}>TIME</button>
|
|
240
183
|
</div>
|
|
241
184
|
{/if}
|
|
242
185
|
<div class="sv-dtp__body">
|
|
243
|
-
{#if tab === 'date' && showDateTab}
|
|
186
|
+
{#if dtp.tab === 'date' && dtp.showDateTab}
|
|
244
187
|
<SvCalendar
|
|
245
|
-
value={current}
|
|
188
|
+
value={dtp.current}
|
|
246
189
|
selectionMode="one"
|
|
247
190
|
min={min}
|
|
248
191
|
max={max}
|
|
@@ -250,22 +193,22 @@
|
|
|
250
193
|
{weekNumbers}
|
|
251
194
|
{locale}
|
|
252
195
|
{animate}
|
|
253
|
-
onChange={onCalendarChange}
|
|
196
|
+
onChange={dtp.onCalendarChange}
|
|
254
197
|
/>
|
|
255
|
-
{:else if showTimeTab}
|
|
198
|
+
{:else if dtp.showTimeTab}
|
|
256
199
|
<SvTimePicker
|
|
257
|
-
value={current}
|
|
200
|
+
value={dtp.current}
|
|
258
201
|
format={hourFormat}
|
|
259
202
|
{minuteInterval}
|
|
260
203
|
footer
|
|
261
|
-
onChange={onTimeChange}
|
|
204
|
+
onChange={dtp.onTimeChange}
|
|
262
205
|
/>
|
|
263
206
|
{/if}
|
|
264
207
|
</div>
|
|
265
208
|
</div>
|
|
266
209
|
{/if}
|
|
267
210
|
|
|
268
|
-
{#if name}<input type="hidden" {name} value={current ? current.toISOString() : ''} />{/if}
|
|
211
|
+
{#if name}<input type="hidden" {name} value={dtp.current ? dtp.current.toISOString() : ''} />{/if}
|
|
269
212
|
</div>
|
|
270
213
|
|
|
271
214
|
<style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export type DateTimeValue
|
|
2
|
-
import { type
|
|
1
|
+
export type { DateTimeValue } from './createDateTimePicker.svelte';
|
|
2
|
+
import { type DateTimeValue, type DropDownDisplayMode } from './createDateTimePicker.svelte';
|
|
3
|
+
import type { DateLike } from './datetime/date-core';
|
|
3
4
|
type Props = {
|
|
4
5
|
value?: DateTimeValue;
|
|
5
6
|
onChange?: (value: Date | null) => void;
|
|
@@ -23,7 +24,7 @@ type Props = {
|
|
|
23
24
|
hourFormat?: '12-hour' | '24-hour';
|
|
24
25
|
minuteInterval?: number;
|
|
25
26
|
/** Which tabs the dropdown shows. */
|
|
26
|
-
dropDownDisplayMode?:
|
|
27
|
+
dropDownDisplayMode?: DropDownDisplayMode;
|
|
27
28
|
/** Up/down spinner buttons that bump the value by `stepMinutes`. */
|
|
28
29
|
spinButtons?: boolean;
|
|
29
30
|
stepMinutes?: number;
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* SvDropDownList - a single-select dropdown (trigger button + portalled list,
|
|
4
4
|
* no typing). Parity: Smart `smart-drop-down-list`. Controlled via `value` +
|
|
5
5
|
* `onChange`. Popover escapes scroll clipping via popover.ts.
|
|
6
|
+
*
|
|
7
|
+
* One styled renderer over the headless `createDropdownList` core (state +
|
|
8
|
+
* keyboard + ARIA); only portal/measure/scroll render concerns live here.
|
|
6
9
|
*/
|
|
7
10
|
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
8
11
|
import type { ListOption } from './list-option'
|
|
12
|
+
import { createDropdownList } from './createDropdownList.svelte'
|
|
9
13
|
|
|
10
14
|
type Props = {
|
|
11
15
|
options: ReadonlyArray<ListOption>
|
|
@@ -21,93 +25,66 @@
|
|
|
21
25
|
|
|
22
26
|
let { options, value = null, onChange, placeholder = 'Select…', disabled = false, name, size = 'md', ariaLabel, autoOpen = false }: Props = $props()
|
|
23
27
|
|
|
24
|
-
let open = $state(false)
|
|
25
|
-
let active = $state(-1)
|
|
26
28
|
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
27
29
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
28
30
|
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
+
const ddl = createDropdownList({
|
|
33
|
+
options: () => options,
|
|
34
|
+
value: () => value,
|
|
35
|
+
onChange: (v) => onChange?.(v),
|
|
36
|
+
disabled: () => disabled,
|
|
37
|
+
ariaLabel: () => ariaLabel,
|
|
38
|
+
focusTrigger: () => triggerEl?.focus(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const selected = $derived(ddl.selected)
|
|
32
42
|
|
|
33
43
|
function updatePos() {
|
|
34
44
|
if (!triggerEl) return
|
|
35
45
|
rect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: Math.min(options.length, 8) * 34 + 8 })
|
|
36
46
|
}
|
|
37
|
-
function openPanel() {
|
|
38
|
-
if (disabled || open) return
|
|
39
|
-
open = true
|
|
40
|
-
active = Math.max(0, options.findIndex((o) => o.value === value))
|
|
41
|
-
updatePos()
|
|
42
|
-
}
|
|
43
|
-
function close() { open = false }
|
|
44
|
-
function toggle() { open ? close() : openPanel() }
|
|
45
|
-
function pick(i: number) {
|
|
46
|
-
const o = options[i]
|
|
47
|
-
if (!o || o.disabled) return
|
|
48
|
-
onChange?.(o.value); close(); triggerEl?.focus()
|
|
49
|
-
}
|
|
50
|
-
function move(d: number) {
|
|
51
|
-
const pos = enabledIdx.indexOf(active)
|
|
52
|
-
active = enabledIdx[(pos + d + enabledIdx.length) % enabledIdx.length] ?? active
|
|
53
|
-
queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
54
|
-
}
|
|
55
|
-
function onKeydown(e: KeyboardEvent) {
|
|
56
|
-
if (disabled) return
|
|
57
|
-
if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPanel(); return }
|
|
58
|
-
if (!open) return
|
|
59
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
|
|
60
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
|
|
61
|
-
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(active) }
|
|
62
|
-
else if (e.key === 'Escape') { e.preventDefault(); close(); triggerEl?.focus() }
|
|
63
|
-
else if (e.key === 'Home') { active = enabledIdx[0] ?? 0 }
|
|
64
|
-
else if (e.key === 'End') { active = enabledIdx.at(-1) ?? 0 }
|
|
65
|
-
}
|
|
66
47
|
|
|
67
48
|
$effect(() => {
|
|
68
|
-
if (!open) return
|
|
49
|
+
if (!ddl.open) return
|
|
50
|
+
updatePos()
|
|
69
51
|
const rp = () => updatePos()
|
|
70
52
|
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
71
|
-
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; close() }
|
|
53
|
+
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; ddl.close() }
|
|
72
54
|
document.addEventListener('pointerdown', od, true)
|
|
73
55
|
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
74
56
|
})
|
|
75
|
-
|
|
57
|
+
// Keep the active option scrolled into view (render concern).
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (!ddl.open) return
|
|
60
|
+
const i = ddl.activeIndex
|
|
61
|
+
queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
62
|
+
})
|
|
63
|
+
function focusOpen(node: HTMLButtonElement) { if (autoOpen) { node.focus(); ddl.openPanel() } }
|
|
76
64
|
</script>
|
|
77
65
|
|
|
78
66
|
<button
|
|
79
67
|
bind:this={triggerEl}
|
|
80
|
-
type="button"
|
|
81
68
|
class="sv-ddl sv-ddl--{size}"
|
|
82
|
-
class:is-open={open}
|
|
69
|
+
class:is-open={ddl.open}
|
|
83
70
|
class:is-disabled={disabled}
|
|
84
|
-
|
|
85
|
-
aria-expanded={open}
|
|
86
|
-
aria-label={ariaLabel}
|
|
87
|
-
{disabled}
|
|
88
|
-
onclick={toggle}
|
|
89
|
-
onkeydown={onKeydown}
|
|
71
|
+
{...ddl.triggerProps()}
|
|
90
72
|
use:focusOpen
|
|
91
73
|
>
|
|
92
74
|
<span class="sv-ddl__value" class:is-placeholder={!selected}>{selected?.label ?? placeholder}</span>
|
|
93
75
|
<svg class="sv-ddl__chev" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
94
76
|
</button>
|
|
95
77
|
|
|
96
|
-
{#if open}
|
|
97
|
-
<div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`}
|
|
78
|
+
{#if ddl.open}
|
|
79
|
+
<div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} {...ddl.listboxProps()}>
|
|
98
80
|
{#each options as opt, i (opt.value)}
|
|
99
81
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
100
82
|
<div
|
|
101
83
|
class="sv-ddl__opt"
|
|
102
|
-
class:is-active={i
|
|
103
|
-
class:is-selected={opt
|
|
84
|
+
class:is-active={ddl.isActive(i)}
|
|
85
|
+
class:is-selected={ddl.isSelected(opt)}
|
|
104
86
|
class:is-disabled={opt.disabled}
|
|
105
|
-
|
|
106
|
-
tabindex="-1"
|
|
107
|
-
aria-selected={opt.value === value}
|
|
108
|
-
data-idx={i}
|
|
109
|
-
onclick={() => pick(i)}
|
|
110
|
-
onpointermove={() => { if (!opt.disabled) active = i }}
|
|
87
|
+
{...ddl.optionProps(i)}
|
|
111
88
|
>{opt.label}</div>
|
|
112
89
|
{/each}
|
|
113
90
|
</div>
|
package/dist/SvGauge.svelte
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* SvGauge - a radial arc gauge (SVG) that renders a value within [min, max]
|
|
4
4
|
* with optional colored threshold bands, a needle, and a center label. Parity:
|
|
5
5
|
* Smart `smart-gauge` (radial). Display control; theme-driven.
|
|
6
|
+
*
|
|
7
|
+
* The arc/needle geometry + `role="meter"` ARIA live in the headless
|
|
8
|
+
* `createGauge` core; this component is just one styled renderer over it.
|
|
6
9
|
*/
|
|
10
|
+
import { createGauge } from './createGauge.svelte'
|
|
11
|
+
|
|
7
12
|
type Band = { from: number; to: number; color: string }
|
|
8
13
|
|
|
9
14
|
type Props = {
|
|
@@ -40,32 +45,25 @@
|
|
|
40
45
|
ariaLabel,
|
|
41
46
|
}: Props = $props()
|
|
42
47
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const p0 = polar(a0, radius)
|
|
59
|
-
const p1 = polar(a1, radius)
|
|
60
|
-
const large = a1 - a0 > 180 ? 1 : 0
|
|
61
|
-
return `M ${p0.x} ${p0.y} A ${radius} ${radius} 0 ${large} 1 ${p1.x} ${p1.y}`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const needleEnd = $derived(polar(angleOf(clamped), r - thickness / 2))
|
|
48
|
+
const gauge = createGauge({
|
|
49
|
+
value: () => value,
|
|
50
|
+
min: () => min,
|
|
51
|
+
max: () => max,
|
|
52
|
+
sweep: () => sweep,
|
|
53
|
+
size: () => size,
|
|
54
|
+
thickness: () => thickness,
|
|
55
|
+
ariaLabel: () => ariaLabel,
|
|
56
|
+
})
|
|
57
|
+
const clamped = $derived(gauge.clamped)
|
|
58
|
+
const cx = $derived(gauge.cx)
|
|
59
|
+
const cy = $derived(gauge.cy)
|
|
60
|
+
const r = $derived(gauge.r)
|
|
61
|
+
const arcPath = (v0: number, v1: number, radius: number) => gauge.arcPath(v0, v1, radius)
|
|
62
|
+
const needleEnd = $derived(gauge.needleEnd)
|
|
65
63
|
const display = $derived(label ?? `${formatValue ? formatValue(clamped) : Math.round(clamped)}${unit}`)
|
|
66
64
|
</script>
|
|
67
65
|
|
|
68
|
-
<div class="sv-gauge"
|
|
66
|
+
<div class="sv-gauge" {...gauge.rootProps()}>
|
|
69
67
|
<svg viewBox={`0 0 ${size} ${size}`} width={size} height={size} class="sv-gauge__svg">
|
|
70
68
|
<!-- track -->
|
|
71
69
|
<path d={arcPath(min, max, r)} class="sv-gauge__track" fill="none" stroke-width={thickness} stroke-linecap="round" />
|
package/dist/SvGauge.svelte.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SvGauge - a radial arc gauge (SVG) that renders a value within [min, max]
|
|
3
|
-
* with optional colored threshold bands, a needle, and a center label. Parity:
|
|
4
|
-
* Smart `smart-gauge` (radial). Display control; theme-driven.
|
|
5
|
-
*/
|
|
6
1
|
type Band = {
|
|
7
2
|
from: number;
|
|
8
3
|
to: number;
|
package/dist/SvListBox.svelte
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* via `value` (scalar or array) + `onChange`.
|
|
6
6
|
*/
|
|
7
7
|
import type { ListOption } from './list-option'
|
|
8
|
+
import { createListbox } from './createListbox.svelte'
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
10
11
|
options: ReadonlyArray<ListOption>
|
|
@@ -15,89 +16,55 @@
|
|
|
15
16
|
/** Visible height in rows before scrolling. */
|
|
16
17
|
rows?: number
|
|
17
18
|
ariaLabel?: string
|
|
19
|
+
/** Form field name; emits a hidden input per selected value so the listbox
|
|
20
|
+
* posts in a native <form> (multiple selection -> repeated name). */
|
|
21
|
+
name?: string
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
let { options, value = null, onChange, multiple = false, disabled = false, rows = 7, ariaLabel }: Props = $props()
|
|
24
|
+
let { options, value = null, onChange, multiple = false, disabled = false, rows = 7, ariaLabel, name }: Props = $props()
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// The styled listbox is just a renderer over the headless core.
|
|
27
|
+
const lb = createListbox({
|
|
28
|
+
options: () => options,
|
|
29
|
+
value: () => value,
|
|
30
|
+
onChange: (v) => onChange?.(v),
|
|
31
|
+
multiple: () => multiple,
|
|
32
|
+
disabled: () => disabled,
|
|
33
|
+
ariaLabel: () => ariaLabel,
|
|
30
34
|
})
|
|
31
|
-
|
|
32
|
-
function isSel(o: ListOption) { return selected.includes(o.value) }
|
|
33
|
-
|
|
34
|
-
function pick(i: number) {
|
|
35
|
-
const o = options[i]
|
|
36
|
-
if (!o || o.disabled || disabled) return
|
|
37
|
-
active = i
|
|
38
|
-
if (multiple) {
|
|
39
|
-
const set = new Set(selected)
|
|
40
|
-
set.has(o.value) ? set.delete(o.value) : set.add(o.value)
|
|
41
|
-
onChange?.([...set])
|
|
42
|
-
} else {
|
|
43
|
-
onChange?.(o.value)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function move(delta: number) {
|
|
48
|
-
const pos = enabledIdx.indexOf(active)
|
|
49
|
-
const next = enabledIdx[(pos + delta + enabledIdx.length) % enabledIdx.length]
|
|
50
|
-
if (next != null) { active = next; scrollActive() }
|
|
51
|
-
}
|
|
35
|
+
const isSel = (o: ListOption) => lb.isSelected(o.value)
|
|
52
36
|
|
|
53
37
|
let listEl: HTMLUListElement | null = null
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (disabled) return
|
|
60
|
-
switch (e.key) {
|
|
61
|
-
case 'ArrowDown': e.preventDefault(); move(1); break
|
|
62
|
-
case 'ArrowUp': e.preventDefault(); move(-1); break
|
|
63
|
-
case 'Home': e.preventDefault(); active = enabledIdx[0] ?? 0; scrollActive(); break
|
|
64
|
-
case 'End': e.preventDefault(); active = enabledIdx.at(-1) ?? 0; scrollActive(); break
|
|
65
|
-
case ' ':
|
|
66
|
-
case 'Enter': e.preventDefault(); pick(active); break
|
|
67
|
-
}
|
|
68
|
-
}
|
|
38
|
+
// Scroll-into-view is a render concern, so it lives here (not in the core).
|
|
39
|
+
$effect(() => {
|
|
40
|
+
const i = lb.activeIndex
|
|
41
|
+
queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
|
|
42
|
+
})
|
|
69
43
|
</script>
|
|
70
44
|
|
|
45
|
+
<div style="display: contents">
|
|
71
46
|
<ul
|
|
72
47
|
bind:this={listEl}
|
|
73
48
|
class="sv-listbox"
|
|
74
|
-
role="listbox"
|
|
75
|
-
aria-multiselectable={multiple}
|
|
76
|
-
aria-label={ariaLabel}
|
|
77
|
-
aria-disabled={disabled}
|
|
78
|
-
tabindex={disabled ? -1 : 0}
|
|
79
49
|
style:--sv-rows={rows}
|
|
80
|
-
|
|
50
|
+
{...lb.rootProps()}
|
|
81
51
|
>
|
|
82
52
|
{#each options as opt, i (opt.value)}
|
|
83
53
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
84
54
|
<li
|
|
85
55
|
class="sv-listbox__opt"
|
|
86
56
|
class:is-selected={isSel(opt)}
|
|
87
|
-
class:is-active={i
|
|
57
|
+
class:is-active={lb.isActive(i)}
|
|
88
58
|
class:is-disabled={opt.disabled}
|
|
89
|
-
|
|
90
|
-
aria-selected={isSel(opt)}
|
|
91
|
-
aria-disabled={opt.disabled}
|
|
92
|
-
data-idx={i}
|
|
93
|
-
onclick={() => pick(i)}
|
|
94
|
-
onpointermove={() => { if (!opt.disabled) active = i }}
|
|
59
|
+
{...lb.optionProps(i)}
|
|
95
60
|
>
|
|
96
61
|
{#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
|
|
97
62
|
<span class="sv-listbox__label">{opt.label}</span>
|
|
98
63
|
</li>
|
|
99
64
|
{/each}
|
|
100
65
|
</ul>
|
|
66
|
+
{#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
|
|
67
|
+
</div>
|
|
101
68
|
|
|
102
69
|
<style>
|
|
103
70
|
.sv-listbox {
|
|
@@ -13,6 +13,9 @@ type Props = {
|
|
|
13
13
|
/** Visible height in rows before scrolling. */
|
|
14
14
|
rows?: number;
|
|
15
15
|
ariaLabel?: string;
|
|
16
|
+
/** Form field name; emits a hidden input per selected value so the listbox
|
|
17
|
+
* posts in a native <form> (multiple selection -> repeated name). */
|
|
18
|
+
name?: string;
|
|
16
19
|
};
|
|
17
20
|
declare const SvListBox: import("svelte").Component<Props, {}, "">;
|
|
18
21
|
type SvListBox = ReturnType<typeof SvListBox>;
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
type
|
|
1
|
+
import { type SvEditorProps } from './editor-contract';
|
|
2
|
+
type Props = SvEditorProps & {
|
|
2
3
|
value?: number | null;
|
|
3
4
|
onChange?: (value: number | null) => void;
|
|
4
5
|
min?: number;
|
|
@@ -10,12 +11,7 @@ type Props = {
|
|
|
10
11
|
prefix?: string;
|
|
11
12
|
suffix?: string;
|
|
12
13
|
placeholder?: string;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
readonly?: boolean;
|
|
15
14
|
spinButtons?: boolean;
|
|
16
|
-
name?: string;
|
|
17
|
-
size?: 'sm' | 'md' | 'lg';
|
|
18
|
-
ariaLabel?: string;
|
|
19
15
|
};
|
|
20
16
|
declare const SvNumberInput: import("svelte").Component<Props, {}, "">;
|
|
21
17
|
type SvNumberInput = ReturnType<typeof SvNumberInput>;
|