@voila.dev/ui 1.1.9
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/package.json +78 -0
- package/src/components/ui/accordion.tsx +79 -0
- package/src/components/ui/alert-dialog.tsx +198 -0
- package/src/components/ui/alert.tsx +105 -0
- package/src/components/ui/aspect-ratio.tsx +30 -0
- package/src/components/ui/avatar.tsx +125 -0
- package/src/components/ui/badge-variants.ts +166 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/banner.tsx +84 -0
- package/src/components/ui/breadcrumb.tsx +127 -0
- package/src/components/ui/button-group.tsx +99 -0
- package/src/components/ui/button-variants.ts +108 -0
- package/src/components/ui/button.tsx +54 -0
- package/src/components/ui/calendar.tsx +266 -0
- package/src/components/ui/card.tsx +155 -0
- package/src/components/ui/carousel.tsx +319 -0
- package/src/components/ui/chat.tsx +896 -0
- package/src/components/ui/checkbox-group.tsx +44 -0
- package/src/components/ui/checkbox.tsx +34 -0
- package/src/components/ui/chip.tsx +60 -0
- package/src/components/ui/collapsible.tsx +44 -0
- package/src/components/ui/color-picker.tsx +191 -0
- package/src/components/ui/combobox.tsx +317 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/confirm-dialog.tsx +125 -0
- package/src/components/ui/context-menu.tsx +254 -0
- package/src/components/ui/copyable-text.tsx +65 -0
- package/src/components/ui/date-picker.tsx +316 -0
- package/src/components/ui/date-time-picker.tsx +1071 -0
- package/src/components/ui/dialog.tsx +176 -0
- package/src/components/ui/direction.tsx +15 -0
- package/src/components/ui/drawer.tsx +178 -0
- package/src/components/ui/dropdown-menu.tsx +247 -0
- package/src/components/ui/empty.tsx +131 -0
- package/src/components/ui/field.tsx +257 -0
- package/src/components/ui/formatted-input.tsx +219 -0
- package/src/components/ui/gallery.tsx +161 -0
- package/src/components/ui/hover-card.tsx +77 -0
- package/src/components/ui/icon-picker.tsx +193 -0
- package/src/components/ui/image-cropper.tsx +787 -0
- package/src/components/ui/image-upload-field.tsx +344 -0
- package/src/components/ui/input-group.tsx +175 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +212 -0
- package/src/components/ui/kbd.tsx +62 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/list.tsx +46 -0
- package/src/components/ui/menu-variants.ts +71 -0
- package/src/components/ui/menubar.tsx +226 -0
- package/src/components/ui/money-input.tsx +96 -0
- package/src/components/ui/native-date-picker.tsx +120 -0
- package/src/components/ui/native-select.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +183 -0
- package/src/components/ui/pagination.tsx +150 -0
- package/src/components/ui/popover.tsx +104 -0
- package/src/components/ui/profile-header.tsx +196 -0
- package/src/components/ui/progress.tsx +71 -0
- package/src/components/ui/radio-group.tsx +115 -0
- package/src/components/ui/rating.tsx +200 -0
- package/src/components/ui/resizable.tsx +61 -0
- package/src/components/ui/responsive-dialog.tsx +278 -0
- package/src/components/ui/responsive-select.tsx +289 -0
- package/src/components/ui/responsive-sheet.tsx +225 -0
- package/src/components/ui/scroll-area.tsx +51 -0
- package/src/components/ui/section.tsx +106 -0
- package/src/components/ui/segmented-control.tsx +148 -0
- package/src/components/ui/select.tsx +216 -0
- package/src/components/ui/separator.tsx +69 -0
- package/src/components/ui/sheet.tsx +154 -0
- package/src/components/ui/shortcut.tsx +41 -0
- package/src/components/ui/sidebar.tsx +739 -0
- package/src/components/ui/skeleton.tsx +23 -0
- package/src/components/ui/slider.tsx +74 -0
- package/src/components/ui/sonner.tsx +64 -0
- package/src/components/ui/spinner.tsx +19 -0
- package/src/components/ui/stat-card.tsx +138 -0
- package/src/components/ui/stepper.tsx +181 -0
- package/src/components/ui/sticky-action-bar.tsx +42 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +132 -0
- package/src/components/ui/tabs.tsx +77 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/time-picker.tsx +221 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +79 -0
- package/src/components/ui/translation-input.tsx +126 -0
- package/src/components/ui/user-avatar.tsx +74 -0
- package/src/css-modules.d.ts +4 -0
- package/src/hooks/use-command-palette.ts +28 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-pagination.ts +81 -0
- package/src/hooks/use-picker-state.ts +44 -0
- package/src/lib/cva.ts +18 -0
- package/src/lib/time-math.ts +62 -0
- package/src/lib/utils.ts +14 -0
- package/src/styles/globals.css +36 -0
- package/src/styles.css +10 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "#/lib/cva.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The single canonical badge cva - base, variants, palette and sizes - shared
|
|
5
|
+
* by `Badge` and `Chip`. Like `button-variants.ts`/`menu-variants.ts`, the
|
|
6
|
+
* recipe lives in a React-free module so it stays importable from non-React
|
|
7
|
+
* surfaces and cross-component without pulling the component in.
|
|
8
|
+
*/
|
|
9
|
+
export const badgeVariants = cva({
|
|
10
|
+
base: "group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden rounded-4xl border border-transparent font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-3",
|
|
11
|
+
variants: {
|
|
12
|
+
// Solid variants only hover when rendered as an anchor ([a]:hover) -
|
|
13
|
+
// static chips should stay inert - while `ghost` and `link` exist for
|
|
14
|
+
// interactive use and hover unconditionally.
|
|
15
|
+
variant: {
|
|
16
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
19
|
+
provider: "bg-provider text-provider-foreground [a]:hover:bg-provider/90",
|
|
20
|
+
organization:
|
|
21
|
+
"bg-organization text-organization-foreground [a]:hover:bg-organization/90",
|
|
22
|
+
destructive:
|
|
23
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
24
|
+
outline:
|
|
25
|
+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
26
|
+
ghost:
|
|
27
|
+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
28
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
29
|
+
},
|
|
30
|
+
// Catalog palette. Declared after `variant` so its bg/text win conflicts
|
|
31
|
+
// via tailwind-merge when both a `variant` and a `color` are passed. Keep
|
|
32
|
+
// in sync with `BadgeColorEnum` in the domain and the `--badge-*` tokens.
|
|
33
|
+
color: {
|
|
34
|
+
slate:
|
|
35
|
+
"bg-badge-slate text-badge-slate-foreground [a]:hover:bg-badge-slate/90",
|
|
36
|
+
gray: "bg-badge-gray text-badge-gray-foreground [a]:hover:bg-badge-gray/90",
|
|
37
|
+
zinc: "bg-badge-zinc text-badge-zinc-foreground [a]:hover:bg-badge-zinc/90",
|
|
38
|
+
red: "bg-badge-red text-badge-red-foreground [a]:hover:bg-badge-red/90",
|
|
39
|
+
orange:
|
|
40
|
+
"bg-badge-orange text-badge-orange-foreground [a]:hover:bg-badge-orange/90",
|
|
41
|
+
amber:
|
|
42
|
+
"bg-badge-amber text-badge-amber-foreground [a]:hover:bg-badge-amber/90",
|
|
43
|
+
yellow:
|
|
44
|
+
"bg-badge-yellow text-badge-yellow-foreground [a]:hover:bg-badge-yellow/90",
|
|
45
|
+
lime: "bg-badge-lime text-badge-lime-foreground [a]:hover:bg-badge-lime/90",
|
|
46
|
+
green:
|
|
47
|
+
"bg-badge-green text-badge-green-foreground [a]:hover:bg-badge-green/90",
|
|
48
|
+
emerald:
|
|
49
|
+
"bg-badge-emerald text-badge-emerald-foreground [a]:hover:bg-badge-emerald/90",
|
|
50
|
+
teal: "bg-badge-teal text-badge-teal-foreground [a]:hover:bg-badge-teal/90",
|
|
51
|
+
cyan: "bg-badge-cyan text-badge-cyan-foreground [a]:hover:bg-badge-cyan/90",
|
|
52
|
+
sky: "bg-badge-sky text-badge-sky-foreground [a]:hover:bg-badge-sky/90",
|
|
53
|
+
blue: "bg-badge-blue text-badge-blue-foreground [a]:hover:bg-badge-blue/90",
|
|
54
|
+
indigo:
|
|
55
|
+
"bg-badge-indigo text-badge-indigo-foreground [a]:hover:bg-badge-indigo/90",
|
|
56
|
+
violet:
|
|
57
|
+
"bg-badge-violet text-badge-violet-foreground [a]:hover:bg-badge-violet/90",
|
|
58
|
+
purple:
|
|
59
|
+
"bg-badge-purple text-badge-purple-foreground [a]:hover:bg-badge-purple/90",
|
|
60
|
+
fuchsia:
|
|
61
|
+
"bg-badge-fuchsia text-badge-fuchsia-foreground [a]:hover:bg-badge-fuchsia/90",
|
|
62
|
+
pink: "bg-badge-pink text-badge-pink-foreground [a]:hover:bg-badge-pink/90",
|
|
63
|
+
rose: "bg-badge-rose text-badge-rose-foreground [a]:hover:bg-badge-rose/90",
|
|
64
|
+
},
|
|
65
|
+
size: {
|
|
66
|
+
default:
|
|
67
|
+
"h-5 gap-1 px-2 py-0.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
|
68
|
+
sm: "h-4 gap-0.5 px-1.5 py-0 text-[10px] has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 [&>svg:not([class*='size-'])]:size-2.5",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export type BadgeVariants = VariantProps<typeof badgeVariants>;
|
|
74
|
+
|
|
75
|
+
/** The catalog palette names - keep in sync with the `color` variant above. */
|
|
76
|
+
export const badgeColors = [
|
|
77
|
+
"slate",
|
|
78
|
+
"gray",
|
|
79
|
+
"zinc",
|
|
80
|
+
"red",
|
|
81
|
+
"orange",
|
|
82
|
+
"amber",
|
|
83
|
+
"yellow",
|
|
84
|
+
"lime",
|
|
85
|
+
"green",
|
|
86
|
+
"emerald",
|
|
87
|
+
"teal",
|
|
88
|
+
"cyan",
|
|
89
|
+
"sky",
|
|
90
|
+
"blue",
|
|
91
|
+
"indigo",
|
|
92
|
+
"violet",
|
|
93
|
+
"purple",
|
|
94
|
+
"fuchsia",
|
|
95
|
+
"pink",
|
|
96
|
+
"rose",
|
|
97
|
+
] as const satisfies readonly NonNullable<BadgeVariants["color"]>[];
|
|
98
|
+
|
|
99
|
+
export type BadgeColor = (typeof badgeColors)[number];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Static lookup for the palette background utilities. Consumers (e.g. the
|
|
103
|
+
* color picker) must use this instead of composing `bg-badge-${name}` at
|
|
104
|
+
* runtime, which would silently break if these literals stopped being
|
|
105
|
+
* scanned by Tailwind.
|
|
106
|
+
*/
|
|
107
|
+
export const badgeColorBackgroundClass: Record<BadgeColor, string> = {
|
|
108
|
+
slate: "bg-badge-slate",
|
|
109
|
+
gray: "bg-badge-gray",
|
|
110
|
+
zinc: "bg-badge-zinc",
|
|
111
|
+
red: "bg-badge-red",
|
|
112
|
+
orange: "bg-badge-orange",
|
|
113
|
+
amber: "bg-badge-amber",
|
|
114
|
+
yellow: "bg-badge-yellow",
|
|
115
|
+
lime: "bg-badge-lime",
|
|
116
|
+
green: "bg-badge-green",
|
|
117
|
+
emerald: "bg-badge-emerald",
|
|
118
|
+
teal: "bg-badge-teal",
|
|
119
|
+
cyan: "bg-badge-cyan",
|
|
120
|
+
sky: "bg-badge-sky",
|
|
121
|
+
blue: "bg-badge-blue",
|
|
122
|
+
indigo: "bg-badge-indigo",
|
|
123
|
+
violet: "bg-badge-violet",
|
|
124
|
+
purple: "bg-badge-purple",
|
|
125
|
+
fuchsia: "bg-badge-fuchsia",
|
|
126
|
+
pink: "bg-badge-pink",
|
|
127
|
+
rose: "bg-badge-rose",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Static lookup for the contrast-aware palette foreground utilities. */
|
|
131
|
+
export const badgeColorForegroundClass: Record<BadgeColor, string> = {
|
|
132
|
+
slate: "text-badge-slate-foreground",
|
|
133
|
+
gray: "text-badge-gray-foreground",
|
|
134
|
+
zinc: "text-badge-zinc-foreground",
|
|
135
|
+
red: "text-badge-red-foreground",
|
|
136
|
+
orange: "text-badge-orange-foreground",
|
|
137
|
+
amber: "text-badge-amber-foreground",
|
|
138
|
+
yellow: "text-badge-yellow-foreground",
|
|
139
|
+
lime: "text-badge-lime-foreground",
|
|
140
|
+
green: "text-badge-green-foreground",
|
|
141
|
+
emerald: "text-badge-emerald-foreground",
|
|
142
|
+
teal: "text-badge-teal-foreground",
|
|
143
|
+
cyan: "text-badge-cyan-foreground",
|
|
144
|
+
sky: "text-badge-sky-foreground",
|
|
145
|
+
blue: "text-badge-blue-foreground",
|
|
146
|
+
indigo: "text-badge-indigo-foreground",
|
|
147
|
+
violet: "text-badge-violet-foreground",
|
|
148
|
+
purple: "text-badge-purple-foreground",
|
|
149
|
+
fuchsia: "text-badge-fuchsia-foreground",
|
|
150
|
+
pink: "text-badge-pink-foreground",
|
|
151
|
+
rose: "text-badge-rose-foreground",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** The semantic variant names - keep in sync with the `variant` axis above. */
|
|
155
|
+
export const badgeVariantOptions = [
|
|
156
|
+
"default",
|
|
157
|
+
"secondary",
|
|
158
|
+
"provider",
|
|
159
|
+
"organization",
|
|
160
|
+
"destructive",
|
|
161
|
+
"outline",
|
|
162
|
+
"ghost",
|
|
163
|
+
"link",
|
|
164
|
+
] as const satisfies readonly NonNullable<BadgeVariants["variant"]>[];
|
|
165
|
+
|
|
166
|
+
export type BadgeVariant = (typeof badgeVariantOptions)[number];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import {
|
|
4
|
+
type BadgeVariants,
|
|
5
|
+
badgeVariants,
|
|
6
|
+
} from "#/components/ui/badge-variants.ts";
|
|
7
|
+
import { cn } from "#/lib/utils.ts";
|
|
8
|
+
|
|
9
|
+
function Badge({
|
|
10
|
+
className,
|
|
11
|
+
variant = "default",
|
|
12
|
+
color,
|
|
13
|
+
size = "default",
|
|
14
|
+
render,
|
|
15
|
+
...props
|
|
16
|
+
}: useRender.ComponentProps<"span"> & BadgeVariants) {
|
|
17
|
+
return useRender({
|
|
18
|
+
defaultTagName: "span",
|
|
19
|
+
props: mergeProps<"span">(
|
|
20
|
+
{
|
|
21
|
+
className: cn(badgeVariants({ variant, color, size }), className),
|
|
22
|
+
},
|
|
23
|
+
props,
|
|
24
|
+
),
|
|
25
|
+
render,
|
|
26
|
+
state: {
|
|
27
|
+
slot: "badge",
|
|
28
|
+
variant,
|
|
29
|
+
color,
|
|
30
|
+
size,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
type BadgeColor,
|
|
37
|
+
type BadgeVariant,
|
|
38
|
+
type BadgeVariants,
|
|
39
|
+
badgeColorBackgroundClass,
|
|
40
|
+
badgeColorForegroundClass,
|
|
41
|
+
badgeColors,
|
|
42
|
+
badgeVariantOptions,
|
|
43
|
+
badgeVariants,
|
|
44
|
+
} from "#/components/ui/badge-variants.ts";
|
|
45
|
+
export { Badge };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { XIcon } from "@phosphor-icons/react";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
import { cva, type VariantProps } from "#/lib/cva.ts";
|
|
4
|
+
|
|
5
|
+
import { cn } from "#/lib/utils.ts";
|
|
6
|
+
|
|
7
|
+
const bannerVariants = cva({
|
|
8
|
+
base: "group/banner relative flex w-full items-center gap-3 px-4 py-2.5 text-sm has-data-[slot=banner-close]:pr-12 md:px-6 [&>svg]:shrink-0 [&>svg:not([class*='size-'])]:size-4",
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground",
|
|
12
|
+
muted: "bg-muted text-foreground",
|
|
13
|
+
success: "bg-success text-success-foreground",
|
|
14
|
+
warning: "bg-warning text-warning-foreground",
|
|
15
|
+
destructive: "bg-destructive text-destructive-foreground",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "default",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Page-level announcement bar spanning the full width of its container
|
|
25
|
+
* (release notes, maintenance windows, billing reminders). Unlike `Alert`,
|
|
26
|
+
* which sits inline within the content flow, a Banner is pinned at the top of
|
|
27
|
+
* a page or layout and uses solid semantic backgrounds.
|
|
28
|
+
*/
|
|
29
|
+
function Banner({
|
|
30
|
+
className,
|
|
31
|
+
variant,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof bannerVariants>) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
data-slot="banner"
|
|
37
|
+
role="status"
|
|
38
|
+
className={cn(bannerVariants({ variant }), className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function BannerTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
data-slot="banner-title"
|
|
48
|
+
className={cn(
|
|
49
|
+
"flex-1 font-medium text-balance [&_a]:underline [&_a]:underline-offset-4",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function BannerAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
data-slot="banner-action"
|
|
61
|
+
className={cn("flex shrink-0 items-center gap-2", className)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function BannerClose({ className, ...props }: React.ComponentProps<"button">) {
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
data-slot="banner-close"
|
|
72
|
+
aria-label="Dismiss"
|
|
73
|
+
className={cn(
|
|
74
|
+
"absolute top-1/2 right-3 inline-flex size-6 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md opacity-70 transition-opacity outline-none hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-current/50 [&>svg]:size-4",
|
|
75
|
+
className,
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
<XIcon aria-hidden />
|
|
80
|
+
</button>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { Banner, BannerAction, BannerClose, BannerTitle, bannerVariants };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { CaretRightIcon, DotsThreeIcon } from "@phosphor-icons/react";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
import { cn } from "#/lib/utils.ts";
|
|
6
|
+
|
|
7
|
+
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
|
8
|
+
return (
|
|
9
|
+
<nav
|
|
10
|
+
aria-label="breadcrumb"
|
|
11
|
+
data-slot="breadcrumb"
|
|
12
|
+
className={className}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
19
|
+
return (
|
|
20
|
+
<ol
|
|
21
|
+
data-slot="breadcrumb-list"
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
32
|
+
return (
|
|
33
|
+
<li
|
|
34
|
+
data-slot="breadcrumb-item"
|
|
35
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function BreadcrumbLink({
|
|
42
|
+
className,
|
|
43
|
+
render,
|
|
44
|
+
...props
|
|
45
|
+
}: useRender.ComponentProps<"a">) {
|
|
46
|
+
return useRender({
|
|
47
|
+
defaultTagName: "a",
|
|
48
|
+
props: mergeProps<"a">(
|
|
49
|
+
{
|
|
50
|
+
className: cn("transition-colors hover:text-foreground", className),
|
|
51
|
+
},
|
|
52
|
+
props,
|
|
53
|
+
),
|
|
54
|
+
render,
|
|
55
|
+
state: {
|
|
56
|
+
slot: "breadcrumb-link",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
62
|
+
return (
|
|
63
|
+
<span
|
|
64
|
+
data-slot="breadcrumb-page"
|
|
65
|
+
aria-current="page"
|
|
66
|
+
className={cn("font-normal text-foreground", className)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function BreadcrumbSeparator({
|
|
73
|
+
children,
|
|
74
|
+
className,
|
|
75
|
+
...props
|
|
76
|
+
}: React.ComponentProps<"li">) {
|
|
77
|
+
return (
|
|
78
|
+
<li
|
|
79
|
+
data-slot="breadcrumb-separator"
|
|
80
|
+
role="presentation"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
className={cn("[&>svg]:size-3.5", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{children ?? <CaretRightIcon />}
|
|
86
|
+
</li>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function BreadcrumbEllipsis({
|
|
91
|
+
className,
|
|
92
|
+
render,
|
|
93
|
+
...props
|
|
94
|
+
}: useRender.ComponentProps<"span">) {
|
|
95
|
+
return useRender({
|
|
96
|
+
defaultTagName: "span",
|
|
97
|
+
props: mergeProps<"span">(
|
|
98
|
+
{
|
|
99
|
+
className: cn(
|
|
100
|
+
"flex size-5 items-center justify-center [&>svg]:size-4",
|
|
101
|
+
className,
|
|
102
|
+
),
|
|
103
|
+
children: (
|
|
104
|
+
<>
|
|
105
|
+
<DotsThreeIcon />
|
|
106
|
+
<span className="sr-only">More</span>
|
|
107
|
+
</>
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
props,
|
|
111
|
+
),
|
|
112
|
+
render,
|
|
113
|
+
state: {
|
|
114
|
+
slot: "breadcrumb-ellipsis",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
Breadcrumb,
|
|
121
|
+
BreadcrumbEllipsis,
|
|
122
|
+
BreadcrumbItem,
|
|
123
|
+
BreadcrumbLink,
|
|
124
|
+
BreadcrumbList,
|
|
125
|
+
BreadcrumbPage,
|
|
126
|
+
BreadcrumbSeparator,
|
|
127
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Separator } from "#/components/ui/separator.tsx";
|
|
5
|
+
import { cva, type VariantProps } from "#/lib/cva.ts";
|
|
6
|
+
import { cn } from "#/lib/utils.ts";
|
|
7
|
+
|
|
8
|
+
const buttonGroupVariants = cva({
|
|
9
|
+
base: "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
10
|
+
variants: {
|
|
11
|
+
orientation: {
|
|
12
|
+
horizontal:
|
|
13
|
+
"*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
|
|
14
|
+
vertical:
|
|
15
|
+
"flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ButtonGroupContext = React.createContext<"horizontal" | "vertical">(
|
|
21
|
+
"horizontal",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Visual-only grouping: label it for screen readers with `aria-label` or
|
|
26
|
+
* `aria-labelledby`. For a single-select segmented control (Day/Week/Month),
|
|
27
|
+
* prefer `ToggleGroup`, which carries the pressed-state semantics.
|
|
28
|
+
*/
|
|
29
|
+
function ButtonGroup({
|
|
30
|
+
className,
|
|
31
|
+
orientation = "horizontal",
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
34
|
+
return (
|
|
35
|
+
<ButtonGroupContext.Provider value={orientation}>
|
|
36
|
+
<div
|
|
37
|
+
role="group"
|
|
38
|
+
data-slot="button-group"
|
|
39
|
+
data-orientation={orientation}
|
|
40
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
</ButtonGroupContext.Provider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ButtonGroupText({
|
|
48
|
+
className,
|
|
49
|
+
render,
|
|
50
|
+
...props
|
|
51
|
+
}: useRender.ComponentProps<"div">) {
|
|
52
|
+
return useRender({
|
|
53
|
+
defaultTagName: "div",
|
|
54
|
+
props: mergeProps<"div">(
|
|
55
|
+
{
|
|
56
|
+
className: cn(
|
|
57
|
+
"flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
58
|
+
className,
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
props,
|
|
62
|
+
),
|
|
63
|
+
render,
|
|
64
|
+
state: {
|
|
65
|
+
slot: "button-group-text",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ButtonGroupSeparator({
|
|
71
|
+
className,
|
|
72
|
+
orientation,
|
|
73
|
+
...props
|
|
74
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
75
|
+
const groupOrientation = React.useContext(ButtonGroupContext);
|
|
76
|
+
return (
|
|
77
|
+
<Separator
|
|
78
|
+
data-slot="button-group-separator"
|
|
79
|
+
// A horizontal group needs a vertical rule between members, and
|
|
80
|
+
// vice versa - derived from the group so consumers never pass it.
|
|
81
|
+
orientation={
|
|
82
|
+
orientation ??
|
|
83
|
+
(groupOrientation === "horizontal" ? "vertical" : "horizontal")
|
|
84
|
+
}
|
|
85
|
+
className={cn(
|
|
86
|
+
"relative self-stretch bg-input data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
ButtonGroup,
|
|
96
|
+
ButtonGroupSeparator,
|
|
97
|
+
ButtonGroupText,
|
|
98
|
+
buttonGroupVariants,
|
|
99
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "#/lib/cva.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The single canonical button cva - base, variants and sizes - shared by the
|
|
5
|
+
* app's React `Button` (Base UI) and the marketing Astro button, so both
|
|
6
|
+
* surfaces render the same button. Built on the project-wide,
|
|
7
|
+
* tailwind-merge-configured `cva` (see `#/lib/cva.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Variant colors map to the design tokens: `default` (primary, black),
|
|
10
|
+
* `provider` (blue), `organization` (orange), plus the neutral/utility roles.
|
|
11
|
+
* `primary` is an alias of `default` for the marketing site, whose buttons
|
|
12
|
+
* default to the `primary` variant name.
|
|
13
|
+
*
|
|
14
|
+
* `active:not-aria-[haspopup]:translate-y-px` gives a tactile press nudge on
|
|
15
|
+
* click, but skips popup triggers (dropdown/menu/select) so they don't jump
|
|
16
|
+
* down as their menu opens.
|
|
17
|
+
*/
|
|
18
|
+
export const buttonVariants = cva({
|
|
19
|
+
base: "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
20
|
+
variants: {
|
|
21
|
+
// Solid variants (default/primary/provider/organization) only hover when
|
|
22
|
+
// rendered as an anchor ([a]:hover) - a plain <button> is usually the
|
|
23
|
+
// page's primary action and gets its feedback from active:translate-y-px
|
|
24
|
+
// instead, so it stays inert under the cursor. The neutral/utility roles
|
|
25
|
+
// (secondary/outline/ghost/destructive/link) hover unconditionally.
|
|
26
|
+
// Mirrors the same split in `badgeVariants`.
|
|
27
|
+
variant: {
|
|
28
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
29
|
+
primary: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
30
|
+
secondary:
|
|
31
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
32
|
+
provider: "bg-provider text-provider-foreground [a]:hover:bg-provider/90",
|
|
33
|
+
organization:
|
|
34
|
+
"bg-organization text-organization-foreground [a]:hover:bg-organization/90",
|
|
35
|
+
outline:
|
|
36
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
37
|
+
ghost:
|
|
38
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
39
|
+
destructive:
|
|
40
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
41
|
+
link: "border-transparent text-primary underline-offset-4 hover:underline",
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
default:
|
|
45
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
46
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
47
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
48
|
+
lg: "h-11 gap-2 px-6 text-base has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
|
|
49
|
+
icon: "size-8",
|
|
50
|
+
"icon-xs":
|
|
51
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
52
|
+
"icon-sm":
|
|
53
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
54
|
+
"icon-lg": "size-9",
|
|
55
|
+
},
|
|
56
|
+
// Corner shape. `default` keeps the size-driven radius; `pill` forces a
|
|
57
|
+
// fully-rounded button (used by the storefront CTAs). Listed last so its
|
|
58
|
+
// rounded-full wins tailwind-merge over the size radii above.
|
|
59
|
+
shape: {
|
|
60
|
+
default: "",
|
|
61
|
+
pill: "rounded-full",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultVariants: {
|
|
65
|
+
variant: "default",
|
|
66
|
+
size: "default",
|
|
67
|
+
shape: "default",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The semantic variant names - keep in sync with the `variant` axis above.
|
|
75
|
+
* Exported (like `badgeColors`) so stories and form builders can iterate the
|
|
76
|
+
* set instead of hand-copying it.
|
|
77
|
+
*/
|
|
78
|
+
export const buttonVariantOptions = [
|
|
79
|
+
"default",
|
|
80
|
+
"primary",
|
|
81
|
+
"secondary",
|
|
82
|
+
"provider",
|
|
83
|
+
"organization",
|
|
84
|
+
"outline",
|
|
85
|
+
"ghost",
|
|
86
|
+
"destructive",
|
|
87
|
+
"link",
|
|
88
|
+
] as const satisfies readonly NonNullable<ButtonVariants["variant"]>[];
|
|
89
|
+
|
|
90
|
+
export type ButtonVariant = (typeof buttonVariantOptions)[number];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The size names, ordered smallest to largest (text sizes then icon sizes) -
|
|
94
|
+
* keep in sync with the `size` axis above. The scale is xs < sm < default < lg,
|
|
95
|
+
* with `default` (h-8) as the unmarked middle.
|
|
96
|
+
*/
|
|
97
|
+
export const buttonSizeOptions = [
|
|
98
|
+
"xs",
|
|
99
|
+
"sm",
|
|
100
|
+
"default",
|
|
101
|
+
"lg",
|
|
102
|
+
"icon-xs",
|
|
103
|
+
"icon-sm",
|
|
104
|
+
"icon",
|
|
105
|
+
"icon-lg",
|
|
106
|
+
] as const satisfies readonly NonNullable<ButtonVariants["size"]>[];
|
|
107
|
+
|
|
108
|
+
export type ButtonSize = (typeof buttonSizeOptions)[number];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ButtonVariants,
|
|
5
|
+
buttonVariants,
|
|
6
|
+
} from "#/components/ui/button-variants.ts";
|
|
7
|
+
import { Spinner } from "#/components/ui/spinner.tsx";
|
|
8
|
+
import { cn } from "#/lib/utils.ts";
|
|
9
|
+
|
|
10
|
+
type ButtonProps = ButtonPrimitive.Props &
|
|
11
|
+
ButtonVariants & {
|
|
12
|
+
/**
|
|
13
|
+
* Show a leading spinner and mark the button `aria-busy`, disabling
|
|
14
|
+
* interaction while an async action is in flight (e.g. a form submit).
|
|
15
|
+
*/
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function Button({
|
|
20
|
+
className,
|
|
21
|
+
variant = "default",
|
|
22
|
+
size = "default",
|
|
23
|
+
shape = "default",
|
|
24
|
+
loading = false,
|
|
25
|
+
disabled,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: ButtonProps) {
|
|
29
|
+
return (
|
|
30
|
+
<ButtonPrimitive
|
|
31
|
+
data-slot="button"
|
|
32
|
+
data-variant={variant}
|
|
33
|
+
data-size={size}
|
|
34
|
+
data-shape={shape}
|
|
35
|
+
disabled={disabled || loading}
|
|
36
|
+
aria-busy={loading || undefined}
|
|
37
|
+
className={cn(buttonVariants({ variant, size, shape }), className)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{loading ? <Spinner /> : null}
|
|
41
|
+
{children}
|
|
42
|
+
</ButtonPrimitive>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
type ButtonSize,
|
|
48
|
+
type ButtonVariant,
|
|
49
|
+
type ButtonVariants,
|
|
50
|
+
buttonSizeOptions,
|
|
51
|
+
buttonVariantOptions,
|
|
52
|
+
buttonVariants,
|
|
53
|
+
} from "#/components/ui/button-variants.ts";
|
|
54
|
+
export { Button, type ButtonProps };
|