@classic-homes/theme-svelte 0.1.4 → 0.1.5

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 (40) hide show
  1. package/dist/lib/components/Combobox.svelte +187 -0
  2. package/dist/lib/components/Combobox.svelte.d.ts +38 -0
  3. package/dist/lib/components/DateTimePicker.svelte +415 -0
  4. package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
  5. package/dist/lib/components/MultiSelect.svelte +244 -0
  6. package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
  7. package/dist/lib/components/NumberInput.svelte +205 -0
  8. package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
  9. package/dist/lib/components/OTPInput.svelte +213 -0
  10. package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
  11. package/dist/lib/components/RadioGroup.svelte +124 -0
  12. package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
  13. package/dist/lib/components/Signature.svelte +1070 -0
  14. package/dist/lib/components/Signature.svelte.d.ts +74 -0
  15. package/dist/lib/components/Slider.svelte +136 -0
  16. package/dist/lib/components/Slider.svelte.d.ts +30 -0
  17. package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
  18. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
  19. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  20. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  21. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  22. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  23. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  24. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  25. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  27. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  28. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  29. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  30. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  31. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  32. package/dist/lib/components/layout/sidebar/index.js +10 -0
  33. package/dist/lib/index.d.ts +9 -1
  34. package/dist/lib/index.js +8 -0
  35. package/dist/lib/schemas/auth.d.ts +6 -6
  36. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  37. package/dist/lib/stores/sidebar.svelte.js +171 -1
  38. package/dist/lib/types/components.d.ts +105 -0
  39. package/dist/lib/types/layout.d.ts +32 -2
  40. package/package.json +1 -1
