@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.
Files changed (41) 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/AppShell.svelte +1 -1
  18. package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
  19. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
  20. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  21. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  22. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  23. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  24. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  25. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
  27. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  28. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  29. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  30. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  31. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  32. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  33. package/dist/lib/components/layout/sidebar/index.js +10 -0
  34. package/dist/lib/index.d.ts +9 -1
  35. package/dist/lib/index.js +8 -0
  36. package/dist/lib/schemas/auth.d.ts +6 -6
  37. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  38. package/dist/lib/stores/sidebar.svelte.js +171 -1
  39. package/dist/lib/types/components.d.ts +105 -0
  40. package/dist/lib/types/layout.d.ts +32 -2
  41. package/package.json +1 -1
@@ -0,0 +1,244 @@
1
+ <script lang="ts">
2
+ import { Combobox as ComboboxPrimitive } from 'bits-ui';
3
+ import { cn } from '../utils.js';
4
+ import Spinner from './Spinner.svelte';
5
+
6
+ export interface MultiSelectOption {
7
+ value: string;
8
+ label: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ interface Props {
13
+ /** Currently selected values */
14
+ value?: string[];
15
+ /** Callback when values change */
16
+ onValueChange?: (values: string[]) => void;
17
+ /** Array of selectable options */
18
+ options: MultiSelectOption[];
19
+ /** Placeholder text for the input */
20
+ placeholder?: string;
21
+ /** Message to display when no results found */
22
+ emptyMessage?: string;
23
+ /** Whether the multiselect is disabled */
24
+ disabled?: boolean;
25
+ /** Whether selection is required */
26
+ required?: boolean;
27
+ /** Name attribute for form submission */
28
+ name?: string;
29
+ /** Element ID */
30
+ id?: string;
31
+ /** Error message to display */
32
+ error?: string;
33
+ /** Maximum number of selections allowed */
34
+ max?: number;
35
+ /** Whether async data is loading */
36
+ loading?: boolean;
37
+ /** Callback when search query changes (for async loading) */
38
+ onSearch?: (query: string) => void;
39
+ /** Debounce delay in ms for search callback (default: 300) */
40
+ debounceMs?: number;
41
+ /** Additional class for the trigger */
42
+ class?: string;
43
+ }
44
+
45
+ let {
46
+ value = $bindable([]),
47
+ onValueChange,
48
+ options,
49
+ placeholder = 'Select options...',
50
+ emptyMessage = 'No results found.',
51
+ disabled = false,
52
+ required = false,
53
+ name,
54
+ id,
55
+ error,
56
+ max,
57
+ loading = false,
58
+ onSearch,
59
+ debounceMs = 300,
60
+ class: className,
61
+ }: Props = $props();
62
+
63
+ let searchQuery = $state('');
64
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
65
+ let open = $state(false);
66
+
67
+ // Get labels for selected values
68
+ const selectedLabels = $derived(
69
+ value.map((v) => options.find((opt) => opt.value === v)?.label).filter(Boolean) as string[]
70
+ );
71
+
72
+ // Always filter options locally based on search query
73
+ const filteredOptions = $derived.by(() => {
74
+ if (!searchQuery) return options;
75
+ const query = searchQuery.toLowerCase();
76
+ return options.filter((opt) => opt.label.toLowerCase().includes(query));
77
+ });
78
+
79
+ // Check if max selections reached
80
+ const maxReached = $derived(max !== undefined && value.length >= max);
81
+
82
+ function handleSearchInput(e: Event) {
83
+ const target = e.target as HTMLInputElement;
84
+ searchQuery = target.value;
85
+
86
+ if (onSearch) {
87
+ // Debounce the search callback
88
+ if (debounceTimer) clearTimeout(debounceTimer);
89
+ debounceTimer = setTimeout(() => {
90
+ onSearch(searchQuery);
91
+ }, debounceMs);
92
+ }
93
+ }
94
+
95
+ function handleValueChange(newValues: string[] | undefined) {
96
+ if (newValues !== undefined) {
97
+ value = newValues;
98
+ onValueChange?.(newValues);
99
+ }
100
+ }
101
+
102
+ function removeValue(valueToRemove: string) {
103
+ const newValues = value.filter((v) => v !== valueToRemove);
104
+ value = newValues;
105
+ onValueChange?.(newValues);
106
+ }
107
+
108
+ function handleOpenChange(isOpen: boolean) {
109
+ open = isOpen;
110
+ if (!isOpen) {
111
+ searchQuery = '';
112
+ }
113
+ }
114
+
115
+ // Cleanup debounce timer on unmount
116
+ $effect(() => {
117
+ return () => {
118
+ if (debounceTimer) clearTimeout(debounceTimer);
119
+ };
120
+ });
121
+ </script>
122
+
123
+ <div class={cn('w-full', className)}>
124
+ <!-- Selected tags -->
125
+ {#if selectedLabels.length > 0}
126
+ <div class="flex flex-wrap gap-1 mb-2">
127
+ {#each value as selectedValue}
128
+ {@const label = options.find((o) => o.value === selectedValue)?.label}
129
+ {#if label}
130
+ <span
131
+ class="inline-flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-xs font-medium text-secondary-foreground"
132
+ >
133
+ {label}
134
+ <button
135
+ type="button"
136
+ onclick={() => removeValue(selectedValue)}
137
+ class="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
138
+ {disabled}
139
+ aria-label={`Remove ${label}`}
140
+ >
141
+ <svg
142
+ xmlns="http://www.w3.org/2000/svg"
143
+ width="14"
144
+ height="14"
145
+ viewBox="0 0 24 24"
146
+ fill="none"
147
+ stroke="currentColor"
148
+ stroke-width="2"
149
+ stroke-linecap="round"
150
+ stroke-linejoin="round"
151
+ >
152
+ <path d="M18 6 6 18" />
153
+ <path d="m6 6 12 12" />
154
+ </svg>
155
+ </button>
156
+ </span>
157
+ {/if}
158
+ {/each}
159
+ </div>
160
+ {/if}
161
+
162
+ <ComboboxPrimitive.Root
163
+ type="multiple"
164
+ {disabled}
165
+ {required}
166
+ {name}
167
+ bind:open
168
+ onOpenChange={handleOpenChange}
169
+ onValueChange={handleValueChange}
170
+ {value}
171
+ >
172
+ <div class="relative">
173
+ <ComboboxPrimitive.Input
174
+ {id}
175
+ placeholder={value.length > 0 ? `${value.length} selected` : placeholder}
176
+ oninput={handleSearchInput}
177
+ onfocus={() => (open = true)}
178
+ onclick={() => (open = true)}
179
+ class={cn(
180
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
181
+ error && 'border-destructive focus:ring-destructive'
182
+ )}
183
+ aria-invalid={error ? 'true' : undefined}
184
+ />
185
+ </div>
186
+
187
+ <ComboboxPrimitive.Content
188
+ class="relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
189
+ sideOffset={4}
190
+ >
191
+ <div class="p-1 max-h-[300px] overflow-y-auto">
192
+ {#if loading}
193
+ <div class="flex items-center justify-center py-6">
194
+ <Spinner size="sm" />
195
+ <span class="ml-2 text-sm text-muted-foreground">Loading...</span>
196
+ </div>
197
+ {:else if filteredOptions.length === 0}
198
+ <div class="py-6 text-center text-sm text-muted-foreground">
199
+ {emptyMessage}
200
+ </div>
201
+ {:else}
202
+ {#each filteredOptions as option}
203
+ {@const isSelected = value.includes(option.value)}
204
+ {@const isDisabledByMax = maxReached && !isSelected}
205
+ <ComboboxPrimitive.Item
206
+ value={option.value}
207
+ label={option.label}
208
+ disabled={option.disabled || isDisabledByMax}
209
+ class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
210
+ >
211
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
212
+ {#if isSelected}
213
+ <svg
214
+ xmlns="http://www.w3.org/2000/svg"
215
+ width="16"
216
+ height="16"
217
+ viewBox="0 0 24 24"
218
+ fill="none"
219
+ stroke="currentColor"
220
+ stroke-width="2"
221
+ stroke-linecap="round"
222
+ stroke-linejoin="round"
223
+ class="h-4 w-4"
224
+ >
225
+ <polyline points="20 6 9 17 4 12" />
226
+ </svg>
227
+ {:else}
228
+ <span class="h-3.5 w-3.5 rounded-sm border border-primary"></span>
229
+ {/if}
230
+ </span>
231
+ {option.label}
232
+ </ComboboxPrimitive.Item>
233
+ {/each}
234
+ {/if}
235
+ </div>
236
+ </ComboboxPrimitive.Content>
237
+ </ComboboxPrimitive.Root>
238
+ </div>
239
+
240
+ {#if error}
241
+ <p class="text-sm text-destructive mt-1.5" role="alert">
242
+ {error}
243
+ </p>
244
+ {/if}
@@ -0,0 +1,40 @@
1
+ export interface MultiSelectOption {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+ interface Props {
7
+ /** Currently selected values */
8
+ value?: string[];
9
+ /** Callback when values change */
10
+ onValueChange?: (values: string[]) => void;
11
+ /** Array of selectable options */
12
+ options: MultiSelectOption[];
13
+ /** Placeholder text for the input */
14
+ placeholder?: string;
15
+ /** Message to display when no results found */
16
+ emptyMessage?: string;
17
+ /** Whether the multiselect is disabled */
18
+ disabled?: boolean;
19
+ /** Whether selection is required */
20
+ required?: boolean;
21
+ /** Name attribute for form submission */
22
+ name?: string;
23
+ /** Element ID */
24
+ id?: string;
25
+ /** Error message to display */
26
+ error?: string;
27
+ /** Maximum number of selections allowed */
28
+ max?: number;
29
+ /** Whether async data is loading */
30
+ loading?: boolean;
31
+ /** Callback when search query changes (for async loading) */
32
+ onSearch?: (query: string) => void;
33
+ /** Debounce delay in ms for search callback (default: 300) */
34
+ debounceMs?: number;
35
+ /** Additional class for the trigger */
36
+ class?: string;
37
+ }
38
+ declare const MultiSelect: import("svelte").Component<Props, {}, "value">;
39
+ type MultiSelect = ReturnType<typeof MultiSelect>;
40
+ export default MultiSelect;
@@ -0,0 +1,205 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+
4
+ interface Props {
5
+ /** Current numeric value */
6
+ value?: number | null;
7
+ /** Minimum allowed value */
8
+ min?: number;
9
+ /** Maximum allowed value */
10
+ max?: number;
11
+ /** Step increment for stepper buttons and arrow keys */
12
+ step?: number;
13
+ /** Whether the input is disabled */
14
+ disabled?: boolean;
15
+ /** Whether the input is readonly */
16
+ readonly?: boolean;
17
+ /** Whether the input is required */
18
+ required?: boolean;
19
+ /** Placeholder text */
20
+ placeholder?: string;
21
+ /** Name attribute for form submission */
22
+ name?: string;
23
+ /** Element ID */
24
+ id?: string;
25
+ /** Whether to show +/- stepper buttons */
26
+ showStepper?: boolean;
27
+ /** Callback when value changes */
28
+ onValueChange?: (value: number | null) => void;
29
+ /** Error state */
30
+ error?: string;
31
+ /** Additional class for the container */
32
+ class?: string;
33
+ }
34
+
35
+ let {
36
+ value = $bindable(null),
37
+ min,
38
+ max,
39
+ step = 1,
40
+ disabled = false,
41
+ readonly = false,
42
+ required = false,
43
+ placeholder,
44
+ name,
45
+ id,
46
+ showStepper = true,
47
+ onValueChange,
48
+ error,
49
+ class: className,
50
+ }: Props = $props();
51
+
52
+ function clampValue(val: number): number {
53
+ let result = val;
54
+ if (min !== undefined && result < min) result = min;
55
+ if (max !== undefined && result > max) result = max;
56
+ return result;
57
+ }
58
+
59
+ function handleInput(e: Event) {
60
+ const target = e.target as HTMLInputElement;
61
+ const inputValue = target.value;
62
+
63
+ if (inputValue === '' || inputValue === '-') {
64
+ value = null;
65
+ onValueChange?.(null);
66
+ return;
67
+ }
68
+
69
+ const parsed = parseFloat(inputValue);
70
+ if (!isNaN(parsed)) {
71
+ value = parsed;
72
+ onValueChange?.(parsed);
73
+ }
74
+ }
75
+
76
+ function handleBlur() {
77
+ // Clamp value on blur
78
+ if (value !== null) {
79
+ const clamped = clampValue(value);
80
+ if (clamped !== value) {
81
+ value = clamped;
82
+ onValueChange?.(clamped);
83
+ }
84
+ }
85
+ }
86
+
87
+ function increment() {
88
+ if (disabled || readonly) return;
89
+ const currentValue = value ?? min ?? 0;
90
+ const newValue = clampValue(currentValue + step);
91
+ value = newValue;
92
+ onValueChange?.(newValue);
93
+ }
94
+
95
+ function decrement() {
96
+ if (disabled || readonly) return;
97
+ const currentValue = value ?? max ?? 0;
98
+ const newValue = clampValue(currentValue - step);
99
+ value = newValue;
100
+ onValueChange?.(newValue);
101
+ }
102
+
103
+ function handleKeyDown(e: KeyboardEvent) {
104
+ if (e.key === 'ArrowUp') {
105
+ e.preventDefault();
106
+ increment();
107
+ } else if (e.key === 'ArrowDown') {
108
+ e.preventDefault();
109
+ decrement();
110
+ }
111
+ }
112
+
113
+ const canDecrement = $derived(
114
+ !disabled && !readonly && (min === undefined || (value ?? 0) > min)
115
+ );
116
+ const canIncrement = $derived(
117
+ !disabled && !readonly && (max === undefined || (value ?? 0) < max)
118
+ );
119
+
120
+ const displayValue = $derived(value !== null ? String(value) : '');
121
+ </script>
122
+
123
+ <div class={cn('relative flex items-center', className)}>
124
+ {#if showStepper}
125
+ <button
126
+ type="button"
127
+ onclick={decrement}
128
+ disabled={disabled || !canDecrement}
129
+ class={cn(
130
+ 'flex h-11 w-11 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
131
+ error && 'border-destructive'
132
+ )}
133
+ aria-label="Decrease value"
134
+ >
135
+ <svg
136
+ xmlns="http://www.w3.org/2000/svg"
137
+ width="16"
138
+ height="16"
139
+ viewBox="0 0 24 24"
140
+ fill="none"
141
+ stroke="currentColor"
142
+ stroke-width="2"
143
+ stroke-linecap="round"
144
+ stroke-linejoin="round"
145
+ >
146
+ <path d="M5 12h14" />
147
+ </svg>
148
+ </button>
149
+ {/if}
150
+
151
+ <input
152
+ type="text"
153
+ inputmode="decimal"
154
+ value={displayValue}
155
+ {placeholder}
156
+ {disabled}
157
+ {readonly}
158
+ {required}
159
+ {name}
160
+ {id}
161
+ class={cn(
162
+ 'flex h-11 w-full bg-background px-3 py-2 text-sm text-center ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
163
+ showStepper ? 'border-y border-input' : 'border border-input rounded-md',
164
+ error && 'border-destructive focus-visible:ring-destructive'
165
+ )}
166
+ oninput={handleInput}
167
+ onblur={handleBlur}
168
+ onkeydown={handleKeyDown}
169
+ aria-invalid={error ? 'true' : undefined}
170
+ />
171
+
172
+ {#if showStepper}
173
+ <button
174
+ type="button"
175
+ onclick={increment}
176
+ disabled={disabled || !canIncrement}
177
+ class={cn(
178
+ 'flex h-11 w-11 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
179
+ error && 'border-destructive'
180
+ )}
181
+ aria-label="Increase value"
182
+ >
183
+ <svg
184
+ xmlns="http://www.w3.org/2000/svg"
185
+ width="16"
186
+ height="16"
187
+ viewBox="0 0 24 24"
188
+ fill="none"
189
+ stroke="currentColor"
190
+ stroke-width="2"
191
+ stroke-linecap="round"
192
+ stroke-linejoin="round"
193
+ >
194
+ <path d="M5 12h14" />
195
+ <path d="M12 5v14" />
196
+ </svg>
197
+ </button>
198
+ {/if}
199
+ </div>
200
+
201
+ {#if error}
202
+ <p class="text-sm text-destructive mt-1.5" role="alert">
203
+ {error}
204
+ </p>
205
+ {/if}
@@ -0,0 +1,33 @@
1
+ interface Props {
2
+ /** Current numeric value */
3
+ value?: number | null;
4
+ /** Minimum allowed value */
5
+ min?: number;
6
+ /** Maximum allowed value */
7
+ max?: number;
8
+ /** Step increment for stepper buttons and arrow keys */
9
+ step?: number;
10
+ /** Whether the input is disabled */
11
+ disabled?: boolean;
12
+ /** Whether the input is readonly */
13
+ readonly?: boolean;
14
+ /** Whether the input is required */
15
+ required?: boolean;
16
+ /** Placeholder text */
17
+ placeholder?: string;
18
+ /** Name attribute for form submission */
19
+ name?: string;
20
+ /** Element ID */
21
+ id?: string;
22
+ /** Whether to show +/- stepper buttons */
23
+ showStepper?: boolean;
24
+ /** Callback when value changes */
25
+ onValueChange?: (value: number | null) => void;
26
+ /** Error state */
27
+ error?: string;
28
+ /** Additional class for the container */
29
+ class?: string;
30
+ }
31
+ declare const NumberInput: import("svelte").Component<Props, {}, "value">;
32
+ type NumberInput = ReturnType<typeof NumberInput>;
33
+ export default NumberInput;