@open-mercato/ui 0.6.4-develop.4178.1.aad9ddaa95 → 0.6.4-develop.4210.1.d412061cfe
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/AGENTS.md +14 -0
- package/dist/backend/DataTable.js +28 -62
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/primitives/activity-feed.js +196 -0
- package/dist/primitives/activity-feed.js.map +7 -0
- package/dist/primitives/avatar.js +140 -14
- package/dist/primitives/avatar.js.map +3 -3
- package/dist/primitives/badge.js +94 -5
- package/dist/primitives/badge.js.map +2 -2
- package/dist/primitives/button-group.js +52 -0
- package/dist/primitives/button-group.js.map +7 -0
- package/dist/primitives/color-picker.js +520 -0
- package/dist/primitives/color-picker.js.map +7 -0
- package/dist/primitives/command-menu.js +355 -0
- package/dist/primitives/command-menu.js.map +7 -0
- package/dist/primitives/dialog.js +210 -35
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/drawer.js +219 -0
- package/dist/primitives/drawer.js.map +7 -0
- package/dist/primitives/notification-feed.js +213 -0
- package/dist/primitives/notification-feed.js.map +7 -0
- package/dist/primitives/notification.js +23 -2
- package/dist/primitives/notification.js.map +2 -2
- package/dist/primitives/pagination.js +245 -0
- package/dist/primitives/pagination.js.map +7 -0
- package/dist/primitives/progress.js +178 -5
- package/dist/primitives/progress.js.map +2 -2
- package/dist/primitives/rating.js +174 -0
- package/dist/primitives/rating.js.map +7 -0
- package/dist/primitives/scroll-area.js +82 -0
- package/dist/primitives/scroll-area.js.map +7 -0
- package/dist/primitives/segmented-control.js +101 -0
- package/dist/primitives/segmented-control.js.map +7 -0
- package/dist/primitives/separator.js +103 -4
- package/dist/primitives/separator.js.map +2 -2
- package/dist/primitives/slider.js +115 -0
- package/dist/primitives/slider.js.map +7 -0
- package/dist/primitives/step-indicator.js +187 -0
- package/dist/primitives/step-indicator.js.map +7 -0
- package/dist/primitives/table.js +102 -10
- package/dist/primitives/table.js.map +2 -2
- package/dist/primitives/tabs.js +122 -14
- package/dist/primitives/tabs.js.map +2 -2
- package/dist/primitives/time-picker.js +9 -11
- package/dist/primitives/time-picker.js.map +2 -2
- package/package.json +8 -4
- package/src/backend/DataTable.tsx +43 -60
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
- package/src/primitives/__tests__/avatar.test.tsx +131 -0
- package/src/primitives/__tests__/badge.test.tsx +189 -0
- package/src/primitives/__tests__/button-group.test.tsx +117 -0
- package/src/primitives/__tests__/color-picker.test.tsx +589 -0
- package/src/primitives/__tests__/command-menu.test.tsx +231 -0
- package/src/primitives/__tests__/dialog.test.tsx +290 -0
- package/src/primitives/__tests__/drawer.test.tsx +264 -0
- package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
- package/src/primitives/__tests__/notification.test.tsx +102 -2
- package/src/primitives/__tests__/pagination.test.tsx +268 -0
- package/src/primitives/__tests__/progress.test.tsx +222 -0
- package/src/primitives/__tests__/rating.test.tsx +164 -0
- package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
- package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
- package/src/primitives/__tests__/separator.test.tsx +143 -0
- package/src/primitives/__tests__/slider.test.tsx +86 -0
- package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
- package/src/primitives/__tests__/table.test.tsx +199 -0
- package/src/primitives/__tests__/tabs.test.tsx +139 -0
- package/src/primitives/activity-feed.tsx +326 -0
- package/src/primitives/avatar.tsx +225 -22
- package/src/primitives/badge.tsx +141 -6
- package/src/primitives/button-group.tsx +100 -0
- package/src/primitives/color-picker.tsx +693 -0
- package/src/primitives/command-menu.tsx +503 -0
- package/src/primitives/dialog.tsx +315 -51
- package/src/primitives/drawer.tsx +338 -0
- package/src/primitives/notification-feed.tsx +358 -0
- package/src/primitives/notification.tsx +42 -2
- package/src/primitives/pagination.tsx +376 -0
- package/src/primitives/progress.tsx +267 -6
- package/src/primitives/rating.tsx +245 -0
- package/src/primitives/scroll-area.tsx +169 -0
- package/src/primitives/segmented-control.tsx +162 -0
- package/src/primitives/separator.tsx +163 -3
- package/src/primitives/slider.tsx +160 -0
- package/src/primitives/step-indicator.tsx +287 -0
- package/src/primitives/table.tsx +142 -11
- package/src/primitives/tabs.tsx +174 -12
- package/src/primitives/time-picker.tsx +9 -11
|
@@ -1,7 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from 'react'
|
|
2
4
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5
|
+
|
|
3
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* User / entity avatar with auto-generated initials, optional photo,
|
|
10
|
+
* optional icon override, optional **status indicator** (online/away/
|
|
11
|
+
* busy/away dot per Figma `Bottom Status [1.1]`), optional **badge**
|
|
12
|
+
* slot for top-right affordances (verified check, premium icon, X to
|
|
13
|
+
* remove — per Figma `Top Status [1.1]`), and optional outer **ring**
|
|
14
|
+
* (accent / status-colored).
|
|
15
|
+
*
|
|
16
|
+
* Phase B.4 rewrite per Figma `Avatars` page (`210:4129`):
|
|
17
|
+
* - `Avatar [1.1]` (`245:18786`)
|
|
18
|
+
* - `Bottom Status [1.1]` (`245:18721`) — 5 dot tones
|
|
19
|
+
* - `Top Status [1.1]` (`245:18697`) — 6 icon-badge variants
|
|
20
|
+
* - `Avatar Group [1.1]` (`581:6198`) + `Compact Avatar Group [1.1]`
|
|
21
|
+
* (`2906:14962`) — see `AvatarStack`.
|
|
22
|
+
*
|
|
23
|
+
* Backward compatibility (5 import sites — customers linking
|
|
24
|
+
* adapters + LinkEntityDialog + ds-v5 demo): existing
|
|
25
|
+
* `<Avatar src label size variant icon />` API stays callable
|
|
26
|
+
* verbatim. New optional props (`status`, `statusPosition`, `ring`,
|
|
27
|
+
* `badge`, `badgeClassName`) are additive.
|
|
28
|
+
*/
|
|
29
|
+
|
|
5
30
|
const avatarVariants = cva(
|
|
6
31
|
'inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold select-none',
|
|
7
32
|
{
|
|
@@ -37,38 +62,213 @@ function computeInitials(label: string): string {
|
|
|
37
62
|
return (first + last).toUpperCase()
|
|
38
63
|
}
|
|
39
64
|
|
|
65
|
+
// Status tone → background color class. Surface stays opaque + ringed
|
|
66
|
+
// by the page background (`ring-background`) so the badge reads as
|
|
67
|
+
// an overlay on top of the avatar circle.
|
|
68
|
+
export type AvatarStatus =
|
|
69
|
+
| 'online'
|
|
70
|
+
| 'offline'
|
|
71
|
+
| 'busy'
|
|
72
|
+
| 'away'
|
|
73
|
+
| 'success'
|
|
74
|
+
| 'warning'
|
|
75
|
+
| 'error'
|
|
76
|
+
| 'info'
|
|
77
|
+
|
|
78
|
+
const AVATAR_STATUS_BG: Record<AvatarStatus, string> = {
|
|
79
|
+
online: 'bg-status-success-icon',
|
|
80
|
+
offline: 'bg-muted-foreground',
|
|
81
|
+
busy: 'bg-status-error-icon',
|
|
82
|
+
away: 'bg-status-warning-icon',
|
|
83
|
+
success: 'bg-status-success-icon',
|
|
84
|
+
warning: 'bg-status-warning-icon',
|
|
85
|
+
error: 'bg-status-error-icon',
|
|
86
|
+
info: 'bg-status-info-icon',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Status dot size per avatar size — slightly less than 1/3 of the
|
|
90
|
+
// avatar diameter so the dot reads but doesn't overpower.
|
|
91
|
+
const AVATAR_STATUS_SIZE: Record<NonNullable<VariantProps<typeof avatarVariants>['size']>, string> = {
|
|
92
|
+
xs: 'size-1.5',
|
|
93
|
+
sm: 'size-2',
|
|
94
|
+
md: 'size-2.5',
|
|
95
|
+
lg: 'size-3',
|
|
96
|
+
xl: 'size-4',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Outer ring tones (story-style highlight, selected, status outline).
|
|
100
|
+
export type AvatarRingTone = 'accent' | 'success' | 'warning' | 'error' | 'muted'
|
|
101
|
+
|
|
102
|
+
const AVATAR_RING_CLASS: Record<AvatarRingTone, string> = {
|
|
103
|
+
accent: 'ring-2 ring-accent-indigo ring-offset-2 ring-offset-background',
|
|
104
|
+
success: 'ring-2 ring-status-success-icon ring-offset-2 ring-offset-background',
|
|
105
|
+
warning: 'ring-2 ring-status-warning-icon ring-offset-2 ring-offset-background',
|
|
106
|
+
error: 'ring-2 ring-status-error-icon ring-offset-2 ring-offset-background',
|
|
107
|
+
muted: 'ring-2 ring-input ring-offset-2 ring-offset-background',
|
|
108
|
+
}
|
|
109
|
+
|
|
40
110
|
export type AvatarProps = {
|
|
41
111
|
label: string
|
|
42
112
|
src?: string | null
|
|
43
113
|
icon?: React.ReactNode
|
|
44
114
|
ariaLabel?: string
|
|
115
|
+
/** Bottom-right (default) or top-right status dot. */
|
|
116
|
+
status?: AvatarStatus
|
|
117
|
+
/** Where the status dot sits. @default 'bottom-right' */
|
|
118
|
+
statusPosition?: 'bottom-right' | 'top-right'
|
|
119
|
+
/** Outer ring affordance — pass `true` for accent (alias for
|
|
120
|
+
* `'accent'`) or a specific tone. */
|
|
121
|
+
ring?: boolean | AvatarRingTone
|
|
122
|
+
/** Custom top-right badge slot — overrides `status` when both are
|
|
123
|
+
* set with `statusPosition='top-right'`. Use for verified check
|
|
124
|
+
* icons, premium markers, "X to remove" affordances per Figma
|
|
125
|
+
* `Top Status [1.1]`. The badge content is rendered inside a
|
|
126
|
+
* `rounded-full ring-2 ring-background` shell sized relative to
|
|
127
|
+
* the avatar — pass a simple icon (`<Check className="size-3" />`)
|
|
128
|
+
* for the cleanest look. */
|
|
129
|
+
badge?: React.ReactNode
|
|
130
|
+
/** Override the badge wrapper className (size, bg, etc.). */
|
|
131
|
+
badgeClassName?: string
|
|
45
132
|
} & VariantProps<typeof avatarVariants> &
|
|
46
133
|
Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'aria-label'>
|
|
47
134
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
135
|
+
type AvatarCircleProps = Omit<
|
|
136
|
+
AvatarProps,
|
|
137
|
+
'status' | 'statusPosition' | 'ring' | 'badge' | 'badgeClassName'
|
|
138
|
+
> & {
|
|
139
|
+
className?: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Inner div (the actual avatar circle). Implemented as forwardRef so
|
|
143
|
+
// the outer Avatar wrapper can forward refs through to the rendered
|
|
144
|
+
// `<div role="img">` regardless of whether it lives directly inside
|
|
145
|
+
// Avatar or inside the decorated `<span data-slot="avatar-root">`
|
|
146
|
+
// wrapper.
|
|
147
|
+
const AvatarCircle = React.forwardRef<HTMLDivElement, AvatarCircleProps>(function AvatarCircle(
|
|
148
|
+
{ className, label, src, icon, size, variant, ariaLabel, ...rest },
|
|
149
|
+
ref,
|
|
150
|
+
) {
|
|
151
|
+
const initials = React.useMemo(() => computeInitials(label), [label])
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
ref={ref}
|
|
155
|
+
role="img"
|
|
156
|
+
aria-label={ariaLabel ?? label}
|
|
157
|
+
data-slot="avatar"
|
|
158
|
+
className={cn(avatarVariants({ size, variant }), className)}
|
|
159
|
+
{...rest}
|
|
160
|
+
>
|
|
161
|
+
{src ? (
|
|
162
|
+
<img src={src} alt="" className="size-full object-cover" aria-hidden="true" />
|
|
163
|
+
) : icon ? (
|
|
164
|
+
<span aria-hidden="true" className="flex items-center justify-center [&>svg]:size-[55%]">
|
|
165
|
+
{icon}
|
|
166
|
+
</span>
|
|
167
|
+
) : (
|
|
168
|
+
<span aria-hidden="true">{initials}</span>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>((props, ref) => {
|
|
175
|
+
const {
|
|
176
|
+
className,
|
|
177
|
+
label,
|
|
178
|
+
src,
|
|
179
|
+
icon,
|
|
180
|
+
size = 'md',
|
|
181
|
+
variant,
|
|
182
|
+
ariaLabel,
|
|
183
|
+
status,
|
|
184
|
+
statusPosition = 'bottom-right',
|
|
185
|
+
ring,
|
|
186
|
+
badge,
|
|
187
|
+
badgeClassName,
|
|
188
|
+
...rest
|
|
189
|
+
} = props
|
|
190
|
+
|
|
191
|
+
const resolvedRingTone: AvatarRingTone | null = ring === true ? 'accent' : ring || null
|
|
192
|
+
const showStatus = status !== undefined
|
|
193
|
+
const showBadge =
|
|
194
|
+
badge !== undefined && (statusPosition === 'top-right' || !showStatus)
|
|
195
|
+
const showTopStatus = showStatus && statusPosition === 'top-right' && !showBadge
|
|
196
|
+
const showBottomStatus = showStatus && statusPosition === 'bottom-right' && badge === undefined
|
|
197
|
+
|
|
198
|
+
// Plain avatar (no overlay decorations) — preserve the original
|
|
199
|
+
// single-element output for backward compat.
|
|
200
|
+
if (!showStatus && !showBadge && !resolvedRingTone) {
|
|
51
201
|
return (
|
|
52
|
-
<
|
|
53
|
-
ref={ref}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
202
|
+
<AvatarCircle
|
|
203
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
204
|
+
className={className}
|
|
205
|
+
label={label}
|
|
206
|
+
src={src}
|
|
207
|
+
icon={icon}
|
|
208
|
+
size={size}
|
|
209
|
+
variant={variant}
|
|
210
|
+
ariaLabel={ariaLabel}
|
|
57
211
|
{...rest}
|
|
58
|
-
|
|
59
|
-
{src ? (
|
|
60
|
-
<img src={src} alt="" className="size-full object-cover" aria-hidden="true" />
|
|
61
|
-
) : icon ? (
|
|
62
|
-
<span aria-hidden="true" className="flex items-center justify-center [&>svg]:size-[55%]">
|
|
63
|
-
{icon}
|
|
64
|
-
</span>
|
|
65
|
-
) : (
|
|
66
|
-
<span aria-hidden="true">{initials}</span>
|
|
67
|
-
)}
|
|
68
|
-
</div>
|
|
212
|
+
/>
|
|
69
213
|
)
|
|
70
|
-
}
|
|
71
|
-
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const ringClass = resolvedRingTone ? AVATAR_RING_CLASS[resolvedRingTone] : ''
|
|
217
|
+
const statusSize = AVATAR_STATUS_SIZE[size ?? 'md']
|
|
218
|
+
const statusPositionClass =
|
|
219
|
+
statusPosition === 'top-right'
|
|
220
|
+
? '-right-0 -top-0 translate-x-1/4 -translate-y-1/4'
|
|
221
|
+
: '-right-0 -bottom-0 translate-x-1/4 translate-y-1/4'
|
|
222
|
+
const badgePositionClass = '-right-0 -top-0 translate-x-1/4 -translate-y-1/4'
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<span
|
|
226
|
+
data-slot="avatar-root"
|
|
227
|
+
className={cn('relative inline-flex shrink-0 rounded-full', ringClass)}
|
|
228
|
+
>
|
|
229
|
+
<AvatarCircle
|
|
230
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
231
|
+
className={className}
|
|
232
|
+
label={label}
|
|
233
|
+
src={src}
|
|
234
|
+
icon={icon}
|
|
235
|
+
size={size}
|
|
236
|
+
variant={variant}
|
|
237
|
+
ariaLabel={ariaLabel}
|
|
238
|
+
{...rest}
|
|
239
|
+
/>
|
|
240
|
+
{showBottomStatus || showTopStatus ? (
|
|
241
|
+
<span
|
|
242
|
+
data-slot="avatar-status"
|
|
243
|
+
data-status={status}
|
|
244
|
+
data-position={statusPosition}
|
|
245
|
+
aria-hidden="true"
|
|
246
|
+
className={cn(
|
|
247
|
+
'absolute inline-block rounded-full ring-2 ring-background',
|
|
248
|
+
statusSize,
|
|
249
|
+
AVATAR_STATUS_BG[status as AvatarStatus],
|
|
250
|
+
statusPositionClass,
|
|
251
|
+
)}
|
|
252
|
+
/>
|
|
253
|
+
) : null}
|
|
254
|
+
{showBadge ? (
|
|
255
|
+
<span
|
|
256
|
+
data-slot="avatar-badge"
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
className={cn(
|
|
259
|
+
'absolute inline-flex items-center justify-center rounded-full ring-2 ring-background bg-background text-foreground',
|
|
260
|
+
// Default size ~40% of avatar; consumer overrides via badgeClassName.
|
|
261
|
+
size === 'xs' || size === 'sm' ? 'size-3.5' : size === 'md' ? 'size-4' : size === 'lg' ? 'size-5' : 'size-6',
|
|
262
|
+
badgePositionClass,
|
|
263
|
+
badgeClassName,
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
266
|
+
{badge}
|
|
267
|
+
</span>
|
|
268
|
+
) : null}
|
|
269
|
+
</span>
|
|
270
|
+
)
|
|
271
|
+
})
|
|
72
272
|
|
|
73
273
|
Avatar.displayName = 'Avatar'
|
|
74
274
|
|
|
@@ -87,7 +287,10 @@ export function AvatarStack({ children, max = 4, size = 'md', className }: Avata
|
|
|
87
287
|
const overflow = items.length - max
|
|
88
288
|
|
|
89
289
|
return (
|
|
90
|
-
<div
|
|
290
|
+
<div
|
|
291
|
+
data-slot="avatar-stack"
|
|
292
|
+
className={cn('flex items-center [&>*:not(:first-child)]:-ml-2 [&>*]:ring-2 [&>*]:ring-background', className)}
|
|
293
|
+
>
|
|
91
294
|
{visible}
|
|
92
295
|
{overflow > 0 && (
|
|
93
296
|
<Avatar label={`+${overflow}`} size={size} variant="monochrome" className="-ml-2" />
|
package/src/primitives/badge.tsx
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from 'react'
|
|
4
|
+
import { X } from 'lucide-react'
|
|
2
5
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
6
|
+
|
|
3
7
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
4
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Status / category pill primitive. Phase B.8 rewrite per Figma
|
|
11
|
+
* `Badge` page (`119:2863`) — `Badge [1.1]` (`118:2324`, color + size
|
|
12
|
+
* matrix) and `Status Badge [1.1]` (`171:5100`, semantic-colored
|
|
13
|
+
* status pills).
|
|
14
|
+
*
|
|
15
|
+
* Backward compatibility (83 import sites — biggest cascade in v5):
|
|
16
|
+
* - The complete `variant` union (`default | secondary | destructive
|
|
17
|
+
* | outline | muted | success | warning | info | neutral | error`)
|
|
18
|
+
* stays callable verbatim.
|
|
19
|
+
* - Every existing import-site renders identically — no padding
|
|
20
|
+
* bump, no font-weight delta, no color change.
|
|
21
|
+
* - `className` passthrough works for every variant.
|
|
22
|
+
*
|
|
23
|
+
* New (additive):
|
|
24
|
+
* - `size: 'sm' | 'default' | 'lg'` — text-xs / text-xs / text-sm
|
|
25
|
+
* typography ladder with matching px/py padding.
|
|
26
|
+
* - `dot: boolean` — leading 6px dot in the variant's accent color
|
|
27
|
+
* (status-style "● Active" pattern per Figma).
|
|
28
|
+
* - `removable: boolean` + `onRemove` — trailing X icon-button for
|
|
29
|
+
* tag-style dismissible badges.
|
|
30
|
+
* - `brand` variant — brand-violet/10 tint per `Tag` brand variant
|
|
31
|
+
* (kept distinct from generic info / neutral — used for renewal
|
|
32
|
+
* tags, custom views).
|
|
33
|
+
*/
|
|
34
|
+
|
|
5
35
|
const badgeVariants = cva(
|
|
6
|
-
'inline-flex items-center rounded-full border
|
|
36
|
+
'inline-flex items-center gap-1 rounded-full border font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
7
37
|
{
|
|
8
38
|
variants: {
|
|
9
39
|
variant: {
|
|
10
40
|
default: 'border-transparent bg-primary text-primary-foreground shadow',
|
|
11
41
|
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
42
|
+
// Kept verbatim from the base contract — BC guarantee in spec
|
|
43
|
+
// 2026-05-13-ds-foundation-v5.md mandates existing variants must
|
|
44
|
+
// render byte-identically. For the soft error-toned look, use
|
|
45
|
+
// the new `error` variant below.
|
|
12
46
|
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow',
|
|
13
47
|
outline: 'text-foreground',
|
|
14
48
|
muted: 'border-transparent bg-muted text-muted-foreground',
|
|
@@ -17,19 +51,120 @@ const badgeVariants = cva(
|
|
|
17
51
|
info: 'border-status-info-border bg-status-info-bg text-status-info-text',
|
|
18
52
|
neutral: 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text',
|
|
19
53
|
error: 'border-status-error-border bg-status-error-bg text-status-error-text',
|
|
54
|
+
// Phase B.8 addition — brand-violet tinted pill for custom
|
|
55
|
+
// views / renewal tags. Mirrors `Tag` brand variant.
|
|
56
|
+
brand: 'border-brand-violet/30 bg-brand-violet/10 text-brand-violet',
|
|
57
|
+
},
|
|
58
|
+
size: {
|
|
59
|
+
sm: 'px-2 py-0.5 text-[10px]',
|
|
60
|
+
default: 'px-2.5 py-0.5 text-xs',
|
|
61
|
+
lg: 'px-3 py-1 text-sm',
|
|
20
62
|
},
|
|
21
63
|
},
|
|
22
64
|
defaultVariants: {
|
|
23
65
|
variant: 'default',
|
|
66
|
+
size: 'default',
|
|
24
67
|
},
|
|
25
|
-
}
|
|
68
|
+
},
|
|
26
69
|
)
|
|
27
70
|
|
|
28
|
-
|
|
71
|
+
// Leading-dot tone per variant. Uses the icon-color end of the status
|
|
72
|
+
// token palette so the dot reads against the soft tinted background.
|
|
73
|
+
const BADGE_DOT_TONE: Record<NonNullable<VariantProps<typeof badgeVariants>['variant']>, string> = {
|
|
74
|
+
default: 'bg-primary-foreground',
|
|
75
|
+
secondary: 'bg-secondary-foreground',
|
|
76
|
+
destructive: 'bg-status-error-icon',
|
|
77
|
+
outline: 'bg-foreground',
|
|
78
|
+
muted: 'bg-muted-foreground',
|
|
79
|
+
success: 'bg-status-success-icon',
|
|
80
|
+
warning: 'bg-status-warning-icon',
|
|
81
|
+
info: 'bg-status-info-icon',
|
|
82
|
+
neutral: 'bg-muted-foreground',
|
|
83
|
+
error: 'bg-status-error-icon',
|
|
84
|
+
brand: 'bg-brand-violet',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Leading-dot size per badge size.
|
|
88
|
+
const BADGE_DOT_SIZE: Record<NonNullable<VariantProps<typeof badgeVariants>['size']>, string> = {
|
|
89
|
+
sm: 'size-1.5',
|
|
90
|
+
default: 'size-1.5',
|
|
91
|
+
lg: 'size-2',
|
|
92
|
+
}
|
|
29
93
|
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
94
|
+
export type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>['variant']>
|
|
95
|
+
export type BadgeSize = NonNullable<VariantProps<typeof badgeVariants>['size']>
|
|
96
|
+
|
|
97
|
+
export type BadgeProps = React.HTMLAttributes<HTMLDivElement> &
|
|
98
|
+
VariantProps<typeof badgeVariants> & {
|
|
99
|
+
/** Leading status dot in the variant's accent tone. */
|
|
100
|
+
dot?: boolean
|
|
101
|
+
/** Trailing X icon-button for tag-style dismissible badges.
|
|
102
|
+
* Renders only when `removable` is true. */
|
|
103
|
+
removable?: boolean
|
|
104
|
+
/** Click handler for the remove button. Required when `removable`
|
|
105
|
+
* is true. */
|
|
106
|
+
onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
|
107
|
+
/** Accessible label for the remove button. Default `'Remove'`. */
|
|
108
|
+
removeAriaLabel?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
|
112
|
+
(
|
|
113
|
+
{
|
|
114
|
+
className,
|
|
115
|
+
variant,
|
|
116
|
+
size,
|
|
117
|
+
dot = false,
|
|
118
|
+
removable = false,
|
|
119
|
+
onRemove,
|
|
120
|
+
removeAriaLabel = 'Remove',
|
|
121
|
+
children,
|
|
122
|
+
...props
|
|
123
|
+
},
|
|
124
|
+
ref,
|
|
125
|
+
) => {
|
|
126
|
+
const resolvedVariant = (variant ?? 'default') as BadgeVariant
|
|
127
|
+
const resolvedSize = (size ?? 'default') as BadgeSize
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
ref={ref}
|
|
131
|
+
data-slot="badge"
|
|
132
|
+
data-variant={resolvedVariant}
|
|
133
|
+
data-size={resolvedSize}
|
|
134
|
+
className={cn(badgeVariants({ variant, size }), className)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
{dot ? (
|
|
138
|
+
<span
|
|
139
|
+
data-slot="badge-dot"
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
className={cn(
|
|
142
|
+
'inline-block shrink-0 rounded-full',
|
|
143
|
+
BADGE_DOT_SIZE[resolvedSize],
|
|
144
|
+
BADGE_DOT_TONE[resolvedVariant],
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
) : null}
|
|
148
|
+
{children}
|
|
149
|
+
{removable ? (
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
data-slot="badge-remove"
|
|
153
|
+
aria-label={removeAriaLabel}
|
|
154
|
+
onClick={onRemove}
|
|
155
|
+
className={cn(
|
|
156
|
+
'-mr-0.5 ml-0.5 inline-flex shrink-0 items-center justify-center rounded-full outline-none transition-opacity',
|
|
157
|
+
'opacity-70 hover:opacity-100 focus-visible:opacity-100',
|
|
158
|
+
resolvedSize === 'lg' ? 'size-4' : 'size-3.5',
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
<X aria-hidden="true" className={resolvedSize === 'lg' ? 'size-3' : 'size-2.5'} />
|
|
162
|
+
</button>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
},
|
|
167
|
+
)
|
|
33
168
|
|
|
34
169
|
Badge.displayName = 'Badge'
|
|
35
170
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Joined / connected button group per Figma `Button Group [1.1]`
|
|
10
|
+
* (component set id `199961:1616` in DS Open Mercato). Renders an outer
|
|
11
|
+
* rounded border with N children that touch each other to share an
|
|
12
|
+
* internal separator line. Use for related actions on the same row
|
|
13
|
+
* (Save / Save & New / overflow ellipsis) or compact view-mode toggles.
|
|
14
|
+
*
|
|
15
|
+
* `ButtonGroup` is for *related actions* (each does something different).
|
|
16
|
+
* For mutually-exclusive selection states (only one selected at a time)
|
|
17
|
+
* use `SegmentedControl` instead.
|
|
18
|
+
*
|
|
19
|
+
* The wrapper takes a `size` (2xs / sm / default) that controls the outer
|
|
20
|
+
* radius per Figma — `2xs` = `rounded-sm` (Figma 2X-Small 24, radius 6),
|
|
21
|
+
* `sm` and `default` = `rounded-md` (Figma X-Small 32 + Small 36, radius 8).
|
|
22
|
+
* Same-row sizing convention from `packages/ui/AGENTS.md` still applies:
|
|
23
|
+
* every child Button / IconButton inside the group MUST share the same
|
|
24
|
+
* `size` prop as the group itself.
|
|
25
|
+
*
|
|
26
|
+
* Visual joining is handled at the wrapper level via descendant CSS
|
|
27
|
+
* selectors — children render normally, the wrapper strips their own
|
|
28
|
+
* corners, shadow, and outer borders, and adds an internal separator
|
|
29
|
+
* (`border-r` / `border-b`) between siblings. This way no `cloneElement`
|
|
30
|
+
* is required and any element that accepts a `className` (Button,
|
|
31
|
+
* IconButton, LinkButton, asChild wrappers, etc.) can be a group member.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ButtonGroup>
|
|
35
|
+
* <Button>Save</Button>
|
|
36
|
+
* <Button>Save & New</Button>
|
|
37
|
+
* <IconButton aria-label="More"><MoreHorizontal /></IconButton>
|
|
38
|
+
* </ButtonGroup>
|
|
39
|
+
*
|
|
40
|
+
* <ButtonGroup orientation="vertical" size="sm">
|
|
41
|
+
* <Button variant="outline">Up</Button>
|
|
42
|
+
* <Button variant="outline">Down</Button>
|
|
43
|
+
* </ButtonGroup>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
const buttonGroupVariants = cva(
|
|
47
|
+
// Outer chrome: rounded shell + border + clipped corners + shared shadow.
|
|
48
|
+
// Descendant rules collapse children into segments:
|
|
49
|
+
// * strip own corners / shadow / outer border on every child
|
|
50
|
+
// * raise focus-visible above neighbours so the focus ring is not clipped
|
|
51
|
+
// * separator: border-r (horizontal) or border-b (vertical) on every
|
|
52
|
+
// child except the last one
|
|
53
|
+
'inline-flex w-fit border border-input bg-background shadow-xs overflow-hidden ' +
|
|
54
|
+
"[&>*]:rounded-none [&>*]:shadow-none [&>*]:border-0 " +
|
|
55
|
+
"[&>*]:focus-visible:relative [&>*]:focus-visible:z-10 " +
|
|
56
|
+
'disabled:opacity-50',
|
|
57
|
+
{
|
|
58
|
+
variants: {
|
|
59
|
+
orientation: {
|
|
60
|
+
horizontal:
|
|
61
|
+
'flex-row [&>*:not(:last-child)]:border-r [&>*:not(:last-child)]:border-input',
|
|
62
|
+
vertical:
|
|
63
|
+
'flex-col [&>*:not(:last-child)]:border-b [&>*:not(:last-child)]:border-input',
|
|
64
|
+
},
|
|
65
|
+
size: {
|
|
66
|
+
// Figma `2X-Small (24)` → cornerRadius 6 → DS `rounded-sm` (6px).
|
|
67
|
+
'2xs': 'rounded-sm',
|
|
68
|
+
// Figma `X-Small (32)` → cornerRadius 8 → DS `rounded-md` (8px).
|
|
69
|
+
sm: 'rounded-md',
|
|
70
|
+
// Figma `Small (36)` → cornerRadius 8 → DS `rounded-md` (8px).
|
|
71
|
+
default: 'rounded-md',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
defaultVariants: {
|
|
75
|
+
orientation: 'horizontal',
|
|
76
|
+
size: 'default',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
export type ButtonGroupProps = React.HTMLAttributes<HTMLDivElement> &
|
|
82
|
+
VariantProps<typeof buttonGroupVariants> & {
|
|
83
|
+
/** Forwarded to the wrapper's `aria-label` for screen-reader context. */
|
|
84
|
+
'aria-label'?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
88
|
+
({ className, orientation, size, ...props }, ref) => (
|
|
89
|
+
<div
|
|
90
|
+
ref={ref}
|
|
91
|
+
data-slot="button-group"
|
|
92
|
+
role="group"
|
|
93
|
+
className={cn(buttonGroupVariants({ orientation, size }), className)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
ButtonGroup.displayName = 'ButtonGroup'
|
|
99
|
+
|
|
100
|
+
export { buttonGroupVariants }
|