@gunjo/ui 0.0.1-alpha.1 → 0.0.1-alpha.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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
@@ -0,0 +1,519 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { addDays, addMonths, addYears, format, type Locale } from "date-fns"
5
+ import { IconCalendar as CalendarIcon } from "@tabler/icons-react";
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { Button } from "../inputs/Button"
9
+ import { Input } from "../inputs/Input"
10
+ import { Calendar } from "./Calendar"
11
+ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../overlay/Popover"
12
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
13
+
14
+ export interface DatePickerProps {
15
+ id?: string
16
+ value?: Date
17
+ onValueChange?: (date: Date | undefined) => void
18
+ placeholder?: string
19
+ dateFormat?: string
20
+ className?: string
21
+ triggerClassName?: string
22
+ editable?: boolean
23
+ disabled?: boolean
24
+ locale?: Locale
25
+ calendarLabel?: string
26
+ todayLabel?: string
27
+ previousLabel?: string
28
+ showTodayButton?: boolean
29
+ closeOnSelect?: boolean
30
+ }
31
+
32
+ type DateSegment = "year" | "month" | "day"
33
+
34
+ function parseIsoDate(value: string): Date | undefined {
35
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim())
36
+ if (!match) return undefined
37
+
38
+ const year = Number(match[1])
39
+ const month = Number(match[2])
40
+ const day = Number(match[3])
41
+ const date = new Date(year, month - 1, day)
42
+
43
+ if (
44
+ date.getFullYear() !== year ||
45
+ date.getMonth() !== month - 1 ||
46
+ date.getDate() !== day
47
+ ) {
48
+ return undefined
49
+ }
50
+
51
+ return date
52
+ }
53
+
54
+ function formatDate(date: Date | undefined, dateFormat: string, locale?: Locale): string {
55
+ if (!date) return ""
56
+ return format(date, dateFormat, locale ? { locale } : undefined)
57
+ }
58
+
59
+ function getTodayLabel(locale?: Locale): string {
60
+ return locale?.code === "ja" ? "今日" : "Today"
61
+ }
62
+
63
+ function getCloseCalendarLabel(locale?: Locale): string {
64
+ return locale?.code === "ja" ? "カレンダーを閉じる" : "Close calendar"
65
+ }
66
+
67
+ function getPreviousLabel(locale?: Locale): string {
68
+ return locale?.code === "ja" ? "直前の日付へ戻る" : "Return to previous date"
69
+ }
70
+
71
+ function isSameCalendarDay(first: Date | undefined, second: Date | undefined): boolean {
72
+ if (!first || !second) return false
73
+ return (
74
+ first.getFullYear() === second.getFullYear() &&
75
+ first.getMonth() === second.getMonth() &&
76
+ first.getDate() === second.getDate()
77
+ )
78
+ }
79
+
80
+ function formatDateInputValue(value: string): string {
81
+ const digits = value.replace(/\D/g, "").slice(0, 8)
82
+
83
+ if (digits.length <= 4) return digits
84
+ if (digits.length <= 6) return `${digits.slice(0, 4)}-${digits.slice(4)}`
85
+ return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6)}`
86
+ }
87
+
88
+ function getInputPositionForDigitCount(digitCount: number): number {
89
+ if (digitCount <= 4) return digitCount
90
+ if (digitCount <= 6) return digitCount + 1
91
+ return digitCount + 2
92
+ }
93
+
94
+ function getDateSegment(position: number): DateSegment {
95
+ if (position <= 4) return "year"
96
+ if (position <= 7) return "month"
97
+ return "day"
98
+ }
99
+
100
+ function getSegmentRange(segment: DateSegment): [number, number] {
101
+ if (segment === "year") return [0, 4]
102
+ if (segment === "month") return [5, 7]
103
+ return [8, 10]
104
+ }
105
+
106
+ function moveDateSegment(segment: DateSegment, direction: 1 | -1): DateSegment {
107
+ const order: DateSegment[] = ["year", "month", "day"]
108
+ const nextIndex = Math.min(Math.max(order.indexOf(segment) + direction, 0), order.length - 1)
109
+ return order[nextIndex]
110
+ }
111
+
112
+ const DatePicker = React.forwardRef<HTMLInputElement, DatePickerProps>(
113
+ (
114
+ {
115
+ id,
116
+ value,
117
+ onValueChange,
118
+ placeholder = "yyyy-mm-dd",
119
+ dateFormat = "yyyy-MM-dd",
120
+ className,
121
+ triggerClassName,
122
+ editable = true,
123
+ disabled,
124
+ locale,
125
+ calendarLabel = "Open calendar",
126
+ todayLabel,
127
+ previousLabel,
128
+ showTodayButton = true,
129
+ closeOnSelect = true,
130
+ },
131
+ ref
132
+ ) => {
133
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
134
+ const [open, setOpen] = React.useState(false)
135
+ const [inputValue, setInputValue] = React.useState(() =>
136
+ formatDate(value, dateFormat, locale)
137
+ )
138
+ const [calendarMonth, setCalendarMonth] = React.useState<Date | undefined>(value)
139
+ const [calendarSelectedDate, setCalendarSelectedDate] = React.useState<Date | undefined>(value)
140
+ const [previousShortcutDate, setPreviousShortcutDate] = React.useState<Date | undefined>()
141
+ const [keepOpenAfterShortcut, setKeepOpenAfterShortcut] = React.useState(false)
142
+ const [invalid, setInvalid] = React.useState(false)
143
+
144
+ React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
145
+
146
+ const calendarReferenceDate = calendarMonth ?? calendarSelectedDate ?? value ?? new Date()
147
+ const calendarStartMonth = React.useMemo(
148
+ () => new Date(calendarReferenceDate.getFullYear() - 10, 0, 1),
149
+ [calendarReferenceDate]
150
+ )
151
+ const calendarEndMonth = React.useMemo(
152
+ () => new Date(calendarReferenceDate.getFullYear() + 10, 11, 31),
153
+ [calendarReferenceDate]
154
+ )
155
+ const calendarButtonLabel = open ? getCloseCalendarLabel(locale) : calendarLabel
156
+
157
+ const selectSegment = React.useCallback((segment: DateSegment) => {
158
+ const [selectionStart, selectionEnd] = getSegmentRange(segment)
159
+ window.requestAnimationFrame(() => {
160
+ inputRef.current?.setSelectionRange(selectionStart, selectionEnd)
161
+ })
162
+ }, [])
163
+
164
+ const openCalendarOnNextFrame = React.useCallback(() => {
165
+ window.requestAnimationFrame(() => {
166
+ setOpen(true)
167
+ inputRef.current?.focus()
168
+ })
169
+ }, [])
170
+
171
+ const keepCalendarOpenOnNextFrame = React.useCallback(() => {
172
+ window.requestAnimationFrame(() => {
173
+ setOpen(true)
174
+ })
175
+ }, [])
176
+
177
+ const handleOpenChange = React.useCallback((nextOpen: boolean) => {
178
+ setOpen(nextOpen)
179
+ if (!nextOpen) {
180
+ setKeepOpenAfterShortcut(false)
181
+ }
182
+ }, [])
183
+
184
+ React.useEffect(() => {
185
+ setInputValue(formatDate(value, dateFormat, locale))
186
+ setCalendarMonth(value)
187
+ setCalendarSelectedDate(value)
188
+ setInvalid(false)
189
+ }, [dateFormat, locale, value])
190
+
191
+ const alignCalendarToInput = React.useCallback(
192
+ (nextInputValue = inputValue) => {
193
+ const normalizedInputValue =
194
+ dateFormat === "yyyy-MM-dd"
195
+ ? formatDateInputValue(nextInputValue)
196
+ : nextInputValue
197
+ const parsedDate = parseIsoDate(normalizedInputValue)
198
+ setCalendarMonth(parsedDate ?? value)
199
+ setCalendarSelectedDate(parsedDate ?? value)
200
+ },
201
+ [dateFormat, inputValue, value]
202
+ )
203
+
204
+ const syncValidInputValue = React.useCallback(
205
+ (nextInputValue: string) => {
206
+ const normalizedInputValue =
207
+ dateFormat === "yyyy-MM-dd"
208
+ ? formatDateInputValue(nextInputValue)
209
+ : nextInputValue
210
+ const trimmedValue = normalizedInputValue.trim()
211
+
212
+ if (trimmedValue === "") {
213
+ setCalendarMonth(undefined)
214
+ setCalendarSelectedDate(undefined)
215
+ onValueChange?.(undefined)
216
+ return
217
+ }
218
+
219
+ const nextDate = parseIsoDate(trimmedValue)
220
+ if (!nextDate) return
221
+
222
+ setCalendarMonth(nextDate)
223
+ setCalendarSelectedDate(nextDate)
224
+ onValueChange?.(nextDate)
225
+ },
226
+ [dateFormat, onValueChange]
227
+ )
228
+
229
+ const commitInputValue = React.useCallback((nextInputValue = inputValue) => {
230
+ const normalizedInputValue =
231
+ dateFormat === "yyyy-MM-dd"
232
+ ? formatDateInputValue(nextInputValue)
233
+ : nextInputValue
234
+ const trimmedValue = normalizedInputValue.trim()
235
+
236
+ if (trimmedValue === "") {
237
+ onValueChange?.(undefined)
238
+ setCalendarMonth(undefined)
239
+ setCalendarSelectedDate(undefined)
240
+ setInvalid(false)
241
+ return
242
+ }
243
+
244
+ const nextDate = parseIsoDate(trimmedValue)
245
+ if (!nextDate) {
246
+ setInvalid(true)
247
+ setInputValue(formatDate(value, dateFormat, locale))
248
+ return
249
+ }
250
+
251
+ setInvalid(false)
252
+ setInputValue(formatDate(nextDate, dateFormat, locale))
253
+ setCalendarMonth(nextDate)
254
+ setCalendarSelectedDate(nextDate)
255
+ onValueChange?.(nextDate)
256
+ }, [dateFormat, inputValue, locale, onValueChange, value])
257
+
258
+ const handleCalendarSelect = React.useCallback(
259
+ (nextDate: Date | undefined) => {
260
+ setInvalid(false)
261
+ setCalendarMonth(nextDate)
262
+ setCalendarSelectedDate(nextDate)
263
+ onValueChange?.(nextDate)
264
+ if (nextDate && closeOnSelect && !keepOpenAfterShortcut) {
265
+ setOpen(false)
266
+ } else if (nextDate) {
267
+ keepCalendarOpenOnNextFrame()
268
+ }
269
+ },
270
+ [closeOnSelect, keepCalendarOpenOnNextFrame, keepOpenAfterShortcut, onValueChange]
271
+ )
272
+
273
+ const handleTodaySelect = React.useCallback(() => {
274
+ const today = new Date()
275
+ const nextInputValue = formatDate(today, dateFormat, locale)
276
+ const currentDate = parseIsoDate(inputValue) ?? calendarSelectedDate ?? value
277
+
278
+ if (currentDate && !isSameCalendarDay(currentDate, today)) {
279
+ setPreviousShortcutDate(currentDate)
280
+ }
281
+ setInvalid(false)
282
+ setInputValue(nextInputValue)
283
+ setCalendarMonth(today)
284
+ setCalendarSelectedDate(today)
285
+ onValueChange?.(today)
286
+ setKeepOpenAfterShortcut(true)
287
+ setOpen(true)
288
+ keepCalendarOpenOnNextFrame()
289
+ }, [calendarSelectedDate, dateFormat, inputValue, keepCalendarOpenOnNextFrame, locale, onValueChange, value])
290
+
291
+ const handlePreviousShortcutSelect = React.useCallback(() => {
292
+ if (!previousShortcutDate) return
293
+
294
+ const nextInputValue = formatDate(previousShortcutDate, dateFormat, locale)
295
+ setInvalid(false)
296
+ setInputValue(nextInputValue)
297
+ setCalendarMonth(previousShortcutDate)
298
+ setCalendarSelectedDate(previousShortcutDate)
299
+ onValueChange?.(previousShortcutDate)
300
+ setPreviousShortcutDate(undefined)
301
+ setKeepOpenAfterShortcut(true)
302
+ setOpen(true)
303
+ keepCalendarOpenOnNextFrame()
304
+ }, [dateFormat, keepCalendarOpenOnNextFrame, locale, onValueChange, previousShortcutDate])
305
+
306
+ const stepInputValue = React.useCallback(
307
+ (direction: 1 | -1, cursorPosition: number) => {
308
+ if (dateFormat !== "yyyy-MM-dd") return false
309
+
310
+ const segment = getDateSegment(cursorPosition)
311
+ const currentDate = parseIsoDate(inputValue) ?? value ?? new Date()
312
+ const nextDate =
313
+ segment === "year"
314
+ ? addYears(currentDate, direction)
315
+ : segment === "month"
316
+ ? addMonths(currentDate, direction)
317
+ : addDays(currentDate, direction)
318
+ const nextInputValue = formatDate(nextDate, dateFormat, locale)
319
+ const [selectionStart, selectionEnd] = getSegmentRange(segment)
320
+
321
+ setInvalid(false)
322
+ setInputValue(nextInputValue)
323
+ setCalendarMonth(nextDate)
324
+ setCalendarSelectedDate(nextDate)
325
+ onValueChange?.(nextDate)
326
+ window.requestAnimationFrame(() => {
327
+ inputRef.current?.setSelectionRange(selectionStart, selectionEnd)
328
+ })
329
+ return true
330
+ },
331
+ [dateFormat, inputValue, locale, onValueChange, value]
332
+ )
333
+
334
+ return (
335
+ <Popover open={open} onOpenChange={handleOpenChange}>
336
+ <PopoverAnchor asChild>
337
+ <div className="relative w-full" data-slot="date-picker">
338
+ <Input
339
+ id={id}
340
+ ref={inputRef}
341
+ value={inputValue}
342
+ onFocus={() => {
343
+ alignCalendarToInput()
344
+ setOpen(true)
345
+ }}
346
+ onChange={(event) => {
347
+ const rawInputValue = event.target.value
348
+ const digitsBeforeCursor = rawInputValue
349
+ .slice(0, event.target.selectionStart ?? rawInputValue.length)
350
+ .replace(/\D/g, "").length
351
+ const nextInputValue =
352
+ dateFormat === "yyyy-MM-dd"
353
+ ? formatDateInputValue(rawInputValue)
354
+ : rawInputValue
355
+ setInputValue(nextInputValue)
356
+ setInvalid(false)
357
+ syncValidInputValue(nextInputValue)
358
+ if (dateFormat === "yyyy-MM-dd") {
359
+ const nextCursorPosition =
360
+ getInputPositionForDigitCount(digitsBeforeCursor)
361
+ window.requestAnimationFrame(() => {
362
+ inputRef.current?.setSelectionRange(
363
+ nextCursorPosition,
364
+ nextCursorPosition
365
+ )
366
+ })
367
+ }
368
+ }}
369
+ onKeyDown={(event) => {
370
+ if (!editable) return
371
+ if (dateFormat === "yyyy-MM-dd" && event.key === "-") {
372
+ event.preventDefault()
373
+ return
374
+ }
375
+ if (event.key === "Enter") {
376
+ event.preventDefault()
377
+ const nextInputValue =
378
+ dateFormat === "yyyy-MM-dd"
379
+ ? formatDateInputValue(event.currentTarget.value)
380
+ : event.currentTarget.value
381
+ setInputValue(nextInputValue)
382
+ commitInputValue(nextInputValue)
383
+ alignCalendarToInput(nextInputValue)
384
+ openCalendarOnNextFrame()
385
+ return
386
+ }
387
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
388
+ const handled = stepInputValue(
389
+ event.key === "ArrowUp" ? 1 : -1,
390
+ event.currentTarget.selectionStart ?? inputValue.length
391
+ )
392
+ if (handled) {
393
+ event.preventDefault()
394
+ }
395
+ return
396
+ }
397
+ if (
398
+ dateFormat === "yyyy-MM-dd" &&
399
+ (event.key === "ArrowLeft" || event.key === "ArrowRight") &&
400
+ !event.altKey &&
401
+ !event.ctrlKey &&
402
+ !event.metaKey &&
403
+ inputValue.length >= 10
404
+ ) {
405
+ const segment = getDateSegment(
406
+ event.currentTarget.selectionStart ?? inputValue.length
407
+ )
408
+ selectSegment(
409
+ moveDateSegment(segment, event.key === "ArrowRight" ? 1 : -1)
410
+ )
411
+ event.preventDefault()
412
+ return
413
+ }
414
+ if (
415
+ dateFormat === "yyyy-MM-dd" &&
416
+ event.key === "Tab" &&
417
+ inputValue.length >= 10
418
+ ) {
419
+ const segment = getDateSegment(
420
+ event.currentTarget.selectionStart ?? inputValue.length
421
+ )
422
+ if (!event.shiftKey && segment !== "day") {
423
+ selectSegment(moveDateSegment(segment, 1))
424
+ event.preventDefault()
425
+ return
426
+ }
427
+ if (event.shiftKey && segment !== "year") {
428
+ selectSegment(moveDateSegment(segment, -1))
429
+ event.preventDefault()
430
+ }
431
+ }
432
+ }}
433
+ placeholder={placeholder}
434
+ readOnly={!editable}
435
+ disabled={disabled}
436
+ inputMode={dateFormat === "yyyy-MM-dd" ? "numeric" : undefined}
437
+ aria-invalid={invalid || undefined}
438
+ className={cn(
439
+ "w-full pr-10",
440
+ !editable && "cursor-pointer",
441
+ triggerClassName
442
+ )}
443
+ />
444
+ <Tooltip>
445
+ <TooltipTrigger asChild>
446
+ <PopoverTrigger asChild>
447
+ <Button
448
+ type="button"
449
+ variant="ghost"
450
+ disabled={disabled}
451
+ className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 p-0 text-muted-foreground hover:text-foreground"
452
+ aria-label={calendarButtonLabel}
453
+ >
454
+ <CalendarIcon className="h-4 w-4" />
455
+ </Button>
456
+ </PopoverTrigger>
457
+ </TooltipTrigger>
458
+ <TooltipContent>{calendarButtonLabel}</TooltipContent>
459
+ </Tooltip>
460
+ </div>
461
+ </PopoverAnchor>
462
+ <PopoverContent
463
+ className={cn(
464
+ "w-auto overflow-visible p-0",
465
+ className
466
+ )}
467
+ align="end"
468
+ sideOffset={8}
469
+ onOpenAutoFocus={(event) => event.preventDefault()}
470
+ >
471
+ <Calendar
472
+ className={showTodayButton ? "rounded-b-none border-0 p-3" : undefined}
473
+ mode="single"
474
+ selected={calendarSelectedDate}
475
+ onSelect={handleCalendarSelect}
476
+ month={calendarMonth}
477
+ onMonthChange={setCalendarMonth}
478
+ startMonth={calendarStartMonth}
479
+ endMonth={calendarEndMonth}
480
+ locale={locale}
481
+ />
482
+ {showTodayButton ? (
483
+ <div className="flex items-stretch justify-between gap-2 border-t bg-card p-1.5">
484
+ {previousShortcutDate ? (
485
+ <Button
486
+ type="button"
487
+ variant="ghost"
488
+ className="h-10 w-fit max-w-[calc(100%-3.5rem)] flex-col items-start justify-center gap-0 px-2 py-1 text-left"
489
+ onClick={handlePreviousShortcutSelect}
490
+ onMouseDown={(event) => event.preventDefault()}
491
+ >
492
+ <span className="block max-w-full truncate text-[11px] leading-4 text-muted-foreground">
493
+ {previousLabel ?? getPreviousLabel(locale)}
494
+ </span>
495
+ <span className="block max-w-full truncate font-mono text-[11px] leading-4 text-foreground">
496
+ {formatDate(previousShortcutDate, dateFormat, locale)}
497
+ </span>
498
+ </Button>
499
+ ) : null}
500
+ <Button
501
+ type="button"
502
+ variant="secondary"
503
+ size="sm"
504
+ className="ml-auto h-10 shrink-0 px-3 text-xs"
505
+ onClick={handleTodaySelect}
506
+ onMouseDown={(event) => event.preventDefault()}
507
+ >
508
+ {todayLabel ?? getTodayLabel(locale)}
509
+ </Button>
510
+ </div>
511
+ ) : null}
512
+ </PopoverContent>
513
+ </Popover>
514
+ )
515
+ }
516
+ )
517
+ DatePicker.displayName = "DatePicker"
518
+
519
+ export { DatePicker }