@gunjo/ui 0.0.1-alpha.0 → 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,878 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { addDays, addMonths, addYears, differenceInCalendarDays, format, type Locale } from "date-fns"
5
+ import { IconCalendar as CalendarIcon } from "@tabler/icons-react";
6
+ import type { DateRange } from "react-day-picker"
7
+
8
+ import { cn } from "../../lib/utils"
9
+ import { Button } from "../inputs/Button"
10
+ import { Input } from "../inputs/Input"
11
+ import { Calendar } from "./Calendar"
12
+ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../overlay/Popover"
13
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
14
+
15
+ export interface DateRangePickerProps {
16
+ id?: string
17
+ value?: DateRange
18
+ onValueChange?: (range: DateRange | undefined) => void
19
+ placeholder?: string
20
+ dateFormat?: string
21
+ numberOfMonths?: number
22
+ className?: string
23
+ triggerClassName?: string
24
+ editable?: boolean
25
+ disabled?: boolean
26
+ locale?: Locale
27
+ calendarLabel?: string
28
+ todayLabel?: string
29
+ previousLabel?: string
30
+ showTodayButton?: boolean
31
+ closeOnSelect?: boolean
32
+ maxRangeDays?: number
33
+ responsiveMonths?: boolean
34
+ }
35
+
36
+ function parseIsoDate(value: string): Date | undefined {
37
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim())
38
+ if (!match) return undefined
39
+
40
+ const year = Number(match[1])
41
+ const month = Number(match[2])
42
+ const day = Number(match[3])
43
+ const date = new Date(year, month - 1, day)
44
+
45
+ if (
46
+ date.getFullYear() !== year ||
47
+ date.getMonth() !== month - 1 ||
48
+ date.getDate() !== day
49
+ ) {
50
+ return undefined
51
+ }
52
+
53
+ return date
54
+ }
55
+
56
+ function formatDate(date: Date | undefined, dateFormat: string, locale?: Locale): string {
57
+ if (!date) return ""
58
+ return format(date, dateFormat, locale ? { locale } : undefined)
59
+ }
60
+
61
+ function formatRange(range: DateRange | undefined, dateFormat: string, locale?: Locale): string {
62
+ if (!range?.from) return ""
63
+ if (!range.to) return `${formatDate(range.from, dateFormat, locale)} - `
64
+ return `${formatDate(range.from, dateFormat, locale)} - ${formatDate(range.to, dateFormat, locale)}`
65
+ }
66
+
67
+ function getTodayLabel(locale?: Locale): string {
68
+ return locale?.code === "ja" ? "今日" : "Today"
69
+ }
70
+
71
+ function getCloseCalendarLabel(locale?: Locale): string {
72
+ return locale?.code === "ja" ? "カレンダーを閉じる" : "Close calendar"
73
+ }
74
+
75
+ function getPreviousLabel(locale?: Locale): string {
76
+ return locale?.code === "ja" ? "直前の期間へ戻る" : "Return to previous range"
77
+ }
78
+
79
+ function isSameCalendarDay(first: Date | undefined, second: Date | undefined): boolean {
80
+ if (!first || !second) return false
81
+ return (
82
+ first.getFullYear() === second.getFullYear() &&
83
+ first.getMonth() === second.getMonth() &&
84
+ first.getDate() === second.getDate()
85
+ )
86
+ }
87
+
88
+ function isSameRange(first: DateRange | undefined, second: DateRange | undefined): boolean {
89
+ return (
90
+ isSameCalendarDay(first?.from, second?.from) &&
91
+ isSameCalendarDay(first?.to, second?.to)
92
+ )
93
+ }
94
+
95
+ function normalizeRange(range: DateRange | undefined): DateRange | undefined {
96
+ if (!range?.from) return undefined
97
+ if (!range.to) return { from: range.from }
98
+ if (range.from <= range.to) return range
99
+ return { from: range.to, to: range.from }
100
+ }
101
+
102
+ type RangeValidationReason = "format" | "order" | "length"
103
+
104
+ function getRangeValidation(
105
+ range: DateRange | undefined,
106
+ maxRangeDays?: number
107
+ ): RangeValidationReason | undefined {
108
+ if (!range?.from || !range.to) return undefined
109
+ if (range.from > range.to) return "order"
110
+ if (
111
+ typeof maxRangeDays === "number" &&
112
+ maxRangeDays > 0 &&
113
+ differenceInCalendarDays(range.to, range.from) + 1 > maxRangeDays
114
+ ) {
115
+ return "length"
116
+ }
117
+ return undefined
118
+ }
119
+
120
+ function getRangeValidationMessage(
121
+ reason: RangeValidationReason | undefined,
122
+ locale?: Locale,
123
+ maxRangeDays?: number
124
+ ): string | undefined {
125
+ if (!reason) return undefined
126
+ if (locale?.code === "ja") {
127
+ if (reason === "format") return "日付は yyyy-mm-dd - yyyy-mm-dd 形式で入力してください。"
128
+ if (reason === "order") return "終了日は開始日以降を指定してください。"
129
+ return `期間は最大${maxRangeDays ?? 0}日以内で指定してください。`
130
+ }
131
+ if (reason === "format") return "Enter dates in yyyy-mm-dd - yyyy-mm-dd format."
132
+ if (reason === "order") return "End date must be the same as or after the start date."
133
+ return `Choose a range of ${maxRangeDays ?? 0} days or less.`
134
+ }
135
+
136
+ function parseRangeInput(value: string): DateRange | undefined {
137
+ const matches = value.match(/\d{4}-\d{2}-\d{2}/g)
138
+ if (!matches?.length) return undefined
139
+
140
+ const from = parseIsoDate(matches[0])
141
+ const to = matches[1] ? parseIsoDate(matches[1]) : undefined
142
+ return from ? { from, to } : undefined
143
+ }
144
+
145
+ type RangeDateSegment =
146
+ | "fromYear"
147
+ | "fromMonth"
148
+ | "fromDay"
149
+ | "toYear"
150
+ | "toMonth"
151
+ | "toDay"
152
+
153
+ type ActiveRangeEndpoint = "from" | "to"
154
+
155
+ const rangeDateSegmentOrder: RangeDateSegment[] = [
156
+ "fromYear",
157
+ "fromMonth",
158
+ "fromDay",
159
+ "toYear",
160
+ "toMonth",
161
+ "toDay",
162
+ ]
163
+
164
+ function formatSingleDateInputValue(value: string): string {
165
+ if (value.includes("-")) {
166
+ const [yearValue = "", monthValue = "", ...dayValues] = value.split("-")
167
+ const yearDigits = yearValue.replace(/\D/g, "")
168
+ const year = yearDigits.slice(0, 4)
169
+ const monthDigits = `${yearDigits.slice(4)}${monthValue.replace(/\D/g, "")}`
170
+ const month = monthDigits.slice(0, 2)
171
+ const dayDigits = `${monthDigits.slice(2)}${dayValues.join("").replace(/\D/g, "")}`
172
+ const day = dayDigits.slice(0, 2)
173
+
174
+ if (value.split("-").length >= 3 || dayDigits.length > 0) return `${year}-${month}-${day}`
175
+ return `${year}-${month}`
176
+ }
177
+
178
+ const digits = value.replace(/\D/g, "").slice(0, 8)
179
+
180
+ if (digits.length <= 4) return digits
181
+ if (digits.length <= 6) return `${digits.slice(0, 4)}-${digits.slice(4)}`
182
+ return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6)}`
183
+ }
184
+
185
+ function formatDateInputValue(value: string): string {
186
+ const parts = value.split(/\s+-\s+/)
187
+ if (parts.length > 1) {
188
+ const fromValue = formatSingleDateInputValue(parts[0] ?? "")
189
+ const toValue = formatSingleDateInputValue(parts.slice(1).join(" - "))
190
+ return toValue ? `${fromValue} - ${toValue}` : `${fromValue} - `
191
+ }
192
+
193
+ const digits = value.replace(/\D/g, "").slice(0, 16)
194
+ if (digits.length <= 8) return formatSingleDateInputValue(digits)
195
+ return `${formatSingleDateInputValue(digits.slice(0, 8))} - ${formatSingleDateInputValue(digits.slice(8))}`
196
+ }
197
+
198
+ function getSingleDateInputPositionForDigitCount(digitCount: number): number {
199
+ if (digitCount <= 4) return digitCount
200
+ if (digitCount <= 6) return digitCount + 1
201
+ return digitCount + 2
202
+ }
203
+
204
+ function getRangeInputPositionForDigitCount(digitCount: number): number {
205
+ if (digitCount <= 8) return getSingleDateInputPositionForDigitCount(digitCount)
206
+ return 13 + getSingleDateInputPositionForDigitCount(digitCount - 8)
207
+ }
208
+
209
+ function getRangeDateSegment(position: number): RangeDateSegment {
210
+ if (position <= 4) return "fromYear"
211
+ if (position <= 7) return "fromMonth"
212
+ if (position <= 10) return "fromDay"
213
+ if (position <= 17) return "toYear"
214
+ if (position <= 20) return "toMonth"
215
+ return "toDay"
216
+ }
217
+
218
+ function getRangeDateSegmentRange(segment: RangeDateSegment): [number, number] {
219
+ if (segment === "fromYear") return [0, 4]
220
+ if (segment === "fromMonth") return [5, 7]
221
+ if (segment === "fromDay") return [8, 10]
222
+ if (segment === "toYear") return [13, 17]
223
+ if (segment === "toMonth") return [18, 20]
224
+ return [21, 23]
225
+ }
226
+
227
+ function moveRangeDateSegment(segment: RangeDateSegment, direction: 1 | -1): RangeDateSegment {
228
+ const nextIndex = Math.min(
229
+ Math.max(rangeDateSegmentOrder.indexOf(segment) + direction, 0),
230
+ rangeDateSegmentOrder.length - 1
231
+ )
232
+ return rangeDateSegmentOrder[nextIndex]
233
+ }
234
+
235
+ function stepDateBySegment(date: Date, segment: RangeDateSegment, direction: 1 | -1): Date {
236
+ if (segment.endsWith("Year")) return addYears(date, direction)
237
+ if (segment.endsWith("Month")) return addMonths(date, direction)
238
+ return addDays(date, direction)
239
+ }
240
+
241
+ function getRangeEndpoint(segment: RangeDateSegment): ActiveRangeEndpoint {
242
+ return segment.startsWith("from") ? "from" : "to"
243
+ }
244
+
245
+ function getActiveEndpointLabel(endpoint: ActiveRangeEndpoint, locale?: Locale): string {
246
+ if (locale?.code === "ja") return endpoint === "from" ? "開始日を編集中" : "終了日を編集中"
247
+ return endpoint === "from" ? "Editing start date" : "Editing end date"
248
+ }
249
+
250
+ function useSmallCalendarViewport() {
251
+ const [isSmallViewport, setIsSmallViewport] = React.useState(false)
252
+
253
+ React.useEffect(() => {
254
+ const mediaQuery = window.matchMedia("(max-width: 639px)")
255
+ const update = () => setIsSmallViewport(mediaQuery.matches)
256
+
257
+ update()
258
+ mediaQuery.addEventListener("change", update)
259
+ return () => mediaQuery.removeEventListener("change", update)
260
+ }, [])
261
+
262
+ return isSmallViewport
263
+ }
264
+
265
+ const DateRangePicker = React.forwardRef<HTMLInputElement, DateRangePickerProps>(
266
+ (
267
+ {
268
+ id,
269
+ value,
270
+ onValueChange,
271
+ placeholder = "yyyy-mm-dd - yyyy-mm-dd",
272
+ dateFormat = "yyyy-MM-dd",
273
+ numberOfMonths = 2,
274
+ className,
275
+ triggerClassName,
276
+ editable = true,
277
+ disabled,
278
+ locale,
279
+ calendarLabel = "Open calendar",
280
+ todayLabel,
281
+ previousLabel,
282
+ showTodayButton = true,
283
+ closeOnSelect = true,
284
+ maxRangeDays,
285
+ responsiveMonths = true,
286
+ },
287
+ ref
288
+ ) => {
289
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
290
+ const [open, setOpen] = React.useState(false)
291
+ const [inputValue, setInputValue] = React.useState(() =>
292
+ formatRange(value, dateFormat, locale)
293
+ )
294
+ const [calendarMonth, setCalendarMonth] = React.useState<Date | undefined>(value?.from)
295
+ const [calendarSelectedRange, setCalendarSelectedRange] = React.useState<DateRange | undefined>(value)
296
+ const [previousShortcutRange, setPreviousShortcutRange] = React.useState<DateRange | undefined>()
297
+ const [activeEndpoint, setActiveEndpoint] = React.useState<ActiveRangeEndpoint>("from")
298
+ const [keepOpenAfterShortcut, setKeepOpenAfterShortcut] = React.useState(false)
299
+ const [calendarSelectionAnchor, setCalendarSelectionAnchor] = React.useState<Date | undefined>()
300
+ const [validationReason, setValidationReason] = React.useState<RangeValidationReason | undefined>()
301
+ const isSmallCalendarViewport = useSmallCalendarViewport()
302
+ const calendarNumberOfMonths =
303
+ responsiveMonths && isSmallCalendarViewport
304
+ ? 1
305
+ : numberOfMonths
306
+ const useCompactMultiMonthCalendar = Number(calendarNumberOfMonths) > 1
307
+
308
+ React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
309
+
310
+ const calendarReferenceDate = calendarMonth ?? calendarSelectedRange?.from ?? value?.from ?? new Date()
311
+ const calendarStartMonth = React.useMemo(
312
+ () => new Date(calendarReferenceDate.getFullYear() - 10, 0, 1),
313
+ [calendarReferenceDate]
314
+ )
315
+ const calendarEndMonth = React.useMemo(
316
+ () => new Date(calendarReferenceDate.getFullYear() + 10, 11, 31),
317
+ [calendarReferenceDate]
318
+ )
319
+ const calendarButtonLabel = open ? getCloseCalendarLabel(locale) : calendarLabel
320
+
321
+ const selectSegment = React.useCallback((segment: RangeDateSegment) => {
322
+ const [selectionStart, selectionEnd] = getRangeDateSegmentRange(segment)
323
+ setActiveEndpoint(getRangeEndpoint(segment))
324
+ window.requestAnimationFrame(() => {
325
+ inputRef.current?.setSelectionRange(selectionStart, selectionEnd)
326
+ })
327
+ }, [])
328
+
329
+ const updateActiveEndpointFromInput = React.useCallback((position: number) => {
330
+ setActiveEndpoint(getRangeEndpoint(getRangeDateSegment(position)))
331
+ }, [])
332
+
333
+ const keepCalendarOpenOnNextFrame = React.useCallback(() => {
334
+ window.requestAnimationFrame(() => {
335
+ setOpen(true)
336
+ })
337
+ }, [])
338
+
339
+ const openCalendarOnNextFrame = React.useCallback(() => {
340
+ window.requestAnimationFrame(() => {
341
+ setOpen(true)
342
+ inputRef.current?.focus()
343
+ })
344
+ }, [])
345
+
346
+ const handleOpenChange = React.useCallback((nextOpen: boolean) => {
347
+ setOpen(nextOpen)
348
+ if (!nextOpen) {
349
+ setKeepOpenAfterShortcut(false)
350
+ setCalendarSelectionAnchor(undefined)
351
+ }
352
+ }, [])
353
+
354
+ React.useEffect(() => {
355
+ setInputValue(formatRange(value, dateFormat, locale))
356
+ setCalendarMonth(value?.from)
357
+ setCalendarSelectedRange(value)
358
+ setValidationReason(undefined)
359
+ }, [dateFormat, locale, value])
360
+
361
+ const alignCalendarToInput = React.useCallback(
362
+ (nextInputValue = inputValue) => {
363
+ const normalizedInputValue =
364
+ dateFormat === "yyyy-MM-dd"
365
+ ? formatDateInputValue(nextInputValue)
366
+ : nextInputValue
367
+ const parsedRange = parseRangeInput(normalizedInputValue)
368
+ setCalendarMonth(parsedRange?.from ?? value?.from)
369
+ const validation = getRangeValidation(parsedRange, maxRangeDays)
370
+ setCalendarSelectedRange(validation === "order" ? value : parsedRange ?? value)
371
+ },
372
+ [dateFormat, inputValue, maxRangeDays, value]
373
+ )
374
+
375
+ const syncValidInputValue = React.useCallback(
376
+ (nextInputValue: string) => {
377
+ const normalizedInputValue =
378
+ dateFormat === "yyyy-MM-dd"
379
+ ? formatDateInputValue(nextInputValue)
380
+ : nextInputValue
381
+ const trimmedValue = normalizedInputValue.trim()
382
+
383
+ if (trimmedValue === "") {
384
+ setCalendarMonth(undefined)
385
+ setCalendarSelectedRange(undefined)
386
+ onValueChange?.(undefined)
387
+ return
388
+ }
389
+
390
+ const nextRange = parseRangeInput(trimmedValue)
391
+ if (!nextRange) return
392
+
393
+ const validation = getRangeValidation(nextRange, maxRangeDays)
394
+ setCalendarMonth(nextRange.from)
395
+ setValidationReason(validation)
396
+ setCalendarSelectedRange(validation === "order" ? value : nextRange)
397
+ if (!validation && nextRange.from && nextRange.to) {
398
+ onValueChange?.(nextRange)
399
+ }
400
+ },
401
+ [dateFormat, maxRangeDays, onValueChange, value]
402
+ )
403
+
404
+ const commitInputValue = React.useCallback((nextInputValue = inputValue) => {
405
+ const normalizedInputValue =
406
+ dateFormat === "yyyy-MM-dd"
407
+ ? formatDateInputValue(nextInputValue)
408
+ : nextInputValue
409
+ const trimmedValue = normalizedInputValue.trim()
410
+
411
+ if (trimmedValue === "") {
412
+ onValueChange?.(undefined)
413
+ setCalendarMonth(undefined)
414
+ setCalendarSelectedRange(undefined)
415
+ setValidationReason(undefined)
416
+ return
417
+ }
418
+
419
+ const nextRange = parseRangeInput(trimmedValue)
420
+ if (!nextRange) {
421
+ setValidationReason("format")
422
+ return
423
+ }
424
+ const validation = getRangeValidation(nextRange, maxRangeDays)
425
+ if (validation) {
426
+ setValidationReason(validation)
427
+ setCalendarMonth(nextRange.from)
428
+ setCalendarSelectedRange(validation === "order" ? value : nextRange)
429
+ return
430
+ }
431
+
432
+ setValidationReason(undefined)
433
+ setInputValue(formatRange(nextRange, dateFormat, locale))
434
+ setCalendarMonth(nextRange.from)
435
+ setCalendarSelectedRange(nextRange)
436
+ onValueChange?.(nextRange)
437
+ }, [dateFormat, inputValue, locale, maxRangeDays, onValueChange, value])
438
+
439
+ const handleCalendarSelect = React.useCallback(
440
+ (_nextRange: DateRange | undefined, triggerDate?: Date) => {
441
+ if (!triggerDate) return
442
+
443
+ const anchor = calendarSelectionAnchor ?? (
444
+ calendarSelectedRange?.from && !calendarSelectedRange.to
445
+ ? calendarSelectedRange.from
446
+ : undefined
447
+ )
448
+ const isCompletingRange = Boolean(anchor)
449
+ const normalizedRange = isCompletingRange
450
+ ? normalizeRange({ from: anchor, to: triggerDate })
451
+ : { from: triggerDate }
452
+ const validation = getRangeValidation(normalizedRange, maxRangeDays)
453
+ setValidationReason(validation)
454
+ setCalendarMonth(normalizedRange?.from)
455
+ setCalendarSelectedRange(normalizedRange)
456
+ setInputValue(formatRange(normalizedRange, dateFormat, locale))
457
+
458
+ if (!isCompletingRange) {
459
+ setCalendarSelectionAnchor(triggerDate)
460
+ setActiveEndpoint("to")
461
+ onValueChange?.(normalizedRange)
462
+ keepCalendarOpenOnNextFrame()
463
+ return
464
+ }
465
+
466
+ setCalendarSelectionAnchor(undefined)
467
+ setActiveEndpoint("to")
468
+ if (!validation) {
469
+ onValueChange?.(normalizedRange)
470
+ }
471
+
472
+ if (!validation && normalizedRange?.from && normalizedRange.to && closeOnSelect && !keepOpenAfterShortcut) {
473
+ setOpen(false)
474
+ } else if (normalizedRange?.from) {
475
+ keepCalendarOpenOnNextFrame()
476
+ }
477
+ },
478
+ [calendarSelectedRange, calendarSelectionAnchor, closeOnSelect, dateFormat, keepCalendarOpenOnNextFrame, keepOpenAfterShortcut, locale, maxRangeDays, onValueChange]
479
+ )
480
+
481
+ const handleTodaySelect = React.useCallback(() => {
482
+ const today = new Date()
483
+ const todayRange = { from: today, to: today }
484
+ const currentRange = parseRangeInput(inputValue) ?? calendarSelectedRange ?? value
485
+
486
+ if (currentRange && !isSameRange(currentRange, todayRange)) {
487
+ setPreviousShortcutRange(currentRange)
488
+ }
489
+ setValidationReason(undefined)
490
+ setInputValue(formatRange(todayRange, dateFormat, locale))
491
+ setCalendarMonth(today)
492
+ setCalendarSelectedRange(todayRange)
493
+ setCalendarSelectionAnchor(undefined)
494
+ onValueChange?.(todayRange)
495
+ setKeepOpenAfterShortcut(true)
496
+ setOpen(true)
497
+ keepCalendarOpenOnNextFrame()
498
+ }, [calendarSelectedRange, dateFormat, inputValue, keepCalendarOpenOnNextFrame, locale, onValueChange, value])
499
+
500
+ const handlePreviousShortcutSelect = React.useCallback(() => {
501
+ if (!previousShortcutRange?.from) return
502
+
503
+ setValidationReason(undefined)
504
+ setInputValue(formatRange(previousShortcutRange, dateFormat, locale))
505
+ setCalendarMonth(previousShortcutRange.from)
506
+ setCalendarSelectedRange(previousShortcutRange)
507
+ setCalendarSelectionAnchor(undefined)
508
+ onValueChange?.(previousShortcutRange)
509
+ setPreviousShortcutRange(undefined)
510
+ setKeepOpenAfterShortcut(true)
511
+ setOpen(true)
512
+ keepCalendarOpenOnNextFrame()
513
+ }, [dateFormat, keepCalendarOpenOnNextFrame, locale, onValueChange, previousShortcutRange])
514
+
515
+ const handleInputChange = React.useCallback(
516
+ (event: React.ChangeEvent<HTMLInputElement>) => {
517
+ const rawInputValue = event.target.value
518
+ const digitsBeforeCursor = rawInputValue
519
+ .slice(0, event.target.selectionStart ?? rawInputValue.length)
520
+ .replace(/\D/g, "").length
521
+ const nextInputValue =
522
+ dateFormat === "yyyy-MM-dd"
523
+ ? formatDateInputValue(rawInputValue)
524
+ : rawInputValue
525
+ setInputValue(nextInputValue)
526
+ syncValidInputValue(nextInputValue)
527
+ if (dateFormat === "yyyy-MM-dd") {
528
+ const nextCursorPosition =
529
+ getRangeInputPositionForDigitCount(digitsBeforeCursor)
530
+ window.requestAnimationFrame(() => {
531
+ inputRef.current?.setSelectionRange(
532
+ nextCursorPosition,
533
+ nextCursorPosition
534
+ )
535
+ })
536
+ }
537
+ },
538
+ [dateFormat, syncValidInputValue]
539
+ )
540
+
541
+ const stepInputValue = React.useCallback(
542
+ (direction: 1 | -1, cursorPosition: number) => {
543
+ if (dateFormat !== "yyyy-MM-dd") return false
544
+
545
+ const segment = getRangeDateSegment(cursorPosition)
546
+ const currentRange = parseRangeInput(inputValue) ?? value
547
+ const fallbackDate = new Date()
548
+ const from = currentRange?.from ?? fallbackDate
549
+ const to = currentRange?.to ?? currentRange?.from ?? fallbackDate
550
+ let nextFrom = segment.startsWith("from")
551
+ ? stepDateBySegment(from, segment, direction)
552
+ : from
553
+ let nextTo = segment.startsWith("to")
554
+ ? stepDateBySegment(to, segment, direction)
555
+ : to
556
+
557
+ if (nextFrom > nextTo) {
558
+ if (segment.startsWith("from")) {
559
+ nextTo = nextFrom
560
+ } else {
561
+ nextFrom = nextTo
562
+ }
563
+ }
564
+ if (typeof maxRangeDays === "number" && maxRangeDays > 0) {
565
+ const maxOffset = maxRangeDays - 1
566
+ if (differenceInCalendarDays(nextTo, nextFrom) > maxOffset) {
567
+ if (segment.startsWith("from")) {
568
+ nextTo = addDays(nextFrom, maxOffset)
569
+ } else {
570
+ nextFrom = addDays(nextTo, -maxOffset)
571
+ }
572
+ }
573
+ }
574
+ const nextRange = { from: nextFrom, to: nextTo }
575
+
576
+ const nextInputValue = formatRange(nextRange, dateFormat, locale)
577
+ const [selectionStart, selectionEnd] = getRangeDateSegmentRange(segment)
578
+
579
+ setValidationReason(undefined)
580
+ setInputValue(nextInputValue)
581
+ setCalendarMonth(nextRange.from)
582
+ setCalendarSelectedRange(nextRange)
583
+ if (nextRange.from && nextRange.to) {
584
+ onValueChange?.(nextRange)
585
+ }
586
+ window.requestAnimationFrame(() => {
587
+ inputRef.current?.setSelectionRange(selectionStart, selectionEnd)
588
+ })
589
+ return true
590
+ },
591
+ [dateFormat, inputValue, locale, maxRangeDays, onValueChange, value]
592
+ )
593
+
594
+ const deleteInputValue = React.useCallback(
595
+ (key: "Backspace" | "Delete", selectionStart: number, selectionEnd: number) => {
596
+ if (dateFormat !== "yyyy-MM-dd") return false
597
+
598
+ let removeStart = selectionStart
599
+ let removeEnd = selectionEnd
600
+
601
+ if (removeStart === removeEnd) {
602
+ if (key === "Backspace") {
603
+ let cursor = removeStart - 1
604
+ while (cursor >= 0 && !/\d/.test(inputValue[cursor] ?? "")) {
605
+ cursor -= 1
606
+ }
607
+ if (cursor < 0) return true
608
+ removeStart = cursor
609
+ removeEnd = cursor + 1
610
+ } else {
611
+ let cursor = removeEnd
612
+ while (cursor < inputValue.length && !/\d/.test(inputValue[cursor] ?? "")) {
613
+ cursor += 1
614
+ }
615
+ if (cursor >= inputValue.length) return true
616
+ removeStart = cursor
617
+ removeEnd = cursor + 1
618
+ }
619
+ }
620
+
621
+ const nextInputValue = `${inputValue.slice(0, removeStart)}${inputValue.slice(removeEnd)}`
622
+ const parsedRange = parseRangeInput(nextInputValue)
623
+
624
+ setInputValue(nextInputValue)
625
+ setValidationReason(undefined)
626
+ if (parsedRange) {
627
+ const validation = getRangeValidation(parsedRange, maxRangeDays)
628
+ setCalendarMonth(parsedRange.from)
629
+ setValidationReason(validation)
630
+ setCalendarSelectedRange(validation === "order" ? value : parsedRange)
631
+ if (!validation && parsedRange.from && parsedRange.to) {
632
+ onValueChange?.(parsedRange)
633
+ }
634
+ }
635
+ window.requestAnimationFrame(() => {
636
+ inputRef.current?.setSelectionRange(removeStart, removeStart)
637
+ })
638
+ return true
639
+ },
640
+ [dateFormat, inputValue, maxRangeDays, onValueChange, value]
641
+ )
642
+
643
+ const validationMessage = getRangeValidationMessage(validationReason, locale, maxRangeDays)
644
+
645
+ return (
646
+ <Popover open={open} onOpenChange={handleOpenChange}>
647
+ <PopoverAnchor asChild>
648
+ <div className="relative w-full" data-slot="date-range-picker">
649
+ <Input
650
+ id={id}
651
+ ref={inputRef}
652
+ value={inputValue}
653
+ onFocus={() => {
654
+ alignCalendarToInput()
655
+ updateActiveEndpointFromInput(inputRef.current?.selectionStart ?? 0)
656
+ setOpen(true)
657
+ }}
658
+ onClick={(event) => {
659
+ updateActiveEndpointFromInput(event.currentTarget.selectionStart ?? 0)
660
+ }}
661
+ onKeyUp={(event) => {
662
+ updateActiveEndpointFromInput(event.currentTarget.selectionStart ?? 0)
663
+ }}
664
+ onChange={handleInputChange}
665
+ onKeyDown={(event) => {
666
+ if (!editable) return
667
+ if (dateFormat === "yyyy-MM-dd" && event.key === "-") {
668
+ event.preventDefault()
669
+ return
670
+ }
671
+ if (event.key === "Backspace" || event.key === "Delete") {
672
+ const handled = deleteInputValue(
673
+ event.key,
674
+ event.currentTarget.selectionStart ?? inputValue.length,
675
+ event.currentTarget.selectionEnd ?? inputValue.length
676
+ )
677
+ if (handled) {
678
+ event.preventDefault()
679
+ }
680
+ return
681
+ }
682
+ if (event.key === "Enter") {
683
+ event.preventDefault()
684
+ const nextInputValue =
685
+ dateFormat === "yyyy-MM-dd"
686
+ ? formatDateInputValue(event.currentTarget.value)
687
+ : event.currentTarget.value
688
+ setInputValue(nextInputValue)
689
+ commitInputValue(nextInputValue)
690
+ alignCalendarToInput(nextInputValue)
691
+ openCalendarOnNextFrame()
692
+ return
693
+ }
694
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
695
+ const handled = stepInputValue(
696
+ event.key === "ArrowUp" ? 1 : -1,
697
+ event.currentTarget.selectionStart ?? inputValue.length
698
+ )
699
+ if (handled) {
700
+ event.preventDefault()
701
+ }
702
+ return
703
+ }
704
+ if (
705
+ dateFormat === "yyyy-MM-dd" &&
706
+ (event.key === "ArrowLeft" || event.key === "ArrowRight") &&
707
+ !event.altKey &&
708
+ !event.ctrlKey &&
709
+ !event.metaKey &&
710
+ inputValue.length >= 23
711
+ ) {
712
+ const segment = getRangeDateSegment(
713
+ event.currentTarget.selectionStart ?? inputValue.length
714
+ )
715
+ selectSegment(
716
+ moveRangeDateSegment(segment, event.key === "ArrowRight" ? 1 : -1)
717
+ )
718
+ event.preventDefault()
719
+ return
720
+ }
721
+ if (
722
+ dateFormat === "yyyy-MM-dd" &&
723
+ event.key === "Tab" &&
724
+ inputValue.length >= 23
725
+ ) {
726
+ const segment = getRangeDateSegment(
727
+ event.currentTarget.selectionStart ?? inputValue.length
728
+ )
729
+ if (!event.shiftKey && segment !== "toDay") {
730
+ selectSegment(moveRangeDateSegment(segment, 1))
731
+ event.preventDefault()
732
+ return
733
+ }
734
+ if (event.shiftKey && segment !== "fromYear") {
735
+ selectSegment(moveRangeDateSegment(segment, -1))
736
+ event.preventDefault()
737
+ }
738
+ }
739
+ }}
740
+ placeholder={placeholder}
741
+ readOnly={!editable}
742
+ disabled={disabled}
743
+ inputMode={dateFormat === "yyyy-MM-dd" ? "numeric" : undefined}
744
+ aria-invalid={Boolean(validationReason) || undefined}
745
+ className={cn(
746
+ "w-full pr-10",
747
+ !editable && "cursor-pointer",
748
+ triggerClassName
749
+ )}
750
+ />
751
+ <Tooltip>
752
+ <TooltipTrigger asChild>
753
+ <PopoverTrigger asChild>
754
+ <Button
755
+ type="button"
756
+ variant="ghost"
757
+ disabled={disabled}
758
+ className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 p-0 text-muted-foreground hover:text-foreground"
759
+ aria-label={calendarButtonLabel}
760
+ >
761
+ <CalendarIcon className="h-4 w-4" />
762
+ </Button>
763
+ </PopoverTrigger>
764
+ </TooltipTrigger>
765
+ <TooltipContent>{calendarButtonLabel}</TooltipContent>
766
+ </Tooltip>
767
+ </div>
768
+ </PopoverAnchor>
769
+ <PopoverContent
770
+ className={cn("w-auto overflow-visible p-0", className)}
771
+ align="center"
772
+ sideOffset={8}
773
+ onInteractOutside={(event) => {
774
+ const target = event.target
775
+ if (
776
+ target instanceof Node &&
777
+ inputRef.current?.contains(target)
778
+ ) {
779
+ event.preventDefault()
780
+ }
781
+ }}
782
+ onOpenAutoFocus={(event) => event.preventDefault()}
783
+ >
784
+ <Calendar
785
+ className={cn(
786
+ showTodayButton && "rounded-b-none border-0",
787
+ useCompactMultiMonthCalendar ? "p-2" : showTodayButton ? "p-3" : undefined
788
+ )}
789
+ classNames={
790
+ useCompactMultiMonthCalendar
791
+ ? {
792
+ months: "relative flex flex-col gap-2 sm:flex-row",
793
+ month: "space-y-2",
794
+ weekday: "h-7 w-9 select-none text-center text-xs font-normal text-muted-foreground",
795
+ day: "relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20",
796
+ day_button:
797
+ "relative flex h-9 w-9 items-center justify-center rounded-md p-0 font-normal transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none",
798
+ }
799
+ : undefined
800
+ }
801
+ mode="range"
802
+ selected={calendarSelectedRange}
803
+ onSelect={handleCalendarSelect}
804
+ month={calendarMonth}
805
+ onMonthChange={setCalendarMonth}
806
+ startMonth={calendarStartMonth}
807
+ endMonth={calendarEndMonth}
808
+ numberOfMonths={calendarNumberOfMonths}
809
+ locale={locale}
810
+ modifiers={{
811
+ activeRangeStart: calendarSelectedRange?.from,
812
+ activeRangeEnd: calendarSelectedRange?.to,
813
+ }}
814
+ modifiersClassNames={{
815
+ activeRangeStart:
816
+ activeEndpoint === "from"
817
+ ? "[&>button]:ring-2 [&>button]:ring-primary-border [&>button]:ring-offset-2 [&>button]:ring-offset-background"
818
+ : "",
819
+ activeRangeEnd:
820
+ activeEndpoint === "to"
821
+ ? "[&>button]:ring-2 [&>button]:ring-info-border [&>button]:ring-offset-2 [&>button]:ring-offset-background"
822
+ : "",
823
+ }}
824
+ />
825
+ {showTodayButton ? (
826
+ <div className="space-y-1.5 border-t bg-card p-1.5">
827
+ <div className="flex items-center gap-2 px-1 text-[11px] text-muted-foreground">
828
+ <span
829
+ className={cn(
830
+ "h-2 w-2 rounded-full",
831
+ activeEndpoint === "from" ? "bg-primary" : "bg-info"
832
+ )}
833
+ />
834
+ <span>{getActiveEndpointLabel(activeEndpoint, locale)}</span>
835
+ </div>
836
+ {validationMessage ? (
837
+ <p className="px-1 text-[11px] font-medium text-destructive">
838
+ {validationMessage}
839
+ </p>
840
+ ) : null}
841
+ <div className="flex items-stretch justify-between gap-2">
842
+ {previousShortcutRange?.from ? (
843
+ <Button
844
+ type="button"
845
+ variant="ghost"
846
+ 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"
847
+ onClick={handlePreviousShortcutSelect}
848
+ onMouseDown={(event) => event.preventDefault()}
849
+ >
850
+ <span className="block max-w-full truncate text-[11px] leading-4 text-muted-foreground">
851
+ {previousLabel ?? getPreviousLabel(locale)}
852
+ </span>
853
+ <span className="block max-w-full truncate font-mono text-[11px] leading-4 text-foreground">
854
+ {formatRange(previousShortcutRange, dateFormat, locale)}
855
+ </span>
856
+ </Button>
857
+ ) : null}
858
+ <Button
859
+ type="button"
860
+ variant="secondary"
861
+ size="sm"
862
+ className="ml-auto h-10 shrink-0 px-3 text-xs"
863
+ onClick={handleTodaySelect}
864
+ onMouseDown={(event) => event.preventDefault()}
865
+ >
866
+ {todayLabel ?? getTodayLabel(locale)}
867
+ </Button>
868
+ </div>
869
+ </div>
870
+ ) : null}
871
+ </PopoverContent>
872
+ </Popover>
873
+ )
874
+ }
875
+ )
876
+ DateRangePicker.displayName = "DateRangePicker"
877
+
878
+ export { DateRangePicker }