sveltacular 1.0.0 → 1.0.1
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/forms/check-box/check-box-group.svelte +16 -12
- package/dist/forms/combo-box/combo-box.svelte +31 -43
- package/dist/forms/combo-box/combo-box.svelte.d.ts +2 -22
- package/dist/forms/date-box/date-box.svelte +7 -7
- package/dist/forms/file-box/file-box.svelte +1 -5
- package/dist/forms/form-field.svelte +58 -0
- package/dist/forms/form-field.svelte.d.ts +7 -0
- package/dist/forms/form-row.svelte +29 -0
- package/dist/forms/form-row.svelte.d.ts +7 -0
- package/dist/forms/form.svelte +6 -0
- package/dist/forms/info-box/info-box.svelte +1 -5
- package/dist/forms/list-box/list-box.svelte +1 -5
- package/dist/forms/money-box/money-box.svelte +6 -7
- package/dist/forms/number-box/number-box.svelte +1 -5
- package/dist/forms/phone-box/phone-box.svelte +9 -7
- package/dist/forms/radio-group/radio-group.svelte +1 -5
- package/dist/forms/slider/slider.svelte +1 -5
- package/dist/forms/text-area/text-area.svelte +18 -15
- package/dist/forms/text-box/text-box.svelte +40 -53
- package/dist/forms/time-box/time-box.svelte +1 -5
- package/dist/generic/avatar/avatar.svelte +1 -0
- package/dist/generic/chip/chip.svelte +1 -0
- package/dist/navigation/context-menu/README.md +1 -0
- package/dist/navigation/context-menu/context-menu-divider.svelte +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
|
|
3
4
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
5
|
import CheckBox from './check-box.svelte';
|
|
6
6
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
7
7
|
|
|
@@ -31,13 +31,20 @@
|
|
|
31
31
|
// Sync itemsWithState when items or group changes (one-way: items/group -> itemsWithState)
|
|
32
32
|
// Reassign the entire array to avoid reading itemsWithState in the effect
|
|
33
33
|
$effect(() => {
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Track items and group as dependencies
|
|
35
|
+
const currentItems = items;
|
|
36
|
+
const currentGroup = group;
|
|
37
|
+
|
|
38
|
+
// Use untrack to prevent writing to itemsWithState from triggering this effect again
|
|
39
|
+
untrack(() => {
|
|
40
|
+
// Rebuild itemsWithState from items, using group to determine checked state
|
|
41
|
+
// Reassign instead of mutate to avoid circular dependency
|
|
42
|
+
const newItems = currentItems.map((item) => ({
|
|
43
|
+
...item,
|
|
44
|
+
isChecked: currentGroup.includes(item.value ?? '')
|
|
45
|
+
}));
|
|
46
|
+
itemsWithState = newItems;
|
|
47
|
+
});
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
const handleCheckboxChange = (data: { isChecked: boolean; value: string }) => {
|
|
@@ -53,10 +60,7 @@
|
|
|
53
60
|
};
|
|
54
61
|
</script>
|
|
55
62
|
|
|
56
|
-
<FormField {size}>
|
|
57
|
-
{#if label}
|
|
58
|
-
<FormLabel {id} {required} {label} />
|
|
59
|
-
{/if}
|
|
63
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
60
64
|
<div>
|
|
61
65
|
{#each itemsWithState as item}
|
|
62
66
|
<CheckBox
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* />
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
+
import { untrack } from 'svelte';
|
|
22
23
|
import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
|
|
23
24
|
import FormField from '../form-field.svelte';
|
|
24
|
-
import FormLabel from '../form-label.svelte';
|
|
25
25
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
26
26
|
import Menu from '../../generic/menu/menu.svelte';
|
|
27
27
|
import AngleUpIcon from '../../icons/angle-up-icon.svelte';
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
multiSelect = false,
|
|
41
41
|
placeholder = '',
|
|
42
42
|
label = undefined,
|
|
43
|
-
|
|
43
|
+
helperText = undefined,
|
|
44
44
|
errorText = undefined,
|
|
45
45
|
successText = undefined,
|
|
46
46
|
maxSelections = undefined as number | undefined,
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
placeholder?: string;
|
|
69
69
|
/** Label text for the combobox */
|
|
70
70
|
label?: string;
|
|
71
|
-
/**
|
|
72
|
-
|
|
71
|
+
/** Helper text displayed below the input */
|
|
72
|
+
helperText?: string;
|
|
73
73
|
/** Error message to display */
|
|
74
74
|
errorText?: string;
|
|
75
75
|
/** Success message to display */
|
|
@@ -368,9 +368,28 @@
|
|
|
368
368
|
}
|
|
369
369
|
};
|
|
370
370
|
|
|
371
|
-
//
|
|
371
|
+
// Filter items when items or searchQuery changes
|
|
372
|
+
// Use untrack to prevent reading filteredItems/highlightIndex from triggering the effect
|
|
372
373
|
$effect(() => {
|
|
373
|
-
|
|
374
|
+
// Track only the dependencies we care about
|
|
375
|
+
const query = searchQuery.trim().toLowerCase();
|
|
376
|
+
const currentItems = items;
|
|
377
|
+
|
|
378
|
+
// Use untrack to write to state without triggering this effect again
|
|
379
|
+
untrack(() => {
|
|
380
|
+
if (query && searchable) {
|
|
381
|
+
filteredItems = currentItems.filter((item) =>
|
|
382
|
+
item.name.toLowerCase().includes(query)
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
filteredItems = [...currentItems];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Reset highlight if out of bounds
|
|
389
|
+
if (highlightIndex >= filteredItems.length) {
|
|
390
|
+
highlightIndex = Math.max(0, filteredItems.length - 1);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
374
393
|
});
|
|
375
394
|
|
|
376
395
|
// Derived state for open/closed
|
|
@@ -379,16 +398,15 @@
|
|
|
379
398
|
// Clear search query when menu closes in single-select mode
|
|
380
399
|
$effect(() => {
|
|
381
400
|
if (!isMenuOpen && !multiSelect) {
|
|
382
|
-
|
|
401
|
+
// Use untrack to prevent triggering other effects
|
|
402
|
+
untrack(() => {
|
|
403
|
+
searchQuery = '';
|
|
404
|
+
});
|
|
383
405
|
}
|
|
384
406
|
});
|
|
385
407
|
</script>
|
|
386
408
|
|
|
387
|
-
<FormField {size}>
|
|
388
|
-
{#if label}
|
|
389
|
-
<FormLabel {id} {required} {disabled} {label} />
|
|
390
|
-
{/if}
|
|
391
|
-
|
|
409
|
+
<FormField {size} {label} {id} {required} {disabled} {helperText} {errorText} {successText}>
|
|
392
410
|
<div class="combobox-wrapper {open ? 'open' : 'closed'} {disabled ? 'disabled' : 'enabled'}">
|
|
393
411
|
<!-- Multi-select chip display -->
|
|
394
412
|
{#if multiSelect && selectedItems.length > 0}
|
|
@@ -423,7 +441,7 @@
|
|
|
423
441
|
aria-activedescendant={activeDescendant}
|
|
424
442
|
aria-haspopup="listbox"
|
|
425
443
|
aria-label={label}
|
|
426
|
-
aria-describedby={
|
|
444
|
+
aria-describedby={helperText || errorText || successText ? `${id}-helper ${id}-error ${id}-success` : undefined}
|
|
427
445
|
aria-invalid={errorText ? 'true' : undefined}
|
|
428
446
|
onfocus={() => {
|
|
429
447
|
if (!disabled) {
|
|
@@ -489,19 +507,6 @@
|
|
|
489
507
|
</div>
|
|
490
508
|
</div>
|
|
491
509
|
|
|
492
|
-
<!-- Help/Error/Success text -->
|
|
493
|
-
{#if helpText || errorText || successText}
|
|
494
|
-
<div id="{id}-description" class="description">
|
|
495
|
-
{#if errorText}
|
|
496
|
-
<span class="error-text" role="alert">{errorText}</span>
|
|
497
|
-
{:else if successText}
|
|
498
|
-
<span class="success-text" role="status">{successText}</span>
|
|
499
|
-
{:else if helpText}
|
|
500
|
-
<span class="help-text">{helpText}</span>
|
|
501
|
-
{/if}
|
|
502
|
-
</div>
|
|
503
|
-
{/if}
|
|
504
|
-
|
|
505
510
|
<!-- Max selections indicator -->
|
|
506
511
|
{#if multiSelect && maxSelections !== undefined}
|
|
507
512
|
<div class="max-selections-indicator">
|
|
@@ -629,23 +634,6 @@ button:focus-visible {
|
|
|
629
634
|
margin-top: 0.25rem;
|
|
630
635
|
}
|
|
631
636
|
|
|
632
|
-
.description {
|
|
633
|
-
margin-top: 0.5rem;
|
|
634
|
-
font-size: var(--font-sm);
|
|
635
|
-
line-height: 1.4;
|
|
636
|
-
}
|
|
637
|
-
.description .help-text {
|
|
638
|
-
color: var(--gray-600);
|
|
639
|
-
}
|
|
640
|
-
.description .error-text {
|
|
641
|
-
color: var(--danger);
|
|
642
|
-
font-weight: 500;
|
|
643
|
-
}
|
|
644
|
-
.description .success-text {
|
|
645
|
-
color: var(--success);
|
|
646
|
-
font-weight: 500;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
637
|
.max-selections-indicator {
|
|
650
638
|
margin-top: 0.5rem;
|
|
651
639
|
font-size: var(--font-sm);
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Combobox Component
|
|
3
|
-
*
|
|
4
|
-
* A searchable select component with typeahead, multi-select support, and virtual scrolling.
|
|
5
|
-
* Follows the ARIA 1.2 Combobox pattern for full accessibility.
|
|
6
|
-
*
|
|
7
|
-
* @component
|
|
8
|
-
* @example
|
|
9
|
-
* ```svelte
|
|
10
|
-
* <ComboBox
|
|
11
|
-
* bind:value
|
|
12
|
-
* items={[
|
|
13
|
-
* { value: '1', name: 'Option 1' },
|
|
14
|
-
* { value: '2', name: 'Option 2' }
|
|
15
|
-
* ]}
|
|
16
|
-
* label="Select an option"
|
|
17
|
-
* searchable
|
|
18
|
-
* />
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
1
|
import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
|
|
22
2
|
type $$ComponentProps = {
|
|
23
3
|
/** Current selected value(s) - string for single-select, string[] for multi-select */
|
|
@@ -38,8 +18,8 @@ type $$ComponentProps = {
|
|
|
38
18
|
placeholder?: string;
|
|
39
19
|
/** Label text for the combobox */
|
|
40
20
|
label?: string;
|
|
41
|
-
/**
|
|
42
|
-
|
|
21
|
+
/** Helper text displayed below the input */
|
|
22
|
+
helperText?: string;
|
|
43
23
|
/** Error message to display */
|
|
44
24
|
errorText?: string;
|
|
45
25
|
/** Success message to display */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { addUnits, currentDateTime, isDateString, isDateOrDateTimeString, isDateTimeString } from '../../helpers/date.js';
|
|
3
4
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
4
5
|
import FormField from '../form-field.svelte';
|
|
5
|
-
import FormLabel from '../form-label.svelte';
|
|
6
6
|
import type { DateUnit, FormFieldSizeOptions } from '../../index.js';
|
|
7
7
|
import Button from '../button/button.svelte';
|
|
8
8
|
|
|
@@ -68,17 +68,17 @@
|
|
|
68
68
|
|
|
69
69
|
$effect(() => {
|
|
70
70
|
if (!value) {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// Use untrack to prevent writes to enabled/value from triggering this effect again
|
|
72
|
+
untrack(() => {
|
|
73
|
+
if (nullable) enabled = false;
|
|
74
|
+
else value = getDefaultValue();
|
|
75
|
+
});
|
|
73
76
|
}
|
|
74
77
|
});
|
|
75
78
|
let disabled = $derived(!enabled);
|
|
76
79
|
</script>
|
|
77
80
|
|
|
78
|
-
<FormField {size}>
|
|
79
|
-
{#if label}
|
|
80
|
-
<FormLabel {id} {required} {label} />
|
|
81
|
-
{/if}
|
|
81
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
82
82
|
<div class:nullable class:disabled>
|
|
83
83
|
<span class="input">
|
|
84
84
|
<input {...{ type }} {id} {placeholder} {disabled} bind:value {required} oninput={onInput} />
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
4
4
|
import FormField from '../form-field.svelte';
|
|
5
|
-
import FormLabel from '../form-label.svelte';
|
|
6
5
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
7
6
|
|
|
8
7
|
const id = uniqueId();
|
|
@@ -32,10 +31,7 @@
|
|
|
32
31
|
} = $props();
|
|
33
32
|
</script>
|
|
34
33
|
|
|
35
|
-
<FormField {size}>
|
|
36
|
-
{#if label}
|
|
37
|
-
<FormLabel {id} {required} {label} />
|
|
38
|
-
{/if}
|
|
34
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
39
35
|
<div>
|
|
40
36
|
<input
|
|
41
37
|
{id}
|
|
@@ -2,24 +2,82 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { ComponentSize } from '../types/size.js';
|
|
4
4
|
import { getMaxWidth, getDisplayType } from '../types/size.js';
|
|
5
|
+
import FormLabel from './form-label.svelte';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
size = 'full',
|
|
9
|
+
label = undefined,
|
|
10
|
+
id = undefined,
|
|
11
|
+
required = false,
|
|
12
|
+
disabled = false,
|
|
13
|
+
helperText = undefined,
|
|
14
|
+
errorText = undefined,
|
|
15
|
+
successText = undefined,
|
|
8
16
|
children
|
|
9
17
|
}: {
|
|
10
18
|
size?: ComponentSize;
|
|
19
|
+
label?: string | undefined;
|
|
20
|
+
id?: string | undefined;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
helperText?: string | undefined;
|
|
24
|
+
errorText?: string | undefined;
|
|
25
|
+
successText?: string | undefined;
|
|
11
26
|
children: Snippet;
|
|
12
27
|
} = $props();
|
|
13
28
|
|
|
14
29
|
let displayType = $derived(getDisplayType(size));
|
|
15
30
|
let maxWidth = $derived(getMaxWidth(size));
|
|
31
|
+
|
|
32
|
+
let showHelperText = $derived(!!helperText && !errorText && !successText);
|
|
33
|
+
let showSuccessText = $derived(!!successText && !errorText);
|
|
34
|
+
let showErrorText = $derived(!!errorText);
|
|
16
35
|
</script>
|
|
17
36
|
|
|
18
37
|
<div style={`display: ${displayType}; width: 100%; min-width: 10rem; max-width: ${maxWidth}`}>
|
|
38
|
+
{#if label}
|
|
39
|
+
<FormLabel {id} {required} {disabled} {label} />
|
|
40
|
+
{/if}
|
|
19
41
|
{@render children?.()}
|
|
42
|
+
{#if showHelperText}
|
|
43
|
+
<div class="helper-text" id="{id}-helper">{helperText}</div>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if showSuccessText}
|
|
46
|
+
<div class="success-text" id="{id}-success" role="status" aria-live="polite">
|
|
47
|
+
{successText}
|
|
48
|
+
</div>
|
|
49
|
+
{/if}
|
|
50
|
+
{#if showErrorText}
|
|
51
|
+
<div class="error-text" id="{id}-error" role="alert" aria-live="assertive">
|
|
52
|
+
{errorText}
|
|
53
|
+
</div>
|
|
54
|
+
{/if}
|
|
20
55
|
</div>
|
|
21
56
|
|
|
22
57
|
<style>div {
|
|
23
58
|
margin-bottom: var(--spacing-base);
|
|
24
59
|
margin-right: var(--spacing-base);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.helper-text {
|
|
63
|
+
font-size: var(--font-sm);
|
|
64
|
+
line-height: 1.25rem;
|
|
65
|
+
padding: var(--spacing-xs);
|
|
66
|
+
color: var(--body-fg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.success-text {
|
|
70
|
+
font-size: var(--font-sm);
|
|
71
|
+
line-height: 1.25rem;
|
|
72
|
+
padding: var(--spacing-xs);
|
|
73
|
+
color: var(--success, #28a745);
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.error-text {
|
|
78
|
+
font-size: var(--font-sm);
|
|
79
|
+
line-height: 1.25rem;
|
|
80
|
+
padding: var(--spacing-xs);
|
|
81
|
+
color: var(--danger, #dc3545);
|
|
82
|
+
font-weight: 500;
|
|
25
83
|
}</style>
|
|
@@ -2,6 +2,13 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { ComponentSize } from '../types/size.js';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
size?: ComponentSize;
|
|
5
|
+
label?: string | undefined;
|
|
6
|
+
id?: string | undefined;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
helperText?: string | undefined;
|
|
10
|
+
errorText?: string | undefined;
|
|
11
|
+
successText?: string | undefined;
|
|
5
12
|
children: Snippet;
|
|
6
13
|
};
|
|
7
14
|
declare const FormField: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: Snippet;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let { children }: Props = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div class="form-row">
|
|
12
|
+
{@render children()}
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<style>/* ============================================
|
|
16
|
+
BREAKPOINTS - Responsive Design
|
|
17
|
+
============================================ */
|
|
18
|
+
.form-row {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
gap: var(--spacing-base);
|
|
22
|
+
align-items: flex-start;
|
|
23
|
+
}
|
|
24
|
+
@media (max-width: 479.98px) {
|
|
25
|
+
.form-row {
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
align-items: stretch;
|
|
28
|
+
}
|
|
29
|
+
}</style>
|
package/dist/forms/form.svelte
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import LinkIcon from '../../icons/link-icon.svelte';
|
|
3
3
|
import type { FormFieldSizeOptions } from '../../index.js';
|
|
4
4
|
import FormField from '../form-field.svelte';
|
|
5
|
-
import FormLabel from '../form-label.svelte';
|
|
6
5
|
|
|
7
6
|
let {
|
|
8
7
|
size = 'md' as FormFieldSizeOptions,
|
|
@@ -17,10 +16,7 @@
|
|
|
17
16
|
} = $props();
|
|
18
17
|
</script>
|
|
19
18
|
|
|
20
|
-
<FormField {size}>
|
|
21
|
-
{#if label}
|
|
22
|
-
<FormLabel {label} />
|
|
23
|
-
{/if}
|
|
19
|
+
<FormField {size} {label}>
|
|
24
20
|
<div class="input">
|
|
25
21
|
{#if href}
|
|
26
22
|
<span class="icon">
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { DropdownOption, FormFieldSizeOptions, MenuOption } from '../../types/form.js';
|
|
3
3
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
4
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
6
5
|
import Menu from '../../generic/menu/menu.svelte';
|
|
7
6
|
import AngleUpIcon from '../../icons/angle-up-icon.svelte';
|
|
@@ -151,10 +150,7 @@
|
|
|
151
150
|
let open = $derived(isMenuOpen && !disabled);
|
|
152
151
|
</script>
|
|
153
152
|
|
|
154
|
-
<FormField {size}>
|
|
155
|
-
{#if label}
|
|
156
|
-
<FormLabel {id} {required} {disabled} {label} />
|
|
157
|
-
{/if}
|
|
153
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
158
154
|
<div class="{open ? 'open' : 'closed'} {disabled ? 'disabled' : 'enabled'}">
|
|
159
155
|
<input
|
|
160
156
|
type="text"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { uniqueId, type FormFieldSizeOptions } from '../../index.js';
|
|
3
3
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
4
|
import { untrack } from 'svelte';
|
|
6
5
|
|
|
7
6
|
const id = uniqueId();
|
|
@@ -58,8 +57,11 @@
|
|
|
58
57
|
]);
|
|
59
58
|
$effect(() => {
|
|
60
59
|
if (value !== null && value >= 0) {
|
|
61
|
-
dollars
|
|
62
|
-
|
|
60
|
+
// Use untrack to prevent writes to dollars/cents from triggering this effect again
|
|
61
|
+
untrack(() => {
|
|
62
|
+
dollars = getDollarsFromValue();
|
|
63
|
+
cents = getCentsFromValue();
|
|
64
|
+
});
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
67
|
|
|
@@ -207,10 +209,7 @@
|
|
|
207
209
|
|
|
208
210
|
</script>
|
|
209
211
|
|
|
210
|
-
<FormField {size}>
|
|
211
|
-
{#if label}
|
|
212
|
-
<FormLabel {id} {label} />
|
|
213
|
-
{/if}
|
|
212
|
+
<FormField {size} {label} {id}>
|
|
214
213
|
<div class="input {currency}" class:allowCents {id}>
|
|
215
214
|
{#if prefix}
|
|
216
215
|
<span class="prefix">{prefix}</span>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { roundToDecimals } from '../../helpers/round-to-decimals.js';
|
|
3
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
4
4
|
import FormField from '../form-field.svelte';
|
|
5
|
-
import FormLabel from '../form-label.svelte';
|
|
6
5
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
7
6
|
const id = uniqueId();
|
|
8
7
|
|
|
@@ -64,10 +63,7 @@
|
|
|
64
63
|
};
|
|
65
64
|
</script>
|
|
66
65
|
|
|
67
|
-
<FormField {size}>
|
|
68
|
-
{#if label}
|
|
69
|
-
<FormLabel {id} {label} />
|
|
70
|
-
{/if}
|
|
66
|
+
<FormField {size} {label} {id}>
|
|
71
67
|
<div class="input {type}">
|
|
72
68
|
{#if prefix}
|
|
73
69
|
<span class="prefix">{prefix}</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { uniqueId, type FormFieldSizeOptions } from '../../index.js';
|
|
3
4
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
value = $bindable('' as string | null),
|
|
@@ -135,16 +135,18 @@
|
|
|
135
135
|
setValue(value ?? '');
|
|
136
136
|
|
|
137
137
|
$effect(() => {
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// Only trigger when the phone number parts change
|
|
139
|
+
const hasValue = areaCode || localExt || lastFour;
|
|
140
|
+
if (hasValue) {
|
|
141
|
+
// Use untrack to prevent value changes from triggering this effect again
|
|
142
|
+
untrack(() => {
|
|
143
|
+
publishChange();
|
|
144
|
+
});
|
|
140
145
|
}
|
|
141
146
|
});
|
|
142
147
|
</script>
|
|
143
148
|
|
|
144
|
-
<FormField {size}>
|
|
145
|
-
{#if label}
|
|
146
|
-
<FormLabel id="{id}-areaCode" {label} />
|
|
147
|
-
{/if}
|
|
149
|
+
<FormField {size} {label} id="{id}-areaCode">
|
|
148
150
|
<div class="input">
|
|
149
151
|
<span class="areaCode segment">
|
|
150
152
|
<span>(</span>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
|
|
3
3
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
4
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
6
5
|
import RadioBox from './radio-box.svelte';
|
|
7
6
|
|
|
@@ -24,10 +23,7 @@
|
|
|
24
23
|
} = $props();
|
|
25
24
|
</script>
|
|
26
25
|
|
|
27
|
-
<FormField {size}>
|
|
28
|
-
{#if label}
|
|
29
|
-
<FormLabel {id} {required} {label} />
|
|
30
|
-
{/if}
|
|
26
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
31
27
|
<div>
|
|
32
28
|
{#each items as item}
|
|
33
29
|
<RadioBox bind:group {disabled} value={item.value}>{item.name}</RadioBox>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
3
3
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
4
|
import type { ComponentSize } from '../../types/size.js';
|
|
6
5
|
|
|
7
6
|
const id = uniqueId();
|
|
@@ -53,10 +52,7 @@
|
|
|
53
52
|
let displayValue = $derived(formatValue ? formatValue(value) : String(value));
|
|
54
53
|
</script>
|
|
55
54
|
|
|
56
|
-
<FormField {size}>
|
|
57
|
-
{#if label}
|
|
58
|
-
<FormLabel {id} {label} />
|
|
59
|
-
{/if}
|
|
55
|
+
<FormField {size} {label} {id} {disabled}>
|
|
60
56
|
<div class="slider-wrapper">
|
|
61
57
|
<div class="slider-track-container">
|
|
62
58
|
<input
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
3
4
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
5
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
6
6
|
|
|
7
7
|
const id = uniqueId();
|
|
@@ -55,31 +55,34 @@
|
|
|
55
55
|
// Run auto-resize when value changes
|
|
56
56
|
$effect(() => {
|
|
57
57
|
if (value !== null && value !== undefined) {
|
|
58
|
-
|
|
58
|
+
// Use untrack to prevent DOM manipulations from triggering effects
|
|
59
|
+
untrack(() => {
|
|
60
|
+
handleAutoResize();
|
|
61
|
+
});
|
|
59
62
|
}
|
|
60
63
|
});
|
|
61
64
|
|
|
62
65
|
// Run auto-resize on mount
|
|
63
66
|
$effect(() => {
|
|
64
67
|
if (textareaElement && autoResize) {
|
|
65
|
-
|
|
68
|
+
// Use untrack to prevent DOM manipulations from triggering effects
|
|
69
|
+
untrack(() => {
|
|
70
|
+
handleAutoResize();
|
|
71
|
+
});
|
|
66
72
|
}
|
|
67
73
|
});
|
|
68
74
|
</script>
|
|
69
75
|
|
|
70
|
-
<FormField {size}>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{placeholder}
|
|
78
|
-
rows={autoResize ? minRows : rows}
|
|
79
|
-
bind:value
|
|
76
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
77
|
+
<textarea
|
|
78
|
+
wrap="soft"
|
|
79
|
+
{id}
|
|
80
|
+
{placeholder}
|
|
81
|
+
rows={autoResize ? minRows : rows}
|
|
82
|
+
bind:value
|
|
80
83
|
bind:this={textareaElement}
|
|
81
|
-
{required}
|
|
82
|
-
{disabled}
|
|
84
|
+
{required}
|
|
85
|
+
{disabled}
|
|
83
86
|
{readonly}
|
|
84
87
|
data-auto-resize={autoResize}
|
|
85
88
|
oninput={handleAutoResize}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
3
4
|
import { animateShake, animateScaleIn } from '../../helpers/animations.js';
|
|
4
5
|
import FormField from '../form-field.svelte';
|
|
5
|
-
import FormLabel from '../form-label.svelte';
|
|
6
6
|
import CheckIcon from '../../icons/check-icon.svelte';
|
|
7
7
|
import type { AllowedTextInputTypes, FormFieldSizeOptions } from '../../types/form.js';
|
|
8
8
|
|
|
@@ -73,26 +73,45 @@
|
|
|
73
73
|
: 'near-limit'
|
|
74
74
|
: ''
|
|
75
75
|
);
|
|
76
|
-
|
|
76
|
+
|
|
77
|
+
// Update describedByIds array when helper/error/success text changes
|
|
77
78
|
$effect(() => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// Track the dependencies we care about
|
|
80
|
+
const hasHelper = !!helperText;
|
|
81
|
+
const hasErrorMsg = !!errorText;
|
|
82
|
+
const hasSuccessMsg = !!successText;
|
|
83
|
+
|
|
84
|
+
// Use untrack to write to describedByIds without triggering this effect again
|
|
85
|
+
untrack(() => {
|
|
86
|
+
describedByIds = [];
|
|
87
|
+
if (hasHelper) describedByIds.push(`${id}-helper`);
|
|
88
|
+
if (hasErrorMsg) describedByIds.push(`${id}-error`);
|
|
89
|
+
if (hasSuccessMsg) describedByIds.push(`${id}-success`);
|
|
90
|
+
});
|
|
82
91
|
});
|
|
83
92
|
|
|
84
|
-
// Trigger shake animation when error appears
|
|
93
|
+
// Trigger shake animation when error appears (track previous error state)
|
|
94
|
+
let prevHasError = $state(false);
|
|
85
95
|
$effect(() => {
|
|
86
|
-
if (hasError && inputElement) {
|
|
87
|
-
|
|
96
|
+
if (hasError && !prevHasError && inputElement) {
|
|
97
|
+
// Use untrack to prevent animation from triggering effect again
|
|
98
|
+
untrack(() => {
|
|
99
|
+
animateShake(inputElement!); // Already checked above
|
|
100
|
+
});
|
|
88
101
|
}
|
|
102
|
+
prevHasError = hasError;
|
|
89
103
|
});
|
|
90
104
|
|
|
91
|
-
// Trigger scale-in animation when success appears
|
|
105
|
+
// Trigger scale-in animation when success appears (track previous success state)
|
|
106
|
+
let prevHasSuccess = $state(false);
|
|
92
107
|
$effect(() => {
|
|
93
|
-
if (hasSuccess && successIconElement) {
|
|
94
|
-
|
|
108
|
+
if (hasSuccess && !prevHasSuccess && successIconElement) {
|
|
109
|
+
// Use untrack to prevent animation from triggering effect again
|
|
110
|
+
untrack(() => {
|
|
111
|
+
animateScaleIn(successIconElement!); // Already checked above
|
|
112
|
+
});
|
|
95
113
|
}
|
|
114
|
+
prevHasSuccess = hasSuccess;
|
|
96
115
|
});
|
|
97
116
|
|
|
98
117
|
// Don't allow certain characters to be typed into the input
|
|
@@ -130,11 +149,13 @@
|
|
|
130
149
|
};
|
|
131
150
|
</script>
|
|
132
151
|
|
|
133
|
-
<FormField {size}>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
<FormField {size} {label} {id} {required} {disabled} {helperText} {errorText} {successText}>
|
|
153
|
+
<div
|
|
154
|
+
class="input {disabled ? 'disabled' : 'enabled'}"
|
|
155
|
+
class:error={hasError}
|
|
156
|
+
class:success={hasSuccess}
|
|
157
|
+
bind:this={inputElement}
|
|
158
|
+
>
|
|
138
159
|
{#if prefix}
|
|
139
160
|
<div class="prefix">{prefix}</div>
|
|
140
161
|
{/if}
|
|
@@ -174,19 +195,6 @@
|
|
|
174
195
|
{characterCount} / {maxlength}
|
|
175
196
|
</div>
|
|
176
197
|
{/if}
|
|
177
|
-
{#if helperText}
|
|
178
|
-
<div class="helper-text" id="{id}-helper">{helperText}</div>
|
|
179
|
-
{/if}
|
|
180
|
-
{#if successText}
|
|
181
|
-
<div class="success-text" id="{id}-success" role="status" aria-live="polite">
|
|
182
|
-
{successText}
|
|
183
|
-
</div>
|
|
184
|
-
{/if}
|
|
185
|
-
{#if errorText}
|
|
186
|
-
<div class="error-text" id="{id}-error" role="alert" aria-live="assertive">
|
|
187
|
-
{errorText}
|
|
188
|
-
</div>
|
|
189
|
-
{/if}
|
|
190
198
|
</FormField>
|
|
191
199
|
|
|
192
200
|
<style>.input {
|
|
@@ -283,6 +291,8 @@
|
|
|
283
291
|
padding: var(--spacing-xs);
|
|
284
292
|
text-align: right;
|
|
285
293
|
color: var(--body-fg);
|
|
294
|
+
position: absolute;
|
|
295
|
+
right: 0;
|
|
286
296
|
}
|
|
287
297
|
.character-count.near-limit {
|
|
288
298
|
color: var(--warning, #ffc107);
|
|
@@ -291,27 +301,4 @@
|
|
|
291
301
|
.character-count.at-limit {
|
|
292
302
|
color: var(--danger, #dc3545);
|
|
293
303
|
font-weight: 600;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
.helper-text {
|
|
297
|
-
font-size: var(--font-sm);
|
|
298
|
-
line-height: 1.25rem;
|
|
299
|
-
padding: var(--spacing-xs);
|
|
300
|
-
color: var(--body-fg);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.success-text {
|
|
304
|
-
font-size: var(--font-sm);
|
|
305
|
-
line-height: 1.25rem;
|
|
306
|
-
padding: var(--spacing-xs);
|
|
307
|
-
color: var(--success, #28a745);
|
|
308
|
-
font-weight: 500;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.error-text {
|
|
312
|
-
font-size: var(--font-sm);
|
|
313
|
-
line-height: 1.25rem;
|
|
314
|
-
padding: var(--spacing-xs);
|
|
315
|
-
color: var(--danger, #dc3545);
|
|
316
|
-
font-weight: 500;
|
|
317
304
|
}</style>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
3
3
|
import FormField from '../form-field.svelte';
|
|
4
|
-
import FormLabel from '../form-label.svelte';
|
|
5
4
|
import type { ComponentSize } from '../../types/size.js';
|
|
6
5
|
|
|
7
6
|
const id = uniqueId();
|
|
@@ -29,10 +28,7 @@
|
|
|
29
28
|
};
|
|
30
29
|
</script>
|
|
31
30
|
|
|
32
|
-
<FormField {size}>
|
|
33
|
-
{#if label}
|
|
34
|
-
<FormLabel {id} {required} {label} />
|
|
35
|
-
{/if}
|
|
31
|
+
<FormField {size} {label} {id} {required} {disabled}>
|
|
36
32
|
<input
|
|
37
33
|
{id}
|
|
38
34
|
type="time"
|