@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
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
value?: string;
|
|
3
|
+
onChange?: (value: string, parts: {
|
|
4
|
+
country: string;
|
|
5
|
+
dial: string;
|
|
6
|
+
national: string;
|
|
7
|
+
}) => void;
|
|
8
|
+
/** Default country ISO code. */
|
|
9
|
+
country?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
ariaLabel?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const SvPhoneInput: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type SvPhoneInput = ReturnType<typeof SvPhoneInput>;
|
|
19
|
+
export default SvPhoneInput;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type RadioOption = { value: string | number; label: string; disabled?: boolean }
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvRadioGroup - an accessible radio group (WAI-ARIA radiogroup pattern) with
|
|
8
|
+
* roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
|
|
9
|
+
* (as a managed group). Controlled via `value` + `onChange`.
|
|
10
|
+
*/
|
|
11
|
+
type Props = {
|
|
12
|
+
options: ReadonlyArray<RadioOption>
|
|
13
|
+
value?: string | number | null
|
|
14
|
+
onChange?: (value: string | number) => void
|
|
15
|
+
name?: string
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
orientation?: 'vertical' | 'horizontal'
|
|
18
|
+
size?: 'sm' | 'md' | 'lg'
|
|
19
|
+
ariaLabel?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
options,
|
|
24
|
+
value = null,
|
|
25
|
+
onChange,
|
|
26
|
+
name,
|
|
27
|
+
disabled = false,
|
|
28
|
+
orientation = 'vertical',
|
|
29
|
+
size = 'md',
|
|
30
|
+
ariaLabel,
|
|
31
|
+
}: Props = $props()
|
|
32
|
+
|
|
33
|
+
const enabledIdx = $derived(options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
|
|
34
|
+
const selectedIdx = $derived(options.findIndex((o) => o.value === value))
|
|
35
|
+
// The roving-tabindex target: the selected option, else the first enabled.
|
|
36
|
+
const focusIdx = $derived(selectedIdx >= 0 ? selectedIdx : enabledIdx[0] ?? -1)
|
|
37
|
+
|
|
38
|
+
function pick(i: number) {
|
|
39
|
+
const o = options[i]
|
|
40
|
+
if (!o || o.disabled || disabled) return
|
|
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
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
|
|
62
|
+
role="radiogroup"
|
|
63
|
+
aria-label={ariaLabel}
|
|
64
|
+
aria-disabled={disabled}
|
|
65
|
+
>
|
|
66
|
+
{#each options as opt, i (opt.value)}
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
role="radio"
|
|
70
|
+
class="sv-radio"
|
|
71
|
+
class:is-checked={opt.value === value}
|
|
72
|
+
aria-checked={opt.value === value}
|
|
73
|
+
disabled={disabled || opt.disabled}
|
|
74
|
+
tabindex={i === focusIdx ? 0 : -1}
|
|
75
|
+
onclick={() => pick(i)}
|
|
76
|
+
onkeydown={(e) => onKeydown(e, i)}
|
|
77
|
+
>
|
|
78
|
+
<span class="sv-radio__dot"></span>
|
|
79
|
+
<span class="sv-radio__label">{opt.label}</span>
|
|
80
|
+
</button>
|
|
81
|
+
{/each}
|
|
82
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.sv-radiogroup { --_accent: var(--sg-accent, #2563eb); display: flex; gap: 10px; }
|
|
87
|
+
.sv-radiogroup--vertical { flex-direction: column; }
|
|
88
|
+
.sv-radiogroup--horizontal { flex-direction: row; flex-wrap: wrap; }
|
|
89
|
+
.sv-radio {
|
|
90
|
+
display: inline-flex; align-items: center; gap: 8px; background: none; border: 0; padding: 2px;
|
|
91
|
+
font: inherit; color: var(--sg-fg, #0f172a); cursor: pointer; user-select: none;
|
|
92
|
+
}
|
|
93
|
+
.sv-radiogroup--sm .sv-radio { font-size: 12px; }
|
|
94
|
+
.sv-radiogroup--md .sv-radio { font-size: 13px; }
|
|
95
|
+
.sv-radiogroup--lg .sv-radio { font-size: 15px; }
|
|
96
|
+
.sv-radio[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
97
|
+
.sv-radio__dot {
|
|
98
|
+
width: 17px; height: 17px; flex: none; border-radius: 50%;
|
|
99
|
+
border: 1.5px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
|
|
100
|
+
display: grid; place-items: center; transition: border-color 0.12s;
|
|
101
|
+
}
|
|
102
|
+
.sv-radio.is-checked .sv-radio__dot { border-color: var(--_accent); }
|
|
103
|
+
.sv-radio.is-checked .sv-radio__dot::after {
|
|
104
|
+
content: ''; width: 9px; height: 9px; border-radius: 50%; background: var(--_accent);
|
|
105
|
+
}
|
|
106
|
+
.sv-radio:focus-visible { outline: none; }
|
|
107
|
+
.sv-radio:focus-visible .sv-radio__dot { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RadioOption = {
|
|
2
|
+
value: string | number;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* SvRadioGroup - an accessible radio group (WAI-ARIA radiogroup pattern) with
|
|
8
|
+
* roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
|
|
9
|
+
* (as a managed group). Controlled via `value` + `onChange`.
|
|
10
|
+
*/
|
|
11
|
+
type Props = {
|
|
12
|
+
options: ReadonlyArray<RadioOption>;
|
|
13
|
+
value?: string | number | null;
|
|
14
|
+
onChange?: (value: string | number) => void;
|
|
15
|
+
name?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
orientation?: 'vertical' | 'horizontal';
|
|
18
|
+
size?: 'sm' | 'md' | 'lg';
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
};
|
|
21
|
+
declare const SvRadioGroup: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type SvRadioGroup = ReturnType<typeof SvRadioGroup>;
|
|
23
|
+
export default SvRadioGroup;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvRating - a star rating control (WAI-ARIA slider). Parity: Smart
|
|
4
|
+
* `smart-rating`. Hover preview, keyboard, optional half steps, readOnly.
|
|
5
|
+
* Uses the grid's `--sg-rating-*` tokens so it matches the in-grid editor.
|
|
6
|
+
*/
|
|
7
|
+
type Props = {
|
|
8
|
+
value?: number
|
|
9
|
+
onChange?: (value: number) => void
|
|
10
|
+
max?: number
|
|
11
|
+
/** Allow half-star precision. */
|
|
12
|
+
allowHalf?: boolean
|
|
13
|
+
readOnly?: boolean
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
size?: 'sm' | 'md' | 'lg'
|
|
16
|
+
name?: string
|
|
17
|
+
ariaLabel?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
value = 0,
|
|
22
|
+
onChange,
|
|
23
|
+
max = 5,
|
|
24
|
+
allowHalf = false,
|
|
25
|
+
readOnly = false,
|
|
26
|
+
disabled = false,
|
|
27
|
+
size = 'md',
|
|
28
|
+
name,
|
|
29
|
+
ariaLabel,
|
|
30
|
+
}: Props = $props()
|
|
31
|
+
|
|
32
|
+
let hover = $state<number | null>(null)
|
|
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
|
+
}
|
|
48
|
+
|
|
49
|
+
function onKeydown(e: KeyboardEvent) {
|
|
50
|
+
if (!isInteractive) return
|
|
51
|
+
const step = allowHalf ? 0.5 : 1
|
|
52
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); onChange?.(Math.min(max, value + step)) }
|
|
53
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); onChange?.(Math.max(0, value - step)) }
|
|
54
|
+
else if (e.key === 'Home') { e.preventDefault(); onChange?.(0) }
|
|
55
|
+
else if (e.key === 'End') { e.preventDefault(); onChange?.(max) }
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<div
|
|
60
|
+
class="sv-rating sv-rating--{size}"
|
|
61
|
+
class:is-readonly={readOnly}
|
|
62
|
+
class:is-disabled={disabled}
|
|
63
|
+
role="slider"
|
|
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)}
|
|
72
|
+
>
|
|
73
|
+
{#each Array(max) as _, i (i)}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="sv-rating__star is-{fillOf(i)}"
|
|
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)) }}
|
|
82
|
+
>
|
|
83
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
84
|
+
<defs>
|
|
85
|
+
<linearGradient id="half-{i}"><stop offset="50%" stop-color="var(--_on)" /><stop offset="50%" stop-color="var(--_off)" /></linearGradient>
|
|
86
|
+
</defs>
|
|
87
|
+
<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)'} />
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
{/each}
|
|
92
|
+
{#if name}<input type="hidden" {name} value={value} />{/if}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<style>
|
|
96
|
+
.sv-rating {
|
|
97
|
+
--_on: var(--sg-rating-on, #f59e0b);
|
|
98
|
+
--_off: var(--sg-rating-empty, #d1d5db);
|
|
99
|
+
--_sz: 22px;
|
|
100
|
+
display: inline-flex; gap: 2px; align-items: center;
|
|
101
|
+
}
|
|
102
|
+
.sv-rating--sm { --_sz: 16px; }
|
|
103
|
+
.sv-rating--lg { --_sz: 30px; }
|
|
104
|
+
.sv-rating.is-disabled { opacity: 0.5; }
|
|
105
|
+
.sv-rating__star {
|
|
106
|
+
width: var(--_sz); height: var(--_sz); padding: 0; border: 0; background: none; cursor: pointer; line-height: 0;
|
|
107
|
+
}
|
|
108
|
+
.sv-rating.is-readonly .sv-rating__star, .sv-rating.is-disabled .sv-rating__star { cursor: default; }
|
|
109
|
+
.sv-rating__star svg { width: 100%; height: 100%; }
|
|
110
|
+
.sv-rating:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: 2px; border-radius: 4px; }
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvRating - a star rating control (WAI-ARIA slider). Parity: Smart
|
|
3
|
+
* `smart-rating`. Hover preview, keyboard, optional half steps, readOnly.
|
|
4
|
+
* Uses the grid's `--sg-rating-*` tokens so it matches the in-grid editor.
|
|
5
|
+
*/
|
|
6
|
+
type Props = {
|
|
7
|
+
value?: number;
|
|
8
|
+
onChange?: (value: number) => void;
|
|
9
|
+
max?: number;
|
|
10
|
+
/** Allow half-star precision. */
|
|
11
|
+
allowHalf?: boolean;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
name?: string;
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
};
|
|
18
|
+
declare const SvRating: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type SvRating = ReturnType<typeof SvRating>;
|
|
20
|
+
export default SvRating;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvRepeatButton - fires `onclick` repeatedly while held down (after an
|
|
4
|
+
* initial delay, then at a steady interval). Parity: Smart `smart-repeat-button`.
|
|
5
|
+
* Used for spinners / steppers.
|
|
6
|
+
*/
|
|
7
|
+
import type { Snippet } from 'svelte'
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
onclick?: () => void
|
|
11
|
+
/** Delay before repeats begin (ms). */
|
|
12
|
+
delay?: number
|
|
13
|
+
/** Interval between repeats (ms). */
|
|
14
|
+
interval?: number
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
size?: 'sm' | 'md' | 'lg'
|
|
17
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
|
|
18
|
+
ariaLabel?: string
|
|
19
|
+
children?: Snippet
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
onclick,
|
|
24
|
+
delay = 300,
|
|
25
|
+
interval = 60,
|
|
26
|
+
disabled = false,
|
|
27
|
+
size = 'md',
|
|
28
|
+
variant = 'secondary',
|
|
29
|
+
ariaLabel,
|
|
30
|
+
children,
|
|
31
|
+
}: Props = $props()
|
|
32
|
+
|
|
33
|
+
let delayTimer: ReturnType<typeof setTimeout> | null = null
|
|
34
|
+
let repeatTimer: ReturnType<typeof setInterval> | null = null
|
|
35
|
+
|
|
36
|
+
function stop() {
|
|
37
|
+
if (delayTimer) { clearTimeout(delayTimer); delayTimer = null }
|
|
38
|
+
if (repeatTimer) { clearInterval(repeatTimer); repeatTimer = null }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function start(e: PointerEvent) {
|
|
42
|
+
if (disabled || e.button !== 0) return
|
|
43
|
+
onclick?.() // fire once immediately
|
|
44
|
+
delayTimer = setTimeout(() => {
|
|
45
|
+
repeatTimer = setInterval(() => onclick?.(), Math.max(16, interval))
|
|
46
|
+
}, Math.max(0, delay))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onKeydown(e: KeyboardEvent) {
|
|
50
|
+
if (disabled) return
|
|
51
|
+
if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) { e.preventDefault(); onclick?.() }
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<svelte:window onpointerup={stop} onpointercancel={stop} />
|
|
56
|
+
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
class="sv-btn sv-btn--{variant} sv-btn--{size} sv-repeat"
|
|
60
|
+
{disabled}
|
|
61
|
+
aria-label={ariaLabel}
|
|
62
|
+
onpointerdown={start}
|
|
63
|
+
onpointerleave={stop}
|
|
64
|
+
onkeydown={onKeydown}
|
|
65
|
+
>
|
|
66
|
+
{#if children}{@render children()}{/if}
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
/* Reuses the SvButton visual language (kept local so it works standalone). */
|
|
71
|
+
.sv-btn {
|
|
72
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
73
|
+
--_border: var(--sg-border, #cbd5e1);
|
|
74
|
+
--_radius: var(--sg-radius, 8px);
|
|
75
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
76
|
+
font: inherit; font-weight: 600; line-height: 1; border: 1px solid transparent;
|
|
77
|
+
border-radius: var(--_radius); cursor: pointer; user-select: none; touch-action: none;
|
|
78
|
+
}
|
|
79
|
+
.sv-btn--sm { padding: 5px 9px; font-size: 12px; }
|
|
80
|
+
.sv-btn--md { padding: 8px 12px; font-size: 13px; }
|
|
81
|
+
.sv-btn--lg { padding: 11px 16px; font-size: 15px; }
|
|
82
|
+
.sv-btn[disabled] { opacity: 0.55; cursor: not-allowed; }
|
|
83
|
+
.sv-btn--primary { background: var(--_accent); color: var(--sg-on-accent, #fff); }
|
|
84
|
+
.sv-btn--secondary { background: var(--sg-header-bg, #f1f5f9); color: var(--sg-fg, #0f172a); border-color: var(--_border); }
|
|
85
|
+
.sv-btn--outline { background: transparent; color: var(--_accent); border-color: var(--_accent); }
|
|
86
|
+
.sv-btn--ghost { background: transparent; color: var(--sg-fg, #0f172a); }
|
|
87
|
+
.sv-btn:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
88
|
+
.sv-btn:active { transform: translateY(0.5px); }
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvRepeatButton - fires `onclick` repeatedly while held down (after an
|
|
3
|
+
* initial delay, then at a steady interval). Parity: Smart `smart-repeat-button`.
|
|
4
|
+
* Used for spinners / steppers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
type Props = {
|
|
8
|
+
onclick?: () => void;
|
|
9
|
+
/** Delay before repeats begin (ms). */
|
|
10
|
+
delay?: number;
|
|
11
|
+
/** Interval between repeats (ms). */
|
|
12
|
+
interval?: number;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
};
|
|
19
|
+
declare const SvRepeatButton: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type SvRepeatButton = ReturnType<typeof SvRepeatButton>;
|
|
21
|
+
export default SvRepeatButton;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvSlider - a single or range slider (WAI-ARIA slider) with steps, ticks,
|
|
4
|
+
* keyboard + pointer drag. Parity: Smart `smart-slider`. Single emits number;
|
|
5
|
+
* range emits [number, number].
|
|
6
|
+
*/
|
|
7
|
+
type Props = {
|
|
8
|
+
value?: number | [number, number]
|
|
9
|
+
onChange?: (value: any) => void
|
|
10
|
+
min?: number
|
|
11
|
+
max?: number
|
|
12
|
+
step?: number
|
|
13
|
+
range?: boolean
|
|
14
|
+
/** Show evenly spaced tick marks (count or explicit values). */
|
|
15
|
+
ticks?: number | number[]
|
|
16
|
+
/** Show the value bubble above the thumb. */
|
|
17
|
+
showValue?: boolean
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
orientation?: 'horizontal' | 'vertical'
|
|
20
|
+
name?: string
|
|
21
|
+
ariaLabel?: string
|
|
22
|
+
formatValue?: (v: number) => string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
value = 0,
|
|
27
|
+
onChange,
|
|
28
|
+
min = 0,
|
|
29
|
+
max = 100,
|
|
30
|
+
step = 1,
|
|
31
|
+
range = false,
|
|
32
|
+
ticks,
|
|
33
|
+
showValue = false,
|
|
34
|
+
disabled = false,
|
|
35
|
+
orientation = 'horizontal',
|
|
36
|
+
name,
|
|
37
|
+
ariaLabel,
|
|
38
|
+
formatValue,
|
|
39
|
+
}: Props = $props()
|
|
40
|
+
|
|
41
|
+
const isVert = $derived(orientation === 'vertical')
|
|
42
|
+
const lo = $derived(range ? (value as [number, number])[0] : min)
|
|
43
|
+
const hi = $derived(range ? (value as [number, number])[1] : (value as number))
|
|
44
|
+
|
|
45
|
+
function clampStep(v: number): number {
|
|
46
|
+
const stepped = Math.round((v - min) / step) * step + min
|
|
47
|
+
return Math.min(max, Math.max(min, Number(stepped.toFixed(10))))
|
|
48
|
+
}
|
|
49
|
+
const pct = (v: number) => ((v - min) / (max - min)) * 100
|
|
50
|
+
|
|
51
|
+
const tickValues = $derived.by<number[]>(() => {
|
|
52
|
+
if (!ticks) return []
|
|
53
|
+
if (Array.isArray(ticks)) return ticks
|
|
54
|
+
return Array.from({ length: ticks }, (_, i) => min + ((max - min) / (ticks - 1)) * i)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
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
|
+
function onTrackDown(e: PointerEvent) {
|
|
72
|
+
if (disabled) return
|
|
73
|
+
const v = valueFromPointer(e)
|
|
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
|
+
}
|
|
82
|
+
trackEl?.setPointerCapture(e.pointerId)
|
|
83
|
+
}
|
|
84
|
+
function onMove(e: PointerEvent) {
|
|
85
|
+
if (!dragging) return
|
|
86
|
+
const v = valueFromPointer(e)
|
|
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)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fmt = (v: number) => (formatValue ? formatValue(v) : String(v))
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
class="sv-slider sv-slider--{orientation}"
|
|
115
|
+
class:is-disabled={disabled}
|
|
116
|
+
aria-label={ariaLabel}
|
|
117
|
+
>
|
|
118
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
119
|
+
<div
|
|
120
|
+
bind:this={trackEl}
|
|
121
|
+
class="sv-slider__track"
|
|
122
|
+
onpointerdown={onTrackDown}
|
|
123
|
+
onpointermove={onMove}
|
|
124
|
+
onpointerup={onUp}
|
|
125
|
+
onpointercancel={onUp}
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
class="sv-slider__fill"
|
|
129
|
+
style:--a={`${range ? pct(lo) : 0}%`}
|
|
130
|
+
style:--b={`${range ? pct(hi) : pct(hi)}%`}
|
|
131
|
+
></div>
|
|
132
|
+
{#each tickValues as t (t)}
|
|
133
|
+
<span class="sv-slider__tick" style:--p={`${pct(t)}%`}></span>
|
|
134
|
+
{/each}
|
|
135
|
+
|
|
136
|
+
{#if range}
|
|
137
|
+
<div class="sv-slider__thumb" style:--p={`${pct(lo)}%`} role="slider" tabindex={disabled ? -1 : 0} aria-valuemin={min} aria-valuemax={hi} aria-valuenow={lo} aria-label="Minimum" onkeydown={(e) => onThumbKey(e, 'lo')}>
|
|
138
|
+
{#if showValue}<span class="sv-slider__bubble">{fmt(lo)}</span>{/if}
|
|
139
|
+
</div>
|
|
140
|
+
<div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} role="slider" tabindex={disabled ? -1 : 0} aria-valuemin={lo} aria-valuemax={max} aria-valuenow={hi} aria-label="Maximum" onkeydown={(e) => onThumbKey(e, 'hi')}>
|
|
141
|
+
{#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
|
|
142
|
+
</div>
|
|
143
|
+
{:else}
|
|
144
|
+
<div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} role="slider" tabindex={disabled ? -1 : 0} aria-valuemin={min} aria-valuemax={max} aria-valuenow={hi} aria-label={ariaLabel ?? 'Value'} onkeydown={(e) => onThumbKey(e, 'single')}>
|
|
145
|
+
{#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
{#if name}<input type="hidden" {name} value={range ? `${lo},${hi}` : hi} />{/if}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<style>
|
|
153
|
+
.sv-slider { --_accent: var(--sg-accent, #2563eb); --_track: var(--sg-border, #e2e8f0); padding: 10px; }
|
|
154
|
+
.sv-slider--horizontal { width: 240px; }
|
|
155
|
+
.sv-slider--vertical { height: 200px; width: 40px; }
|
|
156
|
+
.sv-slider.is-disabled { opacity: 0.5; pointer-events: none; }
|
|
157
|
+
.sv-slider__track {
|
|
158
|
+
position: relative; background: var(--_track); border-radius: 999px; cursor: pointer; touch-action: none;
|
|
159
|
+
}
|
|
160
|
+
.sv-slider--horizontal .sv-slider__track { height: 6px; }
|
|
161
|
+
.sv-slider--vertical .sv-slider__track { width: 6px; height: 100%; margin: 0 auto; }
|
|
162
|
+
.sv-slider__fill { position: absolute; background: var(--_accent); border-radius: 999px; }
|
|
163
|
+
.sv-slider--horizontal .sv-slider__fill { left: var(--a); right: calc(100% - var(--b)); top: 0; bottom: 0; }
|
|
164
|
+
.sv-slider--vertical .sv-slider__fill { bottom: var(--a); top: calc(100% - var(--b)); left: 0; right: 0; }
|
|
165
|
+
.sv-slider__thumb {
|
|
166
|
+
position: absolute; width: 16px; height: 16px; border-radius: 50%;
|
|
167
|
+
background: #fff; border: 2px solid var(--_accent); box-shadow: 0 1px 3px rgba(0,0,0,0.25);
|
|
168
|
+
cursor: grab; touch-action: none;
|
|
169
|
+
}
|
|
170
|
+
.sv-slider--horizontal .sv-slider__thumb { left: var(--p); top: 50%; transform: translate(-50%, -50%); }
|
|
171
|
+
.sv-slider--vertical .sv-slider__thumb { bottom: var(--p); left: 50%; transform: translate(-50%, 50%); }
|
|
172
|
+
.sv-slider__thumb:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
173
|
+
.sv-slider__thumb:active { cursor: grabbing; }
|
|
174
|
+
.sv-slider__bubble {
|
|
175
|
+
position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%);
|
|
176
|
+
background: var(--_accent); color: var(--sg-on-accent, #fff); font-size: 11px; font-weight: 600;
|
|
177
|
+
padding: 2px 6px; border-radius: 5px; white-space: nowrap;
|
|
178
|
+
}
|
|
179
|
+
.sv-slider__tick { position: absolute; width: 2px; height: 2px; border-radius: 50%; background: var(--sg-muted, #94a3b8); }
|
|
180
|
+
.sv-slider--horizontal .sv-slider__tick { left: var(--p); top: 50%; transform: translate(-50%, -50%); }
|
|
181
|
+
.sv-slider--vertical .sv-slider__tick { bottom: var(--p); left: 50%; transform: translate(-50%, 50%); }
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvSlider - a single or range slider (WAI-ARIA slider) with steps, ticks,
|
|
3
|
+
* keyboard + pointer drag. Parity: Smart `smart-slider`. Single emits number;
|
|
4
|
+
* range emits [number, number].
|
|
5
|
+
*/
|
|
6
|
+
type Props = {
|
|
7
|
+
value?: number | [number, number];
|
|
8
|
+
onChange?: (value: any) => void;
|
|
9
|
+
min?: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
step?: number;
|
|
12
|
+
range?: boolean;
|
|
13
|
+
/** Show evenly spaced tick marks (count or explicit values). */
|
|
14
|
+
ticks?: number | number[];
|
|
15
|
+
/** Show the value bubble above the thumb. */
|
|
16
|
+
showValue?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
orientation?: 'horizontal' | 'vertical';
|
|
19
|
+
name?: string;
|
|
20
|
+
ariaLabel?: string;
|
|
21
|
+
formatValue?: (v: number) => string;
|
|
22
|
+
};
|
|
23
|
+
declare const SvSlider: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type SvSlider = ReturnType<typeof SvSlider>;
|
|
25
|
+
export default SvSlider;
|