@torch-ui/solid 0.1.3

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 (118) hide show
  1. package/README.md +166 -0
  2. package/package.json +67 -0
  3. package/src/components/actions/Button.tsx +612 -0
  4. package/src/components/actions/ButtonGroup.tsx +728 -0
  5. package/src/components/actions/Copy.tsx +98 -0
  6. package/src/components/actions/DarkModeToggle.tsx +80 -0
  7. package/src/components/actions/Link.tsx +37 -0
  8. package/src/components/actions/index.ts +19 -0
  9. package/src/components/actions/useCopyToClipboard.ts +90 -0
  10. package/src/components/charts/Chart.tsx +331 -0
  11. package/src/components/charts/Sparkline.tsx +156 -0
  12. package/src/components/charts/index.ts +13 -0
  13. package/src/components/data-display/Avatar.tsx +208 -0
  14. package/src/components/data-display/AvatarGroup.tsx +228 -0
  15. package/src/components/data-display/Badge.tsx +70 -0
  16. package/src/components/data-display/Carousel.tsx +214 -0
  17. package/src/components/data-display/ColorSwatch.tsx +56 -0
  18. package/src/components/data-display/DataTable.tsx +886 -0
  19. package/src/components/data-display/EmptyState.tsx +61 -0
  20. package/src/components/data-display/Image.tsx +277 -0
  21. package/src/components/data-display/Kbd.tsx +114 -0
  22. package/src/components/data-display/Persona.tsx +78 -0
  23. package/src/components/data-display/StatCard.tsx +338 -0
  24. package/src/components/data-display/Table.tsx +147 -0
  25. package/src/components/data-display/Tag.tsx +91 -0
  26. package/src/components/data-display/Timeline.tsx +200 -0
  27. package/src/components/data-display/TreeView.tsx +172 -0
  28. package/src/components/data-display/Video.tsx +95 -0
  29. package/src/components/data-display/avatar-utils.ts +32 -0
  30. package/src/components/data-display/index.ts +81 -0
  31. package/src/components/feedback/Loading.tsx +159 -0
  32. package/src/components/feedback/Progress.tsx +321 -0
  33. package/src/components/feedback/Skeleton.tsx +62 -0
  34. package/src/components/feedback/SkeletonBlocks.tsx +222 -0
  35. package/src/components/feedback/Toast.tsx +648 -0
  36. package/src/components/feedback/index.ts +44 -0
  37. package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
  38. package/src/components/feedback/password/password-strength.ts +115 -0
  39. package/src/components/feedback/password/password-validation-data.ts +66 -0
  40. package/src/components/feedback/password/password-validation.ts +93 -0
  41. package/src/components/forms/Autocomplete.tsx +268 -0
  42. package/src/components/forms/Checkbox.tsx +155 -0
  43. package/src/components/forms/CodeInput.tsx +237 -0
  44. package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
  45. package/src/components/forms/ColorPicker/color-utils.ts +75 -0
  46. package/src/components/forms/ColorPicker/index.ts +2 -0
  47. package/src/components/forms/DatePicker.tsx +516 -0
  48. package/src/components/forms/DateRangePicker.tsx +464 -0
  49. package/src/components/forms/FieldPicker.tsx +64 -0
  50. package/src/components/forms/FileUpload.tsx +614 -0
  51. package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
  52. package/src/components/forms/FilterBuilder.tsx +16 -0
  53. package/src/components/forms/FilterRuleRow.tsx +68 -0
  54. package/src/components/forms/Input.tsx +200 -0
  55. package/src/components/forms/MultiSelect.tsx +361 -0
  56. package/src/components/forms/NumberField.tsx +145 -0
  57. package/src/components/forms/RadioGroup.tsx +135 -0
  58. package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
  59. package/src/components/forms/ReorderableList.tsx +163 -0
  60. package/src/components/forms/Select.tsx +268 -0
  61. package/src/components/forms/Slider.tsx +260 -0
  62. package/src/components/forms/Switch.tsx +135 -0
  63. package/src/components/forms/TextArea.tsx +202 -0
  64. package/src/components/forms/ViewCustomizer.tsx +44 -0
  65. package/src/components/forms/index.ts +43 -0
  66. package/src/components/layout/Accordion.tsx +110 -0
  67. package/src/components/layout/Alert.tsx +156 -0
  68. package/src/components/layout/BlockQuote.tsx +70 -0
  69. package/src/components/layout/Card.tsx +166 -0
  70. package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
  71. package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
  72. package/src/components/layout/CodeBlock/prism.ts +81 -0
  73. package/src/components/layout/Collapsible.tsx +84 -0
  74. package/src/components/layout/Container.tsx +55 -0
  75. package/src/components/layout/Divider.tsx +64 -0
  76. package/src/components/layout/Form.tsx +39 -0
  77. package/src/components/layout/FormActions.tsx +50 -0
  78. package/src/components/layout/Grid.tsx +53 -0
  79. package/src/components/layout/PageHeading.tsx +46 -0
  80. package/src/components/layout/PromptWithAction.tsx +49 -0
  81. package/src/components/layout/Section.tsx +60 -0
  82. package/src/components/layout/TablePanel.tsx +24 -0
  83. package/src/components/layout/TableView/TableView.tsx +1018 -0
  84. package/src/components/layout/TableView/index.ts +3 -0
  85. package/src/components/layout/TableView/types.ts +51 -0
  86. package/src/components/layout/WizardStep.tsx +40 -0
  87. package/src/components/layout/WizardStepper.tsx +173 -0
  88. package/src/components/layout/index.ts +96 -0
  89. package/src/components/navigation/Breadcrumbs.tsx +66 -0
  90. package/src/components/navigation/DropdownMenu.tsx +86 -0
  91. package/src/components/navigation/MegaMenu.tsx +480 -0
  92. package/src/components/navigation/NavigationMenu.tsx +305 -0
  93. package/src/components/navigation/Pagination.tsx +298 -0
  94. package/src/components/navigation/Sidebar.tsx +280 -0
  95. package/src/components/navigation/Tabs.tsx +122 -0
  96. package/src/components/navigation/ViewSwitcher.tsx +314 -0
  97. package/src/components/navigation/index.ts +66 -0
  98. package/src/components/overlays/AlertDialog.tsx +174 -0
  99. package/src/components/overlays/ContextMenu.tsx +65 -0
  100. package/src/components/overlays/Dialog.tsx +279 -0
  101. package/src/components/overlays/Drawer.tsx +370 -0
  102. package/src/components/overlays/HoverCard.tsx +107 -0
  103. package/src/components/overlays/Popover.tsx +73 -0
  104. package/src/components/overlays/Tooltip.tsx +31 -0
  105. package/src/components/overlays/index.ts +71 -0
  106. package/src/components/typography/Code.tsx +72 -0
  107. package/src/components/typography/Icon.tsx +36 -0
  108. package/src/components/typography/index.ts +10 -0
  109. package/src/env.d.ts +9 -0
  110. package/src/index.ts +13 -0
  111. package/src/styles/theme.css +226 -0
  112. package/src/types/avatar-types.ts +11 -0
  113. package/src/types/filter-types.ts +35 -0
  114. package/src/utilities/classNames.ts +6 -0
  115. package/src/utilities/componentSize.ts +46 -0
  116. package/src/utilities/i18n.tsx +60 -0
  117. package/src/utilities/mergeRefs.ts +12 -0
  118. package/src/utilities/relativeDateDefault.ts +14 -0
