@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.
Files changed (50) hide show
  1. package/dist/index.js.map +1 -1
  2. package/dist/lib/format-numbers.js.map +1 -1
  3. package/dist/lib/utils.js.map +1 -1
  4. package/dist/react/components/accordion.js.map +1 -1
  5. package/dist/react/components/alert-dialog.js.map +1 -1
  6. package/dist/react/components/alert.js.map +1 -1
  7. package/dist/react/components/avatar.js.map +1 -1
  8. package/dist/react/components/badge.js.map +1 -1
  9. package/dist/react/components/button.js.map +1 -1
  10. package/dist/react/components/card.js.map +1 -1
  11. package/dist/react/components/checkbox.js.map +1 -1
  12. package/dist/react/components/chunk-error-recovery.js.map +1 -1
  13. package/dist/react/components/collapsible.js.map +1 -1
  14. package/dist/react/components/consequence-dialog.js.map +1 -1
  15. package/dist/react/components/date-picker.d.ts +5 -0
  16. package/dist/react/components/date-picker.d.ts.map +1 -1
  17. package/dist/react/components/date-picker.js +56 -13
  18. package/dist/react/components/date-picker.js.map +1 -1
  19. package/dist/react/components/dialog.js.map +1 -1
  20. package/dist/react/components/dropdown-menu.js.map +1 -1
  21. package/dist/react/components/end-of-list-indicator.js.map +1 -1
  22. package/dist/react/components/form.js.map +1 -1
  23. package/dist/react/components/input.js.map +1 -1
  24. package/dist/react/components/label.js.map +1 -1
  25. package/dist/react/components/layout/screen-adaptive-view.js.map +1 -1
  26. package/dist/react/components/menubar.js.map +1 -1
  27. package/dist/react/components/popover.js.map +1 -1
  28. package/dist/react/components/progress.js.map +1 -1
  29. package/dist/react/components/radio-group.js.map +1 -1
  30. package/dist/react/components/relative-time.js.map +1 -1
  31. package/dist/react/components/scroll-area.js.map +1 -1
  32. package/dist/react/components/scroll-to-top-button.js.map +1 -1
  33. package/dist/react/components/search-dropdown.js.map +1 -1
  34. package/dist/react/components/select.js.map +1 -1
  35. package/dist/react/components/separator.js.map +1 -1
  36. package/dist/react/components/sheet.js.map +1 -1
  37. package/dist/react/components/skeleton.js.map +1 -1
  38. package/dist/react/components/slider.js.map +1 -1
  39. package/dist/react/components/switch.js.map +1 -1
  40. package/dist/react/components/table.js.map +1 -1
  41. package/dist/react/components/tabs.js.map +1 -1
  42. package/dist/react/components/textarea.js.map +1 -1
  43. package/dist/react/components/toast.js.map +1 -1
  44. package/dist/react/components/toaster.js.map +1 -1
  45. package/dist/react/components/tooltip.js.map +1 -1
  46. package/dist/react/hooks/use-media-query.js.map +1 -1
  47. package/dist/react/hooks/use-toast.js.map +1 -1
  48. package/dist/react/index.js.map +1 -1
  49. package/package.json +3 -3
  50. 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, ChevronUp, ChevronDown } from "lucide-react"
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 [showYearPicker, setShowYearPicker] = React.useState(false)
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 variant="default" size="sm" className="h-8 w-8 p-0" onClick={previousMonth}>
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
- <div className="flex items-center gap-2">
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
- onClick={() => setShowYearPicker(!showYearPicker)}
113
- className="text-base font-bold hover:opacity-80 transition-opacity text-foreground"
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()]} {viewDate.getFullYear()}
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 variant="default" size="sm" className="h-8 w-8 p-0" onClick={nextMonth}>
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
- <div className="grid grid-cols-7 gap-1 mb-2">
136
- {DAYS.map((day) => (
137
- <div
138
- key={day}
139
- className="text-center text-sm font-bold py-2 text-foreground"
140
- >
141
- {day}
142
- </div>
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
- {date.getDate()}
171
- {disabled && (
172
- <div className="absolute inset-0 flex items-center justify-center">
173
- <div className="w-px h-full bg-current transform rotate-45" />
174
- </div>
175
- )}
176
- </button>
177
- )
178
- })}
179
- </div>
180
-
181
- <div className="mt-4 pt-3 border-t border-border text-xs space-y-1">
182
- <div className="flex items-center gap-2">
183
- <div className="h-3 w-3 rounded-sm bg-primary" />
184
- <span className="text-muted-foreground">Selected</span>
185
- </div>
186
- <div className="flex items-center gap-2">
187
- <div className="h-3 w-3 rounded-sm bg-accent ring-2 ring-border" />
188
- <span className="text-muted-foreground">Today</span>
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
- {(disablePast || disableFuture || disabled) && (
191
- <div className="flex items-center gap-2">
192
- <div className="h-3 w-3 rounded-sm bg-muted opacity-30 relative">
193
- <div className="absolute inset-0 flex items-center justify-center">
194
- <div className="w-px h-full bg-foreground transform rotate-45" />
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
- <span className="text-muted-foreground">Unavailable</span>
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
- </div>
331
+ </>
332
+ )}
201
333
  </div>
202
334
  )
203
335
  }