@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCombobox - the HEADLESS core behind <SvComboBox>, in the same spirit as
|
|
3
|
+
* `createListbox` / `createSvGrid`: a runes-based state machine (type-to-filter,
|
|
4
|
+
* roving active index, full keyboard, unmatched-text revert) exposed as
|
|
5
|
+
* **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM, no portal
|
|
6
|
+
* or measurement - those render concerns stay in the styled component.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createCombobox } from '@svgrid/grid'
|
|
11
|
+
* let value = $state<string | null>(null)
|
|
12
|
+
* const cb = createCombobox({ options: () => options, value: () => value, onChange: (v) => (value = v) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <input {...cb.inputProps()} />
|
|
15
|
+
* {#if cb.open}
|
|
16
|
+
* <ul {...cb.listboxProps()}>
|
|
17
|
+
* {#each cb.filtered as opt, i (opt.value)}<li {...cb.optionProps(i)}>{opt.label}</li>{/each}
|
|
18
|
+
* </ul>
|
|
19
|
+
* {/if}
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { type ListOption } from './list-option';
|
|
23
|
+
export type ComboboxValue = string | number | null;
|
|
24
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
25
|
+
export type ComboboxConfig = {
|
|
26
|
+
options: () => ReadonlyArray<ListOption>;
|
|
27
|
+
value: () => ComboboxValue;
|
|
28
|
+
onChange?: (value: ComboboxValue) => void;
|
|
29
|
+
disabled?: () => boolean;
|
|
30
|
+
ariaLabel?: () => string | undefined;
|
|
31
|
+
/** DOM focus hooks provided by the renderer; the core itself never touches the DOM. */
|
|
32
|
+
focusInput?: () => void;
|
|
33
|
+
blurInput?: () => void;
|
|
34
|
+
};
|
|
35
|
+
export declare function createCombobox(config: ComboboxConfig): {
|
|
36
|
+
/** Panel open state. */
|
|
37
|
+
readonly open: boolean;
|
|
38
|
+
/** Highlighted option index into `filtered`. */
|
|
39
|
+
readonly activeIndex: number;
|
|
40
|
+
/** True while the user is typing a filter query. */
|
|
41
|
+
readonly editing: boolean;
|
|
42
|
+
/** Current filter query text. */
|
|
43
|
+
readonly query: string;
|
|
44
|
+
/** Options after the live substring filter. */
|
|
45
|
+
readonly filtered: ListOption[];
|
|
46
|
+
/** The option matching the controlled value, if any. */
|
|
47
|
+
readonly selected: ListOption | null;
|
|
48
|
+
/** Text shown in the field (query while editing, else the selected label). */
|
|
49
|
+
readonly shownText: string;
|
|
50
|
+
/** Controlled value. */
|
|
51
|
+
readonly value: ComboboxValue;
|
|
52
|
+
/** id of the listbox element (matches inputProps `aria-controls`). */
|
|
53
|
+
listId: string;
|
|
54
|
+
isActive: (i: number) => boolean;
|
|
55
|
+
isSelected: (o: ListOption) => boolean;
|
|
56
|
+
setActive: (i: number) => void;
|
|
57
|
+
openPanel: () => void;
|
|
58
|
+
close: (revert?: boolean) => void;
|
|
59
|
+
toggle: () => void;
|
|
60
|
+
pick: (o: ListOption | undefined) => void;
|
|
61
|
+
onInput: (e: Event) => void;
|
|
62
|
+
onFocus: () => void;
|
|
63
|
+
onKeydown: (e: KeyboardEvent) => void;
|
|
64
|
+
/** Spread onto the editable <input>. */
|
|
65
|
+
inputProps: () => {
|
|
66
|
+
role: "combobox";
|
|
67
|
+
'aria-expanded': boolean;
|
|
68
|
+
'aria-controls': string;
|
|
69
|
+
'aria-autocomplete': "list";
|
|
70
|
+
'aria-label': string | undefined;
|
|
71
|
+
value: string;
|
|
72
|
+
disabled: boolean;
|
|
73
|
+
oninput: (e: Event) => void;
|
|
74
|
+
onfocus: () => void;
|
|
75
|
+
onkeydown: (e: KeyboardEvent) => void;
|
|
76
|
+
};
|
|
77
|
+
/** Spread onto the chevron/toggle <button>. */
|
|
78
|
+
triggerProps: () => {
|
|
79
|
+
type: "button";
|
|
80
|
+
tabindex: number;
|
|
81
|
+
'aria-label': string;
|
|
82
|
+
disabled: boolean;
|
|
83
|
+
onclick: () => void;
|
|
84
|
+
};
|
|
85
|
+
/** Spread onto the listbox container. */
|
|
86
|
+
listboxProps: () => {
|
|
87
|
+
id: string;
|
|
88
|
+
role: "listbox";
|
|
89
|
+
};
|
|
90
|
+
/** Spread onto the option at `index` (index into `filtered`). */
|
|
91
|
+
optionProps: (index: number) => {
|
|
92
|
+
role: "option";
|
|
93
|
+
id: string;
|
|
94
|
+
tabindex: number;
|
|
95
|
+
'aria-selected': boolean;
|
|
96
|
+
'aria-disabled': boolean | undefined;
|
|
97
|
+
'data-idx': number;
|
|
98
|
+
onclick: () => void;
|
|
99
|
+
onpointermove: () => void;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
export type Combobox = ReturnType<typeof createCombobox>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCombobox - the HEADLESS core behind <SvComboBox>, in the same spirit as
|
|
3
|
+
* `createListbox` / `createSvGrid`: a runes-based state machine (type-to-filter,
|
|
4
|
+
* roving active index, full keyboard, unmatched-text revert) exposed as
|
|
5
|
+
* **prop-getters** you spread onto YOUR OWN markup. No styles, no DOM, no portal
|
|
6
|
+
* or measurement - those render concerns stay in the styled component.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createCombobox } from '@svgrid/grid'
|
|
11
|
+
* let value = $state<string | null>(null)
|
|
12
|
+
* const cb = createCombobox({ options: () => options, value: () => value, onChange: (v) => (value = v) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <input {...cb.inputProps()} />
|
|
15
|
+
* {#if cb.open}
|
|
16
|
+
* <ul {...cb.listboxProps()}>
|
|
17
|
+
* {#each cb.filtered as opt, i (opt.value)}<li {...cb.optionProps(i)}>{opt.label}</li>{/each}
|
|
18
|
+
* </ul>
|
|
19
|
+
* {/if}
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { filterOptions } from './list-option';
|
|
23
|
+
let uid = 0;
|
|
24
|
+
export function createCombobox(config) {
|
|
25
|
+
const id = `sv-combo-${uid++}`;
|
|
26
|
+
const listId = `${id}-list`;
|
|
27
|
+
const opts = () => config.options();
|
|
28
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
29
|
+
let open = $state(false);
|
|
30
|
+
let query = $state('');
|
|
31
|
+
let active = $state(0);
|
|
32
|
+
let editing = $state(false);
|
|
33
|
+
const selected = $derived(opts().find((o) => o.value === config.value()) ?? null);
|
|
34
|
+
const filtered = $derived(editing ? filterOptions(opts(), query) : [...opts()]);
|
|
35
|
+
const shownText = $derived(editing ? query : selected?.label ?? '');
|
|
36
|
+
// While not actively editing, keep the shown text in sync with the selection.
|
|
37
|
+
$effect(() => { if (!editing)
|
|
38
|
+
query = selected?.label ?? ''; });
|
|
39
|
+
const optionId = (i) => `${listId}-opt-${i}`;
|
|
40
|
+
function openPanel() { if (disabled() || open)
|
|
41
|
+
return; open = true; active = 0; }
|
|
42
|
+
function close(revert = true) {
|
|
43
|
+
open = false;
|
|
44
|
+
editing = false;
|
|
45
|
+
if (revert)
|
|
46
|
+
query = selected?.label ?? '';
|
|
47
|
+
}
|
|
48
|
+
function toggle() { if (open) {
|
|
49
|
+
close();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
config.focusInput?.();
|
|
53
|
+
openPanel();
|
|
54
|
+
} }
|
|
55
|
+
function pick(o) {
|
|
56
|
+
if (!o || o.disabled)
|
|
57
|
+
return;
|
|
58
|
+
config.onChange?.(o.value);
|
|
59
|
+
query = o.label;
|
|
60
|
+
editing = false;
|
|
61
|
+
open = false;
|
|
62
|
+
config.blurInput?.();
|
|
63
|
+
}
|
|
64
|
+
function setActive(i) { const o = filtered[i]; if (o && !o.disabled)
|
|
65
|
+
active = i; }
|
|
66
|
+
function onInput(e) {
|
|
67
|
+
query = e.currentTarget.value;
|
|
68
|
+
editing = true;
|
|
69
|
+
active = 0;
|
|
70
|
+
if (!open)
|
|
71
|
+
openPanel();
|
|
72
|
+
}
|
|
73
|
+
function onFocus() { editing = true; query = selected?.label ?? ''; openPanel(); }
|
|
74
|
+
function onKeydown(e) {
|
|
75
|
+
if (disabled())
|
|
76
|
+
return;
|
|
77
|
+
if (!open && e.key === 'ArrowDown') {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
editing = true;
|
|
80
|
+
openPanel();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (e.key === 'ArrowDown') {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
active = Math.min(active + 1, filtered.length - 1);
|
|
86
|
+
}
|
|
87
|
+
else if (e.key === 'ArrowUp') {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
active = Math.max(active - 1, 0);
|
|
90
|
+
}
|
|
91
|
+
else if (e.key === 'Enter') {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
pick(filtered[active]);
|
|
94
|
+
}
|
|
95
|
+
else if (e.key === 'Escape') {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
/** Panel open state. */
|
|
102
|
+
get open() { return open; },
|
|
103
|
+
/** Highlighted option index into `filtered`. */
|
|
104
|
+
get activeIndex() { return active; },
|
|
105
|
+
/** True while the user is typing a filter query. */
|
|
106
|
+
get editing() { return editing; },
|
|
107
|
+
/** Current filter query text. */
|
|
108
|
+
get query() { return query; },
|
|
109
|
+
/** Options after the live substring filter. */
|
|
110
|
+
get filtered() { return filtered; },
|
|
111
|
+
/** The option matching the controlled value, if any. */
|
|
112
|
+
get selected() { return selected; },
|
|
113
|
+
/** Text shown in the field (query while editing, else the selected label). */
|
|
114
|
+
get shownText() { return shownText; },
|
|
115
|
+
/** Controlled value. */
|
|
116
|
+
get value() { return config.value(); },
|
|
117
|
+
/** id of the listbox element (matches inputProps `aria-controls`). */
|
|
118
|
+
listId,
|
|
119
|
+
isActive: (i) => i === active,
|
|
120
|
+
isSelected: (o) => o.value === config.value(),
|
|
121
|
+
setActive,
|
|
122
|
+
openPanel,
|
|
123
|
+
close,
|
|
124
|
+
toggle,
|
|
125
|
+
pick,
|
|
126
|
+
onInput,
|
|
127
|
+
onFocus,
|
|
128
|
+
onKeydown,
|
|
129
|
+
/** Spread onto the editable <input>. */
|
|
130
|
+
inputProps: () => ({
|
|
131
|
+
role: 'combobox',
|
|
132
|
+
'aria-expanded': open,
|
|
133
|
+
'aria-controls': listId,
|
|
134
|
+
'aria-autocomplete': 'list',
|
|
135
|
+
'aria-label': config.ariaLabel?.(),
|
|
136
|
+
value: shownText,
|
|
137
|
+
disabled: disabled(),
|
|
138
|
+
oninput: onInput,
|
|
139
|
+
onfocus: onFocus,
|
|
140
|
+
onkeydown: onKeydown,
|
|
141
|
+
}),
|
|
142
|
+
/** Spread onto the chevron/toggle <button>. */
|
|
143
|
+
triggerProps: () => ({
|
|
144
|
+
type: 'button',
|
|
145
|
+
tabindex: -1,
|
|
146
|
+
'aria-label': 'Toggle',
|
|
147
|
+
disabled: disabled(),
|
|
148
|
+
onclick: toggle,
|
|
149
|
+
}),
|
|
150
|
+
/** Spread onto the listbox container. */
|
|
151
|
+
listboxProps: () => ({
|
|
152
|
+
id: listId,
|
|
153
|
+
role: 'listbox',
|
|
154
|
+
}),
|
|
155
|
+
/** Spread onto the option at `index` (index into `filtered`). */
|
|
156
|
+
optionProps: (index) => {
|
|
157
|
+
const o = filtered[index];
|
|
158
|
+
return {
|
|
159
|
+
role: 'option',
|
|
160
|
+
id: optionId(index),
|
|
161
|
+
tabindex: -1,
|
|
162
|
+
'aria-selected': o ? o.value === config.value() : false,
|
|
163
|
+
'aria-disabled': o?.disabled,
|
|
164
|
+
'data-idx': index,
|
|
165
|
+
onclick: () => pick(o),
|
|
166
|
+
onpointermove: () => { if (o && !o.disabled)
|
|
167
|
+
active = index; },
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCountryInput - the HEADLESS core behind <SvCountryInput>: a searchable
|
|
3
|
+
* country picker (search by name, dial code, or ISO code; roving active index +
|
|
4
|
+
* keyboard) that emits the ISO 3166-1 alpha-2 code. Exposed as **prop-getters**
|
|
5
|
+
* you spread onto YOUR OWN markup. No styles/DOM/portal - those stay in the
|
|
6
|
+
* styled component.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createCountryInput, flagEmoji } from '@svgrid/grid'
|
|
11
|
+
* let value = $state<string | null>(null)
|
|
12
|
+
* const ci = createCountryInput({ value: () => value, onChange: (c) => (value = c) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <button {...ci.triggerProps()}>{ci.selected?.name ?? 'Select'}</button>
|
|
15
|
+
* {#if ci.open}
|
|
16
|
+
* <input {...ci.searchProps()} />
|
|
17
|
+
* <ul {...ci.listboxProps()}>
|
|
18
|
+
* {#each ci.filtered as c, i (c.code)}<li {...ci.optionProps(i)}>{c.name}</li>{/each}
|
|
19
|
+
* </ul>
|
|
20
|
+
* {/if}
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { type Country } from './countries';
|
|
24
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
25
|
+
export type CountryInputConfig = {
|
|
26
|
+
value: () => string | null;
|
|
27
|
+
onChange?: (code: string) => void;
|
|
28
|
+
disabled?: () => boolean;
|
|
29
|
+
ariaLabel?: () => string | undefined;
|
|
30
|
+
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
31
|
+
focusTrigger?: () => void;
|
|
32
|
+
};
|
|
33
|
+
export declare function createCountryInput(config: CountryInputConfig): {
|
|
34
|
+
/** Panel open state. */
|
|
35
|
+
readonly open: boolean;
|
|
36
|
+
/** Highlighted country index into `filtered`. */
|
|
37
|
+
readonly activeIndex: number;
|
|
38
|
+
/** Current search query. */
|
|
39
|
+
readonly query: string;
|
|
40
|
+
/** Countries after the current search filter. */
|
|
41
|
+
readonly filtered: Country[];
|
|
42
|
+
/** The selected country, if any. */
|
|
43
|
+
readonly selected: Country | null;
|
|
44
|
+
/** Controlled value (ISO alpha-2 code). */
|
|
45
|
+
readonly value: string | null;
|
|
46
|
+
isActive: (i: number) => boolean;
|
|
47
|
+
isSelected: (code: string) => boolean;
|
|
48
|
+
setActive: (i: number) => void;
|
|
49
|
+
setQuery: (v: string) => void;
|
|
50
|
+
openPanel: () => void;
|
|
51
|
+
close: () => void;
|
|
52
|
+
toggle: () => void;
|
|
53
|
+
pick: (code: string) => void;
|
|
54
|
+
onKeydown: (e: KeyboardEvent) => void;
|
|
55
|
+
/** Spread onto the trigger <button>. */
|
|
56
|
+
triggerProps: () => {
|
|
57
|
+
type: "button";
|
|
58
|
+
'aria-haspopup': "listbox";
|
|
59
|
+
'aria-expanded': boolean;
|
|
60
|
+
'aria-label': string | undefined;
|
|
61
|
+
disabled: boolean;
|
|
62
|
+
onclick: () => void;
|
|
63
|
+
};
|
|
64
|
+
/** Spread onto the search <input> inside the panel. */
|
|
65
|
+
searchProps: () => {
|
|
66
|
+
type: "text";
|
|
67
|
+
value: string;
|
|
68
|
+
'aria-label': string;
|
|
69
|
+
oninput: (e: Event) => void;
|
|
70
|
+
onkeydown: (e: KeyboardEvent) => void;
|
|
71
|
+
};
|
|
72
|
+
/** Spread onto the listbox container. */
|
|
73
|
+
listboxProps: () => {
|
|
74
|
+
role: "listbox";
|
|
75
|
+
};
|
|
76
|
+
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
77
|
+
optionProps: (index: number) => {
|
|
78
|
+
role: "option";
|
|
79
|
+
tabindex: number;
|
|
80
|
+
'aria-selected': boolean;
|
|
81
|
+
'data-idx': number;
|
|
82
|
+
onclick: () => void;
|
|
83
|
+
onpointermove: () => void;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
export type CountryInput = ReturnType<typeof createCountryInput>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCountryInput - the HEADLESS core behind <SvCountryInput>: a searchable
|
|
3
|
+
* country picker (search by name, dial code, or ISO code; roving active index +
|
|
4
|
+
* keyboard) that emits the ISO 3166-1 alpha-2 code. Exposed as **prop-getters**
|
|
5
|
+
* you spread onto YOUR OWN markup. No styles/DOM/portal - those stay in the
|
|
6
|
+
* styled component.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createCountryInput, flagEmoji } from '@svgrid/grid'
|
|
11
|
+
* let value = $state<string | null>(null)
|
|
12
|
+
* const ci = createCountryInput({ value: () => value, onChange: (c) => (value = c) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <button {...ci.triggerProps()}>{ci.selected?.name ?? 'Select'}</button>
|
|
15
|
+
* {#if ci.open}
|
|
16
|
+
* <input {...ci.searchProps()} />
|
|
17
|
+
* <ul {...ci.listboxProps()}>
|
|
18
|
+
* {#each ci.filtered as c, i (c.code)}<li {...ci.optionProps(i)}>{c.name}</li>{/each}
|
|
19
|
+
* </ul>
|
|
20
|
+
* {/if}
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { COUNTRIES, COUNTRY_BY_CODE } from './countries';
|
|
24
|
+
export function createCountryInput(config) {
|
|
25
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
26
|
+
let open = $state(false);
|
|
27
|
+
let query = $state('');
|
|
28
|
+
let active = $state(0);
|
|
29
|
+
const selected = $derived(config.value() ? COUNTRY_BY_CODE.get(config.value()) ?? null : null);
|
|
30
|
+
const filtered = $derived(query.trim() === ''
|
|
31
|
+
? COUNTRIES
|
|
32
|
+
: COUNTRIES.filter((c) => c.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
33
|
+
c.dial.includes(query) ||
|
|
34
|
+
c.code.toLowerCase() === query.toLowerCase()));
|
|
35
|
+
function openPanel() { if (disabled() || open)
|
|
36
|
+
return; open = true; query = ''; active = 0; }
|
|
37
|
+
function close() { open = false; config.focusTrigger?.(); }
|
|
38
|
+
function toggle() { if (open) {
|
|
39
|
+
open = false;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
openPanel();
|
|
43
|
+
} }
|
|
44
|
+
function pick(code) { config.onChange?.(code); open = false; config.focusTrigger?.(); }
|
|
45
|
+
function setQuery(v) { query = v; active = 0; }
|
|
46
|
+
function onSearchInput(e) { setQuery(e.currentTarget.value); }
|
|
47
|
+
function onKeydown(e) {
|
|
48
|
+
if (!open)
|
|
49
|
+
return;
|
|
50
|
+
if (e.key === 'ArrowDown') {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
active = Math.min(active + 1, filtered.length - 1);
|
|
53
|
+
}
|
|
54
|
+
else if (e.key === 'ArrowUp') {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
active = Math.max(active - 1, 0);
|
|
57
|
+
}
|
|
58
|
+
else if (e.key === 'Enter') {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
const c = filtered[active];
|
|
61
|
+
if (c)
|
|
62
|
+
pick(c.code);
|
|
63
|
+
}
|
|
64
|
+
else if (e.key === 'Escape') {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
/** Panel open state. */
|
|
71
|
+
get open() { return open; },
|
|
72
|
+
/** Highlighted country index into `filtered`. */
|
|
73
|
+
get activeIndex() { return active; },
|
|
74
|
+
/** Current search query. */
|
|
75
|
+
get query() { return query; },
|
|
76
|
+
/** Countries after the current search filter. */
|
|
77
|
+
get filtered() { return filtered; },
|
|
78
|
+
/** The selected country, if any. */
|
|
79
|
+
get selected() { return selected; },
|
|
80
|
+
/** Controlled value (ISO alpha-2 code). */
|
|
81
|
+
get value() { return config.value(); },
|
|
82
|
+
isActive: (i) => i === active,
|
|
83
|
+
isSelected: (code) => code === config.value(),
|
|
84
|
+
setActive: (i) => { active = i; },
|
|
85
|
+
setQuery,
|
|
86
|
+
openPanel,
|
|
87
|
+
close,
|
|
88
|
+
toggle,
|
|
89
|
+
pick,
|
|
90
|
+
onKeydown,
|
|
91
|
+
/** Spread onto the trigger <button>. */
|
|
92
|
+
triggerProps: () => ({
|
|
93
|
+
type: 'button',
|
|
94
|
+
'aria-haspopup': 'listbox',
|
|
95
|
+
'aria-expanded': open,
|
|
96
|
+
'aria-label': config.ariaLabel?.(),
|
|
97
|
+
disabled: disabled(),
|
|
98
|
+
onclick: toggle,
|
|
99
|
+
}),
|
|
100
|
+
/** Spread onto the search <input> inside the panel. */
|
|
101
|
+
searchProps: () => ({
|
|
102
|
+
type: 'text',
|
|
103
|
+
value: query,
|
|
104
|
+
'aria-label': 'Search countries',
|
|
105
|
+
oninput: onSearchInput,
|
|
106
|
+
onkeydown: onKeydown,
|
|
107
|
+
}),
|
|
108
|
+
/** Spread onto the listbox container. */
|
|
109
|
+
listboxProps: () => ({
|
|
110
|
+
role: 'listbox',
|
|
111
|
+
}),
|
|
112
|
+
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
113
|
+
optionProps: (index) => {
|
|
114
|
+
const c = filtered[index];
|
|
115
|
+
return {
|
|
116
|
+
role: 'option',
|
|
117
|
+
tabindex: -1,
|
|
118
|
+
'aria-selected': c ? c.code === config.value() : false,
|
|
119
|
+
'data-idx': index,
|
|
120
|
+
onclick: () => { if (c)
|
|
121
|
+
pick(c.code); },
|
|
122
|
+
onpointermove: () => { active = index; },
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type DateLike } from './datetime/date-core';
|
|
2
|
+
export type DateTimeValue = Date | string | number | null;
|
|
3
|
+
export type DropDownDisplayMode = 'both' | 'calendar' | 'time';
|
|
4
|
+
export type DateTimeTab = 'date' | 'time';
|
|
5
|
+
/** Reactive inputs are getters; callbacks are closures. */
|
|
6
|
+
export type DateTimePickerConfig = {
|
|
7
|
+
value: () => DateTimeValue;
|
|
8
|
+
onChange?: (value: Date | null) => void;
|
|
9
|
+
/** Fired when the value is finalized (Enter, blur, or a single-date pick). */
|
|
10
|
+
onCommit?: (value: Date | null) => void;
|
|
11
|
+
/** Fired on Escape / dismiss without committing. */
|
|
12
|
+
onCancel?: () => void;
|
|
13
|
+
formatString?: () => string;
|
|
14
|
+
min?: () => DateLike | null;
|
|
15
|
+
max?: () => DateLike | null;
|
|
16
|
+
nullable?: () => boolean;
|
|
17
|
+
locale?: () => string;
|
|
18
|
+
dropDownDisplayMode?: () => DropDownDisplayMode;
|
|
19
|
+
spinButtons?: () => boolean;
|
|
20
|
+
stepMinutes?: () => number;
|
|
21
|
+
disabled?: () => boolean;
|
|
22
|
+
readonly?: () => boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function createDateTimePicker(config: DateTimePickerConfig): {
|
|
25
|
+
readonly current: Date | null;
|
|
26
|
+
text: string;
|
|
27
|
+
readonly open: boolean;
|
|
28
|
+
readonly tab: DateTimeTab;
|
|
29
|
+
readonly isInteractive: boolean;
|
|
30
|
+
readonly showDateTab: boolean;
|
|
31
|
+
readonly showTimeTab: boolean;
|
|
32
|
+
commit: (next: Date | null) => void;
|
|
33
|
+
commitFromText: () => void;
|
|
34
|
+
bump: (deltaMin: number) => void;
|
|
35
|
+
onCalendarChange: (dates: Date[]) => void;
|
|
36
|
+
onTimeChange: (t: Date) => void;
|
|
37
|
+
openPanel: () => void;
|
|
38
|
+
closePanel: () => void;
|
|
39
|
+
toggle: () => void;
|
|
40
|
+
setTab: (t: DateTimeTab) => void;
|
|
41
|
+
onInputKeydown: (e: KeyboardEvent) => void;
|
|
42
|
+
/** The text field: masked value round-trips through the token engine. Spread
|
|
43
|
+
* alongside `bind:value={dtp.text}`. */
|
|
44
|
+
inputProps(): {
|
|
45
|
+
type: "text";
|
|
46
|
+
disabled: boolean;
|
|
47
|
+
readonly: boolean;
|
|
48
|
+
onblur: () => void;
|
|
49
|
+
onkeydown: (e: KeyboardEvent) => void;
|
|
50
|
+
};
|
|
51
|
+
/** The calendar-toggle button. */
|
|
52
|
+
toggleProps(): {
|
|
53
|
+
type: "button";
|
|
54
|
+
'aria-label': string;
|
|
55
|
+
'aria-haspopup': "dialog";
|
|
56
|
+
'aria-expanded': boolean;
|
|
57
|
+
disabled: boolean;
|
|
58
|
+
tabindex: number;
|
|
59
|
+
onclick: () => void;
|
|
60
|
+
};
|
|
61
|
+
/** The inline clear (x) button (nullable values). */
|
|
62
|
+
clearProps(): {
|
|
63
|
+
type: "button";
|
|
64
|
+
'aria-label': string;
|
|
65
|
+
onclick: () => void;
|
|
66
|
+
};
|
|
67
|
+
/** A spin (increment/decrement) button that steps by `stepMinutes`. */
|
|
68
|
+
spinProps(dir: 1 | -1): {
|
|
69
|
+
type: "button";
|
|
70
|
+
tabindex: number;
|
|
71
|
+
'aria-label': string;
|
|
72
|
+
disabled: boolean;
|
|
73
|
+
onclick: () => void;
|
|
74
|
+
};
|
|
75
|
+
/** A DATE / TIME tab button. */
|
|
76
|
+
tabProps(which: DateTimeTab): {
|
|
77
|
+
type: "button";
|
|
78
|
+
role: "tab";
|
|
79
|
+
'aria-selected': boolean;
|
|
80
|
+
onclick: () => void;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export type DateTimePicker = ReturnType<typeof createDateTimePicker>;
|