yaxa-svelte 1.3.0 → 1.4.0

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/README.md CHANGED
@@ -68,7 +68,7 @@ export default defineConfig({
68
68
 
69
69
  ### 3. Define Your Site Configuration
70
70
 
71
- Create `src/site.config.ts` — your single source of truth for branding, metadata, and SEO:
71
+ Create `src/site.config.ts` — your single source of truth for branding, metadata, licensing, and SEO:
72
72
 
73
73
  ```ts
74
74
  // src/site.config.ts
@@ -79,14 +79,20 @@ export const siteConfig = defineSiteConfig({
79
79
  title: 'My Indie SaaS — Build Fast with SvelteKit',
80
80
  description: 'Fast, beautiful, accessible web applications.',
81
81
  url: 'https://my-app.com',
82
+ // Single-email mode: Solo devs can set one email for contact, support & DPO
83
+ email: 'hello@my-app.com',
84
+ project: {
85
+ license: 'MIT',
86
+ type: 'open-source',
87
+ repositoryUrl: 'https://github.com/my-org/my-app'
88
+ },
82
89
  theme: {
83
90
  accent: 'svelte', // 'svelte' | 'emerald' | 'amber' | 'sky' | 'violet' | 'rose' | 'indigo'
84
91
  neutral: 'zinc', // 'zinc' | 'slate' | 'stone' | 'neutral'
85
92
  defaultMode: 'dark'
86
93
  },
87
94
  socials: {
88
- github: 'https://github.com/my-org/my-app',
89
- twitter: '@my_app'
95
+ github: 'https://github.com/my-org/my-app'
90
96
  }
91
97
  });
92
98
  ```
@@ -219,15 +225,15 @@ export const POST = createPolarWebhookHandler();
219
225
 
220
226
  ## 🧩 Component Library
221
227
 
222
- | Category | Components |
223
- | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
224
- | **SaaS Suite** | `<Gate>`, `<AuthCard>`, `<UserMenu>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
225
- | **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
226
- | **Elements** | `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `DataTable`, `Chip`, `Meter`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo` |
227
- | **Forms** | `Form`, `FormField`, `Input`, `Textarea`, `Checkbox`, `Switch`, `Select`, `RadioGroup`, `Slider`, `ColorPicker` |
228
- | **Layout** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp` |
229
- | **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Slideover`, `Popover`, `Tooltip`, `Alert`, `Accordion` |
230
- | **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce` |
228
+ | Category | Components |
229
+ | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
230
+ | **SaaS Suite** | `<Gate>`, `<AuthCard>`, `<UserMenu>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
231
+ | **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
232
+ | **Elements** | `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `DataTable`, `Chip`, `Meter`, `MetricCard`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo` |
233
+ | **Forms** | `Form`, `FormField`, `Input`, `InputOTP`, `Textarea`, `Checkbox`, `Switch`, `Select`, `RadioGroup`, `Slider`, `ColorPicker`, `ToggleGroup`, `DatePicker`, `DateRangePicker` |
234
+ | **Layout & Blocks** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp`, Page Blocks (Hero, SaaS Metrics, 2FA Security) |
235
+ | **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Slideover`, `Popover`, `Tooltip`, `Alert`, `Accordion` |
236
+ | **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce` |
231
237
 
232
238
  ---
233
239
 
@@ -0,0 +1,228 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Icon, { type IconSource } from './Icon.svelte';
4
+ import Badge from './Badge.svelte';
5
+ import Skeleton from './Skeleton.svelte';
6
+ import { cn } from '../../utils/cn';
7
+
8
+ interface Props {
9
+ title: string;
10
+ value: string | number;
11
+ change?: number;
12
+ changeType?: 'percent' | 'absolute';
13
+ changePeriod?: string;
14
+ icon?: IconSource;
15
+ sparkline?: number[];
16
+ sparklineColor?: 'primary' | 'success' | 'warning' | 'error' | 'neutral' | 'auto';
17
+ variant?: 'outline' | 'flat' | 'elevated';
18
+ loading?: boolean;
19
+ class?: string;
20
+ children?: Snippet;
21
+ }
22
+
23
+ let {
24
+ title,
25
+ value,
26
+ change,
27
+ changeType = 'percent',
28
+ changePeriod = 'vs last period',
29
+ icon,
30
+ sparkline,
31
+ sparklineColor = 'auto',
32
+ variant = 'outline',
33
+ loading = false,
34
+ class: className = '',
35
+ children
36
+ }: Props = $props();
37
+
38
+ let isPositive = $derived(change !== undefined && change >= 0);
39
+ let changeFormatted = $derived.by(() => {
40
+ if (change === undefined) return '';
41
+ const prefix = change >= 0 ? '+' : '';
42
+ return `${prefix}${change}${changeType === 'percent' ? '%' : ''}`;
43
+ });
44
+
45
+ let resolvedColor = $derived.by(() => {
46
+ if (sparklineColor !== 'auto') return sparklineColor;
47
+ if (change !== undefined) {
48
+ return change >= 0 ? 'success' : 'error';
49
+ }
50
+ return 'primary';
51
+ });
52
+
53
+ // SVG Sparkline geometry calculation (viewBox: 0 0 120 36)
54
+ let sparklineData = $derived.by(() => {
55
+ if (!sparkline || sparkline.length < 2) return null;
56
+ const width = 120;
57
+ const height = 36;
58
+ const padding = 3;
59
+
60
+ const min = Math.min(...sparkline);
61
+ const max = Math.max(...sparkline);
62
+ const range = max === min ? 1 : max - min;
63
+
64
+ const points = sparkline.map((val, idx) => {
65
+ const x = (idx / (sparkline.length - 1)) * width;
66
+ const y = height - padding - ((val - min) / range) * (height - padding * 2);
67
+ return { x, y };
68
+ });
69
+
70
+ const linePath = points.reduce(
71
+ (acc, pt, i) =>
72
+ i === 0
73
+ ? `M ${pt.x.toFixed(1)} ${pt.y.toFixed(1)}`
74
+ : `${acc} L ${pt.x.toFixed(1)} ${pt.y.toFixed(1)}`,
75
+ ''
76
+ );
77
+
78
+ const firstX = points[0].x.toFixed(1);
79
+ const lastX = points[points.length - 1].x.toFixed(1);
80
+ const areaPath = `${linePath} L ${lastX} ${height} L ${firstX} ${height} Z`;
81
+
82
+ const lastPoint = points[points.length - 1];
83
+
84
+ return { linePath, areaPath, lastPoint };
85
+ });
86
+
87
+ let colorHex = $derived.by(() => {
88
+ switch (resolvedColor) {
89
+ case 'success':
90
+ return { stroke: '#10b981', fill: '#10b981' };
91
+ case 'error':
92
+ return { stroke: '#f43f5e', fill: '#f43f5e' };
93
+ case 'warning':
94
+ return { stroke: '#f59e0b', fill: '#f59e0b' };
95
+ case 'neutral':
96
+ return { stroke: '#71717a', fill: '#71717a' };
97
+ case 'primary':
98
+ default:
99
+ return { stroke: '#ff3e00', fill: '#ff3e00' };
100
+ }
101
+ });
102
+
103
+ let containerClasses = $derived.by(() => {
104
+ const base =
105
+ 'relative overflow-hidden rounded-xl p-5 transition-all duration-200 bg-white dark:bg-neutral-900';
106
+ switch (variant) {
107
+ case 'flat':
108
+ return cn(base, 'bg-neutral-50/80 dark:bg-neutral-900/60 border-0', className);
109
+ case 'elevated':
110
+ return cn(
111
+ base,
112
+ 'shadow-md hover:shadow-lg border border-neutral-200/80 dark:border-neutral-800',
113
+ className
114
+ );
115
+ case 'outline':
116
+ default:
117
+ return cn(
118
+ base,
119
+ 'border border-neutral-200 dark:border-neutral-800 hover:border-neutral-300 dark:hover:border-neutral-700',
120
+ className
121
+ );
122
+ }
123
+ });
124
+
125
+ let uniqueId = $derived(`sparkline-${Math.random().toString(36).slice(2, 8)}`);
126
+ </script>
127
+
128
+ <div class={containerClasses}>
129
+ {#if loading}
130
+ <div class="space-y-3">
131
+ <div class="flex items-center justify-between">
132
+ <Skeleton class="h-4 w-28" />
133
+ <Skeleton class="h-8 w-8 rounded-lg" />
134
+ </div>
135
+ <Skeleton class="h-8 w-36" />
136
+ <div class="flex items-center gap-2 pt-1">
137
+ <Skeleton class="h-4 w-16" />
138
+ <Skeleton class="h-4 w-24" />
139
+ </div>
140
+ </div>
141
+ {:else}
142
+ <div class="flex items-start justify-between gap-2">
143
+ <span class="truncate text-sm font-medium text-neutral-500 dark:text-neutral-400">
144
+ {title}
145
+ </span>
146
+ {#if icon}
147
+ <div
148
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300"
149
+ >
150
+ <Icon name={icon} size="sm" />
151
+ </div>
152
+ {/if}
153
+ </div>
154
+
155
+ <div class="mt-2 flex items-baseline justify-between gap-4">
156
+ <div>
157
+ <div class="text-2xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50">
158
+ {value}
159
+ </div>
160
+
161
+ {#if change !== undefined || changePeriod}
162
+ <div class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs">
163
+ {#if change !== undefined}
164
+ <Badge
165
+ color={isPositive ? 'success' : 'error'}
166
+ variant="soft"
167
+ size="sm"
168
+ class="gap-0.5 font-semibold"
169
+ >
170
+ <Icon
171
+ name={isPositive ? 'arrow-up-right' : 'arrow-down-right'}
172
+ size="xs"
173
+ class="inline-block"
174
+ />
175
+ {changeFormatted}
176
+ </Badge>
177
+ {/if}
178
+ {#if changePeriod}
179
+ <span class="text-neutral-500 dark:text-neutral-400">
180
+ {changePeriod}
181
+ </span>
182
+ {/if}
183
+ </div>
184
+ {/if}
185
+ </div>
186
+
187
+ <!-- Sparkline Chart -->
188
+ {#if sparklineData}
189
+ <div class="flex h-10 w-24 shrink-0 items-center">
190
+ <svg
191
+ viewBox="0 0 120 36"
192
+ class="h-full w-full overflow-visible"
193
+ preserveAspectRatio="none"
194
+ >
195
+ <defs>
196
+ <linearGradient id={uniqueId} x1="0" y1="0" x2="0" y2="1">
197
+ <stop offset="0%" stop-color={colorHex.fill} stop-opacity="0.35" />
198
+ <stop offset="100%" stop-color={colorHex.fill} stop-opacity="0" />
199
+ </linearGradient>
200
+ </defs>
201
+ <path d={sparklineData.areaPath} fill="url(#{uniqueId})" />
202
+ <path
203
+ d={sparklineData.linePath}
204
+ fill="none"
205
+ stroke={colorHex.stroke}
206
+ stroke-width="2.2"
207
+ stroke-linecap="round"
208
+ stroke-linejoin="round"
209
+ />
210
+ <circle
211
+ cx={sparklineData.lastPoint.x}
212
+ cy={sparklineData.lastPoint.y}
213
+ r="3"
214
+ fill={colorHex.stroke}
215
+ class="animate-pulse"
216
+ />
217
+ </svg>
218
+ </div>
219
+ {/if}
220
+ </div>
221
+
222
+ {#if children}
223
+ <div class="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800">
224
+ {@render children()}
225
+ </div>
226
+ {/if}
227
+ {/if}
228
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type IconSource } from './Icon.svelte';
3
+ interface Props {
4
+ title: string;
5
+ value: string | number;
6
+ change?: number;
7
+ changeType?: 'percent' | 'absolute';
8
+ changePeriod?: string;
9
+ icon?: IconSource;
10
+ sparkline?: number[];
11
+ sparklineColor?: 'primary' | 'success' | 'warning' | 'error' | 'neutral' | 'auto';
12
+ variant?: 'outline' | 'flat' | 'elevated';
13
+ loading?: boolean;
14
+ class?: string;
15
+ children?: Snippet;
16
+ }
17
+ declare const MetricCard: import("svelte").Component<Props, {}, "">;
18
+ type MetricCard = ReturnType<typeof MetricCard>;
19
+ export default MetricCard;
@@ -0,0 +1,322 @@
1
+ <script lang="ts">
2
+ import { Popover } from 'bits-ui';
3
+ import Icon from '../elements/Icon.svelte';
4
+ import { cn } from '../../utils/cn';
5
+
6
+ export interface DatePreset {
7
+ label: string;
8
+ getValue: () => Date;
9
+ }
10
+
11
+ interface Props {
12
+ value?: string | Date;
13
+ placeholder?: string;
14
+ min?: string | Date;
15
+ max?: string | Date;
16
+ disabled?: boolean;
17
+ presets?: DatePreset[];
18
+ class?: string;
19
+ onchange?: (date: Date | null, dateString: string) => void;
20
+ }
21
+
22
+ let {
23
+ value = $bindable(),
24
+ placeholder = 'Select date',
25
+ min,
26
+ max,
27
+ disabled = false,
28
+ presets,
29
+ class: className = '',
30
+ onchange
31
+ }: Props = $props();
32
+
33
+ let open = $state(false);
34
+
35
+ // Parse input date
36
+ function toDate(d: string | Date | undefined): Date | null {
37
+ if (!d) return null;
38
+ if (d instanceof Date) return isNaN(d.getTime()) ? null : d;
39
+ const parsed = new Date(d);
40
+ return isNaN(parsed.getTime()) ? null : parsed;
41
+ }
42
+
43
+ function formatDateIso(d: Date): string {
44
+ const year = d.getFullYear();
45
+ const month = String(d.getMonth() + 1).padStart(2, '0');
46
+ const day = String(d.getDate()).padStart(2, '0');
47
+ return `${year}-${month}-${day}`;
48
+ }
49
+
50
+ function formatDisplay(d: Date | null): string {
51
+ if (!d) return '';
52
+ return d.toLocaleDateString('en-US', {
53
+ month: 'short',
54
+ day: 'numeric',
55
+ year: 'numeric'
56
+ });
57
+ }
58
+
59
+ let selectedDate = $derived(toDate(value));
60
+ let viewDate = $state(new Date());
61
+
62
+ $effect(() => {
63
+ if (selectedDate) {
64
+ viewDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1);
65
+ }
66
+ });
67
+
68
+ let viewYear = $derived(viewDate.getFullYear());
69
+ let viewMonth = $derived(viewDate.getMonth());
70
+
71
+ const MONTH_NAMES = [
72
+ 'January',
73
+ 'February',
74
+ 'March',
75
+ 'April',
76
+ 'May',
77
+ 'June',
78
+ 'July',
79
+ 'August',
80
+ 'September',
81
+ 'October',
82
+ 'November',
83
+ 'December'
84
+ ];
85
+
86
+ const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
87
+
88
+ const defaultPresets: DatePreset[] = [
89
+ { label: 'Today', getValue: () => new Date() },
90
+ {
91
+ label: 'Tomorrow',
92
+ getValue: () => new Date(Date.now() + 24 * 60 * 60 * 1000)
93
+ },
94
+ {
95
+ label: '+7 Days',
96
+ getValue: () => new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
97
+ }
98
+ ];
99
+
100
+ let activePresets = $derived(presets || defaultPresets);
101
+
102
+ let calendarDays = $derived.by(() => {
103
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
104
+ const firstDayIndex = new Date(viewYear, viewMonth, 1).getDay();
105
+ const prevMonthDays = new Date(viewYear, viewMonth, 0).getDate();
106
+
107
+ const cells: Array<{
108
+ day: number;
109
+ monthOffset: number; // -1: prev, 0: current, 1: next
110
+ date: Date;
111
+ isSelected: boolean;
112
+ isToday: boolean;
113
+ isDisabled: boolean;
114
+ }> = [];
115
+
116
+ const minDate = toDate(min);
117
+ const maxDate = toDate(max);
118
+ const today = new Date();
119
+
120
+ // Prev month padding
121
+ for (let i = firstDayIndex - 1; i >= 0; i--) {
122
+ const day = prevMonthDays - i;
123
+ const d = new Date(viewYear, viewMonth - 1, day);
124
+ cells.push({
125
+ day,
126
+ monthOffset: -1,
127
+ date: d,
128
+ isSelected: Boolean(selectedDate && formatDateIso(d) === formatDateIso(selectedDate)),
129
+ isToday: formatDateIso(d) === formatDateIso(today),
130
+ isDisabled: Boolean((minDate && d < minDate) || (maxDate && d > maxDate))
131
+ });
132
+ }
133
+
134
+ // Current month
135
+ for (let day = 1; day <= daysInMonth; day++) {
136
+ const d = new Date(viewYear, viewMonth, day);
137
+ cells.push({
138
+ day,
139
+ monthOffset: 0,
140
+ date: d,
141
+ isSelected: Boolean(selectedDate && formatDateIso(d) === formatDateIso(selectedDate)),
142
+ isToday: formatDateIso(d) === formatDateIso(today),
143
+ isDisabled: Boolean((minDate && d < minDate) || (maxDate && d > maxDate))
144
+ });
145
+ }
146
+
147
+ // Next month padding to fill complete weeks
148
+ const remaining = (7 - (cells.length % 7)) % 7;
149
+ for (let day = 1; day <= remaining; day++) {
150
+ const d = new Date(viewYear, viewMonth + 1, day);
151
+ cells.push({
152
+ day,
153
+ monthOffset: 1,
154
+ date: d,
155
+ isSelected: Boolean(selectedDate && formatDateIso(d) === formatDateIso(selectedDate)),
156
+ isToday: formatDateIso(d) === formatDateIso(today),
157
+ isDisabled: Boolean((minDate && d < minDate) || (maxDate && d > maxDate))
158
+ });
159
+ }
160
+
161
+ return cells;
162
+ });
163
+
164
+ function prevMonth() {
165
+ viewDate = new Date(viewYear, viewMonth - 1, 1);
166
+ }
167
+
168
+ function nextMonth() {
169
+ viewDate = new Date(viewYear, viewMonth + 1, 1);
170
+ }
171
+
172
+ function selectDay(date: Date) {
173
+ const iso = formatDateIso(date);
174
+ if (value instanceof Date) {
175
+ value = date;
176
+ } else {
177
+ value = iso;
178
+ }
179
+ onchange?.(date, iso);
180
+ open = false;
181
+ }
182
+
183
+ function applyPreset(preset: DatePreset) {
184
+ const date = preset.getValue();
185
+ viewDate = new Date(date.getFullYear(), date.getMonth(), 1);
186
+ selectDay(date);
187
+ }
188
+
189
+ function clearDate() {
190
+ value = undefined;
191
+ onchange?.(null, '');
192
+ open = false;
193
+ }
194
+ </script>
195
+
196
+ <Popover.Root bind:open>
197
+ <Popover.Trigger
198
+ {disabled}
199
+ class={cn(
200
+ 'inline-flex items-center justify-between gap-2.5 rounded-lg border px-3.5 py-2 text-sm font-medium transition-all duration-150 outline-none',
201
+ 'border-neutral-300 bg-white text-neutral-900 hover:border-neutral-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20',
202
+ 'dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:border-neutral-600 dark:focus:border-primary-400 dark:focus:ring-primary-400/20',
203
+ disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
204
+ className
205
+ )}
206
+ >
207
+ <div class="inline-flex items-center gap-2 truncate">
208
+ <Icon name="calendar" size="sm" class="shrink-0 text-neutral-500 dark:text-neutral-400" />
209
+ <span
210
+ class={selectedDate
211
+ ? 'font-semibold text-neutral-900 dark:text-neutral-100'
212
+ : 'text-neutral-500 dark:text-neutral-400'}
213
+ >
214
+ {selectedDate ? formatDisplay(selectedDate) : placeholder}
215
+ </span>
216
+ </div>
217
+ <Icon name="chevron-down" size="xs" class="shrink-0 text-neutral-400 dark:text-neutral-500" />
218
+ </Popover.Trigger>
219
+
220
+ <Popover.Portal>
221
+ <Popover.Content
222
+ class="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 z-50 w-72 rounded-xl border border-neutral-200 bg-white p-3 shadow-xl duration-150 outline-none dark:border-neutral-800 dark:bg-neutral-900"
223
+ sideOffset={6}
224
+ >
225
+ <!-- Presets Bar -->
226
+ {#if activePresets.length > 0}
227
+ <div
228
+ class="mb-3 flex flex-wrap gap-1 border-b border-neutral-100 pb-2.5 dark:border-neutral-800"
229
+ >
230
+ {#each activePresets as preset}
231
+ <button
232
+ type="button"
233
+ onclick={() => applyPreset(preset)}
234
+ class="rounded-md bg-neutral-100 px-2 py-1 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
235
+ >
236
+ {preset.label}
237
+ </button>
238
+ {/each}
239
+ </div>
240
+ {/if}
241
+
242
+ <!-- Month / Year Navigation -->
243
+ <div class="flex items-center justify-between px-1 py-1">
244
+ <button
245
+ type="button"
246
+ onclick={prevMonth}
247
+ class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
248
+ aria-label="Previous month"
249
+ >
250
+ <Icon name="chevron-left" size="sm" />
251
+ </button>
252
+ <span class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
253
+ {MONTH_NAMES[viewMonth]}
254
+ {viewYear}
255
+ </span>
256
+ <button
257
+ type="button"
258
+ onclick={nextMonth}
259
+ class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
260
+ aria-label="Next month"
261
+ >
262
+ <Icon name="chevron-right" size="sm" />
263
+ </button>
264
+ </div>
265
+
266
+ <!-- Weekday Headers -->
267
+ <div class="mt-2 grid grid-cols-7 gap-1 text-center">
268
+ {#each WEEKDAYS as day}
269
+ <span class="py-0.5 text-xs font-medium text-neutral-400 dark:text-neutral-500">
270
+ {day}
271
+ </span>
272
+ {/each}
273
+ </div>
274
+
275
+ <!-- Calendar Matrix -->
276
+ <div class="mt-1 grid grid-cols-7 gap-1">
277
+ {#each calendarDays as cell}
278
+ <button
279
+ type="button"
280
+ disabled={cell.isDisabled}
281
+ onclick={() => selectDay(cell.date)}
282
+ class={cn(
283
+ 'inline-flex h-8 w-8 items-center justify-center rounded-lg text-xs font-medium transition-all duration-100',
284
+ cell.monthOffset !== 0
285
+ ? 'text-neutral-400 dark:text-neutral-600'
286
+ : 'text-neutral-900 dark:text-neutral-100',
287
+ cell.isToday && !cell.isSelected
288
+ ? 'border border-primary-500 font-bold text-primary-600 dark:text-primary-400'
289
+ : '',
290
+ cell.isSelected
291
+ ? 'bg-primary-600 font-bold text-white shadow-xs hover:bg-primary-500 dark:bg-primary-500 dark:hover:bg-primary-400'
292
+ : 'hover:bg-neutral-100 dark:hover:bg-neutral-800',
293
+ cell.isDisabled
294
+ ? 'cursor-not-allowed opacity-30 hover:bg-transparent'
295
+ : 'cursor-pointer'
296
+ )}
297
+ >
298
+ {cell.day}
299
+ </button>
300
+ {/each}
301
+ </div>
302
+
303
+ <!-- Footer -->
304
+ {#if selectedDate}
305
+ <div
306
+ class="mt-3 flex items-center justify-between border-t border-neutral-100 pt-2 dark:border-neutral-800"
307
+ >
308
+ <button
309
+ type="button"
310
+ onclick={clearDate}
311
+ class="text-xs font-medium text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
312
+ >
313
+ Clear
314
+ </button>
315
+ <span class="text-xs text-neutral-500 dark:text-neutral-400">
316
+ {formatDisplay(selectedDate)}
317
+ </span>
318
+ </div>
319
+ {/if}
320
+ </Popover.Content>
321
+ </Popover.Portal>
322
+ </Popover.Root>
@@ -0,0 +1,17 @@
1
+ export interface DatePreset {
2
+ label: string;
3
+ getValue: () => Date;
4
+ }
5
+ interface Props {
6
+ value?: string | Date;
7
+ placeholder?: string;
8
+ min?: string | Date;
9
+ max?: string | Date;
10
+ disabled?: boolean;
11
+ presets?: DatePreset[];
12
+ class?: string;
13
+ onchange?: (date: Date | null, dateString: string) => void;
14
+ }
15
+ declare const DatePicker: import("svelte").Component<Props, {}, "value">;
16
+ type DatePicker = ReturnType<typeof DatePicker>;
17
+ export default DatePicker;