design system token v0.1

This commit is contained in:
Juan
2026-04-13 15:33:00 -05:00
parent 52b4156653
commit c3215945f2
63 changed files with 11562 additions and 181 deletions

View File

@@ -0,0 +1,111 @@
import type { Meta, StoryObj } from '@storybook/react'
import { CTASection } from '@/components/ui/cta-section'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Composition/CTASection',
component: CTASection,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
argTypes: {
variant: {
control: 'select',
options: ['centered', 'left-aligned'],
},
background: {
control: 'select',
options: ['default', 'muted', 'accent', 'subtle'],
},
},
} satisfies Meta<typeof CTASection>
export default meta
type Story = StoryObj<typeof meta>
const defaultActions = (
<>
<Button size="lg">Get Started</Button>
<Button size="lg" variant="outline">Contact Sales</Button>
</>
)
export const Default: Story = {
args: {
heading: 'Ready to get started?',
description: 'Join thousands of teams building better products with our design system.',
actions: defaultActions,
},
}
export const Centered: Story = {
args: {
variant: 'centered',
background: 'muted',
heading: 'Start building today',
description: 'Free for open source. Affordable for teams.',
actions: defaultActions,
},
}
export const LeftAligned: Story = {
args: {
variant: 'left-aligned',
background: 'muted',
heading: 'Need help getting started?',
description: 'Our team is ready to help you integrate the design system into your project.',
actions: (
<>
<Button size="lg">Talk to us</Button>
</>
),
},
}
export const AccentBackground: Story = {
args: {
variant: 'centered',
background: 'accent',
heading: 'Upgrade your workflow',
description: 'Take your team to the next level with our premium plan.',
actions: (
<>
<Button size="lg" variant="secondary">Start Free Trial</Button>
<Button
size="lg"
variant="outline"
className="border-primary-foreground/20 text-primary-foreground hover:bg-primary-foreground/10"
>
Learn More
</Button>
</>
),
},
}
export const SubtleBackground: Story = {
args: {
variant: 'centered',
background: 'subtle',
heading: 'Stay in the loop',
description: 'Subscribe to our newsletter for the latest updates and releases.',
actions: (
<Button size="lg">Subscribe</Button>
),
},
}
export const DefaultBackground: Story = {
args: {
variant: 'centered',
background: 'default',
heading: 'Questions? We have answers.',
description: 'Check out our documentation or reach out to our support team.',
actions: (
<>
<Button size="lg">View Docs</Button>
<Button size="lg" variant="ghost">Contact Support</Button>
</>
),
},
}

View File

@@ -0,0 +1,92 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Footer } from '@/components/ui/footer'
import { Logo } from '@/components/ui/logo'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Composition/Footer',
component: Footer,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
argTypes: {
variant: {
control: 'select',
options: ['minimal', 'full'],
},
},
} satisfies Meta<typeof Footer>
export default meta
type Story = StoryObj<typeof meta>
export const Minimal: Story = {
args: {
variant: 'minimal',
logo: <Logo size="sm" />,
copyright: <>&copy; 2026 Greyhaven. All rights reserved.</>,
actions: (
<div className="flex gap-4 text-sm text-muted-foreground">
<a href="#" className="hover:text-foreground transition-colors">Privacy</a>
<a href="#" className="hover:text-foreground transition-colors">Terms</a>
<a href="#" className="hover:text-foreground transition-colors">Contact</a>
</div>
),
},
}
export const Full: Story = {
args: {
variant: 'full',
logo: <Logo size="md" />,
copyright: <>&copy; 2026 Greyhaven. All rights reserved.</>,
linkGroups: [
{
title: 'Product',
links: [
{ label: 'Features', href: '#' },
{ label: 'Pricing', href: '#' },
{ label: 'Changelog', href: '#' },
{ label: 'Docs', href: '#' },
],
},
{
title: 'Company',
links: [
{ label: 'About', href: '#' },
{ label: 'Blog', href: '#' },
{ label: 'Careers', href: '#' },
{ label: 'Contact', href: '#' },
],
},
{
title: 'Legal',
links: [
{ label: 'Privacy Policy', href: '#' },
{ label: 'Terms of Service', href: '#' },
{ label: 'Cookie Policy', href: '#' },
],
},
],
actions: (
<div className="flex gap-2">
<Button variant="ghost" size="sm">Twitter</Button>
<Button variant="ghost" size="sm">GitHub</Button>
<Button variant="ghost" size="sm">Discord</Button>
</div>
),
},
}
export const MinimalNoCopyright: Story = {
args: {
variant: 'minimal',
logo: <Logo size="sm" />,
actions: (
<div className="flex gap-4 text-sm text-muted-foreground">
<a href="#" className="hover:text-foreground transition-colors">Docs</a>
<a href="#" className="hover:text-foreground transition-colors">GitHub</a>
</div>
),
},
}

