@spree/dashboard-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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/package.json +86 -0
  4. package/src/hooks/use-copy-to-clipboard.ts +44 -0
  5. package/src/hooks/use-mobile.ts +19 -0
  6. package/src/hooks/use-scrolled.ts +30 -0
  7. package/src/index.ts +99 -0
  8. package/src/lib/date-locale.ts +29 -0
  9. package/src/lib/utils.ts +6 -0
  10. package/src/lib/validation-messages.ts +19 -0
  11. package/src/spree/address-block.tsx +74 -0
  12. package/src/spree/back-button.tsx +48 -0
  13. package/src/spree/bulk-dialog.tsx +71 -0
  14. package/src/spree/bulk-price-table.tsx +254 -0
  15. package/src/spree/calculator-summary.tsx +120 -0
  16. package/src/spree/color-picker.tsx +202 -0
  17. package/src/spree/confirm-dialog.tsx +82 -0
  18. package/src/spree/copy-to-clipboard-button.tsx +90 -0
  19. package/src/spree/country-flag.tsx +30 -0
  20. package/src/spree/data-grid/cells.tsx +415 -0
  21. package/src/spree/data-grid/context.ts +59 -0
  22. package/src/spree/data-grid/data-grid.tsx +214 -0
  23. package/src/spree/data-grid/fill-handle.tsx +153 -0
  24. package/src/spree/data-grid/helpers.ts +18 -0
  25. package/src/spree/data-grid/index.ts +5 -0
  26. package/src/spree/data-grid/types.ts +36 -0
  27. package/src/spree/data-grid/use-data-grid-keyboard.ts +254 -0
  28. package/src/spree/data-grid/use-stable-cell-registration.ts +45 -0
  29. package/src/spree/drag-handle.tsx +51 -0
  30. package/src/spree/form-actions.tsx +140 -0
  31. package/src/spree/json-preview-drawer.tsx +254 -0
  32. package/src/spree/json-value-view.tsx +149 -0
  33. package/src/spree/language-menu-items.tsx +54 -0
  34. package/src/spree/metadata/metadata-card.tsx +62 -0
  35. package/src/spree/relative-time.tsx +49 -0
  36. package/src/spree/resource-combobox.tsx +108 -0
  37. package/src/spree/resource-layout.tsx +31 -0
  38. package/src/spree/resource-multi-autocomplete.tsx +180 -0
  39. package/src/spree/resource-name-cell.tsx +44 -0
  40. package/src/spree/route-error-boundary.tsx +102 -0
  41. package/src/spree/row-actions.tsx +85 -0
  42. package/src/spree/row-click-bridge.tsx +17 -0
  43. package/src/spree/secret-input.tsx +137 -0
  44. package/src/spree/storefront-visible-switch.tsx +58 -0
  45. package/src/spree/tag-list.tsx +26 -0
  46. package/src/spree/theme-provider.tsx +83 -0
  47. package/src/spree/theme-toggle.tsx +35 -0
  48. package/src/styles.css +338 -0
  49. package/src/ui/attachment.tsx +187 -0
  50. package/src/ui/avatar.tsx +94 -0
  51. package/src/ui/badge.tsx +161 -0
  52. package/src/ui/breadcrumb.tsx +109 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/calendar.tsx +66 -0
  55. package/src/ui/card.tsx +72 -0
  56. package/src/ui/chart.tsx +94 -0
  57. package/src/ui/checkbox.tsx +50 -0
  58. package/src/ui/collapsible.tsx +18 -0
  59. package/src/ui/combobox.tsx +270 -0
  60. package/src/ui/command.tsx +172 -0
  61. package/src/ui/data-table.tsx +60 -0
  62. package/src/ui/date-picker.tsx +253 -0
  63. package/src/ui/date-range-picker.tsx +150 -0
  64. package/src/ui/dialog.tsx +214 -0
  65. package/src/ui/dropdown-menu.tsx +345 -0
  66. package/src/ui/empty.tsx +94 -0
  67. package/src/ui/field.tsx +221 -0
  68. package/src/ui/input-group.tsx +141 -0
  69. package/src/ui/input.tsx +19 -0
  70. package/src/ui/label.tsx +29 -0
  71. package/src/ui/pagination.tsx +123 -0
  72. package/src/ui/popover.tsx +76 -0
  73. package/src/ui/progress.tsx +73 -0
  74. package/src/ui/radio-group.tsx +42 -0
  75. package/src/ui/rich-text-editor.tsx +263 -0
  76. package/src/ui/select.tsx +213 -0
  77. package/src/ui/separator.tsx +31 -0
  78. package/src/ui/sheet.tsx +208 -0
  79. package/src/ui/sidebar.tsx +679 -0
  80. package/src/ui/skeleton.tsx +13 -0
  81. package/src/ui/slot.tsx +35 -0
  82. package/src/ui/sonner.tsx +47 -0
  83. package/src/ui/switch.tsx +39 -0
  84. package/src/ui/textarea.tsx +18 -0
  85. package/src/ui/tooltip.tsx +100 -0
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from 'react'
2
+ import { cn } from '../lib/utils'
3
+
4
+ function Table({ className, ...props }: React.ComponentProps<'table'>) {
5
+ return (
6
+ <div className="overflow-x-auto">
7
+ <table className={cn('w-full align-top text-foreground', className)} {...props} />
8
+ </div>
9
+ )
10
+ }
11
+
12
+ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
13
+ return <thead className={cn('align-bottom', className)} {...props} />
14
+ }
15
+
16
+ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
17
+ return <tbody className={cn('align-middle', className)} {...props} />
18
+ }
19
+
20
+ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
21
+ return (
22
+ <tr className={cn('group/row hover:bg-muted/60 last:*:border-b-0', className)} {...props} />
23
+ )
24
+ }
25
+
26
+ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
27
+ return (
28
+ <th
29
+ className={cn(
30
+ 'text-left text-sm font-medium text-muted-foreground bg-muted/50 p-2 border-b border-border whitespace-nowrap first:pl-4 last:pr-4',
31
+ className,
32
+ )}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+
38
+ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
39
+ return (
40
+ <td
41
+ className={cn(
42
+ 'py-3 px-2 border-b border-border align-middle first:pl-4 last:pr-4 group-last/row:first:rounded-bl-xl group-last/row:last:rounded-br-xl',
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function TableEmpty({ children, colSpan }: { children: ReactNode; colSpan: number }) {
51
+ return (
52
+ <tr>
53
+ <td colSpan={colSpan} className="py-12 text-center text-muted-foreground">
54
+ {children}
55
+ </td>
56
+ </tr>
57
+ )
58
+ }
59
+
60
+ export { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow }
@@ -0,0 +1,253 @@
1
+ import { format, parse, parseISO } from 'date-fns'
2
+ import { formatInTimeZone, fromZonedTime, toZonedTime } from 'date-fns-tz'
3
+ import i18n from 'i18next'
4
+ import { CalendarIcon, XIcon } from 'lucide-react'
5
+ import { useEffect, useMemo, useRef, useState } from 'react'
6
+ import { activeDateLocale } from '../lib/date-locale'
7
+ import { cn } from '../lib/utils'
8
+ import { Button } from './button'
9
+ import { Calendar } from './calendar'
10
+ import { Input } from './input'
11
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
12
+
13
+ interface DatePickerProps {
14
+ value?: string | null
15
+ onChange?: (value: string | null) => void
16
+ placeholder?: string
17
+ className?: string
18
+ disabled?: boolean
19
+ includeTime?: boolean
20
+ /**
21
+ * IANA timezone (e.g. `Europe/Berlin`) used to interpret datetime values.
22
+ * Defaults to the browser's resolved timezone. **Pass the store timezone
23
+ * for any admin-facing surface** so that wall-clock times mean the same
24
+ * thing for every admin regardless of where they're logged in from. Use
25
+ * `<StoreDatePicker>` (in `components/spree/`) to do this automatically.
26
+ *
27
+ * Date-only mode (`includeTime=false`) is timezone-agnostic — values are
28
+ * plain `yyyy-MM-dd` strings with no clock component.
29
+ */
30
+ timezone?: string
31
+ /**
32
+ * Render the calendar panel inline (absolute-positioned beneath the
33
+ * trigger) instead of via Base UI's `<Popover>` Portal. Set this on any
34
+ * usage nested inside a `<Sheet>` — Base UI's Popover Portal silently
35
+ * fails to mount inside deeply nested portal trees, so callers in form
36
+ * sheets / edit drawers must opt in. Filter panels and page-level forms
37
+ * keep the default Popover behavior.
38
+ */
39
+ inline?: boolean
40
+ }
41
+
42
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/
43
+
44
+ // Parse the inbound value into a Date whose **browser-local components**
45
+ // match the *store-local* date/time the value represents:
46
+ // - `yyyy-MM-dd` (date-only): timezone-agnostic; parse in browser-local.
47
+ // - Full ISO timestamp (datetime): UTC instant reinterpreted in store TZ.
48
+ function parseValue(value: string | null | undefined, tz: string): Date | undefined {
49
+ if (!value) return undefined
50
+ if (DATE_ONLY.test(value)) return parse(value, 'yyyy-MM-dd', new Date())
51
+ return toZonedTime(parseISO(value), tz)
52
+ }
53
+
54
+ // Format the value for the trigger button. Date-only uses the locally-anchored
55
+ // Date; datetime uses store-TZ formatting on the original UTC instant.
56
+ function formatTriggerLabel(
57
+ value: string | null | undefined,
58
+ date: Date | undefined,
59
+ tz: string,
60
+ includeTime: boolean,
61
+ placeholder: string,
62
+ ): string {
63
+ if (!date || Number.isNaN(date.getTime())) return placeholder
64
+ const locale = activeDateLocale()
65
+ if (includeTime && value && !DATE_ONLY.test(value)) {
66
+ return formatInTimeZone(parseISO(value), tz, 'PPP p', { locale })
67
+ }
68
+ return format(date, includeTime ? 'PPP p' : 'PPP', { locale })
69
+ }
70
+
71
+ function DatePicker({
72
+ value,
73
+ onChange,
74
+ placeholder = i18n.t('admin.common.pick_date'),
75
+ className,
76
+ disabled = false,
77
+ includeTime = false,
78
+ timezone,
79
+ inline = false,
80
+ }: DatePickerProps) {
81
+ const [open, setOpen] = useState(false)
82
+ const containerRef = useRef<HTMLDivElement | null>(null)
83
+
84
+ // Browser TZ is the fallback — used in non-admin contexts (Storybook,
85
+ // isolated tests) where there's no store context to pull from.
86
+ const tz = useMemo(
87
+ () => timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC',
88
+ [timezone],
89
+ )
90
+
91
+ // The Calendar and trigger label both read browser-local components, so
92
+ // anchoring them to store-local wall-clock is what makes the UI correct.
93
+ const date = useMemo(() => parseValue(value, tz), [value, tz])
94
+ const isValidDate = !!date && !Number.isNaN(date.getTime())
95
+
96
+ // Inline mode only: close on outside pointerdown / Escape. Popover mode
97
+ // gets this behavior from Base UI.
98
+ useEffect(() => {
99
+ if (!inline || !open) return
100
+ function handle(event: PointerEvent) {
101
+ const target = event.target as Node | null
102
+ if (!target || !containerRef.current) return
103
+ if (!containerRef.current.contains(target)) setOpen(false)
104
+ }
105
+ function handleKey(event: KeyboardEvent) {
106
+ if (event.key === 'Escape') setOpen(false)
107
+ }
108
+ document.addEventListener('pointerdown', handle)
109
+ document.addEventListener('keydown', handleKey)
110
+ return () => {
111
+ document.removeEventListener('pointerdown', handle)
112
+ document.removeEventListener('keydown', handleKey)
113
+ }
114
+ }, [inline, open])
115
+
116
+ const handleSelect = (selected: Date | undefined) => {
117
+ if (!selected) {
118
+ onChange?.(null)
119
+ return
120
+ }
121
+
122
+ if (!includeTime) {
123
+ // Date-only: emit `yyyy-MM-dd` using browser-local components. The
124
+ // user clicked a specific calendar day; the day number is what
125
+ // matters, not the timezone.
126
+ onChange?.(format(selected, 'yyyy-MM-dd'))
127
+ setOpen(false)
128
+ return
129
+ }
130
+
131
+ // Datetime: the picked Date has browser-local components for the
132
+ // selected day at (preserved or zero) hours/minutes. Reinterpret those
133
+ // wall-clock components as **store-local time**, then emit the UTC
134
+ // instant. `fromZonedTime` does exactly this conversion.
135
+ if (date && isValidDate) {
136
+ selected.setHours(date.getHours(), date.getMinutes())
137
+ }
138
+ onChange?.(fromZonedTime(selected, tz).toISOString())
139
+ }
140
+
141
+ const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
142
+ const raw = e.target.value
143
+ if (!raw) {
144
+ onChange?.(null)
145
+ return
146
+ }
147
+ const [hours, minutes] = raw.split(':').map(Number)
148
+ if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return
149
+ const d = date && isValidDate ? new Date(date) : new Date()
150
+ d.setHours(hours, minutes, 0, 0)
151
+ onChange?.(fromZonedTime(d, tz).toISOString())
152
+ }
153
+
154
+ const handleClear = (e: React.SyntheticEvent) => {
155
+ e.preventDefault()
156
+ e.stopPropagation()
157
+ onChange?.(null)
158
+ }
159
+
160
+ const timeValue = useMemo(
161
+ () =>
162
+ isValidDate && date
163
+ ? `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
164
+ : '',
165
+ [isValidDate, date],
166
+ )
167
+
168
+ const triggerLabel = useMemo(
169
+ () => formatTriggerLabel(value, date, tz, includeTime, placeholder),
170
+ [value, date, tz, includeTime, placeholder],
171
+ )
172
+
173
+ const triggerChildren = (
174
+ <>
175
+ <CalendarIcon className="size-4 shrink-0 text-muted-foreground" />
176
+ <span className="flex-1 truncate">{triggerLabel}</span>
177
+ {isValidDate && (
178
+ <span
179
+ role="button"
180
+ tabIndex={0}
181
+ aria-label={i18n.t('admin.a11y.clear_date')}
182
+ className="inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
183
+ onClick={handleClear}
184
+ onKeyDown={(e) => {
185
+ if (e.key === 'Enter' || e.key === ' ') handleClear(e)
186
+ }}
187
+ >
188
+ <XIcon className="size-3.5" />
189
+ </span>
190
+ )}
191
+ </>
192
+ )
193
+
194
+ const panelChildren = (
195
+ <>
196
+ <Calendar
197
+ mode="single"
198
+ selected={isValidDate ? date : undefined}
199
+ onSelect={handleSelect}
200
+ defaultMonth={isValidDate ? date : undefined}
201
+ />
202
+ {includeTime && (
203
+ <div className="border-t border-border px-3 py-2">
204
+ <Input type="time" value={timeValue} onChange={handleTimeChange} />
205
+ </div>
206
+ )}
207
+ </>
208
+ )
209
+
210
+ if (inline) {
211
+ return (
212
+ <div ref={containerRef} className={cn('relative', className)}>
213
+ <Button
214
+ type="button"
215
+ variant="outline"
216
+ data-empty={!isValidDate}
217
+ disabled={disabled}
218
+ aria-haspopup="dialog"
219
+ aria-expanded={open}
220
+ onClick={() => setOpen((v) => !v)}
221
+ className="w-full justify-start"
222
+ >
223
+ {triggerChildren}
224
+ </Button>
225
+ {open && (
226
+ <div
227
+ role="dialog"
228
+ aria-label={placeholder}
229
+ className="absolute top-full left-0 z-50 mt-2 w-auto rounded-lg border border-border bg-popover p-0 text-popover-foreground shadow-md"
230
+ >
231
+ {panelChildren}
232
+ </div>
233
+ )}
234
+ </div>
235
+ )
236
+ }
237
+
238
+ return (
239
+ <Popover open={open} onOpenChange={setOpen}>
240
+ <PopoverTrigger asChild disabled={disabled}>
241
+ <Button type="button" variant="outline" data-empty={!isValidDate} className={className}>
242
+ {triggerChildren}
243
+ </Button>
244
+ </PopoverTrigger>
245
+ <PopoverContent className="w-auto p-0" align="start">
246
+ {panelChildren}
247
+ </PopoverContent>
248
+ </Popover>
249
+ )
250
+ }
251
+
252
+ export type { DatePickerProps }
253
+ export { DatePicker }
@@ -0,0 +1,150 @@
1
+ import { format, startOfMonth, startOfYear, subDays } from 'date-fns'
2
+ import { CalendarIcon, ChevronDownIcon } from 'lucide-react'
3
+ import { useState } from 'react'
4
+ import type { DateRange as DayPickerDateRange } from 'react-day-picker'
5
+ import { useTranslation } from 'react-i18next'
6
+ import { activeDateLocale } from '../lib/date-locale'
7
+ import { cn } from '../lib/utils'
8
+ import { Button } from './button'
9
+ import { Calendar } from './calendar'
10
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
11
+
12
+ export interface DateRange {
13
+ from: Date
14
+ to: Date
15
+ }
16
+
17
+ type PresetKey = '7d' | '14d' | '30d' | '90d' | 'this_month' | 'ytd' | 'custom'
18
+
19
+ interface Preset {
20
+ key: PresetKey
21
+ labelKey: string
22
+ value: () => DateRange
23
+ }
24
+
25
+ const presets: Preset[] = [
26
+ {
27
+ key: '7d',
28
+ labelKey: 'admin.components.date_range_picker.presets.last_7_days',
29
+ value: () => ({ from: subDays(new Date(), 7), to: new Date() }),
30
+ },
31
+ {
32
+ key: '14d',
33
+ labelKey: 'admin.components.date_range_picker.presets.last_14_days',
34
+ value: () => ({ from: subDays(new Date(), 14), to: new Date() }),
35
+ },
36
+ {
37
+ key: '30d',
38
+ labelKey: 'admin.components.date_range_picker.presets.last_30_days',
39
+ value: () => ({ from: subDays(new Date(), 30), to: new Date() }),
40
+ },
41
+ {
42
+ key: '90d',
43
+ labelKey: 'admin.components.date_range_picker.presets.last_90_days',
44
+ value: () => ({ from: subDays(new Date(), 90), to: new Date() }),
45
+ },
46
+ {
47
+ key: 'this_month',
48
+ labelKey: 'admin.components.date_range_picker.presets.this_month',
49
+ value: () => ({ from: startOfMonth(new Date()), to: new Date() }),
50
+ },
51
+ {
52
+ key: 'ytd',
53
+ labelKey: 'admin.components.date_range_picker.presets.year_to_date',
54
+ value: () => ({ from: startOfYear(new Date()), to: new Date() }),
55
+ },
56
+ ]
57
+
58
+ interface DateRangePickerProps {
59
+ value: DateRange
60
+ onChange: (range: DateRange) => void
61
+ }
62
+
63
+ export function DateRangePicker({ value, onChange }: DateRangePickerProps) {
64
+ const { t } = useTranslation()
65
+ const [open, setOpen] = useState(false)
66
+ const [activePreset, setActivePreset] = useState<PresetKey>('30d')
67
+ const [calendarRange, setCalendarRange] = useState<DayPickerDateRange | undefined>()
68
+
69
+ const activePresetLabelKey =
70
+ presets.find((p) => p.key === activePreset)?.labelKey ??
71
+ 'admin.components.date_range_picker.presets.last_30_days'
72
+
73
+ const dateLocale = activeDateLocale()
74
+ const triggerLabel =
75
+ activePreset === 'custom'
76
+ ? `${format(value.from, 'MMM d', { locale: dateLocale })} – ${format(value.to, 'MMM d', { locale: dateLocale })}`
77
+ : t(activePresetLabelKey)
78
+
79
+ function selectPreset(preset: Preset) {
80
+ const range = preset.value()
81
+ setActivePreset(preset.key)
82
+ setCalendarRange({ from: range.from, to: range.to })
83
+ onChange(range)
84
+ setOpen(false)
85
+ }
86
+
87
+ function applyCustomRange() {
88
+ if (calendarRange?.from && calendarRange?.to) {
89
+ setActivePreset('custom')
90
+ onChange({ from: calendarRange.from, to: calendarRange.to })
91
+ setOpen(false)
92
+ }
93
+ }
94
+
95
+ const canApply = calendarRange?.from && calendarRange?.to
96
+
97
+ return (
98
+ <Popover
99
+ open={open}
100
+ onOpenChange={(nextOpen) => {
101
+ setOpen(nextOpen)
102
+ if (nextOpen) {
103
+ setCalendarRange({ from: value.from, to: value.to })
104
+ }
105
+ }}
106
+ >
107
+ <PopoverTrigger asChild>
108
+ <Button size="sm" variant="outline" className="h-8 gap-2 text-sm font-normal">
109
+ <CalendarIcon className="size-3.5" />
110
+ {triggerLabel}
111
+ <ChevronDownIcon className="size-3.5 text-muted-foreground" />
112
+ </Button>
113
+ </PopoverTrigger>
114
+ <PopoverContent align="end" className="w-auto p-0">
115
+ <div className="flex">
116
+ <div className="flex flex-col border-r py-2">
117
+ {presets.map((preset) => (
118
+ <button
119
+ key={preset.key}
120
+ type="button"
121
+ onClick={() => selectPreset(preset)}
122
+ className={cn(
123
+ 'whitespace-nowrap px-4 py-1.5 text-left text-sm transition-colors hover:bg-accent',
124
+ activePreset === preset.key && 'font-medium text-foreground',
125
+ activePreset !== preset.key && 'text-muted-foreground',
126
+ )}
127
+ >
128
+ {t(preset.labelKey)}
129
+ </button>
130
+ ))}
131
+ </div>
132
+ <div className="flex flex-col p-3">
133
+ <Calendar
134
+ mode="range"
135
+ defaultMonth={calendarRange?.from}
136
+ selected={calendarRange}
137
+ onSelect={setCalendarRange}
138
+ numberOfMonths={2}
139
+ />
140
+ <div className="flex justify-end border-t pt-3">
141
+ <Button size="sm" disabled={!canApply} onClick={applyCustomRange}>
142
+ {t('admin.actions.apply')}
143
+ </Button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </PopoverContent>
148
+ </Popover>
149
+ )
150
+ }
@@ -0,0 +1,214 @@
1
+ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
2
+ import i18n from 'i18next'
3
+ import { XIcon } from 'lucide-react'
4
+ import * as React from 'react'
5
+ import { cn } from '../lib/utils'
6
+ import { Button } from './button'
7
+
8
+ function Dialog({
9
+ children,
10
+ open,
11
+ defaultOpen,
12
+ onOpenChange,
13
+ modal,
14
+ ...props
15
+ }: {
16
+ children?: React.ReactNode
17
+ open?: boolean
18
+ defaultOpen?: boolean
19
+ onOpenChange?: (open: boolean, eventDetails: DialogPrimitive.Root.ChangeEventDetails) => void
20
+ modal?: boolean | 'trap-focus'
21
+ }) {
22
+ return (
23
+ <DialogPrimitive.Root
24
+ data-slot="dialog"
25
+ open={open}
26
+ defaultOpen={defaultOpen}
27
+ onOpenChange={onOpenChange}
28
+ modal={modal}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </DialogPrimitive.Root>
33
+ )
34
+ }
35
+
36
+ function DialogTrigger({
37
+ asChild,
38
+ children,
39
+ ...props
40
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger> & {
41
+ asChild?: boolean
42
+ }) {
43
+ if (asChild && React.isValidElement(children)) {
44
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" render={children} {...props} />
45
+ }
46
+ return (
47
+ <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props}>
48
+ {children}
49
+ </DialogPrimitive.Trigger>
50
+ )
51
+ }
52
+
53
+ function DialogPortal({ children, ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
54
+ return (
55
+ <DialogPrimitive.Portal data-slot="dialog-portal" {...props}>
56
+ {children}
57
+ </DialogPrimitive.Portal>
58
+ )
59
+ }
60
+
61
+ function DialogClose({
62
+ asChild,
63
+ children,
64
+ ...props
65
+ }: React.ComponentProps<typeof DialogPrimitive.Close> & {
66
+ asChild?: boolean
67
+ }) {
68
+ if (asChild && React.isValidElement(children)) {
69
+ return <DialogPrimitive.Close data-slot="dialog-close" render={children} {...props} />
70
+ }
71
+ return (
72
+ <DialogPrimitive.Close data-slot="dialog-close" {...props}>
73
+ {children}
74
+ </DialogPrimitive.Close>
75
+ )
76
+ }
77
+
78
+ function DialogOverlay({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof DialogPrimitive.Backdrop>) {
82
+ return (
83
+ <DialogPrimitive.Backdrop
84
+ data-slot="dialog-overlay"
85
+ className={cn(
86
+ 'fixed inset-0 isolate z-50 bg-gray-100/75 dark:bg-black/60 duration-100 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 transition-opacity',
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function DialogContent({
95
+ className,
96
+ children,
97
+ showCloseButton = true,
98
+ ...props
99
+ }: React.ComponentProps<typeof DialogPrimitive.Popup> & {
100
+ showCloseButton?: boolean
101
+ }) {
102
+ return (
103
+ <DialogPortal>
104
+ <DialogOverlay />
105
+ <DialogPrimitive.Popup
106
+ data-slot="dialog-content"
107
+ className={cn(
108
+ 'fixed top-1/2 left-1/2 z-50 flex w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-border bg-card text-foreground text-sm shadow-lg duration-100 outline-none sm:max-w-[500px] data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform]',
109
+ className,
110
+ )}
111
+ style={{ maxHeight: '90vh' }}
112
+ {...props}
113
+ >
114
+ {children}
115
+ {showCloseButton && (
116
+ <DialogPrimitive.Close
117
+ data-slot="dialog-close"
118
+ render={
119
+ <Button variant="ghost" className="absolute top-3 right-3" size="icon-sm">
120
+ <XIcon />
121
+ <span className="sr-only">{i18n.t('admin.actions.close')}</span>
122
+ </Button>
123
+ }
124
+ />
125
+ )}
126
+ </DialogPrimitive.Popup>
127
+ </DialogPortal>
128
+ )
129
+ }
130
+
131
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
132
+ return (
133
+ <div
134
+ data-slot="dialog-header"
135
+ className={cn('flex flex-col gap-1.5 border-b p-4', className)}
136
+ {...props}
137
+ />
138
+ )
139
+ }
140
+
141
+ function DialogBody({ className, ...props }: React.ComponentProps<'div'>) {
142
+ return (
143
+ <div
144
+ data-slot="dialog-body"
145
+ className={cn('flex-auto overflow-y-auto p-4', className)}
146
+ {...props}
147
+ />
148
+ )
149
+ }
150
+
151
+ function DialogFooter({
152
+ className,
153
+ showCloseButton = false,
154
+ children,
155
+ ...props
156
+ }: React.ComponentProps<'div'> & {
157
+ showCloseButton?: boolean
158
+ }) {
159
+ return (
160
+ <div
161
+ data-slot="dialog-footer"
162
+ className={cn(
163
+ 'flex flex-col-reverse gap-2 border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
164
+ className,
165
+ )}
166
+ {...props}
167
+ >
168
+ {children}
169
+ {showCloseButton && (
170
+ <DialogPrimitive.Close render={<Button>{i18n.t('admin.actions.close')}</Button>} />
171
+ )}
172
+ </div>
173
+ )
174
+ }
175
+
176
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
177
+ return (
178
+ <DialogPrimitive.Title
179
+ data-slot="dialog-title"
180
+ className={cn('text-base leading-none font-medium', className)}
181
+ {...props}
182
+ />
183
+ )
184
+ }
185
+
186
+ function DialogDescription({
187
+ className,
188
+ ...props
189
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
190
+ return (
191
+ <DialogPrimitive.Description
192
+ data-slot="dialog-description"
193
+ className={cn(
194
+ 'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
195
+ className,
196
+ )}
197
+ {...props}
198
+ />
199
+ )
200
+ }
201
+
202
+ export {
203
+ Dialog,
204
+ DialogBody,
205
+ DialogClose,
206
+ DialogContent,
207
+ DialogDescription,
208
+ DialogFooter,
209
+ DialogHeader,
210
+ DialogOverlay,
211
+ DialogPortal,
212
+ DialogTitle,
213
+ DialogTrigger,
214
+ }