@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,264 @@
|
|
|
1
|
+
// Combobox — free-text autocomplete input.
|
|
2
|
+
//
|
|
3
|
+
// Distinct from `ReferenceCombobox` (in @modern-admin/react) which strictly
|
|
4
|
+
// constrains the value to an existing referenced record. This primitive is
|
|
5
|
+
// permissive: the user may type any string, and the suggestion list is
|
|
6
|
+
// purely advisory. Suggestions can be:
|
|
7
|
+
// • static (declared per field, e.g. enum-like hints), or
|
|
8
|
+
// • dynamic (loaded by the parent — e.g. distinct values pulled from a
|
|
9
|
+
// resource's column). The component itself is i18n-unaware and does
|
|
10
|
+
// no fetching: callers feed `suggestions` and toggle `loading`.
|
|
11
|
+
//
|
|
12
|
+
// Behaviour:
|
|
13
|
+
// • The input is fully controlled (`value` / `onChange`).
|
|
14
|
+
// • Suggestions filter as you type: case-insensitive substring match
|
|
15
|
+
// against label and value.
|
|
16
|
+
// • Down/Up cycle highlight; Enter commits the highlighted item or
|
|
17
|
+
// keeps the typed value if none is highlighted.
|
|
18
|
+
// • Escape closes the panel; click-outside closes it (handled by Radix
|
|
19
|
+
// Popover). Selecting an item sets the input value and closes.
|
|
20
|
+
//
|
|
21
|
+
// Mobile-first: the panel matches the input width via the
|
|
22
|
+
// `--radix-popover-trigger-width` CSS var.
|
|
23
|
+
//
|
|
24
|
+
// i18n: optional `labels` prop with English defaults. The React layer is
|
|
25
|
+
// expected to translate and feed them in.
|
|
26
|
+
|
|
27
|
+
import * as React from 'react'
|
|
28
|
+
import { ChevronDown, Loader2 } from 'lucide-react'
|
|
29
|
+
import { cn } from '../lib/utils.js'
|
|
30
|
+
import { Input } from './input.js'
|
|
31
|
+
import {
|
|
32
|
+
Popover,
|
|
33
|
+
PopoverAnchor,
|
|
34
|
+
PopoverContent,
|
|
35
|
+
} from './popover.js'
|
|
36
|
+
|
|
37
|
+
/** Each suggestion may be a bare string (used as both value + label) or
|
|
38
|
+
* an explicit `{ value, label }` pair. */
|
|
39
|
+
export type ComboboxSuggestion = string | { value: string; label: string }
|
|
40
|
+
|
|
41
|
+
export interface ComboboxLabels {
|
|
42
|
+
/** Shown while `loading` is true and the panel is open. Default: 'Loading…'. */
|
|
43
|
+
loading?: string
|
|
44
|
+
/**
|
|
45
|
+
* Shown when the panel is open, the user has typed something, but no
|
|
46
|
+
* suggestion matches. Default: 'No matches — press Enter to keep what
|
|
47
|
+
* you typed.' Use `{value}` to interpolate the current input value.
|
|
48
|
+
*/
|
|
49
|
+
noMatches?: string
|
|
50
|
+
/** Visually-hidden trigger label for screen readers. Default: 'Toggle suggestions'. */
|
|
51
|
+
toggleSuggestions?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ComboboxProps {
|
|
55
|
+
/** Current input value. */
|
|
56
|
+
value: string
|
|
57
|
+
/** Called on every keystroke and on suggestion pick. */
|
|
58
|
+
onChange(next: string): void
|
|
59
|
+
/** Called when the input loses focus (after the picker closes). */
|
|
60
|
+
onBlur?(): void
|
|
61
|
+
/** Static or pre-loaded suggestion list. May change while `loading`. */
|
|
62
|
+
suggestions?: ReadonlyArray<ComboboxSuggestion>
|
|
63
|
+
/** When true and the panel is open, render a small spinner. */
|
|
64
|
+
loading?: boolean
|
|
65
|
+
/** Disables the input and prevents the panel from opening. */
|
|
66
|
+
disabled?: boolean
|
|
67
|
+
placeholder?: string
|
|
68
|
+
/** Forwarded to the `<input>` for accessibility (paired with a `<label>`). */
|
|
69
|
+
id?: string
|
|
70
|
+
'aria-label'?: string
|
|
71
|
+
className?: string
|
|
72
|
+
/**
|
|
73
|
+
* Maximum suggestions to render after filtering. Default: 50. Keeps the
|
|
74
|
+
* panel snappy when callers pass thousands of distinct values.
|
|
75
|
+
*/
|
|
76
|
+
maxItems?: number
|
|
77
|
+
labels?: ComboboxLabels
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const defaultLabels: Required<ComboboxLabels> = {
|
|
81
|
+
loading: 'Loading…',
|
|
82
|
+
noMatches: 'No matches — press Enter to keep "{value}".',
|
|
83
|
+
toggleSuggestions: 'Toggle suggestions',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Normalise a `ComboboxSuggestion` into `{ value, label }`. */
|
|
87
|
+
const normalise = (s: ComboboxSuggestion): { value: string; label: string } =>
|
|
88
|
+
typeof s === 'string' ? { value: s, label: s } : s
|
|
89
|
+
|
|
90
|
+
/** Case-insensitive substring match on label OR value. */
|
|
91
|
+
const matchesQuery = (
|
|
92
|
+
s: { value: string; label: string },
|
|
93
|
+
q: string,
|
|
94
|
+
): boolean => {
|
|
95
|
+
if (!q) return true
|
|
96
|
+
const needle = q.toLowerCase()
|
|
97
|
+
return (
|
|
98
|
+
s.label.toLowerCase().includes(needle) ||
|
|
99
|
+
s.value.toLowerCase().includes(needle)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function Combobox({
|
|
104
|
+
value,
|
|
105
|
+
onChange,
|
|
106
|
+
onBlur,
|
|
107
|
+
suggestions,
|
|
108
|
+
loading,
|
|
109
|
+
disabled,
|
|
110
|
+
placeholder,
|
|
111
|
+
id,
|
|
112
|
+
'aria-label': ariaLabel,
|
|
113
|
+
className,
|
|
114
|
+
maxItems = 50,
|
|
115
|
+
labels,
|
|
116
|
+
}: ComboboxProps): React.ReactElement {
|
|
117
|
+
const l = { ...defaultLabels, ...labels }
|
|
118
|
+
const [focused, setFocused] = React.useState(false)
|
|
119
|
+
const [highlight, setHighlight] = React.useState(0)
|
|
120
|
+
|
|
121
|
+
const items = React.useMemo(() => {
|
|
122
|
+
const all = (suggestions ?? []).map(normalise)
|
|
123
|
+
return all.filter((s) => matchesQuery(s, value)).slice(0, maxItems)
|
|
124
|
+
}, [suggestions, value, maxItems])
|
|
125
|
+
|
|
126
|
+
// Reset highlight whenever the filtered set changes — otherwise the
|
|
127
|
+
// index can point past the end after typing a more specific query.
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
setHighlight(0)
|
|
130
|
+
}, [items.length])
|
|
131
|
+
|
|
132
|
+
// Open whenever the input is focused AND there's something to show
|
|
133
|
+
// (either matching items or a loading spinner). Empty + non-loading =
|
|
134
|
+
// no panel, so the component degrades to a plain input.
|
|
135
|
+
const open =
|
|
136
|
+
!disabled && focused && (items.length > 0 || Boolean(loading))
|
|
137
|
+
|
|
138
|
+
const commit = (s: { value: string; label: string }): void => {
|
|
139
|
+
onChange(s.value)
|
|
140
|
+
setFocused(false)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
144
|
+
if (!open) return
|
|
145
|
+
if (e.key === 'ArrowDown') {
|
|
146
|
+
e.preventDefault()
|
|
147
|
+
setHighlight((h) => Math.min(h + 1, items.length - 1))
|
|
148
|
+
} else if (e.key === 'ArrowUp') {
|
|
149
|
+
e.preventDefault()
|
|
150
|
+
setHighlight((h) => Math.max(h - 1, 0))
|
|
151
|
+
} else if (e.key === 'Enter') {
|
|
152
|
+
const picked = items[highlight]
|
|
153
|
+
if (picked) {
|
|
154
|
+
e.preventDefault()
|
|
155
|
+
commit(picked)
|
|
156
|
+
}
|
|
157
|
+
} else if (e.key === 'Escape') {
|
|
158
|
+
setFocused(false)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Popover open={open}>
|
|
164
|
+
<PopoverAnchor asChild>
|
|
165
|
+
<div className={cn('relative', className)}>
|
|
166
|
+
<Input
|
|
167
|
+
id={id}
|
|
168
|
+
type="text"
|
|
169
|
+
value={value}
|
|
170
|
+
placeholder={placeholder}
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
aria-label={ariaLabel}
|
|
173
|
+
aria-autocomplete="list"
|
|
174
|
+
aria-expanded={open}
|
|
175
|
+
autoComplete="off"
|
|
176
|
+
className="pr-8"
|
|
177
|
+
onFocus={() => setFocused(true)}
|
|
178
|
+
onBlur={() => {
|
|
179
|
+
// Defer so a click on a suggestion (which fires after blur)
|
|
180
|
+
// can still commit before we close the panel.
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
setFocused(false)
|
|
183
|
+
onBlur?.()
|
|
184
|
+
}, 120)
|
|
185
|
+
}}
|
|
186
|
+
onChange={(e) => onChange(e.target.value)}
|
|
187
|
+
onKeyDown={handleKeyDown}
|
|
188
|
+
/>
|
|
189
|
+
{/* Trailing affordance: spinner while loading, otherwise a chevron
|
|
190
|
+
hint that suggestions exist. Pure decoration — focusing the
|
|
191
|
+
input is what opens the panel. */}
|
|
192
|
+
<span
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
className="pointer-events-none absolute inset-y-0 right-2 flex items-center text-muted-foreground"
|
|
195
|
+
>
|
|
196
|
+
{loading ? (
|
|
197
|
+
<Loader2 className="size-4 animate-spin" />
|
|
198
|
+
) : (
|
|
199
|
+
<ChevronDown className="size-4 opacity-50" />
|
|
200
|
+
)}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
</PopoverAnchor>
|
|
204
|
+
<PopoverContent
|
|
205
|
+
align="start"
|
|
206
|
+
sideOffset={4}
|
|
207
|
+
// Prevent Radix from stealing focus from the input when the panel
|
|
208
|
+
// opens — otherwise typing would move focus to the popover root.
|
|
209
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
210
|
+
// Allow clicks inside the panel without closing the popover before
|
|
211
|
+
// the click handler on the option fires.
|
|
212
|
+
onInteractOutside={(e) => {
|
|
213
|
+
// The blur handler closes the panel; nothing more to do here.
|
|
214
|
+
// Preventing the default keeps Radix from re-toggling internal state.
|
|
215
|
+
e.preventDefault()
|
|
216
|
+
}}
|
|
217
|
+
className="w-[var(--radix-popover-trigger-width)] p-1"
|
|
218
|
+
>
|
|
219
|
+
{loading && items.length === 0 ? (
|
|
220
|
+
<div className="px-2 py-2 text-sm text-muted-foreground">
|
|
221
|
+
{l.loading}
|
|
222
|
+
</div>
|
|
223
|
+
) : items.length === 0 ? (
|
|
224
|
+
<div className="px-2 py-2 text-sm text-muted-foreground">
|
|
225
|
+
{l.noMatches.replace('{value}', value)}
|
|
226
|
+
</div>
|
|
227
|
+
) : (
|
|
228
|
+
<ul role="listbox" className="max-h-60 overflow-y-auto">
|
|
229
|
+
{items.map((s, i) => {
|
|
230
|
+
const active = i === highlight
|
|
231
|
+
return (
|
|
232
|
+
<li
|
|
233
|
+
key={s.value}
|
|
234
|
+
role="option"
|
|
235
|
+
aria-selected={active}
|
|
236
|
+
// Use mousedown (fires before input blur) so the click
|
|
237
|
+
// commits the value before the panel closes.
|
|
238
|
+
onMouseDown={(e) => {
|
|
239
|
+
e.preventDefault()
|
|
240
|
+
commit(s)
|
|
241
|
+
}}
|
|
242
|
+
onMouseEnter={() => setHighlight(i)}
|
|
243
|
+
className={cn(
|
|
244
|
+
'cursor-pointer rounded-sm px-2 py-1.5 text-sm',
|
|
245
|
+
active
|
|
246
|
+
? 'bg-accent text-accent-foreground'
|
|
247
|
+
: 'text-foreground',
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
<span className="truncate">{s.label}</span>
|
|
251
|
+
{s.label !== s.value ? (
|
|
252
|
+
<span className="ml-2 text-xs text-muted-foreground">
|
|
253
|
+
{s.value}
|
|
254
|
+
</span>
|
|
255
|
+
) : null}
|
|
256
|
+
</li>
|
|
257
|
+
)
|
|
258
|
+
})}
|
|
259
|
+
</ul>
|
|
260
|
+
)}
|
|
261
|
+
</PopoverContent>
|
|
262
|
+
</Popover>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Command as CommandPrimitive } from 'cmdk'
|
|
3
|
+
import { Search } from 'lucide-react'
|
|
4
|
+
import { cn } from '../lib/utils.js'
|
|
5
|
+
import { Dialog, DialogContent } from './dialog.js'
|
|
6
|
+
|
|
7
|
+
export const Command = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof CommandPrimitive>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<CommandPrimitive
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn(
|
|
14
|
+
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
))
|
|
20
|
+
Command.displayName = CommandPrimitive.displayName
|
|
21
|
+
|
|
22
|
+
export const CommandDialog = ({
|
|
23
|
+
children,
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof Dialog>): React.ReactElement => (
|
|
26
|
+
<Dialog {...props}>
|
|
27
|
+
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
|
28
|
+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
29
|
+
{children}
|
|
30
|
+
</Command>
|
|
31
|
+
</DialogContent>
|
|
32
|
+
</Dialog>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export const CommandInput = React.forwardRef<
|
|
36
|
+
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
37
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
38
|
+
>(({ className, ...props }, ref) => (
|
|
39
|
+
<div className="flex items-center border-b border-border px-3" cmdk-input-wrapper="">
|
|
40
|
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
41
|
+
<CommandPrimitive.Input
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
))
|
|
51
|
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
|
52
|
+
|
|
53
|
+
export const CommandList = React.forwardRef<
|
|
54
|
+
React.ElementRef<typeof CommandPrimitive.List>,
|
|
55
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
56
|
+
>(({ className, ...props }, ref) => (
|
|
57
|
+
<CommandPrimitive.List
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
))
|
|
63
|
+
CommandList.displayName = CommandPrimitive.List.displayName
|
|
64
|
+
|
|
65
|
+
export const CommandEmpty = React.forwardRef<
|
|
66
|
+
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
67
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
68
|
+
>((props, ref) => (
|
|
69
|
+
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
|
70
|
+
))
|
|
71
|
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
72
|
+
|
|
73
|
+
export const CommandGroup = React.forwardRef<
|
|
74
|
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
75
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
76
|
+
>(({ className, ...props }, ref) => (
|
|
77
|
+
<CommandPrimitive.Group
|
|
78
|
+
ref={ref}
|
|
79
|
+
className={cn(
|
|
80
|
+
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
87
|
+
|
|
88
|
+
export const CommandSeparator = React.forwardRef<
|
|
89
|
+
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
90
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
91
|
+
>(({ className, ...props }, ref) => (
|
|
92
|
+
<CommandPrimitive.Separator ref={ref} className={cn('-mx-1 h-px bg-border/40', className)} {...props} />
|
|
93
|
+
))
|
|
94
|
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
95
|
+
|
|
96
|
+
export const CommandItem = React.forwardRef<
|
|
97
|
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
98
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
99
|
+
>(({ className, ...props }, ref) => (
|
|
100
|
+
<CommandPrimitive.Item
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn(
|
|
103
|
+
'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
104
|
+
className,
|
|
105
|
+
)}
|
|
106
|
+
{...props}
|
|
107
|
+
/>
|
|
108
|
+
))
|
|
109
|
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
110
|
+
|
|
111
|
+
export const CommandShortcut = ({
|
|
112
|
+
className,
|
|
113
|
+
...props
|
|
114
|
+
}: React.HTMLAttributes<HTMLSpanElement>): React.ReactElement => (
|
|
115
|
+
<span
|
|
116
|
+
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
CommandShortcut.displayName = 'CommandShortcut'
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Popover-driven date / datetime input. Single picker handles both modes:
|
|
2
|
+
// pass `mode="datetime"` to surface an HH:MM time input below the calendar.
|
|
3
|
+
//
|
|
4
|
+
// Value is an ISO-ish string (`yyyy-MM-dd` for dates, `yyyy-MM-ddTHH:mm` for
|
|
5
|
+
// datetime — same shape <input type="date">/<input type="datetime-local">
|
|
6
|
+
// produce, so callers can stay format-stable). The trigger is a real text
|
|
7
|
+
// input so users can also type the date manually; clicks on the trailing
|
|
8
|
+
// calendar icon open the popover with the inline picker.
|
|
9
|
+
|
|
10
|
+
import * as React from 'react'
|
|
11
|
+
import { format, isValid, parse, parseISO } from 'date-fns'
|
|
12
|
+
import { Calendar as CalendarIcon } from 'lucide-react'
|
|
13
|
+
import { cn } from '../lib/utils.js'
|
|
14
|
+
import { Button } from './button.js'
|
|
15
|
+
import { Calendar } from './calendar.js'
|
|
16
|
+
import { Input } from './input.js'
|
|
17
|
+
import { Popover, PopoverContent, PopoverTrigger } from './popover.js'
|
|
18
|
+
|
|
19
|
+
export type DatePickerMode = 'date' | 'datetime'
|
|
20
|
+
|
|
21
|
+
export interface DatePickerProps {
|
|
22
|
+
value: string | null | undefined
|
|
23
|
+
onChange(next: string): void
|
|
24
|
+
mode?: DatePickerMode
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
placeholder?: string
|
|
27
|
+
/** Applied to the outer wrapper div (controls width, etc.). */
|
|
28
|
+
className?: string
|
|
29
|
+
/** Applied to the inner text Input — use to override height / font size. */
|
|
30
|
+
inputClassName?: string
|
|
31
|
+
/** ARIA label forwarded to the trigger input (mobile users / screen readers). */
|
|
32
|
+
ariaLabel?: string
|
|
33
|
+
/** ARIA label for the calendar icon button. Default: "Open calendar". */
|
|
34
|
+
openCalendarLabel?: string
|
|
35
|
+
/** Label for the time input shown in datetime mode. Default: "Time". */
|
|
36
|
+
timeLabel?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DATE_FMT = 'yyyy-MM-dd'
|
|
40
|
+
const DATETIME_FMT = "yyyy-MM-dd'T'HH:mm"
|
|
41
|
+
// Friendlier display format for the datetime input — space instead of `T`.
|
|
42
|
+
const DATETIME_DISPLAY_FMT = 'yyyy-MM-dd HH:mm'
|
|
43
|
+
|
|
44
|
+
function parseValue(value: string | null | undefined): Date | undefined {
|
|
45
|
+
if (!value) return undefined
|
|
46
|
+
const direct = value.length <= 10 ? parse(value, DATE_FMT, new Date()) : parseISO(value)
|
|
47
|
+
return Number.isNaN(direct.getTime()) ? undefined : direct
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Try to parse a manually-typed string in any of the supported shapes.
|
|
52
|
+
* Returns `undefined` on blank, `null` on invalid (non-blank) input so the
|
|
53
|
+
* caller can distinguish "cleared" from "typo".
|
|
54
|
+
*/
|
|
55
|
+
function parseTyped(raw: string, mode: DatePickerMode): Date | undefined | null {
|
|
56
|
+
const trimmed = raw.trim()
|
|
57
|
+
if (trimmed === '') return undefined
|
|
58
|
+
const candidates =
|
|
59
|
+
mode === 'datetime'
|
|
60
|
+
? [DATETIME_FMT, DATETIME_DISPLAY_FMT, "yyyy-MM-dd'T'HH:mm:ss", DATE_FMT]
|
|
61
|
+
: [DATE_FMT]
|
|
62
|
+
for (const fmt of candidates) {
|
|
63
|
+
const parsed = parse(trimmed, fmt, new Date())
|
|
64
|
+
if (isValid(parsed)) return parsed
|
|
65
|
+
}
|
|
66
|
+
// Last resort: ISO with timezone, etc.
|
|
67
|
+
const iso = parseISO(trimmed)
|
|
68
|
+
return isValid(iso) ? iso : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatForInput(date: Date | undefined, mode: DatePickerMode): string {
|
|
72
|
+
if (!date) return ''
|
|
73
|
+
return format(date, mode === 'datetime' ? DATETIME_DISPLAY_FMT : DATE_FMT)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatForApi(date: Date, mode: DatePickerMode): string {
|
|
77
|
+
return format(date, mode === 'datetime' ? DATETIME_FMT : DATE_FMT)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function DatePicker({
|
|
81
|
+
value,
|
|
82
|
+
onChange,
|
|
83
|
+
mode = 'date',
|
|
84
|
+
disabled,
|
|
85
|
+
placeholder,
|
|
86
|
+
className,
|
|
87
|
+
inputClassName,
|
|
88
|
+
ariaLabel,
|
|
89
|
+
openCalendarLabel = 'Open calendar',
|
|
90
|
+
timeLabel = 'Time',
|
|
91
|
+
}: DatePickerProps): React.ReactElement {
|
|
92
|
+
const [open, setOpen] = React.useState(false)
|
|
93
|
+
const date = parseValue(value)
|
|
94
|
+
// Local draft for the text input — keeps user typing intact even when
|
|
95
|
+
// intermediate strings don't yet parse.
|
|
96
|
+
const [draft, setDraft] = React.useState(() => formatForInput(date, mode))
|
|
97
|
+
// Re-sync draft whenever the canonical value changes from outside (e.g.
|
|
98
|
+
// calendar selection or form reset). Compare via formatted shape so a noop
|
|
99
|
+
// update doesn't clobber what the user is typing.
|
|
100
|
+
const lastFormatted = React.useRef(formatForInput(date, mode))
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
const next = formatForInput(date, mode)
|
|
103
|
+
if (next !== lastFormatted.current) {
|
|
104
|
+
lastFormatted.current = next
|
|
105
|
+
setDraft(next)
|
|
106
|
+
}
|
|
107
|
+
}, [value, mode]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
108
|
+
|
|
109
|
+
const time = date ? format(date, 'HH:mm') : '00:00'
|
|
110
|
+
|
|
111
|
+
const commitDate = (next: Date | undefined): void => {
|
|
112
|
+
if (!next) {
|
|
113
|
+
lastFormatted.current = ''
|
|
114
|
+
setDraft('')
|
|
115
|
+
onChange('')
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (mode === 'datetime') {
|
|
119
|
+
const [h, m] = time.split(':').map(Number)
|
|
120
|
+
next.setHours(h ?? 0, m ?? 0, 0, 0)
|
|
121
|
+
}
|
|
122
|
+
const formatted = formatForInput(next, mode)
|
|
123
|
+
lastFormatted.current = formatted
|
|
124
|
+
setDraft(formatted)
|
|
125
|
+
onChange(formatForApi(next, mode))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const setTime = (raw: string): void => {
|
|
129
|
+
if (!date) return
|
|
130
|
+
const [h, m] = raw.split(':').map(Number)
|
|
131
|
+
const next = new Date(date)
|
|
132
|
+
next.setHours(h ?? 0, m ?? 0, 0, 0)
|
|
133
|
+
const formatted = formatForInput(next, mode)
|
|
134
|
+
lastFormatted.current = formatted
|
|
135
|
+
setDraft(formatted)
|
|
136
|
+
onChange(formatForApi(next, mode))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
140
|
+
const text = e.target.value
|
|
141
|
+
setDraft(text)
|
|
142
|
+
const parsed = parseTyped(text, mode)
|
|
143
|
+
if (parsed === undefined) {
|
|
144
|
+
// cleared
|
|
145
|
+
lastFormatted.current = ''
|
|
146
|
+
onChange('')
|
|
147
|
+
} else if (parsed) {
|
|
148
|
+
lastFormatted.current = formatForInput(parsed, mode)
|
|
149
|
+
onChange(formatForApi(parsed, mode))
|
|
150
|
+
}
|
|
151
|
+
// parsed === null → keep draft, don't fire onChange yet
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
155
|
+
// On blur, if the draft is invalid, snap back to the canonical value.
|
|
156
|
+
const parsed = parseTyped(draft, mode)
|
|
157
|
+
if (parsed === null) {
|
|
158
|
+
const restored = formatForInput(date, mode)
|
|
159
|
+
lastFormatted.current = restored
|
|
160
|
+
setDraft(restored)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const inputPlaceholder =
|
|
165
|
+
placeholder ?? (mode === 'datetime' ? 'YYYY-MM-DD HH:MM' : 'YYYY-MM-DD')
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
169
|
+
<div className={cn('relative w-full', className)}>
|
|
170
|
+
<Input
|
|
171
|
+
type="text"
|
|
172
|
+
inputMode="numeric"
|
|
173
|
+
value={draft}
|
|
174
|
+
onChange={handleInputChange}
|
|
175
|
+
onBlur={handleInputBlur}
|
|
176
|
+
placeholder={inputPlaceholder}
|
|
177
|
+
disabled={disabled}
|
|
178
|
+
aria-label={ariaLabel}
|
|
179
|
+
className={cn(inputClassName, 'pr-10')}
|
|
180
|
+
/>
|
|
181
|
+
<PopoverTrigger asChild>
|
|
182
|
+
<Button
|
|
183
|
+
type="button"
|
|
184
|
+
variant="ghost"
|
|
185
|
+
size="icon"
|
|
186
|
+
disabled={disabled}
|
|
187
|
+
aria-label={openCalendarLabel}
|
|
188
|
+
className="absolute right-1 top-1/2 size-8 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
189
|
+
>
|
|
190
|
+
<CalendarIcon className="size-4" />
|
|
191
|
+
</Button>
|
|
192
|
+
</PopoverTrigger>
|
|
193
|
+
</div>
|
|
194
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
195
|
+
<Calendar
|
|
196
|
+
mode="single"
|
|
197
|
+
selected={date}
|
|
198
|
+
defaultMonth={date}
|
|
199
|
+
onSelect={(d) => {
|
|
200
|
+
commitDate(d)
|
|
201
|
+
if (mode === 'date') setOpen(false)
|
|
202
|
+
}}
|
|
203
|
+
autoFocus
|
|
204
|
+
/>
|
|
205
|
+
{mode === 'datetime' && (
|
|
206
|
+
<div className="flex items-center gap-2 border-t p-3">
|
|
207
|
+
<span className="text-xs text-muted-foreground">{timeLabel}</span>
|
|
208
|
+
<Input
|
|
209
|
+
type="time"
|
|
210
|
+
value={time}
|
|
211
|
+
disabled={!date}
|
|
212
|
+
onChange={(e) => setTime(e.target.value)}
|
|
213
|
+
className="h-8 w-32"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</PopoverContent>
|
|
218
|
+
</Popover>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
DatePicker.displayName = 'DatePicker'
|