View File

@@ -0,0 +1,111 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Hero } from '@/components/ui/hero'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Composition/Hero',
component: Hero,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
argTypes: {
variant: {
control: 'select',
options: ['centered', 'left-aligned', 'split'],
},
background: {
control: 'select',
options: ['default', 'muted', 'accent', 'dark'],
},
},
} satisfies Meta<typeof Hero>
export default meta
type Story = StoryObj<typeof meta>
const defaultActions = (
<>
<Button size="lg">Get Started</Button>
<Button size="lg" variant="outline">Learn More</Button>
</>
)
export const Centered: Story = {
args: {
variant: 'centered',
heading: 'Build better products with Greyhaven',
subheading:
'A modern design system that helps you create consistent, accessible, and beautiful user interfaces.',
actions: defaultActions,
},
}
export const LeftAligned: Story = {
args: {
variant: 'left-aligned',
heading: 'Ship faster with confidence',
subheading:
'Pre-built components, design tokens, and patterns so your team can focus on what matters.',
actions: defaultActions,
},
}
export const Split: Story = {
args: {
variant: 'split',
heading: 'Design meets engineering',
subheading:
'Bridging the gap between design and code with a shared language of components and tokens.',
actions: defaultActions,
media: (
<div className="w-full aspect-video bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
Image / Media Placeholder
</div>
),
},
}
export const MutedBackground: Story = {
args: {
variant: 'centered',
background: 'muted',
heading: 'Welcome to the platform',
subheading: 'Everything you need to build and scale your project.',
actions: defaultActions,
},
}
export const AccentBackground: Story = {
args: {
variant: 'centered',
background: 'accent',
heading: 'Start building today',
subheading: 'Join thousands of developers using our design system.',
actions: defaultActions,
},
}
export const DarkBackground: Story = {
args: {
variant: 'centered',
background: 'dark',
heading: 'The future of design systems',
subheading: 'A bold new approach to building consistent user interfaces at scale.',
actions: (
<>
<Button size="lg" variant="secondary">Get Started</Button>
<Button size="lg" variant="outline" className="border-background/20 text-background hover:bg-background/10">
Learn More
</Button>
</>
),
},
}
export const WithoutActions: Story = {
args: {
variant: 'centered',
heading: 'A hero section without action buttons',
subheading: 'Sometimes you just need a heading and description.',
},
}

View File

@@ -0,0 +1,167 @@
import type { Meta, StoryObj } from '@storybook/react'
import { PageLayout } from '@/components/ui/page-layout'
import { Navbar, NavbarLink } from '@/components/ui/navbar'
import { Footer } from '@/components/ui/footer'
import { Hero } from '@/components/ui/hero'
import { Section } from '@/components/ui/section'
import { CTASection } from '@/components/ui/cta-section'
import { Logo } from '@/components/ui/logo'
import { Button } from '@/components/ui/button'
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '@/components/ui/card'
const meta = {
title: 'Composition/PageLayout',
component: PageLayout,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
} satisfies Meta<typeof PageLayout>
export default meta
type Story = StoryObj<typeof meta>
const navLinks = (
<>
<NavbarLink href="#" active>Home</NavbarLink>
<NavbarLink href="#">Features</NavbarLink>
<NavbarLink href="#">Pricing</NavbarLink>
<NavbarLink href="#">Docs</NavbarLink>
</>
)
const navActions = (
<>
<Button variant="ghost" size="sm">Log in</Button>
<Button size="sm">Sign up</Button>
</>
)
const sampleNavbar = (
<Navbar
variant="solid"
logo={<Logo size="sm" />}
actions={navActions}
>
{navLinks}
</Navbar>
)
const sampleFooter = (
<Footer
variant="minimal"
logo={<Logo size="sm" />}
copyright={<>&copy; 2026 Greyhaven. All rights reserved.</>}
actions={
<div className="flex gap-4 text-sm text-muted-foreground">
<a href="#" className="hover:text-foreground transition-colors">Privacy</a>
<a href="#" className="hover:text-foreground transition-colors">Terms</a>
</div>
}
/>
)
export const FullPage: Story = {
args: {
navbar: sampleNavbar,
footer: sampleFooter,
children: (
<>
<Hero
variant="centered"
heading="Build something great"
subheading="A complete design system for modern web applications."
actions={
<>
<Button size="lg">Get Started</Button>
<Button size="lg" variant="outline">View Docs</Button>
</>
}
/>
<Section
title="Features"
description="Everything you need to build beautiful interfaces."
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{['Components', 'Tokens', 'Patterns'].map((title) => (
<Card key={title}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>
Pre-built {title.toLowerCase()} for rapid development.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Fully customizable {title.toLowerCase()} that follow best practices.
</p>
</CardContent>
</Card>
))}
</div>
</Section>
<CTASection
background="muted"
heading="Ready to start?"
description="Get up and running in minutes."
actions={<Button size="lg">Get Started Free</Button>}
/>
</>
),
},
}
export const WithSidebar: Story = {
args: {
navbar: sampleNavbar,
footer: sampleFooter,
sidebar: (
<nav className="p-4 space-y-2">
<h3 className="font-semibold text-sm mb-4">Navigation</h3>
{['Dashboard', 'Projects', 'Team', 'Settings'].map((item) => (
<a
key={item}
href="#"
className="block px-3 py-2 text-sm rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
{item}
</a>
))}
</nav>
),
children: (
<Section title="Dashboard" description="Overview of your workspace.">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{['Revenue', 'Users', 'Orders', 'Growth'].map((metric) => (
<Card key={metric}>
<CardHeader>
<CardTitle>{metric}</CardTitle>
<CardDescription>Last 30 days</CardDescription>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">1,234</p>
</CardContent>
</Card>
))}
</div>
</Section>
),
},
}
export const ContentOnly: Story = {
args: {
children: (
<Section title="Standalone Content" description="A page layout with no navbar or footer.">
<p className="text-muted-foreground">
This demonstrates the PageLayout component with only content, no navbar, sidebar, or footer.
</p>
</Section>
),
},
}

