@modern-admin/ui 0.1.0

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 (268) hide show
  1. package/dist/components/accordion.d.ts +7 -0
  2. package/dist/components/accordion.d.ts.map +1 -0
  3. package/dist/components/accordion.jsx +19 -0
  4. package/dist/components/accordion.jsx.map +1 -0
  5. package/dist/components/alert-dialog.d.ts +22 -0
  6. package/dist/components/alert-dialog.d.ts.map +1 -0
  7. package/dist/components/alert-dialog.jsx +27 -0
  8. package/dist/components/alert-dialog.jsx.map +1 -0
  9. package/dist/components/audit-timeline.d.ts +24 -0
  10. package/dist/components/audit-timeline.d.ts.map +1 -0
  11. package/dist/components/audit-timeline.jsx +60 -0
  12. package/dist/components/audit-timeline.jsx.map +1 -0
  13. package/dist/components/avatar.d.ts +6 -0
  14. package/dist/components/avatar.d.ts.map +1 -0
  15. package/dist/components/avatar.jsx +10 -0
  16. package/dist/components/avatar.jsx.map +1 -0
  17. package/dist/components/badge.d.ts +10 -0
  18. package/dist/components/badge.d.ts.map +1 -0
  19. package/dist/components/badge.jsx +19 -0
  20. package/dist/components/badge.jsx.map +1 -0
  21. package/dist/components/breadcrumb.d.ts +17 -0
  22. package/dist/components/breadcrumb.d.ts.map +1 -0
  23. package/dist/components/breadcrumb.jsx +27 -0
  24. package/dist/components/breadcrumb.jsx.map +1 -0
  25. package/dist/components/button.d.ts +12 -0
  26. package/dist/components/button.d.ts.map +1 -0
  27. package/dist/components/button.jsx +37 -0
  28. package/dist/components/button.jsx.map +1 -0
  29. package/dist/components/calendar.d.ts +9 -0
  30. package/dist/components/calendar.d.ts.map +1 -0
  31. package/dist/components/calendar.jsx +102 -0
  32. package/dist/components/calendar.jsx.map +1 -0
  33. package/dist/components/card.d.ts +8 -0
  34. package/dist/components/card.d.ts.map +1 -0
  35. package/dist/components/card.jsx +18 -0
  36. package/dist/components/card.jsx.map +1 -0
  37. package/dist/components/chart.d.ts +97 -0
  38. package/dist/components/chart.d.ts.map +1 -0
  39. package/dist/components/chart.jsx +233 -0
  40. package/dist/components/chart.jsx.map +1 -0
  41. package/dist/components/checkbox.d.ts +4 -0
  42. package/dist/components/checkbox.d.ts.map +1 -0
  43. package/dist/components/checkbox.jsx +11 -0
  44. package/dist/components/checkbox.jsx.map +1 -0
  45. package/dist/components/combobox.d.ts +46 -0
  46. package/dist/components/combobox.d.ts.map +1 -0
  47. package/dist/components/combobox.jsx +145 -0
  48. package/dist/components/combobox.jsx.map +1 -0
  49. package/dist/components/command.d.ts +80 -0
  50. package/dist/components/command.d.ts.map +1 -0
  51. package/dist/components/command.jsx +32 -0
  52. package/dist/components/command.jsx.map +1 -0
  53. package/dist/components/date-picker.d.ts +24 -0
  54. package/dist/components/date-picker.d.ts.map +1 -0
  55. package/dist/components/date-picker.jsx +149 -0
  56. package/dist/components/date-picker.jsx.map +1 -0
  57. package/dist/components/date-range-input.d.ts +22 -0
  58. package/dist/components/date-range-input.d.ts.map +1 -0
  59. package/dist/components/date-range-input.jsx +202 -0
  60. package/dist/components/date-range-input.jsx.map +1 -0
  61. package/dist/components/dialog.d.ts +19 -0
  62. package/dist/components/dialog.d.ts.map +1 -0
  63. package/dist/components/dialog.jsx +30 -0
  64. package/dist/components/dialog.jsx.map +1 -0
  65. package/dist/components/diff-view.d.ts +24 -0
  66. package/dist/components/diff-view.d.ts.map +1 -0
  67. package/dist/components/diff-view.jsx +69 -0
  68. package/dist/components/diff-view.jsx.map +1 -0
  69. package/dist/components/dropdown-menu.d.ts +27 -0
  70. package/dist/components/dropdown-menu.d.ts.map +1 -0
  71. package/dist/components/dropdown-menu.jsx +48 -0
  72. package/dist/components/dropdown-menu.jsx.map +1 -0
  73. package/dist/components/empty.d.ts +15 -0
  74. package/dist/components/empty.d.ts.map +1 -0
  75. package/dist/components/empty.jsx +27 -0
  76. package/dist/components/empty.jsx.map +1 -0
  77. package/dist/components/field.d.ts +23 -0
  78. package/dist/components/field.d.ts.map +1 -0
  79. package/dist/components/field.jsx +60 -0
  80. package/dist/components/field.jsx.map +1 -0
  81. package/dist/components/file-input.d.ts +50 -0
  82. package/dist/components/file-input.d.ts.map +1 -0
  83. package/dist/components/file-input.jsx +104 -0
  84. package/dist/components/file-input.jsx.map +1 -0
  85. package/dist/components/form.d.ts +20 -0
  86. package/dist/components/form.d.ts.map +1 -0
  87. package/dist/components/form.jsx +66 -0
  88. package/dist/components/form.jsx.map +1 -0
  89. package/dist/components/info-tooltip.d.ts +11 -0
  90. package/dist/components/info-tooltip.d.ts.map +1 -0
  91. package/dist/components/info-tooltip.jsx +17 -0
  92. package/dist/components/info-tooltip.jsx.map +1 -0
  93. package/dist/components/input.d.ts +13 -0
  94. package/dist/components/input.d.ts.map +1 -0
  95. package/dist/components/input.jsx +19 -0
  96. package/dist/components/input.jsx.map +1 -0
  97. package/dist/components/json-editor.d.ts +23 -0
  98. package/dist/components/json-editor.d.ts.map +1 -0
  99. package/dist/components/json-editor.jsx +143 -0
  100. package/dist/components/json-editor.jsx.map +1 -0
  101. package/dist/components/kbd.d.ts +15 -0
  102. package/dist/components/kbd.d.ts.map +1 -0
  103. package/dist/components/kbd.jsx +23 -0
  104. package/dist/components/kbd.jsx.map +1 -0
  105. package/dist/components/key-value-editor.d.ts +92 -0
  106. package/dist/components/key-value-editor.d.ts.map +1 -0
  107. package/dist/components/key-value-editor.jsx +187 -0
  108. package/dist/components/key-value-editor.jsx.map +1 -0
  109. package/dist/components/keyboard-shortcuts-help.d.ts +17 -0
  110. package/dist/components/keyboard-shortcuts-help.d.ts.map +1 -0
  111. package/dist/components/keyboard-shortcuts-help.jsx +97 -0
  112. package/dist/components/keyboard-shortcuts-help.jsx.map +1 -0
  113. package/dist/components/label.d.ts +5 -0
  114. package/dist/components/label.d.ts.map +1 -0
  115. package/dist/components/label.jsx +8 -0
  116. package/dist/components/label.jsx.map +1 -0
  117. package/dist/components/media-preview.d.ts +30 -0
  118. package/dist/components/media-preview.d.ts.map +1 -0
  119. package/dist/components/media-preview.jsx +189 -0
  120. package/dist/components/media-preview.jsx.map +1 -0
  121. package/dist/components/multi-file-input.d.ts +76 -0
  122. package/dist/components/multi-file-input.d.ts.map +1 -0
  123. package/dist/components/multi-file-input.jsx +131 -0
  124. package/dist/components/multi-file-input.jsx.map +1 -0
  125. package/dist/components/password-input.d.ts +10 -0
  126. package/dist/components/password-input.d.ts.map +1 -0
  127. package/dist/components/password-input.jsx +18 -0
  128. package/dist/components/password-input.jsx.map +1 -0
  129. package/dist/components/popover.d.ts +7 -0
  130. package/dist/components/popover.d.ts.map +1 -0
  131. package/dist/components/popover.jsx +11 -0
  132. package/dist/components/popover.jsx.map +1 -0
  133. package/dist/components/revision-timeline.d.ts +30 -0
  134. package/dist/components/revision-timeline.d.ts.map +1 -0
  135. package/dist/components/revision-timeline.jsx +42 -0
  136. package/dist/components/revision-timeline.jsx.map +1 -0
  137. package/dist/components/richtext-editor.d.ts +43 -0
  138. package/dist/components/richtext-editor.d.ts.map +1 -0
  139. package/dist/components/richtext-editor.jsx +319 -0
  140. package/dist/components/richtext-editor.jsx.map +1 -0
  141. package/dist/components/richtext-mode.d.ts +23 -0
  142. package/dist/components/richtext-mode.d.ts.map +1 -0
  143. package/dist/components/richtext-mode.js +36 -0
  144. package/dist/components/richtext-mode.js.map +1 -0
  145. package/dist/components/richtext-render.d.ts +8 -0
  146. package/dist/components/richtext-render.d.ts.map +1 -0
  147. package/dist/components/richtext-render.jsx +33 -0
  148. package/dist/components/richtext-render.jsx.map +1 -0
  149. package/dist/components/richtext-sync.d.ts +37 -0
  150. package/dist/components/richtext-sync.d.ts.map +1 -0
  151. package/dist/components/richtext-sync.js +46 -0
  152. package/dist/components/richtext-sync.js.map +1 -0
  153. package/dist/components/scroll-area.d.ts +5 -0
  154. package/dist/components/scroll-area.d.ts.map +1 -0
  155. package/dist/components/scroll-area.jsx +16 -0
  156. package/dist/components/scroll-area.jsx.map +1 -0
  157. package/dist/components/select.d.ts +36 -0
  158. package/dist/components/select.d.ts.map +1 -0
  159. package/dist/components/select.jsx +87 -0
  160. package/dist/components/select.jsx.map +1 -0
  161. package/dist/components/separator.d.ts +4 -0
  162. package/dist/components/separator.d.ts.map +1 -0
  163. package/dist/components/separator.jsx +6 -0
  164. package/dist/components/separator.jsx.map +1 -0
  165. package/dist/components/sheet.d.ts +29 -0
  166. package/dist/components/sheet.d.ts.map +1 -0
  167. package/dist/components/sheet.jsx +44 -0
  168. package/dist/components/sheet.jsx.map +1 -0
  169. package/dist/components/sidebar.d.ts +70 -0
  170. package/dist/components/sidebar.d.ts.map +1 -0
  171. package/dist/components/sidebar.jsx +245 -0
  172. package/dist/components/sidebar.jsx.map +1 -0
  173. package/dist/components/skeleton.d.ts +3 -0
  174. package/dist/components/skeleton.d.ts.map +1 -0
  175. package/dist/components/skeleton.jsx +6 -0
  176. package/dist/components/skeleton.jsx.map +1 -0
  177. package/dist/components/sonner.d.ts +6 -0
  178. package/dist/components/sonner.d.ts.map +1 -0
  179. package/dist/components/sonner.jsx +29 -0
  180. package/dist/components/sonner.jsx.map +1 -0
  181. package/dist/components/switch.d.ts +4 -0
  182. package/dist/components/switch.d.ts.map +1 -0
  183. package/dist/components/switch.jsx +8 -0
  184. package/dist/components/switch.jsx.map +1 -0
  185. package/dist/components/table.d.ts +10 -0
  186. package/dist/components/table.d.ts.map +1 -0
  187. package/dist/components/table.jsx +21 -0
  188. package/dist/components/table.jsx.map +1 -0
  189. package/dist/components/tabs.d.ts +7 -0
  190. package/dist/components/tabs.d.ts.map +1 -0
  191. package/dist/components/tabs.jsx +14 -0
  192. package/dist/components/tabs.jsx.map +1 -0
  193. package/dist/components/textarea.d.ts +4 -0
  194. package/dist/components/textarea.d.ts.map +1 -0
  195. package/dist/components/textarea.jsx +5 -0
  196. package/dist/components/textarea.jsx.map +1 -0
  197. package/dist/components/tooltip.d.ts +7 -0
  198. package/dist/components/tooltip.d.ts.map +1 -0
  199. package/dist/components/tooltip.jsx +11 -0
  200. package/dist/components/tooltip.jsx.map +1 -0
  201. package/dist/index.d.ts +52 -0
  202. package/dist/index.d.ts.map +1 -0
  203. package/dist/index.js +72 -0
  204. package/dist/index.js.map +1 -0
  205. package/dist/lib/theme.d.ts +11 -0
  206. package/dist/lib/theme.d.ts.map +1 -0
  207. package/dist/lib/theme.js +44 -0
  208. package/dist/lib/theme.js.map +1 -0
  209. package/dist/lib/utils.d.ts +3 -0
  210. package/dist/lib/utils.d.ts.map +1 -0
  211. package/dist/lib/utils.js +6 -0
  212. package/dist/lib/utils.js.map +1 -0
  213. package/dist/styles.css +242 -0
  214. package/package.json +85 -0
  215. package/src/components/accordion.tsx +48 -0
  216. package/src/components/alert-dialog.tsx +113 -0
  217. package/src/components/audit-timeline.tsx +102 -0
  218. package/src/components/avatar.tsx +42 -0
  219. package/src/components/badge.tsx +34 -0
  220. package/src/components/breadcrumb.tsx +99 -0
  221. package/src/components/button.tsx +58 -0
  222. package/src/components/calendar.tsx +176 -0
  223. package/src/components/card.tsx +60 -0
  224. package/src/components/chart.tsx +558 -0
  225. package/src/components/checkbox.tsx +23 -0
  226. package/src/components/combobox.tsx +264 -0
  227. package/src/components/command.tsx +120 -0
  228. package/src/components/date-picker.tsx +221 -0
  229. package/src/components/date-range-input.tsx +295 -0
  230. package/src/components/dialog.tsx +94 -0
  231. package/src/components/diff-view.tsx +182 -0
  232. package/src/components/dropdown-menu.tsx +165 -0
  233. package/src/components/empty.tsx +100 -0
  234. package/src/components/field.tsx +168 -0
  235. package/src/components/file-input.tsx +233 -0
  236. package/src/components/form.tsx +152 -0
  237. package/src/components/info-tooltip.tsx +40 -0
  238. package/src/components/input.tsx +55 -0
  239. package/src/components/json-editor.tsx +210 -0
  240. package/src/components/kbd.tsx +35 -0
  241. package/src/components/key-value-editor.tsx +423 -0
  242. package/src/components/keyboard-shortcuts-help.tsx +136 -0
  243. package/src/components/label.tsx +16 -0
  244. package/src/components/media-preview.tsx +278 -0
  245. package/src/components/multi-file-input.tsx +315 -0
  246. package/src/components/password-input.tsx +50 -0
  247. package/src/components/popover.tsx +26 -0
  248. package/src/components/revision-timeline.tsx +93 -0
  249. package/src/components/richtext-editor.tsx +624 -0
  250. package/src/components/richtext-mode.ts +39 -0
  251. package/src/components/richtext-render.tsx +51 -0
  252. package/src/components/richtext-sync.ts +57 -0
  253. package/src/components/scroll-area.tsx +41 -0
  254. package/src/components/select.tsx +200 -0
  255. package/src/components/separator.tsx +21 -0
  256. package/src/components/sheet.tsx +109 -0
  257. package/src/components/sidebar.tsx +660 -0
  258. package/src/components/skeleton.tsx +9 -0
  259. package/src/components/sonner.tsx +45 -0
  260. package/src/components/switch.tsx +24 -0
  261. package/src/components/table.tsx +93 -0
  262. package/src/components/tabs.tsx +57 -0
  263. package/src/components/textarea.tsx +18 -0
  264. package/src/components/tooltip.tsx +25 -0
  265. package/src/index.ts +342 -0
  266. package/src/lib/theme.ts +45 -0
  267. package/src/lib/utils.ts +6 -0
  268. package/src/styles.css +242 -0
