azamat-ui-kit-cli 0.2.2

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 (213) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +432 -0
  3. package/package.json +34 -0
  4. package/vendor/package.json +4 -0
  5. package/vendor/src/components/actions/action-bar.tsx +35 -0
  6. package/vendor/src/components/actions/action-menu.tsx +120 -0
  7. package/vendor/src/components/actions/button-group.tsx +47 -0
  8. package/vendor/src/components/actions/copy-button.tsx +91 -0
  9. package/vendor/src/components/actions/copy-field.tsx +31 -0
  10. package/vendor/src/components/actions/floating-action-button.tsx +33 -0
  11. package/vendor/src/components/actions/index.ts +7 -0
  12. package/vendor/src/components/actions/public.ts +5 -0
  13. package/vendor/src/components/actions/quick-action-grid.tsx +162 -0
  14. package/vendor/src/components/calendar/calendar.tsx +328 -0
  15. package/vendor/src/components/calendar/date-picker.tsx +78 -0
  16. package/vendor/src/components/calendar/date-range-picker.tsx +96 -0
  17. package/vendor/src/components/calendar/date-utils.ts +89 -0
  18. package/vendor/src/components/calendar/index.ts +4 -0
  19. package/vendor/src/components/charts/charts.tsx +275 -0
  20. package/vendor/src/components/charts/horizontal-bar-chart.tsx +46 -0
  21. package/vendor/src/components/charts/index.ts +4 -0
  22. package/vendor/src/components/charts/kpi.tsx +68 -0
  23. package/vendor/src/components/charts/progress-ring.tsx +45 -0
  24. package/vendor/src/components/charts/public.ts +1 -0
  25. package/vendor/src/components/command/command-palette.tsx +375 -0
  26. package/vendor/src/components/command/index.ts +1 -0
  27. package/vendor/src/components/data-table/data-table-actions-column.tsx +58 -0
  28. package/vendor/src/components/data-table/data-table-bulk-actions.tsx +84 -0
  29. package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +79 -0
  30. package/vendor/src/components/data-table/data-table-pagination.tsx +91 -0
  31. package/vendor/src/components/data-table/data-table-row-actions.tsx +48 -0
  32. package/vendor/src/components/data-table/data-table-select-column.tsx +59 -0
  33. package/vendor/src/components/data-table/data-table-sortable-header.tsx +45 -0
  34. package/vendor/src/components/data-table/data-table-toolbar.tsx +76 -0
  35. package/vendor/src/components/data-table/data-table-view-presets.tsx +128 -0
  36. package/vendor/src/components/data-table/data-table.tsx +507 -0
  37. package/vendor/src/components/data-table/index.ts +12 -0
  38. package/vendor/src/components/data-table/public.ts +10 -0
  39. package/vendor/src/components/data-table/table-export-menu.tsx +56 -0
  40. package/vendor/src/components/data-table/table-import-button.tsx +43 -0
  41. package/vendor/src/components/display/activity-feed.tsx +97 -0
  42. package/vendor/src/components/display/avatar.tsx +131 -0
  43. package/vendor/src/components/display/code-block.tsx +33 -0
  44. package/vendor/src/components/display/data-state.tsx +63 -0
  45. package/vendor/src/components/display/description-list.tsx +119 -0
  46. package/vendor/src/components/display/descriptions.tsx +83 -0
  47. package/vendor/src/components/display/entity-card.tsx +53 -0
  48. package/vendor/src/components/display/file-card.tsx +54 -0
  49. package/vendor/src/components/display/index.ts +30 -0
  50. package/vendor/src/components/display/kanban.tsx +104 -0
  51. package/vendor/src/components/display/keyboard-shortcut.tsx +31 -0
  52. package/vendor/src/components/display/list.tsx +100 -0
  53. package/vendor/src/components/display/metric-grid.tsx +86 -0
  54. package/vendor/src/components/display/progress.tsx +162 -0
  55. package/vendor/src/components/display/property-grid.tsx +54 -0
  56. package/vendor/src/components/display/result.tsx +90 -0
  57. package/vendor/src/components/display/smart-card.tsx +168 -0
  58. package/vendor/src/components/display/statistic.tsx +107 -0
  59. package/vendor/src/components/display/status-legend.tsx +108 -0
  60. package/vendor/src/components/display/tag-list.tsx +52 -0
  61. package/vendor/src/components/display/timeline.tsx +132 -0
  62. package/vendor/src/components/display/tree-view.tsx +116 -0
  63. package/vendor/src/components/feedback/alert.tsx +69 -0
  64. package/vendor/src/components/feedback/empty-state.tsx +56 -0
  65. package/vendor/src/components/feedback/index.ts +5 -0
  66. package/vendor/src/components/feedback/loading-state.tsx +39 -0
  67. package/vendor/src/components/feedback/page-state.tsx +69 -0
  68. package/vendor/src/components/feedback/status-badge.tsx +62 -0
  69. package/vendor/src/components/filters/filter-bar.tsx +89 -0
  70. package/vendor/src/components/filters/filter-chips.tsx +69 -0
  71. package/vendor/src/components/filters/index.ts +2 -0
  72. package/vendor/src/components/form/form-actions.tsx +53 -0
  73. package/vendor/src/components/form/form-async-select.tsx +26 -0
  74. package/vendor/src/components/form/form-date-input.tsx +19 -0
  75. package/vendor/src/components/form/form-date-picker.tsx +54 -0
  76. package/vendor/src/components/form/form-date-range-input.tsx +79 -0
  77. package/vendor/src/components/form/form-date-range-picker.tsx +57 -0
  78. package/vendor/src/components/form/form-field-shell.tsx +191 -0
  79. package/vendor/src/components/form/form-input.tsx +480 -0
  80. package/vendor/src/components/form/form-number-input.tsx +19 -0
  81. package/vendor/src/components/form/form-password-input.tsx +19 -0
  82. package/vendor/src/components/form/form-phone-input.tsx +22 -0
  83. package/vendor/src/components/form/form-search-input.tsx +19 -0
  84. package/vendor/src/components/form/form-section.tsx +29 -0
  85. package/vendor/src/components/form/form-select.tsx +194 -0
  86. package/vendor/src/components/form/form-switch.tsx +145 -0
  87. package/vendor/src/components/form/form-textarea.tsx +103 -0
  88. package/vendor/src/components/form/index.ts +17 -0
  89. package/vendor/src/components/form/public.ts +14 -0
  90. package/vendor/src/components/form/smart-form-shell.tsx +59 -0
  91. package/vendor/src/components/inputs/async-select.tsx +1143 -0
  92. package/vendor/src/components/inputs/clearable-input.tsx +78 -0
  93. package/vendor/src/components/inputs/color-input.tsx +47 -0
  94. package/vendor/src/components/inputs/combobox.tsx +89 -0
  95. package/vendor/src/components/inputs/date-input.tsx +32 -0
  96. package/vendor/src/components/inputs/date-range-input.tsx +67 -0
  97. package/vendor/src/components/inputs/index.ts +19 -0
  98. package/vendor/src/components/inputs/input-chrome.tsx +37 -0
  99. package/vendor/src/components/inputs/input-decorator.tsx +64 -0
  100. package/vendor/src/components/inputs/input-value.ts +42 -0
  101. package/vendor/src/components/inputs/masked-input.tsx +51 -0
  102. package/vendor/src/components/inputs/money-input.tsx +73 -0
  103. package/vendor/src/components/inputs/number-input.tsx +87 -0
  104. package/vendor/src/components/inputs/numeric-value.ts +39 -0
  105. package/vendor/src/components/inputs/otp-input.tsx +102 -0
  106. package/vendor/src/components/inputs/password-input.tsx +85 -0
  107. package/vendor/src/components/inputs/phone-input.tsx +46 -0
  108. package/vendor/src/components/inputs/quantity-input.tsx +116 -0
  109. package/vendor/src/components/inputs/quantity-stepper.tsx +49 -0
  110. package/vendor/src/components/inputs/rating.tsx +98 -0
  111. package/vendor/src/components/inputs/search-input.tsx +26 -0
  112. package/vendor/src/components/inputs/simple-select.tsx +72 -0
  113. package/vendor/src/components/inputs/slider.tsx +149 -0
  114. package/vendor/src/components/inputs/tag-input.tsx +104 -0
  115. package/vendor/src/components/layout/app-header.tsx +46 -0
  116. package/vendor/src/components/layout/app-shell.tsx +243 -0
  117. package/vendor/src/components/layout/app-sidebar.tsx +179 -0
  118. package/vendor/src/components/layout/breadcrumbs.tsx +72 -0
  119. package/vendor/src/components/layout/index.ts +11 -0
  120. package/vendor/src/components/layout/page-container.tsx +30 -0
  121. package/vendor/src/components/layout/page-header.tsx +60 -0
  122. package/vendor/src/components/layout/public.ts +10 -0
  123. package/vendor/src/components/layout/section.tsx +76 -0
  124. package/vendor/src/components/layout/sidebar-nav.tsx +147 -0
  125. package/vendor/src/components/layout/stat-card.tsx +88 -0
  126. package/vendor/src/components/layout/sticky-footer-bar.tsx +23 -0
  127. package/vendor/src/components/layout/workspace-shell.tsx +50 -0
  128. package/vendor/src/components/navigation/anchor-nav.tsx +44 -0
  129. package/vendor/src/components/navigation/index.ts +4 -0
  130. package/vendor/src/components/navigation/page-tabs.tsx +67 -0
  131. package/vendor/src/components/navigation/pagination.tsx +179 -0
  132. package/vendor/src/components/navigation/stepper-tabs.tsx +67 -0
  133. package/vendor/src/components/notifications/index.ts +1 -0
  134. package/vendor/src/components/notifications/toast.tsx +259 -0
  135. package/vendor/src/components/overlay/confirm-dialog.tsx +66 -0
  136. package/vendor/src/components/overlay/dialog-actions.tsx +68 -0
  137. package/vendor/src/components/overlay/index.ts +4 -0
  138. package/vendor/src/components/overlay/modal-shell.tsx +93 -0
  139. package/vendor/src/components/overlay/sheet-shell.tsx +212 -0
  140. package/vendor/src/components/patterns/action-system.tsx +116 -0
  141. package/vendor/src/components/patterns/crud-system.tsx +53 -0
  142. package/vendor/src/components/patterns/data-view.tsx +84 -0
  143. package/vendor/src/components/patterns/entity-details.tsx +66 -0
  144. package/vendor/src/components/patterns/filter-builder.tsx +113 -0
  145. package/vendor/src/components/patterns/form-builder-presets.ts +131 -0
  146. package/vendor/src/components/patterns/form-builder.tsx +334 -0
  147. package/vendor/src/components/patterns/index.ts +12 -0
  148. package/vendor/src/components/patterns/public.ts +4 -0
  149. package/vendor/src/components/patterns/resource-detail-page.tsx +160 -0
  150. package/vendor/src/components/patterns/resource-page.tsx +159 -0
  151. package/vendor/src/components/patterns/resource-system.tsx +61 -0
  152. package/vendor/src/components/patterns/settings-section.tsx +46 -0
  153. package/vendor/src/components/patterns/status-system.tsx +89 -0
  154. package/vendor/src/components/theme-provider.tsx +51 -0
  155. package/vendor/src/components/ui/badge.tsx +52 -0
  156. package/vendor/src/components/ui/button.tsx +61 -0
  157. package/vendor/src/components/ui/card.tsx +103 -0
  158. package/vendor/src/components/ui/checkbox.tsx +82 -0
  159. package/vendor/src/components/ui/collapse.tsx +126 -0
  160. package/vendor/src/components/ui/command.tsx +194 -0
  161. package/vendor/src/components/ui/dialog.tsx +160 -0
  162. package/vendor/src/components/ui/divider.tsx +46 -0
  163. package/vendor/src/components/ui/dropdown-menu.tsx +266 -0
  164. package/vendor/src/components/ui/input-group.tsx +158 -0
  165. package/vendor/src/components/ui/input.tsx +20 -0
  166. package/vendor/src/components/ui/popover.tsx +90 -0
  167. package/vendor/src/components/ui/segmented-control.tsx +78 -0
  168. package/vendor/src/components/ui/select.tsx +201 -0
  169. package/vendor/src/components/ui/skeleton.tsx +75 -0
  170. package/vendor/src/components/ui/spinner.tsx +50 -0
  171. package/vendor/src/components/ui/switch.tsx +71 -0
  172. package/vendor/src/components/ui/table.tsx +114 -0
  173. package/vendor/src/components/ui/tabs.tsx +55 -0
  174. package/vendor/src/components/ui/textarea.tsx +18 -0
  175. package/vendor/src/components/ui/tooltip.tsx +38 -0
  176. package/vendor/src/components/upload/file-upload.tsx +483 -0
  177. package/vendor/src/components/upload/image-upload.tsx +118 -0
  178. package/vendor/src/components/upload/index.ts +2 -0
  179. package/vendor/src/components/wizard/index.ts +2 -0
  180. package/vendor/src/components/wizard/stepper.tsx +53 -0
  181. package/vendor/src/components/wizard/wizard.tsx +60 -0
  182. package/vendor/src/families/card-family.ts +28 -0
  183. package/vendor/src/families/catalog.ts +96 -0
  184. package/vendor/src/families/data-table-family.ts +31 -0
  185. package/vendor/src/families/docs-adoption.ts +103 -0
  186. package/vendor/src/families/docs-groups.ts +209 -0
  187. package/vendor/src/families/docs-queries.ts +84 -0
  188. package/vendor/src/families/docs-routing.ts +89 -0
  189. package/vendor/src/families/form-family.ts +45 -0
  190. package/vendor/src/families/index.ts +17 -0
  191. package/vendor/src/families/input-family.ts +61 -0
  192. package/vendor/src/families/member-metadata.ts +466 -0
  193. package/vendor/src/families/member-queries.ts +28 -0
  194. package/vendor/src/families/member-snippet-queries.ts +54 -0
  195. package/vendor/src/families/member-snippets.ts +673 -0
  196. package/vendor/src/families/migration-map.ts +79 -0
  197. package/vendor/src/families/queries.ts +63 -0
  198. package/vendor/src/families/select-family.ts +33 -0
  199. package/vendor/src/families/views.ts +81 -0
  200. package/vendor/src/hooks/index.ts +6 -0
  201. package/vendor/src/hooks/use-before-unload-when-dirty.ts +21 -0
  202. package/vendor/src/hooks/use-data-table-view-state.ts +122 -0
  203. package/vendor/src/hooks/use-debounce.ts +52 -0
  204. package/vendor/src/hooks/use-disclosure.ts +38 -0
  205. package/vendor/src/hooks/use-is-mobile.ts +28 -0
  206. package/vendor/src/hooks/use-session-storage-state.ts +85 -0
  207. package/vendor/src/index.ts +38 -0
  208. package/vendor/src/lib/utils.ts +6 -0
  209. package/vendor/templates/components/button.tsx +0 -0
  210. package/vendor/templates/components/data-table.tsx +0 -0
  211. package/vendor/templates/components/input.tsx +0 -0
  212. package/vendor/templates/lib/utils.ts +0 -0
  213. package/vendor/templates/styles/globals.css +0 -0