View File

@@ -0,0 +1,135 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Section } from '@/components/ui/section'
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '@/components/ui/card'
const meta = {
title: 'Composition/Section',
component: Section,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
argTypes: {
variant: {
control: 'select',
options: ['default', 'highlighted', 'accent'],
},
width: {
control: 'select',
options: ['narrow', 'default', 'wide', 'full'],
},
},
} satisfies Meta<typeof Section>
export default meta
type Story = StoryObj<typeof meta>
const sampleCards = (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{['Design', 'Develop', 'Deploy'].map((title) => (
<Card key={title}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>Description for the {title.toLowerCase()} phase.</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Content explaining the {title.toLowerCase()} process in detail.
</p>
</CardContent>
</Card>
))}
</div>
)
export const Default: Story = {
args: {
title: 'Our Process',
description: 'How we build great products from concept to delivery.',
children: sampleCards,
},
}
export const Highlighted: Story = {
args: {
variant: 'highlighted',
title: 'Featured Section',
description: 'This section uses a highlighted background to stand out.',
children: sampleCards,
},
}
export const Accent: Story = {
args: {
variant: 'accent',
title: 'Accent Section',
description: 'A subtle accent background to differentiate this area.',
children: sampleCards,
},
}
export const Narrow: Story = {
args: {
width: 'narrow',
title: 'Narrow Section',
description: 'Constrained width for focused reading.',
children: (
<p className="text-muted-foreground">
This is a narrow section with max-w-3xl. Useful for text-heavy content that
benefits from shorter line lengths for readability.
</p>
),
},
}
export const Wide: Story = {
args: {
width: 'wide',
title: 'Wide Section',
description: 'Extended width for content-rich layouts.',
children: sampleCards,
},
}
export const Full: Story = {
args: {
width: 'full',
variant: 'highlighted',
title: 'Full Width Section',
description: 'Spans the full width of the viewport.',
children: sampleCards,
},
}
export const NoHeader: Story = {
args: {
children: sampleCards,
},
}
export const AllCombinations: Story = {
render: () => (
<div>
{(['default', 'highlighted', 'accent'] as const).map((variant) =>
(['narrow', 'default', 'wide'] as const).map((width) => (
<Section
key={`${variant}-${width}`}
variant={variant}
width={width}
title={`${variant} / ${width}`}
description={`Section with variant="${variant}" and width="${width}".`}
>
<div className="h-20 rounded-lg border-2 border-dashed border-muted-foreground/25 flex items-center justify-center text-sm text-muted-foreground">
Content area
</div>
</Section>
)),
)}
</div>
),
}

View File

