feat: add badge card avatar alert and progress

This commit is contained in:
2026-03-19 17:24:22 +08:00
parent 063179933c
commit cb15b46b0c
23 changed files with 1342 additions and 0 deletions
+2
View File
@@ -18,10 +18,12 @@
},
"dependencies": {
"@ai-ui/tokens": "workspace:*",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
+45
View File
@@ -0,0 +1,45 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Alert, AlertDescription, AlertTitle } from "./alert";
describe("Alert", () => {
it("renders root, icon, title, and description slots", () => {
render(
<Alert
icon={
<svg aria-hidden="true" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" />
</svg>
}
variant="warning"
>
<AlertTitle>Heads up</AlertTitle>
<AlertDescription>Review the rollout settings before publishing.</AlertDescription>
</Alert>
);
const alert = screen.getByRole("alert");
expect(alert).toHaveAttribute("data-slot", "root");
expect(alert).toHaveAttribute("data-variant", "warning");
expect(alert).toHaveAttribute("data-has-icon", "");
expect(alert.querySelector('[data-slot="icon"]')).toBeInTheDocument();
expect(screen.getByText("Heads up")).toHaveAttribute("data-slot", "label");
expect(screen.getByText("Review the rollout settings before publishing.")).toHaveAttribute(
"data-slot",
"description"
);
});
it("keeps the default alert role unless overridden", () => {
render(
<Alert role="status">
<AlertTitle>Synced</AlertTitle>
<AlertDescription>Everything is up to date.</AlertDescription>
</Alert>
);
expect(screen.getByRole("status")).toHaveAttribute("data-variant", "default");
});
});
+77
View File
@@ -0,0 +1,77 @@
import { forwardRef } from "react";
import {
alertDescriptionVariants,
alertIconVariants,
alertTitleVariants,
alertVariants
} from "./alert.variants";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
export type AlertProps = React.ComponentPropsWithoutRef<"div"> &
VariantProps<typeof alertVariants> & {
icon?: React.ReactNode;
};
export const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(
{
children,
className,
icon,
variant = "default",
...props
},
ref
) {
const hasIcon = Boolean(icon);
return (
<div
{...props}
{...createSlot("root")}
{...createDataAttributes({
"has-icon": hasIcon,
variant
})}
className={cn(alertVariants({ hasIcon, variant }), className)}
ref={ref}
role={props.role ?? "alert"}
>
{hasIcon ? <span {...createSlot("icon")} className={alertIconVariants()}>{icon}</span> : null}
{children}
</div>
);
});
export type AlertTitleProps = React.ComponentPropsWithoutRef<"h4">;
export const AlertTitle = forwardRef<HTMLHeadingElement, AlertTitleProps>(function AlertTitle(
{ className, ...props },
ref
) {
return (
<h4
{...props}
{...createSlot("label")}
className={cn(alertTitleVariants(), className)}
ref={ref}
/>
);
});
export type AlertDescriptionProps = React.ComponentPropsWithoutRef<"div">;
export const AlertDescription = forwardRef<HTMLDivElement, AlertDescriptionProps>(
function AlertDescription({ className, ...props }, ref) {
return (
<div
{...props}
{...createSlot("description")}
className={cn(alertDescriptionVariants(), className)}
ref={ref}
/>
);
}
);
@@ -0,0 +1,44 @@
import { cva } from "../lib/cva";
import { getMotionRecipeClassNames } from "../lib/motion";
export const alertVariants = cva(
[
"relative grid gap-x-3 gap-y-1 rounded-[var(--radius-lg)] border p-4 shadow-[var(--shadow-xs)]",
"text-[var(--color-foreground)]",
getMotionRecipeClassNames("transition", "ring")
],
{
variants: {
variant: {
default:
"border-[var(--color-border)] bg-[var(--color-card)]",
success:
"border-[color-mix(in_oklch,var(--color-success)_34%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-success)_10%,var(--color-card))]",
warning:
"border-[color-mix(in_oklch,var(--color-warning)_34%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-warning)_12%,var(--color-card))]",
destructive:
"border-[color-mix(in_oklch,var(--color-destructive)_38%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-destructive)_10%,var(--color-card))]"
},
hasIcon: {
false: "",
true: "grid-cols-[auto_1fr]"
}
},
defaultVariants: {
variant: "default",
hasIcon: false
}
}
);
export const alertIconVariants = cva(
"row-span-2 mt-0.5 inline-flex size-5 items-center justify-center rounded-[var(--radius-full)] text-[var(--color-muted-foreground)]"
);
export const alertTitleVariants = cva(
"text-sm font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]"
);
export const alertDescriptionVariants = cva(
"text-sm leading-6 text-[var(--color-muted-foreground)]"
);
@@ -0,0 +1,36 @@
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
describe("Avatar", () => {
it("renders root slot metadata and fallback content", async () => {
render(
<Avatar shape="rounded" size="lg" tone="accent">
<AvatarFallback delayMs={0}>AC</AvatarFallback>
</Avatar>
);
await waitFor(() => {
expect(screen.getByText("AC")).toHaveAttribute("data-slot", "fallback");
});
const root = screen.getByText("AC").closest('[data-slot="root"]');
expect(root).toHaveAttribute("data-size", "lg");
expect(root).toHaveAttribute("data-shape", "rounded");
expect(root).toHaveAttribute("data-tone", "accent");
});
it("shows a fallback without an image", async () => {
render(
<Avatar>
<AvatarFallback delayMs={0}>JD</AvatarFallback>
</Avatar>
);
await waitFor(() => {
expect(screen.getByText("JD")).toHaveAttribute("data-slot", "fallback");
});
});
});
+68
View File
@@ -0,0 +1,68 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
import {
avatarFallbackVariants,
avatarImageVariants,
avatarVariants
} from "./avatar.variants";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
export type AvatarProps = ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> &
VariantProps<typeof avatarVariants>;
export const Avatar = forwardRef<
ElementRef<typeof AvatarPrimitive.Root>,
AvatarProps
>(function Avatar(
{ className, shape, size, tone, ...props },
ref
) {
return (
<AvatarPrimitive.Root
{...props}
{...createSlot("root")}
{...createDataAttributes({
shape,
size,
tone
})}
className={cn(avatarVariants({ shape, size, tone }), className)}
ref={ref}
/>
);
});
export type AvatarImageProps = ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>;
export const AvatarImage = forwardRef<
ElementRef<typeof AvatarPrimitive.Image>,
AvatarImageProps
>(function AvatarImage({ className, ...props }, ref) {
return (
<AvatarPrimitive.Image
{...props}
{...createSlot("image")}
className={cn(avatarImageVariants(), className)}
ref={ref}
/>
);
});
export type AvatarFallbackProps = ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>;
export const AvatarFallback = forwardRef<
ElementRef<typeof AvatarPrimitive.Fallback>,
AvatarFallbackProps
>(function AvatarFallback({ className, ...props }, ref) {
return (
<AvatarPrimitive.Fallback
{...props}
{...createSlot("fallback")}
className={cn(avatarFallbackVariants(), className)}
ref={ref}
/>
);
});
@@ -0,0 +1,43 @@
import { cva } from "../lib/cva";
import { getMotionRecipeClassNames } from "../lib/motion";
export const avatarVariants = cva(
[
"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden border shadow-[var(--shadow-xs)]",
"bg-[var(--color-card)] text-[var(--color-foreground)]",
getMotionRecipeClassNames("transition", "ring")
],
{
variants: {
size: {
sm: "size-9 text-xs",
md: "size-11 text-sm",
lg: "size-14 text-base",
xl: "size-18 text-lg"
},
shape: {
circle: "rounded-full",
rounded: "rounded-[var(--radius-md)]"
},
tone: {
default: "border-[var(--color-border)] bg-[var(--color-card)]",
subtle: "border-[var(--color-border)] bg-[var(--color-surface)]",
accent:
"border-[color-mix(in_oklch,var(--color-accent)_28%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-accent)_14%,var(--color-card))]"
}
},
defaultVariants: {
size: "md",
shape: "circle",
tone: "default"
}
}
);
export const avatarImageVariants = cva([
"size-full object-cover object-center"
]);
export const avatarFallbackVariants = cva([
"flex size-full items-center justify-center bg-[inherit] text-inherit font-medium uppercase tracking-[0.08em]"
]);
+35
View File
@@ -0,0 +1,35 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Badge } from "./badge";
describe("Badge", () => {
it("renders with root and label slots plus data hooks", () => {
render(
<Badge size="sm" tone="success" variant="solid">
Stable
</Badge>
);
const badge = screen.getByText("Stable").closest('[data-slot="root"]');
expect(badge).toBeInTheDocument();
expect(badge).toHaveAttribute("data-size", "sm");
expect(badge).toHaveAttribute("data-tone", "success");
expect(badge).toHaveAttribute("data-variant", "solid");
expect(screen.getByText("Stable")).toHaveAttribute("data-slot", "label");
});
it("supports asChild rendering", () => {
render(
<Badge asChild tone="primary">
<a href="/release">Release</a>
</Badge>
);
const link = screen.getByRole("link", { name: "Release" });
expect(link).toHaveAttribute("data-slot", "root");
expect(link).toHaveAttribute("data-tone", "primary");
});
});
+42
View File
@@ -0,0 +1,42 @@
import { Slot, Slottable } from "@radix-ui/react-slot";
import { forwardRef } from "react";
import { badgeVariants } from "./badge.variants";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot, type AsChildProp } from "../lib/contracts";
export type BadgeProps = React.ComponentPropsWithoutRef<"span"> &
AsChildProp &
VariantProps<typeof badgeVariants>;
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
{
asChild = false,
children,
className,
size,
tone,
variant,
...props
},
ref
) {
const Component = asChild ? Slot : "span";
return (
<Component
{...props}
{...createSlot("root")}
{...createDataAttributes({
size,
tone,
variant
})}
className={cn(badgeVariants({ size, tone, variant }), className)}
ref={ref}
>
{asChild ? <Slottable>{children}</Slottable> : <span {...createSlot("label")}>{children}</span>}
</Component>
);
});
@@ -0,0 +1,42 @@
import { cva } from "../lib/cva";
import { getMotionRecipeClassNames } from "../lib/motion";
export const badgeVariants = cva(
[
"inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-[var(--radius-full)] border font-medium",
"outline-none select-none",
getMotionRecipeClassNames("transition", "ring")
],
{
variants: {
size: {
sm: "min-h-6 px-2 py-0.5 text-[0.7rem]",
md: "min-h-7 px-2.5 py-1 text-xs"
},
variant: {
subtle:
"border-[var(--color-border)] bg-[var(--color-card)] text-[var(--color-foreground)]",
solid:
"border-transparent bg-[var(--color-foreground)] text-[var(--color-background)]",
outline:
"border-[var(--color-border-strong)] bg-transparent text-[var(--color-foreground)]"
},
tone: {
neutral: "",
primary:
"data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-primary)_14%,var(--color-card))] data-[variant=subtle]:text-[var(--color-primary)] data-[variant=solid]:bg-[var(--color-primary)] data-[variant=solid]:text-[var(--color-primary-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-primary)_38%,var(--color-border-strong))] data-[variant=outline]:text-[var(--color-primary)]",
success:
"data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-success)_14%,var(--color-card))] data-[variant=subtle]:text-[color-mix(in_oklch,var(--color-success)_78%,var(--color-foreground))] data-[variant=solid]:bg-[var(--color-success)] data-[variant=solid]:text-[var(--color-success-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-success)_38%,var(--color-border-strong))] data-[variant=outline]:text-[color-mix(in_oklch,var(--color-success)_72%,var(--color-foreground))]",
warning:
"data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-warning)_18%,var(--color-card))] data-[variant=subtle]:text-[color-mix(in_oklch,var(--color-warning)_70%,var(--color-foreground))] data-[variant=solid]:bg-[var(--color-warning)] data-[variant=solid]:text-[var(--color-warning-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-warning)_40%,var(--color-border-strong))] data-[variant=outline]:text-[color-mix(in_oklch,var(--color-warning)_70%,var(--color-foreground))]",
destructive:
"data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-destructive)_12%,var(--color-card))] data-[variant=subtle]:text-[var(--color-destructive)] data-[variant=solid]:bg-[var(--color-destructive)] data-[variant=solid]:text-[var(--color-destructive-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-destructive)_38%,var(--color-border-strong))] data-[variant=outline]:text-[var(--color-destructive)]"
}
},
defaultVariants: {
size: "md",
tone: "neutral",
variant: "subtle"
}
}
);
+48
View File
@@ -0,0 +1,48 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "./card";
describe("Card", () => {
it("renders root and semantic slots", () => {
render(
<Card tone="accent">
<CardHeader>
<CardTitle>Quarterly release</CardTitle>
<CardDescription>Ready for internal review.</CardDescription>
</CardHeader>
<CardContent>Checklist complete.</CardContent>
<CardFooter>Updated 2h ago</CardFooter>
</Card>
);
expect(screen.getByText("Quarterly release").closest('[data-slot="root"]')).toHaveAttribute(
"data-tone",
"accent"
);
expect(screen.getByText("Quarterly release")).toHaveAttribute("data-slot", "label");
expect(screen.getByText("Ready for internal review.")).toHaveAttribute(
"data-slot",
"description"
);
expect(screen.getByText("Checklist complete.")).toHaveAttribute("data-slot", "content");
expect(screen.getByText("Updated 2h ago")).toHaveAttribute("data-slot", "footer");
});
it("supports interactive state hooks", () => {
render(
<Card data-testid="card" interactive>
<CardContent>Hover capable</CardContent>
</Card>
);
expect(screen.getByTestId("card")).toHaveAttribute("data-interactive", "");
});
});
+119
View File
@@ -0,0 +1,119 @@
import { forwardRef } from "react";
import {
cardContentVariants,
cardDescriptionVariants,
cardFooterVariants,
cardHeaderVariants,
cardTitleVariants,
cardVariants
} from "./card.variants";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
export type CardProps = React.ComponentPropsWithoutRef<"div"> &
VariantProps<typeof cardVariants>;
export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
{
className,
interactive,
tone,
...props
},
ref
) {
return (
<div
{...props}
{...createSlot("root")}
{...createDataAttributes({
interactive,
tone
})}
className={cn(cardVariants({ interactive, tone }), className)}
ref={ref}
/>
);
});
export type CardHeaderProps = React.ComponentPropsWithoutRef<"div">;
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(function CardHeader(
{ className, ...props },
ref
) {
return (
<div
{...props}
{...createSlot("header")}
className={cn(cardHeaderVariants(), className)}
ref={ref}
/>
);
});
export type CardTitleProps = React.ComponentPropsWithoutRef<"h3">;
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(function CardTitle(
{ className, ...props },
ref
) {
return (
<h3
{...props}
{...createSlot("label")}
className={cn(cardTitleVariants(), className)}
ref={ref}
/>
);
}
);
export type CardDescriptionProps = React.ComponentPropsWithoutRef<"p">;
export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
function CardDescription({ className, ...props }, ref) {
return (
<p
{...props}
{...createSlot("description")}
className={cn(cardDescriptionVariants(), className)}
ref={ref}
/>
);
}
);
export type CardContentProps = React.ComponentPropsWithoutRef<"div">;
export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(function CardContent(
{ className, ...props },
ref
) {
return (
<div
{...props}
{...createSlot("content")}
className={cn(cardContentVariants(), className)}
ref={ref}
/>
);
});
export type CardFooterProps = React.ComponentPropsWithoutRef<"div">;
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(function CardFooter(
{ className, ...props },
ref
) {
return (
<div
{...props}
{...createSlot("footer")}
className={cn(cardFooterVariants(), className)}
ref={ref}
/>
);
});
@@ -0,0 +1,43 @@
import { cva } from "../lib/cva";
import { getMotionRecipeClassNames } from "../lib/motion";
export const cardVariants = cva(
[
"rounded-[var(--radius-lg)] border text-[var(--color-card-foreground)]",
"bg-[var(--color-card)] shadow-[var(--shadow-sm)]",
getMotionRecipeClassNames("transition", "ring")
],
{
variants: {
tone: {
default: "border-[var(--color-border)]",
subtle:
"border-[color-mix(in_oklch,var(--color-border)_86%,transparent)] bg-[var(--color-surface)] shadow-[var(--shadow-xs)]",
accent:
"border-[color-mix(in_oklch,var(--color-primary)_26%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-primary)_8%,var(--color-card))]"
},
interactive: {
false: "",
true: "hover:-translate-y-[2px] hover:shadow-[var(--shadow-md)]"
}
},
defaultVariants: {
tone: "default",
interactive: false
}
}
);
export const cardHeaderVariants = cva("grid gap-2 p-6 pb-0");
export const cardTitleVariants = cva(
"text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]"
);
export const cardDescriptionVariants = cva(
"text-sm leading-6 text-[var(--color-muted-foreground)]"
);
export const cardContentVariants = cva("p-6");
export const cardFooterVariants = cva("flex flex-wrap items-center gap-3 p-6 pt-0");
@@ -0,0 +1,40 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Progress } from "./progress";
describe("Progress", () => {
it("renders root and indicator slots for a determinate value", () => {
render(<Progress aria-label="Upload progress" size="lg" value={64} variant="success" />);
const progressbar = screen.getByRole("progressbar", { name: "Upload progress" });
const indicator = progressbar.querySelector('[data-slot="indicator"]');
expect(progressbar).toHaveAttribute("data-slot", "root");
expect(progressbar).toHaveAttribute("data-size", "lg");
expect(progressbar).toHaveAttribute("data-state", "loading");
expect(progressbar).toHaveAttribute("aria-valuenow", "64");
expect(indicator).toHaveAttribute("data-variant", "success");
expect(indicator).toHaveStyle({ width: "64%" });
});
it("supports indeterminate and complete states", () => {
const { rerender } = render(<Progress aria-label="Sync status" value={null} />);
let progressbar = screen.getByRole("progressbar", { name: "Sync status" });
let indicator = progressbar.querySelector('[data-slot="indicator"]');
expect(progressbar).toHaveAttribute("data-state", "indeterminate");
expect(progressbar).not.toHaveAttribute("aria-valuenow");
expect(indicator).toHaveStyle({ width: "38%" });
rerender(<Progress aria-label="Sync status" max={120} value={120} variant="default" />);
progressbar = screen.getByRole("progressbar", { name: "Sync status" });
indicator = progressbar.querySelector('[data-slot="indicator"]');
expect(progressbar).toHaveAttribute("data-state", "complete");
expect(progressbar).toHaveAttribute("aria-valuenow", "120");
expect(indicator).toHaveStyle({ width: "100%" });
});
});
+83
View File
@@ -0,0 +1,83 @@
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
import {
progressIndicatorVariants,
progressVariants
} from "./progress.variants";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
function clampValue(value: number, max: number) {
return Math.min(Math.max(value, 0), max);
}
function getState(value: number | null | undefined, max: number) {
if (value == null) {
return "indeterminate";
}
return clampValue(value, max) >= max ? "complete" : "loading";
}
function getIndicatorWidth(value: number | null | undefined, max: number) {
if (value == null) {
return "38%";
}
return `${(clampValue(value, max) / max) * 100}%`;
}
export type ProgressProps = ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> &
VariantProps<typeof progressVariants> &
VariantProps<typeof progressIndicatorVariants>;
export const Progress = forwardRef<
ElementRef<typeof ProgressPrimitive.Root>,
ProgressProps
>(function Progress(
{
className,
max = 100,
size,
tone,
value,
variant,
...props
},
ref
) {
const resolvedMax = max > 0 ? max : 100;
const state = getState(value, resolvedMax);
return (
<ProgressPrimitive.Root
{...props}
{...createSlot("root")}
{...createDataAttributes({
size,
state,
tone,
variant
})}
className={cn(progressVariants({ size, tone }), className)}
max={resolvedMax}
ref={ref}
value={value ?? undefined}
>
<ProgressPrimitive.Indicator
{...createSlot("indicator")}
{...createDataAttributes({
size,
state,
variant
})}
className={cn(progressIndicatorVariants({ variant }))}
style={{
width: getIndicatorWidth(value, resolvedMax)
}}
/>
</ProgressPrimitive.Root>
);
});
@@ -0,0 +1,46 @@
import { cva } from "../lib/cva";
import { getMotionRecipeClassNames } from "../lib/motion";
export const progressVariants = cva(
[
"relative w-full overflow-hidden rounded-full border",
"border-[color-mix(in_oklch,var(--color-border)_92%,transparent)] bg-[var(--color-surface)]"
],
{
variants: {
size: {
sm: "h-2",
md: "h-3",
lg: "h-4"
},
tone: {
default: "bg-[var(--color-surface)]",
subtle: "bg-[var(--color-muted)]"
}
},
defaultVariants: {
size: "md",
tone: "default"
}
}
);
export const progressIndicatorVariants = cva(
[
"h-full rounded-full transition-[width,background-color] duration-[var(--dur-base)] ease-[var(--ease-emphasized)]",
getMotionRecipeClassNames("transition")
],
{
variants: {
variant: {
default: "bg-[var(--color-primary)]",
success: "bg-[var(--color-success)]",
warning: "bg-[var(--color-warning)]",
destructive: "bg-[var(--color-destructive)]"
}
},
defaultVariants: {
variant: "default"
}
}
);
+59
View File
@@ -1,5 +1,56 @@
export {
Alert,
AlertDescription,
AlertTitle,
type AlertDescriptionProps,
type AlertProps,
type AlertTitleProps
} from "./components/alert";
export {
alertDescriptionVariants,
alertIconVariants,
alertTitleVariants,
alertVariants
} from "./components/alert.variants";
export {
Avatar,
AvatarFallback,
AvatarImage,
type AvatarFallbackProps,
type AvatarImageProps,
type AvatarProps
} from "./components/avatar";
export {
avatarFallbackVariants,
avatarImageVariants,
avatarVariants
} from "./components/avatar.variants";
export { Badge, type BadgeProps } from "./components/badge";
export { badgeVariants } from "./components/badge.variants";
export { Button, type ButtonProps } from "./components/button";
export { buttonVariants } from "./components/button.variants";
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
type CardContentProps,
type CardDescriptionProps,
type CardFooterProps,
type CardHeaderProps,
type CardProps,
type CardTitleProps
} from "./components/card";
export {
cardContentVariants,
cardDescriptionVariants,
cardFooterVariants,
cardHeaderVariants,
cardTitleVariants,
cardVariants
} from "./components/card.variants";
export { Checkbox, type CheckboxProps } from "./components/checkbox";
export { checkboxVariants } from "./components/checkbox.variants";
export {
@@ -79,6 +130,14 @@ export {
type PopoverContentProps
} from "./components/popover";
export { popoverContentVariants } from "./components/popover.variants";
export {
Progress,
type ProgressProps
} from "./components/progress";
export {
progressIndicatorVariants,
progressVariants
} from "./components/progress.variants";
export {
RadioGroup,
RadioGroupItem,