sveltacular 1.0.4 → 1.0.6
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 +21 -2
- package/dist/forms/base-input-wrapper.svelte +0 -2
- package/dist/forms/check-box/check-box-group.svelte +2 -2
- package/dist/forms/date-box/date-box.svelte +15 -4
- package/dist/forms/file-box/file-box.svelte +1 -1
- package/dist/forms/form-field/form-field.svelte +86 -0
- package/dist/forms/form-field/form-field.svelte.d.ts +16 -0
- package/dist/forms/form-footer.svelte +7 -5
- package/dist/forms/form-label/form-label.svelte +30 -0
- package/dist/forms/form-label/form-label.svelte.d.ts +9 -0
- package/dist/forms/form-row/form-row.svelte +29 -0
- package/dist/forms/form-row/form-row.svelte.d.ts +7 -0
- package/dist/forms/form-section/form-section.svelte +36 -0
- package/dist/forms/form-section/form-section.svelte.d.ts +10 -0
- package/dist/forms/index.d.ts +10 -6
- package/dist/forms/index.js +10 -7
- package/dist/forms/info-box/info-box.svelte +1 -1
- package/dist/forms/list-box/list-box.svelte +3 -3
- package/dist/forms/money-box/money-box.svelte +27 -17
- package/dist/forms/number-box/number-box.svelte +5 -2
- package/dist/forms/number-range-box/number-range-box.svelte +218 -0
- package/dist/forms/number-range-box/number-range-box.svelte.d.ts +21 -0
- package/dist/forms/phone-box/phone-box.svelte +25 -13
- package/dist/forms/radio-group/radio-group.svelte +1 -1
- package/dist/forms/slider/slider.svelte +15 -15
- package/dist/forms/tag-input-box/tag-input-box.svelte +203 -0
- package/dist/forms/tag-input-box/tag-input-box.svelte.d.ts +17 -0
- package/dist/forms/text-area/text-area.svelte +1 -1
- package/dist/forms/text-box/text-box.svelte +1 -1
- package/dist/forms/time-box/time-box.svelte +42 -17
- package/dist/generic/avatar/avatar.svelte +3 -0
- package/dist/generic/chip/chip.svelte +3 -0
- package/dist/navigation/context-menu/README.md +3 -0
- package/dist/navigation/context-menu/context-menu-divider.svelte +3 -0
- package/dist/sveltacular.css +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,18 +12,37 @@ npm i sveltacular
|
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### 1. Import the default stylesheet (once, in your app root)
|
|
16
|
+
|
|
17
|
+
**For SvelteKit**, add this to `src/routes/+layout.svelte`:
|
|
16
18
|
|
|
17
19
|
```svelte
|
|
18
20
|
<script lang="ts">
|
|
19
21
|
import 'sveltacular/styles.css';
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<slot />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**For regular Svelte**, add it to your main `App.svelte` or root component:
|
|
28
|
+
|
|
29
|
+
```svelte
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import 'sveltacular/styles.css';
|
|
32
|
+
</script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Use components anywhere
|
|
36
|
+
|
|
37
|
+
```svelte
|
|
38
|
+
<script lang="ts">
|
|
20
39
|
import { Button } from 'sveltacular';
|
|
21
40
|
</script>
|
|
22
41
|
|
|
23
42
|
<Button variant="primary" label="Hello World" />
|
|
24
43
|
```
|
|
25
44
|
|
|
26
|
-
**Note**: The
|
|
45
|
+
**Note**: The stylesheet import is required for components to render with default styling. Import it once at the app level (not in individual components). If you prefer to provide your own theme, you can skip this import and define your own CSS variables (see [Theming](#theming) below).
|
|
27
46
|
|
|
28
47
|
## Component Catalog
|
|
29
48
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
|
|
4
|
-
import FormField from '../form-field.svelte';
|
|
4
|
+
import FormField from '../form-field/form-field.svelte';
|
|
5
5
|
import CheckBox from './check-box.svelte';
|
|
6
6
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
7
7
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// Track items and group as dependencies
|
|
35
35
|
const currentItems = items;
|
|
36
36
|
const currentGroup = group;
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
// Use untrack to prevent writing to itemsWithState from triggering this effect again
|
|
39
39
|
untrack(() => {
|
|
40
40
|
// Rebuild itemsWithState from items, using group to determine checked state
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
addUnits,
|
|
5
|
+
currentDateTime,
|
|
6
|
+
isDateString,
|
|
7
|
+
isDateOrDateTimeString,
|
|
8
|
+
isDateTimeString
|
|
9
|
+
} from '../../helpers/date.js';
|
|
4
10
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
5
|
-
import FormField from '../form-field.svelte';
|
|
11
|
+
import FormField from '../form-field/form-field.svelte';
|
|
6
12
|
import type { DateUnit, FormFieldSizeOptions } from '../../index.js';
|
|
7
13
|
import Button from '../button/button.svelte';
|
|
8
14
|
|
|
9
|
-
type DateIncrementStep = { label: string; value: number
|
|
15
|
+
type DateIncrementStep = { label: string; value: number; unit: DateUnit };
|
|
10
16
|
|
|
11
17
|
const id = uniqueId();
|
|
12
18
|
|
|
@@ -91,7 +97,12 @@
|
|
|
91
97
|
{#if steps.length > 0}
|
|
92
98
|
<span class="steps">
|
|
93
99
|
{#each steps as step}
|
|
94
|
-
<Button
|
|
100
|
+
<Button
|
|
101
|
+
noMargin={true}
|
|
102
|
+
collapse={true}
|
|
103
|
+
onClick={() => incrementValue(step)}
|
|
104
|
+
label={step.label}
|
|
105
|
+
/>
|
|
95
106
|
{/each}
|
|
96
107
|
</span>
|
|
97
108
|
{/if}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
4
|
-
import FormField from '../form-field.svelte';
|
|
4
|
+
import FormField from '../form-field/form-field.svelte';
|
|
5
5
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
6
6
|
|
|
7
7
|
const id = uniqueId();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { ComponentSize } from '../../types/size.js';
|
|
4
|
+
import { getMaxWidth, getDisplayType } from '../../types/size.js';
|
|
5
|
+
import FormLabel from '../form-label/form-label.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
size = 'full',
|
|
9
|
+
label = undefined,
|
|
10
|
+
id = undefined,
|
|
11
|
+
required = false,
|
|
12
|
+
disabled = false,
|
|
13
|
+
helperText = undefined,
|
|
14
|
+
errorText = undefined,
|
|
15
|
+
successText = undefined,
|
|
16
|
+
children
|
|
17
|
+
}: {
|
|
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;
|
|
26
|
+
children: Snippet;
|
|
27
|
+
} = $props();
|
|
28
|
+
|
|
29
|
+
let displayType = $derived(getDisplayType(size));
|
|
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);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div class="form-field {size} {displayType} {maxWidth}">
|
|
38
|
+
{#if label}
|
|
39
|
+
<FormLabel {id} {required} {disabled} {label} />
|
|
40
|
+
{/if}
|
|
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}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style>/* ============================================
|
|
58
|
+
BREAKPOINTS - Responsive Design
|
|
59
|
+
============================================ */
|
|
60
|
+
.form-field {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 0.25rem;
|
|
64
|
+
flex: 1;
|
|
65
|
+
}
|
|
66
|
+
@media (max-width: 479.98px) {
|
|
67
|
+
.form-field {
|
|
68
|
+
width: 100%;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.success-text {
|
|
73
|
+
font-size: var(--font-sm);
|
|
74
|
+
line-height: 1.25rem;
|
|
75
|
+
padding: var(--spacing-xs);
|
|
76
|
+
color: var(--success, #28a745);
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.error-text {
|
|
81
|
+
font-size: var(--font-sm);
|
|
82
|
+
line-height: 1.25rem;
|
|
83
|
+
padding: var(--spacing-xs);
|
|
84
|
+
color: var(--danger, #dc3545);
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
}</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ComponentSize } from '../../types/size.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
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;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
};
|
|
14
|
+
declare const FormField: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type FormField = ReturnType<typeof FormField>;
|
|
16
|
+
export default FormField;
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import FlexRow from '../layout/flex-row.svelte';
|
|
4
3
|
|
|
5
4
|
let { children }: { children: Snippet } = $props();
|
|
6
5
|
</script>
|
|
7
6
|
|
|
8
7
|
<div>
|
|
9
|
-
|
|
10
|
-
{@render children?.()}
|
|
11
|
-
</FlexRow>
|
|
8
|
+
{@render children?.()}
|
|
12
9
|
</div>
|
|
13
10
|
|
|
14
11
|
<style>
|
|
15
12
|
div {
|
|
16
|
-
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
gap: var(--spacing-base);
|
|
16
|
+
justify-content: flex-end;
|
|
17
|
+
margin-top: var(--spacing-lg);
|
|
18
|
+
margin-top: var(--spacing-xl);
|
|
17
19
|
}
|
|
18
20
|
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
id = undefined,
|
|
4
|
+
required = false,
|
|
5
|
+
disabled = false,
|
|
6
|
+
label = ''
|
|
7
|
+
}: {
|
|
8
|
+
id?: string | undefined;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
label?: string;
|
|
12
|
+
} = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<label for={id} class:required class:disabled aria-required={required}>{label}</label>
|
|
16
|
+
|
|
17
|
+
<style>label {
|
|
18
|
+
display: block;
|
|
19
|
+
margin-bottom: var(--spacing-sm);
|
|
20
|
+
font-weight: 500;
|
|
21
|
+
font-size: var(--font-base);
|
|
22
|
+
}
|
|
23
|
+
label.required::after {
|
|
24
|
+
content: "*";
|
|
25
|
+
margin-left: var(--spacing-xs);
|
|
26
|
+
}
|
|
27
|
+
label.disabled {
|
|
28
|
+
opacity: 0.5;
|
|
29
|
+
cursor: not-allowed;
|
|
30
|
+
}</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
id?: string | undefined;
|
|
3
|
+
required?: boolean;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const FormLabel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type FormLabel = ReturnType<typeof FormLabel>;
|
|
9
|
+
export default FormLabel;
|
|
@@ -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>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { SectionLevel } from '../../types/generic.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
title = undefined,
|
|
7
|
+
level = 4,
|
|
8
|
+
children
|
|
9
|
+
}: {
|
|
10
|
+
title?: string | undefined;
|
|
11
|
+
level?: SectionLevel;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
} = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<fieldset>
|
|
17
|
+
{#if title}
|
|
18
|
+
<legend aria-level={level}>{title}</legend>
|
|
19
|
+
{/if}
|
|
20
|
+
{@render children?.()}
|
|
21
|
+
</fieldset>
|
|
22
|
+
|
|
23
|
+
<style>fieldset {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
gap: var(--spacing-base);
|
|
27
|
+
border: var(--border-thin) solid var(--form-input-border);
|
|
28
|
+
border-radius: var(--radius-md);
|
|
29
|
+
padding: var(--spacing-base);
|
|
30
|
+
}
|
|
31
|
+
fieldset legend {
|
|
32
|
+
font-size: var(--font-lg);
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
line-height: var(--line-height-base);
|
|
35
|
+
color: var(--form-input-fg);
|
|
36
|
+
}</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { SectionLevel } from '../../types/generic.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
title?: string | undefined;
|
|
5
|
+
level?: SectionLevel;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const FormSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type FormSection = ReturnType<typeof FormSection>;
|
|
10
|
+
export default FormSection;
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -7,20 +7,24 @@ export { default as InfoBox } from './info-box/info-box.svelte';
|
|
|
7
7
|
export { default as MoneyBox } from './money-box/money-box.svelte';
|
|
8
8
|
export { default as NewOrExistingCombo } from './combo/new-or-existing-combo.svelte';
|
|
9
9
|
export { default as NumberBox } from './number-box/number-box.svelte';
|
|
10
|
+
export { default as NumberRangeBox } from './number-range-box/number-range-box.svelte';
|
|
11
|
+
export { default as PhoneBox } from './phone-box/phone-box.svelte';
|
|
12
|
+
export { default as Slider } from './slider/slider.svelte';
|
|
13
|
+
export { default as SwitchBox } from './switch-box/switch-box.svelte';
|
|
14
|
+
export { default as TagInputBox } from './tag-input-box/tag-input-box.svelte';
|
|
10
15
|
export { default as TextArea } from './text-area/text-area.svelte';
|
|
11
16
|
export { default as TextBox } from './text-box/text-box.svelte';
|
|
17
|
+
export { default as TimeBox } from './time-box/time-box.svelte';
|
|
12
18
|
export { default as UrlBox } from './url-box/url-box.svelte';
|
|
13
19
|
export * from './check-box/index.js';
|
|
14
|
-
export * from './combo-box/index.js';
|
|
15
20
|
export * from './list-box/index.js';
|
|
16
21
|
export * from './phone-box/index.js';
|
|
17
22
|
export * from './radio-group/index.js';
|
|
18
23
|
export { default as Form } from './form.svelte';
|
|
19
|
-
export { default as FormField } from './form-field.svelte';
|
|
24
|
+
export { default as FormField } from './form-field/form-field.svelte';
|
|
20
25
|
export { default as FormFooter } from './form-footer.svelte';
|
|
21
26
|
export { default as FormHeader } from './form-header.svelte';
|
|
22
|
-
export { default as FormLabel } from './form-label.svelte';
|
|
23
|
-
export { default as FormSection } from './form-section.svelte';
|
|
24
|
-
export { default as
|
|
25
|
-
export { default as TimeBox } from './time-box/time-box.svelte';
|
|
27
|
+
export { default as FormLabel } from './form-label/form-label.svelte';
|
|
28
|
+
export { default as FormSection } from './form-section/form-section.svelte';
|
|
29
|
+
export { default as FormRow } from './form-row/form-row.svelte';
|
|
26
30
|
export * from './validation.js';
|
package/dist/forms/index.js
CHANGED
|
@@ -8,24 +8,27 @@ export { default as InfoBox } from './info-box/info-box.svelte';
|
|
|
8
8
|
export { default as MoneyBox } from './money-box/money-box.svelte';
|
|
9
9
|
export { default as NewOrExistingCombo } from './combo/new-or-existing-combo.svelte';
|
|
10
10
|
export { default as NumberBox } from './number-box/number-box.svelte';
|
|
11
|
+
export { default as NumberRangeBox } from './number-range-box/number-range-box.svelte';
|
|
12
|
+
export { default as PhoneBox } from './phone-box/phone-box.svelte';
|
|
13
|
+
export { default as Slider } from './slider/slider.svelte';
|
|
14
|
+
export { default as SwitchBox } from './switch-box/switch-box.svelte';
|
|
15
|
+
export { default as TagInputBox } from './tag-input-box/tag-input-box.svelte';
|
|
11
16
|
export { default as TextArea } from './text-area/text-area.svelte';
|
|
12
17
|
export { default as TextBox } from './text-box/text-box.svelte';
|
|
18
|
+
export { default as TimeBox } from './time-box/time-box.svelte';
|
|
13
19
|
export { default as UrlBox } from './url-box/url-box.svelte';
|
|
14
20
|
// Form components with barrel files
|
|
15
21
|
export * from './check-box/index.js';
|
|
16
|
-
export * from './combo-box/index.js';
|
|
17
22
|
export * from './list-box/index.js';
|
|
18
23
|
export * from './phone-box/index.js';
|
|
19
24
|
export * from './radio-group/index.js';
|
|
20
25
|
// Form structure components
|
|
21
26
|
export { default as Form } from './form.svelte';
|
|
22
|
-
export { default as FormField } from './form-field.svelte';
|
|
27
|
+
export { default as FormField } from './form-field/form-field.svelte';
|
|
23
28
|
export { default as FormFooter } from './form-footer.svelte';
|
|
24
29
|
export { default as FormHeader } from './form-header.svelte';
|
|
25
|
-
export { default as FormLabel } from './form-label.svelte';
|
|
26
|
-
export { default as FormSection } from './form-section.svelte';
|
|
27
|
-
|
|
28
|
-
export { default as Slider } from './slider/slider.svelte';
|
|
29
|
-
export { default as TimeBox } from './time-box/time-box.svelte';
|
|
30
|
+
export { default as FormLabel } from './form-label/form-label.svelte';
|
|
31
|
+
export { default as FormSection } from './form-section/form-section.svelte';
|
|
32
|
+
export { default as FormRow } from './form-row/form-row.svelte';
|
|
30
33
|
// Validation utilities
|
|
31
34
|
export * from './validation.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import LinkIcon from '../../icons/link-icon.svelte';
|
|
3
3
|
import type { FormFieldSizeOptions } from '../../index.js';
|
|
4
|
-
import FormField from '../form-field.svelte';
|
|
4
|
+
import FormField from '../form-field/form-field.svelte';
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
size = 'md' as FormFieldSizeOptions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { DropdownOption, FormFieldSizeOptions, MenuOption } from '../../types/form.js';
|
|
3
|
-
import FormField from '../form-field.svelte';
|
|
3
|
+
import FormField 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';
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
let highlightIndex = $state(-1);
|
|
46
46
|
let filteredItems = $state<MenuOption[]>([]);
|
|
47
47
|
let isSeachable = $derived(searchable || !!search);
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
// Get the ID of the highlighted option for ARIA
|
|
50
50
|
let activeDescendant = $derived(
|
|
51
51
|
highlightIndex >= 0 && filteredItems[highlightIndex]
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
{open}
|
|
202
202
|
closeAfterSelect={false}
|
|
203
203
|
searchText={text}
|
|
204
|
-
|
|
204
|
+
{onSelect}
|
|
205
205
|
size="full"
|
|
206
206
|
bind:highlightIndex
|
|
207
207
|
bind:value
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { uniqueId, type FormFieldSizeOptions } from '../../index.js';
|
|
3
|
-
import FormField from '../form-field.svelte';
|
|
3
|
+
import FormField from '../form-field/form-field.svelte';
|
|
4
4
|
import { untrack } from 'svelte';
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
const id = uniqueId();
|
|
7
7
|
|
|
8
8
|
let {
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
|
|
36
36
|
let isValueInCents = $derived(units === 'cents');
|
|
37
37
|
const fieldOrder = ['dollars', 'cents'];
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
const getDollarsFromValue = () => {
|
|
40
40
|
if (!value) return '0';
|
|
41
41
|
if (isValueInCents) return String(Math.abs(Math.floor(value / 100)));
|
|
42
42
|
return String(Math.abs(Math.floor(value)));
|
|
43
|
-
}
|
|
43
|
+
};
|
|
44
44
|
|
|
45
45
|
const getCentsFromValue = () => {
|
|
46
46
|
if (!value) return '00';
|
|
47
47
|
if (isValueInCents) return String(Math.abs(Math.round(value % 100))).padStart(2, '0');
|
|
48
48
|
return String(Math.abs(Math.round((value % 1) * 100))).padStart(2, '0');
|
|
49
|
-
}
|
|
49
|
+
};
|
|
50
50
|
|
|
51
51
|
let dollars = $state(getDollarsFromValue());
|
|
52
52
|
let cents = $state(getCentsFromValue());
|
|
@@ -74,9 +74,10 @@
|
|
|
74
74
|
const selection = [target.selectionStart ?? 0, target.selectionEnd ?? 0];
|
|
75
75
|
const key = e instanceof KeyboardEvent ? e.key : '';
|
|
76
76
|
const isNumber = !isNaN(Number(key));
|
|
77
|
-
const isDecimal =key === '.';
|
|
77
|
+
const isDecimal = key === '.';
|
|
78
78
|
const isBackspace = key === 'Backspace';
|
|
79
|
-
const isAllowed =
|
|
79
|
+
const isAllowed =
|
|
80
|
+
isNumber || isDecimal || ['Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(key);
|
|
80
81
|
return {
|
|
81
82
|
element: target,
|
|
82
83
|
name,
|
|
@@ -91,8 +92,8 @@
|
|
|
91
92
|
isAllowed,
|
|
92
93
|
lastState: lastState[index],
|
|
93
94
|
value: target.value,
|
|
94
|
-
next: nextName ? document.getElementById(`${id}-${nextName}`) as HTMLInputElement : null,
|
|
95
|
-
previous: prevName ? document.getElementById(`${id}-${prevName}`) as HTMLInputElement : null
|
|
95
|
+
next: nextName ? (document.getElementById(`${id}-${nextName}`) as HTMLInputElement) : null,
|
|
96
|
+
previous: prevName ? (document.getElementById(`${id}-${prevName}`) as HTMLInputElement) : null
|
|
96
97
|
};
|
|
97
98
|
};
|
|
98
99
|
|
|
@@ -116,8 +117,8 @@
|
|
|
116
117
|
|
|
117
118
|
const moveExtraCentsToDollars = (centsValue: string, append = true) => {
|
|
118
119
|
if (centsValue.length > 2 && isNumericString(centsValue) && Number(centsValue) > 0) {
|
|
119
|
-
const whole = centsValue.substring(0, centsValue.length -2);
|
|
120
|
-
const decimal = centsValue.substring(centsValue.length -2);
|
|
120
|
+
const whole = centsValue.substring(0, centsValue.length - 2);
|
|
121
|
+
const decimal = centsValue.substring(centsValue.length - 2);
|
|
121
122
|
dollars = append ? `${dollars}${whole}` : whole;
|
|
122
123
|
cents = decimal;
|
|
123
124
|
}
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
e.preventDefault();
|
|
131
132
|
if (target.next && allowCents) focusAndHighlightText(target.next);
|
|
132
133
|
return;
|
|
133
|
-
}
|
|
134
|
+
}
|
|
134
135
|
if (target.name === 'cents' && target.value.length >= 2 && !target.isHighligted) {
|
|
135
136
|
if (target.isNumber) moveExtraCentsToDollars(`${target.value}${e.key}`);
|
|
136
137
|
return e.preventDefault();
|
|
@@ -141,14 +142,24 @@
|
|
|
141
142
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
142
143
|
const target = getTargetProperties(e);
|
|
143
144
|
// Back arrow
|
|
144
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
target.key === 'ArrowLeft' &&
|
|
147
|
+
!target.isHighligted &&
|
|
148
|
+
target.previous &&
|
|
149
|
+
target.lastState.selectionStart === 0
|
|
150
|
+
) {
|
|
145
151
|
const preservedValue = String(target.previous.value);
|
|
146
152
|
focusAndHighlightText(target.previous);
|
|
147
153
|
target.previous.value = preservedValue;
|
|
148
154
|
return e.preventDefault();
|
|
149
155
|
}
|
|
150
156
|
// Right arrow
|
|
151
|
-
if (
|
|
157
|
+
if (
|
|
158
|
+
target.key === 'ArrowRight' &&
|
|
159
|
+
!target.isHighligted &&
|
|
160
|
+
target.next &&
|
|
161
|
+
target.lastState.selectionStart === target.value.length
|
|
162
|
+
) {
|
|
152
163
|
focusAndHighlightText(target.next);
|
|
153
164
|
return e.preventDefault();
|
|
154
165
|
}
|
|
@@ -197,8 +208,8 @@
|
|
|
197
208
|
let centValue = Math.abs(isNumericString(cents) ? Number(cents) : 0);
|
|
198
209
|
let dollarValue = Math.abs(isNumericString(dollars) ? Number(dollars) : 0);
|
|
199
210
|
// Update value
|
|
200
|
-
if (isValueInCents) value =
|
|
201
|
-
else value = dollarValue +
|
|
211
|
+
if (isValueInCents) value = dollarValue * 100 + centValue;
|
|
212
|
+
else value = dollarValue + centValue / 100;
|
|
202
213
|
// Enforce min and max
|
|
203
214
|
if (min && value < min) value = min;
|
|
204
215
|
if (max && value > max) value = max;
|
|
@@ -206,7 +217,6 @@
|
|
|
206
217
|
cents = String(centValue).padStart(2, '0');
|
|
207
218
|
onChange?.(value);
|
|
208
219
|
};
|
|
209
|
-
|
|
210
220
|
</script>
|
|
211
221
|
|
|
212
222
|
<FormField {size} {label} {id}>
|
|
@@ -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.svelte';
|
|
4
|
+
import FormField from '../form-field/form-field.svelte';
|
|
5
5
|
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
6
6
|
const id = uniqueId();
|
|
7
7
|
|
|
@@ -54,7 +54,10 @@
|
|
|
54
54
|
const onKeyPress = (e: KeyboardEvent) => {
|
|
55
55
|
const isNumber = !isNaN(Number(e.key));
|
|
56
56
|
const isDecimal = e.key === '.';
|
|
57
|
-
const isAllowed =
|
|
57
|
+
const isAllowed =
|
|
58
|
+
isNumber ||
|
|
59
|
+
isDecimal ||
|
|
60
|
+
['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key);
|
|
58
61
|
if (!isAllowed) return e.preventDefault();
|
|
59
62
|
if (isDecimal && decimals === 0) return e.preventDefault();
|
|
60
63
|
const newValue = `${value}${e.key}`;
|