@@ -0,0 +1,88 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Progress } from '@/components/ui/progress'
const meta = {
title: 'Data/Progress',
component: Progress,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
value: {
control: { type: 'range', min: 0, max: 100, step: 1 },
},
},
decorators: [
(Story) => (
<div className="w-100">
<Story />
</div>
),
],
} satisfies Meta<typeof Progress>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
value: 60,
},
}
export const Empty: Story = {
args: {
value: 0,
},
}
export const Quarter: Story = {
args: {
value: 25,
},
}
export const Half: Story = {
args: {
value: 50,
},
}
export const ThreeQuarters: Story = {
args: {
value: 75,
},
}
export const Complete: Story = {
args: {
value: 100,
},
}
export const AllStages: Story = {
render: () => (
<div className="flex flex-col gap-4 w-100">
<div className="space-y-1">
<span className="text-sm text-muted-foreground">0%</span>
<Progress value={0} />
</div>
<div className="space-y-1">
<span className="text-sm text-muted-foreground">25%</span>
<Progress value={25} />
</div>
<div className="space-y-1">
<span className="text-sm text-muted-foreground">50%</span>
<Progress value={50} />
</div>
<div className="space-y-1">
<span className="text-sm text-muted-foreground">75%</span>
<Progress value={75} />
</div>
<div className="space-y-1">
<span className="text-sm text-muted-foreground">100%</span>
<Progress value={100} />
</div>
</div>
),
}

View File

@@ -0,0 +1,117 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
} from '@/components/ui/table'
const meta = {
title: 'Data/Table',
component: Table,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Table>
export default meta
type Story = StoryObj<typeof meta>
const invoices = [
{ invoice: 'INV001', status: 'Paid', method: 'Credit Card', amount: '$250.00' },
{ invoice: 'INV002', status: 'Pending', method: 'PayPal', amount: '$150.00' },
{ invoice: 'INV003', status: 'Unpaid', method: 'Bank Transfer', amount: '$350.00' },
{ invoice: 'INV004', status: 'Paid', method: 'Credit Card', amount: '$450.00' },
{ invoice: 'INV005', status: 'Paid', method: 'PayPal', amount: '$550.00' },
]
export const Default: Story = {
render: () => (
<div className="w-150">
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-25">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$1,750.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
),
}
export const Simple: Story = {
render: () => (
<div className="w-100">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Alice</TableCell>
<TableCell>Engineer</TableCell>
</TableRow>
<TableRow>
<TableCell>Bob</TableCell>
<TableCell>Designer</TableCell>
</TableRow>
<TableRow>
<TableCell>Charlie</TableCell>
<TableCell>Manager</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
),
}
export const Empty: Story = {
render: () => (
<div className="w-100">
<Table>
<TableCaption>No data available.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={3} className="text-center text-muted-foreground h-24">
No results found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
),
}

View File

@@ -0,0 +1,71 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Terminal, AlertCircle } from 'lucide-react'
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
const meta = {
title: 'Feedback/Alert',
component: Alert,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
variant: {
control: 'select',
options: ['default', 'destructive'],
},
},
decorators: [
(Story) => (
<div className="w-125">
<Story />
</div>
),
],
} satisfies Meta<typeof Alert>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<Alert>
<Terminal className="size-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components to your app using the CLI.
</AlertDescription>
</Alert>
),
}
export const Destructive: Story = {
render: () => (
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired. Please log in again.
</AlertDescription>
</Alert>
),
}
export const WithoutIcon: Story = {
render: () => (
<Alert>
<AlertTitle>Note</AlertTitle>
<AlertDescription>
This alert has no icon, just a title and description.
</AlertDescription>
</Alert>
),
}
export const TitleOnly: Story = {
render: () => (
<Alert>
<Terminal className="size-4" />
<AlertTitle>A simple alert with only a title.</AlertTitle>
</Alert>
),
}

View File

@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Skeleton } from '@/components/ui/skeleton'
const meta = {
title: 'Feedback/Skeleton',
component: Skeleton,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Skeleton>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
className: 'h-4 w-62.5',
},
}
export const Circle: Story = {
args: {
className: 'size-12 rounded-full',
},
}
export const CardSkeleton: Story = {
render: () => (
<div className="flex items-center space-x-4">
<Skeleton className="size-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-62.5" />
<Skeleton className="h-4 w-50" />
</div>
</div>
),
}
export const FormSkeleton: Story = {
render: () => (
<div className="space-y-4 w-75">
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-9 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-30" />
<Skeleton className="h-9 w-full" />
</div>
<Skeleton className="h-9 w-25" />
</div>
),
}
export const TextBlock: Story = {
render: () => (
<div className="space-y-2 w-87.5">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
),
}

View File

@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Spinner } from '@/components/ui/spinner'
const meta = {
title: 'Feedback/Spinner',
component: Spinner,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Spinner>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const Small: Story = {
args: {
className: 'size-3 animate-spin',
},
}
export const Large: Story = {
args: {
className: 'size-8 animate-spin',
},
}
export const ExtraLarge: Story = {
args: {
className: 'size-12 animate-spin',
},
}
export const WithText: Story = {
render: () => (
<div className="flex items-center gap-2">
<Spinner />
<span className="text-sm text-muted-foreground">Loading...</span>
</div>
),
}
export const Sizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<Spinner className="size-3 animate-spin" />
<Spinner />
<Spinner className="size-6 animate-spin" />
<Spinner className="size-8 animate-spin" />
<Spinner className="size-12 animate-spin" />
</div>
),
}

