@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.
- package/README.md +166 -0
- package/package.json +67 -0
- package/src/components/actions/Button.tsx +612 -0
- package/src/components/actions/ButtonGroup.tsx +728 -0
- package/src/components/actions/Copy.tsx +98 -0
- package/src/components/actions/DarkModeToggle.tsx +80 -0
- package/src/components/actions/Link.tsx +37 -0
- package/src/components/actions/index.ts +19 -0
- package/src/components/actions/useCopyToClipboard.ts +90 -0
- package/src/components/charts/Chart.tsx +331 -0
- package/src/components/charts/Sparkline.tsx +156 -0
- package/src/components/charts/index.ts +13 -0
- package/src/components/data-display/Avatar.tsx +208 -0
- package/src/components/data-display/AvatarGroup.tsx +228 -0
- package/src/components/data-display/Badge.tsx +70 -0
- package/src/components/data-display/Carousel.tsx +214 -0
- package/src/components/data-display/ColorSwatch.tsx +56 -0
- package/src/components/data-display/DataTable.tsx +886 -0
- package/src/components/data-display/EmptyState.tsx +61 -0
- package/src/components/data-display/Image.tsx +277 -0
- package/src/components/data-display/Kbd.tsx +114 -0
- package/src/components/data-display/Persona.tsx +78 -0
- package/src/components/data-display/StatCard.tsx +338 -0
- package/src/components/data-display/Table.tsx +147 -0
- package/src/components/data-display/Tag.tsx +91 -0
- package/src/components/data-display/Timeline.tsx +200 -0
- package/src/components/data-display/TreeView.tsx +172 -0
- package/src/components/data-display/Video.tsx +95 -0
- package/src/components/data-display/avatar-utils.ts +32 -0
- package/src/components/data-display/index.ts +81 -0
- package/src/components/feedback/Loading.tsx +159 -0
- package/src/components/feedback/Progress.tsx +321 -0
- package/src/components/feedback/Skeleton.tsx +62 -0
- package/src/components/feedback/SkeletonBlocks.tsx +222 -0
- package/src/components/feedback/Toast.tsx +648 -0
- package/src/components/feedback/index.ts +44 -0
- package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
- package/src/components/feedback/password/password-strength.ts +115 -0
- package/src/components/feedback/password/password-validation-data.ts +66 -0
- package/src/components/feedback/password/password-validation.ts +93 -0
- package/src/components/forms/Autocomplete.tsx +268 -0
- package/src/components/forms/Checkbox.tsx +155 -0
- package/src/components/forms/CodeInput.tsx +237 -0
- package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
- package/src/components/forms/ColorPicker/color-utils.ts +75 -0
- package/src/components/forms/ColorPicker/index.ts +2 -0
- package/src/components/forms/DatePicker.tsx +516 -0
- package/src/components/forms/DateRangePicker.tsx +464 -0
- package/src/components/forms/FieldPicker.tsx +64 -0
- package/src/components/forms/FileUpload.tsx +614 -0
- package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
- package/src/components/forms/FilterBuilder.tsx +16 -0
- package/src/components/forms/FilterRuleRow.tsx +68 -0
- package/src/components/forms/Input.tsx +200 -0
- package/src/components/forms/MultiSelect.tsx +361 -0
- package/src/components/forms/NumberField.tsx +145 -0
- package/src/components/forms/RadioGroup.tsx +135 -0
- package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
- package/src/components/forms/ReorderableList.tsx +163 -0
- package/src/components/forms/Select.tsx +268 -0
- package/src/components/forms/Slider.tsx +260 -0
- package/src/components/forms/Switch.tsx +135 -0
- package/src/components/forms/TextArea.tsx +202 -0
- package/src/components/forms/ViewCustomizer.tsx +44 -0
- package/src/components/forms/index.ts +43 -0
- package/src/components/layout/Accordion.tsx +110 -0
- package/src/components/layout/Alert.tsx +156 -0
- package/src/components/layout/BlockQuote.tsx +70 -0
- package/src/components/layout/Card.tsx +166 -0
- package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
- package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
- package/src/components/layout/CodeBlock/prism.ts +81 -0
- package/src/components/layout/Collapsible.tsx +84 -0
- package/src/components/layout/Container.tsx +55 -0
- package/src/components/layout/Divider.tsx +64 -0
- package/src/components/layout/Form.tsx +39 -0
- package/src/components/layout/FormActions.tsx +50 -0
- package/src/components/layout/Grid.tsx +53 -0
- package/src/components/layout/PageHeading.tsx +46 -0
- package/src/components/layout/PromptWithAction.tsx +49 -0
- package/src/components/layout/Section.tsx +60 -0
- package/src/components/layout/TablePanel.tsx +24 -0
- package/src/components/layout/TableView/TableView.tsx +1018 -0
- package/src/components/layout/TableView/index.ts +3 -0
- package/src/components/layout/TableView/types.ts +51 -0
- package/src/components/layout/WizardStep.tsx +40 -0
- package/src/components/layout/WizardStepper.tsx +173 -0
- package/src/components/layout/index.ts +96 -0
- package/src/components/navigation/Breadcrumbs.tsx +66 -0
- package/src/components/navigation/DropdownMenu.tsx +86 -0
- package/src/components/navigation/MegaMenu.tsx +480 -0
- package/src/components/navigation/NavigationMenu.tsx +305 -0
- package/src/components/navigation/Pagination.tsx +298 -0
- package/src/components/navigation/Sidebar.tsx +280 -0
- package/src/components/navigation/Tabs.tsx +122 -0
- package/src/components/navigation/ViewSwitcher.tsx +314 -0
- package/src/components/navigation/index.ts +66 -0
- package/src/components/overlays/AlertDialog.tsx +174 -0
- package/src/components/overlays/ContextMenu.tsx +65 -0
- package/src/components/overlays/Dialog.tsx +279 -0
- package/src/components/overlays/Drawer.tsx +370 -0
- package/src/components/overlays/HoverCard.tsx +107 -0
- package/src/components/overlays/Popover.tsx +73 -0
- package/src/components/overlays/Tooltip.tsx +31 -0
- package/src/components/overlays/index.ts +71 -0
- package/src/components/typography/Code.tsx +72 -0
- package/src/components/typography/Icon.tsx +36 -0
- package/src/components/typography/index.ts +10 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +13 -0
- package/src/styles/theme.css +226 -0
- package/src/types/avatar-types.ts +11 -0
- package/src/types/filter-types.ts +35 -0
- package/src/utilities/classNames.ts +6 -0
- package/src/utilities/componentSize.ts +46 -0
- package/src/utilities/i18n.tsx +60 -0
- package/src/utilities/mergeRefs.ts +12 -0
- 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
|
+
}
|