@momentumworld/ui-react 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2HQPCV2L.js +895 -0
- package/dist/chunk-2HQPCV2L.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +356 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2162 -0
- package/dist/index.js +11640 -0
- package/dist/index.js.map +1 -0
- package/dist/input-JCZs61MO.d.ts +24 -0
- package/package.json +90 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/layout/dashboard-topbar.tsx +221 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-counter.tsx +109 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/aurora-orb.tsx +163 -0
- package/src/components/ui/aurora-text.tsx +76 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +150 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/chat-divider.tsx +67 -0
- package/src/components/ui/chat.tsx +494 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/color-picker.tsx +389 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/confetti.tsx +196 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/context-meter.tsx +134 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/data-table-paged.tsx +163 -0
- package/src/components/ui/date-picker.tsx +154 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/fab.tsx +45 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/help-tip.tsx +94 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/markdown.tsx +188 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/pipeline.tsx +102 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/segmented-control.tsx +83 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +774 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +206 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +242 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/thinking-orb.tsx +6 -0
- package/src/components/ui/time-picker.tsx +308 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +143 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +329 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { CheckIcon, PlusIcon, UserRoundIcon } from "lucide-react";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../lib/utils/css";
|
|
9
|
+
|
|
10
|
+
const avatarVariants = cva(
|
|
11
|
+
"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-muted align-middle font-medium",
|
|
12
|
+
{
|
|
13
|
+
defaultVariants: { size: "sm" },
|
|
14
|
+
variants: {
|
|
15
|
+
size: {
|
|
16
|
+
xs: "size-6 text-[0.625rem]",
|
|
17
|
+
sm: "size-8 text-xs",
|
|
18
|
+
md: "size-10 text-sm",
|
|
19
|
+
lg: "size-12 text-base",
|
|
20
|
+
xl: "size-14 text-lg",
|
|
21
|
+
"2xl": "size-16 text-xl",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
|
|
28
|
+
|
|
29
|
+
interface AvatarProps
|
|
30
|
+
extends AvatarPrimitive.Root.Props,
|
|
31
|
+
VariantProps<typeof avatarVariants> {
|
|
32
|
+
/** Image URL. When omitted or it fails to load, the fallback is shown. */
|
|
33
|
+
src?: string;
|
|
34
|
+
alt?: string;
|
|
35
|
+
/** Initials shown when there is no image. */
|
|
36
|
+
initials?: React.ReactNode;
|
|
37
|
+
/** Custom fallback node (overrides initials and the placeholder icon). */
|
|
38
|
+
placeholder?: React.ReactNode;
|
|
39
|
+
/** Icon component used as the fallback when nothing else is provided. */
|
|
40
|
+
placeholderIcon?: React.ElementType;
|
|
41
|
+
/** A presence dot in the bottom-right corner. */
|
|
42
|
+
status?: "online" | "offline";
|
|
43
|
+
/** Shows a verified tick in the bottom-right corner. */
|
|
44
|
+
verified?: boolean;
|
|
45
|
+
/** A node (e.g. <AvatarCompanyIcon />) shown in the top-right corner. */
|
|
46
|
+
badge?: React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Avatar({
|
|
50
|
+
className,
|
|
51
|
+
size,
|
|
52
|
+
src,
|
|
53
|
+
alt,
|
|
54
|
+
initials,
|
|
55
|
+
placeholder,
|
|
56
|
+
placeholderIcon: PlaceholderIcon = UserRoundIcon,
|
|
57
|
+
status,
|
|
58
|
+
verified,
|
|
59
|
+
badge,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: AvatarProps) {
|
|
63
|
+
const fallback = initials ? (
|
|
64
|
+
initials
|
|
65
|
+
) : (
|
|
66
|
+
(placeholder ?? <PlaceholderIcon className="size-[55%] text-muted-foreground" />)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const root = (
|
|
70
|
+
<AvatarPrimitive.Root
|
|
71
|
+
className={cn(avatarVariants({ size }), className)}
|
|
72
|
+
data-slot="avatar"
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
{children ?? (
|
|
76
|
+
<>
|
|
77
|
+
{src ? <AvatarImage alt={alt} src={src} /> : null}
|
|
78
|
+
<AvatarFallback>{fallback}</AvatarFallback>
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
</AvatarPrimitive.Root>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!(status || verified || badge)) return root;
|
|
85
|
+
|
|
86
|
+
// Overlays live in a non-clipping wrapper so the rounded avatar's
|
|
87
|
+
// overflow-hidden doesn't cut them off at the corners.
|
|
88
|
+
return (
|
|
89
|
+
<span
|
|
90
|
+
className="relative inline-flex shrink-0 align-middle"
|
|
91
|
+
data-slot="avatar-root"
|
|
92
|
+
>
|
|
93
|
+
{root}
|
|
94
|
+
{badge ? (
|
|
95
|
+
<span className="absolute end-0 top-0 flex" data-slot="avatar-badge">
|
|
96
|
+
{badge}
|
|
97
|
+
</span>
|
|
98
|
+
) : null}
|
|
99
|
+
{status ? (
|
|
100
|
+
<span
|
|
101
|
+
className="absolute end-[6%] bottom-[6%] h-1/4 w-1/4 rounded-full ring-2 ring-background data-[status=offline]:bg-muted-foreground/40 data-[status=online]:bg-success"
|
|
102
|
+
data-slot="avatar-status"
|
|
103
|
+
data-status={status}
|
|
104
|
+
/>
|
|
105
|
+
) : null}
|
|
106
|
+
{verified ? (
|
|
107
|
+
<span
|
|
108
|
+
className="absolute end-0 bottom-0 flex h-1/3 w-1/3 items-center justify-center rounded-full bg-brand text-background ring-2 ring-background"
|
|
109
|
+
data-slot="avatar-verified"
|
|
110
|
+
>
|
|
111
|
+
<CheckIcon className="size-2/3" strokeWidth={3} />
|
|
112
|
+
</span>
|
|
113
|
+
) : null}
|
|
114
|
+
</span>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
|
119
|
+
return (
|
|
120
|
+
<AvatarPrimitive.Image
|
|
121
|
+
className={cn("size-full object-cover", className)}
|
|
122
|
+
data-slot="avatar-image"
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function AvatarFallback({
|
|
129
|
+
className,
|
|
130
|
+
...props
|
|
131
|
+
}: AvatarPrimitive.Fallback.Props) {
|
|
132
|
+
return (
|
|
133
|
+
<AvatarPrimitive.Fallback
|
|
134
|
+
className={cn(
|
|
135
|
+
"flex size-full items-center justify-center rounded-[inherit] bg-muted text-muted-foreground",
|
|
136
|
+
className,
|
|
137
|
+
)}
|
|
138
|
+
data-slot="avatar-fallback"
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A small company/brand logo badge, meant for the Avatar `badge` prop. */
|
|
145
|
+
function AvatarCompanyIcon({
|
|
146
|
+
className,
|
|
147
|
+
size = "md",
|
|
148
|
+
alt,
|
|
149
|
+
...props
|
|
150
|
+
}: React.ComponentProps<"img"> & { size?: "sm" | "md" | "lg" }) {
|
|
151
|
+
const sizes = { lg: "size-5", md: "size-4", sm: "size-3.5" } as const;
|
|
152
|
+
return (
|
|
153
|
+
// biome-ignore lint/a11y/useAltText: alt forwarded via props
|
|
154
|
+
<img
|
|
155
|
+
alt={alt}
|
|
156
|
+
className={cn(
|
|
157
|
+
"rounded-[4px] object-cover ring-2 ring-background",
|
|
158
|
+
sizes[size],
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
data-slot="avatar-company-icon"
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A dashed "add" button sized to match avatars — for avatar groups. */
|
|
168
|
+
function AvatarAddButton({
|
|
169
|
+
className,
|
|
170
|
+
size = "sm",
|
|
171
|
+
...props
|
|
172
|
+
}: React.ComponentProps<"button"> & { size?: AvatarSize }) {
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
className={cn(
|
|
176
|
+
avatarVariants({ size }),
|
|
177
|
+
"border border-input border-dashed bg-transparent text-muted-foreground outline-none transition-colors hover:border-transparent hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
178
|
+
className,
|
|
179
|
+
)}
|
|
180
|
+
data-slot="avatar-add-button"
|
|
181
|
+
type="button"
|
|
182
|
+
{...props}
|
|
183
|
+
>
|
|
184
|
+
<PlusIcon className="size-1/2" />
|
|
185
|
+
</button>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const labelGroupText = {
|
|
190
|
+
lg: { subtitle: "text-sm", title: "text-base" },
|
|
191
|
+
md: { subtitle: "text-sm", title: "text-sm" },
|
|
192
|
+
sm: { subtitle: "text-xs", title: "text-sm" },
|
|
193
|
+
} as const;
|
|
194
|
+
|
|
195
|
+
/** An avatar paired with a title and subtitle. */
|
|
196
|
+
function AvatarLabelGroup({
|
|
197
|
+
className,
|
|
198
|
+
size = "md",
|
|
199
|
+
src,
|
|
200
|
+
alt,
|
|
201
|
+
initials,
|
|
202
|
+
status,
|
|
203
|
+
verified,
|
|
204
|
+
title,
|
|
205
|
+
subtitle,
|
|
206
|
+
...props
|
|
207
|
+
}: React.ComponentProps<"div"> & {
|
|
208
|
+
size?: "sm" | "md" | "lg";
|
|
209
|
+
src?: string;
|
|
210
|
+
alt?: string;
|
|
211
|
+
initials?: string;
|
|
212
|
+
status?: "online" | "offline";
|
|
213
|
+
verified?: boolean;
|
|
214
|
+
title: React.ReactNode;
|
|
215
|
+
subtitle?: React.ReactNode;
|
|
216
|
+
}) {
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
className={cn("flex items-center gap-3", className)}
|
|
220
|
+
data-slot="avatar-label-group"
|
|
221
|
+
{...props}
|
|
222
|
+
>
|
|
223
|
+
<Avatar
|
|
224
|
+
alt={alt}
|
|
225
|
+
initials={initials}
|
|
226
|
+
size={size}
|
|
227
|
+
src={src}
|
|
228
|
+
status={status}
|
|
229
|
+
verified={verified}
|
|
230
|
+
/>
|
|
231
|
+
<div className="flex min-w-0 flex-col">
|
|
232
|
+
<span
|
|
233
|
+
className={cn(
|
|
234
|
+
"truncate font-medium text-foreground leading-tight",
|
|
235
|
+
labelGroupText[size].title,
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
238
|
+
{title}
|
|
239
|
+
</span>
|
|
240
|
+
{subtitle ? (
|
|
241
|
+
<span
|
|
242
|
+
className={cn(
|
|
243
|
+
"truncate text-muted-foreground leading-tight",
|
|
244
|
+
labelGroupText[size].subtitle,
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
{subtitle}
|
|
248
|
+
</span>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const profilePhotoSizes = { lg: "size-20", md: "size-16", sm: "size-14" } as const;
|
|
256
|
+
|
|
257
|
+
/** A larger, framed profile photo with optional verified tick. */
|
|
258
|
+
function AvatarProfilePhoto({
|
|
259
|
+
className,
|
|
260
|
+
size = "md",
|
|
261
|
+
...props
|
|
262
|
+
}: Omit<AvatarProps, "size"> & { size?: "sm" | "md" | "lg" }) {
|
|
263
|
+
return (
|
|
264
|
+
<Avatar
|
|
265
|
+
className={cn(
|
|
266
|
+
"ring-1 ring-black/8 ring-inset dark:ring-white/12",
|
|
267
|
+
profilePhotoSizes[size],
|
|
268
|
+
className,
|
|
269
|
+
)}
|
|
270
|
+
data-slot="avatar-profile-photo"
|
|
271
|
+
{...props}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export {
|
|
277
|
+
Avatar,
|
|
278
|
+
AvatarImage,
|
|
279
|
+
AvatarFallback,
|
|
280
|
+
AvatarCompanyIcon,
|
|
281
|
+
AvatarAddButton,
|
|
282
|
+
AvatarLabelGroup,
|
|
283
|
+
AvatarProfilePhoto,
|
|
284
|
+
avatarVariants,
|
|
285
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
4
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
7
|
+
import type * as React from "react";
|
|
8
|
+
|
|
9
|
+
import { cn } from "../../lib/utils/css";
|
|
10
|
+
|
|
11
|
+
const badgeGroupVariants = cva(
|
|
12
|
+
"relative inline-flex max-w-full shrink-0 items-center gap-2 whitespace-nowrap border border-transparent font-medium text-sm outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background sm:text-xs [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11",
|
|
13
|
+
{
|
|
14
|
+
compoundVariants: [
|
|
15
|
+
{
|
|
16
|
+
className: "bg-secondary text-secondary-foreground",
|
|
17
|
+
color: "gray",
|
|
18
|
+
variant: "pill",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
className: "bg-brand/12 text-sidebar-primary dark:bg-brand/20",
|
|
22
|
+
color: "brand",
|
|
23
|
+
variant: "pill",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
className:
|
|
27
|
+
"bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
|
|
28
|
+
color: "error",
|
|
29
|
+
variant: "pill",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
className: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
|
|
33
|
+
color: "warning",
|
|
34
|
+
variant: "pill",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
className: "bg-success/8 text-success-foreground dark:bg-success/16",
|
|
38
|
+
color: "success",
|
|
39
|
+
variant: "pill",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
defaultVariants: {
|
|
43
|
+
align: "leading",
|
|
44
|
+
color: "gray",
|
|
45
|
+
variant: "pill",
|
|
46
|
+
},
|
|
47
|
+
variants: {
|
|
48
|
+
align: {
|
|
49
|
+
leading: "p-1 pe-2.5",
|
|
50
|
+
trailing: "p-1 ps-3",
|
|
51
|
+
},
|
|
52
|
+
color: {
|
|
53
|
+
brand: "",
|
|
54
|
+
error: "",
|
|
55
|
+
gray: "",
|
|
56
|
+
success: "",
|
|
57
|
+
warning: "",
|
|
58
|
+
},
|
|
59
|
+
variant: {
|
|
60
|
+
modern:
|
|
61
|
+
"rounded-[10px] border-input bg-background text-foreground shadow-xs dark:bg-input/32",
|
|
62
|
+
pill: "rounded-full",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
type BadgeGroupColor = NonNullable<
|
|
69
|
+
VariantProps<typeof badgeGroupVariants>["color"]
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
/** Ring tint of the inner pill badge, per color. */
|
|
73
|
+
const pillBadgeRing: Record<BadgeGroupColor, string> = {
|
|
74
|
+
brand: "ring-brand/48",
|
|
75
|
+
error: "ring-destructive/32",
|
|
76
|
+
gray: "ring-border",
|
|
77
|
+
success: "ring-success/32",
|
|
78
|
+
warning: "ring-warning/40",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Status dot of the inner modern badge, per color. */
|
|
82
|
+
const modernBadgeDot: Record<BadgeGroupColor, string> = {
|
|
83
|
+
brand: "bg-brand",
|
|
84
|
+
error: "bg-destructive",
|
|
85
|
+
gray: "bg-muted-foreground",
|
|
86
|
+
success: "bg-success",
|
|
87
|
+
warning: "bg-warning",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
interface BadgeGroupProps
|
|
91
|
+
extends Omit<useRender.ComponentProps<"span">, "color"> {
|
|
92
|
+
/** `pill` is a tonal, fully-rounded chip; `modern` is a bordered card-style chip with a status dot. */
|
|
93
|
+
variant?: VariantProps<typeof badgeGroupVariants>["variant"];
|
|
94
|
+
color?: BadgeGroupColor;
|
|
95
|
+
/** Which side of the message the inner badge sits on. */
|
|
96
|
+
align?: VariantProps<typeof badgeGroupVariants>["align"];
|
|
97
|
+
/** Short label rendered inside the inner badge. */
|
|
98
|
+
badge: React.ReactNode;
|
|
99
|
+
/** Arrow shown after the message (leading) or inside the badge (trailing). Pass `null` to hide. */
|
|
100
|
+
icon?: React.ReactNode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function BadgeGroup({
|
|
104
|
+
className,
|
|
105
|
+
variant = "pill",
|
|
106
|
+
color = "gray",
|
|
107
|
+
align = "leading",
|
|
108
|
+
badge,
|
|
109
|
+
icon,
|
|
110
|
+
children,
|
|
111
|
+
render,
|
|
112
|
+
...props
|
|
113
|
+
}: BadgeGroupProps) {
|
|
114
|
+
const arrow =
|
|
115
|
+
icon === null ? null : (icon ?? <ArrowRightIcon className="rtl:-scale-x-100" />);
|
|
116
|
+
|
|
117
|
+
const innerBadge = (
|
|
118
|
+
<span
|
|
119
|
+
className={cn(
|
|
120
|
+
"inline-flex shrink-0 items-center gap-1.5 bg-background px-2.5 py-0.5",
|
|
121
|
+
variant === "modern"
|
|
122
|
+
? "rounded-md border border-input shadow-xs dark:bg-input/32"
|
|
123
|
+
: cn("rounded-full ring-1 ring-inset", pillBadgeRing[color]),
|
|
124
|
+
align === "trailing" && "pe-2",
|
|
125
|
+
)}
|
|
126
|
+
data-slot="badge-group-badge"
|
|
127
|
+
>
|
|
128
|
+
{variant === "modern" ? (
|
|
129
|
+
<span
|
|
130
|
+
className={cn("size-1.5 rounded-full", modernBadgeDot[color])}
|
|
131
|
+
data-slot="badge-group-dot"
|
|
132
|
+
/>
|
|
133
|
+
) : null}
|
|
134
|
+
{badge}
|
|
135
|
+
{align === "trailing" ? arrow : null}
|
|
136
|
+
</span>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const defaultProps = {
|
|
140
|
+
className: cn(badgeGroupVariants({ align, className, color, variant })),
|
|
141
|
+
"data-slot": "badge-group",
|
|
142
|
+
children: (
|
|
143
|
+
<>
|
|
144
|
+
{align === "leading" ? innerBadge : null}
|
|
145
|
+
<span className="truncate" data-slot="badge-group-text">
|
|
146
|
+
{children}
|
|
147
|
+
</span>
|
|
148
|
+
{align === "leading" ? arrow : innerBadge}
|
|
149
|
+
</>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return useRender({
|
|
154
|
+
defaultTagName: "span",
|
|
155
|
+
props: mergeProps<"span">(defaultProps, props),
|
|
156
|
+
render,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { BadgeGroup, badgeGroupVariants };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
4
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { XIcon } from "lucide-react";
|
|
7
|
+
import type * as React from "react";
|
|
8
|
+
|
|
9
|
+
import { cn } from "../../lib/utils/css";
|
|
10
|
+
|
|
11
|
+
const badgeVariants = cva(
|
|
12
|
+
"relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11",
|
|
13
|
+
{
|
|
14
|
+
compoundVariants: [
|
|
15
|
+
{ className: "rounded-sm", shape: "default", size: ["default", "lg"] },
|
|
16
|
+
{
|
|
17
|
+
className: "rounded-[calc(var(--radius-sm)-2px)]",
|
|
18
|
+
shape: "default",
|
|
19
|
+
size: "sm",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
shape: "default",
|
|
24
|
+
size: "default",
|
|
25
|
+
variant: "default",
|
|
26
|
+
},
|
|
27
|
+
variants: {
|
|
28
|
+
shape: {
|
|
29
|
+
default: "",
|
|
30
|
+
pill: "rounded-full",
|
|
31
|
+
},
|
|
32
|
+
size: {
|
|
33
|
+
default:
|
|
34
|
+
"h-5.5 min-w-5.5 px-[calc(--spacing(2)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs",
|
|
35
|
+
lg: "h-6.5 min-w-6.5 px-[calc(--spacing(2.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm",
|
|
36
|
+
sm: "h-5 min-w-5 px-[calc(--spacing(1.5)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
|
|
37
|
+
},
|
|
38
|
+
variant: {
|
|
39
|
+
default:
|
|
40
|
+
"bg-primary text-primary-foreground [button,a&]:hover:bg-primary/90",
|
|
41
|
+
destructive:
|
|
42
|
+
"bg-destructive text-white [button,a&]:hover:bg-destructive/90",
|
|
43
|
+
error:
|
|
44
|
+
"bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
|
|
45
|
+
info: "bg-info/8 text-info-foreground dark:bg-info/16",
|
|
46
|
+
modern:
|
|
47
|
+
"border-input bg-background text-foreground shadow-xs dark:bg-input/32 [button,a&]:hover:bg-accent/50 dark:[button,a&]:hover:bg-input/48",
|
|
48
|
+
outline:
|
|
49
|
+
"border-input bg-background text-foreground dark:bg-input/32 [button,a&]:hover:bg-accent/50 dark:[button,a&]:hover:bg-input/48",
|
|
50
|
+
secondary:
|
|
51
|
+
"bg-secondary text-secondary-foreground [button,a&]:hover:bg-secondary/90",
|
|
52
|
+
success: "bg-success/8 text-success-foreground dark:bg-success/16",
|
|
53
|
+
warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
interface BadgeProps extends useRender.ComponentProps<"span"> {
|
|
60
|
+
variant?: VariantProps<typeof badgeVariants>["variant"];
|
|
61
|
+
size?: VariantProps<typeof badgeVariants>["size"];
|
|
62
|
+
shape?: VariantProps<typeof badgeVariants>["shape"];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Badge({
|
|
66
|
+
className,
|
|
67
|
+
variant,
|
|
68
|
+
size,
|
|
69
|
+
shape,
|
|
70
|
+
render,
|
|
71
|
+
...props
|
|
72
|
+
}: BadgeProps) {
|
|
73
|
+
const defaultProps = {
|
|
74
|
+
className: cn(badgeVariants({ className, shape, size, variant })),
|
|
75
|
+
"data-slot": "badge",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return useRender({
|
|
79
|
+
defaultTagName: "span",
|
|
80
|
+
props: mergeProps<"span">(defaultProps, props),
|
|
81
|
+
render,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A small status dot. Inherits the badge's text color; recolor via className. */
|
|
86
|
+
function BadgeDot({ className, ...props }: React.ComponentProps<"span">) {
|
|
87
|
+
return (
|
|
88
|
+
<span
|
|
89
|
+
aria-hidden="true"
|
|
90
|
+
className={cn(
|
|
91
|
+
"size-1.5 shrink-0 rounded-full bg-current sm:size-[5px]",
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
data-slot="badge-dot"
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A tiny round avatar image sized to sit inside a badge. */
|
|
101
|
+
function BadgeAvatar({ alt, className, ...props }: React.ComponentProps<"img">) {
|
|
102
|
+
return (
|
|
103
|
+
// biome-ignore lint/a11y/useAltText: alt forwarded via props
|
|
104
|
+
<img
|
|
105
|
+
alt={alt}
|
|
106
|
+
className={cn(
|
|
107
|
+
"size-4 shrink-0 rounded-full object-cover sm:size-3.5",
|
|
108
|
+
className,
|
|
109
|
+
)}
|
|
110
|
+
data-slot="badge-avatar"
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A round country flag. `country` is an ISO 3166-1 alpha-2 code (e.g. "AU"). */
|
|
117
|
+
function BadgeFlag({
|
|
118
|
+
alt,
|
|
119
|
+
className,
|
|
120
|
+
country,
|
|
121
|
+
src,
|
|
122
|
+
...props
|
|
123
|
+
}: React.ComponentProps<"img"> & { country?: string }) {
|
|
124
|
+
return (
|
|
125
|
+
// biome-ignore lint/a11y/useAltText: alt forwarded via props
|
|
126
|
+
<img
|
|
127
|
+
alt={alt ?? country}
|
|
128
|
+
className={cn(
|
|
129
|
+
"size-4 shrink-0 rounded-full object-cover sm:size-3.5",
|
|
130
|
+
className,
|
|
131
|
+
)}
|
|
132
|
+
data-slot="badge-flag"
|
|
133
|
+
src={
|
|
134
|
+
src ??
|
|
135
|
+
(country
|
|
136
|
+
? `https://hatscripts.github.io/circle-flags/flags/${country.toLowerCase()}.svg`
|
|
137
|
+
: undefined)
|
|
138
|
+
}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A small remove button for dismissible badges. Renders an X by default. */
|
|
145
|
+
function BadgeCloseButton({
|
|
146
|
+
children,
|
|
147
|
+
className,
|
|
148
|
+
...props
|
|
149
|
+
}: React.ComponentProps<"button">) {
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
className={cn(
|
|
153
|
+
"-me-px inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full text-current opacity-64 outline-none transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring",
|
|
154
|
+
className,
|
|
155
|
+
)}
|
|
156
|
+
data-slot="badge-close-button"
|
|
157
|
+
type="button"
|
|
158
|
+
{...props}
|
|
159
|
+
>
|
|
160
|
+
{children ?? <XIcon className="pointer-events-none" />}
|
|
161
|
+
</button>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export {
|
|
166
|
+
Badge,
|
|
167
|
+
BadgeDot,
|
|
168
|
+
BadgeAvatar,
|
|
169
|
+
BadgeFlag,
|
|
170
|
+
BadgeCloseButton,
|
|
171
|
+
badgeVariants,
|
|
172
|
+
};
|