View File

@@ -0,0 +1,211 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Form/Form',
component: Form,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Form>
export default meta
type Story = StoryObj<typeof meta>
const profileSchema = z.object({
username: z
.string()
.min(2, { message: 'Username must be at least 2 characters.' })
.max(30, { message: 'Username must not be longer than 30 characters.' }),
email: z.string().email({ message: 'Please enter a valid email address.' }),
})
type ProfileValues = z.infer<typeof profileSchema>
function ProfileForm() {
const form = useForm<ProfileValues>({
resolver: zodResolver(profileSchema),
defaultValues: {
username: '',
email: '',
},
})
function onSubmit(data: ProfileValues) {
alert(JSON.stringify(data, null, 2))
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-100 space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormDescription>
We will never share your email with anyone.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
export const Default: Story = {
render: () => <ProfileForm />,
}
const loginSchema = z.object({
email: z.string().email({ message: 'Invalid email address.' }),
password: z
.string()
.min(8, { message: 'Password must be at least 8 characters.' }),
})
type LoginValues = z.infer<typeof loginSchema>
function LoginForm() {
const form = useForm<LoginValues>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
},
})
function onSubmit(data: LoginValues) {
alert(JSON.stringify(data, null, 2))
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-100 space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Sign in
</Button>
</form>
</Form>
)
}
export const Login: Story = {
render: () => <LoginForm />,
}
function PrefilledErrorForm() {
const form = useForm<ProfileValues>({
resolver: zodResolver(profileSchema),
defaultValues: {
username: 'a',
email: 'not-an-email',
},
})
// Trigger validation on mount
React.useEffect(() => {
form.trigger()
}, [form])
return (
<Form {...form}>
<form className="w-100 space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
export const WithErrors: Story = {
render: () => <PrefilledErrorForm />,
}

View File

@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/components/ui/accordion'
const meta = {
title: 'Layout/Accordion',
component: Accordion,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Accordion>
export default meta
type Story = StoryObj<typeof meta>
export const Single: Story = {
render: () => (
<Accordion type="single" collapsible className="w-100">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that match the other components.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It uses CSS animations for smooth open and close transitions.
</AccordionContent>
</AccordionItem>
</Accordion>
),
}
export const Multiple: Story = {
render: () => (
<Accordion type="multiple" className="w-100">
<AccordionItem value="item-1">
<AccordionTrigger>What is Greyhaven?</AccordionTrigger>
<AccordionContent>
Greyhaven is a design system built with Radix UI and Tailwind CSS.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>How do I install it?</AccordionTrigger>
<AccordionContent>
You can install it via npm or pnpm. Check the documentation for details.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Can I customize themes?</AccordionTrigger>
<AccordionContent>
Absolutely. The design system uses CSS custom properties for full theme control.
</AccordionContent>
</AccordionItem>
</Accordion>
),
}
export const DefaultOpen: Story = {
render: () => (
<Accordion type="single" defaultValue="item-1" collapsible className="w-100">
<AccordionItem value="item-1">
<AccordionTrigger>Open by default</AccordionTrigger>
<AccordionContent>
This accordion item is open by default.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Click to open</AccordionTrigger>
<AccordionContent>
This accordion item starts closed.
</AccordionContent>
</AccordionItem>
</Accordion>
),
}

View File

@@ -0,0 +1,97 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
CardAction,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Layout/Card',
component: Card,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Card>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<Card className="w-87.5">
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here.</CardDescription>
</CardHeader>
<CardContent>
<p>Card content with some example text to demonstrate the layout.</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
),
}
export const WithAction: Story = {
render: () => (
<Card className="w-87.5">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>You have 3 unread messages.</CardDescription>
<CardAction>
<Button variant="outline" size="sm">Mark all read</Button>
</CardAction>
</CardHeader>
<CardContent>
<p>Here are your latest notifications.</p>
</CardContent>
</Card>
),
}
export const Simple: Story = {
render: () => (
<Card className="w-87.5">
<CardHeader>
<CardTitle>Simple Card</CardTitle>
</CardHeader>
<CardContent>
<p>A card with just a title and content.</p>
</CardContent>
</Card>
),
}
export const WithFooter: Story = {
render: () => (
<Card className="w-87.5">
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one click.</CardDescription>
</CardHeader>
<CardContent>
<p>Configure your project settings below.</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
),
}
export const ContentOnly: Story = {
render: () => (
<Card className="w-87.5">
<CardContent>
<p>A minimal card with only content, no header or footer.</p>
</CardContent>
</Card>
),
}

View File

@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Separator } from '@/components/ui/separator'
const meta = {
title: 'Layout/Separator',
component: Separator,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
orientation: {
control: 'select',
options: ['horizontal', 'vertical'],
},
decorative: { control: 'boolean' },
},
} satisfies Meta<typeof Separator>
export default meta
type Story = StoryObj<typeof meta>
export const Horizontal: Story = {
args: {
orientation: 'horizontal',
},
decorators: [
(Story) => (
<div className="w-75">
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">Greyhaven Design System</h4>
<p className="text-sm text-muted-foreground">An open-source UI component library.</p>
</div>
<Story />
<div className="flex h-5 items-center space-x-4 text-sm">
<div>Blog</div>
<div>Docs</div>
<div>Source</div>
</div>
</div>
),
],
}
export const Vertical: Story = {
args: {
orientation: 'vertical',
},
decorators: [
(Story) => (
<div className="flex h-5 items-center space-x-4 text-sm">
<div>Blog</div>
<Story />
<div>Docs</div>
<Story />
<div>Source</div>
</div>
),
],
}