@@ -0,0 +1,328 @@
1
+ import * as React from "react"
2
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
3
+
4
+ import { Button } from "@/components/ui/button"
5
+ import { cn } from "@/lib/utils"
6
+ import {
7
+ addMonths,
8
+ getMonthDays,
9
+ getMonthLabel,
10
+ getWeekdayLabels,
11
+ isAfterDate,
12
+ isBeforeDate,
13
+ isSameMonth,
14
+ isWithinRange,
15
+ parseDateKey,
16
+ startOfMonth,
17
+ toDateKey,
18
+ } from "./date-utils"
19
+
20
+ export type CalendarDateRange = {
21
+ from?: string | null
22
+ to?: string | null
23
+ }
24
+
25
+ export type CalendarDisabledReason = "disabled" | "min" | "max" | "range"
26
+
27
+ export type CalendarLabels = {
28
+ previousMonth?: string
29
+ nextMonth?: string
30
+ selectDate?: (date: string) => string
31
+ disabledDate?: (date: string, reason: CalendarDisabledReason) => string
32
+ }
33
+
34
+ export type CalendarProps = React.ComponentProps<"div"> & {
35
+ value?: string | null
36
+ range?: CalendarDateRange
37
+ onValueChange?: (value: string) => void
38
+ onRangeChange?: (range: CalendarDateRange) => void
39
+ mode?: "single" | "range"
40
+ month?: Date
41
+ defaultMonth?: Date | string | null
42
+ onMonthChange?: (month: Date) => void
43
+ min?: string
44
+ max?: string
45
+ disabledDates?: string[]
46
+ locale?: string
47
+ weekStartsOn?: 0 | 1
48
+ labels?: CalendarLabels
49
+ }
50
+
51
+ function getInitialMonth(defaultMonth?: Date | string | null, value?: string | null, range?: CalendarDateRange) {
52
+ if (defaultMonth instanceof Date) return startOfMonth(defaultMonth)
53
+
54
+ const fromDefault = parseDateKey(defaultMonth)
55
+ if (fromDefault) return startOfMonth(fromDefault)
56
+
57
+ const fromValue = parseDateKey(value)
58
+ if (fromValue) return startOfMonth(fromValue)
59
+
60
+ const fromRange = parseDateKey(range?.from ?? range?.to)
61
+ if (fromRange) return startOfMonth(fromRange)
62
+
63
+ return startOfMonth(new Date())
64
+ }
65
+
66
+ function addDays(date: Date, amount: number) {
67
+ const next = new Date(date)
68
+ next.setDate(date.getDate() + amount)
69
+ return next
70
+ }
71
+
72
+ function getDateAtSameDayInMonth(date: Date, month: Date) {
73
+ const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate()
74
+ return new Date(month.getFullYear(), month.getMonth(), Math.min(date.getDate(), lastDay))
75
+ }
76
+
77
+ function getDateKeysBetween(from: string, to: string) {
78
+ const start = parseDateKey(from)
79
+ const end = parseDateKey(to)
80
+
81
+ if (!start || !end || to < from) return []
82
+
83
+ const keys: string[] = []
84
+ const cursor = new Date(start)
85
+
86
+ while (toDateKey(cursor) <= to) {
87
+ keys.push(toDateKey(cursor))
88
+ cursor.setDate(cursor.getDate() + 1)
89
+ }
90
+
91
+ return keys
92
+ }
93
+
94
+ function Calendar({
95
+ className,
96
+ value,
97
+ range,
98
+ onValueChange,
99
+ onRangeChange,
100
+ mode = "single",
101
+ month,
102
+ defaultMonth,
103
+ onMonthChange,
104
+ min,
105
+ max,
106
+ disabledDates,
107
+ locale = "en-US",
108
+ weekStartsOn = 1,
109
+ labels,
110
+ ...props
111
+ }: CalendarProps) {
112
+ const [internalMonth, setInternalMonth] = React.useState(() => getInitialMonth(defaultMonth, value, range))
113
+ const currentMonth = month ?? internalMonth
114
+ const todayKey = toDateKey(new Date())
115
+ const disabledSet = React.useMemo(() => new Set(disabledDates ?? []), [disabledDates])
116
+ const monthDays = React.useMemo(() => getMonthDays(currentMonth, weekStartsOn), [currentMonth, weekStartsOn])
117
+ const weekdayLabels = React.useMemo(() => getWeekdayLabels(locale, weekStartsOn), [locale, weekStartsOn])
118
+ const buttonRefs = React.useRef(new Map<string, HTMLButtonElement>())
119
+ const [focusedDateKey, setFocusedDateKey] = React.useState(() => value ?? range?.from ?? todayKey)
120
+
121
+ const getDisabledReason = React.useCallback(
122
+ (dateKey: string): CalendarDisabledReason | undefined => {
123
+ if (disabledSet.has(dateKey)) return "disabled"
124
+ if (isBeforeDate(dateKey, min)) return "min"
125
+ if (isAfterDate(dateKey, max)) return "max"
126
+ return undefined
127
+ },
128
+ [disabledSet, max, min]
129
+ )
130
+
131
+ const isDateDisabled = React.useCallback((dateKey: string) => Boolean(getDisabledReason(dateKey)), [getDisabledReason])
132
+
133
+ const visibleEnabledKeys = React.useMemo(
134
+ () => monthDays.map(toDateKey).filter((dateKey) => !isDateDisabled(dateKey)),
135
+ [isDateDisabled, monthDays]
136
+ )
137
+
138
+ const tabbableDateKey = React.useMemo(() => {
139
+ const preferred = value ?? range?.from ?? todayKey
140
+ if (visibleEnabledKeys.includes(focusedDateKey)) return focusedDateKey
141
+ if (visibleEnabledKeys.includes(preferred)) return preferred
142
+ return visibleEnabledKeys[0]
143
+ }, [focusedDateKey, range?.from, todayKey, value, visibleEnabledKeys])
144
+
145
+ React.useEffect(() => {
146
+ if (!focusedDateKey) return
147
+ buttonRefs.current.get(focusedDateKey)?.focus()
148
+ }, [focusedDateKey])
149
+
150
+ const setMonth = (nextMonth: Date) => {
151
+ const next = startOfMonth(nextMonth)
152
+ setInternalMonth(next)
153
+ onMonthChange?.(next)
154
+ }
155
+
156
+ const moveFocus = (date: Date) => {
157
+ let nextDate = date
158
+ let nextKey = toDateKey(nextDate)
159
+ let guard = 0
160
+
161
+ while (isDateDisabled(nextKey) && guard < 370) {
162
+ nextDate = addDays(nextDate, nextDate < date ? -1 : 1)
163
+ nextKey = toDateKey(nextDate)
164
+ guard += 1
165
+ }
166
+
167
+ setFocusedDateKey(nextKey)
168
+
169
+ if (!isSameMonth(nextDate, currentMonth)) {
170
+ setMonth(startOfMonth(nextDate))
171
+ }
172
+ }
173
+
174
+ const handleDateSelect = (dateKey: string) => {
175
+ if (isDateDisabled(dateKey)) return
176
+
177
+ if (mode === "single") {
178
+ onValueChange?.(dateKey)
179
+ return
180
+ }
181
+
182
+ const from = range?.from ?? null
183
+ const to = range?.to ?? null
184
+
185
+ if (!from || (from && to) || dateKey < from) {
186
+ onRangeChange?.({ from: dateKey, to: null })
187
+ return
188
+ }
189
+
190
+ const rangeHasDisabledDate = getDateKeysBetween(from, dateKey).some((key) => isDateDisabled(key))
191
+
192
+ if (rangeHasDisabledDate) {
193
+ onRangeChange?.({ from: dateKey, to: null })
194
+ return
195
+ }
196
+
197
+ onRangeChange?.({ from, to: dateKey })
198
+ }
199
+
200
+ const handleDateKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, date: Date) => {
201
+ const columnIndex = monthDays.findIndex((item) => toDateKey(item) === toDateKey(date)) % 7
202
+
203
+ switch (event.key) {
204
+ case "ArrowRight":
205
+ event.preventDefault()
206
+ moveFocus(addDays(date, 1))
207
+ break
208
+ case "ArrowLeft":
209
+ event.preventDefault()
210
+ moveFocus(addDays(date, -1))
211
+ break
212
+ case "ArrowDown":
213
+ event.preventDefault()
214
+ moveFocus(addDays(date, 7))
215
+ break
216
+ case "ArrowUp":
217
+ event.preventDefault()
218
+ moveFocus(addDays(date, -7))
219
+ break
220
+ case "Home":
221
+ event.preventDefault()
222
+ moveFocus(addDays(date, -columnIndex))
223
+ break
224
+ case "End":
225
+ event.preventDefault()
226
+ moveFocus(addDays(date, 6 - columnIndex))
227
+ break
228
+ case "PageUp":
229
+ event.preventDefault()
230
+ moveFocus(getDateAtSameDayInMonth(date, addMonths(date, -1)))
231
+ break
232
+ case "PageDown":
233
+ event.preventDefault()
234
+ moveFocus(getDateAtSameDayInMonth(date, addMonths(date, 1)))
235
+ break
236
+ }
237
+ }
238
+
239
+ return (
240
+ <div
241
+ data-slot="calendar"
242
+ className={cn(
243
+ "w-72 rounded-[calc(var(--radius-2xl)+2px)] border border-border/75 bg-popover/98 p-3 text-popover-foreground shadow-[0_24px_70px_rgba(15,23,42,0.14)] backdrop-blur",
244
+ className
245
+ )}
246
+ {...props}
247
+ >
248
+ <div className="mb-3 flex items-center justify-between gap-2">
249
+ <Button
250
+ type="button"
251
+ variant="outline"
252
+ size="icon-sm"
253
+ className="rounded-full border-border/80 bg-background/70 text-foreground shadow-none hover:bg-accent hover:text-accent-foreground"
254
+ aria-label={labels?.previousMonth ?? "Previous month"}
255
+ onClick={() => setMonth(addMonths(currentMonth, -1))}
256
+ >
257
+ <ChevronLeftIcon />
258
+ </Button>
259
+ <div className="text-base font-semibold capitalize tracking-tight text-foreground">{getMonthLabel(currentMonth, locale)}</div>
260
+ <Button
261
+ type="button"
262
+ variant="outline"
263
+ size="icon-sm"
264
+ className="rounded-full border-border/80 bg-background/70 text-foreground shadow-none hover:bg-accent hover:text-accent-foreground"
265
+ aria-label={labels?.nextMonth ?? "Next month"}
266
+ onClick={() => setMonth(addMonths(currentMonth, 1))}
267
+ >
268
+ <ChevronRightIcon />
269
+ </Button>
270
+ </div>
271
+
272
+ <div className="grid grid-cols-7 gap-1 text-center text-[0.72rem] font-medium uppercase tracking-[0.18em] text-muted-foreground">
273
+ {weekdayLabels.map((weekday) => (
274
+ <div key={weekday} className="py-1.5">
275
+ {weekday}
276
+ </div>
277
+ ))}
278
+ </div>
279
+
280
+ <div className="mt-2 grid grid-cols-7 gap-1.5">
281
+ {monthDays.map((date) => {
282
+ const dateKey = toDateKey(date)
283
+ const outside = !isSameMonth(date, currentMonth)
284
+ const selected = mode === "single" ? value === dateKey : dateKey === range?.from || dateKey === range?.to
285
+ const inRange = mode === "range" && isWithinRange(dateKey, range?.from, range?.to)
286
+ const disabledReason = getDisabledReason(dateKey)
287
+ const disabled = Boolean(disabledReason)
288
+ const disabledLabel = disabledReason ? labels?.disabledDate?.(dateKey, disabledReason) : undefined
289
+
290
+ return (
291
+ <button
292
+ key={dateKey}
293
+ ref={(node) => {
294
+ if (node) buttonRefs.current.set(dateKey, node)
295
+ else buttonRefs.current.delete(dateKey)
296
+ }}
297
+ type="button"
298
+ disabled={disabled}
299
+ aria-label={disabledLabel ?? labels?.selectDate?.(dateKey) ?? dateKey}
300
+ aria-current={dateKey === todayKey ? "date" : undefined}
301
+ tabIndex={dateKey === tabbableDateKey ? 0 : -1}
302
+ title={disabledLabel}
303
+ data-selected={selected || undefined}
304
+ data-today={dateKey === todayKey || undefined}
305
+ data-outside={outside || undefined}
306
+ data-in-range={inRange || undefined}
307
+ data-disabled-reason={disabledReason}
308
+ className={cn(
309
+ "flex h-9 items-center justify-center rounded-xl border border-transparent text-sm font-medium outline-none transition-[background-color,color,border-color,box-shadow] hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-35",
310
+ outside && "text-muted-foreground/45",
311
+ dateKey === todayKey && "border-primary/30 bg-accent/25 text-foreground",
312
+ inRange && "border-accent/60 bg-accent/75 text-accent-foreground",
313
+ selected && "border-primary/80 bg-primary text-primary-foreground shadow-[0_10px_24px_color-mix(in_oklch,var(--primary),transparent_76%)] hover:bg-primary/92 hover:text-primary-foreground"
314
+ )}
315
+ onFocus={() => setFocusedDateKey(dateKey)}
316
+ onKeyDown={(event) => handleDateKeyDown(event, date)}
317
+ onClick={() => handleDateSelect(dateKey)}
318
+ >
319
+ {date.getDate()}
320
+ </button>
321
+ )
322
+ })}
323
+ </div>
324
+ </div>
325
+ )
326
+ }
327
+
328
+ export { Calendar }
@@ -0,0 +1,78 @@
1
+ import * as React from "react"
2
+ import { CalendarIcon } from "lucide-react"
3
+
4
+ import { Calendar, type CalendarProps } from "@/components/calendar/calendar"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
7
+ import { cn } from "@/lib/utils"
8
+ import { parseDateKey } from "./date-utils"
9
+
10
+ export type DatePickerLabels = CalendarProps["labels"] & {
11
+ placeholder?: string
12
+ }
13
+
14
+ export type DatePickerProps = Omit<
15
+ CalendarProps,
16
+ "mode" | "range" | "onRangeChange" | "labels"
17
+ > & {
18
+ placeholder?: string
19
+ labels?: DatePickerLabels
20
+ disabled?: boolean
21
+ formatValue?: (value: string) => React.ReactNode
22
+ triggerClassName?: string
23
+ contentClassName?: string
24
+ }
25
+
26
+ function defaultFormatValue(value: string) {
27
+ const date = parseDateKey(value)
28
+ if (!date) return value
29
+ return new Intl.DateTimeFormat("en-US", { dateStyle: "medium" }).format(date)
30
+ }
31
+
32
+ function DatePicker({
33
+ value,
34
+ onValueChange,
35
+ placeholder,
36
+ labels,
37
+ disabled = false,
38
+ formatValue = defaultFormatValue,
39
+ triggerClassName,
40
+ contentClassName,
41
+ className,
42
+ ...calendarProps
43
+ }: DatePickerProps) {
44
+ const [open, setOpen] = React.useState(false)
45
+ const hasValue = Boolean(value)
46
+
47
+ const handleSelect = (nextValue: string) => {
48
+ onValueChange?.(nextValue)
49
+ setOpen(false)
50
+ }
51
+
52
+ return (
53
+ <div data-slot="date-picker" className={cn("w-full", className)}>
54
+ <Popover open={open} onOpenChange={setOpen}>
55
+ <PopoverTrigger
56
+ render={
57
+ <Button
58
+ type="button"
59
+ variant="outline"
60
+ disabled={disabled}
61
+ className={cn("w-full justify-start text-left font-normal", !hasValue && "text-muted-foreground", triggerClassName)}
62
+ />
63
+ }
64
+ >
65
+ <CalendarIcon data-icon="inline-start" />
66
+ <span className="min-w-0 flex-1 truncate">
67
+ {hasValue ? formatValue(String(value)) : placeholder ?? labels?.placeholder ?? "Select date"}
68
+ </span>
69
+ </PopoverTrigger>
70
+ <PopoverContent align="start" className={cn("w-auto p-0", contentClassName)}>
71
+ <Calendar value={value} onValueChange={handleSelect} labels={labels} {...calendarProps} />
72
+ </PopoverContent>
73
+ </Popover>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ export { DatePicker }
@@ -0,0 +1,96 @@
1
+ import * as React from "react"
2
+ import { CalendarIcon } from "lucide-react"
3
+
4
+ import { Calendar, type CalendarDateRange, type CalendarProps } from "@/components/calendar/calendar"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
7
+ import { cn } from "@/lib/utils"
8
+ import { parseDateKey } from "./date-utils"
9
+
10
+ export type DateRangePickerValue = CalendarDateRange
11
+
12
+ export type DateRangePickerLabels = CalendarProps["labels"] & {
13
+ placeholder?: string
14
+ }
15
+
16
+ export type DateRangePickerProps = Omit<
17
+ CalendarProps,
18
+ "mode" | "value" | "onValueChange" | "range" | "onRangeChange" | "labels"
19
+ > & {
20
+ value?: DateRangePickerValue
21
+ onValueChange?: (value: DateRangePickerValue) => void
22
+ labels?: DateRangePickerLabels
23
+ placeholder?: string
24
+ disabled?: boolean
25
+ formatValue?: (value: string) => React.ReactNode
26
+ triggerClassName?: string
27
+ contentClassName?: string
28
+ }
29
+
30
+ function defaultFormatValue(value: string) {
31
+ const date = parseDateKey(value)
32
+ if (!date) return value
33
+ return new Intl.DateTimeFormat("en-US", { dateStyle: "medium" }).format(date)
34
+ }
35
+
36
+ function DateRangePicker({
37
+ className,
38
+ value,
39
+ onValueChange,
40
+ labels,
41
+ placeholder,
42
+ disabled = false,
43
+ formatValue = defaultFormatValue,
44
+ triggerClassName,
45
+ contentClassName,
46
+ ...calendarProps
47
+ }: DateRangePickerProps) {
48
+ const [open, setOpen] = React.useState(false)
49
+ const from = value?.from ?? ""
50
+ const to = value?.to ?? ""
51
+ const hasValue = Boolean(from || to)
52
+
53
+ const label = from && to
54
+ ? `${formatValue(from)} - ${formatValue(to)}`
55
+ : from
56
+ ? `${formatValue(from)} - ...`
57
+ : placeholder ?? labels?.placeholder ?? "Select date range"
58
+
59
+ const handleRangeChange = (nextValue: DateRangePickerValue) => {
60
+ onValueChange?.(nextValue)
61
+ if (nextValue.from && nextValue.to) {
62
+ setOpen(false)
63
+ }
64
+ }
65
+
66
+ return (
67
+ <div data-slot="date-range-picker" className={cn("w-full", className)}>
68
+ <Popover open={open} onOpenChange={setOpen}>
69
+ <PopoverTrigger
70
+ render={
71
+ <Button
72
+ type="button"
73
+ variant="outline"
74
+ disabled={disabled}
75
+ className={cn("w-full justify-start text-left font-normal", !hasValue && "text-muted-foreground", triggerClassName)}
76
+ />
77
+ }
78
+ >
79
+ <CalendarIcon data-icon="inline-start" />
80
+ <span className="min-w-0 flex-1 truncate">{label}</span>
81
+ </PopoverTrigger>
82
+ <PopoverContent align="start" className={cn("w-auto p-0", contentClassName)}>
83
+ <Calendar
84
+ mode="range"
85
+ range={value}
86
+ onRangeChange={handleRangeChange}
87
+ labels={labels}
88
+ {...calendarProps}
89
+ />
90
+ </PopoverContent>
91
+ </Popover>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ export { DateRangePicker }
@@ -0,0 +1,89 @@
1
+ const DATE_KEY_REGEXP = /^\d{4}-\d{2}-\d{2}$/
2
+
3
+ function padDatePart(value: number) {
4
+ return String(value).padStart(2, "0")
5
+ }
6
+
7
+ function toDateKey(date: Date) {
8
+ return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
9
+ }
10
+
11
+ function parseDateKey(value?: string | null) {
12
+ if (!value || !DATE_KEY_REGEXP.test(value)) return null
13
+
14
+ const [year, month, day] = value.split("-").map(Number)
15
+ const date = new Date(year, month - 1, day)
16
+
17
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
18
+ return null
19
+ }
20
+
21
+ return date
22
+ }
23
+
24
+ function startOfMonth(date: Date) {
25
+ return new Date(date.getFullYear(), date.getMonth(), 1)
26
+ }
27
+
28
+ function addMonths(date: Date, amount: number) {
29
+ return new Date(date.getFullYear(), date.getMonth() + amount, 1)
30
+ }
31
+
32
+ function isSameMonth(left: Date, right: Date) {
33
+ return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth()
34
+ }
35
+
36
+ function isBeforeDate(left: string, right?: string | null) {
37
+ return Boolean(right && left < right)
38
+ }
39
+
40
+ function isAfterDate(left: string, right?: string | null) {
41
+ return Boolean(right && left > right)
42
+ }
43
+
44
+ function isWithinRange(date: string, from?: string | null, to?: string | null) {
45
+ if (!from || !to) return false
46
+ return date >= from && date <= to
47
+ }
48
+
49
+ function getMonthLabel(date: Date, locale = "en-US") {
50
+ return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(date)
51
+ }
52
+
53
+ function getWeekdayLabels(locale = "en-US", weekStartsOn: 0 | 1 = 1) {
54
+ const baseDate = new Date(2024, 0, weekStartsOn === 1 ? 1 : 7)
55
+
56
+ return Array.from({ length: 7 }, (_, index) => {
57
+ const date = new Date(baseDate)
58
+ date.setDate(baseDate.getDate() + index)
59
+ return new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date)
60
+ })
61
+ }
62
+
63
+ function getMonthDays(month: Date, weekStartsOn: 0 | 1 = 1) {
64
+ const start = startOfMonth(month)
65
+ const dayOfWeek = start.getDay()
66
+ const offset = weekStartsOn === 1 ? (dayOfWeek + 6) % 7 : dayOfWeek
67
+ const firstVisibleDay = new Date(start)
68
+ firstVisibleDay.setDate(start.getDate() - offset)
69
+
70
+ return Array.from({ length: 42 }, (_, index) => {
71
+ const date = new Date(firstVisibleDay)
72
+ date.setDate(firstVisibleDay.getDate() + index)
73
+ return date
74
+ })
75
+ }
76
+
77
+ export {
78
+ addMonths,
79
+ getMonthDays,
80
+ getMonthLabel,
81
+ getWeekdayLabels,
82
+ isAfterDate,
83
+ isBeforeDate,
84
+ isSameMonth,
85
+ isWithinRange,
86
+ parseDateKey,
87
+ startOfMonth,
88
+ toDateKey,
89
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./date-utils"
2
+ export * from "./calendar"
3
+ export * from "./date-picker"
4
+ export * from "./date-range-picker"