@svgrid/grid 1.2.22 → 1.2.24
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 +89 -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 +409 -0
- package/dist/SvCalendar.svelte.d.ts +52 -0
- package/dist/SvCheckBox.svelte +82 -0
- package/dist/SvCheckBox.svelte.d.ts +19 -0
- package/dist/SvColorInput.svelte +160 -0
- package/dist/SvColorInput.svelte.d.ts +15 -0
- package/dist/SvComboBox.svelte +108 -0
- package/dist/SvComboBox.svelte.d.ts +15 -0
- package/dist/SvCountryInput.svelte +125 -0
- package/dist/SvCountryInput.svelte.d.ts +14 -0
- package/dist/SvDateTimePicker.svelte +265 -0
- package/dist/SvDateTimePicker.svelte.d.ts +38 -0
- package/dist/SvDropDownList.svelte +120 -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 +97 -0
- package/dist/SvGauge.svelte.d.ts +26 -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 +89 -0
- package/dist/SvListBox.svelte.d.ts +22 -0
- package/dist/SvMaskedInput.svelte +66 -0
- package/dist/SvMaskedInput.svelte.d.ts +15 -0
- package/dist/SvNumberInput.svelte +116 -0
- package/dist/SvNumberInput.svelte.d.ts +18 -0
- package/dist/SvPasswordInput.svelte +100 -0
- package/dist/SvPasswordInput.svelte.d.ts +18 -0
- package/dist/SvPhoneInput.svelte +87 -0
- package/dist/SvPhoneInput.svelte.d.ts +19 -0
- package/dist/SvRadioGroup.svelte +85 -0
- package/dist/SvRadioGroup.svelte.d.ts +20 -0
- package/dist/SvRating.svelte +91 -0
- package/dist/SvRating.svelte.d.ts +18 -0
- package/dist/SvRepeatButton.svelte +89 -0
- package/dist/SvRepeatButton.svelte.d.ts +21 -0
- package/dist/SvSlider.svelte +155 -0
- package/dist/SvSlider.svelte.d.ts +20 -0
- package/dist/SvSwitchButton.svelte +87 -0
- package/dist/SvSwitchButton.svelte.d.ts +14 -0
- package/dist/SvTabs.svelte +109 -0
- package/dist/SvTabs.svelte.d.ts +26 -0
- package/dist/SvTagsInput.svelte +70 -0
- package/dist/SvTagsInput.svelte.d.ts +14 -0
- package/dist/SvTimePicker.svelte +201 -0
- package/dist/SvTimePicker.svelte.d.ts +30 -0
- package/dist/SvToggleButton.svelte +57 -0
- package/dist/SvToggleButton.svelte.d.ts +16 -0
- package/dist/SvTree.svelte +131 -0
- package/dist/SvTree.svelte.d.ts +27 -0
- package/dist/build-api.js +78 -1
- package/dist/cdn/svgrid.js +11674 -5907
- package/dist/cdn/svgrid.svelte-external.js +10436 -4671
- 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/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/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/editor-contract.d.ts +48 -0
- package/dist/editor-contract.js +26 -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 +60 -0
- package/dist/index.js +72 -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 +89 -0
- package/src/SvButton.svelte +97 -0
- package/src/SvCalendar.svelte +409 -0
- package/src/SvCalendar.test.ts +211 -0
- package/src/SvCheckBox.svelte +82 -0
- package/src/SvColorInput.svelte +160 -0
- package/src/SvComboBox.svelte +108 -0
- package/src/SvCountryInput.svelte +125 -0
- package/src/SvDateTimePicker.svelte +265 -0
- package/src/SvDateTimePicker.test.ts +136 -0
- package/src/SvDropDownList.svelte +120 -0
- package/src/SvForm.svelte +147 -0
- package/src/SvGauge.svelte +97 -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 +89 -0
- package/src/SvMaskedInput.svelte +66 -0
- package/src/SvNumberInput.svelte +116 -0
- package/src/SvPasswordInput.svelte +100 -0
- package/src/SvPhoneInput.svelte +87 -0
- package/src/SvRadioGroup.svelte +85 -0
- package/src/SvRating.svelte +91 -0
- package/src/SvRepeatButton.svelte +89 -0
- package/src/SvSlider.svelte +155 -0
- package/src/SvSwitchButton.svelte +87 -0
- package/src/SvTabs.svelte +109 -0
- package/src/SvTagsInput.svelte +70 -0
- package/src/SvTimePicker.svelte +201 -0
- package/src/SvTimePicker.test.ts +98 -0
- package/src/SvToggleButton.svelte +57 -0
- package/src/SvTree.svelte +131 -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/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/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/editor-contract.test.ts +32 -0
- package/src/editor-contract.ts +60 -0
- 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 +110 -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 +118 -0
- package/src/ui-range.test.ts +61 -0
- package/src/ui-selection.test.ts +119 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
let uid = 0;
|
|
2
|
+
/** Normalize a listbox value to an array (pure). */
|
|
3
|
+
export function toSelectedArray(value, multiple) {
|
|
4
|
+
if (multiple)
|
|
5
|
+
return Array.isArray(value) ? [...value] : value == null ? [] : [value];
|
|
6
|
+
return value == null ? [] : [value];
|
|
7
|
+
}
|
|
8
|
+
export function createListbox(config) {
|
|
9
|
+
const id = `sv-lb-${uid++}`;
|
|
10
|
+
const opts = () => config.options();
|
|
11
|
+
const multiple = () => config.multiple?.() ?? false;
|
|
12
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
13
|
+
let active = $state(0);
|
|
14
|
+
const selected = $derived(toSelectedArray(config.value(), multiple()));
|
|
15
|
+
const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0));
|
|
16
|
+
// Keep the active index on an enabled option as options change.
|
|
17
|
+
$effect(() => {
|
|
18
|
+
const o = opts()[active];
|
|
19
|
+
if (!o || o.disabled)
|
|
20
|
+
active = enabledIdx[0] ?? 0;
|
|
21
|
+
});
|
|
22
|
+
const isSelected = (value) => selected.includes(value);
|
|
23
|
+
const isActive = (index) => index === active;
|
|
24
|
+
const optionId = (index) => `${id}-opt-${index}`;
|
|
25
|
+
function setActive(index) {
|
|
26
|
+
const o = opts()[index];
|
|
27
|
+
if (o && !o.disabled)
|
|
28
|
+
active = index;
|
|
29
|
+
}
|
|
30
|
+
function pick(index) {
|
|
31
|
+
const o = opts()[index];
|
|
32
|
+
if (!o || o.disabled || disabled())
|
|
33
|
+
return;
|
|
34
|
+
active = index;
|
|
35
|
+
if (multiple()) {
|
|
36
|
+
const set = new Set(selected);
|
|
37
|
+
set.has(o.value) ? set.delete(o.value) : set.add(o.value);
|
|
38
|
+
config.onChange?.([...set]);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
config.onChange?.(o.value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function move(delta) {
|
|
45
|
+
const list = enabledIdx;
|
|
46
|
+
if (!list.length)
|
|
47
|
+
return;
|
|
48
|
+
const pos = list.indexOf(active);
|
|
49
|
+
const next = list[(pos + delta + list.length) % list.length];
|
|
50
|
+
if (next != null)
|
|
51
|
+
active = next;
|
|
52
|
+
}
|
|
53
|
+
const first = () => { active = enabledIdx[0] ?? 0; };
|
|
54
|
+
const last = () => { active = enabledIdx.at(-1) ?? 0; };
|
|
55
|
+
function onKeydown(e) {
|
|
56
|
+
if (disabled())
|
|
57
|
+
return;
|
|
58
|
+
switch (e.key) {
|
|
59
|
+
case 'ArrowDown':
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
move(1);
|
|
62
|
+
break;
|
|
63
|
+
case 'ArrowUp':
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
move(-1);
|
|
66
|
+
break;
|
|
67
|
+
case 'Home':
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
first();
|
|
70
|
+
break;
|
|
71
|
+
case 'End':
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
last();
|
|
74
|
+
break;
|
|
75
|
+
case ' ':
|
|
76
|
+
case 'Enter':
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
pick(active);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
get activeIndex() { return active; },
|
|
84
|
+
get selectedValues() { return selected; },
|
|
85
|
+
isSelected,
|
|
86
|
+
isActive,
|
|
87
|
+
setActive,
|
|
88
|
+
pick,
|
|
89
|
+
move,
|
|
90
|
+
first,
|
|
91
|
+
last,
|
|
92
|
+
onKeydown,
|
|
93
|
+
rootProps: () => ({
|
|
94
|
+
role: 'listbox',
|
|
95
|
+
'aria-multiselectable': multiple(),
|
|
96
|
+
'aria-label': config.ariaLabel?.(),
|
|
97
|
+
'aria-disabled': disabled(),
|
|
98
|
+
'aria-activedescendant': opts()[active] ? optionId(active) : undefined,
|
|
99
|
+
tabindex: disabled() ? -1 : 0,
|
|
100
|
+
onkeydown: onKeydown,
|
|
101
|
+
}),
|
|
102
|
+
optionProps: (index) => {
|
|
103
|
+
const o = opts()[index];
|
|
104
|
+
return {
|
|
105
|
+
role: 'option',
|
|
106
|
+
id: optionId(index),
|
|
107
|
+
'aria-selected': o ? isSelected(o.value) : false,
|
|
108
|
+
'aria-disabled': o?.disabled,
|
|
109
|
+
'data-idx': index,
|
|
110
|
+
'data-active': isActive(index) ? '' : undefined,
|
|
111
|
+
'data-selected': o && isSelected(o.value) ? '' : undefined,
|
|
112
|
+
onclick: () => pick(index),
|
|
113
|
+
onpointermove: () => setActive(index),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
2
|
+
export type MaskedInputConfig = {
|
|
3
|
+
value: () => string;
|
|
4
|
+
/** Called with (masked, raw, complete). */
|
|
5
|
+
onChange?: (masked: string, raw: string, complete: boolean) => void;
|
|
6
|
+
mask?: () => string;
|
|
7
|
+
placeholder?: () => string | undefined;
|
|
8
|
+
disabled?: () => boolean;
|
|
9
|
+
readonly?: () => boolean;
|
|
10
|
+
ariaLabel?: () => string | undefined;
|
|
11
|
+
};
|
|
12
|
+
export declare function createMaskedInput(config: MaskedInputConfig): {
|
|
13
|
+
/** The current masked display text. */
|
|
14
|
+
readonly masked: string;
|
|
15
|
+
/** The raw (unmasked) data characters. */
|
|
16
|
+
readonly raw: string;
|
|
17
|
+
/** Whether every token position in the mask is filled. */
|
|
18
|
+
readonly complete: boolean;
|
|
19
|
+
/** Spread onto the text input element. */
|
|
20
|
+
inputProps: () => {
|
|
21
|
+
value: string;
|
|
22
|
+
type: "text";
|
|
23
|
+
placeholder: string;
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
readonly: boolean;
|
|
26
|
+
'aria-label': string | undefined;
|
|
27
|
+
oninput: (e: Event) => void;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export type MaskedInputCore = ReturnType<typeof createMaskedInput>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createMaskedInput - the HEADLESS core behind <SvMaskedInput>: a runes-based
|
|
3
|
+
* state machine that owns the pattern-mask formatting (#=digit, A=letter,
|
|
4
|
+
* *=alnum; other chars are literals) via the pure `./datetime/mask` engine,
|
|
5
|
+
* exposed as **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script lang="ts">
|
|
9
|
+
* import { createMaskedInput } from '@svgrid/grid'
|
|
10
|
+
* let value = $state('')
|
|
11
|
+
* const mi = createMaskedInput({ value: () => value, mask: () => '(###) ###-####', onChange: (m) => (value = m) })
|
|
12
|
+
* </script>
|
|
13
|
+
* <input {...mi.inputProps()} />
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The styled <SvMaskedInput> is just one renderer over this core.
|
|
17
|
+
*/
|
|
18
|
+
import { applyMask, unmask, isMaskComplete } from './datetime/mask';
|
|
19
|
+
export function createMaskedInput(config) {
|
|
20
|
+
const mask = () => config.mask?.() ?? '';
|
|
21
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
22
|
+
const readonly = () => config.readonly?.() ?? false;
|
|
23
|
+
let text = $state('');
|
|
24
|
+
let lastRaw = '';
|
|
25
|
+
// Reflect controlled `value` into the masked display text.
|
|
26
|
+
$effect(() => {
|
|
27
|
+
const m = mask();
|
|
28
|
+
const raw = m ? unmask(config.value(), m) : config.value();
|
|
29
|
+
if (raw !== lastRaw) {
|
|
30
|
+
lastRaw = raw;
|
|
31
|
+
text = m ? applyMask(raw, m).formatted : config.value();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
function onInput(e) {
|
|
35
|
+
const el = e.currentTarget;
|
|
36
|
+
const m = mask();
|
|
37
|
+
const raw = m ? unmask(el.value, m) : el.value;
|
|
38
|
+
const masked = m ? applyMask(raw, m).formatted : el.value;
|
|
39
|
+
lastRaw = raw;
|
|
40
|
+
text = masked;
|
|
41
|
+
el.value = masked;
|
|
42
|
+
config.onChange?.(masked, raw, m ? isMaskComplete(masked, m) : true);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
/** The current masked display text. */
|
|
46
|
+
get masked() { return text; },
|
|
47
|
+
/** The raw (unmasked) data characters. */
|
|
48
|
+
get raw() { return mask() ? unmask(text, mask()) : text; },
|
|
49
|
+
/** Whether every token position in the mask is filled. */
|
|
50
|
+
get complete() { return mask() ? isMaskComplete(text, mask()) : true; },
|
|
51
|
+
/** Spread onto the text input element. */
|
|
52
|
+
inputProps: () => ({
|
|
53
|
+
value: text,
|
|
54
|
+
type: 'text',
|
|
55
|
+
placeholder: config.placeholder?.() ?? mask(),
|
|
56
|
+
disabled: disabled(),
|
|
57
|
+
readonly: readonly(),
|
|
58
|
+
'aria-label': config.ariaLabel?.(),
|
|
59
|
+
oninput: onInput,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
2
|
+
export type NumberInputConfig = {
|
|
3
|
+
value: () => number | null;
|
|
4
|
+
onChange?: (value: number | null) => void;
|
|
5
|
+
min?: () => number;
|
|
6
|
+
max?: () => number;
|
|
7
|
+
step?: () => number;
|
|
8
|
+
precision?: () => number | undefined;
|
|
9
|
+
/** Group thousands (1,234) on display. */
|
|
10
|
+
grouping?: () => boolean;
|
|
11
|
+
prefix?: () => string;
|
|
12
|
+
suffix?: () => string;
|
|
13
|
+
placeholder?: () => string | undefined;
|
|
14
|
+
disabled?: () => boolean;
|
|
15
|
+
readonly?: () => boolean;
|
|
16
|
+
id?: () => string | undefined;
|
|
17
|
+
invalid?: () => boolean;
|
|
18
|
+
required?: () => boolean;
|
|
19
|
+
error?: () => string | undefined;
|
|
20
|
+
ariaLabel?: () => string | undefined;
|
|
21
|
+
};
|
|
22
|
+
export declare function createNumberInput(config: NumberInputConfig): {
|
|
23
|
+
/** The current display text (bind this to your input's value). */
|
|
24
|
+
readonly text: string;
|
|
25
|
+
/** Whether the field is being edited. */
|
|
26
|
+
readonly focused: boolean;
|
|
27
|
+
/** Whether interaction is allowed (not disabled and not readonly). */
|
|
28
|
+
readonly isInteractive: boolean;
|
|
29
|
+
commit: () => void;
|
|
30
|
+
bump: (dir: 1 | -1) => void;
|
|
31
|
+
increment: () => void;
|
|
32
|
+
decrement: () => void;
|
|
33
|
+
/** Spread onto the text input element. */
|
|
34
|
+
inputProps: () => {
|
|
35
|
+
oninput: (e: Event) => void;
|
|
36
|
+
onfocus: () => void;
|
|
37
|
+
onblur: () => void;
|
|
38
|
+
onkeydown: (e: KeyboardEvent) => void;
|
|
39
|
+
'aria-invalid': "true" | undefined;
|
|
40
|
+
'aria-required': "true" | undefined;
|
|
41
|
+
'aria-describedby': string | undefined;
|
|
42
|
+
'aria-label': string | undefined;
|
|
43
|
+
id: string | undefined;
|
|
44
|
+
value: string;
|
|
45
|
+
type: "text";
|
|
46
|
+
inputmode: "decimal";
|
|
47
|
+
role: "spinbutton";
|
|
48
|
+
placeholder: string | undefined;
|
|
49
|
+
disabled: boolean;
|
|
50
|
+
readonly: boolean;
|
|
51
|
+
'aria-valuenow': number | undefined;
|
|
52
|
+
'aria-valuemin': number | undefined;
|
|
53
|
+
'aria-valuemax': number | undefined;
|
|
54
|
+
};
|
|
55
|
+
/** Spread onto a native increment button. */
|
|
56
|
+
incrementProps: () => {
|
|
57
|
+
type: "button";
|
|
58
|
+
'aria-label': string;
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
tabindex: number;
|
|
61
|
+
onclick: () => void;
|
|
62
|
+
};
|
|
63
|
+
/** Spread onto a native decrement button. */
|
|
64
|
+
decrementProps: () => {
|
|
65
|
+
type: "button";
|
|
66
|
+
'aria-label': string;
|
|
67
|
+
disabled: boolean;
|
|
68
|
+
tabindex: number;
|
|
69
|
+
onclick: () => void;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export type NumberInputCore = ReturnType<typeof createNumberInput>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createNumberInput - the HEADLESS core behind <SvNumberInput>, in the same
|
|
3
|
+
* spirit as `createListbox` / `createSvGrid`: a runes-based state machine that
|
|
4
|
+
* owns parse / format / clamp / spinner state + keyboard, exposed as
|
|
5
|
+
* **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script lang="ts">
|
|
9
|
+
* import { createNumberInput } from '@svgrid/grid'
|
|
10
|
+
* let value = $state<number | null>(0)
|
|
11
|
+
* const num = createNumberInput({ value: () => value, onChange: (v) => (value = v) })
|
|
12
|
+
* </script>
|
|
13
|
+
* <input {...num.inputProps()} />
|
|
14
|
+
* <button {...num.incrementProps()}>+</button>
|
|
15
|
+
* <button {...num.decrementProps()}>-</button>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The styled <SvNumberInput> is just one renderer over this core.
|
|
19
|
+
*/
|
|
20
|
+
import { editorAria } from './editor-contract';
|
|
21
|
+
export function createNumberInput(config) {
|
|
22
|
+
const min = () => config.min?.() ?? -Infinity;
|
|
23
|
+
const max = () => config.max?.() ?? Infinity;
|
|
24
|
+
const step = () => config.step?.() ?? 1;
|
|
25
|
+
const precision = () => config.precision?.();
|
|
26
|
+
const grouping = () => config.grouping?.() ?? false;
|
|
27
|
+
const prefix = () => config.prefix?.() ?? '';
|
|
28
|
+
const suffix = () => config.suffix?.() ?? '';
|
|
29
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
30
|
+
const readonly = () => config.readonly?.() ?? false;
|
|
31
|
+
const isInteractive = $derived(!disabled() && !readonly());
|
|
32
|
+
let text = $state('');
|
|
33
|
+
let focused = $state(false);
|
|
34
|
+
let lastKey = '';
|
|
35
|
+
const fmt = $derived(new Intl.NumberFormat(undefined, {
|
|
36
|
+
useGrouping: grouping(),
|
|
37
|
+
minimumFractionDigits: precision() ?? 0,
|
|
38
|
+
maximumFractionDigits: precision() ?? 20,
|
|
39
|
+
}));
|
|
40
|
+
function display(v) {
|
|
41
|
+
if (v == null)
|
|
42
|
+
return '';
|
|
43
|
+
return `${prefix()}${fmt.format(v)}${suffix()}`;
|
|
44
|
+
}
|
|
45
|
+
// Reflect controlled `value` into the display text unless the user is editing.
|
|
46
|
+
$effect(() => {
|
|
47
|
+
const v = config.value();
|
|
48
|
+
const key = v == null ? 'null' : String(v);
|
|
49
|
+
if (key !== lastKey) {
|
|
50
|
+
lastKey = key;
|
|
51
|
+
if (!focused)
|
|
52
|
+
text = display(v);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
function clamp(n) {
|
|
56
|
+
let r = Math.min(max(), Math.max(min(), n));
|
|
57
|
+
const p = precision();
|
|
58
|
+
if (p != null)
|
|
59
|
+
r = Math.round(r * 10 ** p) / 10 ** p;
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
function parse(s) {
|
|
63
|
+
const cleaned = s.replace(prefix(), '').replace(suffix(), '').replace(/[,\s]/g, '').trim();
|
|
64
|
+
if (cleaned === '' || cleaned === '-')
|
|
65
|
+
return null;
|
|
66
|
+
const n = Number(cleaned);
|
|
67
|
+
return isNaN(n) ? null : n;
|
|
68
|
+
}
|
|
69
|
+
function commit() {
|
|
70
|
+
const parsed = parse(text);
|
|
71
|
+
const next = parsed == null ? null : clamp(parsed);
|
|
72
|
+
lastKey = next == null ? 'null' : String(next);
|
|
73
|
+
config.onChange?.(next);
|
|
74
|
+
text = display(next);
|
|
75
|
+
}
|
|
76
|
+
function bump(dir) {
|
|
77
|
+
if (!isInteractive)
|
|
78
|
+
return;
|
|
79
|
+
const base = parse(text) ?? 0;
|
|
80
|
+
const next = clamp(base + dir * step());
|
|
81
|
+
lastKey = String(next);
|
|
82
|
+
config.onChange?.(next);
|
|
83
|
+
text = focused ? String(next) : display(next);
|
|
84
|
+
}
|
|
85
|
+
const increment = () => bump(1);
|
|
86
|
+
const decrement = () => bump(-1);
|
|
87
|
+
function onFocus() {
|
|
88
|
+
focused = true;
|
|
89
|
+
// Show the bare number for easy editing.
|
|
90
|
+
const v = config.value();
|
|
91
|
+
text = v == null ? '' : String(v);
|
|
92
|
+
}
|
|
93
|
+
function onBlur() {
|
|
94
|
+
focused = false;
|
|
95
|
+
commit();
|
|
96
|
+
}
|
|
97
|
+
function onInput(e) {
|
|
98
|
+
text = e.currentTarget.value;
|
|
99
|
+
}
|
|
100
|
+
function onKeydown(e) {
|
|
101
|
+
if (e.key === 'ArrowUp') {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
bump(1);
|
|
104
|
+
}
|
|
105
|
+
else if (e.key === 'ArrowDown') {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
bump(-1);
|
|
108
|
+
}
|
|
109
|
+
else if (e.key === 'Enter') {
|
|
110
|
+
commit();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const ariaState = () => ({
|
|
114
|
+
id: config.id?.(),
|
|
115
|
+
invalid: config.invalid?.(),
|
|
116
|
+
required: config.required?.(),
|
|
117
|
+
error: config.error?.(),
|
|
118
|
+
ariaLabel: config.ariaLabel?.(),
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
/** The current display text (bind this to your input's value). */
|
|
122
|
+
get text() { return text; },
|
|
123
|
+
/** Whether the field is being edited. */
|
|
124
|
+
get focused() { return focused; },
|
|
125
|
+
/** Whether interaction is allowed (not disabled and not readonly). */
|
|
126
|
+
get isInteractive() { return isInteractive; },
|
|
127
|
+
commit,
|
|
128
|
+
bump,
|
|
129
|
+
increment,
|
|
130
|
+
decrement,
|
|
131
|
+
/** Spread onto the text input element. */
|
|
132
|
+
inputProps: () => ({
|
|
133
|
+
id: config.id?.(),
|
|
134
|
+
value: text,
|
|
135
|
+
type: 'text',
|
|
136
|
+
inputmode: 'decimal',
|
|
137
|
+
role: 'spinbutton',
|
|
138
|
+
placeholder: config.placeholder?.(),
|
|
139
|
+
disabled: disabled(),
|
|
140
|
+
readonly: readonly(),
|
|
141
|
+
'aria-valuenow': config.value() ?? undefined,
|
|
142
|
+
'aria-valuemin': min() === -Infinity ? undefined : min(),
|
|
143
|
+
'aria-valuemax': max() === Infinity ? undefined : max(),
|
|
144
|
+
...editorAria(ariaState()),
|
|
145
|
+
oninput: onInput,
|
|
146
|
+
onfocus: onFocus,
|
|
147
|
+
onblur: onBlur,
|
|
148
|
+
onkeydown: onKeydown,
|
|
149
|
+
}),
|
|
150
|
+
/** Spread onto a native increment button. */
|
|
151
|
+
incrementProps: () => ({
|
|
152
|
+
type: 'button',
|
|
153
|
+
'aria-label': 'Increment',
|
|
154
|
+
disabled: !isInteractive,
|
|
155
|
+
tabindex: -1,
|
|
156
|
+
onclick: increment,
|
|
157
|
+
}),
|
|
158
|
+
/** Spread onto a native decrement button. */
|
|
159
|
+
decrementProps: () => ({
|
|
160
|
+
type: 'button',
|
|
161
|
+
'aria-label': 'Decrement',
|
|
162
|
+
disabled: !isInteractive,
|
|
163
|
+
tabindex: -1,
|
|
164
|
+
onclick: decrement,
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPasswordInput - the HEADLESS core behind <SvPasswordInput>: a runes-based
|
|
3
|
+
* state machine that owns the reveal toggle + a 4-level strength heuristic,
|
|
4
|
+
* exposed as **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM.
|
|
5
|
+
*
|
|
6
|
+
* ```svelte
|
|
7
|
+
* <script lang="ts">
|
|
8
|
+
* import { createPasswordInput } from '@svgrid/grid'
|
|
9
|
+
* let value = $state('')
|
|
10
|
+
* const pw = createPasswordInput({ value: () => value, onChange: (v) => (value = v) })
|
|
11
|
+
* </script>
|
|
12
|
+
* <input {...pw.inputProps()} />
|
|
13
|
+
* <button {...pw.toggleProps()}>{pw.revealed ? 'Hide' : 'Show'}</button>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The styled <SvPasswordInput> is just one renderer over this core.
|
|
17
|
+
*/
|
|
18
|
+
export declare const STRENGTH_LABELS: string[];
|
|
19
|
+
/** Score a password 0-4 by length + character-class variety (pure). */
|
|
20
|
+
export declare function passwordStrength(v: string): number;
|
|
21
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
22
|
+
export type PasswordInputConfig = {
|
|
23
|
+
value: () => string;
|
|
24
|
+
onChange?: (value: string) => void;
|
|
25
|
+
placeholder?: () => string | undefined;
|
|
26
|
+
disabled?: () => boolean;
|
|
27
|
+
readonly?: () => boolean;
|
|
28
|
+
ariaLabel?: () => string | undefined;
|
|
29
|
+
autocomplete?: () => string | undefined;
|
|
30
|
+
};
|
|
31
|
+
export declare function createPasswordInput(config: PasswordInputConfig): {
|
|
32
|
+
/** Whether the value is currently shown as plain text. */
|
|
33
|
+
readonly revealed: boolean;
|
|
34
|
+
/** Strength score, 0-4. */
|
|
35
|
+
readonly strength: number;
|
|
36
|
+
/** Human label for the strength score. */
|
|
37
|
+
readonly strengthLabel: string | undefined;
|
|
38
|
+
toggle: () => boolean;
|
|
39
|
+
/** Spread onto the text/password input element. */
|
|
40
|
+
inputProps: () => {
|
|
41
|
+
value: string;
|
|
42
|
+
type: "text" | "password";
|
|
43
|
+
placeholder: string | undefined;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
readonly: boolean;
|
|
46
|
+
autocomplete: AutoFill;
|
|
47
|
+
'aria-label': string | undefined;
|
|
48
|
+
oninput: (e: Event) => void;
|
|
49
|
+
};
|
|
50
|
+
/** Spread onto the reveal (show/hide) toggle button. */
|
|
51
|
+
toggleProps: () => {
|
|
52
|
+
type: "button";
|
|
53
|
+
'aria-label': string;
|
|
54
|
+
'aria-pressed': boolean;
|
|
55
|
+
disabled: boolean;
|
|
56
|
+
tabindex: number;
|
|
57
|
+
onclick: () => boolean;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export type PasswordInputCore = ReturnType<typeof createPasswordInput>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPasswordInput - the HEADLESS core behind <SvPasswordInput>: a runes-based
|
|
3
|
+
* state machine that owns the reveal toggle + a 4-level strength heuristic,
|
|
4
|
+
* exposed as **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM.
|
|
5
|
+
*
|
|
6
|
+
* ```svelte
|
|
7
|
+
* <script lang="ts">
|
|
8
|
+
* import { createPasswordInput } from '@svgrid/grid'
|
|
9
|
+
* let value = $state('')
|
|
10
|
+
* const pw = createPasswordInput({ value: () => value, onChange: (v) => (value = v) })
|
|
11
|
+
* </script>
|
|
12
|
+
* <input {...pw.inputProps()} />
|
|
13
|
+
* <button {...pw.toggleProps()}>{pw.revealed ? 'Hide' : 'Show'}</button>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The styled <SvPasswordInput> is just one renderer over this core.
|
|
17
|
+
*/
|
|
18
|
+
export const STRENGTH_LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong'];
|
|
19
|
+
/** Score a password 0-4 by length + character-class variety (pure). */
|
|
20
|
+
export function passwordStrength(v) {
|
|
21
|
+
if (!v)
|
|
22
|
+
return 0;
|
|
23
|
+
let score = 0;
|
|
24
|
+
if (v.length >= 8)
|
|
25
|
+
score++;
|
|
26
|
+
if (/[a-z]/.test(v) && /[A-Z]/.test(v))
|
|
27
|
+
score++;
|
|
28
|
+
if (/\d/.test(v))
|
|
29
|
+
score++;
|
|
30
|
+
if (/[^a-zA-Z0-9]/.test(v))
|
|
31
|
+
score++;
|
|
32
|
+
return score;
|
|
33
|
+
}
|
|
34
|
+
export function createPasswordInput(config) {
|
|
35
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
36
|
+
const readonly = () => config.readonly?.() ?? false;
|
|
37
|
+
let revealed = $state(false);
|
|
38
|
+
const strength = $derived(passwordStrength(config.value()));
|
|
39
|
+
function onInput(e) {
|
|
40
|
+
config.onChange?.(e.currentTarget.value);
|
|
41
|
+
}
|
|
42
|
+
const toggle = () => (revealed = !revealed);
|
|
43
|
+
return {
|
|
44
|
+
/** Whether the value is currently shown as plain text. */
|
|
45
|
+
get revealed() { return revealed; },
|
|
46
|
+
/** Strength score, 0-4. */
|
|
47
|
+
get strength() { return strength; },
|
|
48
|
+
/** Human label for the strength score. */
|
|
49
|
+
get strengthLabel() { return STRENGTH_LABELS[strength]; },
|
|
50
|
+
toggle,
|
|
51
|
+
/** Spread onto the text/password input element. */
|
|
52
|
+
inputProps: () => ({
|
|
53
|
+
value: config.value(),
|
|
54
|
+
type: (revealed ? 'text' : 'password'),
|
|
55
|
+
placeholder: config.placeholder?.(),
|
|
56
|
+
disabled: disabled(),
|
|
57
|
+
readonly: readonly(),
|
|
58
|
+
autocomplete: (config.autocomplete?.() ?? 'current-password'),
|
|
59
|
+
'aria-label': config.ariaLabel?.(),
|
|
60
|
+
oninput: onInput,
|
|
61
|
+
}),
|
|
62
|
+
/** Spread onto the reveal (show/hide) toggle button. */
|
|
63
|
+
toggleProps: () => ({
|
|
64
|
+
type: 'button',
|
|
65
|
+
'aria-label': revealed ? 'Hide password' : 'Show password',
|
|
66
|
+
'aria-pressed': revealed,
|
|
67
|
+
disabled: disabled(),
|
|
68
|
+
tabindex: -1,
|
|
69
|
+
onclick: toggle,
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
2
|
+
export type PhoneInputConfig = {
|
|
3
|
+
value: () => string;
|
|
4
|
+
onChange?: (value: string, parts: {
|
|
5
|
+
country: string;
|
|
6
|
+
dial: string;
|
|
7
|
+
national: string;
|
|
8
|
+
}) => void;
|
|
9
|
+
/** Default country ISO code. */
|
|
10
|
+
country?: () => string;
|
|
11
|
+
disabled?: () => boolean;
|
|
12
|
+
readonly?: () => boolean;
|
|
13
|
+
placeholder?: () => string | undefined;
|
|
14
|
+
ariaLabel?: () => string | undefined;
|
|
15
|
+
};
|
|
16
|
+
export declare function createPhoneInput(config: PhoneInputConfig): {
|
|
17
|
+
/** Selected country ISO code. */
|
|
18
|
+
readonly iso: string;
|
|
19
|
+
/** Dial code for the selected country (e.g. `+1`). */
|
|
20
|
+
readonly dial: string;
|
|
21
|
+
/** Flag emoji for the selected country. */
|
|
22
|
+
readonly flag: string;
|
|
23
|
+
/** National number formatted for display. */
|
|
24
|
+
readonly displayNational: string;
|
|
25
|
+
/** The full emitted value (`+<dial><digits>`). */
|
|
26
|
+
readonly value: string;
|
|
27
|
+
/** Spread onto the country `<select>` element. */
|
|
28
|
+
selectProps: () => {
|
|
29
|
+
value: string;
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
'aria-label': string;
|
|
32
|
+
onchange: (e: Event) => void;
|
|
33
|
+
};
|
|
34
|
+
/** Spread onto the national number input element. */
|
|
35
|
+
inputProps: () => {
|
|
36
|
+
value: string;
|
|
37
|
+
type: "tel";
|
|
38
|
+
inputmode: "tel";
|
|
39
|
+
placeholder: string;
|
|
40
|
+
disabled: boolean;
|
|
41
|
+
readonly: boolean;
|
|
42
|
+
'aria-label': string;
|
|
43
|
+
oninput: (e: Event) => void;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type PhoneInputCore = ReturnType<typeof createPhoneInput>;
|