@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.
Files changed (137) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/CrudForm.js +46 -129
  3. package/dist/backend/CrudForm.js.map +3 -3
  4. package/dist/backend/DataTable.js +10 -3
  5. package/dist/backend/DataTable.js.map +2 -2
  6. package/dist/backend/EmptyState.js +1 -47
  7. package/dist/backend/EmptyState.js.map +2 -2
  8. package/dist/backend/FilterOverlay.js +31 -26
  9. package/dist/backend/FilterOverlay.js.map +2 -2
  10. package/dist/backend/FlashMessages.js +23 -25
  11. package/dist/backend/FlashMessages.js.map +2 -2
  12. package/dist/backend/JsonBuilder.js +4 -2
  13. package/dist/backend/JsonBuilder.js.map +2 -2
  14. package/dist/backend/LogList.js +49 -0
  15. package/dist/backend/LogList.js.map +7 -0
  16. package/dist/backend/crud/CollapsibleGroup.js +13 -3
  17. package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
  18. package/dist/backend/detail/InlineEditors.js +80 -32
  19. package/dist/backend/detail/InlineEditors.js.map +2 -2
  20. package/dist/backend/inputs/ComboboxInput.js +60 -6
  21. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  22. package/dist/backend/inputs/DatePicker.js +6 -142
  23. package/dist/backend/inputs/DatePicker.js.map +2 -2
  24. package/dist/backend/inputs/DateTimePicker.js +6 -182
  25. package/dist/backend/inputs/DateTimePicker.js.map +2 -2
  26. package/dist/backend/inputs/TagsInput.js +16 -19
  27. package/dist/backend/inputs/TagsInput.js.map +2 -2
  28. package/dist/backend/inputs/TimeInput.js +1 -1
  29. package/dist/backend/inputs/TimeInput.js.map +2 -2
  30. package/dist/backend/inputs/TimePicker.js +83 -72
  31. package/dist/backend/inputs/TimePicker.js.map +2 -2
  32. package/dist/backend/utils/customFieldForms.js +2 -2
  33. package/dist/backend/utils/customFieldForms.js.map +1 -1
  34. package/dist/portal/components/PortalEmptyState.js +13 -9
  35. package/dist/portal/components/PortalEmptyState.js.map +2 -2
  36. package/dist/primitives/accordion.js +137 -0
  37. package/dist/primitives/accordion.js.map +7 -0
  38. package/dist/primitives/alert.js +180 -12
  39. package/dist/primitives/alert.js.map +2 -2
  40. package/dist/primitives/calendar.js +122 -32
  41. package/dist/primitives/calendar.js.map +2 -2
  42. package/dist/primitives/compact-select.js +47 -0
  43. package/dist/primitives/compact-select.js.map +7 -0
  44. package/dist/primitives/counter-input.js +174 -0
  45. package/dist/primitives/counter-input.js.map +7 -0
  46. package/dist/primitives/date-picker-helpers.js +79 -0
  47. package/dist/primitives/date-picker-helpers.js.map +7 -0
  48. package/dist/primitives/date-picker.js +216 -0
  49. package/dist/primitives/date-picker.js.map +7 -0
  50. package/dist/primitives/date-range-picker.js +239 -0
  51. package/dist/primitives/date-range-picker.js.map +7 -0
  52. package/dist/primitives/digit-input.js +157 -0
  53. package/dist/primitives/digit-input.js.map +7 -0
  54. package/dist/primitives/empty-state.js +93 -0
  55. package/dist/primitives/empty-state.js.map +7 -0
  56. package/dist/primitives/inline-input.js +29 -0
  57. package/dist/primitives/inline-input.js.map +7 -0
  58. package/dist/primitives/inline-select.js +41 -0
  59. package/dist/primitives/inline-select.js.map +7 -0
  60. package/dist/primitives/notification-stack.js +116 -0
  61. package/dist/primitives/notification-stack.js.map +7 -0
  62. package/dist/primitives/notification.js +62 -0
  63. package/dist/primitives/notification.js.map +7 -0
  64. package/dist/primitives/rich-editor.js +1230 -0
  65. package/dist/primitives/rich-editor.js.map +7 -0
  66. package/dist/primitives/select.js +1 -0
  67. package/dist/primitives/select.js.map +2 -2
  68. package/dist/primitives/skeleton.js +55 -0
  69. package/dist/primitives/skeleton.js.map +7 -0
  70. package/dist/primitives/tag-input.js +186 -0
  71. package/dist/primitives/tag-input.js.map +7 -0
  72. package/dist/primitives/tag.js +66 -13
  73. package/dist/primitives/tag.js.map +2 -2
  74. package/dist/primitives/time-picker.js +793 -0
  75. package/dist/primitives/time-picker.js.map +7 -0
  76. package/package.json +4 -3
  77. package/src/backend/CrudForm.tsx +61 -113
  78. package/src/backend/DataTable.tsx +17 -5
  79. package/src/backend/EmptyState.tsx +13 -63
  80. package/src/backend/FilterOverlay.tsx +38 -22
  81. package/src/backend/FlashMessages.tsx +23 -24
  82. package/src/backend/JsonBuilder.tsx +4 -2
  83. package/src/backend/LogList.tsx +109 -0
  84. package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
  85. package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
  86. package/src/backend/__tests__/LogList.test.tsx +163 -0
  87. package/src/backend/crud/CollapsibleGroup.tsx +17 -3
  88. package/src/backend/detail/InlineEditors.tsx +78 -25
  89. package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
  90. package/src/backend/inputs/ComboboxInput.tsx +81 -9
  91. package/src/backend/inputs/DatePicker.tsx +28 -155
  92. package/src/backend/inputs/DateTimePicker.tsx +25 -193
  93. package/src/backend/inputs/TagsInput.tsx +10 -13
  94. package/src/backend/inputs/TimeInput.tsx +2 -1
  95. package/src/backend/inputs/TimePicker.tsx +99 -70
  96. package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
  97. package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
  98. package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
  99. package/src/backend/utils/customFieldForms.ts +2 -2
  100. package/src/portal/components/PortalEmptyState.tsx +27 -22
  101. package/src/primitives/__tests__/accordion.test.tsx +197 -0
  102. package/src/primitives/__tests__/alert.test.tsx +204 -0
  103. package/src/primitives/__tests__/compact-select.test.tsx +98 -0
  104. package/src/primitives/__tests__/counter-input.test.tsx +165 -0
  105. package/src/primitives/__tests__/date-picker.test.tsx +221 -0
  106. package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
  107. package/src/primitives/__tests__/digit-input.test.tsx +168 -0
  108. package/src/primitives/__tests__/empty-state.test.tsx +154 -0
  109. package/src/primitives/__tests__/inline-input.test.tsx +100 -0
  110. package/src/primitives/__tests__/inline-select.test.tsx +103 -0
  111. package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
  112. package/src/primitives/__tests__/notification.test.tsx +119 -0
  113. package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
  114. package/src/primitives/__tests__/skeleton.test.tsx +76 -0
  115. package/src/primitives/__tests__/tag-input.test.tsx +198 -0
  116. package/src/primitives/__tests__/tag.test.tsx +123 -0
  117. package/src/primitives/__tests__/time-picker.test.tsx +392 -0
  118. package/src/primitives/accordion.tsx +225 -0
  119. package/src/primitives/alert.tsx +234 -24
  120. package/src/primitives/calendar.tsx +146 -38
  121. package/src/primitives/compact-select.tsx +59 -0
  122. package/src/primitives/counter-input.tsx +211 -0
  123. package/src/primitives/date-picker-helpers.ts +96 -0
  124. package/src/primitives/date-picker.tsx +290 -0
  125. package/src/primitives/date-range-picker.tsx +281 -0
  126. package/src/primitives/digit-input.tsx +208 -0
  127. package/src/primitives/empty-state.tsx +140 -0
  128. package/src/primitives/inline-input.tsx +55 -0
  129. package/src/primitives/inline-select.tsx +74 -0
  130. package/src/primitives/notification-stack.tsx +185 -0
  131. package/src/primitives/notification.tsx +136 -0
  132. package/src/primitives/rich-editor.tsx +1578 -0
  133. package/src/primitives/select.tsx +1 -0
  134. package/src/primitives/skeleton.tsx +70 -0
  135. package/src/primitives/tag-input.tsx +223 -0
  136. package/src/primitives/tag.tsx +69 -10
  137. 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'