@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.
- package/dist/components/accordion.d.ts +7 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.jsx +19 -0
- package/dist/components/accordion.jsx.map +1 -0
- package/dist/components/alert-dialog.d.ts +22 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.jsx +27 -0
- package/dist/components/alert-dialog.jsx.map +1 -0
- package/dist/components/audit-timeline.d.ts +24 -0
- package/dist/components/audit-timeline.d.ts.map +1 -0
- package/dist/components/audit-timeline.jsx +60 -0
- package/dist/components/audit-timeline.jsx.map +1 -0
- package/dist/components/avatar.d.ts +6 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.jsx +10 -0
- package/dist/components/avatar.jsx.map +1 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.jsx +19 -0
- package/dist/components/badge.jsx.map +1 -0
- package/dist/components/breadcrumb.d.ts +17 -0
- package/dist/components/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb.jsx +27 -0
- package/dist/components/breadcrumb.jsx.map +1 -0
- package/dist/components/button.d.ts +12 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.jsx +37 -0
- package/dist/components/button.jsx.map +1 -0
- package/dist/components/calendar.d.ts +9 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.jsx +102 -0
- package/dist/components/calendar.jsx.map +1 -0
- package/dist/components/card.d.ts +8 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.jsx +18 -0
- package/dist/components/card.jsx.map +1 -0
- package/dist/components/chart.d.ts +97 -0
- package/dist/components/chart.d.ts.map +1 -0
- package/dist/components/chart.jsx +233 -0
- package/dist/components/chart.jsx.map +1 -0
- package/dist/components/checkbox.d.ts +4 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.jsx +11 -0
- package/dist/components/checkbox.jsx.map +1 -0
- package/dist/components/combobox.d.ts +46 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.jsx +145 -0
- package/dist/components/combobox.jsx.map +1 -0
- package/dist/components/command.d.ts +80 -0
- package/dist/components/command.d.ts.map +1 -0
- package/dist/components/command.jsx +32 -0
- package/dist/components/command.jsx.map +1 -0
- package/dist/components/date-picker.d.ts +24 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.jsx +149 -0
- package/dist/components/date-picker.jsx.map +1 -0
- package/dist/components/date-range-input.d.ts +22 -0
- package/dist/components/date-range-input.d.ts.map +1 -0
- package/dist/components/date-range-input.jsx +202 -0
- package/dist/components/date-range-input.jsx.map +1 -0
- package/dist/components/dialog.d.ts +19 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.jsx +30 -0
- package/dist/components/dialog.jsx.map +1 -0
- package/dist/components/diff-view.d.ts +24 -0
- package/dist/components/diff-view.d.ts.map +1 -0
- package/dist/components/diff-view.jsx +69 -0
- package/dist/components/diff-view.jsx.map +1 -0
- package/dist/components/dropdown-menu.d.ts +27 -0
- package/dist/components/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu.jsx +48 -0
- package/dist/components/dropdown-menu.jsx.map +1 -0
- package/dist/components/empty.d.ts +15 -0
- package/dist/components/empty.d.ts.map +1 -0
- package/dist/components/empty.jsx +27 -0
- package/dist/components/empty.jsx.map +1 -0
- package/dist/components/field.d.ts +23 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.jsx +60 -0
- package/dist/components/field.jsx.map +1 -0
- package/dist/components/file-input.d.ts +50 -0
- package/dist/components/file-input.d.ts.map +1 -0
- package/dist/components/file-input.jsx +104 -0
- package/dist/components/file-input.jsx.map +1 -0
- package/dist/components/form.d.ts +20 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.jsx +66 -0
- package/dist/components/form.jsx.map +1 -0
- package/dist/components/info-tooltip.d.ts +11 -0
- package/dist/components/info-tooltip.d.ts.map +1 -0
- package/dist/components/info-tooltip.jsx +17 -0
- package/dist/components/info-tooltip.jsx.map +1 -0
- package/dist/components/input.d.ts +13 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.jsx +19 -0
- package/dist/components/input.jsx.map +1 -0
- package/dist/components/json-editor.d.ts +23 -0
- package/dist/components/json-editor.d.ts.map +1 -0
- package/dist/components/json-editor.jsx +143 -0
- package/dist/components/json-editor.jsx.map +1 -0
- package/dist/components/kbd.d.ts +15 -0
- package/dist/components/kbd.d.ts.map +1 -0
- package/dist/components/kbd.jsx +23 -0
- package/dist/components/kbd.jsx.map +1 -0
- package/dist/components/key-value-editor.d.ts +92 -0
- package/dist/components/key-value-editor.d.ts.map +1 -0
- package/dist/components/key-value-editor.jsx +187 -0
- package/dist/components/key-value-editor.jsx.map +1 -0
- package/dist/components/keyboard-shortcuts-help.d.ts +17 -0
- package/dist/components/keyboard-shortcuts-help.d.ts.map +1 -0
- package/dist/components/keyboard-shortcuts-help.jsx +97 -0
- package/dist/components/keyboard-shortcuts-help.jsx.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.d.ts.map +1 -0
- package/dist/components/label.jsx +8 -0
- package/dist/components/label.jsx.map +1 -0
- package/dist/components/media-preview.d.ts +30 -0
- package/dist/components/media-preview.d.ts.map +1 -0
- package/dist/components/media-preview.jsx +189 -0
- package/dist/components/media-preview.jsx.map +1 -0
- package/dist/components/multi-file-input.d.ts +76 -0
- package/dist/components/multi-file-input.d.ts.map +1 -0
- package/dist/components/multi-file-input.jsx +131 -0
- package/dist/components/multi-file-input.jsx.map +1 -0
- package/dist/components/password-input.d.ts +10 -0
- package/dist/components/password-input.d.ts.map +1 -0
- package/dist/components/password-input.jsx +18 -0
- package/dist/components/password-input.jsx.map +1 -0
- package/dist/components/popover.d.ts +7 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.jsx +11 -0
- package/dist/components/popover.jsx.map +1 -0
- package/dist/components/revision-timeline.d.ts +30 -0
- package/dist/components/revision-timeline.d.ts.map +1 -0
- package/dist/components/revision-timeline.jsx +42 -0
- package/dist/components/revision-timeline.jsx.map +1 -0
- package/dist/components/richtext-editor.d.ts +43 -0
- package/dist/components/richtext-editor.d.ts.map +1 -0
- package/dist/components/richtext-editor.jsx +319 -0
- package/dist/components/richtext-editor.jsx.map +1 -0
- package/dist/components/richtext-mode.d.ts +23 -0
- package/dist/components/richtext-mode.d.ts.map +1 -0
- package/dist/components/richtext-mode.js +36 -0
- package/dist/components/richtext-mode.js.map +1 -0
- package/dist/components/richtext-render.d.ts +8 -0
- package/dist/components/richtext-render.d.ts.map +1 -0
- package/dist/components/richtext-render.jsx +33 -0
- package/dist/components/richtext-render.jsx.map +1 -0
- package/dist/components/richtext-sync.d.ts +37 -0
- package/dist/components/richtext-sync.d.ts.map +1 -0
- package/dist/components/richtext-sync.js +46 -0
- package/dist/components/richtext-sync.js.map +1 -0
- package/dist/components/scroll-area.d.ts +5 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/scroll-area.jsx +16 -0
- package/dist/components/scroll-area.jsx.map +1 -0
- package/dist/components/select.d.ts +36 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.jsx +87 -0
- package/dist/components/select.jsx.map +1 -0
- package/dist/components/separator.d.ts +4 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/separator.jsx +6 -0
- package/dist/components/separator.jsx.map +1 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.d.ts.map +1 -0
- package/dist/components/sheet.jsx +44 -0
- package/dist/components/sheet.jsx.map +1 -0
- package/dist/components/sidebar.d.ts +70 -0
- package/dist/components/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar.jsx +245 -0
- package/dist/components/sidebar.jsx.map +1 -0
- package/dist/components/skeleton.d.ts +3 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton.jsx +6 -0
- package/dist/components/skeleton.jsx.map +1 -0
- package/dist/components/sonner.d.ts +6 -0
- package/dist/components/sonner.d.ts.map +1 -0
- package/dist/components/sonner.jsx +29 -0
- package/dist/components/sonner.jsx.map +1 -0
- package/dist/components/switch.d.ts +4 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.jsx +8 -0
- package/dist/components/switch.jsx.map +1 -0
- package/dist/components/table.d.ts +10 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.jsx +21 -0
- package/dist/components/table.jsx.map +1 -0
- package/dist/components/tabs.d.ts +7 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.jsx +14 -0
- package/dist/components/tabs.jsx.map +1 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.jsx +5 -0
- package/dist/components/textarea.jsx.map +1 -0
- package/dist/components/tooltip.d.ts +7 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.jsx +11 -0
- package/dist/components/tooltip.jsx.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/theme.d.ts +11 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +44 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles.css +242 -0
- package/package.json +85 -0
- package/src/components/accordion.tsx +48 -0
- package/src/components/alert-dialog.tsx +113 -0
- package/src/components/audit-timeline.tsx +102 -0
- package/src/components/avatar.tsx +42 -0
- package/src/components/badge.tsx +34 -0
- package/src/components/breadcrumb.tsx +99 -0
- package/src/components/button.tsx +58 -0
- package/src/components/calendar.tsx +176 -0
- package/src/components/card.tsx +60 -0
- package/src/components/chart.tsx +558 -0
- package/src/components/checkbox.tsx +23 -0
- package/src/components/combobox.tsx +264 -0
- package/src/components/command.tsx +120 -0
- package/src/components/date-picker.tsx +221 -0
- package/src/components/date-range-input.tsx +295 -0
- package/src/components/dialog.tsx +94 -0
- package/src/components/diff-view.tsx +182 -0
- package/src/components/dropdown-menu.tsx +165 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +168 -0
- package/src/components/file-input.tsx +233 -0
- package/src/components/form.tsx +152 -0
- package/src/components/info-tooltip.tsx +40 -0
- package/src/components/input.tsx +55 -0
- package/src/components/json-editor.tsx +210 -0
- package/src/components/kbd.tsx +35 -0
- package/src/components/key-value-editor.tsx +423 -0
- package/src/components/keyboard-shortcuts-help.tsx +136 -0
- package/src/components/label.tsx +16 -0
- package/src/components/media-preview.tsx +278 -0
- package/src/components/multi-file-input.tsx +315 -0
- package/src/components/password-input.tsx +50 -0
- package/src/components/popover.tsx +26 -0
- package/src/components/revision-timeline.tsx +93 -0
- package/src/components/richtext-editor.tsx +624 -0
- package/src/components/richtext-mode.ts +39 -0
- package/src/components/richtext-render.tsx +51 -0
- package/src/components/richtext-sync.ts +57 -0
- package/src/components/scroll-area.tsx +41 -0
- package/src/components/select.tsx +200 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +109 -0
- package/src/components/sidebar.tsx +660 -0
- package/src/components/skeleton.tsx +9 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/switch.tsx +24 -0
- package/src/components/table.tsx +93 -0
- package/src/components/tabs.tsx +57 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/tooltip.tsx +25 -0
- package/src/index.ts +342 -0
- package/src/lib/theme.ts +45 -0
- package/src/lib/utils.ts +6 -0
- 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')
|