@open-mercato/ui 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3260.1.dc719cac84
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/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
- package/dist/ai/AiAssistantLauncher.js +8 -0
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +226 -58
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiDock.js +3 -2
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/ModelPicker.js +186 -0
- package/dist/ai/ModelPicker.js.map +7 -0
- package/dist/ai/index.js +3 -1
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiChat.js +12 -16
- package/dist/ai/useAiChat.js.map +2 -2
- 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/ai/AiAssistantLauncher.tsx +8 -0
- package/src/ai/AiChat.tsx +229 -11
- package/src/ai/AiDock.tsx +3 -2
- package/src/ai/ModelPicker.tsx +248 -0
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.test.tsx +180 -0
- package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
- package/src/ai/__tests__/AiDock.test.tsx +12 -1
- package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/useAiChat.ts +29 -16
- 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
|
@@ -0,0 +1,74 @@
|
|
|
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 InlineSelectTriggerProps = Omit<SelectTriggerProps, 'size'> & {
|
|
18
|
+
/**
|
|
19
|
+
* Trigger size. `sm` (h-8) is the default — inline editors are typically
|
|
20
|
+
* embedded inside dense rows. Pass `'default'` (h-9) when the editor sits
|
|
21
|
+
* alongside other h-9 controls.
|
|
22
|
+
*/
|
|
23
|
+
size?: 'sm' | 'default'
|
|
24
|
+
/**
|
|
25
|
+
* Render a subtle trigger border on hover so the affordance is
|
|
26
|
+
* discoverable. Default `true`. Set to `false` for editors that should
|
|
27
|
+
* remain visually invisible until focused.
|
|
28
|
+
*/
|
|
29
|
+
showBorderOnHover?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Borderless variant of `SelectTrigger` — the select-typed counterpart to
|
|
34
|
+
* `InlineInput`. At rest the trigger looks like plain text (transparent
|
|
35
|
+
* border, transparent background, no shadow); hover reveals a subtle
|
|
36
|
+
* border via `showBorderOnHover` (default `true`), and focus inherits the
|
|
37
|
+
* standard `border-foreground` + focus shadow from the underlying
|
|
38
|
+
* `SelectTrigger` for keyboard accessibility.
|
|
39
|
+
*
|
|
40
|
+
* Composes with the regular `Select` root + `SelectContent` /
|
|
41
|
+
* `SelectItem` — `inline-select.tsx` re-exports the rest of the `Select`
|
|
42
|
+
* API so consumers can import the entire composition from one path.
|
|
43
|
+
*/
|
|
44
|
+
export const InlineSelectTrigger = React.forwardRef<
|
|
45
|
+
React.ElementRef<typeof SelectTrigger>,
|
|
46
|
+
InlineSelectTriggerProps
|
|
47
|
+
>(({ className, size = 'sm', showBorderOnHover = true, children, ...props }, ref) => (
|
|
48
|
+
<SelectTrigger
|
|
49
|
+
ref={ref}
|
|
50
|
+
size={size}
|
|
51
|
+
className={cn(
|
|
52
|
+
'border-transparent bg-transparent shadow-none',
|
|
53
|
+
showBorderOnHover ? 'hover:border-input hover:bg-muted/40' : 'hover:bg-transparent',
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
data-slot="inline-select-trigger"
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</SelectTrigger>
|
|
61
|
+
))
|
|
62
|
+
InlineSelectTrigger.displayName = 'InlineSelectTrigger'
|
|
63
|
+
|
|
64
|
+
// Re-export the rest of the Select API so consumers can import the entire
|
|
65
|
+
// inline-select composition from one path.
|
|
66
|
+
export {
|
|
67
|
+
Select,
|
|
68
|
+
SelectContent,
|
|
69
|
+
SelectGroup,
|
|
70
|
+
SelectItem,
|
|
71
|
+
SelectLabel,
|
|
72
|
+
SelectSeparator,
|
|
73
|
+
SelectValue,
|
|
74
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Notification, type NotificationProps } from './notification'
|
|
5
|
+
|
|
6
|
+
export type NotificationStackPlacement =
|
|
7
|
+
| 'top-right'
|
|
8
|
+
| 'top-left'
|
|
9
|
+
| 'bottom-right'
|
|
10
|
+
| 'bottom-left'
|
|
11
|
+
| 'top-center'
|
|
12
|
+
| 'bottom-center'
|
|
13
|
+
|
|
14
|
+
export type NotifyOptions = Omit<NotificationProps, 'id' | 'onDismiss'> & {
|
|
15
|
+
/** Auto-dismiss the notification after this many milliseconds. Omit for manual-only dismiss (Notification default). */
|
|
16
|
+
autoDismissMs?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type NotificationEntry = NotifyOptions & {
|
|
20
|
+
id: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type NotificationContextValue = {
|
|
24
|
+
notifications: NotificationEntry[]
|
|
25
|
+
notify: (options: NotifyOptions) => string
|
|
26
|
+
dismiss: (id: string) => void
|
|
27
|
+
dismissAll: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NotificationContext = React.createContext<NotificationContextValue | null>(null)
|
|
31
|
+
|
|
32
|
+
function generateId(): string {
|
|
33
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
34
|
+
return crypto.randomUUID()
|
|
35
|
+
}
|
|
36
|
+
return `notif-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type NotificationProviderProps = {
|
|
40
|
+
children: React.ReactNode
|
|
41
|
+
/**
|
|
42
|
+
* Maximum number of notifications visible at once. Older entries beyond
|
|
43
|
+
* this cap are removed FIFO. Default `5`.
|
|
44
|
+
*/
|
|
45
|
+
maxVisible?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mount once near the app root (typically inside `AppShell` next to
|
|
50
|
+
* `FlashMessages`). Provides the `useNotification()` hook to children
|
|
51
|
+
* and renders the `NotificationStack` in a fixed-position portal-style
|
|
52
|
+
* wrapper.
|
|
53
|
+
*
|
|
54
|
+
* Stack placement, gap, and per-entry behavior are consumer choice
|
|
55
|
+
* via `NotificationStack` — `NotificationProvider` itself does not
|
|
56
|
+
* render the visual list, only the context.
|
|
57
|
+
*/
|
|
58
|
+
export function NotificationProvider({ children, maxVisible = 5 }: NotificationProviderProps) {
|
|
59
|
+
const [notifications, setNotifications] = React.useState<NotificationEntry[]>([])
|
|
60
|
+
const timersRef = React.useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
|
|
61
|
+
|
|
62
|
+
const clearTimer = React.useCallback((id: string) => {
|
|
63
|
+
const t = timersRef.current.get(id)
|
|
64
|
+
if (t) {
|
|
65
|
+
clearTimeout(t)
|
|
66
|
+
timersRef.current.delete(id)
|
|
67
|
+
}
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
const dismiss = React.useCallback(
|
|
71
|
+
(id: string) => {
|
|
72
|
+
clearTimer(id)
|
|
73
|
+
setNotifications((prev) => prev.filter((n) => n.id !== id))
|
|
74
|
+
},
|
|
75
|
+
[clearTimer],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const notify = React.useCallback(
|
|
79
|
+
(options: NotifyOptions) => {
|
|
80
|
+
const id = generateId()
|
|
81
|
+
setNotifications((prev) => {
|
|
82
|
+
const next = [...prev, { ...options, id }]
|
|
83
|
+
// FIFO trim when exceeding the visible cap.
|
|
84
|
+
return next.length > maxVisible ? next.slice(next.length - maxVisible) : next
|
|
85
|
+
})
|
|
86
|
+
if (typeof options.autoDismissMs === 'number' && options.autoDismissMs > 0) {
|
|
87
|
+
const t = setTimeout(() => dismiss(id), options.autoDismissMs)
|
|
88
|
+
timersRef.current.set(id, t)
|
|
89
|
+
}
|
|
90
|
+
return id
|
|
91
|
+
},
|
|
92
|
+
[dismiss, maxVisible],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const dismissAll = React.useCallback(() => {
|
|
96
|
+
timersRef.current.forEach((t) => clearTimeout(t))
|
|
97
|
+
timersRef.current.clear()
|
|
98
|
+
setNotifications([])
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
return () => {
|
|
103
|
+
timersRef.current.forEach((t) => clearTimeout(t))
|
|
104
|
+
timersRef.current.clear()
|
|
105
|
+
}
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
const value = React.useMemo(
|
|
109
|
+
() => ({ notifications, notify, dismiss, dismissAll }),
|
|
110
|
+
[notifications, notify, dismiss, dismissAll],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return <NotificationContext.Provider value={value}>{children}</NotificationContext.Provider>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function useNotification(): NotificationContextValue {
|
|
117
|
+
const ctx = React.useContext(NotificationContext)
|
|
118
|
+
if (!ctx) {
|
|
119
|
+
throw new Error('useNotification must be used within a NotificationProvider')
|
|
120
|
+
}
|
|
121
|
+
return ctx
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const placementClasses: Record<NotificationStackPlacement, string> = {
|
|
125
|
+
'top-right': 'top-4 right-4 items-end',
|
|
126
|
+
'top-left': 'top-4 left-4 items-start',
|
|
127
|
+
'bottom-right': 'bottom-4 right-4 items-end',
|
|
128
|
+
'bottom-left': 'bottom-4 left-4 items-start',
|
|
129
|
+
'top-center': 'top-4 left-1/2 -translate-x-1/2 items-center',
|
|
130
|
+
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 items-center',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type NotificationStackProps = {
|
|
134
|
+
/** Where the stack floats on the viewport. Default `'top-right'`. */
|
|
135
|
+
placement?: NotificationStackPlacement
|
|
136
|
+
/** Max width per notification card. Default `380` (matches FlashMessages toast). */
|
|
137
|
+
maxWidth?: number
|
|
138
|
+
className?: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Renders the active `NotificationProvider` queue as a fixed-position
|
|
143
|
+
* stack in one of six placements. Mount once next to
|
|
144
|
+
* `NotificationProvider`, typically inside `AppShell` so notifications
|
|
145
|
+
* float above every backend page.
|
|
146
|
+
*
|
|
147
|
+
* Each entry uses the `Notification` primitive — its `onDismiss`
|
|
148
|
+
* automatically calls `dismiss(id)` from the provider so the user
|
|
149
|
+
* clicking X removes the entry from the queue.
|
|
150
|
+
*/
|
|
151
|
+
export function NotificationStack({
|
|
152
|
+
placement = 'top-right',
|
|
153
|
+
maxWidth = 380,
|
|
154
|
+
className,
|
|
155
|
+
}: NotificationStackProps) {
|
|
156
|
+
const { notifications, dismiss } = useNotification()
|
|
157
|
+
if (notifications.length === 0) return null
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
className={[
|
|
162
|
+
'pointer-events-none fixed z-toast flex flex-col gap-2',
|
|
163
|
+
placementClasses[placement],
|
|
164
|
+
className,
|
|
165
|
+
]
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join(' ')}
|
|
168
|
+
data-slot="notification-stack"
|
|
169
|
+
data-placement={placement}
|
|
170
|
+
>
|
|
171
|
+
{notifications.map((entry) => (
|
|
172
|
+
<div
|
|
173
|
+
key={entry.id}
|
|
174
|
+
className="pointer-events-auto w-full"
|
|
175
|
+
style={{ maxWidth }}
|
|
176
|
+
>
|
|
177
|
+
<Notification
|
|
178
|
+
{...entry}
|
|
179
|
+
onDismiss={() => dismiss(entry.id)}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
5
|
+
import {
|
|
6
|
+
Alert,
|
|
7
|
+
AlertDescription,
|
|
8
|
+
AlertTitle,
|
|
9
|
+
type AlertStatus,
|
|
10
|
+
type AlertStyle,
|
|
11
|
+
} from './alert'
|
|
12
|
+
|
|
13
|
+
export type NotificationProps = {
|
|
14
|
+
/**
|
|
15
|
+
* Notification status. Same five values as `Alert`.
|
|
16
|
+
* Default `'information'`.
|
|
17
|
+
*/
|
|
18
|
+
status?: AlertStatus
|
|
19
|
+
/**
|
|
20
|
+
* Visual style. Same four values as `Alert`.
|
|
21
|
+
* Default `'light'`.
|
|
22
|
+
*/
|
|
23
|
+
style?: AlertStyle
|
|
24
|
+
/**
|
|
25
|
+
* Avatar/icon override. If provided, replaces the per-status default
|
|
26
|
+
* Lucide icon — typical for user-driven notifications ("John
|
|
27
|
+
* commented on..." with the author avatar). Pass an `Avatar`
|
|
28
|
+
* primitive instance or any 20×20 visual.
|
|
29
|
+
*/
|
|
30
|
+
avatar?: React.ReactNode
|
|
31
|
+
/**
|
|
32
|
+
* Notification title. Rendered as `AlertTitle` (Label/Small,
|
|
33
|
+
* `font-medium 14/20`).
|
|
34
|
+
*/
|
|
35
|
+
title?: React.ReactNode
|
|
36
|
+
/**
|
|
37
|
+
* Notification body. Rendered as `AlertDescription` at 72% opacity
|
|
38
|
+
* per Figma `170:1839` reference (Paragraph/Small, `font-regular
|
|
39
|
+
* 14/20`).
|
|
40
|
+
*/
|
|
41
|
+
description?: React.ReactNode
|
|
42
|
+
/**
|
|
43
|
+
* Right-aligned timestamp hint next to the title (e.g. `"2 min
|
|
44
|
+
* ago"`). Pre-formatted by the caller — pass through
|
|
45
|
+
* `formatRelativeTime()` from `@open-mercato/shared/lib/time` for
|
|
46
|
+
* the standard look.
|
|
47
|
+
*/
|
|
48
|
+
timestamp?: React.ReactNode
|
|
49
|
+
/**
|
|
50
|
+
* Action buttons / links rendered under the description. Pass a
|
|
51
|
+
* `<>...</>` fragment or an array of action elements. Separators
|
|
52
|
+
* are not auto-inserted — wrap your actions in a `<div>` with
|
|
53
|
+
* `gap-2` if you need custom spacing.
|
|
54
|
+
*/
|
|
55
|
+
actions?: React.ReactNode
|
|
56
|
+
/** Render the trailing X close button. Defaults to `true` — Figma reference always shows it. */
|
|
57
|
+
dismissible?: boolean
|
|
58
|
+
onDismiss?: () => void
|
|
59
|
+
dismissAriaLabel?: string
|
|
60
|
+
className?: string
|
|
61
|
+
/** Stable id used by the `NotificationStack` (or external trackers) to identify the card. Forwarded as `data-notification-id`. */
|
|
62
|
+
id?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Card composition over the `Alert` primitive matching the Figma
|
|
67
|
+
* Notification reference (`170:1839` — Error/Light/Large). Renders a
|
|
68
|
+
* status icon (or custom `avatar`), a title + timestamp row, an
|
|
69
|
+
* `opacity-72` description, and an optional row of action links. Use
|
|
70
|
+
* with `NotificationStack` for corner-floating manual-dismiss UX, or
|
|
71
|
+
* standalone inside a panel / dialog content area.
|
|
72
|
+
*
|
|
73
|
+
* Notification is intentionally a thin wrapper — every visual token
|
|
74
|
+
* comes from the `Alert` primitive so the look stays in sync with the
|
|
75
|
+
* unified Alert / Notification / Toast design.
|
|
76
|
+
*/
|
|
77
|
+
export const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
|
78
|
+
(
|
|
79
|
+
{
|
|
80
|
+
status,
|
|
81
|
+
style,
|
|
82
|
+
avatar,
|
|
83
|
+
title,
|
|
84
|
+
description,
|
|
85
|
+
timestamp,
|
|
86
|
+
actions,
|
|
87
|
+
dismissible = true,
|
|
88
|
+
onDismiss,
|
|
89
|
+
dismissAriaLabel,
|
|
90
|
+
className,
|
|
91
|
+
id,
|
|
92
|
+
},
|
|
93
|
+
ref,
|
|
94
|
+
) => {
|
|
95
|
+
return (
|
|
96
|
+
<Alert
|
|
97
|
+
ref={ref}
|
|
98
|
+
status={status}
|
|
99
|
+
style={style}
|
|
100
|
+
size="default"
|
|
101
|
+
icon={avatar}
|
|
102
|
+
dismissible={dismissible}
|
|
103
|
+
onDismiss={onDismiss}
|
|
104
|
+
dismissAriaLabel={dismissAriaLabel}
|
|
105
|
+
className={className}
|
|
106
|
+
data-slot="notification"
|
|
107
|
+
data-notification-id={id}
|
|
108
|
+
>
|
|
109
|
+
{(title || timestamp) && (
|
|
110
|
+
<div className="flex items-start gap-2">
|
|
111
|
+
{title ? <AlertTitle className="mb-0 flex-1 min-w-0">{title}</AlertTitle> : <span className="flex-1" />}
|
|
112
|
+
{timestamp ? (
|
|
113
|
+
<span className="shrink-0 text-xs leading-5 opacity-60" data-slot="notification-timestamp">
|
|
114
|
+
{timestamp}
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
{description ? (
|
|
120
|
+
<AlertDescription className={cn('opacity-72', title && 'mt-1')} data-slot="notification-description">
|
|
121
|
+
{description}
|
|
122
|
+
</AlertDescription>
|
|
123
|
+
) : null}
|
|
124
|
+
{actions ? (
|
|
125
|
+
<div
|
|
126
|
+
className={cn('flex flex-wrap items-center gap-2', (title || description) && 'mt-2.5')}
|
|
127
|
+
data-slot="notification-actions"
|
|
128
|
+
>
|
|
129
|
+
{actions}
|
|
130
|
+
</div>
|
|
131
|
+
) : null}
|
|
132
|
+
</Alert>
|
|
133
|
+
)
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
Notification.displayName = 'Notification'
|