feat: add animated button component
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { Button } from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Button",
|
||||
component: Button,
|
||||
args: {
|
||||
children: "Save changes",
|
||||
size: "md",
|
||||
variant: "primary"
|
||||
},
|
||||
argTypes: {
|
||||
asChild: {
|
||||
control: "boolean",
|
||||
description: "Render through the child element using Radix Slot."
|
||||
},
|
||||
children: {
|
||||
control: "text",
|
||||
description: "Primary button label."
|
||||
},
|
||||
className: {
|
||||
control: false
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Disables interaction and sets `data-disabled`."
|
||||
},
|
||||
loading: {
|
||||
control: "boolean",
|
||||
description: "Shows a spinner, disables interaction, and sets `data-loading`."
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["sm", "md", "lg", "icon"],
|
||||
description: "Controls density and spacing."
|
||||
},
|
||||
type: {
|
||||
control: "radio",
|
||||
options: ["button", "submit", "reset"],
|
||||
description: "Native button type when not using `asChild`."
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["primary", "secondary", "ghost", "subtle", "destructive"],
|
||||
description: "Semantic appearance style."
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The first production-style component in the system. It demonstrates the Phase 3 pattern: semantic variants, token-driven styling, motion recipes, loading state, stable `data-slot` / `data-*` hooks, and early integration of the `motion` React runtime for refined hover and press animation."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
export const Variants: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-3 sm:grid-cols-2">
|
||||
<Button variant="primary">Primary action</Button>
|
||||
<Button variant="secondary">Secondary action</Button>
|
||||
<Button variant="subtle">Subtle action</Button>
|
||||
<Button variant="ghost">Ghost action</Button>
|
||||
<Button variant="destructive">Delete item</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="md">Medium</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button aria-label="Favorite" size="icon">
|
||||
☆
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const States: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-3 sm:grid-cols-2">
|
||||
<Button>Default</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
<Button loading>Saving</Button>
|
||||
<Button variant="destructive" loading>
|
||||
Deleting
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Motion: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Hover and press these buttons directly in the canvas. Primary and subtle variants now use `motion/react` for a restrained lift-and-settle interaction, while loading keeps the label and spinner transitions smooth."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-3 sm:grid-cols-2">
|
||||
<Button>Premium primary</Button>
|
||||
<Button variant="subtle">Subtle surface</Button>
|
||||
<Button variant="secondary">Secondary action</Button>
|
||||
<Button loading>Saving changes</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const AsLink: Story = {
|
||||
render: () => (
|
||||
<Button asChild variant="ghost">
|
||||
<a href="https://example.com">Read release notes</a>
|
||||
</Button>
|
||||
)
|
||||
};
|
||||
|
||||
export const Anatomy: Story = {
|
||||
render: () => (
|
||||
<div className="w-[680px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Button anatomy
|
||||
</p>
|
||||
<Button loading variant="secondary">
|
||||
Save draft
|
||||
</Button>
|
||||
<div className="grid gap-3 text-sm text-[var(--color-muted-foreground)]">
|
||||
<p>
|
||||
<code className="text-[var(--color-foreground)]">data-slot="root"</code> on the
|
||||
interactive surface.
|
||||
</p>
|
||||
<p>
|
||||
<code className="text-[var(--color-foreground)]">data-slot="icon"</code> on the
|
||||
loading spinner when present.
|
||||
</p>
|
||||
<p>
|
||||
<code className="text-[var(--color-foreground)]">data-slot="label"</code> on the
|
||||
visible button text.
|
||||
</p>
|
||||
<p>
|
||||
<code className="text-[var(--color-foreground)]">data-loading</code>,{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-disabled</code>,{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-size</code>, and{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-variant</code> drive stateful
|
||||
styling.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
Reference in New Issue
Block a user