sveltacular 1.0.5 → 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.
@@ -7,8 +7,14 @@ 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
20
  export * from './list-box/index.js';
@@ -21,6 +27,4 @@ export { default as FormHeader } from './form-header.svelte';
21
27
  export { default as FormLabel } from './form-label/form-label.svelte';
22
28
  export { default as FormSection } from './form-section/form-section.svelte';
23
29
  export { default as FormRow } from './form-row/form-row.svelte';
24
- export { default as Slider } from './slider/slider.svelte';
25
- export { default as TimeBox } from './time-box/time-box.svelte';
26
30
  export * from './validation.js';
@@ -8,8 +8,14 @@ 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';
@@ -24,8 +30,5 @@ export { default as FormHeader } from './form-header.svelte';
24
30
  export { default as FormLabel } from './form-label/form-label.svelte';
25
31
  export { default as FormSection } from './form-section/form-section.svelte';
26
32
  export { default as FormRow } from './form-row/form-row.svelte';
27
- // New form components
28
- export { default as Slider } from './slider/slider.svelte';
29
- export { default as TimeBox } from './time-box/time-box.svelte';
30
33
  // Validation utilities
31
34
  export * from './validation.js';
@@ -0,0 +1,218 @@
1
+ <script lang="ts">
2
+ import { roundToDecimals } from '../../helpers/round-to-decimals.js';
3
+ import { uniqueId } from '../../helpers/unique-id.js';
4
+ import FormField from '../form-field/form-field.svelte';
5
+ import type { FormFieldSizeOptions } from '../../types/form.js';
6
+
7
+ const minId = uniqueId();
8
+ const maxId = uniqueId();
9
+
10
+ let {
11
+ minValue = $bindable(null as number | null),
12
+ maxValue = $bindable(null as number | null),
13
+ minAllowed = 0,
14
+ maxAllowed = 99999,
15
+ step = 1,
16
+ allowDecimals = false,
17
+ minPlaceholder = '0',
18
+ maxPlaceholder = 'No limit',
19
+ prefix = null as string | null,
20
+ suffix = null as string | null,
21
+ stickyEnd = false,
22
+ required = false,
23
+ size = 'full' as FormFieldSizeOptions,
24
+ label = undefined as string | undefined,
25
+ onChange = undefined as ((minValue: number | null, maxValue: number | null) => void) | undefined
26
+ }: {
27
+ minValue?: number | null;
28
+ maxValue?: number | null;
29
+ minAllowed?: number;
30
+ maxAllowed?: number;
31
+ step?: number;
32
+ allowDecimals?: boolean;
33
+ minPlaceholder?: string;
34
+ maxPlaceholder?: string;
35
+ prefix?: string | null;
36
+ suffix?: string | null;
37
+ stickyEnd?: boolean;
38
+ required?: boolean;
39
+ size?: FormFieldSizeOptions;
40
+ label?: string;
41
+ onChange?: ((minValue: number | null, maxValue: number | null) => void) | undefined;
42
+ } = $props();
43
+
44
+ const handleChange = () => {
45
+ // Calculate decimal places from step (e.g., step 0.1 = 1 decimal, step 0.01 = 2 decimals)
46
+ const decimalPlaces = step < 1 ? Math.abs(Math.log10(step)) : 0;
47
+
48
+ // If no decimals, must round to integer
49
+ if (!allowDecimals) {
50
+ if (minValue !== null) minValue = Math.round(minValue);
51
+ if (maxValue !== null) maxValue = Math.round(maxValue);
52
+ }
53
+ // If decimals, must round to step precision
54
+ else {
55
+ if (minValue !== null) minValue = roundToDecimals(minValue, decimalPlaces);
56
+ if (maxValue !== null) maxValue = roundToDecimals(maxValue, decimalPlaces);
57
+ }
58
+
59
+ // Ensure min value is not less than minAllowed
60
+ if (minValue !== null && minValue < minAllowed) {
61
+ minValue = minAllowed;
62
+ }
63
+
64
+ // Ensure max value is not greater than maxAllowed
65
+ if (maxValue !== null && maxValue > maxAllowed) {
66
+ maxValue = maxAllowed;
67
+ }
68
+
69
+ // Ensure max value is greater than or equal to min value
70
+ if (minValue !== null && maxValue !== null && minValue > maxValue) {
71
+ maxValue = minValue;
72
+ }
73
+
74
+ onChange?.(minValue, maxValue);
75
+ };
76
+
77
+ const handleMinChange = () => {
78
+ if (stickyEnd) {
79
+ maxValue = minValue;
80
+ }
81
+ handleChange();
82
+ };
83
+
84
+ const onInput = (e: Event, isMin: boolean) => {
85
+ const input = e.target as HTMLInputElement;
86
+ const newValue = parseFloat(input.value);
87
+ if (isNaN(newValue)) {
88
+ if (isMin) minValue = null;
89
+ else maxValue = null;
90
+ return;
91
+ }
92
+ if (isMin) minValue = newValue;
93
+ else maxValue = newValue;
94
+ };
95
+
96
+ // Don't allow certain characters to be typed into the input
97
+ const onKeyPress = (e: KeyboardEvent, isMin: boolean) => {
98
+ const isNumber = !isNaN(Number(e.key));
99
+ const isDecimal = e.key === '.';
100
+ const isAllowed =
101
+ isNumber ||
102
+ isDecimal ||
103
+ ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', '-'].includes(e.key);
104
+ if (!isAllowed) return e.preventDefault();
105
+ if (isDecimal && !allowDecimals) return e.preventDefault();
106
+ };
107
+ </script>
108
+
109
+ <FormField {size} {label} id={minId} {required}>
110
+ <div class="number-range-inputs">
111
+ <div class="input-group">
112
+ <div class="input">
113
+ {#if prefix}
114
+ <span class="prefix">{prefix}</span>
115
+ {/if}
116
+ <input
117
+ id={minId}
118
+ type="number"
119
+ placeholder={minPlaceholder}
120
+ min={minAllowed}
121
+ max={maxAllowed}
122
+ {step}
123
+ bind:value={minValue}
124
+ onchange={handleMinChange}
125
+ oninput={(e) => onInput(e, true)}
126
+ onkeypress={(e) => onKeyPress(e, true)}
127
+ {required}
128
+ />
129
+ {#if suffix}
130
+ <span class="suffix">{suffix}</span>
131
+ {/if}
132
+ </div>
133
+ </div>
134
+ <div class="input-group">
135
+ <div class="input">
136
+ {#if prefix}
137
+ <span class="prefix">{prefix}</span>
138
+ {/if}
139
+ <input
140
+ id={maxId}
141
+ type="number"
142
+ placeholder={maxPlaceholder}
143
+ min={minAllowed}
144
+ max={maxAllowed}
145
+ {step}
146
+ bind:value={maxValue}
147
+ onchange={handleChange}
148
+ oninput={(e) => onInput(e, false)}
149
+ onkeypress={(e) => onKeyPress(e, false)}
150
+ {required}
151
+ />
152
+ {#if suffix}
153
+ <span class="suffix">{suffix}</span>
154
+ {/if}
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </FormField>
159
+
160
+ <style>.number-range-inputs {
161
+ display: flex;
162
+ gap: var(--spacing-md);
163
+ width: 100%;
164
+ }
165
+
166
+ .input-group {
167
+ flex: 1;
168
+ }
169
+
170
+ .input {
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: flex-start;
174
+ position: relative;
175
+ width: 100%;
176
+ height: 100%;
177
+ border-radius: var(--radius-md);
178
+ border: var(--border-thin) solid var(--form-input-border);
179
+ background-color: var(--form-input-bg);
180
+ color: var(--form-input-fg);
181
+ font-size: var(--font-md);
182
+ font-weight: 500;
183
+ line-height: 2rem;
184
+ 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);
185
+ user-select: none;
186
+ white-space: nowrap;
187
+ }
188
+ .input input {
189
+ background-color: transparent;
190
+ border: none;
191
+ line-height: 2rem;
192
+ font-size: var(--font-md);
193
+ width: 100%;
194
+ flex-grow: 1;
195
+ padding-left: var(--spacing-base);
196
+ padding-right: var(--spacing-base);
197
+ }
198
+ .input input:focus {
199
+ outline: none;
200
+ }
201
+ .input input::placeholder {
202
+ color: var(--form-input-placeholder);
203
+ }
204
+ .input .prefix,
205
+ .input .suffix {
206
+ font-size: var(--font-md);
207
+ line-height: 2rem;
208
+ padding-left: var(--spacing-base);
209
+ padding-right: var(--spacing-base);
210
+ background-color: var(--form-input-accent-bg);
211
+ color: var(--form-input-accent-fg);
212
+ }
213
+ .input .prefix {
214
+ border-right: var(--border-thin) solid var(--form-input-border);
215
+ }
216
+ .input .suffix {
217
+ border-left: var(--border-thin) solid var(--form-input-border);
218
+ }</style>
@@ -0,0 +1,21 @@
1
+ import type { FormFieldSizeOptions } from '../../types/form.js';
2
+ type $$ComponentProps = {
3
+ minValue?: number | null;
4
+ maxValue?: number | null;
5
+ minAllowed?: number;
6
+ maxAllowed?: number;
7
+ step?: number;
8
+ allowDecimals?: boolean;
9
+ minPlaceholder?: string;
10
+ maxPlaceholder?: string;
11
+ prefix?: string | null;
12
+ suffix?: string | null;
13
+ stickyEnd?: boolean;
14
+ required?: boolean;
15
+ size?: FormFieldSizeOptions;
16
+ label?: string;
17
+ onChange?: ((minValue: number | null, maxValue: number | null) => void) | undefined;
18
+ };
19
+ declare const NumberRangeBox: import("svelte").Component<$$ComponentProps, {}, "minValue" | "maxValue">;
20
+ type NumberRangeBox = ReturnType<typeof NumberRangeBox>;
21
+ export default NumberRangeBox;
@@ -0,0 +1,203 @@
1
+ <script lang="ts">
2
+ import { uniqueId } from '../../helpers/unique-id.js';
3
+ import FormField from '../form-field/form-field.svelte';
4
+ import Chip from '../../generic/chip/chip.svelte';
5
+ import type { FormFieldSizeOptions } from '../../types/form.js';
6
+
7
+ const id = uniqueId();
8
+ const datalistId = uniqueId();
9
+
10
+ let {
11
+ value = $bindable([] as string[]),
12
+ placeholder = 'Add a tag...',
13
+ required = false,
14
+ disabled = false,
15
+ autocomplete = [] as string[],
16
+ separators = [',', ';'] as string[],
17
+ size = 'full' as FormFieldSizeOptions,
18
+ label = undefined as string | undefined,
19
+ helperText = undefined as string | undefined,
20
+ errorText = undefined as string | undefined,
21
+ onChange = undefined as ((value: string[]) => void) | undefined
22
+ }: {
23
+ value?: string[];
24
+ placeholder?: string;
25
+ required?: boolean;
26
+ disabled?: boolean;
27
+ autocomplete?: string[];
28
+ separators?: string[];
29
+ size?: FormFieldSizeOptions;
30
+ label?: string;
31
+ helperText?: string;
32
+ errorText?: string;
33
+ onChange?: ((value: string[]) => void) | undefined;
34
+ } = $props();
35
+
36
+ let newTag = $state('');
37
+
38
+ function addTag(tagToAdd?: string) {
39
+ const tag = tagToAdd || newTag.trim();
40
+ if (tag && !value.includes(tag)) {
41
+ value = [...value, tag];
42
+ newTag = '';
43
+ onChange?.(value);
44
+ }
45
+ }
46
+
47
+ function removeTag(tagToRemove: string) {
48
+ value = value.filter((tag) => tag !== tagToRemove);
49
+ onChange?.(value);
50
+ }
51
+
52
+ function handleKeydown(event: KeyboardEvent) {
53
+ if (event.key === 'Enter') {
54
+ event.preventDefault();
55
+ addTag();
56
+ } else if (event.key === 'Backspace' && newTag === '' && value.length > 0) {
57
+ removeTag(value[value.length - 1]);
58
+ }
59
+ }
60
+
61
+ function handleInput(event: Event) {
62
+ const input = event.target as HTMLInputElement;
63
+ const inputValue = input.value;
64
+
65
+ // Check if the last character is a separator
66
+ if (inputValue.length > 0) {
67
+ const lastChar = inputValue[inputValue.length - 1];
68
+ if (separators.includes(lastChar)) {
69
+ // Extract the tag before the separator
70
+ const tagBeforeSeparator = inputValue.slice(0, -1).trim();
71
+ if (tagBeforeSeparator) {
72
+ // Prevent the separator from being added
73
+ newTag = tagBeforeSeparator;
74
+ // Add the tag
75
+ addTag(tagBeforeSeparator);
76
+ } else {
77
+ // If there's no text before the separator, just clear it
78
+ newTag = '';
79
+ }
80
+ }
81
+ }
82
+ }
83
+ </script>
84
+
85
+ <FormField {size} {label} {id} {required} {disabled} {helperText} {errorText}>
86
+ <div class="tag-input">
87
+ <div class="input-container">
88
+ <div class="input-wrapper">
89
+ <input
90
+ {id}
91
+ type="text"
92
+ bind:value={newTag}
93
+ {placeholder}
94
+ onkeydown={handleKeydown}
95
+ oninput={handleInput}
96
+ {disabled}
97
+ {required}
98
+ list={autocomplete.length > 0 ? datalistId : undefined}
99
+ aria-label="Tag input"
100
+ class="tag-input-field"
101
+ />
102
+ {#if autocomplete.length > 0}
103
+ <datalist id={datalistId}>
104
+ {#each autocomplete as option}
105
+ <option value={option}></option>
106
+ {/each}
107
+ </datalist>
108
+ {/if}
109
+ </div>
110
+ <button
111
+ type="button"
112
+ class="add-button"
113
+ onclick={() => addTag()}
114
+ disabled={disabled || !newTag.trim()}
115
+ aria-label="Add tag"
116
+ >
117
+ Add
118
+ </button>
119
+ </div>
120
+
121
+ {#if value.length > 0}
122
+ <div class="tags">
123
+ {#each value as tag}
124
+ <Chip label={tag} removable={true} onRemove={() => removeTag(tag)} />
125
+ {/each}
126
+ </div>
127
+ {/if}
128
+ </div>
129
+ </FormField>
130
+
131
+ <style>.tag-input {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: var(--spacing-sm);
135
+ width: 100%;
136
+ }
137
+ .tag-input .input-container {
138
+ display: flex;
139
+ gap: var(--spacing-sm);
140
+ align-items: flex-end;
141
+ }
142
+ .tag-input .input-container .input-wrapper {
143
+ flex: 1;
144
+ position: relative;
145
+ }
146
+ .tag-input .input-container .tag-input-field {
147
+ width: 100%;
148
+ height: 2.5rem;
149
+ padding: 0 var(--spacing-base);
150
+ border: var(--border-thin) solid var(--form-input-border);
151
+ border-radius: var(--radius-md);
152
+ background-color: var(--form-input-bg);
153
+ color: var(--form-input-fg);
154
+ font-size: var(--font-md);
155
+ font-weight: 500;
156
+ line-height: 2rem;
157
+ 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);
158
+ }
159
+ .tag-input .input-container .tag-input-field:focus {
160
+ outline: none;
161
+ border-color: var(--form-input-border-focus);
162
+ background-color: var(--form-input-bg);
163
+ }
164
+ .tag-input .input-container .tag-input-field:disabled {
165
+ opacity: 0.6;
166
+ cursor: not-allowed;
167
+ }
168
+ .tag-input .input-container .tag-input-field::placeholder {
169
+ color: var(--form-input-placeholder);
170
+ }
171
+ .tag-input .input-container .add-button {
172
+ padding: var(--spacing-sm) var(--spacing-base);
173
+ background-color: var(--button-primary-bg);
174
+ color: var(--button-primary-fg);
175
+ border: var(--border-thin) solid var(--button-primary-border);
176
+ border-radius: var(--radius-md);
177
+ font-size: var(--font-sm);
178
+ font-weight: 500;
179
+ cursor: pointer;
180
+ 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);
181
+ white-space: nowrap;
182
+ height: fit-content;
183
+ min-height: 2.5rem;
184
+ }
185
+ .tag-input .input-container .add-button:hover:not(:disabled) {
186
+ background-color: var(--button-primary-hover-bg);
187
+ color: var(--button-primary-hover-fg);
188
+ }
189
+ .tag-input .input-container .add-button:disabled {
190
+ background-color: var(--gray-400);
191
+ border-color: var(--gray-400);
192
+ color: var(--gray-600);
193
+ cursor: not-allowed;
194
+ opacity: 0.6;
195
+ }
196
+ .tag-input .input-container .add-button:active:not(:disabled) {
197
+ transform: scale(0.98);
198
+ }
199
+ .tag-input .tags {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ gap: var(--spacing-sm);
203
+ }</style>
@@ -0,0 +1,17 @@
1
+ import type { FormFieldSizeOptions } from '../../types/form.js';
2
+ type $$ComponentProps = {
3
+ value?: string[];
4
+ placeholder?: string;
5
+ required?: boolean;
6
+ disabled?: boolean;
7
+ autocomplete?: string[];
8
+ separators?: string[];
9
+ size?: FormFieldSizeOptions;
10
+ label?: string;
11
+ helperText?: string;
12
+ errorText?: string;
13
+ onChange?: ((value: string[]) => void) | undefined;
14
+ };
15
+ declare const TagInputBox: import("svelte").Component<$$ComponentProps, {}, "value">;
16
+ type TagInputBox = ReturnType<typeof TagInputBox>;
17
+ export default TagInputBox;
@@ -92,3 +92,4 @@
92
92
 
93
93
 
94
94
 
95
+
@@ -148,3 +148,4 @@ Due to limitations in the Storybook Svelte CSF parser with advanced Svelte 5 syn
148
148
 
149
149
 
150
150
 
151
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltacular",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "A Svelte component library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",