@classic-homes/theme-svelte 0.1.3 → 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.
- package/dist/lib/components/Combobox.svelte +187 -0
- package/dist/lib/components/Combobox.svelte.d.ts +38 -0
- package/dist/lib/components/DateTimePicker.svelte +415 -0
- package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
- package/dist/lib/components/MultiSelect.svelte +244 -0
- package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
- package/dist/lib/components/NumberInput.svelte +205 -0
- package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
- package/dist/lib/components/OTPInput.svelte +213 -0
- package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
- package/dist/lib/components/RadioGroup.svelte +124 -0
- package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
- package/dist/lib/components/Signature.svelte +1070 -0
- package/dist/lib/components/Signature.svelte.d.ts +74 -0
- package/dist/lib/components/Slider.svelte +136 -0
- package/dist/lib/components/Slider.svelte.d.ts +30 -0
- package/dist/lib/components/layout/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
- package/dist/lib/components/layout/QuickLinks.svelte +49 -29
- package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
- package/dist/lib/components/layout/Sidebar.svelte +345 -86
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
- package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
- package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
- package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
- package/dist/lib/components/layout/sidebar/index.js +10 -0
- package/dist/lib/index.d.ts +9 -1
- package/dist/lib/index.js +8 -0
- package/dist/lib/schemas/auth.d.ts +6 -6
- package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
- package/dist/lib/stores/sidebar.svelte.js +171 -1
- package/dist/lib/types/components.d.ts +105 -0
- package/dist/lib/types/layout.d.ts +32 -2
- 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;
|