@ttt-productions/ui-core 0.11.5 → 0.11.7
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/index.js.map +1 -1
- package/dist/lib/format-numbers.js.map +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/react/components/accordion.js.map +1 -1
- package/dist/react/components/alert-dialog.js.map +1 -1
- package/dist/react/components/alert.js.map +1 -1
- package/dist/react/components/avatar.js.map +1 -1
- package/dist/react/components/badge.js.map +1 -1
- package/dist/react/components/button.js.map +1 -1
- package/dist/react/components/card.js.map +1 -1
- package/dist/react/components/checkbox.js.map +1 -1
- package/dist/react/components/chunk-error-recovery.js.map +1 -1
- package/dist/react/components/collapsible.js.map +1 -1
- package/dist/react/components/consequence-dialog.js.map +1 -1
- package/dist/react/components/date-picker.d.ts +5 -0
- package/dist/react/components/date-picker.d.ts.map +1 -1
- package/dist/react/components/date-picker.js +56 -13
- package/dist/react/components/date-picker.js.map +1 -1
- package/dist/react/components/dialog.js.map +1 -1
- package/dist/react/components/dropdown-menu.js.map +1 -1
- package/dist/react/components/end-of-list-indicator.js.map +1 -1
- package/dist/react/components/form.js.map +1 -1
- package/dist/react/components/input.js.map +1 -1
- package/dist/react/components/label.js.map +1 -1
- package/dist/react/components/layout/screen-adaptive-view.js.map +1 -1
- package/dist/react/components/menubar.js.map +1 -1
- package/dist/react/components/popover.js.map +1 -1
- package/dist/react/components/progress.js.map +1 -1
- package/dist/react/components/radio-group.js.map +1 -1
- package/dist/react/components/relative-time.js.map +1 -1
- package/dist/react/components/scroll-area.js.map +1 -1
- package/dist/react/components/scroll-to-top-button.js.map +1 -1
- package/dist/react/components/search-dropdown.js.map +1 -1
- package/dist/react/components/select.js.map +1 -1
- package/dist/react/components/separator.js.map +1 -1
- package/dist/react/components/sheet.js.map +1 -1
- package/dist/react/components/skeleton.js.map +1 -1
- package/dist/react/components/slider.js.map +1 -1
- package/dist/react/components/switch.js.map +1 -1
- package/dist/react/components/table.js.map +1 -1
- package/dist/react/components/tabs.js.map +1 -1
- package/dist/react/components/textarea.js.map +1 -1
- package/dist/react/components/toast.js.map +1 -1
- package/dist/react/components/toaster.js.map +1 -1
- package/dist/react/components/tooltip.js.map +1 -1
- package/dist/react/hooks/use-media-query.js.map +1 -1
- package/dist/react/hooks/use-toast.js.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +3 -3
- package/src/react/components/date-picker.tsx +215 -83
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { ChevronLeft, ChevronRight
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
5
5
|
import { Button } from "./button.js"
|
|
6
6
|
import { cn } from "../../lib/utils.js"
|
|
7
7
|
|
|
@@ -19,8 +19,20 @@ const MONTHS = [
|
|
|
19
19
|
"July", "August", "September", "October", "November", "December",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
const MONTHS_SHORT = [
|
|
23
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
24
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
25
|
+
]
|
|
26
|
+
|
|
22
27
|
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
|
|
23
28
|
|
|
29
|
+
const YEARS_PER_PAGE = 12
|
|
30
|
+
|
|
31
|
+
/** Calendar with fast month/year navigation: the header's month and year are
|
|
32
|
+
* each buttons — month opens a 12-month grid, year opens a paged 12-year
|
|
33
|
+
* grid (no single-step year arrows; distant years are a couple of clicks).
|
|
34
|
+
* Picking a year drills to the month grid, then the day grid — the natural
|
|
35
|
+
* date-of-birth path — while picking just a month returns straight to days. */
|
|
24
36
|
function DatePicker({
|
|
25
37
|
selected,
|
|
26
38
|
onSelect,
|
|
@@ -30,7 +42,10 @@ function DatePicker({
|
|
|
30
42
|
className,
|
|
31
43
|
}: DatePickerProps) {
|
|
32
44
|
const [viewDate, setViewDate] = React.useState(() => selected || new Date())
|
|
33
|
-
const [
|
|
45
|
+
const [view, setView] = React.useState<"days" | "months" | "years">("days")
|
|
46
|
+
const [yearPageStart, setYearPageStart] = React.useState(
|
|
47
|
+
() => Math.floor((selected || new Date()).getFullYear() / YEARS_PER_PAGE) * YEARS_PER_PAGE,
|
|
48
|
+
)
|
|
34
49
|
|
|
35
50
|
const today = new Date()
|
|
36
51
|
today.setHours(0, 0, 0, 0)
|
|
@@ -58,6 +73,19 @@ function DatePicker({
|
|
|
58
73
|
return false
|
|
59
74
|
}
|
|
60
75
|
|
|
76
|
+
/** A whole month is unpickable only when every day in it is out of range. */
|
|
77
|
+
const isMonthDisabled = (year: number, month: number) => {
|
|
78
|
+
if (disableFuture && new Date(year, month, 1) > today) return true
|
|
79
|
+
if (disablePast && new Date(year, month + 1, 0) < today) return true
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isYearDisabled = (year: number) => {
|
|
84
|
+
if (disableFuture && year > today.getFullYear()) return true
|
|
85
|
+
if (disablePast && year < today.getFullYear()) return true
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
const isDateSelected = (date: Date) => {
|
|
62
90
|
if (!selected) return false
|
|
63
91
|
const selectedDate = new Date(selected)
|
|
@@ -90,114 +118,218 @@ function DatePicker({
|
|
|
90
118
|
const previousMonth = () => setViewDate((prev) => new Date(prev.getFullYear(), prev.getMonth() - 1))
|
|
91
119
|
const nextMonth = () => setViewDate((prev) => new Date(prev.getFullYear(), prev.getMonth() + 1))
|
|
92
120
|
|
|
93
|
-
const previousYear = () => setViewDate((prev) => new Date(prev.getFullYear() - 1, prev.getMonth()))
|
|
94
|
-
const nextYear = () => setViewDate((prev) => new Date(prev.getFullYear() + 1, prev.getMonth()))
|
|
95
|
-
|
|
96
121
|
const handleDateClick = (date: Date) => {
|
|
97
122
|
if (isDateDisabled(date)) return
|
|
98
123
|
onSelect?.(date)
|
|
99
124
|
}
|
|
100
125
|
|
|
126
|
+
const openYears = () => {
|
|
127
|
+
setYearPageStart(Math.floor(viewDate.getFullYear() / YEARS_PER_PAGE) * YEARS_PER_PAGE)
|
|
128
|
+
setView("years")
|
|
129
|
+
}
|
|
130
|
+
|
|
101
131
|
const calendarDays = generateCalendarDays()
|
|
132
|
+
const yearPage = Array.from({ length: YEARS_PER_PAGE }, (_, i) => yearPageStart + i)
|
|
102
133
|
|
|
103
134
|
return (
|
|
104
135
|
<div className={cn("p-4 bg-card rounded-lg", className)}>
|
|
105
136
|
<div className="flex items-center justify-between mb-4">
|
|
106
|
-
<Button
|
|
137
|
+
<Button
|
|
138
|
+
variant="default"
|
|
139
|
+
size="sm"
|
|
140
|
+
className={cn("h-8 w-8 p-0", view !== "days" && "invisible")}
|
|
141
|
+
aria-label="Previous month"
|
|
142
|
+
onClick={previousMonth}
|
|
143
|
+
>
|
|
107
144
|
<ChevronLeft className="h-4 w-4" />
|
|
108
145
|
</Button>
|
|
109
146
|
|
|
110
|
-
|
|
147
|
+
{/* Month and year are EACH pickers: month → 12-month grid; year →
|
|
148
|
+
paged 12-year grid. No single-step year arrows. */}
|
|
149
|
+
<div className="flex items-center gap-1.5">
|
|
111
150
|
<button
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => setView(view === "months" ? "days" : "months")}
|
|
153
|
+
aria-expanded={view === "months"}
|
|
154
|
+
className="text-base font-bold hover:opacity-80 transition-opacity text-foreground underline decoration-1 underline-offset-4 decoration-border"
|
|
114
155
|
>
|
|
115
|
-
{MONTHS[viewDate.getMonth()]}
|
|
156
|
+
{MONTHS[viewDate.getMonth()]}
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={() => (view === "years" ? setView("days") : openYears())}
|
|
161
|
+
aria-expanded={view === "years"}
|
|
162
|
+
className="text-base font-bold hover:opacity-80 transition-opacity text-foreground underline decoration-1 underline-offset-4 decoration-border"
|
|
163
|
+
>
|
|
164
|
+
{viewDate.getFullYear()}
|
|
116
165
|
</button>
|
|
117
|
-
|
|
118
|
-
{showYearPicker && (
|
|
119
|
-
<div className="flex flex-col gap-1">
|
|
120
|
-
<Button variant="ghost" size="sm" className="h-4 w-4 p-0" onClick={nextYear}>
|
|
121
|
-
<ChevronUp className="h-3 w-3" />
|
|
122
|
-
</Button>
|
|
123
|
-
<Button variant="ghost" size="sm" className="h-4 w-4 p-0" onClick={previousYear}>
|
|
124
|
-
<ChevronDown className="h-3 w-3" />
|
|
125
|
-
</Button>
|
|
126
|
-
</div>
|
|
127
|
-
)}
|
|
128
166
|
</div>
|
|
129
167
|
|
|
130
|
-
<Button
|
|
168
|
+
<Button
|
|
169
|
+
variant="default"
|
|
170
|
+
size="sm"
|
|
171
|
+
className={cn("h-8 w-8 p-0", view !== "days" && "invisible")}
|
|
172
|
+
aria-label="Next month"
|
|
173
|
+
onClick={nextMonth}
|
|
174
|
+
>
|
|
131
175
|
<ChevronRight className="h-4 w-4" />
|
|
132
176
|
</Button>
|
|
133
177
|
</div>
|
|
134
178
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<div
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<div className="grid grid-cols-7 gap-1">
|
|
147
|
-
{calendarDays.map((date, index) => {
|
|
148
|
-
if (!date) return <div key={`empty-${index}`} className="h-9" />
|
|
149
|
-
|
|
150
|
-
const disabled = isDateDisabled(date)
|
|
151
|
-
const selected = isDateSelected(date)
|
|
152
|
-
const todayDate = isToday(date)
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<button
|
|
156
|
-
key={date.toISOString()}
|
|
157
|
-
onClick={() => handleDateClick(date)}
|
|
158
|
-
disabled={disabled}
|
|
159
|
-
className={cn(
|
|
160
|
-
"h-9 w-9 rounded-lg text-sm font-semibold transition-all",
|
|
161
|
-
"hover:scale-105 active:scale-95",
|
|
162
|
-
"flex items-center justify-center",
|
|
163
|
-
!disabled && !selected && !todayDate && "text-foreground hover:bg-primary/10",
|
|
164
|
-
todayDate && !selected && "bg-accent text-foreground font-bold ring-2 ring-border",
|
|
165
|
-
selected && "bg-primary text-white font-bold shadow-lg",
|
|
166
|
-
disabled && "opacity-30 cursor-not-allowed hover:scale-100 hover:bg-transparent",
|
|
167
|
-
disabled && "relative"
|
|
168
|
-
)}
|
|
179
|
+
{view === "years" && (
|
|
180
|
+
<>
|
|
181
|
+
<div className="flex items-center justify-between mb-2">
|
|
182
|
+
<Button
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="sm"
|
|
185
|
+
className="h-8 w-8 p-0"
|
|
186
|
+
aria-label={`Previous ${YEARS_PER_PAGE} years`}
|
|
187
|
+
onClick={() => setYearPageStart((s) => s - YEARS_PER_PAGE)}
|
|
169
188
|
>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
<ChevronLeft className="h-4 w-4" />
|
|
190
|
+
</Button>
|
|
191
|
+
<span className="text-sm font-semibold text-muted-foreground">
|
|
192
|
+
{yearPageStart} – {yearPageStart + YEARS_PER_PAGE - 1}
|
|
193
|
+
</span>
|
|
194
|
+
<Button
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="sm"
|
|
197
|
+
className="h-8 w-8 p-0"
|
|
198
|
+
aria-label={`Next ${YEARS_PER_PAGE} years`}
|
|
199
|
+
onClick={() => setYearPageStart((s) => s + YEARS_PER_PAGE)}
|
|
200
|
+
>
|
|
201
|
+
<ChevronRight className="h-4 w-4" />
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="grid grid-cols-3 gap-1">
|
|
205
|
+
{yearPage.map((year) => {
|
|
206
|
+
const yearDisabled = isYearDisabled(year)
|
|
207
|
+
const isViewYear = year === viewDate.getFullYear()
|
|
208
|
+
return (
|
|
209
|
+
<button
|
|
210
|
+
key={year}
|
|
211
|
+
type="button"
|
|
212
|
+
disabled={yearDisabled}
|
|
213
|
+
onClick={() => {
|
|
214
|
+
setViewDate((prev) => new Date(year, prev.getMonth()))
|
|
215
|
+
setView("months")
|
|
216
|
+
}}
|
|
217
|
+
className={cn(
|
|
218
|
+
"h-10 rounded-lg text-sm font-semibold transition-all",
|
|
219
|
+
"flex items-center justify-center",
|
|
220
|
+
!yearDisabled && !isViewYear && "text-foreground hover:bg-primary/10",
|
|
221
|
+
isViewYear && "bg-primary text-white font-bold shadow-lg",
|
|
222
|
+
yearDisabled && "opacity-30 cursor-not-allowed",
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{year}
|
|
226
|
+
</button>
|
|
227
|
+
)
|
|
228
|
+
})}
|
|
229
|
+
</div>
|
|
230
|
+
</>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{view === "months" && (
|
|
234
|
+
<div className="grid grid-cols-3 gap-1">
|
|
235
|
+
{MONTHS_SHORT.map((label, month) => {
|
|
236
|
+
const monthDisabled = isMonthDisabled(viewDate.getFullYear(), month)
|
|
237
|
+
const isViewMonth = month === viewDate.getMonth()
|
|
238
|
+
return (
|
|
239
|
+
<button
|
|
240
|
+
key={label}
|
|
241
|
+
type="button"
|
|
242
|
+
disabled={monthDisabled}
|
|
243
|
+
onClick={() => {
|
|
244
|
+
setViewDate((prev) => new Date(prev.getFullYear(), month))
|
|
245
|
+
setView("days")
|
|
246
|
+
}}
|
|
247
|
+
className={cn(
|
|
248
|
+
"h-10 rounded-lg text-sm font-semibold transition-all",
|
|
249
|
+
"flex items-center justify-center",
|
|
250
|
+
!monthDisabled && !isViewMonth && "text-foreground hover:bg-primary/10",
|
|
251
|
+
isViewMonth && "bg-primary text-white font-bold shadow-lg",
|
|
252
|
+
monthDisabled && "opacity-30 cursor-not-allowed",
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
{label}
|
|
256
|
+
</button>
|
|
257
|
+
)
|
|
258
|
+
})}
|
|
189
259
|
</div>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{view === "days" && (
|
|
263
|
+
<>
|
|
264
|
+
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
265
|
+
{DAYS.map((day) => (
|
|
266
|
+
<div
|
|
267
|
+
key={day}
|
|
268
|
+
className="text-center text-sm font-bold py-2 text-foreground"
|
|
269
|
+
>
|
|
270
|
+
{day}
|
|
195
271
|
</div>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className="grid grid-cols-7 gap-1">
|
|
276
|
+
{calendarDays.map((date, index) => {
|
|
277
|
+
if (!date) return <div key={`empty-${index}`} className="h-9" />
|
|
278
|
+
|
|
279
|
+
const dateDisabled = isDateDisabled(date)
|
|
280
|
+
const dateSelected = isDateSelected(date)
|
|
281
|
+
const todayDate = isToday(date)
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<button
|
|
285
|
+
key={date.toISOString()}
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={() => handleDateClick(date)}
|
|
288
|
+
disabled={dateDisabled}
|
|
289
|
+
className={cn(
|
|
290
|
+
"h-9 w-9 rounded-lg text-sm font-semibold transition-all",
|
|
291
|
+
"hover:scale-105 active:scale-95",
|
|
292
|
+
"flex items-center justify-center",
|
|
293
|
+
!dateDisabled && !dateSelected && !todayDate && "text-foreground hover:bg-primary/10",
|
|
294
|
+
todayDate && !dateSelected && "bg-accent text-foreground font-bold ring-2 ring-border",
|
|
295
|
+
dateSelected && "bg-primary text-white font-bold shadow-lg",
|
|
296
|
+
dateDisabled && "opacity-30 cursor-not-allowed hover:scale-100 hover:bg-transparent",
|
|
297
|
+
dateDisabled && "relative"
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{date.getDate()}
|
|
301
|
+
{dateDisabled && (
|
|
302
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
303
|
+
<div className="w-px h-full bg-current transform rotate-45" />
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</button>
|
|
307
|
+
)
|
|
308
|
+
})}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className="mt-4 pt-3 border-t border-border text-xs space-y-1">
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<div className="h-3 w-3 rounded-sm bg-primary" />
|
|
314
|
+
<span className="text-muted-foreground">Selected</span>
|
|
196
315
|
</div>
|
|
197
|
-
<
|
|
316
|
+
<div className="flex items-center gap-2">
|
|
317
|
+
<div className="h-3 w-3 rounded-sm bg-accent ring-2 ring-border" />
|
|
318
|
+
<span className="text-muted-foreground">Today</span>
|
|
319
|
+
</div>
|
|
320
|
+
{(disablePast || disableFuture || disabled) && (
|
|
321
|
+
<div className="flex items-center gap-2">
|
|
322
|
+
<div className="h-3 w-3 rounded-sm bg-muted opacity-30 relative">
|
|
323
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
324
|
+
<div className="w-px h-full bg-foreground transform rotate-45" />
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
<span className="text-muted-foreground">Unavailable</span>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
198
330
|
</div>
|
|
199
|
-
|
|
200
|
-
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
201
333
|
</div>
|
|
202
334
|
)
|
|
203
335
|
}
|