@open-mercato/ui 0.6.1-develop.3256.1.fe3dec2464 → 0.6.1-develop.3276.1.1fcd145078
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/.turbo/turbo-build.log +1 -1
- package/dist/backend/CrudForm.js +46 -129
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +10 -3
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/EmptyState.js +1 -47
- package/dist/backend/EmptyState.js.map +2 -2
- package/dist/backend/FilterOverlay.js +31 -26
- package/dist/backend/FilterOverlay.js.map +2 -2
- package/dist/backend/FlashMessages.js +23 -25
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/JsonBuilder.js +4 -2
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/LogList.js +49 -0
- package/dist/backend/LogList.js.map +7 -0
- package/dist/backend/crud/CollapsibleGroup.js +13 -3
- package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +80 -32
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +60 -6
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/DatePicker.js +6 -142
- package/dist/backend/inputs/DatePicker.js.map +2 -2
- package/dist/backend/inputs/DateTimePicker.js +6 -182
- package/dist/backend/inputs/DateTimePicker.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +16 -19
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/inputs/TimeInput.js +1 -1
- package/dist/backend/inputs/TimeInput.js.map +2 -2
- package/dist/backend/inputs/TimePicker.js +83 -72
- package/dist/backend/inputs/TimePicker.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +2 -2
- package/dist/backend/utils/customFieldForms.js.map +1 -1
- package/dist/portal/components/PortalEmptyState.js +13 -9
- package/dist/portal/components/PortalEmptyState.js.map +2 -2
- package/dist/primitives/accordion.js +137 -0
- package/dist/primitives/accordion.js.map +7 -0
- package/dist/primitives/alert.js +180 -12
- package/dist/primitives/alert.js.map +2 -2
- package/dist/primitives/calendar.js +122 -32
- package/dist/primitives/calendar.js.map +2 -2
- package/dist/primitives/compact-select.js +47 -0
- package/dist/primitives/compact-select.js.map +7 -0
- package/dist/primitives/counter-input.js +174 -0
- package/dist/primitives/counter-input.js.map +7 -0
- package/dist/primitives/date-picker-helpers.js +79 -0
- package/dist/primitives/date-picker-helpers.js.map +7 -0
- package/dist/primitives/date-picker.js +216 -0
- package/dist/primitives/date-picker.js.map +7 -0
- package/dist/primitives/date-range-picker.js +239 -0
- package/dist/primitives/date-range-picker.js.map +7 -0
- package/dist/primitives/digit-input.js +157 -0
- package/dist/primitives/digit-input.js.map +7 -0
- package/dist/primitives/empty-state.js +93 -0
- package/dist/primitives/empty-state.js.map +7 -0
- package/dist/primitives/inline-input.js +29 -0
- package/dist/primitives/inline-input.js.map +7 -0
- package/dist/primitives/inline-select.js +41 -0
- package/dist/primitives/inline-select.js.map +7 -0
- package/dist/primitives/notification-stack.js +116 -0
- package/dist/primitives/notification-stack.js.map +7 -0
- package/dist/primitives/notification.js +62 -0
- package/dist/primitives/notification.js.map +7 -0
- package/dist/primitives/rich-editor.js +1230 -0
- package/dist/primitives/rich-editor.js.map +7 -0
- package/dist/primitives/select.js +1 -0
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/skeleton.js +55 -0
- package/dist/primitives/skeleton.js.map +7 -0
- package/dist/primitives/tag-input.js +186 -0
- package/dist/primitives/tag-input.js.map +7 -0
- package/dist/primitives/tag.js +66 -13
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/time-picker.js +793 -0
- package/dist/primitives/time-picker.js.map +7 -0
- package/package.json +4 -3
- package/src/backend/CrudForm.tsx +61 -113
- package/src/backend/DataTable.tsx +17 -5
- package/src/backend/EmptyState.tsx +13 -63
- package/src/backend/FilterOverlay.tsx +38 -22
- package/src/backend/FlashMessages.tsx +23 -24
- package/src/backend/JsonBuilder.tsx +4 -2
- package/src/backend/LogList.tsx +109 -0
- package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
- package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
- package/src/backend/__tests__/LogList.test.tsx +163 -0
- package/src/backend/crud/CollapsibleGroup.tsx +17 -3
- package/src/backend/detail/InlineEditors.tsx +78 -25
- package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
- package/src/backend/inputs/ComboboxInput.tsx +81 -9
- package/src/backend/inputs/DatePicker.tsx +28 -155
- package/src/backend/inputs/DateTimePicker.tsx +25 -193
- package/src/backend/inputs/TagsInput.tsx +10 -13
- package/src/backend/inputs/TimeInput.tsx +2 -1
- package/src/backend/inputs/TimePicker.tsx +99 -70
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
- package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
- package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
- package/src/backend/utils/customFieldForms.ts +2 -2
- package/src/portal/components/PortalEmptyState.tsx +27 -22
- package/src/primitives/__tests__/accordion.test.tsx +197 -0
- package/src/primitives/__tests__/alert.test.tsx +204 -0
- package/src/primitives/__tests__/compact-select.test.tsx +98 -0
- package/src/primitives/__tests__/counter-input.test.tsx +165 -0
- package/src/primitives/__tests__/date-picker.test.tsx +221 -0
- package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
- package/src/primitives/__tests__/digit-input.test.tsx +168 -0
- package/src/primitives/__tests__/empty-state.test.tsx +154 -0
- package/src/primitives/__tests__/inline-input.test.tsx +100 -0
- package/src/primitives/__tests__/inline-select.test.tsx +103 -0
- package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
- package/src/primitives/__tests__/notification.test.tsx +119 -0
- package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
- package/src/primitives/__tests__/skeleton.test.tsx +76 -0
- package/src/primitives/__tests__/tag-input.test.tsx +198 -0
- package/src/primitives/__tests__/tag.test.tsx +123 -0
- package/src/primitives/__tests__/time-picker.test.tsx +392 -0
- package/src/primitives/accordion.tsx +225 -0
- package/src/primitives/alert.tsx +234 -24
- package/src/primitives/calendar.tsx +146 -38
- package/src/primitives/compact-select.tsx +59 -0
- package/src/primitives/counter-input.tsx +211 -0
- package/src/primitives/date-picker-helpers.ts +96 -0
- package/src/primitives/date-picker.tsx +290 -0
- package/src/primitives/date-range-picker.tsx +281 -0
- package/src/primitives/digit-input.tsx +208 -0
- package/src/primitives/empty-state.tsx +140 -0
- package/src/primitives/inline-input.tsx +55 -0
- package/src/primitives/inline-select.tsx +74 -0
- package/src/primitives/notification-stack.tsx +185 -0
- package/src/primitives/notification.tsx +136 -0
- package/src/primitives/rich-editor.tsx +1578 -0
- package/src/primitives/select.tsx +1 -0
- package/src/primitives/skeleton.tsx +70 -0
- package/src/primitives/tag-input.tsx +223 -0
- package/src/primitives/tag.tsx +69 -10
- package/src/primitives/time-picker.tsx +1059 -0
package/src/primitives/alert.tsx
CHANGED
|
@@ -1,52 +1,262 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from 'react'
|
|
4
|
+
import { AlertCircle, CheckCircle2, Info, Rocket, X, AlertTriangle } from 'lucide-react'
|
|
2
5
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
6
|
|
|
4
7
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
5
8
|
|
|
9
|
+
export type AlertStatus = 'error' | 'warning' | 'success' | 'information' | 'feature'
|
|
10
|
+
export type AlertStyle = 'filled' | 'light' | 'lighter' | 'stroke'
|
|
11
|
+
export type AlertSize = 'xs' | 'sm' | 'default'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Legacy `variant` values from the pre-Figma-`169:2358` API. Kept for BC —
|
|
15
|
+
* mapped to `status` inside the Alert component so existing call sites do
|
|
16
|
+
* not need migration. The Figma `light` look (`#fecaca` saturated tint
|
|
17
|
+
* for error → `state/{x}/light` token family) is heavier than the
|
|
18
|
+
* pre-Figma look; legacy call sites that want the softer pre-Figma
|
|
19
|
+
* appearance should switch explicitly to `style="lighter"`.
|
|
20
|
+
*/
|
|
21
|
+
type LegacyVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'
|
|
22
|
+
|
|
23
|
+
const legacyVariantToStatus: Record<LegacyVariant, AlertStatus> = {
|
|
24
|
+
default: 'information',
|
|
25
|
+
destructive: 'error',
|
|
26
|
+
success: 'success',
|
|
27
|
+
warning: 'warning',
|
|
28
|
+
info: 'information',
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
const alertVariants = cva(
|
|
7
|
-
|
|
32
|
+
'relative flex w-full gap-2 transition-colors',
|
|
8
33
|
{
|
|
9
34
|
variants: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
status: {
|
|
36
|
+
error: '',
|
|
37
|
+
warning: '',
|
|
38
|
+
success: '',
|
|
39
|
+
information: '',
|
|
40
|
+
feature: '',
|
|
41
|
+
},
|
|
42
|
+
style: {
|
|
43
|
+
filled: '',
|
|
44
|
+
light: '',
|
|
45
|
+
lighter: '',
|
|
46
|
+
stroke: '',
|
|
47
|
+
},
|
|
48
|
+
size: {
|
|
49
|
+
// Figma X-Small: px-[8px] py-[6px], radius-8, Paragraph/X Small (12/16)
|
|
50
|
+
xs: 'min-h-8 items-center rounded-lg px-2 py-1.5 text-xs leading-4 tracking-tight',
|
|
51
|
+
// Figma Small: px-[10px] py-[8px], radius-8, Paragraph/Small (14/20)
|
|
52
|
+
sm: 'min-h-9 items-center rounded-lg px-2.5 py-2 text-sm leading-5 tracking-tight',
|
|
53
|
+
// Figma Large: px-[12px] py-[10px], radius-12, Paragraph/Small (14/20) multi-line
|
|
54
|
+
default: 'items-start rounded-xl px-3 py-2.5 text-sm leading-5 tracking-tight',
|
|
20
55
|
},
|
|
21
56
|
},
|
|
57
|
+
compoundVariants: [
|
|
58
|
+
// === filled (Figma: bg = state/{x}/base, text = static-white) ===
|
|
59
|
+
{ status: 'error', style: 'filled', className: 'border border-transparent bg-status-error-icon text-white' },
|
|
60
|
+
{ status: 'warning', style: 'filled', className: 'border border-transparent bg-status-warning-icon text-white' },
|
|
61
|
+
{ status: 'success', style: 'filled', className: 'border border-transparent bg-status-success-icon text-white' },
|
|
62
|
+
{ status: 'information', style: 'filled', className: 'border border-transparent bg-status-info-icon text-white' },
|
|
63
|
+
{ status: 'feature', style: 'filled', className: 'border border-transparent bg-status-neutral-icon text-white' },
|
|
64
|
+
// === light (Figma: bg = state/{x}/light, text = text/strong-950 = #171717 black) ===
|
|
65
|
+
{ status: 'error', style: 'light', className: 'border border-transparent bg-status-error-border text-foreground' },
|
|
66
|
+
{ status: 'warning', style: 'light', className: 'border border-transparent bg-status-warning-border text-foreground' },
|
|
67
|
+
{ status: 'success', style: 'light', className: 'border border-transparent bg-status-success-border text-foreground' },
|
|
68
|
+
{ status: 'information', style: 'light', className: 'border border-transparent bg-status-info-border text-foreground' },
|
|
69
|
+
{ status: 'feature', style: 'light', className: 'border border-transparent bg-status-neutral-border text-foreground' },
|
|
70
|
+
// === lighter (Figma: bg = state/{x}/lighter, text = text/strong-950 = #171717 black) ===
|
|
71
|
+
{ status: 'error', style: 'lighter', className: 'border border-transparent bg-status-error-bg text-foreground' },
|
|
72
|
+
{ status: 'warning', style: 'lighter', className: 'border border-transparent bg-status-warning-bg text-foreground' },
|
|
73
|
+
{ status: 'success', style: 'lighter', className: 'border border-transparent bg-status-success-bg text-foreground' },
|
|
74
|
+
{ status: 'information', style: 'lighter', className: 'border border-transparent bg-status-info-bg text-foreground' },
|
|
75
|
+
{ status: 'feature', style: 'lighter', className: 'border border-transparent bg-status-neutral-bg text-foreground' },
|
|
76
|
+
// === stroke (Figma: bg = white, border = stroke/soft-200, text = strong-950, shadow = regular-shadow/medium) ===
|
|
77
|
+
{ style: 'stroke', className: 'border border-border bg-background text-foreground shadow-lg' },
|
|
78
|
+
],
|
|
22
79
|
defaultVariants: {
|
|
23
|
-
|
|
80
|
+
status: 'information',
|
|
81
|
+
style: 'light',
|
|
82
|
+
size: 'sm',
|
|
24
83
|
},
|
|
25
|
-
}
|
|
84
|
+
},
|
|
26
85
|
)
|
|
27
86
|
|
|
28
|
-
|
|
87
|
+
// Dismiss button (X) color per Figma: text/strong-950 (#171717 black) at
|
|
88
|
+
// `opacity-40` for light/lighter/stroke — matches the Figma `close-line`
|
|
89
|
+
// icon which is the dark icon-strong-950 fill rendered at 40% alpha
|
|
90
|
+
// (visually ≈ #5c5c5c gray). White at full opacity for filled so it
|
|
91
|
+
// stays legible over the saturated bg.
|
|
92
|
+
const alertDismissVariants = cva(
|
|
93
|
+
'shrink-0 rounded-md p-0.5 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
94
|
+
{
|
|
95
|
+
variants: {
|
|
96
|
+
style: {
|
|
97
|
+
filled: 'text-white opacity-90 hover:opacity-100',
|
|
98
|
+
light: 'text-foreground opacity-40 hover:opacity-100',
|
|
99
|
+
lighter: 'text-foreground opacity-40 hover:opacity-100',
|
|
100
|
+
stroke: 'text-foreground opacity-40 hover:opacity-100',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
defaultVariants: { style: 'light' },
|
|
104
|
+
},
|
|
105
|
+
)
|
|
29
106
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
107
|
+
const alertIconBadgeVariants = cva(
|
|
108
|
+
'inline-flex shrink-0 items-center justify-center rounded-full text-white',
|
|
109
|
+
{
|
|
110
|
+
variants: {
|
|
111
|
+
status: {
|
|
112
|
+
error: 'bg-status-error-icon',
|
|
113
|
+
warning: 'bg-status-warning-icon',
|
|
114
|
+
success: 'bg-status-success-icon',
|
|
115
|
+
information: 'bg-status-info-icon',
|
|
116
|
+
feature: 'bg-status-neutral-icon',
|
|
117
|
+
},
|
|
118
|
+
size: {
|
|
119
|
+
xs: 'size-4 [&>svg]:size-2.5',
|
|
120
|
+
sm: 'size-5 [&>svg]:size-3',
|
|
121
|
+
default: 'size-6 [&>svg]:size-3.5',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
defaultVariants: { status: 'information', size: 'sm' },
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const defaultStatusIcons: Record<AlertStatus, React.ComponentType<{ className?: string; 'aria-hidden'?: 'true' }>> = {
|
|
129
|
+
error: AlertCircle,
|
|
130
|
+
warning: AlertTriangle,
|
|
131
|
+
success: CheckCircle2,
|
|
132
|
+
information: Info,
|
|
133
|
+
feature: Rocket,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type AlertProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'role'> &
|
|
137
|
+
Omit<VariantProps<typeof alertVariants>, 'status' | 'style' | 'size'> & {
|
|
138
|
+
status?: AlertStatus
|
|
139
|
+
style?: AlertStyle
|
|
140
|
+
size?: AlertSize
|
|
141
|
+
/** Show the leading icon. Default `true`. */
|
|
142
|
+
showIcon?: boolean
|
|
143
|
+
/** Override the default per-status Lucide icon. */
|
|
144
|
+
icon?: React.ReactNode
|
|
145
|
+
/** Render a trailing close (X) button. Pair with `onDismiss`. */
|
|
146
|
+
dismissible?: boolean
|
|
147
|
+
onDismiss?: () => void
|
|
148
|
+
dismissAriaLabel?: string
|
|
149
|
+
/** Inline action slot (link buttons) rendered to the right of the body. */
|
|
150
|
+
action?: React.ReactNode
|
|
151
|
+
/**
|
|
152
|
+
* @deprecated Use `status` + `style` instead. Kept for BC. Maps the
|
|
153
|
+
* legacy single-prop variants onto `status` and picks up the new
|
|
154
|
+
* `light` + `sm` defaults.
|
|
155
|
+
*/
|
|
156
|
+
variant?: LegacyVariant
|
|
157
|
+
}
|
|
33
158
|
|
|
159
|
+
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
160
|
+
(
|
|
161
|
+
{
|
|
162
|
+
className,
|
|
163
|
+
status,
|
|
164
|
+
style,
|
|
165
|
+
size,
|
|
166
|
+
showIcon = true,
|
|
167
|
+
icon,
|
|
168
|
+
dismissible = false,
|
|
169
|
+
onDismiss,
|
|
170
|
+
dismissAriaLabel = 'Dismiss',
|
|
171
|
+
action,
|
|
172
|
+
variant,
|
|
173
|
+
children,
|
|
174
|
+
...props
|
|
175
|
+
},
|
|
176
|
+
ref,
|
|
177
|
+
) => {
|
|
178
|
+
const resolvedStatus: AlertStatus = status ?? (variant ? legacyVariantToStatus[variant] : 'information')
|
|
179
|
+
const resolvedStyle: AlertStyle = style ?? 'light'
|
|
180
|
+
const resolvedSize: AlertSize = size ?? 'sm'
|
|
181
|
+
const IconComponent = defaultStatusIcons[resolvedStatus]
|
|
182
|
+
const rawIcon = icon ?? <IconComponent aria-hidden="true" />
|
|
183
|
+
|
|
184
|
+
let iconSlot: React.ReactNode = null
|
|
185
|
+
if (showIcon) {
|
|
186
|
+
if (resolvedStyle === 'filled') {
|
|
187
|
+
// Filled: plain icon over saturated bg — no badge wrap, inherit text-white.
|
|
188
|
+
iconSlot = (
|
|
189
|
+
<span
|
|
190
|
+
className={cn(
|
|
191
|
+
'inline-flex shrink-0 items-center justify-center',
|
|
192
|
+
resolvedSize === 'default' ? '[&>svg]:size-5' : '[&>svg]:size-4',
|
|
193
|
+
)}
|
|
194
|
+
data-slot="alert-icon"
|
|
195
|
+
>
|
|
196
|
+
{rawIcon}
|
|
197
|
+
</span>
|
|
198
|
+
)
|
|
199
|
+
} else {
|
|
200
|
+
// light / lighter / stroke: badge wrap with status color bg + white icon.
|
|
201
|
+
iconSlot = (
|
|
202
|
+
<span
|
|
203
|
+
className={alertIconBadgeVariants({ status: resolvedStatus, size: resolvedSize })}
|
|
204
|
+
data-slot="alert-icon-badge"
|
|
205
|
+
data-status={resolvedStatus}
|
|
206
|
+
>
|
|
207
|
+
{rawIcon}
|
|
208
|
+
</span>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
ref={ref}
|
|
216
|
+
role="alert"
|
|
217
|
+
className={cn(
|
|
218
|
+
alertVariants({ status: resolvedStatus, style: resolvedStyle, size: resolvedSize }),
|
|
219
|
+
className,
|
|
220
|
+
)}
|
|
221
|
+
data-slot="alert"
|
|
222
|
+
data-status={resolvedStatus}
|
|
223
|
+
data-style={resolvedStyle}
|
|
224
|
+
{...props}
|
|
225
|
+
>
|
|
226
|
+
{iconSlot}
|
|
227
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
228
|
+
{action ? <div className="shrink-0">{action}</div> : null}
|
|
229
|
+
{dismissible ? (
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={onDismiss}
|
|
233
|
+
aria-label={dismissAriaLabel}
|
|
234
|
+
className={alertDismissVariants({ style: resolvedStyle })}
|
|
235
|
+
data-slot="alert-dismiss"
|
|
236
|
+
>
|
|
237
|
+
<X aria-hidden="true" className="size-4" />
|
|
238
|
+
</button>
|
|
239
|
+
) : null}
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
},
|
|
243
|
+
)
|
|
34
244
|
Alert.displayName = 'Alert'
|
|
35
245
|
|
|
246
|
+
// Figma "Label/Small": Inter Medium 14 / 20.
|
|
36
247
|
const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
37
248
|
({ className, ...props }, ref) => (
|
|
38
|
-
<h5 ref={ref} className={cn('mb-1 text-sm font-
|
|
39
|
-
)
|
|
249
|
+
<h5 ref={ref} className={cn('mb-1 text-sm font-medium leading-5', className)} {...props} />
|
|
250
|
+
),
|
|
40
251
|
)
|
|
41
|
-
|
|
42
252
|
AlertTitle.displayName = 'AlertTitle'
|
|
43
253
|
|
|
254
|
+
// Figma "Paragraph/Small": Inter Regular 14 / 20.
|
|
44
255
|
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
45
256
|
({ className, ...props }, ref) => (
|
|
46
|
-
<p ref={ref} className={cn('text-sm leading-
|
|
47
|
-
)
|
|
257
|
+
<p ref={ref} className={cn('text-sm leading-5', className)} {...props} />
|
|
258
|
+
),
|
|
48
259
|
)
|
|
49
|
-
|
|
50
260
|
AlertDescription.displayName = 'AlertDescription'
|
|
51
261
|
|
|
52
|
-
export { Alert, AlertDescription, AlertTitle, alertVariants }
|
|
262
|
+
export { Alert, AlertDescription, AlertTitle, alertVariants, alertIconBadgeVariants }
|
|
@@ -1,67 +1,175 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { DayPicker } from 'react-day-picker'
|
|
5
|
-
import type { DayPickerProps } from 'react-day-picker'
|
|
4
|
+
import { DayPicker, useDayPicker } from 'react-day-picker'
|
|
5
|
+
import type { DayPickerProps, CalendarMonth } from 'react-day-picker'
|
|
6
6
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
7
|
+
import { format } from 'date-fns'
|
|
8
|
+
import type { Locale } from 'date-fns'
|
|
7
9
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
8
10
|
|
|
9
11
|
export type CalendarProps = DayPickerProps
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
function MonthNavButton({
|
|
14
|
+
direction,
|
|
15
|
+
locale,
|
|
16
|
+
}: {
|
|
17
|
+
direction: 'prev' | 'next'
|
|
18
|
+
locale?: Locale
|
|
19
|
+
}) {
|
|
20
|
+
const dayPicker = useDayPicker() as unknown as {
|
|
21
|
+
previousMonth?: Date
|
|
22
|
+
nextMonth?: Date
|
|
23
|
+
goToMonth?: (month: Date) => void
|
|
24
|
+
}
|
|
25
|
+
const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth
|
|
26
|
+
const Icon = direction === 'prev' ? ChevronLeft : ChevronRight
|
|
27
|
+
const targetLabel = format(target ?? new Date(), 'MMMM yyyy', locale ? { locale } : undefined)
|
|
28
|
+
const ariaLabel = `Go to ${direction === 'prev' ? 'previous' : 'next'} month: ${targetLabel}`
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
disabled={!target}
|
|
33
|
+
aria-label={ariaLabel}
|
|
34
|
+
onClick={() => {
|
|
35
|
+
if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)
|
|
36
|
+
}}
|
|
37
|
+
className={cn(
|
|
38
|
+
'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',
|
|
39
|
+
'border border-border bg-background text-muted-foreground transition-colors',
|
|
40
|
+
'hover:bg-accent hover:text-accent-foreground hover:border-input',
|
|
41
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
42
|
+
'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<Icon className="h-4 w-4" aria-hidden="true" />
|
|
46
|
+
</button>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildMonthCaption(locale: Locale | undefined, totalMonths: number) {
|
|
51
|
+
return function MonthCaption({
|
|
52
|
+
calendarMonth,
|
|
53
|
+
displayIndex,
|
|
54
|
+
}: {
|
|
55
|
+
calendarMonth: CalendarMonth
|
|
56
|
+
displayIndex?: number
|
|
57
|
+
}) {
|
|
58
|
+
const label = format(calendarMonth.date, 'MMMM yyyy', locale ? { locale } : undefined)
|
|
59
|
+
const index = typeof displayIndex === 'number' ? displayIndex : 0
|
|
60
|
+
// For multi-month layouts (e.g. range pickers) only the leftmost month
|
|
61
|
+
// exposes the previous-month chevron and only the rightmost exposes the
|
|
62
|
+
// next-month chevron. Navigation is always global across all visible
|
|
63
|
+
// months, so showing both arrows on every month is confusing.
|
|
64
|
+
const showPrev = index === 0
|
|
65
|
+
const showNext = index === totalMonths - 1
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex items-center justify-between gap-2 mb-3">
|
|
68
|
+
{showPrev ? (
|
|
69
|
+
<MonthNavButton direction="prev" locale={locale} />
|
|
70
|
+
) : (
|
|
71
|
+
<div className="h-9 w-9 shrink-0" aria-hidden="true" />
|
|
72
|
+
)}
|
|
73
|
+
<div
|
|
74
|
+
className="flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium"
|
|
75
|
+
aria-live="polite"
|
|
76
|
+
>
|
|
77
|
+
{label}
|
|
78
|
+
</div>
|
|
79
|
+
{showNext ? (
|
|
80
|
+
<MonthNavButton direction="next" locale={locale} />
|
|
81
|
+
) : (
|
|
82
|
+
<div className="h-9 w-9 shrink-0" aria-hidden="true" />
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function Calendar({
|
|
90
|
+
className,
|
|
91
|
+
classNames,
|
|
92
|
+
showOutsideDays = true,
|
|
93
|
+
fixedWeeks = true,
|
|
94
|
+
locale,
|
|
95
|
+
components,
|
|
96
|
+
numberOfMonths = 1,
|
|
97
|
+
pagedNavigation = true,
|
|
98
|
+
...props
|
|
99
|
+
}: CalendarProps) {
|
|
100
|
+
const monthCaption = React.useMemo(
|
|
101
|
+
() => buildMonthCaption(locale as Locale | undefined, numberOfMonths),
|
|
102
|
+
[locale, numberOfMonths],
|
|
103
|
+
)
|
|
12
104
|
return (
|
|
13
105
|
<DayPicker
|
|
14
106
|
showOutsideDays={showOutsideDays}
|
|
107
|
+
fixedWeeks={fixedWeeks}
|
|
108
|
+
pagedNavigation={pagedNavigation}
|
|
109
|
+
locale={locale}
|
|
110
|
+
numberOfMonths={numberOfMonths}
|
|
15
111
|
className={cn('p-3', className)}
|
|
16
112
|
classNames={{
|
|
17
113
|
months: 'flex flex-col sm:flex-row gap-4',
|
|
18
|
-
month: 'space-y-
|
|
19
|
-
month_caption: '
|
|
20
|
-
caption_label: '
|
|
21
|
-
nav: '
|
|
22
|
-
|
|
23
|
-
'absolute left-1 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
24
|
-
'inline-flex items-center justify-center rounded-md text-sm font-medium',
|
|
25
|
-
'ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
26
|
-
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
27
|
-
'hover:bg-accent hover:text-accent-foreground border-0'
|
|
28
|
-
),
|
|
29
|
-
button_next: cn(
|
|
30
|
-
'absolute right-1 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
31
|
-
'inline-flex items-center justify-center rounded-md text-sm font-medium',
|
|
32
|
-
'ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
33
|
-
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
34
|
-
'hover:bg-accent hover:text-accent-foreground border-0'
|
|
35
|
-
),
|
|
36
|
-
month_grid: 'w-full border-collapse space-y-1',
|
|
114
|
+
month: 'space-y-2',
|
|
115
|
+
month_caption: '',
|
|
116
|
+
caption_label: 'sr-only',
|
|
117
|
+
nav: 'sr-only',
|
|
118
|
+
month_grid: 'w-full border-collapse',
|
|
37
119
|
weekdays: 'flex',
|
|
38
120
|
weekday: 'text-muted-foreground rounded-md w-9 font-normal text-xs',
|
|
39
|
-
weeks: 'w-full border-collapse
|
|
40
|
-
week: 'flex w-full mt-
|
|
41
|
-
day: 'h-9 w-9 text-center text-sm p-0 relative
|
|
121
|
+
weeks: 'w-full border-collapse',
|
|
122
|
+
week: 'flex w-full mt-1',
|
|
123
|
+
day: 'h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20',
|
|
42
124
|
day_button: cn(
|
|
43
125
|
'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
|
|
44
126
|
'inline-flex items-center justify-center rounded-md text-sm',
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
127
|
+
'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',
|
|
128
|
+
// Focus indicator is a soft accent fill instead of a ring overlay — keyboard
|
|
129
|
+
// users get a visible cue, but mouse-click focus does not leave a stuck ring
|
|
130
|
+
// on top of the selected cell.
|
|
131
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
132
|
+
'focus-visible:bg-accent focus-visible:text-accent-foreground',
|
|
133
|
+
),
|
|
134
|
+
// React-day-picker v9 applies `classNames.selected` / `range_*` to the day
|
|
135
|
+
// CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent
|
|
136
|
+
// fill visible through interaction, we (a) paint the cell with the desired
|
|
137
|
+
// bg/text, and (b) force the inner button to render transparent — so the
|
|
138
|
+
// button's own hover/focus-visible bg overrides cannot cover the cell fill.
|
|
139
|
+
selected: cn(
|
|
140
|
+
'!bg-primary !text-primary-foreground rounded-md',
|
|
141
|
+
'[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
|
|
142
|
+
'[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
|
|
143
|
+
'[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
|
|
144
|
+
),
|
|
145
|
+
range_start: cn(
|
|
146
|
+
'!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',
|
|
147
|
+
'[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
|
|
148
|
+
'[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
|
|
149
|
+
'[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
|
|
150
|
+
),
|
|
151
|
+
range_end: cn(
|
|
152
|
+
'!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',
|
|
153
|
+
'[&_button]:!bg-transparent [&_button]:!text-primary-foreground',
|
|
154
|
+
'[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',
|
|
155
|
+
'[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',
|
|
156
|
+
),
|
|
157
|
+
range_middle: cn(
|
|
158
|
+
'!bg-accent !text-accent-foreground !rounded-none',
|
|
159
|
+
'[&_button]:!bg-transparent [&_button]:!text-accent-foreground',
|
|
160
|
+
'[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',
|
|
161
|
+
'[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',
|
|
48
162
|
),
|
|
49
|
-
|
|
50
|
-
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground rounded-md',
|
|
51
|
-
today: 'bg-accent text-accent-foreground rounded-md',
|
|
163
|
+
today: 'font-semibold text-primary rounded-md',
|
|
52
164
|
outside:
|
|
53
|
-
'day-outside text-muted-foreground opacity-
|
|
165
|
+
'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
|
|
54
166
|
disabled: 'text-muted-foreground opacity-50',
|
|
55
167
|
hidden: 'invisible',
|
|
56
168
|
...classNames,
|
|
57
169
|
}}
|
|
58
170
|
components={{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<ChevronLeft className="h-4 w-4" />
|
|
62
|
-
) : (
|
|
63
|
-
<ChevronRight className="h-4 w-4" />
|
|
64
|
-
),
|
|
171
|
+
MonthCaption: monthCaption,
|
|
172
|
+
...components,
|
|
65
173
|
}}
|
|
66
174
|
{...props}
|
|
67
175
|
/>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectGroup,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectLabel,
|
|
11
|
+
SelectSeparator,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
type SelectTriggerProps,
|
|
15
|
+
} from './select'
|
|
16
|
+
|
|
17
|
+
export type CompactSelectTriggerProps = Omit<SelectTriggerProps, 'size'> & {
|
|
18
|
+
/**
|
|
19
|
+
* Optional inline prefix rendered before the selected value, e.g. `View:` or
|
|
20
|
+
* `Sort by:`. Renders in muted style and shrinks before the value text.
|
|
21
|
+
*/
|
|
22
|
+
triggerLabel?: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CompactSelectTrigger = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof SelectTrigger>,
|
|
27
|
+
CompactSelectTriggerProps
|
|
28
|
+
>(({ triggerLabel, children, className, ...props }, ref) => (
|
|
29
|
+
<SelectTrigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
size="xs"
|
|
32
|
+
className={cn(className)}
|
|
33
|
+
data-slot="compact-select-trigger"
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{triggerLabel ? (
|
|
37
|
+
<span
|
|
38
|
+
className="shrink-0 text-xs font-medium text-muted-foreground"
|
|
39
|
+
data-slot="compact-select-trigger-label"
|
|
40
|
+
>
|
|
41
|
+
{triggerLabel}
|
|
42
|
+
</span>
|
|
43
|
+
) : null}
|
|
44
|
+
{children}
|
|
45
|
+
</SelectTrigger>
|
|
46
|
+
))
|
|
47
|
+
CompactSelectTrigger.displayName = 'CompactSelectTrigger'
|
|
48
|
+
|
|
49
|
+
// Re-export the rest of the Select API so consumers can import the entire
|
|
50
|
+
// compact-select composition from one path.
|
|
51
|
+
export {
|
|
52
|
+
Select,
|
|
53
|
+
SelectContent,
|
|
54
|
+
SelectGroup,
|
|
55
|
+
SelectItem,
|
|
56
|
+
SelectLabel,
|
|
57
|
+
SelectSeparator,
|
|
58
|
+
SelectValue,
|
|
59
|
+
}
|