sveltacular 1.0.6 → 1.0.7

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.
Files changed (43) hide show
  1. package/README.md +232 -28
  2. package/dist/forms/bool-box/bool-box.svelte +21 -2
  3. package/dist/forms/bool-box/bool-box.svelte.d.ts +5 -0
  4. package/dist/forms/check-box/check-box-group.svelte +1 -0
  5. package/dist/forms/check-box/check-box.svelte +73 -31
  6. package/dist/forms/check-box/check-box.svelte.d.ts +7 -0
  7. package/dist/forms/date-box/date-box.svelte +7 -3
  8. package/dist/forms/date-box/date-box.svelte.d.ts +3 -0
  9. package/dist/forms/file-box/file-box.svelte +33 -7
  10. package/dist/forms/form-field/form-field.svelte +128 -33
  11. package/dist/forms/form-field/form-field.svelte.d.ts +9 -3
  12. package/dist/forms/form-label/form-label.svelte +4 -2
  13. package/dist/forms/form-label/form-label.svelte.d.ts +2 -2
  14. package/dist/forms/info-box/info-box.svelte +9 -7
  15. package/dist/forms/list-box/list-box.svelte +270 -89
  16. package/dist/forms/list-box/list-box.svelte.d.ts +3 -0
  17. package/dist/forms/money-box/money-box.svelte +20 -16
  18. package/dist/forms/number-box/number-box.svelte +16 -3
  19. package/dist/forms/number-box/number-box.svelte.d.ts +6 -0
  20. package/dist/forms/phone-box/phone-box.svelte +17 -3
  21. package/dist/forms/phone-box/phone-box.svelte.d.ts +5 -0
  22. package/dist/forms/radio-group/radio-box.svelte +11 -2
  23. package/dist/forms/radio-group/radio-box.svelte.d.ts +1 -0
  24. package/dist/forms/radio-group/radio-group.svelte +10 -4
  25. package/dist/forms/radio-group/radio-group.svelte.d.ts +4 -0
  26. package/dist/forms/switch-box/switch-box.svelte +33 -13
  27. package/dist/forms/switch-box/switch-box.svelte.d.ts +6 -0
  28. package/dist/forms/tag-input-box/tag-input-box.svelte +8 -7
  29. package/dist/forms/tag-input-box/tag-input-box.svelte.d.ts +2 -1
  30. package/dist/forms/text-area/text-area.svelte +19 -3
  31. package/dist/forms/text-area/text-area.svelte.d.ts +7 -0
  32. package/dist/forms/text-box/text-box.svelte +18 -15
  33. package/dist/forms/text-box/text-box.svelte.d.ts +2 -2
  34. package/dist/forms/time-box/time-box.svelte +7 -3
  35. package/dist/forms/time-box/time-box.svelte.d.ts +3 -0
  36. package/dist/forms/url-box/url-box.svelte +31 -1
  37. package/dist/forms/url-box/url-box.svelte.d.ts +10 -0
  38. package/dist/generic/avatar/avatar.svelte +1 -0
  39. package/dist/generic/chip/chip.svelte +1 -0
  40. package/dist/generic/menu/menu.svelte +2 -3
  41. package/dist/navigation/context-menu/README.md +1 -0
  42. package/dist/navigation/context-menu/context-menu-divider.svelte +1 -0
  43. package/package.json +1 -1