@@ -0,0 +1,295 @@
1
+ // Popover-driven date-range picker.
2
+ // A single trigger button shows the selected range (or placeholder);
3
+ // clicking opens a popover with a two-month Calendar in range mode.
4
+ // On narrow screens the calendar collapses to a single month.
5
+ //
6
+ // UX notes:
7
+ // - The picker NEVER auto-closes on date selection — the user confirms
8
+ // explicitly via the "Apply" button. This prevents the "first click
9
+ // closes the picker" bug that occurs when a partial pending range
10
+ // (from without to) is in state and the next click completes it.
11
+ // - "Clear" inside the popover empties the range and closes.
12
+ // - The X inline in the trigger clears directly without opening the picker.
13
+ // - On open: only a COMPLETE committed range (both ends) is mirrored into
14
+ // the calendar state so the user can edit from a known baseline.
15
+ // A committed partial range (from only) resets to blank to avoid
16
+ // accidentally completing it on the first click.
17
+ // - Escape / click-outside discards in-progress selection and reverts to
18
+ // the last committed range.
19
+ //
20
+ // i18n-unaware by design: all visible strings are passed via `labels`.
21
+
22
+ import * as React from 'react'
23
+ import { addMonths, format, isSameMonth, isValid, parse, parseISO, startOfMonth } from 'date-fns'
24
+ import { CalendarRange, X } from 'lucide-react'
25
+ import type { DateRange } from 'react-day-picker'
26
+ import { cn } from '../lib/utils.js'
27
+ import { Button } from './button.js'
28
+ import { Calendar } from './calendar.js'
29
+ import { Popover, PopoverContent, PopoverTrigger } from './popover.js'
30
+
31
+ const DATE_FMT = 'yyyy-MM-dd'
32
+ const DISPLAY_FMT = 'MMM d, yyyy'
33
+
34
+ function parseDate(value: string | null | undefined): Date | undefined {
35
+ if (!value) return undefined
36
+ const d = value.length <= 10 ? parse(value, DATE_FMT, new Date()) : parseISO(value)
37
+ return isValid(d) ? d : undefined
38
+ }
39
+
40
+ export interface DateRangeInputLabels {
41
+ /** Shown in the trigger when nothing is selected. */
42
+ placeholder?: string
43
+ /** "Apply" button inside the popover footer. */
44
+ apply?: string
45
+ /** "Clear" button inside the popover footer + aria-label for the inline X. */
46
+ clear?: string
47
+ }
48
+
49
+ export interface DateRangeInputProps {
50
+ from: string | null | undefined
51
+ to: string | null | undefined
52
+ onChange(from: string, to: string): void
53
+ disabled?: boolean
54
+ className?: string
55
+ labels?: DateRangeInputLabels
56
+ }
57
+
58
+ export function DateRangeInput({
59
+ from,
60
+ to,
61
+ onChange,
62
+ disabled,
63
+ className,
64
+ labels = {},
65
+ }: DateRangeInputProps): React.ReactElement {
66
+ const placeholder = labels.placeholder ?? 'Select date range'
67
+ const applyLabel = labels.apply ?? 'Apply'
68
+ const clearLabel = labels.clear ?? 'Clear'
69
+
70
+ const [open, setOpen] = React.useState(false)
71
+
72
+ // Calendar selection in progress. Committed to onChange only via Apply.
73
+ const [pending, setPending] = React.useState<DateRange | undefined>(() => {
74
+ const f = parseDate(from)
75
+ const t = parseDate(to)
76
+ // Only restore a complete range on init — partial ranges start fresh.
77
+ return f && t ? { from: f, to: t } : undefined
78
+ })
79
+
80
+ // Keep pending in sync when props change externally (e.g. programmatic reset).
81
+ React.useEffect(() => {
82
+ const f = parseDate(from)
83
+ const t = parseDate(to)
84
+ setPending(f ?? t ? { from: f, to: t } : undefined)
85
+ }, [from, to])
86
+
87
+ // Number of calendar months — 1 on narrow, 2 on wide viewports.
88
+ const [months, setMonths] = React.useState(() =>
89
+ typeof window !== 'undefined' && window.innerWidth >= 640 ? 2 : 1,
90
+ )
91
+ React.useEffect(() => {
92
+ const mq = window.matchMedia('(min-width: 640px)')
93
+ const handle = (e: MediaQueryListEvent): void => setMonths(e.matches ? 2 : 1)
94
+ setMonths(mq.matches ? 2 : 1)
95
+ mq.addEventListener('change', handle)
96
+ return () => mq.removeEventListener('change', handle)
97
+ }, [])
98
+
99
+ // Independent navigation for the two-panel layout. Each panel keeps its
100
+ // own visible month; the dropdowns on the right are constrained to
101
+ // months >= left (and the left to months <= right) so the panels can
102
+ // never cross over. State is (re)initialised from the committed range
103
+ // every time the popover opens (see `handleOpenChange`).
104
+ const [leftMonth, setLeftMonth] = React.useState<Date>(() => {
105
+ const f = parseDate(from)
106
+ return startOfMonth(f ?? new Date())
107
+ })
108
+ const [rightMonth, setRightMonth] = React.useState<Date>(() => {
109
+ const f = parseDate(from)
110
+ const t = parseDate(to)
111
+ const left = startOfMonth(f ?? new Date())
112
+ return t && !isSameMonth(left, t) ? startOfMonth(t) : addMonths(left, 1)
113
+ })
114
+
115
+ const handleOpenChange = (next: boolean): void => {
116
+ if (next) {
117
+ // On open: only restore a COMPLETE committed range so the user sees
118
+ // their previous selection as a starting point. A partial range
119
+ // (from without to) is intentionally dropped — carrying it over
120
+ // would cause the very next calendar click to "complete" the range
121
+ // and trigger an immediate commit + close.
122
+ const f = parseDate(from)
123
+ const t = parseDate(to)
124
+ setPending(f && t ? { from: f, to: t } : undefined)
125
+ // Re-derive panel navigation from the committed range so reopening
126
+ // always lands the user on the months they were last looking at —
127
+ // independently for the left and right panels.
128
+ const left = startOfMonth(f ?? new Date())
129
+ const right = t && !isSameMonth(left, t)
130
+ ? startOfMonth(t)
131
+ : addMonths(left, 1)
132
+ setLeftMonth(left)
133
+ setRightMonth(right)
134
+ } else {
135
+ // Closed without Apply (Escape / outside click) — discard in-progress
136
+ // selection and revert to the last committed values.
137
+ const f = parseDate(from)
138
+ const t = parseDate(to)
139
+ setPending(f ?? t ? { from: f, to: t } : undefined)
140
+ }
141
+ setOpen(next)
142
+ }
143
+
144
+ // Navigation guards. The right panel can never cross above the left's
145
+ // month, and the left can never cross above the right's. If a user
146
+ // navigates the LEFT forward past the right (via the prev/next arrow
147
+ // when the dropdown bounds don't disable it), push the right one month
148
+ // ahead; symmetric for the right going below the left.
149
+ const handleLeftMonthChange = (next: Date): void => {
150
+ setLeftMonth(next)
151
+ if (!isSameMonth(next, rightMonth) && next > rightMonth) {
152
+ setRightMonth(addMonths(next, 1))
153
+ }
154
+ }
155
+ const handleRightMonthChange = (next: Date): void => {
156
+ setRightMonth(next)
157
+ if (!isSameMonth(next, leftMonth) && next < leftMonth) {
158
+ setLeftMonth(addMonths(next, -1))
159
+ }
160
+ }
161
+
162
+ // Update the in-progress selection; never auto-commit.
163
+ const handleSelect = (range: DateRange | undefined): void => {
164
+ setPending(range)
165
+ }
166
+
167
+ // Commit whatever is pending (at minimum a start date) and close.
168
+ const handleApply = (): void => {
169
+ if (pending?.from) {
170
+ onChange(
171
+ format(pending.from, DATE_FMT),
172
+ pending.to ? format(pending.to, DATE_FMT) : '',
173
+ )
174
+ }
175
+ setOpen(false)
176
+ }
177
+
178
+ // Clear inside the popover: empty the committed range and close.
179
+ const handleClearPopover = (): void => {
180
+ setPending(undefined)
181
+ onChange('', '')
182
+ setOpen(false)
183
+ }
184
+
185
+ // Inline X on the trigger: clear committed range without opening the picker.
186
+ const handleClearInline = (e: React.MouseEvent): void => {
187
+ e.stopPropagation()
188
+ setPending(undefined)
189
+ onChange('', '')
190
+ }
191
+
192
+ const fromDate = parseDate(from)
193
+ const toDate = parseDate(to)
194
+ const hasValue = !!(from || to)
195
+
196
+ const displayText = hasValue
197
+ ? [
198
+ fromDate ? format(fromDate, DISPLAY_FMT) : '…',
199
+ toDate ? format(toDate, DISPLAY_FMT) : '…',
200
+ ].join(' – ')
201
+ : null
202
+
203
+ return (
204
+ <Popover open={open} onOpenChange={handleOpenChange}>
205
+ <PopoverTrigger asChild>
206
+ <Button
207
+ type="button"
208
+ variant="outline"
209
+ disabled={disabled}
210
+ className={cn(
211
+ 'h-9 w-full justify-start gap-2 px-3 font-normal',
212
+ !hasValue && 'text-muted-foreground',
213
+ className,
214
+ )}
215
+ >
216
+ <CalendarRange className="size-4 shrink-0 opacity-60" />
217
+ <span className="flex-1 truncate text-left text-sm">
218
+ {displayText ?? placeholder}
219
+ </span>
220
+ {hasValue && (
221
+ <span
222
+ role="button"
223
+ tabIndex={0}
224
+ aria-label={clearLabel}
225
+ onClick={handleClearInline}
226
+ onKeyDown={(e) => {
227
+ if (e.key === 'Enter' || e.key === ' ') {
228
+ handleClearInline(e as unknown as React.MouseEvent)
229
+ }
230
+ }}
231
+ className="ml-1 rounded-sm p-0.5 opacity-50 hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
232
+ >
233
+ <X className="size-3.5" />
234
+ </span>
235
+ )}
236
+ </Button>
237
+ </PopoverTrigger>
238
+ <PopoverContent className="w-auto p-0" align="start">
239
+ {months === 2 ? (
240
+ // Two-panel layout: each Calendar is a fully independent single-
241
+ // month instance with its own controlled `month` state. The left
242
+ // panel's dropdowns can't reach past the right panel's month
243
+ // (and vice versa) thanks to the shared start/end-month bounds.
244
+ // Range selection still spans both panels because they share
245
+ // the same `selected` + `onSelect`.
246
+ <div className="flex flex-col sm:flex-row">
247
+ <Calendar
248
+ mode="range"
249
+ selected={pending}
250
+ onSelect={handleSelect}
251
+ numberOfMonths={1}
252
+ month={leftMonth}
253
+ onMonthChange={handleLeftMonthChange}
254
+ endMonth={rightMonth}
255
+ autoFocus
256
+ />
257
+ <Calendar
258
+ mode="range"
259
+ selected={pending}
260
+ onSelect={handleSelect}
261
+ numberOfMonths={1}
262
+ month={rightMonth}
263
+ onMonthChange={handleRightMonthChange}
264
+ startMonth={leftMonth}
265
+ />
266
+ </div>
267
+ ) : (
268
+ <Calendar
269
+ mode="range"
270
+ selected={pending}
271
+ onSelect={handleSelect}
272
+ numberOfMonths={1}
273
+ // react-day-picker doesn't auto-navigate to the selected range on
274
+ // mount — it stays on today's month. The popover re-mounts the
275
+ // Calendar each time it opens, so deriving `defaultMonth` from
276
+ // the (already-restored) pending range puts the user back where
277
+ // they left off without controlling navigation explicitly.
278
+ defaultMonth={pending?.from ?? pending?.to ?? undefined}
279
+ autoFocus
280
+ />
281
+ )}
282
+ <div className="flex items-center justify-between border-t border-border px-3 py-2">
283
+ <Button variant="ghost" size="sm" onClick={handleClearPopover}>
284
+ {clearLabel}
285
+ </Button>
286
+ <Button size="sm" onClick={handleApply} disabled={!pending?.from}>
287
+ {applyLabel}
288
+ </Button>
289
+ </div>
290
+ </PopoverContent>
291
+ </Popover>
292
+ )
293
+ }
294
+
295
+ DateRangeInput.displayName = 'DateRangeInput'
@@ -0,0 +1,94 @@
1
+ import * as React from 'react'
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
3
+ import { X } from 'lucide-react'
4
+ import { cn } from '../lib/utils.js'
5
+
6
+ export const Dialog = DialogPrimitive.Root
7
+ export const DialogTrigger = DialogPrimitive.Trigger
8
+ export const DialogPortal = DialogPrimitive.Portal
9
+ export const DialogClose = DialogPrimitive.Close
10
+
11
+ export const DialogOverlay = React.forwardRef<
12
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
13
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
14
+ >(({ className, ...props }, ref) => (
15
+ <DialogPrimitive.Overlay
16
+ ref={ref}
17
+ className={cn(
18
+ 'fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ ))
24
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
25
+
26
+ export const DialogContent = React.forwardRef<
27
+ React.ElementRef<typeof DialogPrimitive.Content>,
28
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
29
+ >(({ className, children, ...props }, ref) => (
30
+ <DialogPortal>
31
+ <DialogOverlay />
32
+ <DialogPrimitive.Content
33
+ ref={ref}
34
+ className={cn(
35
+ 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg',
36
+ className,
37
+ )}
38
+ {...props}
39
+ >
40
+ {children}
41
+ <DialogPrimitive.Close className="absolute right-4 top-4 cursor-pointer rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
42
+ <X className="h-4 w-4" />
43
+ <span className="sr-only">Close</span>
44
+ </DialogPrimitive.Close>
45
+ </DialogPrimitive.Content>
46
+ </DialogPortal>
47
+ ))
48
+ DialogContent.displayName = DialogPrimitive.Content.displayName
49
+
50
+ export const DialogHeader = ({
51
+ className,
52
+ ...props
53
+ }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement => (
54
+ <div
55
+ className={cn('flex flex-col gap-1.5 text-center sm:text-left', className)}
56
+ {...props}
57
+ />
58
+ )
59
+ DialogHeader.displayName = 'DialogHeader'
60
+
61
+ export const DialogFooter = ({
62
+ className,
63
+ ...props
64
+ }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement => (
65
+ <div
66
+ className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-2', className)}
67
+ {...props}
68
+ />
69
+ )
70
+ DialogFooter.displayName = 'DialogFooter'
71
+
72
+ export const DialogTitle = React.forwardRef<
73
+ React.ElementRef<typeof DialogPrimitive.Title>,
74
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
75
+ >(({ className, ...props }, ref) => (
76
+ <DialogPrimitive.Title
77
+ ref={ref}
78
+ className={cn('text-lg font-semibold leading-none tracking-tight', className)}
79
+ {...props}
80
+ />
81
+ ))
82
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
83
+
84
+ export const DialogDescription = React.forwardRef<
85
+ React.ElementRef<typeof DialogPrimitive.Description>,
86
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
87
+ >(({ className, ...props }, ref) => (
88
+ <DialogPrimitive.Description
89
+ ref={ref}
90
+ className={cn('text-sm text-muted-foreground', className)}
91
+ {...props}
92
+ />
93
+ ))
94
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
@@ -0,0 +1,182 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../lib/utils.js'
3
+
4
+ export interface DiffField {
5
+ path: string
6
+ /** Human-readable property label. When present, shown before the path. */
7
+ label?: string
8
+ before?: unknown
9
+ after?: unknown
10
+ kind: 'added' | 'changed' | 'removed'
11
+ }
12
+
13
+ export interface DiffViewLabels {
14
+ added?: string
15
+ changed?: string
16
+ removed?: string
17
+ before?: string
18
+ after?: string
19
+ noChanges?: string
20
+ }
21
+
22
+ export interface DiffViewProps {
23
+ fields: ReadonlyArray<DiffField>
24
+ labels?: DiffViewLabels
25
+ className?: string
26
+ }
27
+
28
+ const DEFAULT_LABELS: Required<DiffViewLabels> = {
29
+ added: 'Added',
30
+ changed: 'Changed',
31
+ removed: 'Removed',
32
+ before: 'Before',
33
+ after: 'After',
34
+ noChanges: 'No changes',
35
+ }
36
+
37
+ const formatValue = (value: unknown): string => {
38
+ if (value === undefined) return ''
39
+ if (value === null) return 'null'
40
+ if (typeof value === 'string') return value
41
+ return JSON.stringify(value, null, 2)
42
+ }
43
+
44
+ const isMultiline = (text: string): boolean => text.includes('\n') || text.length > 80
45
+
46
+ export function DiffView({ fields, labels, className }: DiffViewProps): React.ReactElement {
47
+ const l = { ...DEFAULT_LABELS, ...labels }
48
+ if (fields.length === 0) {
49
+ return (
50
+ <div className={cn('rounded-md border border-dashed p-4 text-sm text-muted-foreground', className)}>
51
+ {l.noChanges}
52
+ </div>
53
+ )
54
+ }
55
+ return (
56
+ <ul
57
+ className={cn(
58
+ 'divide-y divide-border overflow-hidden rounded-md border bg-card text-xs',
59
+ className,
60
+ )}
61
+ >
62
+ {fields.map((field) => (
63
+ <FieldDiff key={field.path} field={field} labels={l} />
64
+ ))}
65
+ </ul>
66
+ )
67
+ }
68
+
69
+ function FieldDiff({
70
+ field,
71
+ labels,
72
+ }: {
73
+ field: DiffField
74
+ labels: Required<DiffViewLabels>
75
+ }): React.ReactElement {
76
+ const beforeText = field.kind === 'added' ? '' : formatValue(field.before)
77
+ const afterText = field.kind === 'removed' ? '' : formatValue(field.after)
78
+ const compact = !isMultiline(beforeText) && !isMultiline(afterText)
79
+ return (
80
+ <li className="grid grid-cols-[8rem_1fr] gap-x-3 px-3 py-1.5 sm:grid-cols-[10rem_1fr]">
81
+ <div className="min-w-0 pt-0.5">
82
+ {field.label && (
83
+ <p className="truncate text-[11px] font-medium text-foreground" title={field.label}>
84
+ {field.label}
85
+ </p>
86
+ )}
87
+ <code
88
+ className="truncate font-mono text-[10px] text-muted-foreground"
89
+ title={field.path}
90
+ >
91
+ {field.path}
92
+ </code>
93
+ </div>
94
+ {compact ? (
95
+ <CompactValues
96
+ kind={field.kind}
97
+ before={beforeText}
98
+ after={afterText}
99
+ labels={labels}
100
+ />
101
+ ) : (
102
+ <StackedValues
103
+ kind={field.kind}
104
+ before={beforeText}
105
+ after={afterText}
106
+ labels={labels}
107
+ />
108
+ )}
109
+ </li>
110
+ )
111
+ }
112
+
113
+ function CompactValues({
114
+ kind,
115
+ before,
116
+ after,
117
+ labels,
118
+ }: {
119
+ kind: DiffField['kind']
120
+ before: string
121
+ after: string
122
+ labels: Required<DiffViewLabels>
123
+ }): React.ReactElement {
124
+ return (
125
+ <div className="flex min-w-0 flex-col gap-0.5 font-mono text-xs leading-5">
126
+ {kind !== 'added' && (
127
+ <span
128
+ aria-label={labels.before}
129
+ className="truncate rounded bg-red-50 px-1.5 text-red-900 line-through decoration-red-400/60 dark:bg-red-950/40 dark:text-red-100"
130
+ title={before}
131
+ >
132
+ {before || '\u00A0'}
133
+ </span>
134
+ )}
135
+ {kind !== 'removed' && (
136
+ <span
137
+ aria-label={labels.after}
138
+ className="truncate rounded bg-green-50 px-1.5 text-green-900 dark:bg-green-950/40 dark:text-green-100"
139
+ title={after}
140
+ >
141
+ {after || '\u00A0'}
142
+ </span>
143
+ )}
144
+ </div>
145
+ )
146
+ }
147
+
148
+ function StackedValues({
149
+ kind,
150
+ before,
151
+ after,
152
+ labels,
153
+ }: {
154
+ kind: DiffField['kind']
155
+ before: string
156
+ after: string
157
+ labels: Required<DiffViewLabels>
158
+ }): React.ReactElement {
159
+ return (
160
+ <div className="min-w-0 overflow-hidden rounded font-mono text-xs leading-5">
161
+ {kind !== 'added' && (
162
+ <pre
163
+ aria-label={labels.before}
164
+ className="overflow-x-auto whitespace-pre-wrap bg-red-50 px-2 py-0.5 text-red-900 dark:bg-red-950/40 dark:text-red-100"
165
+ >
166
+ {prefixed('-', before)}
167
+ </pre>
168
+ )}
169
+ {kind !== 'removed' && (
170
+ <pre
171
+ aria-label={labels.after}
172
+ className="overflow-x-auto whitespace-pre-wrap bg-green-50 px-2 py-0.5 text-green-900 dark:bg-green-950/40 dark:text-green-100"
173
+ >
174
+ {prefixed('+', after)}
175
+ </pre>
176
+ )}
177
+ </div>
178
+ )
179
+ }
180
+
181
+ const prefixed = (sign: '+' | '-', text: string): string =>
182
+ text.split('\n').map((l) => `${sign} ${l}`).join('\n')