@@ -0,0 +1,213 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+
4
+ interface Props {
5
+ /** Current OTP value */
6
+ value?: string;
7
+ /** Callback when value changes */
8
+ onValueChange?: (value: string) => void;
9
+ /** Callback when all digits are entered */
10
+ onComplete?: (code: string) => void;
11
+ /** Number of digits (default: 6) */
12
+ length?: number;
13
+ /** Whether the input is disabled */
14
+ disabled?: boolean;
15
+ /** Input type: numeric only or alphanumeric */
16
+ type?: 'numeric' | 'alphanumeric';
17
+ /** Name attribute for form submission */
18
+ name?: string;
19
+ /** Error message to display */
20
+ error?: string;
21
+ /** Additional class for the container */
22
+ class?: string;
23
+ }
24
+
25
+ let {
26
+ value = $bindable(''),
27
+ onValueChange,
28
+ onComplete,
29
+ length = 6,
30
+ disabled = false,
31
+ type = 'numeric',
32
+ name,
33
+ error,
34
+ class: className,
35
+ }: Props = $props();
36
+
37
+ let inputs: HTMLInputElement[] = [];
38
+
39
+ // Split value into individual digits, ensuring we always have `length` elements
40
+ const digits = $derived.by(() => {
41
+ const arr = value.split('').slice(0, length);
42
+ // Pad array to always have `length` elements
43
+ while (arr.length < length) {
44
+ arr.push('');
45
+ }
46
+ return arr;
47
+ });
48
+
49
+ // Pattern for input validation
50
+ const pattern = $derived(type === 'numeric' ? '[0-9]' : '[0-9a-zA-Z]');
51
+ const inputMode = $derived(type === 'numeric' ? 'numeric' : 'text');
52
+
53
+ function isValidChar(char: string): boolean {
54
+ if (type === 'numeric') {
55
+ return /^[0-9]$/.test(char);
56
+ }
57
+ return /^[0-9a-zA-Z]$/.test(char);
58
+ }
59
+
60
+ // Track if we've already fired onComplete for the current complete value
61
+ let lastCompletedValue = $state<string | null>(null);
62
+
63
+ function updateValue(newDigits: string[]) {
64
+ // Filter out empty strings and join to get actual value
65
+ const newValue = newDigits
66
+ .filter((d) => d !== '')
67
+ .join('')
68
+ .slice(0, length);
69
+ value = newValue;
70
+ onValueChange?.(newValue);
71
+
72
+ // Only fire onComplete once when all digits are filled and it's a new complete value
73
+ if (newValue.length === length && newValue !== lastCompletedValue) {
74
+ lastCompletedValue = newValue;
75
+ onComplete?.(newValue);
76
+ } else if (newValue.length < length) {
77
+ // Reset tracking when value becomes incomplete
78
+ lastCompletedValue = null;
79
+ }
80
+ }
81
+
82
+ function handleInput(index: number, e: Event) {
83
+ const target = e.target as HTMLInputElement;
84
+ const inputValue = target.value;
85
+
86
+ if (!inputValue) return;
87
+
88
+ // Handle paste of multiple characters
89
+ if (inputValue.length > 1) {
90
+ const chars = inputValue.split('').filter(isValidChar);
91
+ const newDigits = [...digits];
92
+
93
+ chars.forEach((char, i) => {
94
+ if (index + i < length) {
95
+ newDigits[index + i] = char;
96
+ }
97
+ });
98
+
99
+ updateValue(newDigits);
100
+
101
+ // Focus the next empty input or the last input
102
+ const nextIndex = Math.min(index + chars.length, length - 1);
103
+ inputs[nextIndex]?.focus();
104
+ return;
105
+ }
106
+
107
+ // Handle single character
108
+ const char = inputValue.slice(-1);
109
+ if (!isValidChar(char)) {
110
+ target.value = digits[index] || '';
111
+ return;
112
+ }
113
+
114
+ const newDigits = [...digits];
115
+ newDigits[index] = char;
116
+ updateValue(newDigits);
117
+
118
+ // Move to next input
119
+ if (index < length - 1) {
120
+ inputs[index + 1]?.focus();
121
+ }
122
+ }
123
+
124
+ function handleKeyDown(index: number, e: KeyboardEvent) {
125
+ const target = e.target as HTMLInputElement;
126
+
127
+ if (e.key === 'Backspace') {
128
+ e.preventDefault();
129
+
130
+ if (digits[index]) {
131
+ // Clear current digit
132
+ const newDigits = [...digits];
133
+ newDigits[index] = '';
134
+ updateValue(newDigits);
135
+ } else if (index > 0) {
136
+ // Move to previous input and clear it
137
+ const newDigits = [...digits];
138
+ newDigits[index - 1] = '';
139
+ updateValue(newDigits);
140
+ inputs[index - 1]?.focus();
141
+ }
142
+ } else if (e.key === 'ArrowLeft' && index > 0) {
143
+ e.preventDefault();
144
+ inputs[index - 1]?.focus();
145
+ } else if (e.key === 'ArrowRight' && index < length - 1) {
146
+ e.preventDefault();
147
+ inputs[index + 1]?.focus();
148
+ } else if (e.key === 'Delete') {
149
+ e.preventDefault();
150
+ const newDigits = [...digits];
151
+ newDigits[index] = '';
152
+ updateValue(newDigits);
153
+ }
154
+ }
155
+
156
+ function handlePaste(e: ClipboardEvent) {
157
+ e.preventDefault();
158
+ const pastedData = e.clipboardData?.getData('text') || '';
159
+ const chars = pastedData.split('').filter(isValidChar).slice(0, length);
160
+
161
+ if (chars.length > 0) {
162
+ const newDigits = chars.concat(Array(length - chars.length).fill(''));
163
+ updateValue(newDigits);
164
+
165
+ // Focus the input after the last pasted character
166
+ const focusIndex = Math.min(chars.length, length - 1);
167
+ inputs[focusIndex]?.focus();
168
+ }
169
+ }
170
+
171
+ function handleFocus(e: FocusEvent) {
172
+ const target = e.target as HTMLInputElement;
173
+ target.select();
174
+ }
175
+ </script>
176
+
177
+ <div class={cn('flex gap-2', className)} role="group" aria-label="One-time password input">
178
+ {#each { length } as _, index}
179
+ <input
180
+ bind:this={inputs[index]}
181
+ type="text"
182
+ inputmode={inputMode}
183
+ maxlength="1"
184
+ {pattern}
185
+ {disabled}
186
+ value={digits[index] || ''}
187
+ oninput={(e) => handleInput(index, e)}
188
+ onkeydown={(e) => handleKeyDown(index, e)}
189
+ onpaste={handlePaste}
190
+ onfocus={handleFocus}
191
+ class={cn(
192
+ 'h-12 w-12 text-center text-lg font-semibold rounded-md border border-input bg-background ring-offset-background transition-colors',
193
+ 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
194
+ 'disabled:cursor-not-allowed disabled:opacity-50',
195
+ error && 'border-destructive focus:ring-destructive'
196
+ )}
197
+ aria-label={`Digit ${index + 1} of ${length}`}
198
+ aria-invalid={error ? 'true' : undefined}
199
+ autocomplete="one-time-code"
200
+ />
201
+ {/each}
202
+ </div>
203
+
204
+ <!-- Hidden input for form submission -->
205
+ {#if name}
206
+ <input type="hidden" {name} {value} />
207
+ {/if}
208
+
209
+ {#if error}
210
+ <p class="text-sm text-destructive mt-1.5" role="alert">
211
+ {error}
212
+ </p>
213
+ {/if}
@@ -0,0 +1,23 @@
1
+ interface Props {
2
+ /** Current OTP value */
3
+ value?: string;
4
+ /** Callback when value changes */
5
+ onValueChange?: (value: string) => void;
6
+ /** Callback when all digits are entered */
7
+ onComplete?: (code: string) => void;
8
+ /** Number of digits (default: 6) */
9
+ length?: number;
10
+ /** Whether the input is disabled */
11
+ disabled?: boolean;
12
+ /** Input type: numeric only or alphanumeric */
13
+ type?: 'numeric' | 'alphanumeric';
14
+ /** Name attribute for form submission */
15
+ name?: string;
16
+ /** Error message to display */
17
+ error?: string;
18
+ /** Additional class for the container */
19
+ class?: string;
20
+ }
21
+ declare const OTPInput: import("svelte").Component<Props, {}, "value">;
22
+ type OTPInput = ReturnType<typeof OTPInput>;
23
+ export default OTPInput;
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
3
+ import { cn } from '../utils.js';
4
+ import { validateNonEmptyArray } from '../validation.js';
5
+
6
+ export interface RadioOption {
7
+ value: string;
8
+ label: string;
9
+ disabled?: boolean;
10
+ description?: string;
11
+ }
12
+
13
+ interface Props {
14
+ /** Current selected value */
15
+ value?: string;
16
+ /** Callback when value changes */
17
+ onValueChange?: (value: string) => void;
18
+ /** Array of radio options */
19
+ options: RadioOption[];
20
+ /** Whether the entire group is disabled */
21
+ disabled?: boolean;
22
+ /** Whether selection is required */
23
+ required?: boolean;
24
+ /** Name attribute for form submission */
25
+ name?: string;
26
+ /** Layout orientation */
27
+ orientation?: 'horizontal' | 'vertical';
28
+ /** Optional label for the group */
29
+ label?: string;
30
+ /** Error message to display */
31
+ error?: string;
32
+ /** Additional class for the container */
33
+ class?: string;
34
+ }
35
+
36
+ let {
37
+ value = $bindable(''),
38
+ onValueChange,
39
+ options,
40
+ disabled = false,
41
+ required = false,
42
+ name,
43
+ orientation = 'vertical',
44
+ label,
45
+ error,
46
+ class: className,
47
+ }: Props = $props();
48
+
49
+ // Validate props in development
50
+ $effect(() => {
51
+ validateNonEmptyArray(options, 'options', 'RadioGroup');
52
+ });
53
+
54
+ function handleValueChange(newValue: string) {
55
+ value = newValue;
56
+ onValueChange?.(newValue);
57
+ }
58
+ </script>
59
+
60
+ {#if label}
61
+ <span
62
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-2 block"
63
+ id={name ? `${name}-label` : undefined}
64
+ >
65
+ {label}
66
+ {#if required}
67
+ <span class="text-destructive">*</span>
68
+ {/if}
69
+ </span>
70
+ {/if}
71
+
72
+ <RadioGroupPrimitive.Root
73
+ {value}
74
+ {disabled}
75
+ {required}
76
+ {name}
77
+ onValueChange={handleValueChange}
78
+ class={cn(
79
+ 'grid gap-2',
80
+ orientation === 'horizontal' ? 'grid-flow-col auto-cols-max' : 'grid-flow-row',
81
+ className
82
+ )}
83
+ aria-labelledby={label && name ? `${name}-label` : undefined}
84
+ aria-invalid={error ? 'true' : undefined}
85
+ >
86
+ {#each options as option}
87
+ <div class="flex items-start gap-3">
88
+ <RadioGroupPrimitive.Item
89
+ value={option.value}
90
+ disabled={option.disabled || disabled}
91
+ id={name ? `${name}-${option.value}` : undefined}
92
+ class={cn(
93
+ 'relative aspect-square h-5 w-5 rounded-full border border-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 flex items-center justify-center',
94
+ error && 'border-destructive'
95
+ )}
96
+ >
97
+ <!-- Touch target expansion (invisible, extends clickable area to 44x44px) -->
98
+ <span class="absolute inset-0 -m-3" aria-hidden="true"></span>
99
+ <!-- Indicator dot - shown when this option is selected -->
100
+ {#if value === option.value}
101
+ <span class="h-2.5 w-2.5 rounded-full bg-primary"></span>
102
+ {/if}
103
+ </RadioGroupPrimitive.Item>
104
+ <label
105
+ for={name ? `${name}-${option.value}` : undefined}
106
+ class={cn(
107
+ 'text-sm leading-none cursor-pointer select-none',
108
+ (option.disabled || disabled) && 'cursor-not-allowed opacity-50'
109
+ )}
110
+ >
111
+ <span class="font-medium">{option.label}</span>
112
+ {#if option.description}
113
+ <span class="block text-muted-foreground mt-1">{option.description}</span>
114
+ {/if}
115
+ </label>
116
+ </div>
117
+ {/each}
118
+ </RadioGroupPrimitive.Root>
119
+
120
+ {#if error}
121
+ <p class="text-sm text-destructive mt-1.5" role="alert">
122
+ {error}
123
+ </p>
124
+ {/if}
@@ -0,0 +1,31 @@
1
+ export interface RadioOption {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ description?: string;
6
+ }
7
+ interface Props {
8
+ /** Current selected value */
9
+ value?: string;
10
+ /** Callback when value changes */
11
+ onValueChange?: (value: string) => void;
12
+ /** Array of radio options */
13
+ options: RadioOption[];
14
+ /** Whether the entire group is disabled */
15
+ disabled?: boolean;
16
+ /** Whether selection is required */
17
+ required?: boolean;
18
+ /** Name attribute for form submission */
19
+ name?: string;
20
+ /** Layout orientation */
21
+ orientation?: 'horizontal' | 'vertical';
22
+ /** Optional label for the group */
23
+ label?: string;
24
+ /** Error message to display */
25
+ error?: string;
26
+ /** Additional class for the container */
27
+ class?: string;
28
+ }
29
+ declare const RadioGroup: import("svelte").Component<Props, {}, "value">;
30
+ type RadioGroup = ReturnType<typeof RadioGroup>;
31
+ export default RadioGroup;