sveltacular 1.0.6 → 1.0.9
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/README.md +232 -28
- package/dist/forms/bool-box/bool-box.svelte +21 -2
- package/dist/forms/bool-box/bool-box.svelte.d.ts +5 -0
- package/dist/forms/check-box/check-box-group.svelte +1 -0
- package/dist/forms/check-box/check-box.svelte +73 -31
- package/dist/forms/check-box/check-box.svelte.d.ts +7 -0
- package/dist/forms/date-box/date-box.svelte +7 -3
- package/dist/forms/date-box/date-box.svelte.d.ts +3 -0
- package/dist/forms/file-box/file-box.svelte +33 -7
- package/dist/forms/form-field/form-field.svelte +128 -33
- package/dist/forms/form-field/form-field.svelte.d.ts +9 -3
- package/dist/forms/form-label/form-label.svelte +4 -2
- package/dist/forms/form-label/form-label.svelte.d.ts +2 -2
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.js +1 -1
- package/dist/forms/info-box/info-box.svelte +9 -7
- package/dist/forms/list-box/list-box.svelte +270 -89
- package/dist/forms/list-box/list-box.svelte.d.ts +3 -0
- package/dist/forms/money-box/money-box.svelte +20 -16
- package/dist/forms/number-box/number-box.svelte +16 -3
- package/dist/forms/number-box/number-box.svelte.d.ts +6 -0
- package/dist/forms/phone-box/phone-box.svelte +17 -3
- package/dist/forms/phone-box/phone-box.svelte.d.ts +5 -0
- package/dist/forms/radio-group/radio-box.svelte +11 -2
- package/dist/forms/radio-group/radio-box.svelte.d.ts +1 -0
- package/dist/forms/radio-group/radio-group.svelte +10 -4
- package/dist/forms/radio-group/radio-group.svelte.d.ts +4 -0
- package/dist/forms/switch-box/switch-box.svelte +33 -13
- package/dist/forms/switch-box/switch-box.svelte.d.ts +6 -0
- package/dist/forms/tag-box/tag-box.svelte +225 -0
- package/dist/forms/tag-box/tag-box.svelte.d.ts +19 -0
- package/dist/forms/tag-input-box/tag-input-box.svelte +8 -7
- package/dist/forms/tag-input-box/tag-input-box.svelte.d.ts +2 -1
- package/dist/forms/text-area/text-area.svelte +18 -3
- package/dist/forms/text-area/text-area.svelte.d.ts +7 -0
- package/dist/forms/text-box/text-box.svelte +18 -15
- package/dist/forms/text-box/text-box.svelte.d.ts +2 -2
- package/dist/forms/time-box/time-box.svelte +7 -3
- package/dist/forms/time-box/time-box.svelte.d.ts +3 -0
- package/dist/forms/url-box/url-box.svelte +31 -1
- package/dist/forms/url-box/url-box.svelte.d.ts +10 -0
- package/dist/generic/avatar/avatar.svelte +0 -10
- package/dist/generic/badge/badge.svelte +0 -1
- package/dist/generic/chip/chip.svelte +1 -13
- package/dist/generic/header/header.svelte +19 -17
- package/dist/generic/menu/menu.svelte +2 -3
- package/dist/generic/rating/rating.svelte +2 -3
- package/dist/generic/section/section.svelte +7 -3
- package/dist/generic/spinner/spinner.svelte +0 -1
- package/dist/generic/theme-provider/theme-provider-demo.svelte +38 -64
- package/dist/generic/theme-provider/theme-provider.svelte +5 -11
- package/dist/generic/toaster/toaster.svelte +1 -1
- package/dist/helpers/use-position.svelte.js +1 -1
- package/dist/helpers/use-virtual-list.svelte.js +1 -1
- package/dist/icons/check-icon.svelte +0 -2
- package/dist/icons/copy-icon.svelte +0 -1
- package/dist/modals/alert.svelte +6 -1
- package/dist/modals/confirm.svelte +6 -1
- package/dist/modals/modal.svelte +2 -2
- package/dist/modals/prompt.svelte +16 -11
- package/dist/navigation/accordion/accordion.svelte +1 -7
- package/dist/navigation/app-bar/app-bar.svelte +12 -4
- package/dist/navigation/command-palette/command-palette.svelte +34 -41
- package/dist/navigation/context-menu/README.md +34 -55
- package/dist/navigation/context-menu/context-menu-divider.svelte +2 -11
- package/dist/navigation/context-menu/context-menu-item.svelte +10 -11
- package/dist/navigation/context-menu/context-menu.svelte +6 -7
- package/dist/navigation/drawer/drawer.svelte +0 -1
- package/dist/navigation/dropdown-button/dropdown-button.svelte +6 -1
- package/dist/placeholders/skeleton-table.svelte +0 -1
- package/dist/tables/table-row.svelte +2 -6
- package/dist/test-utils/accessibility-helpers.js +2 -3
- package/dist/test-utils/render-helpers.js +4 -7
- package/dist/typography/code.svelte +0 -1
- package/dist/typography/headline.svelte +6 -6
- package/dist/typography/paragraph.svelte +1 -1
- package/dist/typography/subtitle.svelte +1 -1
- package/package.json +4 -8
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { DropdownOption, FormFieldSizeOptions, MenuOption } from '../../types/form.js';
|
|
3
|
-
import FormField from '../form-field/form-field.svelte';
|
|
3
|
+
import FormField, { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
4
4
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
5
5
|
import Menu from '../../generic/menu/menu.svelte';
|
|
6
6
|
import AngleUpIcon from '../../icons/angle-up-icon.svelte';
|
|
7
7
|
import debounce from '../../helpers/debounce.js';
|
|
8
8
|
import { browser } from '$app/environment';
|
|
9
|
+
import { onMount, untrack } from 'svelte';
|
|
9
10
|
import type { SearchFunction } from './list-box.js';
|
|
10
11
|
|
|
11
12
|
let {
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
placeholder = '',
|
|
20
21
|
onChange = undefined,
|
|
21
22
|
label = undefined,
|
|
23
|
+
helperText = undefined,
|
|
24
|
+
feedback = undefined,
|
|
22
25
|
virtualScroll = false,
|
|
23
26
|
itemHeight = 40
|
|
24
27
|
}: {
|
|
@@ -32,19 +35,65 @@
|
|
|
32
35
|
placeholder?: string;
|
|
33
36
|
onChange?: ((value: string | null) => void) | undefined;
|
|
34
37
|
label?: string;
|
|
38
|
+
helperText?: string;
|
|
39
|
+
feedback?: FormFieldFeedback;
|
|
35
40
|
virtualScroll?: boolean;
|
|
36
41
|
itemHeight?: number;
|
|
37
42
|
} = $props();
|
|
38
43
|
|
|
39
44
|
const id = uniqueId();
|
|
40
45
|
const listboxId = `${id}-listbox`;
|
|
41
|
-
const getText = () => items.find((item) => item.value == value)?.name || '';
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
// Use local items state when search function is provided, otherwise use prop
|
|
48
|
+
let localItems = $state<DropdownOption[]>([]);
|
|
49
|
+
let currentItems = $derived(search ? localItems : items);
|
|
50
|
+
|
|
51
|
+
const getText = () => currentItems.find((item) => item.value == value)?.name || '';
|
|
52
|
+
|
|
53
|
+
let text = $state('');
|
|
44
54
|
let isMenuOpen = $state(false);
|
|
45
55
|
let highlightIndex = $state(-1);
|
|
46
|
-
let
|
|
47
|
-
let
|
|
56
|
+
let isSearchable = $derived(searchable || !!search);
|
|
57
|
+
let inputElement: HTMLInputElement | null = $state(null);
|
|
58
|
+
let containerElement: HTMLDivElement | null = $state(null);
|
|
59
|
+
let isUserTyping = $state(false);
|
|
60
|
+
|
|
61
|
+
// Initialize localItems when items prop changes (only when no search function)
|
|
62
|
+
$effect(() => {
|
|
63
|
+
if (!search) {
|
|
64
|
+
localItems = [...items];
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Initialize text from value on mount and when value/items change (but not when user is typing)
|
|
69
|
+
$effect(() => {
|
|
70
|
+
// Track value and currentItems to update text when they change
|
|
71
|
+
const currentValue = value;
|
|
72
|
+
const itemsForText = currentItems;
|
|
73
|
+
const userIsTyping = isUserTyping;
|
|
74
|
+
|
|
75
|
+
// Don't change text if user is currently typing in searchable mode
|
|
76
|
+
if (isSearchable && userIsTyping) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const newText = itemsForText.find((item) => item.value == currentValue)?.name || '';
|
|
81
|
+
// Use untrack to read current text without making effect reactive to text changes
|
|
82
|
+
const currentText = untrack(() => text);
|
|
83
|
+
if (currentText !== newText) {
|
|
84
|
+
text = newText;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Compute filtered items reactively
|
|
89
|
+
let filteredItems = $derived.by(() => {
|
|
90
|
+
const searchText = text.trim().toLowerCase();
|
|
91
|
+
return searchText && isSearchable
|
|
92
|
+
? currentItems
|
|
93
|
+
.map((item, index) => ({ ...item, index }))
|
|
94
|
+
.filter((item) => item.name.toLowerCase().includes(searchText))
|
|
95
|
+
: currentItems.map((item, index) => ({ ...item, index }));
|
|
96
|
+
});
|
|
48
97
|
|
|
49
98
|
// Get the ID of the highlighted option for ARIA
|
|
50
99
|
let activeDescendant = $derived(
|
|
@@ -53,121 +102,203 @@
|
|
|
53
102
|
: undefined
|
|
54
103
|
);
|
|
55
104
|
|
|
105
|
+
// Reset highlight when filter changes
|
|
106
|
+
$effect(() => {
|
|
107
|
+
if (highlightIndex >= filteredItems.length) {
|
|
108
|
+
highlightIndex = -1;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
56
112
|
// When an item is selected from the dropdown menu
|
|
57
113
|
const onSelect = (item: MenuOption) => {
|
|
114
|
+
isUserTyping = false;
|
|
58
115
|
value = item.value;
|
|
59
116
|
onChange?.(value);
|
|
60
117
|
text = getText();
|
|
61
|
-
applyFilter();
|
|
62
118
|
isMenuOpen = false;
|
|
119
|
+
highlightIndex = -1;
|
|
120
|
+
if (browser && inputElement) {
|
|
121
|
+
inputElement.blur();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Open/close dropdown
|
|
126
|
+
const openDropdown = () => {
|
|
127
|
+
if (!disabled) {
|
|
128
|
+
isMenuOpen = true;
|
|
129
|
+
if (browser && inputElement) {
|
|
130
|
+
inputElement.focus();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const closeDropdown = () => {
|
|
136
|
+
isMenuOpen = false;
|
|
137
|
+
highlightIndex = -1;
|
|
138
|
+
isUserTyping = false;
|
|
139
|
+
if (!isSearchable && browser && inputElement) {
|
|
140
|
+
// Reset text to selected value when closing non-searchable dropdown
|
|
141
|
+
text = getText();
|
|
142
|
+
}
|
|
63
143
|
};
|
|
64
144
|
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
145
|
+
const toggleDropdown = () => {
|
|
146
|
+
if (isMenuOpen) {
|
|
147
|
+
closeDropdown();
|
|
148
|
+
} else {
|
|
149
|
+
openDropdown();
|
|
150
|
+
}
|
|
67
151
|
};
|
|
68
152
|
|
|
69
|
-
//
|
|
70
|
-
const
|
|
153
|
+
// Click arrow button
|
|
154
|
+
const clickArrow = (e: MouseEvent | KeyboardEvent) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
if (disabled) return;
|
|
158
|
+
toggleDropdown();
|
|
159
|
+
};
|
|
71
160
|
|
|
72
|
-
//
|
|
73
|
-
const
|
|
161
|
+
// Handle clicks on the input (for non-searchable mode)
|
|
162
|
+
const handleInputClick = (e: MouseEvent) => {
|
|
74
163
|
if (disabled) return;
|
|
75
|
-
|
|
76
|
-
if (isMenuOpen)
|
|
164
|
+
// For non-searchable mode, clicking the input should open the dropdown
|
|
165
|
+
if (!isSearchable && !isMenuOpen) {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
openDropdown();
|
|
168
|
+
}
|
|
77
169
|
};
|
|
78
170
|
|
|
79
171
|
// Handle key presses in the input
|
|
80
|
-
const
|
|
172
|
+
const onInputKeyDown = (e: KeyboardEvent) => {
|
|
81
173
|
if (disabled) return;
|
|
82
|
-
|
|
83
|
-
|
|
174
|
+
|
|
175
|
+
if (e.key === 'Escape') {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
closeDropdown();
|
|
178
|
+
if (browser && inputElement) {
|
|
179
|
+
inputElement.blur();
|
|
180
|
+
}
|
|
84
181
|
return;
|
|
85
182
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
183
|
+
|
|
184
|
+
if (e.key === 'Enter') {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
if (isMenuOpen && highlightIndex >= 0 && filteredItems[highlightIndex]) {
|
|
89
187
|
onSelect(filteredItems[highlightIndex]);
|
|
188
|
+
} else if (!isMenuOpen) {
|
|
189
|
+
openDropdown();
|
|
90
190
|
}
|
|
91
191
|
return;
|
|
92
192
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
isMenuOpen
|
|
193
|
+
|
|
194
|
+
if (e.key === 'Tab') {
|
|
195
|
+
if (isMenuOpen && highlightIndex >= 0 && filteredItems[highlightIndex]) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
onSelect(filteredItems[highlightIndex]);
|
|
198
|
+
}
|
|
96
199
|
return;
|
|
97
200
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
201
|
+
|
|
202
|
+
if (e.key === 'ArrowDown') {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
if (!isMenuOpen) {
|
|
205
|
+
openDropdown();
|
|
206
|
+
}
|
|
207
|
+
if (filteredItems.length > 0) {
|
|
208
|
+
highlightIndex = Math.min(highlightIndex + 1, filteredItems.length - 1);
|
|
209
|
+
}
|
|
101
210
|
return;
|
|
102
211
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
212
|
+
|
|
213
|
+
if (e.key === 'ArrowUp') {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
if (isMenuOpen && filteredItems.length > 0) {
|
|
216
|
+
highlightIndex = Math.max(highlightIndex - 1, 0);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// For searchable mode, allow typing
|
|
222
|
+
if (isSearchable && (e.key.length === 1 || e.key === 'Backspace' || e.key === 'Delete')) {
|
|
223
|
+
isUserTyping = true;
|
|
224
|
+
if (!isMenuOpen) {
|
|
225
|
+
openDropdown();
|
|
226
|
+
}
|
|
227
|
+
highlightIndex = -1;
|
|
228
|
+
// Let the input handle the character, then trigger search
|
|
229
|
+
if (browser) {
|
|
230
|
+
setTimeout(() => triggerSearch(), 0);
|
|
231
|
+
}
|
|
107
232
|
}
|
|
108
233
|
};
|
|
109
234
|
|
|
110
235
|
// User is typing in the search box
|
|
111
236
|
const triggerSearch = debounce(async () => {
|
|
112
|
-
if (search &&
|
|
113
|
-
|
|
237
|
+
if (search && isSearchable) {
|
|
238
|
+
localItems = await search(text);
|
|
114
239
|
}
|
|
115
|
-
updateText();
|
|
116
|
-
applyFilter();
|
|
117
240
|
}, 300);
|
|
118
241
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
filteredItems =
|
|
123
|
-
searchText && isSeachable
|
|
124
|
-
? items
|
|
125
|
-
.map((item, index) => ({ ...item, index }))
|
|
126
|
-
.filter((item) => item.name.toLowerCase().includes(searchText))
|
|
127
|
-
: items.map((item, index) => ({ ...item, index }));
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const clear = () => {
|
|
242
|
+
const clear = (e: MouseEvent | KeyboardEvent) => {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
e.stopPropagation();
|
|
131
245
|
if (disabled) return;
|
|
246
|
+
isUserTyping = false;
|
|
132
247
|
text = '';
|
|
133
|
-
value =
|
|
134
|
-
|
|
135
|
-
|
|
248
|
+
value = null;
|
|
249
|
+
onChange?.(null);
|
|
250
|
+
if (browser && inputElement) {
|
|
251
|
+
inputElement.focus();
|
|
252
|
+
}
|
|
136
253
|
};
|
|
137
254
|
|
|
138
|
-
//
|
|
139
|
-
|
|
255
|
+
// Close dropdown when clicking outside
|
|
256
|
+
onMount(() => {
|
|
257
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
258
|
+
if (containerElement && !containerElement.contains(e.target as Node)) {
|
|
259
|
+
closeDropdown();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
140
263
|
if (browser) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
264
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
265
|
+
return () => {
|
|
266
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
267
|
+
};
|
|
144
268
|
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// Do initial search
|
|
148
|
-
triggerSearch();
|
|
269
|
+
});
|
|
149
270
|
|
|
150
271
|
let open = $derived(isMenuOpen && !disabled);
|
|
151
272
|
</script>
|
|
152
273
|
|
|
153
|
-
<FormField {size} {label} {id} {required} {disabled}>
|
|
154
|
-
<div
|
|
274
|
+
<FormField {size} {label} {id} {required} {disabled} {helperText} {feedback}>
|
|
275
|
+
<div
|
|
276
|
+
class="listbox-container {open ? 'open' : 'closed'} {disabled ? 'disabled' : 'enabled'}"
|
|
277
|
+
bind:this={containerElement}
|
|
278
|
+
>
|
|
155
279
|
<input
|
|
156
280
|
type="text"
|
|
157
281
|
{id}
|
|
158
282
|
bind:value={text}
|
|
283
|
+
bind:this={inputElement}
|
|
159
284
|
{required}
|
|
160
285
|
{disabled}
|
|
161
286
|
{placeholder}
|
|
162
|
-
readonly={!
|
|
287
|
+
readonly={!isSearchable}
|
|
163
288
|
role="combobox"
|
|
164
289
|
aria-expanded={open}
|
|
165
290
|
aria-controls={listboxId}
|
|
166
|
-
aria-autocomplete={
|
|
291
|
+
aria-autocomplete={isSearchable ? 'list' : 'none'}
|
|
167
292
|
aria-activedescendant={activeDescendant}
|
|
168
293
|
aria-haspopup="listbox"
|
|
169
|
-
|
|
170
|
-
|
|
294
|
+
onkeydown={onInputKeyDown}
|
|
295
|
+
onclick={handleInputClick}
|
|
296
|
+
oninput={() => {
|
|
297
|
+
if (isSearchable) {
|
|
298
|
+
isUserTyping = true;
|
|
299
|
+
triggerSearch();
|
|
300
|
+
}
|
|
301
|
+
}}
|
|
171
302
|
data-value={value}
|
|
172
303
|
data-text={text}
|
|
173
304
|
/>
|
|
@@ -182,7 +313,7 @@
|
|
|
182
313
|
>
|
|
183
314
|
<AngleUpIcon />
|
|
184
315
|
</button>
|
|
185
|
-
{#if text &&
|
|
316
|
+
{#if text && isSearchable}
|
|
186
317
|
<button
|
|
187
318
|
type="button"
|
|
188
319
|
class="clear"
|
|
@@ -199,8 +330,8 @@
|
|
|
199
330
|
<Menu
|
|
200
331
|
items={filteredItems}
|
|
201
332
|
{open}
|
|
202
|
-
closeAfterSelect={
|
|
203
|
-
searchText={text}
|
|
333
|
+
closeAfterSelect={true}
|
|
334
|
+
searchText={isSearchable ? text : ''}
|
|
204
335
|
{onSelect}
|
|
205
336
|
size="full"
|
|
206
337
|
bind:highlightIndex
|
|
@@ -213,56 +344,106 @@
|
|
|
213
344
|
</div>
|
|
214
345
|
</FormField>
|
|
215
346
|
|
|
216
|
-
<style
|
|
347
|
+
<style>.listbox-container {
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: flex-start;
|
|
217
351
|
position: relative;
|
|
218
|
-
}
|
|
219
|
-
div.disabled {
|
|
220
|
-
opacity: 0.5;
|
|
221
|
-
cursor: not-allowed;
|
|
222
|
-
pointer-events: none;
|
|
223
|
-
}
|
|
224
|
-
div input {
|
|
225
352
|
width: 100%;
|
|
226
|
-
|
|
353
|
+
height: 100%;
|
|
227
354
|
border-radius: var(--radius-md);
|
|
228
355
|
border: var(--border-thin) solid var(--form-input-border);
|
|
229
356
|
background-color: var(--form-input-bg);
|
|
230
357
|
color: var(--form-input-fg);
|
|
231
|
-
font-size: var(--font-
|
|
358
|
+
font-size: var(--font-md);
|
|
232
359
|
font-weight: 500;
|
|
233
|
-
line-height:
|
|
360
|
+
line-height: 2rem;
|
|
234
361
|
transition: background-color var(--transition-base) var(--ease-in-out), border-color var(--transition-base) var(--ease-in-out), color var(--transition-base) var(--ease-in-out), fill var(--transition-base) var(--ease-in-out), stroke var(--transition-base) var(--ease-in-out);
|
|
362
|
+
}
|
|
363
|
+
.listbox-container.disabled {
|
|
364
|
+
opacity: 0.5;
|
|
365
|
+
cursor: not-allowed;
|
|
366
|
+
pointer-events: none;
|
|
367
|
+
}
|
|
368
|
+
.listbox-container input {
|
|
369
|
+
background-color: transparent;
|
|
370
|
+
border: none;
|
|
371
|
+
line-height: 2rem;
|
|
372
|
+
font-size: var(--font-md);
|
|
373
|
+
width: 100%;
|
|
374
|
+
flex-grow: 1;
|
|
375
|
+
padding-left: var(--spacing-base);
|
|
376
|
+
padding-right: var(--spacing-base);
|
|
377
|
+
color: var(--form-input-fg);
|
|
235
378
|
user-select: none;
|
|
236
379
|
white-space: nowrap;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
}
|
|
382
|
+
.listbox-container input:focus {
|
|
383
|
+
outline: none;
|
|
384
|
+
}
|
|
385
|
+
.listbox-container input:focus-visible {
|
|
386
|
+
outline: 2px solid var(--focus-ring, #007bff);
|
|
387
|
+
outline-offset: 2px;
|
|
237
388
|
}
|
|
238
|
-
|
|
389
|
+
.listbox-container input[readonly] {
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
}
|
|
392
|
+
.listbox-container button {
|
|
239
393
|
border: 0;
|
|
240
394
|
appearance: none;
|
|
241
395
|
background: transparent;
|
|
242
396
|
padding: 0;
|
|
243
397
|
margin: 0;
|
|
244
398
|
position: absolute;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: center;
|
|
248
402
|
z-index: 2;
|
|
249
403
|
color: var(--form-input-fg, black);
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
}
|
|
406
|
+
.listbox-container button:focus {
|
|
407
|
+
outline: none;
|
|
408
|
+
}
|
|
409
|
+
.listbox-container button:focus-visible {
|
|
410
|
+
outline: 2px solid var(--focus-ring, #007bff);
|
|
411
|
+
outline-offset: 2px;
|
|
250
412
|
}
|
|
251
|
-
|
|
252
|
-
right:
|
|
413
|
+
.listbox-container button.icon {
|
|
414
|
+
right: var(--spacing-base);
|
|
415
|
+
width: 1rem;
|
|
416
|
+
height: 1rem;
|
|
253
417
|
transition: transform 0.3s linear;
|
|
254
418
|
transform: rotate(180deg);
|
|
255
419
|
}
|
|
256
|
-
|
|
257
|
-
|
|
420
|
+
.listbox-container button.icon :global(svg) {
|
|
421
|
+
width: 100%;
|
|
422
|
+
height: 100%;
|
|
423
|
+
}
|
|
424
|
+
.listbox-container button.clear {
|
|
425
|
+
right: calc(var(--spacing-base) + 2rem);
|
|
426
|
+
width: 1.25rem;
|
|
427
|
+
height: 1.25rem;
|
|
428
|
+
font-size: var(--font-sm);
|
|
429
|
+
font-weight: 600;
|
|
258
430
|
}
|
|
259
|
-
|
|
431
|
+
.listbox-container.open .icon {
|
|
260
432
|
transform: rotate(0deg);
|
|
261
433
|
}
|
|
262
|
-
|
|
434
|
+
.listbox-container .dropdown {
|
|
263
435
|
position: absolute;
|
|
264
436
|
top: 100%;
|
|
265
437
|
left: 0;
|
|
266
438
|
width: 100%;
|
|
267
|
-
z-index:
|
|
439
|
+
z-index: 1000;
|
|
440
|
+
margin-top: 0.25rem;
|
|
441
|
+
}
|
|
442
|
+
.listbox-container .dropdown :global(.menu) {
|
|
443
|
+
font-size: var(--font-sm, 0.875rem);
|
|
444
|
+
}
|
|
445
|
+
.listbox-container .dropdown :global(.menu) :global(li) :global(div) {
|
|
446
|
+
padding: 0.25rem 0.5rem;
|
|
447
|
+
line-height: 1.25;
|
|
448
|
+
font-size: var(--font-sm, 0.875rem);
|
|
268
449
|
}</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
|
|
2
|
+
import { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
2
3
|
import type { SearchFunction } from './list-box.js';
|
|
3
4
|
type $$ComponentProps = {
|
|
4
5
|
value?: string | null;
|
|
@@ -11,6 +12,8 @@ type $$ComponentProps = {
|
|
|
11
12
|
placeholder?: string;
|
|
12
13
|
onChange?: ((value: string | null) => void) | undefined;
|
|
13
14
|
label?: string;
|
|
15
|
+
helperText?: string;
|
|
16
|
+
feedback?: FormFieldFeedback;
|
|
14
17
|
virtualScroll?: boolean;
|
|
15
18
|
itemHeight?: number;
|
|
16
19
|
};
|
|
@@ -274,23 +274,27 @@
|
|
|
274
274
|
position: relative;
|
|
275
275
|
width: 100%;
|
|
276
276
|
height: 100%;
|
|
277
|
-
|
|
278
|
-
border:
|
|
279
|
-
|
|
280
|
-
color: var(--form-input-
|
|
281
|
-
|
|
277
|
+
box-sizing: border-box;
|
|
278
|
+
border-radius: var(--radius-md);
|
|
279
|
+
border: var(--border-thin) solid var(--form-input-border);
|
|
280
|
+
background-color: var(--form-input-bg);
|
|
281
|
+
color: var(--form-input-fg);
|
|
282
|
+
font-size: var(--font-md);
|
|
282
283
|
font-weight: 500;
|
|
283
284
|
line-height: 2rem;
|
|
284
|
-
transition: background-color
|
|
285
|
+
transition: background-color var(--transition-base) var(--ease-in-out), border-color var(--transition-base) var(--ease-in-out), color var(--transition-base) var(--ease-in-out), fill var(--transition-base) var(--ease-in-out), stroke var(--transition-base) var(--ease-in-out);
|
|
285
286
|
user-select: none;
|
|
286
287
|
white-space: nowrap;
|
|
287
288
|
}
|
|
288
289
|
.input input {
|
|
289
290
|
background-color: transparent;
|
|
290
291
|
border: none;
|
|
292
|
+
box-sizing: border-box;
|
|
291
293
|
line-height: 2rem;
|
|
292
|
-
font-size:
|
|
293
|
-
padding
|
|
294
|
+
font-size: var(--font-md);
|
|
295
|
+
padding: 0;
|
|
296
|
+
padding-left: var(--spacing-base);
|
|
297
|
+
margin: 0;
|
|
294
298
|
}
|
|
295
299
|
.input input:focus {
|
|
296
300
|
outline: none;
|
|
@@ -299,7 +303,7 @@
|
|
|
299
303
|
flex-grow: 1;
|
|
300
304
|
}
|
|
301
305
|
.input .separator {
|
|
302
|
-
width:
|
|
306
|
+
width: var(--spacing-base);
|
|
303
307
|
text-align: center;
|
|
304
308
|
}
|
|
305
309
|
.input .cents {
|
|
@@ -307,16 +311,16 @@
|
|
|
307
311
|
}
|
|
308
312
|
.input .prefix,
|
|
309
313
|
.input .suffix {
|
|
310
|
-
font-size:
|
|
314
|
+
font-size: var(--font-md);
|
|
311
315
|
line-height: 2rem;
|
|
312
|
-
padding-left:
|
|
313
|
-
padding-right:
|
|
314
|
-
background-color: var(--form-input-accent-bg
|
|
315
|
-
color: var(--form-input-accent-fg
|
|
316
|
+
padding-left: var(--spacing-base);
|
|
317
|
+
padding-right: var(--spacing-base);
|
|
318
|
+
background-color: var(--form-input-accent-bg);
|
|
319
|
+
color: var(--form-input-accent-fg);
|
|
316
320
|
}
|
|
317
321
|
.input .prefix {
|
|
318
|
-
border-right:
|
|
322
|
+
border-right: var(--border-thin) solid var(--form-input-border);
|
|
319
323
|
}
|
|
320
324
|
.input .suffix {
|
|
321
|
-
border-left:
|
|
325
|
+
border-left: var(--border-thin) solid var(--form-input-border);
|
|
322
326
|
}</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { roundToDecimals } from '../../helpers/round-to-decimals.js';
|
|
3
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
4
|
-
import FormField from '../form-field/form-field.svelte';
|
|
4
|
+
import FormField, { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
5
5
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
6
6
|
const id = uniqueId();
|
|
7
7
|
|
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
suffix = null as string | null,
|
|
20
20
|
step = 1,
|
|
21
21
|
onChange = undefined,
|
|
22
|
-
label = undefined
|
|
22
|
+
label = undefined,
|
|
23
|
+
helperText = undefined,
|
|
24
|
+
feedback = undefined,
|
|
25
|
+
disabled = false,
|
|
26
|
+
required = false,
|
|
27
|
+
readonly = false
|
|
23
28
|
}: {
|
|
24
29
|
value?: number | null;
|
|
25
30
|
placeholder?: string;
|
|
@@ -33,6 +38,11 @@
|
|
|
33
38
|
step?: number;
|
|
34
39
|
onChange?: ((value: number | null) => void) | undefined;
|
|
35
40
|
label?: string;
|
|
41
|
+
helperText?: string;
|
|
42
|
+
feedback?: FormFieldFeedback;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
readonly?: boolean;
|
|
36
46
|
} = $props();
|
|
37
47
|
|
|
38
48
|
const valueChanged = () => {
|
|
@@ -66,7 +76,7 @@
|
|
|
66
76
|
};
|
|
67
77
|
</script>
|
|
68
78
|
|
|
69
|
-
<FormField {size} {label} {id}>
|
|
79
|
+
<FormField {size} {label} {id} {required} {disabled} {helperText} {feedback}>
|
|
70
80
|
<div class="input {type}">
|
|
71
81
|
{#if prefix}
|
|
72
82
|
<span class="prefix">{prefix}</span>
|
|
@@ -80,6 +90,9 @@
|
|
|
80
90
|
{step}
|
|
81
91
|
{min}
|
|
82
92
|
{max}
|
|
93
|
+
{disabled}
|
|
94
|
+
{readonly}
|
|
95
|
+
{required}
|
|
83
96
|
onchange={valueChanged}
|
|
84
97
|
oninput={onInput}
|
|
85
98
|
onkeypress={onKeyPress}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
1
2
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
2
3
|
type AllowedInputTypes = 'number' | 'currency';
|
|
3
4
|
type $$ComponentProps = {
|
|
@@ -13,6 +14,11 @@ type $$ComponentProps = {
|
|
|
13
14
|
step?: number;
|
|
14
15
|
onChange?: ((value: number | null) => void) | undefined;
|
|
15
16
|
label?: string;
|
|
17
|
+
helperText?: string;
|
|
18
|
+
feedback?: FormFieldFeedback;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
readonly?: boolean;
|
|
16
22
|
};
|
|
17
23
|
declare const NumberBox: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
18
24
|
type NumberBox = ReturnType<typeof NumberBox>;
|