View File

@@ -0,0 +1,109 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
} from '@/components/ui/breadcrumb'
const meta = {
title: 'Navigation/Breadcrumb',
component: Breadcrumb,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Breadcrumb>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
),
}
export const WithEllipsis: Story = {
render: () => (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
),
}
export const TwoLevels: Story = {
render: () => (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Dashboard</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Settings</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
),
}
export const DeepNesting: Story = {
render: () => (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">Products</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">Electronics</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">Laptops</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>MacBook Pro</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
),
}

View File

@@ -0,0 +1,94 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Navbar, NavbarLink } from '@/components/ui/navbar'
import { Logo } from '@/components/ui/logo'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Navigation/Navbar',
component: Navbar,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
argTypes: {
variant: {
control: 'select',
options: ['solid', 'transparent', 'minimal'],
},
},
decorators: [
(Story) => (
<div className="min-h-[200px]">
<Story />
</div>
),
],
} satisfies Meta<typeof Navbar>
export default meta
type Story = StoryObj<typeof meta>
const navLinks = (
<>
<NavbarLink href="#" active>Home</NavbarLink>
<NavbarLink href="#">About</NavbarLink>
<NavbarLink href="#">Services</NavbarLink>
<NavbarLink href="#">Contact</NavbarLink>
</>
)
const navActions = (
<>
<Button variant="ghost" size="sm">Log in</Button>
<Button size="sm">Sign up</Button>
</>
)
export const Solid: Story = {
args: {
variant: 'solid',
logo: <Logo size="sm" />,
actions: navActions,
children: navLinks,
},
}
export const Transparent: Story = {
args: {
variant: 'transparent',
logo: <Logo size="sm" />,
actions: navActions,
children: navLinks,
},
decorators: [
(Story) => (
<div className="min-h-[200px] bg-gradient-to-br from-primary/20 to-primary/5">
<Story />
</div>
),
],
}
export const Minimal: Story = {
args: {
variant: 'minimal',
logo: <Logo size="sm" />,
actions: navActions,
children: navLinks,
},
}
export const WithoutActions: Story = {
args: {
variant: 'solid',
logo: <Logo size="sm" />,
children: navLinks,
},
}
export const LogoOnly: Story = {
args: {
variant: 'solid',
logo: <Logo size="sm" />,
actions: navActions,
},
}

View File

@@ -0,0 +1,72 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Overlay/AlertDialog',
component: AlertDialog,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof AlertDialog>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Delete Account</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
),
}
export const Destructive: Story = {
render: () => (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete Project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete project?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the project and all associated data.
This action cannot be reversed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-white hover:bg-destructive/90">
Yes, delete project
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
),
}

View File

@@ -0,0 +1,106 @@
import type { Meta, StoryObj } from '@storybook/react'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
const meta = {
title: 'Overlay/Dialog',
component: Dialog,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Dialog>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
This is a dialog description. It provides context about the dialog content.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p>Dialog body content goes here.</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
),
}
export const WithForm: Story = {
render: () => (
<Dialog>
<DialogTrigger asChild>
<Button>Edit Profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you are done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="John Doe" />
</div>
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input id="username" defaultValue="@johndoe" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
),
}
export const NoCloseButton: Story = {
render: () => (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open (no close button)</Button>
</DialogTrigger>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>No Close Button</DialogTitle>
<DialogDescription>
This dialog has no close button in the corner.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button>Got it</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
),
}

View File

