feat: add badge card avatar alert and progress
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
import { Alert, AlertDescription, AlertTitle, Badge } from "@ai-ui/ui";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
function SparkIcon() {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M8 1.75L9.45 5.2L12.9 6.65L9.45 8.1L8 11.55L6.55 8.1L3.1 6.65L6.55 5.2L8 1.75Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Components/Alert",
|
||||||
|
component: Alert,
|
||||||
|
args: {
|
||||||
|
variant: "default"
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
className: {
|
||||||
|
control: false
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
control: false
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["default", "success", "warning", "destructive"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: "centered"
|
||||||
|
},
|
||||||
|
tags: ["autodocs"]
|
||||||
|
} satisfies Meta<typeof Alert>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Playground: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Alert {...args} className="w-[460px]" icon={<SparkIcon />}>
|
||||||
|
<AlertTitle>Release status updated</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
The approval chain changed. Review the new ownership before publishing.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Variants: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid w-[760px] gap-4">
|
||||||
|
<Alert icon={<SparkIcon />}>
|
||||||
|
<AlertTitle>Informational callout</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Pair with a <Badge className="ml-1">live</Badge> badge when status can change in real time.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Alert icon={<SparkIcon />} variant="success">
|
||||||
|
<AlertTitle>Release approved</AlertTitle>
|
||||||
|
<AlertDescription>All reviewers signed off and rollout can begin.</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Alert icon={<SparkIcon />} variant="warning">
|
||||||
|
<AlertTitle>Missing follow-up</AlertTitle>
|
||||||
|
<AlertDescription>One checklist item is still unresolved for this launch.</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Alert icon={<SparkIcon />} variant="destructive">
|
||||||
|
<AlertTitle>Publishing blocked</AlertTitle>
|
||||||
|
<AlertDescription>Resolve validation issues before trying again.</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@ai-ui/ui";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
const avatarSrc =
|
||||||
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop stop-color='%23b34f42'/%3E%3Cstop offset='1' stop-color='%23d8a26e'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='20' fill='url(%23g)'/%3E%3Ccircle cx='32' cy='24' r='10' fill='rgba(255,255,255,0.78)'/%3E%3Cpath d='M16 54c2-10 10-16 16-16s14 6 16 16' fill='rgba(255,255,255,0.78)'/%3E%3C/svg%3E";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Components/Avatar",
|
||||||
|
component: Avatar,
|
||||||
|
args: {
|
||||||
|
shape: "circle",
|
||||||
|
size: "md",
|
||||||
|
tone: "default"
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
className: {
|
||||||
|
control: false
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["circle", "rounded"]
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["sm", "md", "lg", "xl"]
|
||||||
|
},
|
||||||
|
tone: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["default", "subtle", "accent"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: "centered"
|
||||||
|
},
|
||||||
|
tags: ["autodocs"]
|
||||||
|
} satisfies Meta<typeof Avatar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Playground: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Avatar {...args}>
|
||||||
|
<AvatarImage alt="Avery Carter" src={avatarSrc} />
|
||||||
|
<AvatarFallback>AC</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
|
{(["sm", "md", "lg", "xl"] as const).map((size) => (
|
||||||
|
<Avatar key={size} size={size}>
|
||||||
|
<AvatarImage alt={`${size} avatar`} src={avatarSrc} />
|
||||||
|
<AvatarFallback>{size.slice(0, 2)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Fallbacks: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
|
<Avatar tone="subtle">
|
||||||
|
<AvatarFallback delayMs={0}>JD</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<Avatar shape="rounded" size="lg" tone="accent">
|
||||||
|
<AvatarFallback delayMs={0}>MK</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Badge } from "@ai-ui/ui";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Components/Badge",
|
||||||
|
component: Badge,
|
||||||
|
args: {
|
||||||
|
children: "Stable",
|
||||||
|
size: "md",
|
||||||
|
tone: "neutral",
|
||||||
|
variant: "subtle"
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
asChild: {
|
||||||
|
control: "boolean"
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
control: "text"
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
control: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["sm", "md"]
|
||||||
|
},
|
||||||
|
tone: {
|
||||||
|
control: "select",
|
||||||
|
options: ["neutral", "primary", "success", "warning", "destructive"]
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["subtle", "solid", "outline"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: "centered"
|
||||||
|
},
|
||||||
|
tags: ["autodocs"]
|
||||||
|
} satisfies Meta<typeof Badge>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Playground: Story = {};
|
||||||
|
|
||||||
|
export const Matrix: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid w-[720px] gap-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Badge>Neutral</Badge>
|
||||||
|
<Badge tone="primary">Primary</Badge>
|
||||||
|
<Badge tone="success">Success</Badge>
|
||||||
|
<Badge tone="warning">Warning</Badge>
|
||||||
|
<Badge tone="destructive">Destructive</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Badge variant="outline">Outline</Badge>
|
||||||
|
<Badge tone="primary" variant="outline">
|
||||||
|
Brand
|
||||||
|
</Badge>
|
||||||
|
<Badge tone="success" variant="solid">
|
||||||
|
Shipped
|
||||||
|
</Badge>
|
||||||
|
<Badge size="sm" tone="warning" variant="solid">
|
||||||
|
Needs review
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ai-ui/ui";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Components/Card",
|
||||||
|
component: Card,
|
||||||
|
args: {
|
||||||
|
tone: "default",
|
||||||
|
interactive: false
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
className: {
|
||||||
|
control: false
|
||||||
|
},
|
||||||
|
interactive: {
|
||||||
|
control: "boolean"
|
||||||
|
},
|
||||||
|
tone: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["default", "subtle", "accent"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: "centered"
|
||||||
|
},
|
||||||
|
tags: ["autodocs"]
|
||||||
|
} satisfies Meta<typeof Card>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Playground: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Card {...args} className="w-[420px]">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Release card</CardTitle>
|
||||||
|
<CardDescription>Summarize state, ownership, and next action.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
This surface is tuned for editorial dashboards and settings views.
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button size="sm">Open</Button>
|
||||||
|
<Button size="sm" variant="ghost">
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Grid: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid w-[760px] gap-4 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Default tone</CardTitle>
|
||||||
|
<CardDescription>Standard elevated panel for data and form sections.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>Reliable baseline for most admin surfaces.</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card interactive tone="accent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Interactive accent</CardTitle>
|
||||||
|
<CardDescription>Hover-capable treatment for navigable cards.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>Use sparingly for overview screens with clear primary actions.</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { Progress } from "@ai-ui/ui";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Components/Progress",
|
||||||
|
component: Progress,
|
||||||
|
args: {
|
||||||
|
size: "md",
|
||||||
|
value: 64,
|
||||||
|
variant: "default"
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
className: {
|
||||||
|
control: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["sm", "md", "lg"]
|
||||||
|
},
|
||||||
|
tone: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["default", "subtle"]
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: {
|
||||||
|
type: "range",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["default", "success", "warning", "destructive"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: "centered"
|
||||||
|
},
|
||||||
|
tags: ["autodocs"]
|
||||||
|
} satisfies Meta<typeof Progress>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Playground: Story = {
|
||||||
|
render: (args) => <Progress aria-label="Upload progress" className="w-[320px]" {...args} />
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Variants: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid w-[360px] gap-4">
|
||||||
|
<Progress aria-label="Primary progress" value={42} variant="default" />
|
||||||
|
<Progress aria-label="Success progress" value={74} variant="success" />
|
||||||
|
<Progress aria-label="Warning progress" value={58} variant="warning" />
|
||||||
|
<Progress aria-label="Destructive progress" value={86} variant="destructive" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const States: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid w-[360px] gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">Determinate</p>
|
||||||
|
<Progress aria-label="Determinate progress" value={68} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">Indeterminate</p>
|
||||||
|
<Progress aria-label="Indeterminate progress" value={null} variant="success" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">Complete</p>
|
||||||
|
<Progress aria-label="Complete progress" value={100} variant="success" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -18,10 +18,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-ui/tokens": "workspace:*",
|
"@ai-ui/tokens": "workspace:*",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@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-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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]"
|
||||||
|
]);
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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", "");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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%" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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 { Button, type ButtonProps } from "./components/button";
|
||||||
export { buttonVariants } from "./components/button.variants";
|
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 { Checkbox, type CheckboxProps } from "./components/checkbox";
|
||||||
export { checkboxVariants } from "./components/checkbox.variants";
|
export { checkboxVariants } from "./components/checkbox.variants";
|
||||||
export {
|
export {
|
||||||
@@ -79,6 +130,14 @@ export {
|
|||||||
type PopoverContentProps
|
type PopoverContentProps
|
||||||
} from "./components/popover";
|
} from "./components/popover";
|
||||||
export { popoverContentVariants } from "./components/popover.variants";
|
export { popoverContentVariants } from "./components/popover.variants";
|
||||||
|
export {
|
||||||
|
Progress,
|
||||||
|
type ProgressProps
|
||||||
|
} from "./components/progress";
|
||||||
|
export {
|
||||||
|
progressIndicatorVariants,
|
||||||
|
progressVariants
|
||||||
|
} from "./components/progress.variants";
|
||||||
export {
|
export {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
|
|||||||
Generated
+95
@@ -106,6 +106,9 @@ importers:
|
|||||||
'@ai-ui/tokens':
|
'@ai-ui/tokens':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../tokens
|
version: link:../tokens
|
||||||
|
'@radix-ui/react-avatar':
|
||||||
|
specifier: ^1.1.11
|
||||||
|
version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.3
|
specifier: ^1.3.3
|
||||||
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -118,6 +121,9 @@ importers:
|
|||||||
'@radix-ui/react-popover':
|
'@radix-ui/react-popover':
|
||||||
specifier: ^1.1.15
|
specifier: ^1.1.15
|
||||||
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-progress':
|
||||||
|
specifier: ^1.1.8
|
||||||
|
version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-radio-group':
|
'@radix-ui/react-radio-group':
|
||||||
specifier: ^1.3.8
|
specifier: ^1.3.8
|
||||||
version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -752,6 +758,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-avatar@1.1.11':
|
||||||
|
resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-checkbox@1.3.3':
|
'@radix-ui/react-checkbox@1.3.3':
|
||||||
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -796,6 +815,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-context@1.1.3':
|
||||||
|
resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.15':
|
'@radix-ui/react-dialog@1.1.15':
|
||||||
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -966,6 +994,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-progress@1.1.8':
|
||||||
|
resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-radio-group@1.3.8':
|
'@radix-ui/react-radio-group@1.3.8':
|
||||||
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
|
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1124,6 +1165,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-is-hydrated@0.1.0':
|
||||||
|
resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.1':
|
'@radix-ui/react-use-layout-effect@1.1.1':
|
||||||
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3107,6 +3157,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
util@0.12.5:
|
util@0.12.5:
|
||||||
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
|
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
|
||||||
|
|
||||||
@@ -3712,6 +3767,19 @@ snapshots:
|
|||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.28
|
||||||
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||||
|
|
||||||
|
'@radix-ui/react-avatar@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-context': 1.1.3(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.28
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||||
|
|
||||||
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3752,6 +3820,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.28
|
||||||
|
|
||||||
|
'@radix-ui/react-context@1.1.3(@types/react@18.3.28)(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.28
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3937,6 +4011,16 @@ snapshots:
|
|||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.28
|
||||||
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||||
|
|
||||||
|
'@radix-ui/react-progress@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-context': 1.1.3(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.28
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||||
|
|
||||||
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -4123,6 +4207,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.28
|
||||||
|
|
||||||
|
'@radix-ui/react-use-is-hydrated@0.1.0(@types/react@18.3.28)(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.28
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1)':
|
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
@@ -6103,6 +6194,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.28
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
util@0.12.5:
|
util@0.12.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
|
|||||||
Reference in New Issue
Block a user