@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
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SvPasswordInput - a password field with a reveal toggle and an optional
|
|
4
4
|
* strength meter. Parity: Smart `smart-password-input`. Emits the string value.
|
|
5
|
+
*
|
|
6
|
+
* The styled renderer over the headless `createPasswordInput` core: the reveal
|
|
7
|
+
* toggle + strength heuristic come from the core, spread here via prop-getters.
|
|
5
8
|
*/
|
|
9
|
+
import { createPasswordInput } from './createPasswordInput.svelte'
|
|
10
|
+
|
|
6
11
|
type Props = {
|
|
7
12
|
value?: string
|
|
8
13
|
onChange?: (value: string) => void
|
|
@@ -33,38 +38,23 @@
|
|
|
33
38
|
autocomplete = 'current-password',
|
|
34
39
|
}: Props = $props()
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) score++
|
|
45
|
-
if (/\d/.test(v)) score++
|
|
46
|
-
if (/[^a-zA-Z0-9]/.test(v)) score++
|
|
47
|
-
return score
|
|
41
|
+
const pw = createPasswordInput({
|
|
42
|
+
value: () => value,
|
|
43
|
+
onChange: (v) => onChange?.(v),
|
|
44
|
+
placeholder: () => placeholder,
|
|
45
|
+
disabled: () => disabled,
|
|
46
|
+
readonly: () => readonly,
|
|
47
|
+
ariaLabel: () => ariaLabel,
|
|
48
|
+
autocomplete: () => autocomplete,
|
|
48
49
|
})
|
|
49
|
-
const strengthLabel = ['', 'Weak', 'Fair', 'Good', 'Strong']
|
|
50
50
|
</script>
|
|
51
51
|
|
|
52
52
|
<div class="sv-pw sv-pw--{size}" class:is-disabled={disabled}>
|
|
53
53
|
<div class="sv-pw__field">
|
|
54
|
-
<input
|
|
55
|
-
class="sv-pw__input"
|
|
56
|
-
type={revealed ? 'text' : 'password'}
|
|
57
|
-
value={value}
|
|
58
|
-
{placeholder}
|
|
59
|
-
{disabled}
|
|
60
|
-
{readonly}
|
|
61
|
-
autocomplete={autocomplete as AutoFill}
|
|
62
|
-
aria-label={ariaLabel}
|
|
63
|
-
oninput={(e) => onChange?.((e.currentTarget as HTMLInputElement).value)}
|
|
64
|
-
/>
|
|
54
|
+
<input class="sv-pw__input" {...pw.inputProps()} />
|
|
65
55
|
{#if revealable}
|
|
66
|
-
<button
|
|
67
|
-
{#if revealed}
|
|
56
|
+
<button class="sv-pw__eye" {...pw.toggleProps()}>
|
|
57
|
+
{#if pw.revealed}
|
|
68
58
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" /><path d="M1 1l22 22" /></svg>
|
|
69
59
|
{:else}
|
|
70
60
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" /></svg>
|
|
@@ -75,10 +65,10 @@
|
|
|
75
65
|
{#if showStrength}
|
|
76
66
|
<div class="sv-pw__meter" aria-hidden="true">
|
|
77
67
|
{#each Array(4) as _, i (i)}
|
|
78
|
-
<span class="sv-pw__bar is-lvl{strength}" class:on={i < strength}></span>
|
|
68
|
+
<span class="sv-pw__bar is-lvl{pw.strength}" class:on={i < pw.strength}></span>
|
|
79
69
|
{/each}
|
|
80
70
|
</div>
|
|
81
|
-
<span class="sv-pw__strength is-lvl{strength}">{strengthLabel
|
|
71
|
+
<span class="sv-pw__strength is-lvl{pw.strength}">{pw.strengthLabel}</span>
|
|
82
72
|
{/if}
|
|
83
73
|
{#if name}<input type="hidden" {name} value={value} />{/if}
|
|
84
74
|
</div>
|
package/src/SvPhoneInput.svelte
CHANGED
|
Binary file
|
package/src/SvRadioGroup.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
export type
|
|
2
|
+
export type { RadioOption } from './createRadioGroup.svelte'
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
|
|
9
9
|
* (as a managed group). Controlled via `value` + `onChange`.
|
|
10
10
|
*/
|
|
11
|
+
import { createRadioGroup, type RadioOption } from './createRadioGroup.svelte'
|
|
12
|
+
|
|
11
13
|
type Props = {
|
|
12
14
|
options: ReadonlyArray<RadioOption>
|
|
13
15
|
value?: string | number | null
|
|
@@ -30,50 +32,25 @@
|
|
|
30
32
|
ariaLabel,
|
|
31
33
|
}: Props = $props()
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
onChange?.(o.value)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function onKeydown(e: KeyboardEvent, i: number) {
|
|
45
|
-
if (disabled) return
|
|
46
|
-
const pos = enabledIdx.indexOf(i)
|
|
47
|
-
if (pos < 0) return
|
|
48
|
-
let nextPos = pos
|
|
49
|
-
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') nextPos = (pos + 1) % enabledIdx.length
|
|
50
|
-
else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') nextPos = (pos - 1 + enabledIdx.length) % enabledIdx.length
|
|
51
|
-
else if (e.key === ' ') { e.preventDefault(); pick(i); return }
|
|
52
|
-
else return
|
|
53
|
-
e.preventDefault()
|
|
54
|
-
const target = enabledIdx[nextPos]!
|
|
55
|
-
pick(target)
|
|
56
|
-
;(e.currentTarget as HTMLElement).parentElement?.querySelectorAll<HTMLElement>('.sv-radio')[target]?.focus()
|
|
57
|
-
}
|
|
35
|
+
// The styled radio group is just a renderer over the headless core.
|
|
36
|
+
const rg = createRadioGroup({
|
|
37
|
+
options: () => options,
|
|
38
|
+
value: () => value,
|
|
39
|
+
onChange: (v) => onChange?.(v),
|
|
40
|
+
disabled: () => disabled,
|
|
41
|
+
ariaLabel: () => ariaLabel,
|
|
42
|
+
})
|
|
58
43
|
</script>
|
|
59
44
|
|
|
60
45
|
<div
|
|
61
46
|
class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
|
|
62
|
-
|
|
63
|
-
aria-label={ariaLabel}
|
|
64
|
-
aria-disabled={disabled}
|
|
47
|
+
{...rg.groupProps()}
|
|
65
48
|
>
|
|
66
|
-
{#each options as opt
|
|
49
|
+
{#each options as opt (opt.value)}
|
|
67
50
|
<button
|
|
68
|
-
type="button"
|
|
69
|
-
role="radio"
|
|
70
51
|
class="sv-radio"
|
|
71
52
|
class:is-checked={opt.value === value}
|
|
72
|
-
|
|
73
|
-
disabled={disabled || opt.disabled}
|
|
74
|
-
tabindex={i === focusIdx ? 0 : -1}
|
|
75
|
-
onclick={() => pick(i)}
|
|
76
|
-
onkeydown={(e) => onKeydown(e, i)}
|
|
53
|
+
{...rg.radioProps(opt)}
|
|
77
54
|
>
|
|
78
55
|
<span class="sv-radio__dot"></span>
|
|
79
56
|
<span class="sv-radio__label">{opt.label}</span>
|
package/src/SvRating.svelte
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* `smart-rating`. Hover preview, keyboard, optional half steps, readOnly.
|
|
5
5
|
* Uses the grid's `--sg-rating-*` tokens so it matches the in-grid editor.
|
|
6
6
|
*/
|
|
7
|
+
import { createRating } from './createRating.svelte'
|
|
8
|
+
|
|
7
9
|
type Props = {
|
|
8
10
|
value?: number
|
|
9
11
|
onChange?: (value: number) => void
|
|
10
12
|
max?: number
|
|
11
13
|
/** Allow half-star precision. */
|
|
12
14
|
allowHalf?: boolean
|
|
15
|
+
/** Read-only: shown but not editable (the kit-standard name). */
|
|
16
|
+
readonly?: boolean
|
|
17
|
+
/** @deprecated use `readonly` - kept for back-compat. */
|
|
13
18
|
readOnly?: boolean
|
|
14
19
|
disabled?: boolean
|
|
15
20
|
size?: 'sm' | 'md' | 'lg'
|
|
@@ -22,6 +27,7 @@
|
|
|
22
27
|
onChange,
|
|
23
28
|
max = 5,
|
|
24
29
|
allowHalf = false,
|
|
30
|
+
readonly = false,
|
|
25
31
|
readOnly = false,
|
|
26
32
|
disabled = false,
|
|
27
33
|
size = 'md',
|
|
@@ -29,63 +35,37 @@
|
|
|
29
35
|
ariaLabel,
|
|
30
36
|
}: Props = $props()
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
const isInteractive = $derived(!readOnly && !disabled)
|
|
34
|
-
const shown = $derived(hover ?? value)
|
|
35
|
-
|
|
36
|
-
function valueAt(e: MouseEvent, index: number): number {
|
|
37
|
-
if (!allowHalf) return index + 1
|
|
38
|
-
const el = e.currentTarget as HTMLElement
|
|
39
|
-
const rect = el.getBoundingClientRect()
|
|
40
|
-
return e.clientX - rect.left < rect.width / 2 ? index + 0.5 : index + 1
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function fillOf(index: number): 'full' | 'half' | 'empty' {
|
|
44
|
-
if (shown >= index + 1) return 'full'
|
|
45
|
-
if (shown >= index + 0.5) return 'half'
|
|
46
|
-
return 'empty'
|
|
47
|
-
}
|
|
38
|
+
const ro = $derived(readonly || readOnly)
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
// The styled rating is just a renderer over the headless core.
|
|
41
|
+
const r = createRating({
|
|
42
|
+
value: () => value,
|
|
43
|
+
onChange: (v) => onChange?.(v),
|
|
44
|
+
max: () => max,
|
|
45
|
+
allowHalf: () => allowHalf,
|
|
46
|
+
readonly: () => ro,
|
|
47
|
+
disabled: () => disabled,
|
|
48
|
+
ariaLabel: () => ariaLabel,
|
|
49
|
+
})
|
|
57
50
|
</script>
|
|
58
51
|
|
|
59
52
|
<div
|
|
60
53
|
class="sv-rating sv-rating--{size}"
|
|
61
|
-
class:is-readonly={
|
|
54
|
+
class:is-readonly={ro}
|
|
62
55
|
class:is-disabled={disabled}
|
|
63
|
-
|
|
64
|
-
tabindex={isInteractive ? 0 : -1}
|
|
65
|
-
aria-label={ariaLabel ?? 'Rating'}
|
|
66
|
-
aria-valuenow={value}
|
|
67
|
-
aria-valuemin="0"
|
|
68
|
-
aria-valuemax={max}
|
|
69
|
-
aria-readonly={readOnly}
|
|
70
|
-
onkeydown={onKeydown}
|
|
71
|
-
onpointerleave={() => (hover = null)}
|
|
56
|
+
{...r.rootProps()}
|
|
72
57
|
>
|
|
73
58
|
{#each Array(max) as _, i (i)}
|
|
74
59
|
<button
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
tabindex="-1"
|
|
78
|
-
disabled={!isInteractive}
|
|
79
|
-
aria-label={`${i + 1} star${i ? 's' : ''}`}
|
|
80
|
-
onpointermove={(e) => { if (isInteractive) hover = valueAt(e, i) }}
|
|
81
|
-
onclick={(e) => { if (isInteractive) onChange?.(valueAt(e, i)) }}
|
|
60
|
+
class="sv-rating__star is-{r.fillOf(i)}"
|
|
61
|
+
{...r.starProps(i)}
|
|
82
62
|
>
|
|
83
63
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
84
64
|
<defs>
|
|
85
65
|
<linearGradient id="half-{i}"><stop offset="50%" stop-color="var(--_on)" /><stop offset="50%" stop-color="var(--_off)" /></linearGradient>
|
|
86
66
|
</defs>
|
|
87
67
|
<path d="M12 2.5l2.9 6.1 6.6.9-4.8 4.6 1.2 6.6L12 18.6 6.1 21.7l1.2-6.6L2.5 9.5l6.6-.9z"
|
|
88
|
-
fill={fillOf(i) === 'full' ? 'var(--_on)' : fillOf(i) === 'half' ? `url(#half-${i})` : 'var(--_off)'} />
|
|
68
|
+
fill={r.fillOf(i) === 'full' ? 'var(--_on)' : r.fillOf(i) === 'half' ? `url(#half-${i})` : 'var(--_off)'} />
|
|
89
69
|
</svg>
|
|
90
70
|
</button>
|
|
91
71
|
{/each}
|
package/src/SvSlider.svelte
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* SvSlider - a single or range slider (WAI-ARIA slider) with steps, ticks,
|
|
4
4
|
* keyboard + pointer drag. Parity: Smart `smart-slider`. Single emits number;
|
|
5
5
|
* range emits [number, number].
|
|
6
|
+
*
|
|
7
|
+
* The behavior lives in the headless `createSlider` core; this component is
|
|
8
|
+
* just one styled renderer over it (it only owns pointer/DOM measurement).
|
|
6
9
|
*/
|
|
10
|
+
import { createSlider } from './createSlider.svelte'
|
|
11
|
+
|
|
7
12
|
type Props = {
|
|
8
13
|
value?: number | [number, number]
|
|
9
14
|
onChange?: (value: any) => void
|
|
@@ -38,15 +43,22 @@
|
|
|
38
43
|
formatValue,
|
|
39
44
|
}: Props = $props()
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// Behavior (value<->position math, stepping, keyboard, drag state, ARIA) lives
|
|
47
|
+
// in the headless `createSlider` core; the component keeps the DOM measuring.
|
|
48
|
+
const slider = createSlider({
|
|
49
|
+
value: () => value,
|
|
50
|
+
onChange: (v) => onChange?.(v),
|
|
51
|
+
min: () => min,
|
|
52
|
+
max: () => max,
|
|
53
|
+
step: () => step,
|
|
54
|
+
range: () => range,
|
|
55
|
+
orientation: () => orientation,
|
|
56
|
+
disabled: () => disabled,
|
|
57
|
+
ariaLabel: () => ariaLabel,
|
|
58
|
+
})
|
|
59
|
+
const lo = $derived(slider.lo)
|
|
60
|
+
const hi = $derived(slider.hi)
|
|
61
|
+
const pct = (v: number) => slider.percentOf(v)
|
|
50
62
|
|
|
51
63
|
const tickValues = $derived.by<number[]>(() => {
|
|
52
64
|
if (!ticks) return []
|
|
@@ -54,58 +66,18 @@
|
|
|
54
66
|
return Array.from({ length: ticks }, (_, i) => min + ((max - min) / (ticks - 1)) * i)
|
|
55
67
|
})
|
|
56
68
|
|
|
69
|
+
// The rect is measured here (a DOM concern) and passed INTO the core.
|
|
57
70
|
let trackEl: HTMLDivElement | null = null
|
|
58
|
-
let dragging = $state<null | 'lo' | 'hi' | 'single'>(null)
|
|
59
|
-
|
|
60
|
-
function valueFromPointer(e: PointerEvent): number {
|
|
61
|
-
const rect = trackEl!.getBoundingClientRect()
|
|
62
|
-
const frac = isVert
|
|
63
|
-
? 1 - (e.clientY - rect.top) / rect.height
|
|
64
|
-
: (e.clientX - rect.left) / rect.width
|
|
65
|
-
return clampStep(min + Math.min(1, Math.max(0, frac)) * (max - min))
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function emitSingle(v: number) { onChange?.(v) }
|
|
69
|
-
function emitRange(nlo: number, nhi: number) { onChange?.([Math.min(nlo, nhi), Math.max(nlo, nhi)]) }
|
|
70
|
-
|
|
71
71
|
function onTrackDown(e: PointerEvent) {
|
|
72
72
|
if (disabled) return
|
|
73
|
-
|
|
74
|
-
if (range) {
|
|
75
|
-
const which = Math.abs(v - lo) <= Math.abs(v - hi) ? 'lo' : 'hi'
|
|
76
|
-
dragging = which
|
|
77
|
-
which === 'lo' ? emitRange(v, hi) : emitRange(lo, v)
|
|
78
|
-
} else {
|
|
79
|
-
dragging = 'single'
|
|
80
|
-
emitSingle(v)
|
|
81
|
-
}
|
|
73
|
+
slider.pointerDown({ x: e.clientX, y: e.clientY }, trackEl!.getBoundingClientRect())
|
|
82
74
|
trackEl?.setPointerCapture(e.pointerId)
|
|
83
75
|
}
|
|
84
76
|
function onMove(e: PointerEvent) {
|
|
85
|
-
if (!dragging) return
|
|
86
|
-
|
|
87
|
-
if (dragging === 'single') emitSingle(v)
|
|
88
|
-
else if (dragging === 'lo') emitRange(v, hi)
|
|
89
|
-
else emitRange(lo, v)
|
|
90
|
-
}
|
|
91
|
-
function onUp(e: PointerEvent) { if (dragging) { dragging = null; trackEl?.releasePointerCapture(e.pointerId) } }
|
|
92
|
-
|
|
93
|
-
function onThumbKey(e: KeyboardEvent, which: 'lo' | 'hi' | 'single') {
|
|
94
|
-
if (disabled) return
|
|
95
|
-
const cur = which === 'lo' ? lo : hi
|
|
96
|
-
let next = cur
|
|
97
|
-
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = clampStep(cur + step)
|
|
98
|
-
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = clampStep(cur - step)
|
|
99
|
-
else if (e.key === 'Home') next = min
|
|
100
|
-
else if (e.key === 'End') next = max
|
|
101
|
-
else if (e.key === 'PageUp') next = clampStep(cur + step * 10)
|
|
102
|
-
else if (e.key === 'PageDown') next = clampStep(cur - step * 10)
|
|
103
|
-
else return
|
|
104
|
-
e.preventDefault()
|
|
105
|
-
if (which === 'single') emitSingle(next)
|
|
106
|
-
else if (which === 'lo') emitRange(next, hi)
|
|
107
|
-
else emitRange(lo, next)
|
|
77
|
+
if (!slider.dragging) return
|
|
78
|
+
slider.setFromPointer({ x: e.clientX, y: e.clientY }, trackEl!.getBoundingClientRect())
|
|
108
79
|
}
|
|
80
|
+
function onUp(e: PointerEvent) { if (slider.dragging) { slider.endDrag(); trackEl?.releasePointerCapture(e.pointerId) } }
|
|
109
81
|
|
|
110
82
|
const fmt = (v: number) => (formatValue ? formatValue(v) : String(v))
|
|
111
83
|
</script>
|
|
@@ -119,6 +91,7 @@
|
|
|
119
91
|
<div
|
|
120
92
|
bind:this={trackEl}
|
|
121
93
|
class="sv-slider__track"
|
|
94
|
+
{...slider.trackProps()}
|
|
122
95
|
onpointerdown={onTrackDown}
|
|
123
96
|
onpointermove={onMove}
|
|
124
97
|
onpointerup={onUp}
|
|
@@ -134,14 +107,14 @@
|
|
|
134
107
|
{/each}
|
|
135
108
|
|
|
136
109
|
{#if range}
|
|
137
|
-
<div class="sv-slider__thumb" style:--p={`${pct(lo)}%`}
|
|
110
|
+
<div class="sv-slider__thumb" style:--p={`${pct(lo)}%`} {...slider.thumbProps(0)}>
|
|
138
111
|
{#if showValue}<span class="sv-slider__bubble">{fmt(lo)}</span>{/if}
|
|
139
112
|
</div>
|
|
140
|
-
<div class="sv-slider__thumb" style:--p={`${pct(hi)}%`}
|
|
113
|
+
<div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} {...slider.thumbProps(1)}>
|
|
141
114
|
{#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
|
|
142
115
|
</div>
|
|
143
116
|
{:else}
|
|
144
|
-
<div class="sv-slider__thumb" style:--p={`${pct(hi)}%`}
|
|
117
|
+
<div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} {...slider.thumbProps(0)}>
|
|
145
118
|
{#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
|
|
146
119
|
</div>
|
|
147
120
|
{/if}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* `smart-switch-button`. Controlled via `checked` + `onChange`; keyboard
|
|
5
5
|
* togglable; optional inline on/off labels.
|
|
6
6
|
*/
|
|
7
|
+
import { createSwitch } from './createSwitch.svelte'
|
|
8
|
+
|
|
7
9
|
type Props = {
|
|
8
10
|
checked?: boolean
|
|
9
11
|
onChange?: (checked: boolean) => void
|
|
@@ -27,28 +29,19 @@
|
|
|
27
29
|
offLabel,
|
|
28
30
|
}: Props = $props()
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else if (e.key === 'ArrowRight') { if (!checked) onChange?.(true) }
|
|
38
|
-
else if (e.key === 'ArrowLeft') { if (checked) onChange?.(false) }
|
|
39
|
-
}
|
|
32
|
+
// The styled switch is just a renderer over the headless core.
|
|
33
|
+
const sw = createSwitch({
|
|
34
|
+
checked: () => checked,
|
|
35
|
+
onChange: (v) => onChange?.(v),
|
|
36
|
+
disabled: () => disabled,
|
|
37
|
+
ariaLabel: () => ariaLabel,
|
|
38
|
+
})
|
|
40
39
|
</script>
|
|
41
40
|
|
|
42
41
|
<button
|
|
43
|
-
type="button"
|
|
44
|
-
role="switch"
|
|
45
42
|
class="sv-switch sv-switch--{size}"
|
|
46
43
|
class:is-on={checked}
|
|
47
|
-
|
|
48
|
-
aria-label={ariaLabel}
|
|
49
|
-
{disabled}
|
|
50
|
-
onclick={toggle}
|
|
51
|
-
onkeydown={onKeydown}
|
|
44
|
+
{...sw.switchProps()}
|
|
52
45
|
>
|
|
53
46
|
<span class="sv-switch__track">
|
|
54
47
|
{#if onLabel || offLabel}
|
package/src/SvTabs.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
export type
|
|
2
|
+
export type { TabItem } from './createTabs.svelte'
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* SvTabs - a WAI-ARIA tabs widget with roving tabindex + arrow-key navigation.
|
|
8
8
|
* Parity: Smart `smart-tabs`. Controlled via `value` + `onChange`; the active
|
|
9
9
|
* panel is rendered through the `panel` snippet (receives the active id).
|
|
10
|
+
*
|
|
11
|
+
* The behavior (roving focus, activation, ARIA) lives in the headless
|
|
12
|
+
* `createTabs` core; this component is just one styled renderer over it.
|
|
10
13
|
*/
|
|
11
14
|
import type { Snippet } from 'svelte'
|
|
15
|
+
import { createTabs, type TabItem } from './createTabs.svelte'
|
|
12
16
|
|
|
13
17
|
type Props = {
|
|
14
18
|
tabs: ReadonlyArray<TabItem>
|
|
@@ -34,58 +38,41 @@
|
|
|
34
38
|
ariaLabel,
|
|
35
39
|
}: Props = $props()
|
|
36
40
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const tabsCtl = createTabs({
|
|
42
|
+
tabs: () => tabs,
|
|
43
|
+
value: () => value,
|
|
44
|
+
onChange: (id) => { value = id; onChange?.(id) },
|
|
45
|
+
orientation: () => orientation,
|
|
46
|
+
activation: () => activation,
|
|
47
|
+
ariaLabel: () => ariaLabel,
|
|
48
|
+
})
|
|
49
|
+
const active = $derived(tabsCtl.activeId)
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
function onKeydown(e: KeyboardEvent) {
|
|
51
|
-
const idx = enabled.findIndex((t) => t.id === active)
|
|
52
|
-
let next: string | null = null
|
|
53
|
-
const fwd = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
|
|
54
|
-
const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
|
|
55
|
-
if (e.key === fwd) next = enabled[(idx + 1) % enabled.length]?.id ?? null
|
|
56
|
-
else if (e.key === back) next = enabled[(idx - 1 + enabled.length) % enabled.length]?.id ?? null
|
|
57
|
-
else if (e.key === 'Home') next = enabled[0]?.id ?? null
|
|
58
|
-
else if (e.key === 'End') next = enabled.at(-1)?.id ?? null
|
|
59
|
-
else if ((e.key === 'Enter' || e.key === ' ') && activation === 'manual') { e.preventDefault(); select(active); return }
|
|
60
|
-
else return
|
|
61
|
-
if (next) {
|
|
62
|
-
e.preventDefault()
|
|
63
|
-
focusTab(next)
|
|
64
|
-
if (activation === 'automatic') select(next)
|
|
51
|
+
// DOM focus movement is a render concern: follow the core's roving focus target.
|
|
52
|
+
let tablistEl: HTMLDivElement | null = null
|
|
53
|
+
let lastFocusTick = 0
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (tabsCtl.focusTick !== lastFocusTick) {
|
|
56
|
+
lastFocusTick = tabsCtl.focusTick
|
|
57
|
+
const fid = tabsCtl.focusId
|
|
58
|
+
queueMicrotask(() => tablistEl?.querySelector<HTMLElement>(`[data-tab="${fid}"]`)?.focus())
|
|
65
59
|
}
|
|
66
|
-
}
|
|
60
|
+
})
|
|
67
61
|
</script>
|
|
68
62
|
|
|
69
63
|
<div class="sv-tabs sv-tabs--{orientation} sv-tabs--{variant}">
|
|
70
|
-
<div bind:this={tablistEl} class="sv-tabs__list"
|
|
64
|
+
<div bind:this={tablistEl} class="sv-tabs__list" {...tabsCtl.tablistProps()}>
|
|
71
65
|
{#each tabs as tab (tab.id)}
|
|
72
66
|
<button
|
|
73
67
|
type="button"
|
|
74
68
|
class="sv-tabs__tab"
|
|
75
69
|
class:is-active={tab.id === active}
|
|
76
|
-
|
|
77
|
-
data-tab={tab.id}
|
|
78
|
-
aria-selected={tab.id === active}
|
|
79
|
-
aria-controls={`panel-${tab.id}`}
|
|
80
|
-
tabindex={tab.id === active ? 0 : -1}
|
|
81
|
-
disabled={tab.disabled}
|
|
82
|
-
onclick={() => select(tab.id)}
|
|
83
|
-
onkeydown={onKeydown}
|
|
70
|
+
{...tabsCtl.tabProps(tab.id)}
|
|
84
71
|
>{tab.label}</button>
|
|
85
72
|
{/each}
|
|
86
73
|
</div>
|
|
87
74
|
{#if panel}
|
|
88
|
-
<div class="sv-tabs__panel"
|
|
75
|
+
<div class="sv-tabs__panel" {...tabsCtl.panelProps(active)}>
|
|
89
76
|
{@render panel(active)}
|
|
90
77
|
</div>
|
|
91
78
|
{/if}
|
package/src/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>
|