@@ -0,0 +1,99 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Plus } from 'lucide-react'
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from '@/components/ui/tooltip'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Overlay/Tooltip',
component: Tooltip,
tags: ['autodocs'],
parameters: { layout: 'centered' },
} satisfies Meta<typeof Tooltip>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipContent>
<p>This is a tooltip</p>
</TooltipContent>
</Tooltip>
),
}
export const Top: Story = {
render: () => (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Top</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Tooltip on top</p>
</TooltipContent>
</Tooltip>
),
}
export const Right: Story = {
render: () => (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Right</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Tooltip on right</p>
</TooltipContent>
</Tooltip>
),
}
export const Bottom: Story = {
render: () => (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Bottom</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Tooltip on bottom</p>
</TooltipContent>
</Tooltip>
),
}
export const Left: Story = {
render: () => (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Left</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Tooltip on left</p>
</TooltipContent>
</Tooltip>
),
}
export const WithIconButton: Story = {
render: () => (
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline">
<Plus className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Add item</p>
</TooltipContent>
</Tooltip>
),
}

View File

@@ -0,0 +1,170 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Badge } from '@/components/ui/badge'
const meta = {
title: 'Primitives/Badge',
component: Badge,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
variant: {
control: 'select',
options: [
'default',
'secondary',
'muted',
'destructive',
'outline',
'success',
'warning',
'info',
'tag',
'value',
'whatsapp',
'email',
'telegram',
'zulip',
'platform',
],
},
},
} satisfies Meta<typeof Badge>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: 'Badge',
variant: 'default',
},
}
export const Secondary: Story = {
args: {
children: 'Secondary',
variant: 'secondary',
},
}
export const Muted: Story = {
args: {
children: 'Muted',
variant: 'muted',
},
}
export const Destructive: Story = {
args: {
children: 'Destructive',
variant: 'destructive',
},
}
export const Outline: Story = {
args: {
children: 'Outline',
variant: 'outline',
},
}
export const Success: Story = {
args: {
children: 'Success',
variant: 'success',
},
}
export const Warning: Story = {
args: {
children: 'Warning',
variant: 'warning',
},
}
export const Info: Story = {
args: {
children: 'Info',
variant: 'info',
},
}
export const Tag: Story = {
args: {
children: 'Tag',
variant: 'tag',
},
}
export const Value: Story = {
args: {
children: '42',
variant: 'value',
},
}
export const Whatsapp: Story = {
args: {
children: 'WhatsApp',
variant: 'whatsapp',
},
}
export const Email: Story = {
args: {
children: 'Email',
variant: 'email',
},
}
export const Telegram: Story = {
args: {
children: 'Telegram',
variant: 'telegram',
},
}
export const Zulip: Story = {
args: {
children: 'Zulip',
variant: 'zulip',
},
}
export const Platform: Story = {
args: {
children: 'Platform',
variant: 'platform',
},
}
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-2">
{(
[
'default',
'secondary',
'muted',
'destructive',
'outline',
'success',
'warning',
'info',
'tag',
'value',
'whatsapp',
'email',
'telegram',
'zulip',
'platform',
] as const
).map((variant) => (
<Badge key={variant} variant={variant}>
{variant}
</Badge>
))}
</div>
),
}

View File

@@ -0,0 +1,171 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ChevronRight, Mail, Loader2, Plus } from 'lucide-react'
import { Button } from '@/components/ui/button'
const meta = {
title: 'Primitives/Button',
component: Button,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
variant: {
control: 'select',
options: ['default', 'secondary', 'outline', 'ghost', 'link', 'destructive'],
},
size: {
control: 'select',
options: ['default', 'sm', 'lg', 'icon', 'icon-sm', 'icon-lg'],
},
disabled: { control: 'boolean' },
asChild: { control: 'boolean' },
},
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: 'Button',
variant: 'default',
size: 'default',
},
}
export const Secondary: Story = {
args: {
children: 'Secondary',
variant: 'secondary',
},
}
export const Outline: Story = {
args: {
children: 'Outline',
variant: 'outline',
},
}
export const Ghost: Story = {
args: {
children: 'Ghost',
variant: 'ghost',
},
}
export const Link: Story = {
args: {
children: 'Link',
variant: 'link',
},
}
export const Destructive: Story = {
args: {
children: 'Delete',
variant: 'destructive',
},
}
export const Small: Story = {
args: {
children: 'Small',
size: 'sm',
},
}
export const Large: Story = {
args: {
children: 'Large',
size: 'lg',
},
}
export const Icon: Story = {
args: {
size: 'icon',
children: <Plus className="size-4" />,
'aria-label': 'Add',
},
}
export const IconSmall: Story = {
args: {
size: 'icon-sm',
children: <Plus className="size-4" />,
'aria-label': 'Add',
},
}
export const IconLarge: Story = {
args: {
size: 'icon-lg',
children: <Plus className="size-4" />,
'aria-label': 'Add',
},
}
export const WithIcon: Story = {
args: {
children: (
<>
<Mail /> Login with Email
</>
),
},
}
export const WithTrailingIcon: Story = {
args: {
children: (
<>
Next <ChevronRight />
</>
),
},
}
export const Loading: Story = {
args: {
disabled: true,
children: (
<>
<Loader2 className="animate-spin" /> Please wait
</>
),
},
}
export const Disabled: Story = {
args: {
children: 'Disabled',
disabled: true,
},
}
export const AsChild: Story = {
args: {
asChild: true,
children: <a href="#">Link styled as Button</a>,
},
}
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-4">
{(['default', 'secondary', 'outline', 'ghost', 'link', 'destructive'] as const).map(
(variant) => (
<div key={variant} className="flex items-center gap-2">
<span className="w-24 text-sm text-muted-foreground">{variant}</span>
<Button variant={variant} size="sm">Small</Button>
<Button variant={variant} size="default">Default</Button>
<Button variant={variant} size="lg">Large</Button>
<Button variant={variant} size="icon"><Plus className="size-4" /></Button>
<Button variant={variant} disabled>Disabled</Button>
</div>
),
)}
</div>
),
}

