@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,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;
|