@@ -0,0 +1,516 @@
1
+ import {
2
+ createSignal,
3
+ createMemo,
4
+ Show,
5
+ For,
6
+ splitProps,
7
+ createUniqueId,
8
+ } from 'solid-js'
9
+ import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X } from 'lucide-solid'
10
+ import { Popover as KobaltePopover } from '@kobalte/core/popover'
11
+ import { cn } from '../../utilities/classNames'
12
+
13
+ /** Value is ISO date string YYYY-MM-DD or empty string. */
14
+ export interface DatePickerProps {
15
+ value?: string
16
+ onValueChange?: (value: string) => void
17
+ placeholder?: string
18
+ disabled?: boolean
19
+ /** Min date YYYY-MM-DD */
20
+ min?: string
21
+ /** Max date YYYY-MM-DD */
22
+ max?: string
23
+ label?: string
24
+ error?: string
25
+ helperText?: string
26
+ bare?: boolean
27
+ required?: boolean
28
+ /** When true, show "optional" on the label row when not required. Default false. */
29
+ optional?: boolean
30
+ class?: string
31
+ id?: string
32
+ }
33
+
34
+ const DAY_NAMES = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
35
+ const MONTH_NAMES = [
36
+ 'January', 'February', 'March', 'April', 'May', 'June',
37
+ 'July', 'August', 'September', 'October', 'November', 'December',
38
+ ]
39
+
40
+ function parseDate(s: string): Date | null {
41
+ if (!s || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return null
42
+ const d = new Date(s + 'T12:00:00')
43
+ return isNaN(d.getTime()) ? null : d
44
+ }
45
+
46
+ function toISODate(d: Date): string {
47
+ const y = d.getFullYear()
48
+ const m = String(d.getMonth() + 1).padStart(2, '0')
49
+ const day = String(d.getDate()).padStart(2, '0')
50
+ return `${y}-${m}-${day}`
51
+ }
52
+
53
+ function formatDisplay(s: string): string {
54
+ const d = parseDate(s)
55
+ if (!d) return ''
56
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
57
+ }
58
+
59
+ function getCalendarDays(year: number, month: number): Date[][] {
60
+ const first = new Date(year, month, 1)
61
+ const last = new Date(year, month + 1, 0)
62
+ const startPad = first.getDay()
63
+ const flat: Date[] = []
64
+ for (let i = 0; i < startPad; i++) flat.push(new Date(year, month, 1 - (startPad - i)))
65
+ for (let d = 1; d <= last.getDate(); d++) flat.push(new Date(year, month, d))
66
+ for (let n = 1; flat.length < 42; n++) flat.push(new Date(year, month + 1, n))
67
+ const grid: Date[][] = []
68
+ for (let r = 0; r < 6; r++) grid.push(flat.slice(r * 7, (r + 1) * 7))
69
+ return grid
70
+ }
71
+
72
+ export function DatePicker(props: DatePickerProps) {
73
+ const [local] = splitProps(props, [
74
+ 'value', 'onValueChange', 'placeholder', 'disabled',
75
+ 'min', 'max', 'label', 'error', 'helperText', 'bare',
76
+ 'required', 'optional', 'class', 'id',
77
+ ])
78
+
79
+ const generatedId = createUniqueId()
80
+ const inputId = () => local.id || `datepicker-${generatedId}`
81
+ const [open, setOpen] = createSignal(false)
82
+
83
+ const valueDate = () => parseDate(local.value ?? '')
84
+ const viewDate = () => valueDate() ?? new Date()
85
+
86
+ const [viewMonthYear, setViewMonthYear] = createSignal<{ year: number; month: number } | null>(null)
87
+ const effectiveViewYear = () => viewMonthYear()?.year ?? viewDate().getFullYear()
88
+ const effectiveViewMonth = () => viewMonthYear()?.month ?? viewDate().getMonth()
89
+ const calendarDays = createMemo(() => getCalendarDays(effectiveViewYear(), effectiveViewMonth()))
90
+
91
+ type ViewMode = 'calendar' | 'months' | 'years'
92
+ const [viewMode, setViewMode] = createSignal<ViewMode>('calendar')
93
+
94
+ const minDate = () => (local.min ? parseDate(local.min) : null)
95
+ const maxDate = () => (local.max ? parseDate(local.max) : null)
96
+
97
+ const yearsList = () => {
98
+ const current = effectiveViewYear()
99
+ const minY = minDate()?.getFullYear() ?? current - 20
100
+ const maxY = maxDate()?.getFullYear() ?? current + 5
101
+ return Array.from({ length: maxY - minY + 1 }, (_, i) => minY + i)
102
+ }
103
+
104
+ function isDisabled(d: Date): boolean {
105
+ const t = d.getTime()
106
+ if (minDate() && t < minDate()!.getTime()) return true
107
+ if (maxDate() && t > maxDate()!.getTime()) return true
108
+ return false
109
+ }
110
+
111
+ function isCurrentMonth(d: Date): boolean {
112
+ return d.getMonth() === effectiveViewMonth() && d.getFullYear() === effectiveViewYear()
113
+ }
114
+
115
+ function isSelected(d: Date): boolean {
116
+ return !!local.value && toISODate(d) === local.value
117
+ }
118
+
119
+ function selectDate(d: Date) {
120
+ if (isDisabled(d)) return
121
+ local.onValueChange?.(toISODate(d))
122
+ setOpen(false)
123
+ }
124
+
125
+ const todayISO = () => toISODate(new Date())
126
+ const todayDisabled = () => isDisabled(new Date())
127
+
128
+ function selectToday() {
129
+ if (todayDisabled()) return
130
+ local.onValueChange?.(todayISO())
131
+ setOpen(false)
132
+ }
133
+
134
+ function goPrevMonth() {
135
+ const d = new Date(effectiveViewYear(), effectiveViewMonth() - 1, 1)
136
+ setViewMonthYear({ year: d.getFullYear(), month: d.getMonth() })
137
+ }
138
+ function goNextMonth() {
139
+ const d = new Date(effectiveViewYear(), effectiveViewMonth() + 1, 1)
140
+ setViewMonthYear({ year: d.getFullYear(), month: d.getMonth() })
141
+ }
142
+ function goPrevYear() {
143
+ setViewMonthYear({ year: effectiveViewYear() - 1, month: effectiveViewMonth() })
144
+ }
145
+ function goNextYear() {
146
+ setViewMonthYear({ year: effectiveViewYear() + 1, month: effectiveViewMonth() })
147
+ }
148
+ function setMonth(m: number) {
149
+ setViewMonthYear({ year: effectiveViewYear(), month: m })
150
+ setViewMode('calendar')
151
+ }
152
+ function setYear(y: number) {
153
+ setViewMonthYear({ year: y, month: effectiveViewMonth() })
154
+ setViewMode('calendar')
155
+ }
156
+
157
+ const canGoPrevMonth = () => {
158
+ const mn = minDate()
159
+ if (!mn) return true
160
+ return new Date(effectiveViewYear(), effectiveViewMonth(), 0).getTime() >= mn.getTime()
161
+ }
162
+ const canGoNextMonth = () => {
163
+ const mx = maxDate()
164
+ if (!mx) return true
165
+ return new Date(effectiveViewYear(), effectiveViewMonth() + 1, 1).getTime() <= mx.getTime()
166
+ }
167
+ const canGoPrevYear = () => {
168
+ const mn = minDate()
169
+ if (!mn) return true
170
+ return effectiveViewYear() > mn.getFullYear()
171
+ }
172
+ const canGoNextYear = () => {
173
+ const mx = maxDate()
174
+ if (!mx) return true
175
+ return effectiveViewYear() < mx.getFullYear()
176
+ }
177
+
178
+ const isMonthDisabled = (m: number) => {
179
+ const y = effectiveViewYear()
180
+ const mn = minDate()
181
+ const mx = maxDate()
182
+ if (mn && new Date(y, m + 1, 0).getTime() < mn.getTime()) return true
183
+ if (mx && new Date(y, m, 1).getTime() > mx.getTime()) return true
184
+ return false
185
+ }
186
+
187
+ const displayValue = () => (local.value ? formatDisplay(local.value) : '')
188
+ const hasError = () => !!local.error
189
+ const msgId = () => (local.error || local.helperText) ? `${inputId()}-msg` : undefined
190
+
191
+ return (
192
+ <div class={cn('w-full', local.class)}>
193
+ <Show when={!local.bare && local.label}>
194
+ <div class="mb-2 flex items-center justify-between">
195
+ <label
196
+ for={inputId()}
197
+ class={cn(
198
+ 'block text-sm font-medium',
199
+ hasError() ? 'text-danger-600 dark:text-danger-400' : 'text-ink-700',
200
+ )}
201
+ >
202
+ {local.label}
203
+ </label>
204
+ <Show when={!local.required && local.optional}>
205
+ <span class="text-xs text-ink-400">optional</span>
206
+ </Show>
207
+ </div>
208
+ </Show>
209
+
210
+ <KobaltePopover
211
+ open={open()}
212
+ onOpenChange={(next) => {
213
+ setOpen(next)
214
+ if (next) {
215
+ const d = valueDate() ?? new Date()
216
+ setViewMonthYear({ year: d.getFullYear(), month: d.getMonth() })
217
+ }
218
+ setViewMode('calendar')
219
+ }}
220
+ gutter={8}
221
+ >
222
+ <div class="relative">
223
+ <KobaltePopover.Trigger
224
+ as="button"
225
+ type="button"
226
+ id={inputId()}
227
+ disabled={local.disabled}
228
+ aria-describedby={msgId()}
229
+ aria-invalid={hasError() ? true : undefined}
230
+ class={cn(
231
+ 'inline-flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors',
232
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
233
+ hasError()
234
+ ? 'border-danger-500 bg-surface-raised text-ink-900 hover:border-danger-600'
235
+ : 'border-surface-border bg-surface-raised text-ink-900 hover:border-ink-400 dark:hover:border-ink-500',
236
+ local.disabled && 'cursor-not-allowed opacity-50',
237
+ displayValue() && !local.disabled && 'pr-8',
238
+ )}
239
+ >
240
+ <CalendarIcon class="h-4 w-4 shrink-0 text-ink-400" aria-hidden="true" />
241
+ <span class={cn('truncate', displayValue() ? 'text-ink-900' : 'text-ink-400')}>
242
+ {displayValue() || (local.placeholder ?? 'Select date')}
243
+ </span>
244
+ </KobaltePopover.Trigger>
245
+ <Show when={displayValue() && !local.disabled}>
246
+ <button
247
+ type="button"
248
+ onClick={(e) => { e.stopPropagation(); local.onValueChange?.('') }}
249
+ class="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-ink-400 hover:text-ink-700 transition-colors"
250
+ aria-label="Clear date"
251
+ >
252
+ <X class="h-3.5 w-3.5" />
253
+ </button>
254
+ </Show>
255
+ </div>
256
+
257
+ <KobaltePopover.Portal>
258
+ <KobaltePopover.Content
259
+ role="dialog"
260
+ aria-label="Choose date"
261
+ class={cn(
262
+ 'z-50 rounded-xl border border-surface-border bg-surface-raised shadow-xl outline-none',
263
+ 'origin-top data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95',
264
+ 'data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95',
265
+ )}
266
+ >
267
+ <div class="p-3">
268
+ {/* Header */}
269
+ <div class="flex items-center justify-between gap-2 mb-3">
270
+ {viewMode() !== 'calendar' ? (
271
+ <button
272
+ type="button"
273
+ onClick={() => setViewMode('calendar')}
274
+ class="flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-surface-overlay transition-colors"
275
+ aria-label="Back to calendar"
276
+ >
277
+ <ChevronLeft class="h-4 w-4" />
278
+ </button>
279
+ ) : (
280
+ <button
281
+ type="button"
282
+ onClick={goPrevMonth}
283
+ disabled={!canGoPrevMonth()}
284
+ class={cn(
285
+ 'flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-surface-overlay transition-colors',
286
+ !canGoPrevMonth() && 'opacity-30 pointer-events-none',
287
+ )}
288
+ aria-label="Previous month"
289
+ >
290
+ <ChevronLeft class="h-4 w-4" />
291
+ </button>
292
+ )}
293
+
294
+ <div class="flex min-w-[140px] items-center justify-center gap-1">
295
+ {viewMode() === 'calendar' && (
296
+ <>
297
+ <button
298
+ type="button"
299
+ onClick={() => setViewMode('months')}
300
+ class="rounded-md px-2 py-1 text-sm font-semibold text-ink-900 hover:bg-surface-overlay transition-colors"
301
+ >
302
+ {MONTH_NAMES[effectiveViewMonth()]}
303
+ </button>
304
+ <button
305
+ type="button"
306
+ onClick={() => setViewMode('years')}
307
+ class="rounded-md px-2 py-1 text-sm font-semibold text-ink-900 hover:bg-surface-overlay transition-colors"
308
+ >
309
+ {effectiveViewYear()}
310
+ </button>
311
+ </>
312
+ )}
313
+ {viewMode() === 'months' && (
314
+ <>
315
+ <button
316
+ type="button"
317
+ onClick={goPrevYear}
318
+ disabled={!canGoPrevYear()}
319
+ class={cn(
320
+ 'flex h-7 w-7 items-center justify-center rounded-md text-ink-400 hover:bg-surface-overlay transition-colors',
321
+ !canGoPrevYear() && 'opacity-30 pointer-events-none',
322
+ )}
323
+ aria-label="Previous year"
324
+ >
325
+ <ChevronLeft class="h-4 w-4" />
326
+ </button>
327
+ <button
328
+ type="button"
329
+ onClick={() => setViewMode('years')}
330
+ class="rounded-md px-2 py-1 text-sm font-semibold text-ink-900 hover:bg-surface-overlay transition-colors"
331
+ >
332
+ {effectiveViewYear()}
333
+ </button>
334
+ <button
335
+ type="button"
336
+ onClick={goNextYear}
337
+ disabled={!canGoNextYear()}
338
+ class={cn(
339
+ 'flex h-7 w-7 items-center justify-center rounded-md text-ink-400 hover:bg-surface-overlay transition-colors',
340
+ !canGoNextYear() && 'opacity-30 pointer-events-none',
341
+ )}
342
+ aria-label="Next year"
343
+ >
344
+ <ChevronRight class="h-4 w-4" />
345
+ </button>
346
+ </>
347
+ )}
348
+ {viewMode() === 'years' && (
349
+ <span class="text-sm font-semibold text-ink-900">Select year</span>
350
+ )}
351
+ </div>
352
+
353
+ {viewMode() !== 'calendar' ? (
354
+ <div class="w-7" aria-hidden />
355
+ ) : (
356
+ <button
357
+ type="button"
358
+ onClick={goNextMonth}
359
+ disabled={!canGoNextMonth()}
360
+ class={cn(
361
+ 'flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-surface-overlay transition-colors',
362
+ !canGoNextMonth() && 'opacity-30 pointer-events-none',
363
+ )}
364
+ aria-label="Next month"
365
+ >
366
+ <ChevronRight class="h-4 w-4" />
367
+ </button>
368
+ )}
369
+ </div>
370
+
371
+ {/* Calendar grid */}
372
+ <Show when={viewMode() === 'calendar'}>
373
+ <div>
374
+ <div class="grid grid-cols-7 mb-2">
375
+ <For each={DAY_NAMES}>
376
+ {(name) => (
377
+ <div class="py-1 text-center text-xs font-medium text-ink-400">{name}</div>
378
+ )}
379
+ </For>
380
+ </div>
381
+ <For each={calendarDays()}>
382
+ {(week) => (
383
+ <div class="grid grid-cols-7">
384
+ <For each={week}>
385
+ {(d) => {
386
+ const disabled = isDisabled(d)
387
+ const currentMonth = isCurrentMonth(d)
388
+ const selected = isSelected(d)
389
+ const isToday = toISODate(d) === todayISO()
390
+ return (
391
+ <div class="relative h-8 flex items-center justify-center">
392
+ <button
393
+ type="button"
394
+ disabled={disabled}
395
+ onClick={() => selectDate(d)}
396
+ aria-label={d.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
397
+ aria-current={selected ? 'date' : undefined}
398
+ class={cn(
399
+ 'relative z-10 h-7 w-7 rounded-full text-xs transition-colors',
400
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
401
+ selected
402
+ ? 'bg-primary-500 text-white font-semibold hover:bg-primary-600'
403
+ : !currentMonth
404
+ ? 'text-ink-300 dark:text-ink-600 hover:bg-surface-overlay'
405
+ : isToday
406
+ ? 'text-primary-600 font-semibold hover:bg-surface-overlay dark:text-primary-400'
407
+ : 'text-ink-800 dark:text-ink-200 hover:bg-surface-overlay',
408
+ disabled && 'cursor-not-allowed opacity-30',
409
+ )}
410
+ >
411
+ {d.getDate()}
412
+ {isToday && !selected && (
413
+ <span class="absolute bottom-0.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-primary-500" />
414
+ )}
415
+ </button>
416
+ </div>
417
+ )
418
+ }}
419
+ </For>
420
+ </div>
421
+ )}
422
+ </For>
423
+ </div>
424
+ </Show>
425
+
426
+ {/* Month picker */}
427
+ <Show when={viewMode() === 'months'}>
428
+ <div class="grid grid-cols-3 gap-1">
429
+ <For each={MONTH_NAMES}>
430
+ {(name, m) => (
431
+ <button
432
+ type="button"
433
+ onClick={() => setMonth(m())}
434
+ disabled={isMonthDisabled(m())}
435
+ class={cn(
436
+ 'rounded-lg py-2 text-sm font-medium transition-colors',
437
+ isMonthDisabled(m())
438
+ ? 'text-ink-300 opacity-50 cursor-not-allowed'
439
+ : m() === effectiveViewMonth()
440
+ ? 'bg-primary-500 text-white'
441
+ : 'text-ink-700 hover:bg-surface-overlay',
442
+ )}
443
+ >
444
+ {name}
445
+ </button>
446
+ )}
447
+ </For>
448
+ </div>
449
+ </Show>
450
+
451
+ {/* Year picker */}
452
+ <Show when={viewMode() === 'years'}>
453
+ <div class="grid max-h-48 grid-cols-4 gap-1 overflow-y-auto">
454
+ <For each={yearsList()}>
455
+ {(y) => (
456
+ <button
457
+ type="button"
458
+ onClick={() => setYear(y)}
459
+ class={cn(
460
+ 'rounded-lg py-2 text-sm font-medium transition-colors',
461
+ y === effectiveViewYear()
462
+ ? 'bg-primary-500 text-white'
463
+ : 'text-ink-700 hover:bg-surface-overlay',
464
+ )}
465
+ >
466
+ {y}
467
+ </button>
468
+ )}
469
+ </For>
470
+ </div>
471
+ </Show>
472
+
473
+ {/* Footer */}
474
+ <div class="mt-3 flex items-center justify-between border-t border-surface-border pt-3">
475
+ <div class="text-xs text-ink-400">{displayValue() || 'No date selected'}</div>
476
+ <div class="flex gap-2">
477
+ <Show when={displayValue()}>
478
+ <button
479
+ type="button"
480
+ onClick={() => { local.onValueChange?.(''); setOpen(false) }}
481
+ class="rounded-md px-2 py-1 text-xs text-ink-500 hover:bg-surface-overlay hover:text-ink-700 transition-colors"
482
+ >
483
+ Clear
484
+ </button>
485
+ </Show>
486
+ <button
487
+ type="button"
488
+ disabled={todayDisabled()}
489
+ onClick={selectToday}
490
+ class={cn(
491
+ 'rounded-md px-2 py-1 text-xs font-medium transition-colors',
492
+ todayDisabled()
493
+ ? 'cursor-not-allowed text-ink-300'
494
+ : 'text-primary-600 hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-primary-500/10',
495
+ )}
496
+ >
497
+ Today
498
+ </button>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ </KobaltePopover.Content>
503
+ </KobaltePopover.Portal>
504
+ </KobaltePopover>
505
+
506
+ <Show when={local.error || local.helperText}>
507
+ <p
508
+ id={msgId()}
509
+ class={cn('mt-1.5 text-xs', hasError() ? 'text-danger-600 dark:text-danger-400' : 'text-ink-500')}
510
+ >
511
+ {local.error ?? local.helperText}
512
+ </p>
513
+ </Show>
514
+ </div>
515
+ )
516
+ }