View File

@@ -0,0 +1,88 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
const meta = {
title: 'Primitives/Input',
component: Input,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
type: {
control: 'select',
options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url', 'file'],
},
disabled: { control: 'boolean' },
placeholder: { control: 'text' },
},
} satisfies Meta<typeof Input>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
placeholder: 'Enter text...',
type: 'text',
},
}
export const Email: Story = {
args: {
type: 'email',
placeholder: 'email@example.com',
},
}
export const Password: Story = {
args: {
type: 'password',
placeholder: 'Enter password...',
},
}
export const File: Story = {
args: {
type: 'file',
},
}
export const Disabled: Story = {
args: {
placeholder: 'Disabled input',
disabled: true,
},
}
export const WithLabel: Story = {
render: () => (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="with-label-input">Email</Label>
<Input id="with-label-input" type="email" placeholder="email@example.com" />
</div>
),
}
export const ErrorState: Story = {
render: () => (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="error-input">Email</Label>
<Input
id="error-input"
type="email"
placeholder="email@example.com"
aria-invalid="true"
defaultValue="invalid-email"
/>
<p className="text-sm text-destructive">Please enter a valid email address.</p>
</div>
),
}
export const WithDefaultValue: Story = {
args: {
type: 'text',
defaultValue: 'Hello world',
},
}

View File

@@ -0,0 +1,103 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Bold, Italic, Underline } from 'lucide-react'
import { Toggle } from '@/components/ui/toggle'
const meta = {
title: 'Primitives/Toggle',
component: Toggle,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
variant: {
control: 'select',
options: ['default', 'outline'],
},
size: {
control: 'select',
options: ['default', 'sm', 'lg'],
},
disabled: { control: 'boolean' },
},
} satisfies Meta<typeof Toggle>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: <Bold className="size-4" />,
'aria-label': 'Toggle bold',
},
}
export const Outline: Story = {
args: {
variant: 'outline',
children: <Italic className="size-4" />,
'aria-label': 'Toggle italic',
},
}
export const Small: Story = {
args: {
size: 'sm',
children: <Bold className="size-4" />,
'aria-label': 'Toggle bold',
},
}
export const Large: Story = {
args: {
size: 'lg',
children: <Bold className="size-4" />,
'aria-label': 'Toggle bold',
},
}
export const WithText: Story = {
args: {
children: (
<>
<Italic className="size-4" />
Italic
</>
),
'aria-label': 'Toggle italic',
},
}
export const Disabled: Story = {
args: {
disabled: true,
children: <Bold className="size-4" />,
'aria-label': 'Toggle bold',
},
}
export const Pressed: Story = {
args: {
defaultPressed: true,
children: <Bold className="size-4" />,
'aria-label': 'Toggle bold',
},
}
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<span className="w-20 text-sm text-muted-foreground">default</span>
<Toggle size="sm" aria-label="Bold"><Bold className="size-4" /></Toggle>
<Toggle size="default" aria-label="Italic"><Italic className="size-4" /></Toggle>
<Toggle size="lg" aria-label="Underline"><Underline className="size-4" /></Toggle>
</div>
<div className="flex items-center gap-2">
<span className="w-20 text-sm text-muted-foreground">outline</span>
<Toggle variant="outline" size="sm" aria-label="Bold"><Bold className="size-4" /></Toggle>
<Toggle variant="outline" size="default" aria-label="Italic"><Italic className="size-4" /></Toggle>
<Toggle variant="outline" size="lg" aria-label="Underline"><Underline className="size-4" /></Toggle>
</div>
</div>
),
}