@classic-homes/theme-svelte 0.1.4 → 0.1.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.
- package/dist/lib/components/CardHeader.svelte +22 -2
- package/dist/lib/components/CardHeader.svelte.d.ts +5 -4
- 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/HeaderSearch.svelte +340 -0
- package/dist/lib/components/HeaderSearch.svelte.d.ts +37 -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/PageHeader.svelte +6 -0
- package/dist/lib/components/PageHeader.svelte.d.ts +1 -1
- 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/AuthLayout.svelte +133 -0
- package/dist/lib/components/layout/AuthLayout.svelte.d.ts +48 -0
- package/dist/lib/components/layout/DashboardLayout.svelte +100 -74
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +17 -10
- package/dist/lib/components/layout/ErrorLayout.svelte +206 -0
- package/dist/lib/components/layout/ErrorLayout.svelte.d.ts +52 -0
- package/dist/lib/components/layout/FormPageLayout.svelte +2 -8
- package/dist/lib/components/layout/Header.svelte +232 -41
- package/dist/lib/components/layout/Header.svelte.d.ts +71 -5
- package/dist/lib/components/layout/PublicLayout.svelte +54 -80
- package/dist/lib/components/layout/PublicLayout.svelte.d.ts +3 -1
- 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 +378 -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 +13 -2
- package/dist/lib/index.js +11 -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 +203 -3
- package/package.json +1 -1
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
5
|
+
|
|
6
|
+
const cardHeaderVariants = tv({
|
|
7
|
+
base: 'flex flex-col space-y-1.5',
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'p-6',
|
|
11
|
+
compact: 'p-4',
|
|
12
|
+
bordered: 'p-6 border-b',
|
|
13
|
+
shaded: 'p-6 bg-muted/50',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: 'default',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type CardHeaderVariants = VariantProps<typeof cardHeaderVariants>;
|
|
4
22
|
|
|
5
23
|
interface Props {
|
|
24
|
+
/** Visual variant */
|
|
25
|
+
variant?: CardHeaderVariants['variant'];
|
|
6
26
|
class?: string;
|
|
7
27
|
children: Snippet;
|
|
8
28
|
[key: string]: unknown;
|
|
9
29
|
}
|
|
10
30
|
|
|
11
|
-
let { class: className, children, ...restProps }: Props = $props();
|
|
31
|
+
let { variant = 'default', class: className, children, ...restProps }: Props = $props();
|
|
12
32
|
</script>
|
|
13
33
|
|
|
14
|
-
<div class={cn(
|
|
34
|
+
<div class={cn(cardHeaderVariants({ variant }), className)} {...restProps}>
|
|
15
35
|
{@render children()}
|
|
16
36
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
2
|
+
declare const CardHeader: import("svelte").Component<{
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
/** Visual variant */
|
|
5
|
+
variant?: "default" | "compact" | "bordered" | "shaded" | undefined;
|
|
3
6
|
class?: string;
|
|
4
7
|
children: Snippet;
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
declare const CardHeader: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
}, {}, "">;
|
|
8
9
|
type CardHeader = ReturnType<typeof CardHeader>;
|
|
9
10
|
export default CardHeader;
|
|
@@ -0,0 +1,187 @@
|
|
|
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 ComboboxOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Current selected value */
|
|
14
|
+
value?: string;
|
|
15
|
+
/** Callback when value changes */
|
|
16
|
+
onValueChange?: (value: string) => void;
|
|
17
|
+
/** Array of selectable options */
|
|
18
|
+
options: ComboboxOption[];
|
|
19
|
+
/** Placeholder text for the input */
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Message to display when no results found */
|
|
22
|
+
emptyMessage?: string;
|
|
23
|
+
/** Whether the combobox 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
|
+
/** Whether async data is loading */
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
/** Callback when search query changes (for async loading) */
|
|
36
|
+
onSearch?: (query: string) => void;
|
|
37
|
+
/** Debounce delay in ms for search callback (default: 300) */
|
|
38
|
+
debounceMs?: number;
|
|
39
|
+
/** Additional class for the trigger */
|
|
40
|
+
class?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
value = $bindable(''),
|
|
45
|
+
onValueChange,
|
|
46
|
+
options,
|
|
47
|
+
placeholder = 'Select an option...',
|
|
48
|
+
emptyMessage = 'No results found.',
|
|
49
|
+
disabled = false,
|
|
50
|
+
required = false,
|
|
51
|
+
name,
|
|
52
|
+
id,
|
|
53
|
+
error,
|
|
54
|
+
loading = false,
|
|
55
|
+
onSearch,
|
|
56
|
+
debounceMs = 300,
|
|
57
|
+
class: className,
|
|
58
|
+
}: Props = $props();
|
|
59
|
+
|
|
60
|
+
let searchQuery = $state('');
|
|
61
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
62
|
+
let open = $state(false);
|
|
63
|
+
|
|
64
|
+
// Find selected option label
|
|
65
|
+
const selectedLabel = $derived(options.find((opt) => opt.value === value)?.label);
|
|
66
|
+
|
|
67
|
+
// Always filter options locally based on search query
|
|
68
|
+
const filteredOptions = $derived.by(() => {
|
|
69
|
+
if (!searchQuery) return options;
|
|
70
|
+
const query = searchQuery.toLowerCase();
|
|
71
|
+
return options.filter((opt) => opt.label.toLowerCase().includes(query));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function handleSearchInput(e: Event) {
|
|
75
|
+
const target = e.target as HTMLInputElement;
|
|
76
|
+
searchQuery = target.value;
|
|
77
|
+
|
|
78
|
+
if (onSearch) {
|
|
79
|
+
// Debounce the search callback
|
|
80
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
81
|
+
debounceTimer = setTimeout(() => {
|
|
82
|
+
onSearch(searchQuery);
|
|
83
|
+
}, debounceMs);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleValueChange(newValue: string | undefined) {
|
|
88
|
+
if (newValue !== undefined) {
|
|
89
|
+
value = newValue;
|
|
90
|
+
onValueChange?.(newValue);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleOpenChange(isOpen: boolean) {
|
|
95
|
+
open = isOpen;
|
|
96
|
+
if (!isOpen) {
|
|
97
|
+
searchQuery = '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cleanup debounce timer on unmount
|
|
102
|
+
$effect(() => {
|
|
103
|
+
return () => {
|
|
104
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<ComboboxPrimitive.Root
|
|
110
|
+
type="single"
|
|
111
|
+
{disabled}
|
|
112
|
+
{required}
|
|
113
|
+
{name}
|
|
114
|
+
bind:open
|
|
115
|
+
onOpenChange={handleOpenChange}
|
|
116
|
+
onValueChange={handleValueChange}
|
|
117
|
+
{value}
|
|
118
|
+
>
|
|
119
|
+
<div class="relative">
|
|
120
|
+
<ComboboxPrimitive.Input
|
|
121
|
+
{id}
|
|
122
|
+
placeholder={selectedLabel || placeholder}
|
|
123
|
+
oninput={handleSearchInput}
|
|
124
|
+
onfocus={() => (open = true)}
|
|
125
|
+
onclick={() => (open = true)}
|
|
126
|
+
class={cn(
|
|
127
|
+
'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',
|
|
128
|
+
error && 'border-destructive focus:ring-destructive',
|
|
129
|
+
className
|
|
130
|
+
)}
|
|
131
|
+
aria-invalid={error ? 'true' : undefined}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<ComboboxPrimitive.Content
|
|
136
|
+
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"
|
|
137
|
+
sideOffset={4}
|
|
138
|
+
>
|
|
139
|
+
<div class="p-1 max-h-[300px] overflow-y-auto">
|
|
140
|
+
{#if loading}
|
|
141
|
+
<div class="flex items-center justify-center py-6">
|
|
142
|
+
<Spinner size="sm" />
|
|
143
|
+
<span class="ml-2 text-sm text-muted-foreground">Loading...</span>
|
|
144
|
+
</div>
|
|
145
|
+
{:else if filteredOptions.length === 0}
|
|
146
|
+
<div class="py-6 text-center text-sm text-muted-foreground">
|
|
147
|
+
{emptyMessage}
|
|
148
|
+
</div>
|
|
149
|
+
{:else}
|
|
150
|
+
{#each filteredOptions as option}
|
|
151
|
+
<ComboboxPrimitive.Item
|
|
152
|
+
value={option.value}
|
|
153
|
+
label={option.label}
|
|
154
|
+
disabled={option.disabled}
|
|
155
|
+
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"
|
|
156
|
+
>
|
|
157
|
+
{#if option.value === value}
|
|
158
|
+
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
159
|
+
<svg
|
|
160
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
161
|
+
width="16"
|
|
162
|
+
height="16"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
stroke-width="2"
|
|
167
|
+
stroke-linecap="round"
|
|
168
|
+
stroke-linejoin="round"
|
|
169
|
+
class="h-4 w-4"
|
|
170
|
+
>
|
|
171
|
+
<polyline points="20 6 9 17 4 12" />
|
|
172
|
+
</svg>
|
|
173
|
+
</span>
|
|
174
|
+
{/if}
|
|
175
|
+
{option.label}
|
|
176
|
+
</ComboboxPrimitive.Item>
|
|
177
|
+
{/each}
|
|
178
|
+
{/if}
|
|
179
|
+
</div>
|
|
180
|
+
</ComboboxPrimitive.Content>
|
|
181
|
+
</ComboboxPrimitive.Root>
|
|
182
|
+
|
|
183
|
+
{#if error}
|
|
184
|
+
<p class="text-sm text-destructive mt-1.5" role="alert">
|
|
185
|
+
{error}
|
|
186
|
+
</p>
|
|
187
|
+
{/if}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ComboboxOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Current selected value */
|
|
8
|
+
value?: string;
|
|
9
|
+
/** Callback when value changes */
|
|
10
|
+
onValueChange?: (value: string) => void;
|
|
11
|
+
/** Array of selectable options */
|
|
12
|
+
options: ComboboxOption[];
|
|
13
|
+
/** Placeholder text for the input */
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** Message to display when no results found */
|
|
16
|
+
emptyMessage?: string;
|
|
17
|
+
/** Whether the combobox 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
|
+
/** Whether async data is loading */
|
|
28
|
+
loading?: boolean;
|
|
29
|
+
/** Callback when search query changes (for async loading) */
|
|
30
|
+
onSearch?: (query: string) => void;
|
|
31
|
+
/** Debounce delay in ms for search callback (default: 300) */
|
|
32
|
+
debounceMs?: number;
|
|
33
|
+
/** Additional class for the trigger */
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
declare const Combobox: import("svelte").Component<Props, {}, "value">;
|
|
37
|
+
type Combobox = ReturnType<typeof Combobox>;
|
|
38
|
+
export default Combobox;
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Popover, Calendar } from 'bits-ui';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Current date/time value */
|
|
7
|
+
value?: Date | null;
|
|
8
|
+
/** Callback when value changes */
|
|
9
|
+
onValueChange?: (date: Date | null) => void;
|
|
10
|
+
/** Placeholder text */
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** Whether the picker is disabled */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Whether selection is required */
|
|
15
|
+
required?: boolean;
|
|
16
|
+
/** Minimum selectable date */
|
|
17
|
+
min?: Date;
|
|
18
|
+
/** Maximum selectable date */
|
|
19
|
+
max?: Date;
|
|
20
|
+
/** Time format (12h or 24h) */
|
|
21
|
+
timeFormat?: '12h' | '24h';
|
|
22
|
+
/** Minute step interval (default: 15) */
|
|
23
|
+
minuteStep?: number;
|
|
24
|
+
/** Name attribute for form submission */
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Element ID */
|
|
27
|
+
id?: string;
|
|
28
|
+
/** Error message to display */
|
|
29
|
+
error?: string;
|
|
30
|
+
/** Additional class for the trigger */
|
|
31
|
+
class?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
value = $bindable(null),
|
|
36
|
+
onValueChange,
|
|
37
|
+
placeholder = 'Select date and time...',
|
|
38
|
+
disabled = false,
|
|
39
|
+
required = false,
|
|
40
|
+
min,
|
|
41
|
+
max,
|
|
42
|
+
timeFormat = '12h',
|
|
43
|
+
minuteStep = 15,
|
|
44
|
+
name,
|
|
45
|
+
id,
|
|
46
|
+
error,
|
|
47
|
+
class: className,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
let open = $state(false);
|
|
51
|
+
|
|
52
|
+
// Calendar state - initialize with current date
|
|
53
|
+
let viewMonth = $state(new Date().getMonth());
|
|
54
|
+
let viewYear = $state(new Date().getFullYear());
|
|
55
|
+
|
|
56
|
+
// Time state
|
|
57
|
+
let hour = $state(12);
|
|
58
|
+
let minute = $state(0);
|
|
59
|
+
let period = $state<'AM' | 'PM'>('PM');
|
|
60
|
+
|
|
61
|
+
// Sync state when value changes externally
|
|
62
|
+
$effect(() => {
|
|
63
|
+
if (value) {
|
|
64
|
+
hour = value.getHours();
|
|
65
|
+
minute = value.getMinutes();
|
|
66
|
+
period = value.getHours() >= 12 ? 'PM' : 'AM';
|
|
67
|
+
viewMonth = value.getMonth();
|
|
68
|
+
viewYear = value.getFullYear();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Format display value
|
|
73
|
+
const displayValue = $derived.by(() => {
|
|
74
|
+
if (!value) return '';
|
|
75
|
+
const dateStr = value.toLocaleDateString('en-US', {
|
|
76
|
+
month: 'short',
|
|
77
|
+
day: 'numeric',
|
|
78
|
+
year: 'numeric',
|
|
79
|
+
});
|
|
80
|
+
const timeStr = value.toLocaleTimeString('en-US', {
|
|
81
|
+
hour: 'numeric',
|
|
82
|
+
minute: '2-digit',
|
|
83
|
+
hour12: timeFormat === '12h',
|
|
84
|
+
});
|
|
85
|
+
return `${dateStr} at ${timeStr}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Generate calendar days for current month
|
|
89
|
+
const calendarDays = $derived.by(() => {
|
|
90
|
+
const firstDay = new Date(viewYear, viewMonth, 1);
|
|
91
|
+
const lastDay = new Date(viewYear, viewMonth + 1, 0);
|
|
92
|
+
const daysInMonth = lastDay.getDate();
|
|
93
|
+
const startDayOfWeek = firstDay.getDay();
|
|
94
|
+
|
|
95
|
+
const days: (number | null)[] = [];
|
|
96
|
+
|
|
97
|
+
// Add empty slots for days before the 1st
|
|
98
|
+
for (let i = 0; i < startDayOfWeek; i++) {
|
|
99
|
+
days.push(null);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add days of the month
|
|
103
|
+
for (let i = 1; i <= daysInMonth; i++) {
|
|
104
|
+
days.push(i);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return days;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Generate time options
|
|
111
|
+
const hourOptions = $derived(
|
|
112
|
+
timeFormat === '12h'
|
|
113
|
+
? Array.from({ length: 12 }, (_, i) => (i === 0 ? 12 : i))
|
|
114
|
+
: Array.from({ length: 24 }, (_, i) => i)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const minuteOptions = $derived(Array.from({ length: 60 / minuteStep }, (_, i) => i * minuteStep));
|
|
118
|
+
|
|
119
|
+
function isDateDisabled(day: number): boolean {
|
|
120
|
+
const date = new Date(viewYear, viewMonth, day);
|
|
121
|
+
if (min && date < new Date(min.getFullYear(), min.getMonth(), min.getDate())) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (max && date > new Date(max.getFullYear(), max.getMonth(), max.getDate())) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isDateSelected(day: number): boolean {
|
|
131
|
+
if (!value) return false;
|
|
132
|
+
return (
|
|
133
|
+
value.getDate() === day && value.getMonth() === viewMonth && value.getFullYear() === viewYear
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function selectDate(day: number) {
|
|
138
|
+
if (isDateDisabled(day)) return;
|
|
139
|
+
|
|
140
|
+
const newDate = new Date(viewYear, viewMonth, day);
|
|
141
|
+
|
|
142
|
+
// Preserve or set time
|
|
143
|
+
let h = hour;
|
|
144
|
+
if (timeFormat === '12h') {
|
|
145
|
+
h = period === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour;
|
|
146
|
+
}
|
|
147
|
+
newDate.setHours(h, minute, 0, 0);
|
|
148
|
+
|
|
149
|
+
value = newDate;
|
|
150
|
+
onValueChange?.(newDate);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updateTime() {
|
|
154
|
+
if (!value) {
|
|
155
|
+
// If no date selected, use today
|
|
156
|
+
const today = new Date();
|
|
157
|
+
let h = hour;
|
|
158
|
+
if (timeFormat === '12h') {
|
|
159
|
+
h = period === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour;
|
|
160
|
+
}
|
|
161
|
+
today.setHours(h, minute, 0, 0);
|
|
162
|
+
value = today;
|
|
163
|
+
onValueChange?.(today);
|
|
164
|
+
} else {
|
|
165
|
+
const newDate = new Date(value);
|
|
166
|
+
let h = hour;
|
|
167
|
+
if (timeFormat === '12h') {
|
|
168
|
+
h = period === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour;
|
|
169
|
+
}
|
|
170
|
+
newDate.setHours(h, minute, 0, 0);
|
|
171
|
+
value = newDate;
|
|
172
|
+
onValueChange?.(newDate);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function prevMonth() {
|
|
177
|
+
if (viewMonth === 0) {
|
|
178
|
+
viewMonth = 11;
|
|
179
|
+
viewYear--;
|
|
180
|
+
} else {
|
|
181
|
+
viewMonth--;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function nextMonth() {
|
|
186
|
+
if (viewMonth === 11) {
|
|
187
|
+
viewMonth = 0;
|
|
188
|
+
viewYear++;
|
|
189
|
+
} else {
|
|
190
|
+
viewMonth++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function clearValue() {
|
|
195
|
+
value = null;
|
|
196
|
+
onValueChange?.(null);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const monthNames = [
|
|
200
|
+
'January',
|
|
201
|
+
'February',
|
|
202
|
+
'March',
|
|
203
|
+
'April',
|
|
204
|
+
'May',
|
|
205
|
+
'June',
|
|
206
|
+
'July',
|
|
207
|
+
'August',
|
|
208
|
+
'September',
|
|
209
|
+
'October',
|
|
210
|
+
'November',
|
|
211
|
+
'December',
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<Popover.Root bind:open>
|
|
218
|
+
<Popover.Trigger
|
|
219
|
+
{id}
|
|
220
|
+
{disabled}
|
|
221
|
+
class={cn(
|
|
222
|
+
'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 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
223
|
+
!value && 'text-muted-foreground',
|
|
224
|
+
error && 'border-destructive focus:ring-destructive',
|
|
225
|
+
className
|
|
226
|
+
)}
|
|
227
|
+
aria-invalid={error ? 'true' : undefined}
|
|
228
|
+
>
|
|
229
|
+
<span class="truncate">
|
|
230
|
+
{displayValue || placeholder}
|
|
231
|
+
</span>
|
|
232
|
+
<svg
|
|
233
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
234
|
+
width="16"
|
|
235
|
+
height="16"
|
|
236
|
+
viewBox="0 0 24 24"
|
|
237
|
+
fill="none"
|
|
238
|
+
stroke="currentColor"
|
|
239
|
+
stroke-width="2"
|
|
240
|
+
stroke-linecap="round"
|
|
241
|
+
stroke-linejoin="round"
|
|
242
|
+
class="h-4 w-4 opacity-50 shrink-0 ml-2"
|
|
243
|
+
>
|
|
244
|
+
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
|
|
245
|
+
<line x1="16" x2="16" y1="2" y2="6" />
|
|
246
|
+
<line x1="8" x2="8" y1="2" y2="6" />
|
|
247
|
+
<line x1="3" x2="21" y1="10" y2="10" />
|
|
248
|
+
<path d="M8 14h.01" />
|
|
249
|
+
<path d="M12 14h.01" />
|
|
250
|
+
<path d="M16 14h.01" />
|
|
251
|
+
<path d="M8 18h.01" />
|
|
252
|
+
<path d="M12 18h.01" />
|
|
253
|
+
<path d="M16 18h.01" />
|
|
254
|
+
</svg>
|
|
255
|
+
</Popover.Trigger>
|
|
256
|
+
|
|
257
|
+
<Popover.Content
|
|
258
|
+
class="w-auto p-0 rounded-md border bg-popover text-popover-foreground shadow-md"
|
|
259
|
+
sideOffset={4}
|
|
260
|
+
align="start"
|
|
261
|
+
>
|
|
262
|
+
<div class="p-3">
|
|
263
|
+
<!-- Calendar header -->
|
|
264
|
+
<div class="flex items-center justify-between mb-2">
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
onclick={prevMonth}
|
|
268
|
+
class="h-7 w-7 inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
|
269
|
+
aria-label="Previous month"
|
|
270
|
+
>
|
|
271
|
+
<svg
|
|
272
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
273
|
+
width="16"
|
|
274
|
+
height="16"
|
|
275
|
+
viewBox="0 0 24 24"
|
|
276
|
+
fill="none"
|
|
277
|
+
stroke="currentColor"
|
|
278
|
+
stroke-width="2"
|
|
279
|
+
stroke-linecap="round"
|
|
280
|
+
stroke-linejoin="round"
|
|
281
|
+
>
|
|
282
|
+
<path d="m15 18-6-6 6-6" />
|
|
283
|
+
</svg>
|
|
284
|
+
</button>
|
|
285
|
+
<span class="text-sm font-medium">
|
|
286
|
+
{monthNames[viewMonth]}
|
|
287
|
+
{viewYear}
|
|
288
|
+
</span>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
onclick={nextMonth}
|
|
292
|
+
class="h-7 w-7 inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
|
293
|
+
aria-label="Next month"
|
|
294
|
+
>
|
|
295
|
+
<svg
|
|
296
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
297
|
+
width="16"
|
|
298
|
+
height="16"
|
|
299
|
+
viewBox="0 0 24 24"
|
|
300
|
+
fill="none"
|
|
301
|
+
stroke="currentColor"
|
|
302
|
+
stroke-width="2"
|
|
303
|
+
stroke-linecap="round"
|
|
304
|
+
stroke-linejoin="round"
|
|
305
|
+
>
|
|
306
|
+
<path d="m9 18 6-6-6-6" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<!-- Day names -->
|
|
312
|
+
<div class="grid grid-cols-7 gap-1 mb-1">
|
|
313
|
+
{#each dayNames as dayName}
|
|
314
|
+
<div
|
|
315
|
+
class="h-8 w-8 flex items-center justify-center text-xs text-muted-foreground font-medium"
|
|
316
|
+
>
|
|
317
|
+
{dayName}
|
|
318
|
+
</div>
|
|
319
|
+
{/each}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<!-- Calendar days -->
|
|
323
|
+
<div class="grid grid-cols-7 gap-1">
|
|
324
|
+
{#each calendarDays as day}
|
|
325
|
+
{#if day === null}
|
|
326
|
+
<div class="h-8 w-8"></div>
|
|
327
|
+
{:else}
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
onclick={() => selectDate(day)}
|
|
331
|
+
disabled={isDateDisabled(day)}
|
|
332
|
+
class={cn(
|
|
333
|
+
'h-8 w-8 inline-flex items-center justify-center rounded-md text-sm transition-colors',
|
|
334
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
335
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
336
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
337
|
+
isDateSelected(day) &&
|
|
338
|
+
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
|
339
|
+
)}
|
|
340
|
+
>
|
|
341
|
+
{day}
|
|
342
|
+
</button>
|
|
343
|
+
{/if}
|
|
344
|
+
{/each}
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<!-- Time selector -->
|
|
348
|
+
<div class="border-t mt-3 pt-3">
|
|
349
|
+
<div class="flex items-center gap-2">
|
|
350
|
+
<span class="text-sm text-muted-foreground">Time:</span>
|
|
351
|
+
<select
|
|
352
|
+
bind:value={hour}
|
|
353
|
+
onchange={updateTime}
|
|
354
|
+
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
355
|
+
>
|
|
356
|
+
{#each hourOptions as h}
|
|
357
|
+
<option value={timeFormat === '12h' ? h : h}>
|
|
358
|
+
{String(h).padStart(2, '0')}
|
|
359
|
+
</option>
|
|
360
|
+
{/each}
|
|
361
|
+
</select>
|
|
362
|
+
<span class="text-muted-foreground">:</span>
|
|
363
|
+
<select
|
|
364
|
+
bind:value={minute}
|
|
365
|
+
onchange={updateTime}
|
|
366
|
+
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
367
|
+
>
|
|
368
|
+
{#each minuteOptions as m}
|
|
369
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
370
|
+
{/each}
|
|
371
|
+
</select>
|
|
372
|
+
{#if timeFormat === '12h'}
|
|
373
|
+
<select
|
|
374
|
+
bind:value={period}
|
|
375
|
+
onchange={updateTime}
|
|
376
|
+
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
377
|
+
>
|
|
378
|
+
<option value="AM">AM</option>
|
|
379
|
+
<option value="PM">PM</option>
|
|
380
|
+
</select>
|
|
381
|
+
{/if}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<!-- Actions -->
|
|
386
|
+
<div class="border-t mt-3 pt-3 flex justify-between">
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
onclick={clearValue}
|
|
390
|
+
class="text-sm text-muted-foreground hover:text-foreground"
|
|
391
|
+
>
|
|
392
|
+
Clear
|
|
393
|
+
</button>
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
onclick={() => (open = false)}
|
|
397
|
+
class="text-sm font-medium text-primary hover:underline"
|
|
398
|
+
>
|
|
399
|
+
Done
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</Popover.Content>
|
|
404
|
+
</Popover.Root>
|
|
405
|
+
|
|
406
|
+
<!-- Hidden input for form submission -->
|
|
407
|
+
{#if name && value}
|
|
408
|
+
<input type="hidden" {name} value={value.toISOString()} />
|
|
409
|
+
{/if}
|
|
410
|
+
|
|
411
|
+
{#if error}
|
|
412
|
+
<p class="text-sm text-destructive mt-1.5" role="alert">
|
|
413
|
+
{error}
|
|
414
|
+
</p>
|
|
415
|
+
{/if}
|