@@ -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
- let text = $state(getText());
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 filteredItems = $state<MenuOption[]>([]);
47
- let isSeachable = $derived(searchable || !!search);
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 focusOnInput = () => {
66
- if (browser) document.getElementById(id)?.focus();
145
+ const toggleDropdown = () => {
146
+ if (isMenuOpen) {
147
+ closeDropdown();
148
+ } else {
149
+ openDropdown();
150
+ }
67
151
  };
68
152
 
69
- // Toggle open or closed
70
- const toggle = () => (isMenuOpen = !open);
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
- // Click arrow
73
- const clickArrow = () => {
161
+ // Handle clicks on the input (for non-searchable mode)
162
+ const handleInputClick = (e: MouseEvent) => {
74
163
  if (disabled) return;
75
- toggle();
76
- if (isMenuOpen) focusOnInput();
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 onInputKeyPress = (e: KeyboardEvent) => {
172
+ const onInputKeyDown = (e: KeyboardEvent) => {
81
173
  if (disabled) return;
82
- if (e.key == 'Escape') {
83
- isMenuOpen = false;
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
- if (e.key == 'Enter' || e.key == 'Tab') {
87
- isMenuOpen = false;
88
- if (highlightIndex > -1) {
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
- if (e.key == 'ArrowDown') {
94
- highlightIndex = Math.min(highlightIndex + 1, filteredItems.length - 1);
95
- isMenuOpen = true;
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
- if (e.key == 'ArrowUp') {
99
- highlightIndex = Math.max(highlightIndex - 1, -1);
100
- if (highlightIndex == -1) isMenuOpen = false;
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
- if (e.key.length == 1 || e.key == 'Backspace' || e.key == 'Delete') {
104
- isMenuOpen = true;
105
- highlightIndex = 0;
106
- triggerSearch();
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 && isSeachable) {
113
- items = await search(text);
237
+ if (search && isSearchable) {
238
+ localItems = await search(text);
114
239
  }
115
- updateText();
116
- applyFilter();
117
240
  }, 300);
118
241
 
119
- // Text or items have changed, we should apply the filter
120
- const applyFilter = () => {
121
- const searchText = text.trim().toLowerCase();
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
- triggerSearch();
135
- focusOnInput();
248
+ value = null;
249
+ onChange?.(null);
250
+ if (browser && inputElement) {
251
+ inputElement.focus();
252
+ }
136
253
  };
137
254
 
138
- // When items change, we should change the text to match the value
139
- const updateText = async () => {
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
- const textBox = document.getElementById(id) as HTMLInputElement;
142
- // Don't change text if they're currently typing
143
- if (document.activeElement != textBox) text = getText();
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 class="{open ? 'open' : 'closed'} {disabled ? 'disabled' : 'enabled'}">
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={!isSeachable}
287
+ readonly={!isSearchable}
163
288
  role="combobox"
164
289
  aria-expanded={open}
165
290
  aria-controls={listboxId}
166
- aria-autocomplete={isSeachable ? 'list' : 'none'}
291
+ aria-autocomplete={isSearchable ? 'list' : 'none'}
167
292
  aria-activedescendant={activeDescendant}
168
293
  aria-haspopup="listbox"
169
- onfocus={() => (isMenuOpen = true)}
170
- onkeyup={onInputKeyPress}
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 && isSeachable}
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={false}
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>div {
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
- padding: var(--spacing-sm) var(--spacing-base);
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-base);
358
+ font-size: var(--font-md);
232
359
  font-weight: 500;
233
- line-height: 1.25rem;
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
- div button {
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
- top: 0.65rem;
246
- width: 1rem;
247
- height: 1rem;
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
- div button.icon {
252
- right: 1rem;
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
- div button.clear {
257
- right: 3rem;
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
- div.open .icon {
431
+ .listbox-container.open .icon {
260
432
  transform: rotate(0deg);
261
433
  }
262
- div .dropdown {
434
+ .listbox-container .dropdown {
263
435
  position: absolute;
264
436
  top: 100%;
265
437
  left: 0;
266
438
  width: 100%;
267
- z-index: 3;
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
- border-radius: 0.25rem;
278
- border: 1px solid var(--form-input-border, black);
279
- background-color: var(--form-input-bg, white);
280
- color: var(--form-input-fg, black);
281
- font-size: 1rem;
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 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out, fill 0.2s ease-in-out, stroke 0.2s ease-in-out;
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: 1rem;
293
- padding-left: 1rem;
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: 1rem;
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: 1rem;
314
+ font-size: var(--font-md);
311
315
  line-height: 2rem;
312
- padding-left: 1rem;
313
- padding-right: 1rem;
314
- background-color: var(--form-input-accent-bg, #ccc);
315
- color: var(--form-input-accent-fg, black);
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: 1px solid var(--form-input-border, black);
322
+ border-right: var(--border-thin) solid var(--form-input-border);
319
323
  }
320
324
  .input .suffix {
321
- border-left: 1px solid var(--form-input-border, black);
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>;