Storybook (#328)

* feat(docs): add interactive examples and API references for Button, Card, and LoadingFallback components
- Updated dependencies
- Set `retries` to a fixed value of 3 for consistent test retries across environments.
- Increased `timeout` from 60 seconds to 120 seconds to allow more time for tests to complete.
- Reduced `expect` timeout from 10 seconds to 5 seconds for quicker feedback on assertions.
This commit is contained in:
Giancarlo Buomprisco
2025-08-22 06:35:44 +07:00
committed by GitHub
parent 360ea30f4b
commit ad427365c9
87 changed files with 30102 additions and 431 deletions

View File

@@ -0,0 +1,937 @@
'use client';
import { useState } from 'react';
import {
AlertTriangle,
Archive,
Ban,
Download,
LogOut,
RefreshCw,
Share,
Trash2,
UserX,
X,
} from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import { useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface AlertDialogControls {
title: string;
description: string;
triggerText: string;
triggerVariant:
| 'default'
| 'destructive'
| 'outline'
| 'secondary'
| 'ghost'
| 'link';
actionText: string;
actionVariant: 'default' | 'destructive';
cancelText: string;
withIcon: boolean;
severity: 'info' | 'warning' | 'error' | 'success';
}
const triggerVariantOptions = [
{ value: 'destructive', label: 'Destructive', description: 'Danger button' },
{ value: 'outline', label: 'Outline', description: 'Outlined button' },
{ value: 'default', label: 'Default', description: 'Primary button' },
{ value: 'secondary', label: 'Secondary', description: 'Secondary style' },
{ value: 'ghost', label: 'Ghost', description: 'Minimal button' },
] as const;
const actionVariantOptions = [
{
value: 'destructive',
label: 'Destructive',
description: 'For dangerous actions',
},
{ value: 'default', label: 'Default', description: 'For normal actions' },
] as const;
const severityOptions = [
{ value: 'info', label: 'Info', description: 'General information' },
{ value: 'warning', label: 'Warning', description: 'Caution required' },
{ value: 'error', label: 'Error', description: 'Destructive action' },
{ value: 'success', label: 'Success', description: 'Positive action' },
] as const;
const iconOptions = [
{ value: 'trash', icon: Trash2, label: 'Trash' },
{ value: 'alert', icon: AlertTriangle, label: 'Alert Triangle' },
{ value: 'logout', icon: LogOut, label: 'Log Out' },
{ value: 'userx', icon: UserX, label: 'User X' },
{ value: 'x', icon: X, label: 'X' },
{ value: 'ban', icon: Ban, label: 'Ban' },
{ value: 'archive', icon: Archive, label: 'Archive' },
{ value: 'download', icon: Download, label: 'Download' },
];
export function AlertDialogStory() {
const { controls, updateControl } = useStoryControls<AlertDialogControls>({
title: 'Are you absolutely sure?',
description:
'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
triggerText: 'Delete Account',
triggerVariant: 'destructive',
actionText: 'Yes, delete account',
actionVariant: 'destructive',
cancelText: 'Cancel',
withIcon: true,
severity: 'error',
});
const [selectedIcon, setSelectedIcon] = useState('trash');
const selectedIconData = iconOptions.find(
(opt) => opt.value === selectedIcon,
);
const IconComponent = selectedIconData?.icon || AlertTriangle;
const generateCode = () => {
let code = `<AlertDialog>\n`;
code += ` <AlertDialogTrigger asChild>\n`;
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
code += ` </AlertDialogTrigger>\n`;
code += ` <AlertDialogContent>\n`;
code += ` <AlertDialogHeader>\n`;
if (controls.withIcon) {
code += ` <div className="flex items-center gap-3">\n`;
code += ` <div className="${getSeverityIconStyles(controls.severity)}">\n`;
const iconName = selectedIconData?.icon.name || 'AlertTriangle';
code += ` <${iconName} className="h-5 w-5" />\n`;
code += ` </div>\n`;
code += ` <AlertDialogTitle>${controls.title}</AlertDialogTitle>\n`;
code += ` </div>\n`;
} else {
code += ` <AlertDialogTitle>${controls.title}</AlertDialogTitle>\n`;
}
if (controls.description) {
code += ` <AlertDialogDescription>\n`;
code += ` ${controls.description}\n`;
code += ` </AlertDialogDescription>\n`;
}
code += ` </AlertDialogHeader>\n`;
code += ` <AlertDialogFooter>\n`;
code += ` <AlertDialogCancel>${controls.cancelText}</AlertDialogCancel>\n`;
if (controls.actionVariant === 'destructive') {
code += ` <AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90">\n`;
} else {
code += ` <AlertDialogAction>\n`;
}
code += ` ${controls.actionText}\n`;
code += ` </AlertDialogAction>\n`;
code += ` </AlertDialogFooter>\n`;
code += ` </AlertDialogContent>\n`;
code += `</AlertDialog>`;
return code;
};
const getSeverityIconStyles = (severity: string) => {
switch (severity) {
case 'error':
return 'flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-destructive/15 text-destructive';
case 'warning':
return 'flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-500';
case 'success':
return 'flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-500';
default:
return 'flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-500';
}
};
const renderPreview = () => {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={controls.triggerVariant}>
{controls.triggerText}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
{controls.withIcon ? (
<div className="flex items-center gap-3">
<div className={getSeverityIconStyles(controls.severity)}>
<IconComponent className="h-5 w-5" />
</div>
<AlertDialogTitle>{controls.title}</AlertDialogTitle>
</div>
) : (
<AlertDialogTitle>{controls.title}</AlertDialogTitle>
)}
{controls.description && (
<AlertDialogDescription>
{controls.description}
</AlertDialogDescription>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{controls.cancelText}</AlertDialogCancel>
<AlertDialogAction
className={
controls.actionVariant === 'destructive'
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: undefined
}
>
{controls.actionText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="severity">Severity Level</Label>
<SimpleStorySelect
value={controls.severity}
onValueChange={(value) => updateControl('severity', value)}
options={severityOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="triggerVariant">Trigger Button Style</Label>
<SimpleStorySelect
value={controls.triggerVariant}
onValueChange={(value) => updateControl('triggerVariant', value)}
options={triggerVariantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="actionVariant">Action Button Style</Label>
<SimpleStorySelect
value={controls.actionVariant}
onValueChange={(value) => updateControl('actionVariant', value)}
options={actionVariantOptions}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="triggerText">Trigger Text</Label>
<Input
id="triggerText"
value={controls.triggerText}
onChange={(e) => updateControl('triggerText', e.target.value)}
placeholder="Button text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="title">Alert Title</Label>
<Input
id="title"
value={controls.title}
onChange={(e) => updateControl('title', e.target.value)}
placeholder="Alert dialog title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={controls.description}
onChange={(e) => updateControl('description', e.target.value)}
placeholder="Alert description"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="actionText">Action Button Text</Label>
<Input
id="actionText"
value={controls.actionText}
onChange={(e) => updateControl('actionText', e.target.value)}
placeholder="Confirm action text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cancelText">Cancel Button Text</Label>
<Input
id="cancelText"
value={controls.cancelText}
onChange={(e) => updateControl('cancelText', e.target.value)}
placeholder="Cancel action text"
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="withIcon">With Icon</Label>
<Switch
id="withIcon"
checked={controls.withIcon}
onCheckedChange={(checked) => updateControl('withIcon', checked)}
/>
</div>
{controls.withIcon && (
<div className="space-y-2">
<Label htmlFor="icon">Icon</Label>
<SimpleStorySelect
value={selectedIcon}
onValueChange={setSelectedIcon}
options={iconOptions.map((opt) => ({
value: opt.value,
label: opt.label,
description: `${opt.label} icon`,
}))}
/>
</div>
)}
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Destructive Actions</CardTitle>
<CardDescription>
Critical confirmations for dangerous operations
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
<Trash2 className="mr-2 h-4 w-4" />
Delete Item
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="bg-destructive/15 text-destructive flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
<Trash2 className="h-5 w-5" />
</div>
<AlertDialogTitle>Delete Item</AlertDialogTitle>
</div>
<AlertDialogDescription>
Are you sure you want to delete this item? This action
cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-500">
<LogOut className="h-5 w-5" />
</div>
<AlertDialogTitle>Sign Out</AlertDialogTitle>
</div>
<AlertDialogDescription>
Are you sure you want to sign out? You'll need to sign in
again to access your account.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Stay Signed In</AlertDialogCancel>
<AlertDialogAction>Sign Out</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<UserX className="mr-2 h-4 w-4" />
Remove User
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="bg-destructive/15 text-destructive flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
<UserX className="h-5 w-5" />
</div>
<AlertDialogTitle>Remove User Access</AlertDialogTitle>
</div>
<AlertDialogDescription>
This will remove the user's access to this workspace. They
will no longer be able to view or edit content.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Remove Access
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Confirmation Actions</CardTitle>
<CardDescription>
Standard confirmations for important actions
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Archive className="mr-2 h-4 w-4" />
Archive Project
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-500">
<Archive className="h-5 w-5" />
</div>
<AlertDialogTitle>Archive Project</AlertDialogTitle>
</div>
<AlertDialogDescription>
This will archive the project and make it read-only. You can
restore it later from the archived projects section.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Archive Project</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button>
<Download className="mr-2 h-4 w-4" />
Export Data
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-500">
<Download className="h-5 w-5" />
</div>
<AlertDialogTitle>Export Your Data</AlertDialogTitle>
</div>
<AlertDialogDescription>
This will generate a complete export of your data. The
export may take a few minutes to complete and will be sent
to your email.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Start Export</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<RefreshCw className="mr-2 h-4 w-4" />
Reset Settings
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-500">
<RefreshCw className="h-5 w-5" />
</div>
<AlertDialogTitle>Reset All Settings</AlertDialogTitle>
</div>
<AlertDialogDescription>
This will reset all your preferences to their default
values. Your data will not be affected, but you'll need to
reconfigure your settings.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Reset Settings</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Different Severity Levels</CardTitle>
<CardDescription>
Visual indicators for different types of actions
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-3">
<h4 className="text-sm font-semibold">Error/Destructive</h4>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
<Trash2 className="mr-2 h-4 w-4" />
Delete Forever
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="bg-destructive/15 text-destructive flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
<Trash2 className="h-5 w-5" />
</div>
<AlertDialogTitle>Permanent Deletion</AlertDialogTitle>
</div>
<AlertDialogDescription>
This action cannot be undone. The item will be permanently
deleted.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete Forever
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<div className="space-y-3">
<h4 className="text-sm font-semibold">Warning</h4>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm">
<AlertTriangle className="mr-2 h-4 w-4" />
Unsaved Changes
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-500">
<AlertTriangle className="h-5 w-5" />
</div>
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
</div>
<AlertDialogDescription>
You have unsaved changes. Are you sure you want to leave
without saving?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Stay Here</AlertDialogCancel>
<AlertDialogAction>Leave Without Saving</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<div className="space-y-3">
<h4 className="text-sm font-semibold">Info</h4>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm">
<Share className="mr-2 h-4 w-4" />
Share Publicly
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-500">
<Share className="h-5 w-5" />
</div>
<AlertDialogTitle>Share Publicly</AlertDialogTitle>
</div>
<AlertDialogDescription>
This will make your project visible to everyone with the
link. Anyone can view and comment on it.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep Private</AlertDialogCancel>
<AlertDialogAction>Make Public</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<div className="space-y-3">
<h4 className="text-sm font-semibold">Success</h4>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm">
<Download className="mr-2 h-4 w-4" />
Complete Setup
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-500">
<Download className="h-5 w-5" />
</div>
<AlertDialogTitle>Complete Setup</AlertDialogTitle>
</div>
<AlertDialogDescription>
You're about to complete the initial setup. This will
activate all features and send you a welcome email.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Not Yet</AlertDialogCancel>
<AlertDialogAction>Complete Setup</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>AlertDialog Components</CardTitle>
<CardDescription>
Complete API reference for AlertDialog components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">AlertDialog</h4>
<p className="text-muted-foreground mb-3 text-sm">
Root container for alert dialogs. Always modal and requires
explicit user action.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">open</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Controlled open state</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">onOpenChange</td>
<td className="p-3 font-mono text-sm">function</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Callback when open state changes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">AlertDialogAction</h4>
<p className="text-muted-foreground mb-3 text-sm">
The button that performs the primary action. Closes the dialog
when clicked.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">
Additional CSS classes (includes button styles by default)
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">AlertDialogCancel</h4>
<p className="text-muted-foreground mb-3 text-sm">
The button that cancels the action. Always closes the dialog
without performing the action.
</p>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">Other Components</h4>
<ul className="space-y-2 text-sm">
<li>
<strong>AlertDialogTrigger:</strong> Element that opens the
alert dialog
</li>
<li>
<strong>AlertDialogContent:</strong> Main dialog content
container
</li>
<li>
<strong>AlertDialogHeader:</strong> Container for title and
description
</li>
<li>
<strong>AlertDialogTitle:</strong> Required accessible title
</li>
<li>
<strong>AlertDialogDescription:</strong> Detailed explanation of
the action
</li>
<li>
<strong>AlertDialogFooter:</strong> Container for action buttons
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use AlertDialog</CardTitle>
<CardDescription>
Best practices for alert dialog usage
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use AlertDialog For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Destructive actions (delete, remove, cancel)</li>
<li> Critical confirmations before irreversible actions</li>
<li> Warning users about consequences</li>
<li> Confirming navigation away from unsaved work</li>
<li> System-critical decisions</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid AlertDialog For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Complex forms or data entry</li>
<li> Informational content (use Dialog instead)</li>
<li> Non-critical confirmations</li>
<li> Multi-step processes</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>AlertDialog vs Dialog</CardTitle>
<CardDescription>Understanding the differences</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="mb-2 text-sm font-semibold">AlertDialog</h4>
<ul className="space-y-1 text-sm">
<li> Always modal and blocking</li>
<li> Requires explicit action</li>
<li> Cannot be dismissed by clicking outside</li>
<li> Purpose-built for confirmations</li>
<li> Has dedicated Action/Cancel buttons</li>
</ul>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold">Dialog</h4>
<ul className="space-y-1 text-sm">
<li> Can be modal or non-modal</li>
<li> Can be dismissed by clicking outside</li>
<li> Flexible content and actions</li>
<li> Better for forms and complex content</li>
<li> Has close button in header</li>
</ul>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>Making alert dialogs accessible</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Focus Management</h4>
<p className="text-muted-foreground text-sm">
Focus moves to Cancel button by default
<br />
Tab navigation between Cancel and Action
<br />
Escape key activates Cancel action
<br /> Enter key activates Action button when focused
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Content Guidelines</h4>
<p className="text-muted-foreground text-sm">
Use clear, specific titles and descriptions
<br />
Explain consequences of the action
<br />
Use action-specific button labels
<br /> Always provide a way to cancel
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Visual Design</h4>
<p className="text-muted-foreground text-sm">
Use appropriate icons and colors for severity
<br />
Make destructive actions visually distinct
<br /> Ensure sufficient contrast for all text
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Writing Effective Alerts</CardTitle>
<CardDescription>
Content guidelines for alert dialogs
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Title Guidelines</h4>
<p className="text-muted-foreground text-sm">
Be specific about the action (not just "Are you sure?")
<br />
Use active voice ("Delete account" not "Account deletion")
<br /> Keep it concise but descriptive
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Description Guidelines</h4>
<p className="text-muted-foreground text-sm">
Explain what will happen
<br />
Mention if the action is irreversible
<br />
Provide context about consequences
<br /> Use plain, non-technical language
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Button Labels</h4>
<p className="text-muted-foreground text-sm">
Use specific verbs ("Delete", "Save", "Continue")
<br />
Match the action being performed
<br />
Avoid generic labels when possible
<br /> Make the primary action clear
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,656 @@
'use client';
import {
AlertCircle,
AlertTriangle,
CheckCircle,
Info,
Lightbulb,
Terminal,
XCircle,
} from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { StorySelect } from './story-select';
interface AlertControls {
variant: 'default' | 'destructive' | 'success' | 'warning' | 'info';
title: string;
description: string;
showIcon: boolean;
showTitle: boolean;
iconType:
| 'alert'
| 'warning'
| 'success'
| 'info'
| 'error'
| 'terminal'
| 'lightbulb';
className: string;
}
const iconOptions = [
{
value: 'alert',
icon: AlertCircle,
iconName: 'AlertCircle',
label: 'Alert Circle',
description: 'General alerts',
},
{
value: 'warning',
icon: AlertTriangle,
iconName: 'AlertTriangle',
label: 'Warning',
description: 'Warning messages',
},
{
value: 'success',
icon: CheckCircle,
iconName: 'CheckCircle',
label: 'Success',
description: 'Success messages',
},
{
value: 'info',
icon: Info,
iconName: 'Info',
label: 'Info',
description: 'Informational messages',
},
{
value: 'error',
icon: XCircle,
iconName: 'XCircle',
label: 'Error',
description: 'Error messages',
},
{
value: 'terminal',
icon: Terminal,
iconName: 'Terminal',
label: 'Terminal',
description: 'Code/technical messages',
},
{
value: 'lightbulb',
icon: Lightbulb,
iconName: 'Lightbulb',
label: 'Lightbulb',
description: 'Tips and suggestions',
},
] as const;
export function AlertStory() {
const { controls, updateControl } = useStoryControls<AlertControls>({
variant: 'default',
title: 'Alert Title',
description:
'This is an alert description that provides additional context and information.',
showIcon: true,
showTitle: true,
iconType: 'alert',
className: '',
});
const selectedIconData = iconOptions.find(
(opt) => opt.value === controls.iconType,
);
const SelectedIcon = selectedIconData?.icon || AlertCircle;
const generateCode = () => {
const propsString = generatePropsString(
{
variant: controls.variant,
className: controls.className,
},
{
variant: 'default',
className: '',
},
);
let code = `<Alert${propsString}>\n`;
if (controls.showIcon) {
const iconName = selectedIconData?.iconName || 'AlertCircle';
code += ` <${iconName} className="h-4 w-4" />\n`;
}
if (controls.showTitle) {
code += ` <AlertTitle>${controls.title}</AlertTitle>\n`;
}
code += ` <AlertDescription>\n ${controls.description}\n </AlertDescription>\n`;
code += `</Alert>`;
return code;
};
const variantOptions = [
{
value: 'default' as const,
label: 'Default',
description: 'Standard alert style',
},
{
value: 'destructive' as const,
label: 'Destructive',
description: 'Error/danger style',
},
{
value: 'success' as const,
label: 'Success',
description: 'Success/positive style',
},
{
value: 'warning' as const,
label: 'Warning',
description: 'Warning/caution style',
},
{
value: 'info' as const,
label: 'Info',
description: 'Information style',
},
];
const renderPreview = () => (
<Alert variant={controls.variant} className={controls.className}>
{controls.showIcon && <SelectedIcon className="h-4 w-4" />}
{controls.showTitle && <AlertTitle>{controls.title}</AlertTitle>}
<AlertDescription>{controls.description}</AlertDescription>
</Alert>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="variant">Variant</Label>
<StorySelect
value={controls.variant}
onValueChange={(value) => updateControl('variant', value)}
options={variantOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="showIcon">Show Icon</Label>
<Switch
id="showIcon"
checked={controls.showIcon}
onCheckedChange={(checked) => updateControl('showIcon', checked)}
/>
</div>
{controls.showIcon && (
<div className="space-y-2">
<Label htmlFor="iconType">Icon Type</Label>
<StorySelect
value={controls.iconType}
onValueChange={(value) => updateControl('iconType', value)}
options={iconOptions}
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="showTitle">Show Title</Label>
<Switch
id="showTitle"
checked={controls.showTitle}
onCheckedChange={(checked) => updateControl('showTitle', checked)}
/>
</div>
{controls.showTitle && (
<div className="space-y-2">
<Label htmlFor="title">Alert Title</Label>
<Input
id="title"
value={controls.title}
onChange={(e) => updateControl('title', e.target.value)}
placeholder="Enter alert title"
/>
</div>
)}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={controls.description}
onChange={(e) => updateControl('description', e.target.value)}
placeholder="Enter alert description"
rows={3}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="className">Custom Classes</Label>
<Input
id="className"
value={controls.className}
onChange={(e) => updateControl('className', e.target.value)}
placeholder="e.g. border-l-4 border-blue-500"
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Information Alerts</CardTitle>
<CardDescription>
General information and status updates
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>Information</AlertTitle>
<AlertDescription>
This is a general information alert that provides useful context
to the user.
</AlertDescription>
</Alert>
<Alert>
<Lightbulb className="h-4 w-4" />
<AlertTitle>Pro Tip</AlertTitle>
<AlertDescription>
You can use keyboard shortcuts to navigate faster. Press Ctrl+K to
open the command palette.
</AlertDescription>
</Alert>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Success Alerts</CardTitle>
<CardDescription>Positive feedback and confirmations</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-green-200 bg-green-50 text-green-800">
<CheckCircle className="h-4 w-4" />
<AlertTitle>Success!</AlertTitle>
<AlertDescription>
Your changes have been saved successfully. All data is now up to
date.
</AlertDescription>
</Alert>
<Alert className="border-green-200 bg-green-50 text-green-800">
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Account created successfully. Welcome to our platform!
</AlertDescription>
</Alert>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Warning Alerts</CardTitle>
<CardDescription>
Caution and attention-needed messages
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-yellow-200 bg-yellow-50 text-yellow-800">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Warning</AlertTitle>
<AlertDescription>
Your free trial expires in 3 days. Upgrade your account to
continue using all features.
</AlertDescription>
</Alert>
<Alert className="border-orange-200 bg-orange-50 text-orange-800">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Action Required</AlertTitle>
<AlertDescription>
Please verify your email address to complete your account setup.
Check your inbox for the verification link.
</AlertDescription>
</Alert>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Error Alerts</CardTitle>
<CardDescription>
Error messages and destructive alerts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to save changes. Please check your internet connection and
try again.
</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Authentication Failed</AlertTitle>
<AlertDescription>
Invalid credentials. Please check your username and password and
try again.
</AlertDescription>
</Alert>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Technical Alerts</CardTitle>
<CardDescription>Code and system-related messages</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<Terminal className="h-4 w-4" />
<AlertTitle>System Status</AlertTitle>
<AlertDescription>
<div className="mt-2">
<code className="bg-muted rounded px-2 py-1 text-sm">
npm install @kit/ui
</code>
</div>
<p className="mt-2">
Run this command to install the UI components package.
</p>
</AlertDescription>
</Alert>
<Alert className="border-blue-200 bg-blue-50 text-blue-800">
<Info className="h-4 w-4" />
<AlertTitle>API Update</AlertTitle>
<AlertDescription>
A new API version is available. Please update your integration to
use v2.0 for improved performance.
</AlertDescription>
</Alert>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Alert Components</CardTitle>
<CardDescription>
Complete API reference for Alert components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Alert */}
<div>
<h4 className="mb-3 text-lg font-semibold">Alert</h4>
<p className="text-muted-foreground mb-3 text-sm">
The root container component for alert messages.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">variant</td>
<td className="p-3 font-mono text-sm">
'default' | 'destructive' | 'success' | 'warning' | 'info'
</td>
<td className="p-3 font-mono text-sm">'default'</td>
<td className="p-3">Visual style variant of the alert</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">
Alert content (icon, title, description)
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* AlertTitle & AlertDescription */}
<div>
<h4 className="mb-3 text-lg font-semibold">
AlertTitle & AlertDescription
</h4>
<p className="text-muted-foreground mb-3 text-sm">
Semantic components for alert titles and descriptions.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Component</th>
<th className="p-3 text-left font-medium">Element</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">AlertTitle</td>
<td className="p-3 font-mono text-sm">h5</td>
<td className="p-3">Main heading for the alert</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">AlertDescription</td>
<td className="p-3 font-mono text-sm">div</td>
<td className="p-3">Detailed description of the alert</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Alerts</CardTitle>
<CardDescription>Best practices for alert usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Alerts For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> System status updates and notifications</li>
<li> Form validation messages</li>
<li> Important announcements</li>
<li> Error messages and troubleshooting info</li>
<li> Success confirmations</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Alerts For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Regular content or body text</li>
<li> Marketing messages (use cards instead)</li>
<li> Navigation elements</li>
<li> Content that doesn't require immediate attention</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Alert Hierarchy</CardTitle>
<CardDescription>
Using alerts effectively by priority
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div>
<div className="mb-2 flex items-center gap-2">
<XCircle className="h-4 w-4 text-red-500" />
<h4 className="text-sm font-semibold">
Critical (Destructive)
</h4>
</div>
<p className="text-muted-foreground ml-6 text-sm">
System errors, failed operations, security issues
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-500" />
<h4 className="text-sm font-semibold">Warning</h4>
</div>
<p className="text-muted-foreground ml-6 text-sm">
Actions needed, potential issues, expiring items
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<h4 className="text-sm font-semibold">Success</h4>
</div>
<p className="text-muted-foreground ml-6 text-sm">
Successful operations, confirmations
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Info className="h-4 w-4 text-blue-500" />
<h4 className="text-sm font-semibold">Information</h4>
</div>
<p className="text-muted-foreground ml-6 text-sm">
General information, tips, status updates
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Content Guidelines</CardTitle>
<CardDescription>Writing effective alert content</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Be Clear and Specific</h4>
<div className="space-y-2">
<div className="rounded border border-red-200 bg-red-50 p-3">
<p className="text-sm text-red-700">
❌ "Something went wrong"
</p>
</div>
<div className="rounded border border-green-200 bg-green-50 p-3">
<p className="text-sm text-green-700">
✅ "Failed to save changes. Please check your internet
connection and try again."
</p>
</div>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Provide Next Steps</h4>
<p className="text-muted-foreground text-sm">
When possible, include actionable steps the user can take to
resolve the issue or continue their workflow.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility</CardTitle>
<CardDescription>Making alerts accessible</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">ARIA Attributes</h4>
<p className="text-muted-foreground text-sm">
The Alert component automatically includes appropriate ARIA
attributes for screen readers.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Color and Icons</h4>
<p className="text-muted-foreground text-sm">
Always pair colors with icons and descriptive text. Don't rely
solely on color to convey meaning.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Focus Management</h4>
<p className="text-muted-foreground text-sm">
For dynamic alerts (appearing after user actions), consider
managing focus appropriately to announce changes to screen
readers.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,430 @@
'use client';
import { useState } from 'react';
import { AlertTriangle, CheckCircle, Clock, Crown, X, Zap } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface BadgeControls {
variant: 'default' | 'secondary' | 'destructive' | 'outline';
text: string;
withIcon: boolean;
iconPosition: 'left' | 'right';
size: 'default' | 'sm' | 'lg';
className: string;
}
const variantOptions = [
{ value: 'default', label: 'Default', description: 'Primary badge style' },
{ value: 'secondary', label: 'Secondary', description: 'Muted badge style' },
{
value: 'destructive',
label: 'Destructive',
description: 'Error or warning style',
},
{ value: 'outline', label: 'Outline', description: 'Outlined badge style' },
] as const;
const sizeOptions = [
{ value: 'sm', label: 'Small', description: 'Compact size' },
{ value: 'default', label: 'Default', description: 'Standard size' },
{ value: 'lg', label: 'Large', description: 'Larger size' },
] as const;
const iconOptions = [
{ value: 'crown', icon: Crown, label: 'Crown' },
{ value: 'zap', icon: Zap, label: 'Zap' },
{ value: 'alert', icon: AlertTriangle, label: 'Alert' },
{ value: 'check', icon: CheckCircle, label: 'Check' },
{ value: 'clock', icon: Clock, label: 'Clock' },
{ value: 'x', icon: X, label: 'X' },
];
export function BadgeStory() {
const { controls, updateControl } = useStoryControls<BadgeControls>({
variant: 'default',
text: 'Badge',
withIcon: false,
iconPosition: 'left',
size: 'default',
className: '',
});
const [selectedIcon, setSelectedIcon] = useState('crown');
const selectedIconData = iconOptions.find(
(opt) => opt.value === selectedIcon,
);
const IconComponent = selectedIconData?.icon || Crown;
const generateCode = () => {
const propsString = generatePropsString(
{
variant: controls.variant,
className: cn(
controls.className,
controls.size === 'sm' && 'px-1.5 py-0.5 text-xs',
controls.size === 'lg' && 'px-3 py-1 text-sm',
),
},
{
variant: 'default',
className: '',
},
);
let code = `<Badge${propsString}>`;
if (controls.withIcon) {
const iconName = selectedIconData?.icon.name || 'Crown';
if (controls.iconPosition === 'left') {
code += `\n <${iconName} className="mr-1 h-3 w-3" />`;
}
}
code += `\n ${controls.text}`;
if (controls.withIcon && controls.iconPosition === 'right') {
const iconName = selectedIconData?.icon.name || 'Crown';
code += `\n <${iconName} className="ml-1 h-3 w-3" />`;
}
code += `\n</Badge>`;
return code;
};
const renderPreview = () => (
<Badge
variant={controls.variant}
className={cn(
controls.className,
controls.size === 'sm' && 'px-1.5 py-0.5 text-xs',
controls.size === 'lg' && 'px-3 py-1 text-sm',
)}
>
{controls.withIcon && controls.iconPosition === 'left' && (
<IconComponent className="mr-1 h-3 w-3" />
)}
{controls.text}
{controls.withIcon && controls.iconPosition === 'right' && (
<IconComponent className="ml-1 h-3 w-3" />
)}
</Badge>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="variant">Variant</Label>
<SimpleStorySelect
value={controls.variant}
onValueChange={(value) => updateControl('variant', value)}
options={variantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="text">Badge Text</Label>
<Input
id="text"
value={controls.text}
onChange={(e) => updateControl('text', e.target.value)}
placeholder="Enter badge text"
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="withIcon">With Icon</Label>
<Switch
id="withIcon"
checked={controls.withIcon}
onCheckedChange={(checked) => updateControl('withIcon', checked)}
/>
</div>
{controls.withIcon && (
<>
<div className="space-y-2">
<Label htmlFor="icon">Icon</Label>
<SimpleStorySelect
value={selectedIcon}
onValueChange={setSelectedIcon}
options={iconOptions.map((opt) => ({
value: opt.value,
label: opt.label,
description: `${opt.label} icon`,
}))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="iconPosition">Icon Position</Label>
<SimpleStorySelect
value={controls.iconPosition}
onValueChange={(value) => updateControl('iconPosition', value)}
options={[
{
value: 'left',
label: 'Left',
description: 'Icon before text',
},
{
value: 'right',
label: 'Right',
description: 'Icon after text',
},
]}
/>
</div>
</>
)}
<div className="space-y-2">
<Label htmlFor="className">Custom Classes</Label>
<Input
id="className"
value={controls.className}
onChange={(e) => updateControl('className', e.target.value)}
placeholder="e.g. border-2 shadow-lg"
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Badge Variants</CardTitle>
<CardDescription>Different badge styles</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Badge Sizes</CardTitle>
<CardDescription>Different badge sizes</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Badge className="px-1.5 py-0.5 text-xs">Small</Badge>
<Badge>Default</Badge>
<Badge className="px-3 py-1 text-sm">Large</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Badges with Icons</CardTitle>
<CardDescription>Badges enhanced with icons</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Badge>
<Crown className="mr-1 h-3 w-3" />
Premium
</Badge>
<Badge variant="secondary">
<CheckCircle className="mr-1 h-3 w-3" />
Verified
</Badge>
<Badge variant="destructive">
<AlertTriangle className="mr-1 h-3 w-3" />
Warning
</Badge>
<Badge variant="outline">
New
<Zap className="ml-1 h-3 w-3" />
</Badge>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Badge Component</CardTitle>
<CardDescription>
Complete API reference for Badge component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Badge</h4>
<p className="text-muted-foreground mb-3 text-sm">
A small labeled status indicator or category tag.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">variant</td>
<td className="p-3 font-mono text-sm">
'default' | 'secondary' | 'destructive' | 'outline'
</td>
<td className="p-3 font-mono text-sm">'default'</td>
<td className="p-3">Visual style variant</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Badge content</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Badges</CardTitle>
<CardDescription>Best practices for badge usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Badges For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Status indicators (new, verified, premium)</li>
<li> Category labels and tags</li>
<li> Notification counts</li>
<li> Feature flags and labels</li>
<li> Version or type indicators</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Badges For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Long text content (use cards instead)</li>
<li> Interactive elements (use buttons instead)</li>
<li> Main navigation items</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Badge Hierarchy</CardTitle>
<CardDescription>Using badge variants effectively</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div>
<div className="mb-2 flex items-center gap-2">
<Badge>Default</Badge>
<h4 className="text-sm font-semibold">Primary</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Important status, featured items, primary categories
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Badge variant="secondary">Secondary</Badge>
<h4 className="text-sm font-semibold">Secondary</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Supporting information, metadata, less prominent labels
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Badge variant="outline">Outline</Badge>
<h4 className="text-sm font-semibold">Neutral</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Subtle labels, optional information, inactive states
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Badge variant="destructive">Destructive</Badge>
<h4 className="text-sm font-semibold">Critical</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Errors, warnings, urgent status, deprecated items
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,498 @@
'use client';
import { useState } from 'react';
import { BarChart3, FileText, Home, Settings, Users } from 'lucide-react';
import {
BorderedNavigationMenu,
BorderedNavigationMenuItem,
} from '@kit/ui/bordered-navigation-menu';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import { Switch } from '@kit/ui/switch';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface BorderedNavigationMenuControls {
showIcons: boolean;
}
export function BorderedNavigationMenuStory() {
const { controls, updateControl } =
useStoryControls<BorderedNavigationMenuControls>({
showIcons: true,
});
const [activeTab, setActiveTab] = useState('#dashboard');
const generateCode = () => {
return `import { BorderedNavigationMenu, BorderedNavigationMenuItem } from '@kit/ui/bordered-navigation-menu';
import { usePathname } from 'next/navigation';
const pathname = usePathname();
<BorderedNavigationMenu>
<BorderedNavigationMenuItem
path="#dashboard"
label="Dashboard"
active={pathname === '#dashboard'}
/>
<BorderedNavigationMenuItem
path="#users"
label="Users"
active={pathname === '#users'}
/>
<BorderedNavigationMenuItem
path="#settings"
label="Settings"
active={pathname === '#settings'}
/>
</BorderedNavigationMenu>`;
};
const navigationItems = [
{
path: '#dashboard',
label: 'Dashboard',
icon: Home,
},
{
path: '#users',
label: 'Users',
icon: Users,
},
{
path: '#analytics',
label: 'Analytics',
icon: BarChart3,
},
{
path: '#reports',
label: 'Reports',
icon: FileText,
},
{
path: '#settings',
label: 'Settings',
icon: Settings,
},
];
const renderPreview = () => (
<div className="w-full">
<BorderedNavigationMenu>
{navigationItems.map((item) => (
<BorderedNavigationMenuItem
key={item.path}
path={item.path}
label={
controls.showIcons ? (
<div className="flex items-center space-x-2">
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</div>
) : (
item.label
)
}
active={activeTab === item.path}
/>
))}
</BorderedNavigationMenu>
<div className="bg-muted/20 mt-8 rounded-lg border p-4">
<h3 className="mb-2 font-semibold">Simulated Navigation</h3>
<p className="text-muted-foreground mb-4 text-sm">
Click tabs above to see active state changes:
</p>
<div className="flex flex-wrap gap-2">
{navigationItems.map((item) => (
<button
key={item.path}
onClick={() => setActiveTab(item.path)}
className={`rounded px-3 py-1 text-sm ${
activeTab === item.path
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'
}`}
>
{item.label}
</button>
))}
</div>
</div>
</div>
);
const renderControls = () => (
<>
<div className="flex items-center justify-between">
<Label htmlFor="showIcons">Show Icons</Label>
<Switch
id="showIcons"
checked={controls.showIcons}
onCheckedChange={(checked) => updateControl('showIcons', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Navigation</CardTitle>
<CardDescription>Simple text-only navigation menu</CardDescription>
</CardHeader>
<CardContent>
<BorderedNavigationMenu>
<BorderedNavigationMenuItem
path="/home"
label="Home"
active={true}
/>
<BorderedNavigationMenuItem
path="/about"
label="About"
active={false}
/>
<BorderedNavigationMenuItem
path="/contact"
label="Contact"
active={false}
/>
</BorderedNavigationMenu>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Navigation with Icons</CardTitle>
<CardDescription>
Navigation menu items with icons and labels
</CardDescription>
</CardHeader>
<CardContent>
<BorderedNavigationMenu>
<BorderedNavigationMenuItem
path="/overview"
label={
<div className="flex items-center space-x-2">
<BarChart3 className="h-4 w-4" />
<span>Overview</span>
</div>
}
active={false}
/>
<BorderedNavigationMenuItem
path="/team"
label={
<div className="flex items-center space-x-2">
<Users className="h-4 w-4" />
<span>Team</span>
</div>
}
active={true}
/>
<BorderedNavigationMenuItem
path="/config"
label={
<div className="flex items-center space-x-2">
<Settings className="h-4 w-4" />
<span>Config</span>
</div>
}
active={false}
/>
</BorderedNavigationMenu>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Responsive Navigation</CardTitle>
<CardDescription>
How navigation adapts to different screen sizes
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h4 className="mb-2 text-sm font-semibold">Desktop View</h4>
<BorderedNavigationMenu>
<BorderedNavigationMenuItem
path="/dashboard"
label="Dashboard"
active={true}
/>
<BorderedNavigationMenuItem
path="/projects"
label="Projects"
active={false}
/>
<BorderedNavigationMenuItem
path="/team"
label="Team Members"
active={false}
/>
<BorderedNavigationMenuItem
path="/billing"
label="Billing & Usage"
active={false}
/>
<BorderedNavigationMenuItem
path="/settings"
label="Account Settings"
active={false}
/>
</BorderedNavigationMenu>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold">
Mobile View (Simulated)
</h4>
<div className="bg-muted/20 rounded-lg border p-2">
<p className="text-muted-foreground text-xs">
On smaller screens, only active and adjacent items are
typically shown, with overflow handled by the navigation
system.
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>BorderedNavigationMenu Components</CardTitle>
<CardDescription>
Complete API reference for BorderedNavigationMenu components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">
BorderedNavigationMenu
</h4>
<p className="text-muted-foreground mb-3 text-sm">
Container component for navigation menu items with bordered active
state.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">
BorderedNavigationMenuItem components
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">
BorderedNavigationMenuItem
</h4>
<p className="text-muted-foreground mb-3 text-sm">
Individual navigation menu item with automatic active state
detection.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">path</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Navigation path/route</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">label</td>
<td className="p-3 font-mono text-sm">
ReactNode | string
</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Display label or component</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">active</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">auto-detected</td>
<td className="p-3">Override active state</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">end</td>
<td className="p-3 font-mono text-sm">
boolean | function
</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Exact path matching</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">buttonClassName</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">CSS classes for button element</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use BorderedNavigationMenu</CardTitle>
<CardDescription>
Best practices for bordered navigation
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use BorderedNavigationMenu For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Primary navigation within sections</li>
<li> Tab-style navigation for related content</li>
<li> Dashboard and admin panel navigation</li>
<li> Settings and configuration sections</li>
<li> Multi-step form navigation</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Use Other Patterns For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Site-wide main navigation (use header navigation)</li>
<li> Deep hierarchical navigation (use sidebar)</li>
<li> Single-action buttons (use regular buttons)</li>
<li> Pagination (use pagination component)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Design Guidelines</CardTitle>
<CardDescription>
Creating effective navigation experiences
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Active State Indication</h4>
<p className="text-muted-foreground text-sm">
The bordered bottom line clearly indicates the current active
section. Use consistent active state styling across your
application.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Label Clarity</h4>
<p className="text-muted-foreground text-sm">
Use clear, concise labels that accurately describe the
destination. Consider adding icons for better visual recognition.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Responsive Behavior</h4>
<p className="text-muted-foreground text-sm">
On smaller screens, non-active items may be hidden to save space.
Plan your navigation hierarchy accordingly.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Features</CardTitle>
<CardDescription>Built-in accessibility support</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
<p className="text-muted-foreground text-sm">
Full keyboard support with Tab navigation and Enter/Space
activation.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Screen Reader Support</h4>
<p className="text-muted-foreground text-sm">
Proper ARIA attributes and semantic HTML for assistive
technologies.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Focus Management</h4>
<p className="text-muted-foreground text-sm">
Clear focus indicators and proper focus management during
navigation.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,619 @@
'use client';
import { useState } from 'react';
import {
ChevronRightIcon,
FileTextIcon,
FolderIcon,
HomeIcon,
SlashIcon,
} from 'lucide-react';
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@kit/ui/breadcrumb';
import { Card, CardContent } from '@kit/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Switch } from '@kit/ui/switch';
import { generateImportStatement } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface BreadcrumbStoryControls {
separator: 'chevron' | 'slash' | 'custom';
showHome: boolean;
showEllipsis: boolean;
maxItems: number;
}
const breadcrumbItems = [
{ id: 'home', label: 'Home', href: '/', icon: HomeIcon },
{ id: 'docs', label: 'Documentation', href: '/docs' },
{
id: 'components',
label: 'Components',
href: '/docs/components',
},
{
id: 'breadcrumb',
label: 'Breadcrumb',
href: '/docs/components/breadcrumb',
},
];
export default function BreadcrumbStory() {
const [controls, setControls] = useState<BreadcrumbStoryControls>({
separator: 'chevron',
showHome: true,
showEllipsis: false,
maxItems: 4,
});
const getSeparator = () => {
switch (controls.separator) {
case 'slash':
return <SlashIcon className="h-4 w-4" />;
case 'custom':
return <span className="text-muted-foreground"></span>;
case 'chevron':
default:
return <ChevronRightIcon className="h-4 w-4" />;
}
};
const getDisplayedItems = () => {
const items = controls.showHome
? breadcrumbItems
: breadcrumbItems.slice(1);
if (!controls.showEllipsis || items.length <= controls.maxItems) {
return items;
}
// Show first item, ellipsis, and last few items
const remainingSlots = controls.maxItems - 2; // Reserve slots for first item and ellipsis
const lastItems = items.slice(-remainingSlots);
return [
items[0],
{ id: 'ellipsis', label: '...', href: '#', ellipsis: true },
...lastItems,
];
};
const displayedItems = getDisplayedItems();
// Controls section
const controlsContent = (
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">Separator</label>
<Select
value={controls.separator}
onValueChange={(value: BreadcrumbStoryControls['separator']) =>
setControls((prev) => ({ ...prev, separator: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="chevron">Chevron ()</SelectItem>
<SelectItem value="slash">Slash (/)</SelectItem>
<SelectItem value="custom">Arrow ()</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Max Items</label>
<Select
value={controls.maxItems.toString()}
onValueChange={(value: string) =>
setControls((prev) => ({
...prev,
maxItems: parseInt(value),
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2 items</SelectItem>
<SelectItem value="3">3 items</SelectItem>
<SelectItem value="4">4 items</SelectItem>
<SelectItem value="5">5 items</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<Switch
id="showHome"
checked={controls.showHome}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showHome: checked }))
}
/>
<label htmlFor="showHome" className="text-sm">
Show Home
</label>
</div>
<div className="flex items-center gap-2">
<Switch
id="showEllipsis"
checked={controls.showEllipsis}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showEllipsis: checked }))
}
/>
<label htmlFor="showEllipsis" className="text-sm">
Show Ellipsis
</label>
</div>
</div>
</div>
);
// Preview section
const previewContent = (
<div className="p-6">
<Breadcrumb>
<BreadcrumbList>
{displayedItems.map((item, index) => (
<div key={item.id} className="flex items-center">
{index > 0 && (
<BreadcrumbSeparator>{getSeparator()}</BreadcrumbSeparator>
)}
<BreadcrumbItem>
{item.ellipsis ? (
<BreadcrumbEllipsis />
) : index === displayedItems.length - 1 ? (
<BreadcrumbPage
className={item.icon ? 'flex items-center gap-2' : ''}
>
{item.icon && <item.icon className="h-4 w-4" />}
{item.label}
</BreadcrumbPage>
) : (
<BreadcrumbLink
href={item.href}
className={item.icon ? 'flex items-center gap-2' : ''}
>
{item.icon && <item.icon className="h-4 w-4" />}
{item.label}
</BreadcrumbLink>
)}
</BreadcrumbItem>
</div>
))}
</BreadcrumbList>
</Breadcrumb>
</div>
);
// Examples section
const examplesContent = (
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Simple Breadcrumb</h3>
<Card>
<CardContent className="pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Documentation</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">With Icons</h3>
<Card>
<CardContent className="pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" className="flex items-center gap-2">
<HomeIcon className="h-4 w-4" />
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink
href="/docs"
className="flex items-center gap-2"
>
<FolderIcon className="h-4 w-4" />
Documentation
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage className="flex items-center gap-2">
<FileTextIcon className="h-4 w-4" />
Breadcrumb
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">With Custom Separator</h3>
<Card>
<CardContent className="pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<SlashIcon className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Documentation</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<SlashIcon className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
With Ellipsis for Long Path
</h3>
<Card>
<CardContent className="pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</CardContent>
</Card>
</div>
</div>
);
// API Reference section
const apiReferenceContent = (
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Components</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Component</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">Breadcrumb</td>
<td className="p-2">
Root component that provides nav element
</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">BreadcrumbList</td>
<td className="p-2">
Ordered list container for breadcrumb items
</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">BreadcrumbItem</td>
<td className="p-2">Individual breadcrumb item container</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">BreadcrumbLink</td>
<td className="p-2">Navigable breadcrumb link</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">BreadcrumbPage</td>
<td className="p-2">Current page (non-navigable)</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">BreadcrumbSeparator</td>
<td className="p-2">Separator between breadcrumb items</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">BreadcrumbEllipsis</td>
<td className="p-2">Ellipsis indicator for collapsed items</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Component Hierarchy</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Current</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Accessibility Features</h3>
<div className="space-y-2 text-sm">
<p>
<code>aria-label="breadcrumb"</code> on nav element
</p>
<p>
<code>aria-current="page"</code> on current page
</p>
<p>
<code>role="presentation"</code> on separators
</p>
<p>
<code>aria-hidden="true"</code> on decorative elements
</p>
<p> Screen reader text for ellipsis ("More")</p>
</div>
</div>
</div>
);
// Usage Guidelines section
const usageGuidelinesContent = (
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Usage</h3>
<p className="text-muted-foreground mb-4 text-sm">
Breadcrumbs provide navigation context and help users understand their
location within a site hierarchy.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@kit/ui/breadcrumb';
function Navigation() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Documentation</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Current Page</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">With Custom Separator</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { SlashIcon } from 'lucide-react';
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<SlashIcon className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Current Page</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">With Ellipsis</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { BreadcrumbEllipsis } from '@kit/ui/breadcrumb';
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Best Practices</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Structure</h4>
<p>
Always use BreadcrumbPage for the current page (non-clickable)
</p>
<p> Use BreadcrumbLink for navigable pages</p>
<p> Include separators between all items</p>
<p> Consider using ellipsis for deep hierarchies (4+ levels)</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Content</h4>
<p> Keep labels concise but descriptive</p>
<p> Match labels with actual page titles</p>
<p> Start with the highest level (usually "Home")</p>
<p> End with the current page</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Accessibility</h4>
<p> Always include aria-label="breadcrumb" on the nav</p>
<p> Use aria-current="page" on the current page</p>
<p> Ensure sufficient color contrast for links</p>
<p> Test with keyboard navigation</p>
</div>
</div>
</div>
</div>
);
const generateCode = () => {
const items = getDisplayedItems();
const importComponents = ['Breadcrumb', 'BreadcrumbList', 'BreadcrumbItem'];
const hasLinks = items.some(
(item) => !item.ellipsis && item.id !== items[items.length - 1].id,
);
const hasEllipsis = items.some((item) => item.ellipsis);
if (hasLinks) importComponents.push('BreadcrumbLink');
if (hasEllipsis) importComponents.push('BreadcrumbEllipsis');
importComponents.push('BreadcrumbPage', 'BreadcrumbSeparator');
const importStatement = generateImportStatement(
importComponents,
'@kit/ui/breadcrumb',
);
let separatorImport = '';
let separatorComponent = '';
if (controls.separator === 'slash') {
separatorImport = "\nimport { SlashIcon } from 'lucide-react';";
separatorComponent =
'\n <BreadcrumbSeparator>\n <SlashIcon className="h-4 w-4" />\n </BreadcrumbSeparator>';
} else if (controls.separator === 'custom') {
separatorComponent =
'\n <BreadcrumbSeparator>\n <span className="text-muted-foreground">→</span>\n </BreadcrumbSeparator>';
} else {
separatorComponent = '\n <BreadcrumbSeparator />';
}
const breadcrumbItems = items
.map((item, index) => {
const isLast = index === items.length - 1;
const separator = isLast ? '' : separatorComponent;
if (item.ellipsis) {
return ` <BreadcrumbItem>\n <BreadcrumbEllipsis />\n </BreadcrumbItem>${separator}`;
}
if (isLast) {
return ` <BreadcrumbItem>\n <BreadcrumbPage>${item.label}</BreadcrumbPage>\n </BreadcrumbItem>`;
}
return ` <BreadcrumbItem>\n <BreadcrumbLink href="${item.href}">${item.label}</BreadcrumbLink>\n </BreadcrumbItem>${separator}`;
})
.join('\n');
const breadcrumbComponent = `<Breadcrumb>\n <BreadcrumbList>\n${breadcrumbItems}\n </BreadcrumbList>\n</Breadcrumb>`;
return `${importStatement}${separatorImport}\n\n${breadcrumbComponent}`;
};
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive Breadcrumb"
previewDescription="Navigation showing hierarchical path"
controlsTitle="Breadcrumb Configuration"
controlsDescription="Customize breadcrumb appearance"
generatedCode={generateCode()}
examples={examplesContent}
apiReference={apiReferenceContent}
usageGuidelines={usageGuidelinesContent}
/>
);
}
export { BreadcrumbStory };

View File

@@ -0,0 +1,501 @@
'use client';
import { Download, Loader2, Mail, Plus, Settings } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface ButtonControls {
variant:
| 'default'
| 'destructive'
| 'outline'
| 'secondary'
| 'ghost'
| 'link';
size: 'default' | 'sm' | 'lg' | 'icon';
disabled: boolean;
loading: boolean;
withIcon: boolean;
fullWidth: boolean;
asChild: boolean;
}
const variantOptions = [
{ value: 'default', label: 'Default', description: 'Primary action button' },
{
value: 'destructive',
label: 'Destructive',
description: 'For dangerous actions',
},
{ value: 'outline', label: 'Outline', description: 'Secondary actions' },
{
value: 'secondary',
label: 'Secondary',
description: 'Less prominent actions',
},
{ value: 'ghost', label: 'Ghost', description: 'Minimal styling' },
{ value: 'link', label: 'Link', description: 'Looks like a link' },
] as const;
const sizeOptions = [
{ value: 'sm', label: 'Small', description: '32px height' },
{ value: 'default', label: 'Default', description: '40px height' },
{ value: 'lg', label: 'Large', description: '48px height' },
{ value: 'icon', label: 'Icon', description: 'Square button for icons' },
] as const;
export function ButtonStory() {
const { controls, updateControl } = useStoryControls<ButtonControls>({
variant: 'default',
size: 'default',
disabled: false,
loading: false,
withIcon: false,
fullWidth: false,
asChild: false,
});
const generateCode = () => {
const propsString = generatePropsString(
{
variant: controls.variant,
size: controls.size,
disabled: controls.disabled,
asChild: controls.asChild,
className: controls.fullWidth ? 'w-full' : '',
},
{
variant: 'default',
size: 'default',
disabled: false,
asChild: false,
className: '',
},
);
let code = `<Button${propsString}>`;
if (controls.loading) {
code += `\n <Loader2 className="mr-2 h-4 w-4 animate-spin" />`;
} else if (controls.withIcon && controls.size !== 'icon') {
code += `\n <Plus className="mr-2 h-4 w-4" />`;
}
if (controls.size === 'icon') {
code += `\n <Plus className="h-4 w-4" />`;
} else {
const buttonText = controls.loading ? 'Loading...' : 'Button';
if (controls.loading || controls.withIcon) {
code += `\n ${buttonText}`;
} else {
code += buttonText;
}
}
code += `\n</Button>`;
return code;
};
const renderPreview = () => (
<Button
variant={controls.variant}
size={controls.size}
disabled={controls.disabled || controls.loading}
className={cn(controls.fullWidth && 'w-full')}
>
{controls.loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</>
) : (
<>
{controls.withIcon && controls.size !== 'icon' && (
<Plus className="mr-2 h-4 w-4" />
)}
{controls.size === 'icon' ? <Plus className="h-4 w-4" /> : 'Button'}
</>
)}
</Button>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="variant">Variant</Label>
<SimpleStorySelect
value={controls.variant}
onValueChange={(value) => updateControl('variant', value)}
options={variantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="disabled">Disabled</Label>
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) => updateControl('disabled', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="loading">Loading</Label>
<Switch
id="loading"
checked={controls.loading}
onCheckedChange={(checked) => updateControl('loading', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="withIcon">With Icon</Label>
<Switch
id="withIcon"
checked={controls.withIcon}
disabled={controls.size === 'icon'}
onCheckedChange={(checked) => updateControl('withIcon', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="fullWidth">Full Width</Label>
<Switch
id="fullWidth"
checked={controls.fullWidth}
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="asChild">As Child</Label>
<Switch
id="asChild"
checked={controls.asChild}
onCheckedChange={(checked) => updateControl('asChild', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Button Variants</CardTitle>
<CardDescription>
Different button styles for various use cases
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Button Sizes</CardTitle>
<CardDescription>
Different button sizes for various contexts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Button size="sm">Small</Button>
<Button>Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<Settings className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Button States</CardTitle>
<CardDescription>Loading and disabled states</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Button>
<Download className="mr-2 h-4 w-4" />
Download
</Button>
<Button disabled>
<Mail className="mr-2 h-4 w-4" />
Disabled
</Button>
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Complex Button Layouts</CardTitle>
<CardDescription>Advanced button configurations</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<Button className="w-full" size="lg">
<Plus className="mr-2 h-4 w-4" />
Full Width Button
</Button>
<div className="flex gap-2">
<Button className="flex-1">Primary</Button>
<Button variant="outline" className="flex-1">
Secondary
</Button>
</div>
<div className="flex items-center gap-2">
<Button size="icon" variant="outline">
<Settings className="h-4 w-4" />
</Button>
<Button className="flex-1">
<Mail className="mr-2 h-4 w-4" />
Send Email
</Button>
<Badge variant="secondary" className="px-2 py-1">
New
</Badge>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Button Component</CardTitle>
<CardDescription>
Complete API reference for Button component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Button</h4>
<p className="text-muted-foreground mb-3 text-sm">
A clickable element that triggers an action or event.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">variant</td>
<td className="p-3 font-mono text-sm">
'default' | 'destructive' | 'outline' | 'secondary' |
'ghost' | 'link'
</td>
<td className="p-3 font-mono text-sm">'default'</td>
<td className="p-3">Visual style variant</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">size</td>
<td className="p-3 font-mono text-sm">
'default' | 'sm' | 'lg' | 'icon'
</td>
<td className="p-3 font-mono text-sm">'default'</td>
<td className="p-3">Button size</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">disabled</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Disable the button</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Button content</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Buttons</CardTitle>
<CardDescription>Best practices for button usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Buttons For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Triggering actions (submit, save, delete)</li>
<li> Navigation to different pages or sections</li>
<li> Opening modals or dialogs</li>
<li> Starting processes or workflows</li>
<li> Toggling states or settings</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Buttons For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Displaying static content</li>
<li> Non-interactive decorative elements</li>
<li> Links to external websites (use links instead)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Button Hierarchy</CardTitle>
<CardDescription>Using button variants effectively</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div>
<div className="mb-2 flex items-center gap-2">
<Button size="sm">Primary</Button>
<h4 className="text-sm font-semibold">Default (Primary)</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Main actions, form submissions, primary CTAs
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Button variant="outline" size="sm">
Secondary
</Button>
<h4 className="text-sm font-semibold">Outline (Secondary)</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Secondary actions, cancel buttons, alternative options
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Button variant="ghost" size="sm">
Tertiary
</Button>
<h4 className="text-sm font-semibold">Ghost (Tertiary)</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Subtle actions, toolbar buttons, optional actions
</p>
</div>
<div>
<div className="mb-2 flex items-center gap-2">
<Button variant="destructive" size="sm">
Destructive
</Button>
<h4 className="text-sm font-semibold">Destructive</h4>
</div>
<p className="text-muted-foreground ml-16 text-sm">
Delete actions, dangerous operations, permanent changes
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility</CardTitle>
<CardDescription>Making buttons accessible</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
<p className="text-muted-foreground text-sm">
Buttons are focusable and can be activated with Enter or Space
keys.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Screen Readers</h4>
<p className="text-muted-foreground text-sm">
Use descriptive button text. Avoid generic text like "Click here"
or "Read more".
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Loading States</h4>
<p className="text-muted-foreground text-sm">
When buttons show loading states, ensure they communicate the
current status to screen readers.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,634 @@
'use client';
import { useState } from 'react';
import { CalendarIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Calendar } from '@kit/ui/calendar';
import { Card, CardContent } from '@kit/ui/card';
import { Popover, PopoverContent, PopoverTrigger } from '@kit/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface CalendarStoryControls {
mode: 'single' | 'multiple' | 'range';
captionLayout: 'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years';
showOutsideDays: boolean;
showWeekNumber: boolean;
numberOfMonths: number;
disabled: boolean;
buttonVariant: 'ghost' | 'outline' | 'secondary';
}
export default function CalendarStory() {
const [controls, setControls] = useState<CalendarStoryControls>({
mode: 'single',
captionLayout: 'label',
showOutsideDays: true,
showWeekNumber: false,
numberOfMonths: 1,
disabled: false,
buttonVariant: 'ghost',
});
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
new Date(),
);
const [selectedDates, setSelectedDates] = useState<Date[]>([]);
const [selectedRange, setSelectedRange] = useState<{
from?: Date;
to?: Date;
}>({});
const handleDateChange = (date: any) => {
if (controls.mode === 'single') {
setSelectedDate(date);
} else if (controls.mode === 'multiple') {
setSelectedDates(date || []);
} else if (controls.mode === 'range') {
setSelectedRange(date || {});
}
};
const getSelectedValue = () => {
switch (controls.mode) {
case 'single':
return selectedDate;
case 'multiple':
return selectedDates;
case 'range':
return selectedRange;
default:
return undefined;
}
};
// Controls section
const controlsContent = (
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">Mode</label>
<Select
value={controls.mode}
onValueChange={(value: CalendarStoryControls['mode']) =>
setControls((prev) => ({ ...prev, mode: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="single">Single Date</SelectItem>
<SelectItem value="multiple">Multiple Dates</SelectItem>
<SelectItem value="range">Date Range</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Caption Layout</label>
<Select
value={controls.captionLayout}
onValueChange={(value: CalendarStoryControls['captionLayout']) =>
setControls((prev) => ({ ...prev, captionLayout: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="label">Label</SelectItem>
<SelectItem value="dropdown">Dropdown</SelectItem>
<SelectItem value="dropdown-months">Dropdown Months</SelectItem>
<SelectItem value="dropdown-years">Dropdown Years</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Button Variant</label>
<Select
value={controls.buttonVariant}
onValueChange={(value: CalendarStoryControls['buttonVariant']) =>
setControls((prev) => ({ ...prev, buttonVariant: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ghost">Ghost</SelectItem>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="secondary">Secondary</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">
Number of Months
</label>
<Select
value={controls.numberOfMonths.toString()}
onValueChange={(value: string) =>
setControls((prev) => ({
...prev,
numberOfMonths: parseInt(value),
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 Month</SelectItem>
<SelectItem value="2">2 Months</SelectItem>
<SelectItem value="3">3 Months</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<Switch
id="showOutsideDays"
checked={controls.showOutsideDays}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showOutsideDays: checked }))
}
/>
<label htmlFor="showOutsideDays" className="text-sm">
Show Outside Days
</label>
</div>
<div className="flex items-center gap-2">
<Switch
id="showWeekNumber"
checked={controls.showWeekNumber}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showWeekNumber: checked }))
}
/>
<label htmlFor="showWeekNumber" className="text-sm">
Show Week Number
</label>
</div>
<div className="flex items-center gap-2">
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, disabled: checked }))
}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
</div>
</div>
);
// Preview section
const previewContent = (
<div className="flex justify-center p-6">
<Calendar
mode={controls.mode}
selected={getSelectedValue()}
onSelect={handleDateChange}
captionLayout={controls.captionLayout}
showOutsideDays={controls.showOutsideDays}
showWeekNumber={controls.showWeekNumber}
numberOfMonths={controls.numberOfMonths}
disabled={controls.disabled}
buttonVariant={controls.buttonVariant}
className="rounded-md border"
/>
</div>
);
// Examples section
const examplesContent = (
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Single Date Selection</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<Calendar
mode="single"
selected={new Date()}
onSelect={() => {}}
className="rounded-md border"
/>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Date Range Selection</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<Calendar
mode="range"
selected={{
from: new Date(),
to: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
}}
onSelect={() => {}}
className="rounded-md border"
/>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Multiple Months</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<Calendar
mode="single"
selected={new Date()}
onSelect={() => {}}
numberOfMonths={2}
className="rounded-md border"
/>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Date Picker in Popover</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="justify-start">
<CalendarIcon className="mr-2 h-4 w-4" />
Pick a date
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={new Date()}
onSelect={() => {}}
initialFocus
/>
</PopoverContent>
</Popover>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">With Week Numbers</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<Calendar
mode="single"
selected={new Date()}
onSelect={() => {}}
showWeekNumber
className="rounded-md border"
/>
</CardContent>
</Card>
</div>
</div>
);
// API Reference section
const apiReferenceContent = (
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Calendar Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">mode</td>
<td className="p-2 font-mono">
'single' | 'multiple' | 'range'
</td>
<td className="p-2">'single'</td>
<td className="p-2">Selection mode</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">selected</td>
<td className="p-2 font-mono">
Date | Date[] | {'{'} from?: Date, to?: Date {'}'}
</td>
<td className="p-2">-</td>
<td className="p-2">Selected date(s)</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">onSelect</td>
<td className="p-2 font-mono">function</td>
<td className="p-2">-</td>
<td className="p-2">Date selection handler</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">captionLayout</td>
<td className="p-2 font-mono">
'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years'
</td>
<td className="p-2">'label'</td>
<td className="p-2">Month/year caption style</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">numberOfMonths</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">1</td>
<td className="p-2">Number of months to display</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">showOutsideDays</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">true</td>
<td className="p-2">Show days outside current month</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">showWeekNumber</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Show week numbers</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">disabled</td>
<td className="p-2 font-mono">boolean | Matcher</td>
<td className="p-2">false</td>
<td className="p-2">Disable dates</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">buttonVariant</td>
<td className="p-2 font-mono">
'ghost' | 'outline' | 'secondary'
</td>
<td className="p-2">'ghost'</td>
<td className="p-2">Date button appearance</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Usage Examples</h3>
<div className="space-y-6">
<div>
<h4 className="mb-2 text-base font-medium">Basic Single Date</h4>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
<code>{`import { Calendar } from '@kit/ui/calendar';
function DatePicker() {
const [date, setDate] = useState<Date>();
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-md border"
/>
);
}`}</code>
</pre>
</div>
</div>
<div>
<h4 className="mb-2 text-base font-medium">Date Range Selection</h4>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
<code>{`import { Calendar } from '@kit/ui/calendar';
function DateRangePicker() {
const [range, setRange] = useState<{from?: Date, to?: Date}>({});
return (
<Calendar
mode="range"
selected={range}
onSelect={setRange}
numberOfMonths={2}
className="rounded-md border"
/>
);
}`}</code>
</pre>
</div>
</div>
<div>
<h4 className="mb-2 text-base font-medium">
Multiple Date Selection
</h4>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
<code>{`import { Calendar } from '@kit/ui/calendar';
function MultiDatePicker() {
const [dates, setDates] = useState<Date[]>([]);
return (
<Calendar
mode="multiple"
selected={dates}
onSelect={setDates}
className="rounded-md border"
/>
);
}`}</code>
</pre>
</div>
</div>
</div>
</div>
</div>
);
// Usage Guidelines section
const usageGuidelinesContent = (
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">When to Use Calendar</h3>
<p className="text-muted-foreground mb-4 text-sm">
Use Calendar component when users need to select dates with visual
context of months and relationships between dates.
</p>
<div className="space-y-2 text-sm">
<p> Date range selection (bookings, reports)</p>
<p> Event scheduling and planning</p>
<p> Birthday or anniversary selection</p>
<p> Multiple date selection for recurring events</p>
<p> When context of surrounding dates is important</p>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Selection Modes</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Single Mode</h4>
<p> Most common use case</p>
<p> Good for birthdays, deadlines, appointments</p>
<p> Simple one-click selection</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Range Mode</h4>
<p> Perfect for booking systems</p>
<p> Hotel reservations, vacation planning</p>
<p> Report date ranges</p>
<p> Shows continuous selection</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Multiple Mode</h4>
<p> Non-continuous date selection</p>
<p> Recurring events or availability</p>
<p> Shift scheduling</p>
<p> Multiple appointment slots</p>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Layout Options</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Caption Layouts</h4>
<p> Label: Simple text display (compact)</p>
<p> Dropdown: Combined month/year selector</p>
<p> Dropdown-months: Month selection dropdown</p>
<p> Dropdown-years: Year selection dropdown</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Multiple Months</h4>
<p> Use 2 months for date ranges</p>
<p> 3+ months for long-term planning</p>
<p> Consider responsive behavior</p>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Accessibility</h3>
<div className="space-y-2 text-sm">
<p> Full keyboard navigation support</p>
<p> Arrow keys to navigate dates</p>
<p> Enter/Space to select dates</p>
<p> Screen reader announcements</p>
<p> Focus management and visible focus indicators</p>
<p> Date announcements when navigating</p>
<p> Supports RTL languages</p>
</div>
</div>
</div>
);
const generateCode = () => {
const propsString = generatePropsString(
{
mode: controls.mode,
selected:
controls.mode === 'single'
? 'date'
: controls.mode === 'multiple'
? 'dates'
: 'range',
onSelect:
controls.mode === 'single'
? 'setDate'
: controls.mode === 'multiple'
? 'setDates'
: 'setRange',
captionLayout:
controls.captionLayout !== 'label'
? controls.captionLayout
: undefined,
showOutsideDays: !controls.showOutsideDays ? false : undefined,
showWeekNumber: controls.showWeekNumber ? true : undefined,
numberOfMonths:
controls.numberOfMonths > 1 ? controls.numberOfMonths : undefined,
disabled: controls.disabled ? true : undefined,
buttonVariant:
controls.buttonVariant !== 'ghost'
? controls.buttonVariant
: undefined,
className: 'rounded-md border',
},
{
mode: 'single',
captionLayout: 'label',
showOutsideDays: true,
showWeekNumber: false,
numberOfMonths: 1,
disabled: false,
buttonVariant: 'ghost',
},
);
const importStatement = generateImportStatement(
['Calendar'],
'@kit/ui/calendar',
);
let stateDeclaration = '';
if (controls.mode === 'single') {
stateDeclaration =
'const [date, setDate] = useState<Date | undefined>();';
} else if (controls.mode === 'multiple') {
stateDeclaration = 'const [dates, setDates] = useState<Date[]>([]);';
} else {
stateDeclaration =
'const [range, setRange] = useState<{from?: Date, to?: Date}>({});';
}
const calendarComponent = `<Calendar${propsString} />`;
return `${importStatement}\n\n${stateDeclaration}\n\n${calendarComponent}`;
};
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive Calendar"
previewDescription="Date picker with multiple selection modes"
controlsTitle="Calendar Configuration"
controlsDescription="Customize calendar appearance and behavior"
generatedCode={generateCode()}
examples={examplesContent}
apiReference={apiReferenceContent}
usageGuidelines={usageGuidelinesContent}
/>
);
}
export { CalendarStory };

View File

@@ -0,0 +1,482 @@
'use client';
import { useState } from 'react';
import {
Calendar,
CreditCard,
FileText,
Settings,
Shield,
Users,
} from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
CardButton,
CardButtonContent,
CardButtonFooter,
CardButtonHeader,
CardButtonTitle,
} from '@kit/ui/card-button';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface CardButtonControls {
showArrow: boolean;
showFooter: boolean;
showBadge: boolean;
clickable: boolean;
}
export function CardButtonStory() {
const { controls, updateControl } = useStoryControls<CardButtonControls>({
showArrow: true,
showFooter: false,
showBadge: false,
clickable: true,
});
const [selectedCard, setSelectedCard] = useState<string | null>(null);
const generateCode = () => {
const propsString = generatePropsString(
{
onClick: controls.clickable ? '() => handleClick()' : undefined,
},
{
onClick: undefined,
},
);
return `<CardButton${propsString}>
<CardButtonHeader displayArrow={${controls.showArrow}}>
<CardButtonTitle>
Card Title
</CardButtonTitle>
</CardButtonHeader>
<CardButtonContent>
<p className="text-sm text-muted-foreground">
Card content goes here...
</p>
</CardButtonContent>
${
controls.showFooter
? `<CardButtonFooter>
<Badge variant="secondary">Footer</Badge>
</CardButtonFooter>`
: ''
}
</CardButton>`;
};
const handleCardClick = (cardName: string) => {
if (controls.clickable) {
setSelectedCard(cardName);
toast.success(`Clicked ${cardName}`);
setTimeout(() => setSelectedCard(null), 1000);
}
};
const renderPreview = () => (
<div className="w-full max-w-sm">
<CardButton
onClick={
controls.clickable ? () => handleCardClick('Preview Card') : undefined
}
className={selectedCard === 'Preview Card' ? 'ring-primary ring-2' : ''}
>
<CardButtonHeader displayArrow={controls.showArrow}>
<div className="flex items-center space-x-2">
<Settings className="text-primary h-5 w-5" />
<CardButtonTitle>Settings</CardButtonTitle>
{controls.showBadge && <Badge variant="secondary">New</Badge>}
</div>
</CardButtonHeader>
<CardButtonContent>
<p className="text-muted-foreground text-sm">
Configure your application settings and preferences.
</p>
</CardButtonContent>
{controls.showFooter && (
<CardButtonFooter>
<Badge variant="outline">Configuration</Badge>
</CardButtonFooter>
)}
</CardButton>
</div>
);
const renderControls = () => (
<>
<div className="flex items-center justify-between">
<Label htmlFor="clickable">Clickable</Label>
<Switch
id="clickable"
checked={controls.clickable}
onCheckedChange={(checked) => updateControl('clickable', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showArrow">Show Arrow</Label>
<Switch
id="showArrow"
checked={controls.showArrow}
onCheckedChange={(checked) => updateControl('showArrow', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showBadge">Show Badge</Label>
<Switch
id="showBadge"
checked={controls.showBadge}
onCheckedChange={(checked) => updateControl('showBadge', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showFooter">Show Footer</Label>
<Switch
id="showFooter"
checked={controls.showFooter}
onCheckedChange={(checked) => updateControl('showFooter', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Feature Cards</CardTitle>
<CardDescription>
Different card button configurations for feature selection
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<CardButton
onClick={() => handleCardClick('Users')}
className={selectedCard === 'Users' ? 'ring-primary ring-2' : ''}
>
<CardButtonHeader>
<div className="flex items-center space-x-2">
<Users className="h-5 w-5 text-blue-500" />
<CardButtonTitle>User Management</CardButtonTitle>
</div>
</CardButtonHeader>
<CardButtonContent>
<p className="text-muted-foreground text-sm">
Manage users, roles, and permissions across your application.
</p>
</CardButtonContent>
</CardButton>
<CardButton
onClick={() => handleCardClick('Billing')}
className={
selectedCard === 'Billing' ? 'ring-primary ring-2' : ''
}
>
<CardButtonHeader>
<div className="flex items-center space-x-2">
<CreditCard className="h-5 w-5 text-green-500" />
<CardButtonTitle>Billing</CardButtonTitle>
</div>
</CardButtonHeader>
<CardButtonContent>
<p className="text-muted-foreground text-sm">
Configure payment methods, invoicing, and subscription plans.
</p>
</CardButtonContent>
</CardButton>
<CardButton
onClick={() => handleCardClick('Security')}
className={
selectedCard === 'Security' ? 'ring-primary ring-2' : ''
}
>
<CardButtonHeader>
<div className="flex items-center space-x-2">
<Shield className="h-5 w-5 text-red-500" />
<CardButtonTitle>Security</CardButtonTitle>
<Badge variant="destructive" className="text-xs">
Important
</Badge>
</div>
</CardButtonHeader>
<CardButtonContent>
<p className="text-muted-foreground text-sm">
Set up two-factor authentication and security policies.
</p>
</CardButtonContent>
</CardButton>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Cards with Footers</CardTitle>
<CardDescription>
Card buttons with footer content and status indicators
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2">
<CardButton
onClick={() => handleCardClick('Reports')}
className={
selectedCard === 'Reports' ? 'ring-primary ring-2' : ''
}
>
<CardButtonHeader>
<div className="flex items-center space-x-2">
<FileText className="h-5 w-5 text-purple-500" />
<CardButtonTitle>Reports</CardButtonTitle>
</div>
</CardButtonHeader>
<CardButtonContent>
<p className="text-muted-foreground text-sm">
Generate and view detailed analytics reports.
</p>
</CardButtonContent>
<CardButtonFooter>
<Badge variant="default">Available</Badge>
</CardButtonFooter>
</CardButton>
<CardButton
onClick={() => handleCardClick('Calendar')}
className={
selectedCard === 'Calendar' ? 'ring-primary ring-2' : ''
}
>
<CardButtonHeader>
<div className="flex items-center space-x-2">
<Calendar className="h-5 w-5 text-orange-500" />
<CardButtonTitle>Calendar</CardButtonTitle>
</div>
</CardButtonHeader>
<CardButtonContent>
<p className="text-muted-foreground text-sm">
Schedule meetings and manage appointments.
</p>
</CardButtonContent>
<CardButtonFooter>
<Badge variant="secondary">Coming Soon</Badge>
</CardButtonFooter>
</CardButton>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>CardButton Components</CardTitle>
<CardDescription>
Complete API reference for CardButton component family
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">CardButton</h4>
<p className="text-muted-foreground mb-3 text-sm">
The main card button container component.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">asChild</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Render as child element</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">onClick</td>
<td className="p-3 font-mono text-sm">function</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Click handler function</td>
</tr>
</tbody>
</table>
</div>
</div>
<Separator />
<div>
<h4 className="mb-3 text-lg font-semibold">CardButtonHeader</h4>
<p className="text-muted-foreground mb-3 text-sm">
Header section with optional arrow indicator.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 font-mono text-sm">displayArrow</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">true</td>
<td className="p-3">Show chevron right arrow</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use CardButton</CardTitle>
<CardDescription>Best practices for card buttons</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use CardButton For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Feature selection and configuration options</li>
<li> Dashboard navigation cards</li>
<li> Settings and preference categories</li>
<li> Action cards with rich content</li>
<li> Onboarding step selection</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Use Regular Buttons For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Simple actions without additional context</li>
<li> Form submissions</li>
<li> Primary/secondary actions in dialogs</li>
<li> Toolbar actions</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Design Guidelines</CardTitle>
<CardDescription>
Creating effective card button layouts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Visual Hierarchy</h4>
<p className="text-muted-foreground text-sm">
Use icons, colors, and typography to create clear visual
distinction.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Content Structure</h4>
<p className="text-muted-foreground text-sm">
Keep titles concise, provide meaningful descriptions, use footers
for status.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Interactive States</h4>
<p className="text-muted-foreground text-sm">
Provide clear hover, active, and selected state feedback.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Layout Patterns</CardTitle>
<CardDescription>Common CardButton arrangements</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Grid Layout</h4>
<p className="text-muted-foreground text-sm">
Use CSS Grid for equal-height cards in responsive layouts.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Vertical Stack</h4>
<p className="text-muted-foreground text-sm">
Stack cards vertically for settings pages or step-by-step flows.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Mixed Sizes</h4>
<p className="text-muted-foreground text-sm">
Vary card sizes based on content importance and hierarchy.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,619 @@
'use client';
import { Eye, Heart, MoreHorizontal, Star, User } from 'lucide-react';
import { Avatar, AvatarFallback } from '@kit/ui/avatar';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Progress } from '@kit/ui/progress';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface CardControls {
showHeader: boolean;
showFooter: boolean;
headerTitle: string;
headerDescription: string;
footerContent: 'buttons' | 'text' | 'none';
variant: 'default' | 'stats' | 'profile' | 'feature';
className: string;
padding: 'default' | 'sm' | 'lg' | 'none';
elevation: 'default' | 'sm' | 'lg' | 'none';
interactive: boolean;
}
const variantOptions = [
{ value: 'default', label: 'Default', description: 'Standard card layout' },
{
value: 'stats',
label: 'Stats',
description: 'Card optimized for statistics',
},
{ value: 'profile', label: 'Profile', description: 'Card for user profiles' },
{
value: 'feature',
label: 'Feature',
description: 'Card for features/products',
},
] as const;
const paddingOptions = [
{ value: 'none', label: 'None', description: 'No padding' },
{ value: 'sm', label: 'Small', description: '12px padding' },
{ value: 'default', label: 'Default', description: '24px padding' },
{ value: 'lg', label: 'Large', description: '32px padding' },
] as const;
const elevationOptions = [
{ value: 'none', label: 'None', description: 'No shadow' },
{ value: 'sm', label: 'Small', description: 'Subtle shadow' },
{ value: 'default', label: 'Default', description: 'Standard shadow' },
{ value: 'lg', label: 'Large', description: 'Prominent shadow' },
] as const;
const footerContentOptions = [
{ value: 'none', label: 'None', description: 'No footer content' },
{ value: 'text', label: 'Text', description: 'Simple text footer' },
{ value: 'buttons', label: 'Buttons', description: 'Action buttons' },
] as const;
export function CardStory() {
const { controls, updateControl } = useStoryControls<CardControls>({
showHeader: true,
showFooter: true,
headerTitle: 'Card Title',
headerDescription: 'Card description goes here',
footerContent: 'buttons',
variant: 'default',
className: '',
padding: 'default',
elevation: 'default',
interactive: false,
});
const generateCode = () => {
const cardClassName = cn(
controls.className,
controls.padding === 'none' && '[&>*]:p-0',
controls.padding === 'sm' && '[&>*]:p-3',
controls.padding === 'lg' && '[&>*]:p-8',
controls.elevation === 'none' && 'shadow-none',
controls.elevation === 'sm' && 'shadow-sm',
controls.elevation === 'lg' && 'shadow-lg',
controls.interactive &&
'cursor-pointer transition-shadow hover:shadow-md',
);
const propsString = generatePropsString(
{
className: cardClassName,
},
{
className: '',
},
);
let code = `<Card${propsString}>`;
if (controls.showHeader) {
code += `\n <CardHeader>`;
code += `\n <CardTitle>${controls.headerTitle}</CardTitle>`;
if (controls.headerDescription) {
code += `\n <CardDescription>${controls.headerDescription}</CardDescription>`;
}
code += `\n </CardHeader>`;
}
code += `\n <CardContent>`;
if (controls.variant === 'stats') {
code += `\n <div className="flex items-center justify-between">`;
code += `\n <div>`;
code += `\n <p className="text-2xl font-bold">1,234</p>`;
code += `\n <p className="text-sm text-muted-foreground">Total Users</p>`;
code += `\n </div>`;
code += `\n <User className="h-8 w-8 text-muted-foreground" />`;
code += `\n </div>`;
} else if (controls.variant === 'profile') {
code += `\n <div className="flex items-center gap-4">`;
code += `\n <Avatar>`;
code += `\n <AvatarImage src="/placeholder.jpg" />`;
code += `\n <AvatarFallback>JD</AvatarFallback>`;
code += `\n </Avatar>`;
code += `\n <div>`;
code += `\n <h3 className="font-semibold">John Doe</h3>`;
code += `\n <p className="text-sm text-muted-foreground">Software Developer</p>`;
code += `\n </div>`;
code += `\n </div>`;
} else if (controls.variant === 'feature') {
code += `\n <div className="space-y-2">`;
code += `\n <Badge variant="secondary">New</Badge>`;
code += `\n <h3 className="font-semibold">Amazing Feature</h3>`;
code += `\n <p className="text-sm text-muted-foreground">This feature will revolutionize your workflow.</p>`;
code += `\n </div>`;
} else {
code += `\n <p>Your content here</p>`;
}
code += `\n </CardContent>`;
if (controls.showFooter && controls.footerContent !== 'none') {
code += `\n <CardFooter>`;
if (controls.footerContent === 'buttons') {
code += `\n <div className="flex gap-2">`;
code += `\n <Button size="sm">Primary</Button>`;
code += `\n <Button variant="outline" size="sm">Secondary</Button>`;
code += `\n </div>`;
} else {
code += `\n <p className="text-sm text-muted-foreground">Footer text</p>`;
}
code += `\n </CardFooter>`;
}
code += `\n</Card>`;
return code;
};
const renderPreview = () => {
const cardClassName = cn(
controls.className,
controls.padding === 'none' && '[&>*]:p-0',
controls.padding === 'sm' && '[&>*]:p-3',
controls.padding === 'lg' && '[&>*]:p-8',
controls.elevation === 'none' && 'shadow-none',
controls.elevation === 'sm' && 'shadow-sm',
controls.elevation === 'lg' && 'shadow-lg',
controls.interactive &&
'cursor-pointer transition-shadow hover:shadow-md',
);
return (
<Card className={cardClassName} style={{ maxWidth: '400px' }}>
{controls.showHeader && (
<CardHeader>
<CardTitle>{controls.headerTitle}</CardTitle>
{controls.headerDescription && (
<CardDescription>{controls.headerDescription}</CardDescription>
)}
</CardHeader>
)}
<CardContent>
{controls.variant === 'stats' && (
<div className="flex items-center justify-between">
<div>
<p className="text-2xl font-bold">1,234</p>
<p className="text-muted-foreground text-sm">Total Users</p>
</div>
<User className="text-muted-foreground h-8 w-8" />
</div>
)}
{controls.variant === 'profile' && (
<div className="flex items-center gap-4">
<Avatar>
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<div>
<h3 className="font-semibold">John Doe</h3>
<p className="text-muted-foreground text-sm">
Software Developer
</p>
</div>
</div>
)}
{controls.variant === 'feature' && (
<div className="space-y-2">
<Badge variant="secondary">New</Badge>
<h3 className="font-semibold">Amazing Feature</h3>
<p className="text-muted-foreground text-sm">
This feature will revolutionize your workflow.
</p>
</div>
)}
{controls.variant === 'default' && <p>Your content here</p>}
</CardContent>
{controls.showFooter && controls.footerContent !== 'none' && (
<CardFooter>
{controls.footerContent === 'buttons' ? (
<div className="flex gap-2">
<Button size="sm">Primary</Button>
<Button variant="outline" size="sm">
Secondary
</Button>
</div>
) : (
<p className="text-muted-foreground text-sm">Footer text</p>
)}
</CardFooter>
)}
</Card>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="variant">Variant</Label>
<SimpleStorySelect
value={controls.variant}
onValueChange={(value) => updateControl('variant', value)}
options={variantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="padding">Padding</Label>
<SimpleStorySelect
value={controls.padding}
onValueChange={(value) => updateControl('padding', value)}
options={paddingOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="elevation">Elevation</Label>
<SimpleStorySelect
value={controls.elevation}
onValueChange={(value) => updateControl('elevation', value)}
options={elevationOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="showHeader">Show Header</Label>
<Switch
id="showHeader"
checked={controls.showHeader}
onCheckedChange={(checked) => updateControl('showHeader', checked)}
/>
</div>
{controls.showHeader && (
<>
<div className="space-y-2">
<Label htmlFor="headerTitle">Header Title</Label>
<Input
id="headerTitle"
value={controls.headerTitle}
onChange={(e) => updateControl('headerTitle', e.target.value)}
placeholder="Card title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="headerDescription">Header Description</Label>
<Textarea
id="headerDescription"
value={controls.headerDescription}
onChange={(e) =>
updateControl('headerDescription', e.target.value)
}
placeholder="Card description"
rows={2}
/>
</div>
</>
)}
<div className="flex items-center justify-between">
<Label htmlFor="showFooter">Show Footer</Label>
<Switch
id="showFooter"
checked={controls.showFooter}
onCheckedChange={(checked) => updateControl('showFooter', checked)}
/>
</div>
{controls.showFooter && (
<div className="space-y-2">
<Label htmlFor="footerContent">Footer Content</Label>
<SimpleStorySelect
value={controls.footerContent}
onValueChange={(value) => updateControl('footerContent', value)}
options={footerContentOptions}
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="interactive">Interactive</Label>
<Switch
id="interactive"
checked={controls.interactive}
onCheckedChange={(checked) => updateControl('interactive', checked)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="className">Custom Classes</Label>
<Input
id="className"
value={controls.className}
onChange={(e) => updateControl('className', e.target.value)}
placeholder="e.g. border-2 bg-accent"
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<div>
<h3 className="mb-4 text-lg font-semibold">Card Variants</h3>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Default Card</CardTitle>
<CardDescription>
Basic card layout with header and content
</CardDescription>
</CardHeader>
<CardContent>
<p>
This is a standard card with header, content, and footer
sections.
</p>
</CardContent>
<CardFooter>
<Button size="sm">Action</Button>
</CardFooter>
</Card>
<Card>
<CardContent>
<div className="flex items-center justify-between">
<div>
<p className="text-2xl font-bold">2,847</p>
<p className="text-muted-foreground text-sm">Active Users</p>
</div>
<User className="text-muted-foreground h-8 w-8" />
</div>
<div className="mt-4">
<Progress value={75} className="h-2" />
<p className="text-muted-foreground mt-2 text-xs">
75% of goal
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="flex items-center gap-4">
<Avatar>
<AvatarFallback>SA</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="font-semibold">Sarah Anderson</h3>
<p className="text-muted-foreground text-sm">
Product Manager
</p>
<div className="mt-2 flex gap-1">
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
</div>
</div>
<Button size="icon" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Badge variant="secondary">Featured</Badge>
<Heart className="text-muted-foreground h-4 w-4" />
</div>
<h3 className="font-semibold">Advanced Analytics</h3>
<p className="text-muted-foreground text-sm">
Get detailed insights into your application performance with
our advanced analytics dashboard.
</p>
<div className="flex items-center gap-2">
<Eye className="text-muted-foreground h-4 w-4" />
<span className="text-muted-foreground text-sm">
1.2k views
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
const renderApiReference = () => (
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Card</h4>
<p className="text-muted-foreground mb-3 text-sm">
Container component for grouping related content with optional header
and footer.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Card content</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">CardHeader</h4>
<p className="text-muted-foreground mb-3 text-sm">
Optional header section for the card, typically containing title and
description.
</p>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">CardTitle</h4>
<p className="text-muted-foreground mb-3 text-sm">
Main heading for the card header.
</p>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">CardDescription</h4>
<p className="text-muted-foreground mb-3 text-sm">
Descriptive text that appears below the card title.
</p>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">CardContent</h4>
<p className="text-muted-foreground mb-3 text-sm">
Main content area of the card.
</p>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">CardFooter</h4>
<p className="text-muted-foreground mb-3 text-sm">
Optional footer section, typically containing actions or additional
information.
</p>
</div>
</div>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<div>
<h4 className="mb-3 text-lg font-semibold">When to Use Cards</h4>
<div className="space-y-4">
<div className="space-y-2">
<h5 className="text-sm font-semibold text-green-700">
Use Cards For
</h5>
<ul className="ml-4 space-y-1 text-sm">
<li> Grouping related information and actions</li>
<li> Displaying content that needs to stand out</li>
<li> Creating scannable layouts with distinct sections</li>
<li> Product listings, user profiles, or feature highlights</li>
<li> Dashboard widgets and statistics</li>
</ul>
</div>
<div className="space-y-2">
<h5 className="text-sm font-semibold text-red-700">
Avoid Cards For
</h5>
<ul className="ml-4 space-y-1 text-sm">
<li> Single pieces of text or data</li>
<li> Navigation menus or button groups</li>
<li> Content that flows naturally together</li>
<li> Overly complex or cluttered information</li>
</ul>
</div>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">
Card Structure Best Practices
</h4>
<div className="space-y-3">
<div>
<h5 className="text-sm font-semibold">Header</h5>
<p className="text-muted-foreground text-sm">
Keep titles concise and descriptive. Use descriptions for
additional context when needed.
</p>
</div>
<div>
<h5 className="text-sm font-semibold">Content</h5>
<p className="text-muted-foreground text-sm">
Focus on the most important information. Use visual hierarchy to
guide the user's attention.
</p>
</div>
<div>
<h5 className="text-sm font-semibold">Footer</h5>
<p className="text-muted-foreground text-sm">
Include primary actions or supplementary information. Limit to 1-2
primary actions.
</p>
</div>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">Accessibility</h4>
<div className="space-y-2">
<div>
<h5 className="text-sm font-semibold">Semantic Structure</h5>
<p className="text-muted-foreground text-sm">
Use proper heading hierarchy (h1-h6) for card titles and sections.
</p>
</div>
<div>
<h5 className="text-sm font-semibold">Interactive Cards</h5>
<p className="text-muted-foreground text-sm">
If the entire card is clickable, ensure it has proper focus states
and keyboard navigation support.
</p>
</div>
</div>
</div>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,688 @@
'use client';
import { useState } from 'react';
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
Line,
LineChart,
Pie,
PieChart,
RadialBar,
RadialBarChart,
XAxis,
YAxis,
} from 'recharts';
import { Badge } from '@kit/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from '@kit/ui/chart';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
const chartData = [
{ month: 'Jan', desktop: 186, mobile: 80, tablet: 45 },
{ month: 'Feb', desktop: 305, mobile: 200, tablet: 88 },
{ month: 'Mar', desktop: 237, mobile: 120, tablet: 67 },
{ month: 'Apr', desktop: 73, mobile: 190, tablet: 55 },
{ month: 'May', desktop: 209, mobile: 130, tablet: 78 },
{ month: 'Jun', desktop: 214, mobile: 140, tablet: 82 },
{ month: 'Jul', desktop: 178, mobile: 160, tablet: 91 },
{ month: 'Aug', desktop: 189, mobile: 180, tablet: 105 },
{ month: 'Sep', desktop: 239, mobile: 220, tablet: 123 },
{ month: 'Oct', desktop: 278, mobile: 260, tablet: 145 },
{ month: 'Nov', desktop: 349, mobile: 290, tablet: 167 },
{ month: 'Dec', desktop: 418, mobile: 340, tablet: 189 },
];
const pieData = [
{ name: 'Desktop', value: 400, fill: 'var(--color-desktop)' },
{ name: 'Mobile', value: 300, fill: 'var(--color-mobile)' },
{ name: 'Tablet', value: 200, fill: 'var(--color-tablet)' },
];
const radialData = [
{ browser: 'Chrome', users: 275, fill: 'var(--color-chrome)' },
{ browser: 'Firefox', users: 200, fill: 'var(--color-firefox)' },
{ browser: 'Safari', users: 187, fill: 'var(--color-safari)' },
{ browser: 'Edge', users: 173, fill: 'var(--color-edge)' },
];
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
tablet: {
label: 'Tablet',
color: 'hsl(var(--chart-3))',
},
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
},
firefox: {
label: 'Firefox',
color: 'hsl(var(--chart-2))',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-3))',
},
edge: {
label: 'Edge',
color: 'hsl(var(--chart-4))',
},
} as const;
interface ChartStoryControls {
chartType: 'line' | 'area' | 'bar' | 'pie' | 'radial';
showTooltip: boolean;
showLegend: boolean;
showGrid: boolean;
}
export default function ChartStory() {
const [controls, setControls] = useState<ChartStoryControls>({
chartType: 'line',
showTooltip: true,
showLegend: true,
showGrid: true,
});
const generateCode = () => {
const chartComponents = ['ChartContainer'];
const rechartsComponents = [];
if (controls.showTooltip) {
chartComponents.push('ChartTooltip', 'ChartTooltipContent');
}
if (controls.showLegend) {
chartComponents.push('ChartLegend', 'ChartLegendContent');
}
let chartComponent = '';
let dataKey = 'desktop';
switch (controls.chartType) {
case 'line':
rechartsComponents.push('LineChart', 'Line', 'XAxis', 'YAxis');
if (controls.showGrid) rechartsComponents.push('CartesianGrid');
chartComponent = `<LineChart data={data}>\n ${controls.showGrid ? '<CartesianGrid strokeDasharray="3 3" />\n ' : ''}<XAxis dataKey="month" />\n <YAxis />\n ${controls.showTooltip ? '<ChartTooltip content={<ChartTooltipContent />} />\n ' : ''}${controls.showLegend ? '<ChartLegend content={<ChartLegendContent />} />\n ' : ''}<Line type="monotone" dataKey="${dataKey}" strokeWidth={2} />\n </LineChart>`;
break;
case 'area':
rechartsComponents.push('AreaChart', 'Area', 'XAxis', 'YAxis');
if (controls.showGrid) rechartsComponents.push('CartesianGrid');
chartComponent = `<AreaChart data={data}>\n ${controls.showGrid ? '<CartesianGrid strokeDasharray="3 3" />\n ' : ''}<XAxis dataKey="month" />\n <YAxis />\n ${controls.showTooltip ? '<ChartTooltip content={<ChartTooltipContent />} />\n ' : ''}${controls.showLegend ? '<ChartLegend content={<ChartLegendContent />} />\n ' : ''}<Area type="monotone" dataKey="${dataKey}" stroke="var(--color-${dataKey})" fill="var(--color-${dataKey})" />\n </AreaChart>`;
break;
case 'bar':
rechartsComponents.push('BarChart', 'Bar', 'XAxis', 'YAxis');
if (controls.showGrid) rechartsComponents.push('CartesianGrid');
chartComponent = `<BarChart data={data}>\n ${controls.showGrid ? '<CartesianGrid strokeDasharray="3 3" />\n ' : ''}<XAxis dataKey="month" />\n <YAxis />\n ${controls.showTooltip ? '<ChartTooltip content={<ChartTooltipContent />} />\n ' : ''}${controls.showLegend ? '<ChartLegend content={<ChartLegendContent />} />\n ' : ''}<Bar dataKey="${dataKey}" fill="var(--color-${dataKey})" />\n </BarChart>`;
break;
case 'pie':
rechartsComponents.push('PieChart', 'Pie', 'Cell');
chartComponent = `<PieChart>\n ${controls.showTooltip ? '<ChartTooltip content={<ChartTooltipContent />} />\n ' : ''}${controls.showLegend ? '<ChartLegend content={<ChartLegendContent />} />\n ' : ''}<Pie data={data} cx="50%" cy="50%" outerRadius={80} dataKey="value">\n {data.map((entry, index) => (\n <Cell key={\`cell-\${index}\`} fill={entry.fill} />\n ))}\n </Pie>\n </PieChart>`;
break;
case 'radial':
rechartsComponents.push('RadialBarChart', 'RadialBar');
chartComponent = `<RadialBarChart cx="50%" cy="50%" innerRadius="30%" outerRadius="80%" data={data}>\n ${controls.showTooltip ? '<ChartTooltip content={<ChartTooltipContent />} />\n ' : ''}${controls.showLegend ? '<ChartLegend content={<ChartLegendContent />} />\n ' : ''}<RadialBar dataKey="users" cornerRadius={10} />\n </RadialBarChart>`;
break;
}
const chartImport = generateImportStatement(
chartComponents,
'@kit/ui/chart',
);
const rechartsImport = generateImportStatement(
rechartsComponents,
'recharts',
);
const containerProps = generatePropsString({
config: 'chartConfig',
className: 'h-[300px]',
});
const configCode = `const chartConfig = {\n desktop: {\n label: 'Desktop',\n color: 'hsl(var(--chart-1))',\n },\n mobile: {\n label: 'Mobile',\n color: 'hsl(var(--chart-2))',\n },\n} as const;\n\nconst data = [\n { month: 'Jan', desktop: 186, mobile: 80 },\n { month: 'Feb', desktop: 305, mobile: 200 },\n { month: 'Mar', desktop: 237, mobile: 120 },\n // ... more data\n];`;
const fullExample = `${chartImport}\n${rechartsImport}\n\n${configCode}\n\nfunction Chart() {\n return (\n <ChartContainer${containerProps}>\n ${chartComponent}\n </ChartContainer>\n );\n}`;
return fullExample;
};
const renderChart = () => {
const commonProps = {
data: chartData,
margin: { top: 20, right: 30, left: 20, bottom: 5 },
};
switch (controls.chartType) {
case 'line':
return (
<LineChart {...commonProps}>
{controls.showGrid && <CartesianGrid strokeDasharray="3 3" />}
<XAxis dataKey="month" />
<YAxis />
{controls.showTooltip && (
<ChartTooltip content={<ChartTooltipContent />} />
)}
{controls.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
<Line type="monotone" dataKey="desktop" strokeWidth={2} />
<Line type="monotone" dataKey="mobile" strokeWidth={2} />
<Line type="monotone" dataKey="tablet" strokeWidth={2} />
</LineChart>
);
case 'area':
return (
<AreaChart {...commonProps}>
{controls.showGrid && <CartesianGrid strokeDasharray="3 3" />}
<XAxis dataKey="month" />
<YAxis />
{controls.showTooltip && (
<ChartTooltip content={<ChartTooltipContent />} />
)}
{controls.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
<Area type="monotone" dataKey="desktop" stackId="1" />
<Area type="monotone" dataKey="mobile" stackId="1" />
<Area type="monotone" dataKey="tablet" stackId="1" />
</AreaChart>
);
case 'bar':
return (
<BarChart {...commonProps}>
{controls.showGrid && <CartesianGrid strokeDasharray="3 3" />}
<XAxis dataKey="month" />
<YAxis />
{controls.showTooltip && (
<ChartTooltip content={<ChartTooltipContent />} />
)}
{controls.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
<Bar dataKey="desktop" />
<Bar dataKey="mobile" />
<Bar dataKey="tablet" />
</BarChart>
);
case 'pie':
return (
<PieChart width={400} height={400}>
{controls.showTooltip && (
<ChartTooltip content={<ChartTooltipContent />} />
)}
{controls.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
<Pie
data={pieData}
cx="50%"
cy="50%"
outerRadius={120}
dataKey="value"
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
</PieChart>
);
case 'radial':
return (
<RadialBarChart
width={400}
height={400}
cx="50%"
cy="50%"
innerRadius="10%"
outerRadius="80%"
data={radialData}
>
{controls.showTooltip && (
<ChartTooltip content={<ChartTooltipContent />} />
)}
{controls.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
<RadialBar dataKey="users" cornerRadius={10} />
</RadialBarChart>
);
default:
return null;
}
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>Chart Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Chart Type</label>
<Select
value={controls.chartType}
onValueChange={(value: ChartStoryControls['chartType']) =>
setControls((prev) => ({ ...prev, chartType: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="line">Line Chart</SelectItem>
<SelectItem value="area">Area Chart</SelectItem>
<SelectItem value="bar">Bar Chart</SelectItem>
<SelectItem value="pie">Pie Chart</SelectItem>
<SelectItem value="radial">Radial Bar Chart</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="showTooltip">Show Tooltip</Label>
<Switch
id="showTooltip"
checked={controls.showTooltip}
onCheckedChange={(checked) =>
setControls((prev) => ({
...prev,
showTooltip: checked,
}))
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showLegend">Show Legend</Label>
<Switch
id="showLegend"
checked={controls.showLegend}
onCheckedChange={(checked) =>
setControls((prev) => ({
...prev,
showLegend: checked,
}))
}
/>
</div>
{(controls.chartType === 'line' ||
controls.chartType === 'area' ||
controls.chartType === 'bar') && (
<div className="flex items-center justify-between">
<Label htmlFor="showGrid">Show Grid</Label>
<Switch
id="showGrid"
checked={controls.showGrid}
onCheckedChange={(checked) =>
setControls((prev) => ({
...prev,
showGrid: checked,
}))
}
/>
</div>
)}
</div>
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardContent className="pt-6">
<ChartContainer config={chartConfig}>{renderChart()}</ChartContainer>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive Chart"
previewDescription="Data visualization components built on top of Recharts"
controlsTitle="Configuration"
controlsDescription="Adjust chart type, height, and display options"
generatedCode={generateCode()}
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">
Line Chart with Multiple Data Series
</h3>
<Card>
<CardContent className="pt-6">
<ChartContainer config={chartConfig} className="h-[300px]">
<LineChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Line type="monotone" dataKey="desktop" strokeWidth={2} />
<Line type="monotone" dataKey="mobile" strokeWidth={2} />
<Line type="monotone" dataKey="tablet" strokeWidth={2} />
</LineChart>
</ChartContainer>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Stacked Area Chart</h3>
<Card>
<CardContent className="pt-6">
<ChartContainer config={chartConfig} className="h-[300px]">
<AreaChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Area type="monotone" dataKey="desktop" stackId="1" />
<Area type="monotone" dataKey="mobile" stackId="1" />
<Area type="monotone" dataKey="tablet" stackId="1" />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<h3 className="mb-4 text-lg font-semibold">Pie Chart</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<ChartContainer
config={chartConfig}
className="h-[300px] w-[300px]"
>
<PieChart width={300} height={300}>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Pie
data={pieData}
cx="50%"
cy="50%"
outerRadius={100}
dataKey="value"
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Radial Bar Chart</h3>
<Card>
<CardContent className="flex justify-center pt-6">
<ChartContainer
config={chartConfig}
className="h-[300px] w-[300px]"
>
<RadialBarChart
width={300}
height={300}
cx="50%"
cy="50%"
innerRadius="30%"
outerRadius="80%"
data={radialData}
>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<RadialBar dataKey="users" cornerRadius={10} />
</RadialBarChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">ChartContainer</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">config</td>
<td className="p-2 font-mono">ChartConfig</td>
<td className="p-2">-</td>
<td className="p-2">
Chart configuration object defining colors and labels
</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">children</td>
<td className="p-2 font-mono">ReactNode</td>
<td className="p-2">-</td>
<td className="p-2">Recharts chart components to render</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">className</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Additional CSS classes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">ChartTooltipContent</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">indicator</td>
<td className="p-2 font-mono">'line' | 'dot' | 'dashed'</td>
<td className="p-2">'dot'</td>
<td className="p-2">Visual indicator style</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">hideLabel</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Hide the tooltip label</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">hideIndicator</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Hide the color indicator</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">ChartConfig</h3>
<p className="text-muted-foreground mb-4 text-sm">
Chart configuration object that defines colors, labels, and icons
for data series.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
// ... more data series
} as const;`}
</pre>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Setup</h3>
<p className="text-muted-foreground mb-4 text-sm">
Charts require a configuration object and data to visualize.
Always wrap chart components with ChartContainer.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@kit/ui/chart';
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer } from 'recharts';
const data = [
{ month: 'Jan', desktop: 186 },
{ month: 'Feb', desktop: 305 },
// ... more data
];
const config = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
};
<ChartContainer config={config}>
<LineChart data={data}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Line type="monotone" dataKey="desktop" />
</LineChart>
</ChartContainer>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Chart Types</h3>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">LineChart</Badge>
<Badge variant="secondary">AreaChart</Badge>
<Badge variant="secondary">BarChart</Badge>
<Badge variant="secondary">PieChart</Badge>
<Badge variant="secondary">RadialBarChart</Badge>
<Badge variant="secondary">ScatterChart</Badge>
<Badge variant="secondary">ComposedChart</Badge>
</div>
<p className="text-muted-foreground text-sm">
All Recharts chart types are supported. Import the chart
components from 'recharts' and use them within ChartContainer.
</p>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Responsive Design</h3>
<p className="text-muted-foreground mb-4 text-sm">
Charts automatically adapt to their container size. Use CSS
classes to control chart dimensions.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`<ChartContainer config={config} className="h-[400px] w-full">
<LineChart data={data}>
{/* Chart components */}
</LineChart>
</ChartContainer>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Accessibility</h3>
<div className="space-y-2 text-sm">
<p> Charts include semantic markup for screen readers</p>
<p> Tooltip content is announced when focused</p>
<p> Color combinations meet WCAG contrast requirements</p>
<p>
Data tables can be provided as fallbacks for complex charts
</p>
</div>
</div>
</div>
}
/>
);
}
export { ChartStory };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
'use client';
import { Check, Copy } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { useCopyCode } from '../lib/story-utils';
interface CodeCardProps {
title?: string;
description?: string;
code: string;
language?: 'tsx' | 'jsx' | 'javascript' | 'typescript';
className?: string;
}
export function CodeCard({
title = 'Generated Code',
description = 'Copy and paste this code into your project',
code,
language = 'tsx',
className,
}: CodeCardProps) {
const { copiedCode, copyCode } = useCopyCode();
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</div>
<Button onClick={() => copyCode(code)} size="sm" variant="outline">
{copiedCode ? (
<Check className="mr-2 h-4 w-4" />
) : (
<Copy className="mr-2 h-4 w-4" />
)}
{copiedCode ? 'Copied!' : 'Copy'}
</Button>
</div>
</CardHeader>
<CardContent>
<pre className="bg-muted overflow-x-auto rounded-lg p-4 text-sm">
<code>{code}</code>
</pre>
</CardContent>
</Card>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
'use client';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
interface ControlPanelProps {
title?: string;
description?: string;
children: React.ReactNode;
className?: string;
}
export function ControlPanel({
title = 'Controls',
description = 'Modify props in real-time',
children,
className,
}: ControlPanelProps) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">{children}</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,505 @@
'use client';
import { useState } from 'react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { CookieBanner, useCookieConsent } from '@kit/ui/cookie-banner';
import { Label } from '@kit/ui/label';
import { Switch } from '@kit/ui/switch';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface CookieBannerControls {
showBanner: boolean;
position: 'bottom-left' | 'bottom-center' | 'bottom-right';
}
export function CookieBannerStory() {
const { controls, updateControl } = useStoryControls<CookieBannerControls>({
showBanner: true,
position: 'bottom-left',
});
const [demoConsent, setDemoConsent] = useState<
'unknown' | 'accepted' | 'rejected'
>('unknown');
const cookieConsent = useCookieConsent();
const generateCode = () => {
return `import { CookieBanner, useCookieConsent } from '@kit/ui/cookie-banner';
function App() {
const { status, accept, reject, clear } = useCookieConsent();
return (
<div>
{/* Your app content */}
<CookieBanner />
{/* Optional: Check consent status */}
{status === 'accepted' && (
<script>
// Load analytics or tracking scripts
</script>
)}
</div>
);
}`;
};
const DemoCookieBanner = () => {
if (demoConsent !== 'unknown' || !controls.showBanner) {
return null;
}
return (
<div
className={`bg-background animate-in fade-in zoom-in-95 slide-in-from-bottom-16 fixed z-50 w-full max-w-lg border p-6 shadow-2xl ${
controls.position === 'bottom-left'
? 'bottom-4 left-4 rounded-lg'
: controls.position === 'bottom-center'
? 'bottom-0 left-1/2 -translate-x-1/2 transform lg:bottom-4 lg:rounded-lg'
: 'right-4 bottom-4 rounded-lg'
}`}
>
<div className="flex flex-col space-y-4">
<div>
<h3 className="text-lg font-semibold">We use cookies</h3>
</div>
<div className="text-gray-500 dark:text-gray-400">
<p className="text-sm">
We use cookies to enhance your experience on our site, analyze
site usage, and assist in our marketing efforts.
</p>
</div>
<div className="flex justify-end space-x-2.5">
<Button variant="ghost" onClick={() => setDemoConsent('rejected')}>
Reject
</Button>
<Button autoFocus onClick={() => setDemoConsent('accepted')}>
Accept
</Button>
</div>
</div>
</div>
);
};
const renderPreview = () => (
<div className="bg-muted/20 relative h-64 overflow-hidden rounded-lg border">
<div className="p-4">
<h3 className="mb-2 font-semibold">Preview Area</h3>
<p className="text-muted-foreground mb-4 text-sm">
This simulates how the cookie banner appears on your site.
</p>
<div className="space-y-2 text-sm">
<div>
<strong>Demo Status:</strong> {demoConsent}
</div>
<div>
<strong>Real Status:</strong> {cookieConsent.status}
</div>
</div>
<div className="mt-4 space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => setDemoConsent('unknown')}
>
Reset Demo Banner
</Button>
<Button size="sm" variant="outline" onClick={cookieConsent.clear}>
Clear Real Consent
</Button>
</div>
</div>
<DemoCookieBanner />
</div>
);
const renderControls = () => (
<>
<div className="flex items-center justify-between">
<Label htmlFor="showBanner">Show Banner</Label>
<Switch
id="showBanner"
checked={controls.showBanner}
onCheckedChange={(checked) => {
updateControl('showBanner', checked);
if (checked) {
setDemoConsent('unknown');
}
}}
/>
</div>
<div className="space-y-2">
<Label>Position</Label>
<div className="flex flex-col gap-2">
{[
{ value: 'bottom-left', label: 'Bottom Left' },
{ value: 'bottom-center', label: 'Bottom Center' },
{ value: 'bottom-right', label: 'Bottom Right' },
].map((option) => (
<button
key={option.value}
className={`rounded border p-2 text-sm ${
controls.position === option.value
? 'bg-primary text-primary-foreground'
: 'bg-background hover:bg-muted'
}`}
onClick={() => updateControl('position', option.value as any)}
>
{option.label}
</button>
))}
</div>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Integration with useCookieConsent Hook</CardTitle>
<CardDescription>
How to use the cookie consent hook in your components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="rounded-lg border p-4">
<h4 className="mb-2 font-semibold">Current Consent Status</h4>
<div className="space-y-2 text-sm">
<div>
<strong>Status:</strong>{' '}
<span
className={`rounded px-2 py-1 text-xs ${
cookieConsent.status === 'accepted'
? 'bg-green-100 text-green-700'
: cookieConsent.status === 'rejected'
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
}`}
>
{cookieConsent.status}
</span>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={cookieConsent.accept}>
Accept
</Button>
<Button
size="sm"
variant="outline"
onClick={cookieConsent.reject}
>
Reject
</Button>
<Button
size="sm"
variant="outline"
onClick={cookieConsent.clear}
>
Clear
</Button>
</div>
</div>
</div>
<div className="bg-muted/20 rounded-lg border p-4">
<h4 className="mb-2 font-semibold">Conditional Content</h4>
<p className="text-muted-foreground mb-2 text-sm">
This content only shows when cookies are accepted:
</p>
{cookieConsent.status === 'accepted' ? (
<div className="rounded border border-green-200 bg-green-50 p-2 text-sm text-green-700">
🍪 Analytics and tracking enabled
</div>
) : (
<div className="rounded border border-gray-200 bg-gray-50 p-2 text-sm text-gray-600">
Analytics disabled - Accept cookies to enable tracking
</div>
)}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Different Consent States</CardTitle>
<CardDescription>
How the banner behaves in different states
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-3">
<div className="rounded-lg border p-4 text-center">
<h4 className="mb-2 font-semibold">Unknown</h4>
<p className="text-muted-foreground mb-2 text-xs">
First visit or cleared consent
</p>
<div className="text-2xl"></div>
<p className="mt-2 text-xs">Banner shows</p>
</div>
<div className="rounded-lg border p-4 text-center">
<h4 className="mb-2 font-semibold">Accepted</h4>
<p className="text-muted-foreground mb-2 text-xs">
User accepted cookies
</p>
<div className="text-2xl"></div>
<p className="mt-2 text-xs">Banner hidden</p>
</div>
<div className="rounded-lg border p-4 text-center">
<h4 className="mb-2 font-semibold">Rejected</h4>
<p className="text-muted-foreground mb-2 text-xs">
User rejected cookies
</p>
<div className="text-2xl"></div>
<p className="mt-2 text-xs">Banner hidden</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>CookieBanner Components</CardTitle>
<CardDescription>
Complete API reference for CookieBanner and useCookieConsent
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">CookieBanner</h4>
<p className="text-muted-foreground mb-3 text-sm">
A modal-style cookie consent banner that appears when consent
status is unknown.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Feature</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">Auto-positioning</td>
<td className="p-3">
Responsive positioning (bottom-left on desktop, full-width
on mobile)
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">Animation</td>
<td className="p-3">
Smooth entrance animation with fade and slide effects
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">Accessibility</td>
<td className="p-3">
Focus management and keyboard navigation
</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">Persistence</td>
<td className="p-3">
Remembers user choice in localStorage
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">
useCookieConsent Hook
</h4>
<p className="text-muted-foreground mb-3 text-sm">
Hook for managing cookie consent state throughout your
application.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Property</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">status</td>
<td className="p-3 font-mono text-sm">
'unknown' | 'accepted' | 'rejected'
</td>
<td className="p-3">Current consent status</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">accept</td>
<td className="p-3 font-mono text-sm">{'() => void'}</td>
<td className="p-3">Function to accept cookies</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">reject</td>
<td className="p-3 font-mono text-sm">{'() => void'}</td>
<td className="p-3">Function to reject cookies</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">clear</td>
<td className="p-3 font-mono text-sm">{'() => void'}</td>
<td className="p-3">Function to reset consent status</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Implementation Guidelines</CardTitle>
<CardDescription>Best practices for cookie consent</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Implementation Best Practices
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Add CookieBanner to your root layout or App component</li>
<li>
Check consent status before loading analytics/tracking scripts
</li>
<li> Provide clear information about cookie usage</li>
<li> Respect user choice and don't show banner repeatedly</li>
<li>• Allow users to change their preference later</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
❌ Common Mistakes
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>• Loading tracking scripts before consent</li>
<li>• Not providing a way to change consent later</li>
<li>• Hiding the reject option or making it hard to find</li>
<li>• Not explaining what cookies are used for</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Legal Compliance</CardTitle>
<CardDescription>GDPR and privacy considerations</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">GDPR Requirements</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>• Consent must be freely given, specific, and informed</li>
<li>• Users must be able to withdraw consent easily</li>
<li>• Essential cookies don't require consent</li>
<li> Pre-ticked boxes are not valid consent</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Cookie Categories</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>
<strong>Essential:</strong> Required for site functionality
</li>
<li>
<strong>Analytics:</strong> Usage statistics and performance
</li>
<li>
<strong>Marketing:</strong> Advertising and personalization
</li>
<li>
<strong>Functional:</strong> Enhanced features and preferences
</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Customization Options</CardTitle>
<CardDescription>Adapting the banner to your needs</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Text Customization</h4>
<p className="text-muted-foreground text-sm">
Customize banner text through i18n keys: cookieBanner.title,
cookieBanner.description, cookieBanner.accept,
cookieBanner.reject.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Styling</h4>
<p className="text-muted-foreground text-sm">
The banner automatically adapts to your theme colors and spacing.
Override CSS classes for custom styling if needed.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Advanced Features</h4>
<p className="text-muted-foreground text-sm">
For granular cookie control, extend the component to support
different cookie categories with individual accept/reject options.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,917 @@
'use client';
import { useState } from 'react';
import {
Download,
Edit,
FileText,
Heart,
Image,
Info,
MessageSquare,
Plus,
Settings,
Share,
Star,
User,
} from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Checkbox } from '@kit/ui/checkbox';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface DialogControls {
title: string;
description: string;
triggerText: string;
triggerVariant:
| 'default'
| 'destructive'
| 'outline'
| 'secondary'
| 'ghost'
| 'link';
size: 'default' | 'sm' | 'lg' | 'xl' | 'full';
withIcon: boolean;
withFooter: boolean;
withForm: boolean;
closable: boolean;
modal: boolean;
}
const triggerVariantOptions = [
{ value: 'default', label: 'Default', description: 'Primary button style' },
{ value: 'outline', label: 'Outline', description: 'Outlined button' },
{ value: 'secondary', label: 'Secondary', description: 'Secondary style' },
{ value: 'ghost', label: 'Ghost', description: 'Minimal button' },
{ value: 'destructive', label: 'Destructive', description: 'Danger button' },
{ value: 'link', label: 'Link', description: 'Link style' },
] as const;
const sizeOptions = [
{ value: 'sm', label: 'Small', description: 'max-w-md' },
{ value: 'default', label: 'Default', description: 'max-w-lg' },
{ value: 'lg', label: 'Large', description: 'max-w-xl' },
{ value: 'xl', label: 'Extra Large', description: 'max-w-2xl' },
{ value: 'full', label: 'Full Screen', description: 'max-w-screen' },
] as const;
const iconOptions = [
{ value: 'settings', icon: Settings, label: 'Settings' },
{ value: 'user', icon: User, label: 'User' },
{ value: 'edit', icon: Edit, label: 'Edit' },
{ value: 'plus', icon: Plus, label: 'Plus' },
{ value: 'info', icon: Info, label: 'Info' },
{ value: 'file', icon: FileText, label: 'File' },
{ value: 'image', icon: Image, label: 'Image' },
{ value: 'share', icon: Share, label: 'Share' },
];
export function DialogStory() {
const { controls, updateControl } = useStoryControls<DialogControls>({
title: 'Edit Profile',
description:
"Make changes to your profile here. Click save when you're done.",
triggerText: 'Open Dialog',
triggerVariant: 'default',
size: 'default',
withIcon: false,
withFooter: true,
withForm: false,
closable: true,
modal: true,
});
const [selectedIcon, setSelectedIcon] = useState('settings');
const [formData, setFormData] = useState({
name: 'John Doe',
email: 'john@example.com',
bio: 'Software developer passionate about user experience.',
});
const selectedIconData = iconOptions.find(
(opt) => opt.value === selectedIcon,
);
const IconComponent = selectedIconData?.icon || Settings;
const generateCode = () => {
const contentClass = cn(
controls.size === 'sm' && 'max-w-md',
controls.size === 'default' && 'max-w-lg',
controls.size === 'lg' && 'max-w-xl',
controls.size === 'xl' && 'max-w-2xl',
controls.size === 'full' && 'h-screen max-w-screen',
);
const contentProps = {
className: contentClass || undefined,
};
const contentPropsString = generatePropsString(contentProps, {
className: undefined,
});
let code = `<Dialog>\n`;
code += ` <DialogTrigger asChild>\n`;
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
code += ` </DialogTrigger>\n`;
code += ` <DialogContent${contentPropsString}>\n`;
code += ` <DialogHeader>\n`;
if (controls.withIcon) {
code += ` <div className="flex items-center gap-3">\n`;
const iconName = selectedIconData?.icon.name || 'Settings';
code += ` <${iconName} className="h-5 w-5" />\n`;
code += ` <DialogTitle>${controls.title}</DialogTitle>\n`;
code += ` </div>\n`;
} else {
code += ` <DialogTitle>${controls.title}</DialogTitle>\n`;
}
if (controls.description) {
code += ` <DialogDescription>\n`;
code += ` ${controls.description}\n`;
code += ` </DialogDescription>\n`;
}
code += ` </DialogHeader>\n`;
if (controls.withForm) {
code += ` <div className="grid gap-4 py-4">\n`;
code += ` <div className="grid gap-2">\n`;
code += ` <Label htmlFor="name">Name</Label>\n`;
code += ` <Input id="name" value="John Doe" />\n`;
code += ` </div>\n`;
code += ` <div className="grid gap-2">\n`;
code += ` <Label htmlFor="email">Email</Label>\n`;
code += ` <Input id="email" type="email" value="john@example.com" />\n`;
code += ` </div>\n`;
code += ` </div>\n`;
} else {
code += ` <div className="py-4">\n`;
code += ` <p>Dialog content goes here.</p>\n`;
code += ` </div>\n`;
}
if (controls.withFooter) {
code += ` <DialogFooter>\n`;
code += ` <DialogClose asChild>\n`;
code += ` <Button variant="outline">Cancel</Button>\n`;
code += ` </DialogClose>\n`;
code += ` <Button>Save Changes</Button>\n`;
code += ` </DialogFooter>\n`;
}
code += ` </DialogContent>\n`;
code += `</Dialog>`;
return code;
};
const renderPreview = () => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant={controls.triggerVariant}>
{controls.triggerText}
</Button>
</DialogTrigger>
<DialogContent
className={cn(
controls.size === 'sm' && 'max-w-md',
controls.size === 'lg' && 'max-w-xl',
controls.size === 'xl' && 'max-w-2xl',
controls.size === 'full' && 'h-screen max-w-screen',
)}
>
<DialogHeader>
{controls.withIcon ? (
<div className="flex items-center gap-3">
<IconComponent className="h-5 w-5" />
<DialogTitle>{controls.title}</DialogTitle>
</div>
) : (
<DialogTitle>{controls.title}</DialogTitle>
)}
{controls.description && (
<DialogDescription>{controls.description}</DialogDescription>
)}
</DialogHeader>
{controls.withForm ? (
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
value={formData.bio}
onChange={(e) =>
setFormData({ ...formData, bio: e.target.value })
}
rows={3}
/>
</div>
</div>
) : (
<div className="py-4">
<p>
This is the dialog content area. You can put any content here
including forms, images, or other interactive elements.
</p>
</div>
)}
{controls.withFooter && (
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Save Changes</Button>
</DialogFooter>
)}
</DialogContent>
</Dialog>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="triggerVariant">Trigger Button Style</Label>
<SimpleStorySelect
value={controls.triggerVariant}
onValueChange={(value) => updateControl('triggerVariant', value)}
options={triggerVariantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="size">Dialog Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="triggerText">Trigger Text</Label>
<Input
id="triggerText"
value={controls.triggerText}
onChange={(e) => updateControl('triggerText', e.target.value)}
placeholder="Button text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="title">Dialog Title</Label>
<Input
id="title"
value={controls.title}
onChange={(e) => updateControl('title', e.target.value)}
placeholder="Dialog title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={controls.description}
onChange={(e) => updateControl('description', e.target.value)}
placeholder="Optional description"
rows={2}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="withIcon">With Icon</Label>
<Switch
id="withIcon"
checked={controls.withIcon}
onCheckedChange={(checked) => updateControl('withIcon', checked)}
/>
</div>
{controls.withIcon && (
<div className="space-y-2">
<Label htmlFor="icon">Icon</Label>
<SimpleStorySelect
value={selectedIcon}
onValueChange={setSelectedIcon}
options={iconOptions.map((opt) => ({
value: opt.value,
label: opt.label,
description: `${opt.label} icon`,
}))}
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="withFooter">With Footer</Label>
<Switch
id="withFooter"
checked={controls.withFooter}
onCheckedChange={(checked) => updateControl('withFooter', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="withForm">With Form Example</Label>
<Switch
id="withForm"
checked={controls.withForm}
onCheckedChange={(checked) => updateControl('withForm', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Dialogs</CardTitle>
<CardDescription>Simple dialog variations</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<Info className="mr-2 h-4 w-4" />
Info Dialog
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Information</DialogTitle>
<DialogDescription>
This is an informational dialog with some important details.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm">
Here you can provide additional context, instructions, or
any other information that helps the user understand what
they need to know.
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button>Got it</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button>
<Edit className="mr-2 h-4 w-4" />
Edit Profile
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Update your profile information below.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">Full Name</Label>
<Input id="edit-name" defaultValue="John Doe" />
</div>
<div className="grid gap-2">
<Label htmlFor="edit-email">Email</Label>
<Input
id="edit-email"
type="email"
defaultValue="john@example.com"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-bio">Bio</Label>
<Textarea
id="edit-bio"
rows={3}
defaultValue="Software developer"
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">
<Settings className="mr-2 h-4 w-4" />
Settings
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Preferences</DialogTitle>
<DialogDescription>
Customize your application settings.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label>Dark Mode</Label>
<p className="text-muted-foreground text-sm">
Switch to dark theme
</p>
</div>
<Switch />
</div>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label>Notifications</Label>
<p className="text-muted-foreground text-sm">
Receive push notifications
</p>
</div>
<Switch defaultChecked />
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Dialog Sizes</CardTitle>
<CardDescription>Different dialog dimensions</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
Small Dialog
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Small Dialog</DialogTitle>
<DialogDescription>
This is a compact dialog.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm">
Perfect for simple confirmations or brief forms.
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button>Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Large Dialog</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Large Dialog</DialogTitle>
<DialogDescription>
This dialog has more space for content.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<p>
This larger dialog can accommodate more complex forms,
detailed information, or multiple sections of content.
</p>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label htmlFor="large-name">Name</Label>
<Input id="large-name" />
</div>
<div>
<Label htmlFor="large-email">Email</Label>
<Input id="large-email" type="email" />
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Content Variations</CardTitle>
<CardDescription>Different types of dialog content</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<Image className="mr-2 h-4 w-4" />
Image Gallery
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Image Preview</DialogTitle>
</DialogHeader>
<div className="py-4">
<div className="bg-muted flex aspect-video items-center justify-center rounded-lg">
<Image className="text-muted-foreground h-12 w-12" />
</div>
<div className="mt-4 flex items-center justify-between">
<div>
<p className="font-medium">landscape.jpg</p>
<p className="text-muted-foreground text-sm">
2.4 MB 1920x1080
</p>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline">
<Download className="mr-2 h-4 w-4" />
Download
</Button>
<Button size="sm" variant="outline">
<Share className="mr-2 h-4 w-4" />
Share
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<MessageSquare className="mr-2 h-4 w-4" />
Feedback
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Send Feedback</DialogTitle>
<DialogDescription>
Help us improve by sharing your thoughts.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="feedback-type">Type of Feedback</Label>
<select
id="feedback-type"
className="border-input focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-2xs transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="bug">Bug Report</option>
<option value="feature">Feature Request</option>
<option value="general">General Feedback</option>
</select>
</div>
<div className="grid gap-2">
<Label htmlFor="feedback-message">Message</Label>
<Textarea
id="feedback-message"
rows={4}
placeholder="Describe your feedback..."
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="feedback-contact" />
<Label htmlFor="feedback-contact" className="text-sm">
You can contact me about this feedback
</Label>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>
<MessageSquare className="mr-2 h-4 w-4" />
Send Feedback
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Dialog Components</CardTitle>
<CardDescription>
Complete API reference for Dialog components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Dialog</h4>
<p className="text-muted-foreground mb-3 text-sm">
Root container for the dialog. Contains all dialog parts.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">open</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Controlled open state</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">onOpenChange</td>
<td className="p-3 font-mono text-sm">function</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Callback when open state changes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">modal</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">true</td>
<td className="p-3">Whether the dialog is modal</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
<p className="text-muted-foreground mb-3 text-sm">
The element that opens the dialog. Use asChild prop to render as
child element.
</p>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">DialogContent</h4>
<p className="text-muted-foreground mb-3 text-sm">
The main dialog content container with overlay and animations.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">Other Components</h4>
<ul className="space-y-2 text-sm">
<li>
<strong>DialogHeader:</strong> Container for title and
description
</li>
<li>
<strong>DialogTitle:</strong> Accessible dialog title
</li>
<li>
<strong>DialogDescription:</strong> Optional dialog description
</li>
<li>
<strong>DialogFooter:</strong> Container for action buttons
</li>
<li>
<strong>DialogClose:</strong> Element that closes the dialog
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Dialog</CardTitle>
<CardDescription>Best practices for dialog usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Dialog For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Complex forms that need focus</li>
<li> Detailed information or settings</li>
<li> Multi-step workflows</li>
<li> Image/media previews</li>
<li> Non-destructive actions</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Dialog For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Critical confirmations (use AlertDialog)</li>
<li> Simple tooltips or hints</li>
<li> Destructive actions without confirmation</li>
<li> Content that should be part of the main flow</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>
Making dialogs accessible to all users
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Focus Management</h4>
<p className="text-muted-foreground text-sm">
Focus moves to dialog when opened
<br />
Focus returns to trigger when closed
<br />
Tab navigation stays within dialog
<br /> Escape key closes the dialog
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Screen Reader Support</h4>
<p className="text-muted-foreground text-sm">
Always include DialogTitle for screen reader users. Use
DialogDescription for additional context.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
<p className="text-muted-foreground text-sm">
All interactive elements should be keyboard accessible with clear
focus indicators.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Design Patterns</CardTitle>
<CardDescription>
Common dialog implementation patterns
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Form Dialog</h4>
<p className="text-muted-foreground text-sm">
Use for complex forms that benefit from focused attention without
page navigation.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Information Dialog</h4>
<p className="text-muted-foreground text-sm">
Present detailed information, help content, or explanatory
material.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Preview Dialog</h4>
<p className="text-muted-foreground text-sm">
Show larger versions of content, image galleries, or detailed
previews.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Settings Dialog</h4>
<p className="text-muted-foreground text-sm">
Organize application preferences and configuration options.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Suspense } from 'react';
import { COMPONENTS_REGISTRY } from '../lib/components-data';
import { LoadingFallback } from './loading-fallback';
interface DocsContentProps {
selectedComponent: string;
}
export function DocsContent({ selectedComponent }: DocsContentProps) {
const component = COMPONENTS_REGISTRY.find(
(c) => c.name === selectedComponent,
);
if (!component) {
return null;
}
return (
<div className="flex-1 overflow-auto">
<Suspense fallback={<LoadingFallback />}>
<div className="p-4">
<component.component />
</div>
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,58 @@
'use client';
import { Settings } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { COMPONENTS_REGISTRY } from '../lib/components-data';
interface DocsHeaderProps {
selectedComponent: string;
}
export function DocsHeader({ selectedComponent }: DocsHeaderProps) {
const component = COMPONENTS_REGISTRY.find(
(c) => c.name === selectedComponent,
);
if (!component) {
return null;
}
return (
<div className="bg-muted/30 border-b p-4">
<div className="flex items-start justify-between">
<div className="space-y-1">
<div className="flex items-center gap-3">
<div className="flex items-center gap-4">
<h2 className="text-2xl font-bold">{component.name}</h2>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline">{component.category}</Badge>
<Badge variant="secondary" className="text-xs">
{component.subcategory}
</Badge>
</div>
</div>
</div>
<p className="text-muted-foreground max-w-2xl">
{component.description}
</p>
<div className="text-muted-foreground flex items-center gap-4 text-sm">
<span className="flex items-center gap-1">
<Settings className="h-4 w-4" />
{component.props.length} props
</span>
<span className="flex items-center gap-1">
{component.sourceFile}
</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,95 @@
'use client';
import { useCallback, useMemo } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import type { ComponentInfo } from '../lib/components-data';
import { components } from '../lib/components-data';
import { DocsContent } from './docs-content';
import { DocsHeader } from './docs-header';
import { DocsSidebar } from './docs-sidebar';
export function DocsProvider() {
const router = useRouter();
const searchParams = useSearchParams();
// Get current values from query params
const componentId = searchParams.get('component');
const categoryParam = searchParams.get('category');
// Find the selected component based on query param, fallback to first component
const selectedComponent = useMemo(() => {
if (componentId) {
const found = components.find((c) => c.id === componentId);
if (found) return found;
}
return components[0];
}, [componentId]);
// Get selected category (null if 'all' or not set)
const selectedCategory = useMemo(() => {
return categoryParam && categoryParam !== 'all' ? categoryParam : null;
}, [categoryParam]);
// Update query params when component changes
const handleComponentSelect = useCallback(
(component: ComponentInfo) => {
const params = new URLSearchParams(searchParams);
params.set('component', component.id);
// If we're selecting a component from a different category, clear category filter
if (selectedCategory && component.category !== selectedCategory) {
params.delete('category');
}
router.push(`?${params.toString()}`);
},
[router, searchParams, selectedCategory],
);
// Update query params when category changes
const handleCategorySelect = useCallback(
(category: string | null) => {
const params = new URLSearchParams(searchParams);
if (category) {
params.set('category', category);
// When selecting a category, auto-select the first component in that category
const firstComponentInCategory = components.find(
(c) => c.category === category,
);
if (firstComponentInCategory) {
params.set('component', firstComponentInCategory.id);
}
} else {
params.delete('category');
// When showing all, select the first component overall
params.set('component', components[0]!.id);
}
router.push(`?${params.toString()}`);
},
[router, searchParams],
);
if (!selectedComponent) {
return null;
}
return (
<div className="bg-background flex h-screen">
<DocsSidebar
selectedComponent={selectedComponent.id}
selectedCategory={selectedCategory}
/>
<div className="flex flex-1 flex-col">
<DocsHeader selectedComponent={selectedComponent.id} />
<DocsContent selectedComponent={selectedComponent.id} />
</div>
</div>
);
}

View File

@@ -0,0 +1,271 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Code2, FileText, Search } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Input } from '@kit/ui/input';
import { ScrollArea } from '@kit/ui/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { cn } from '@kit/ui/utils';
import type { ComponentInfo } from '../lib/components-data';
import {
COMPONENTS_REGISTRY,
categories,
categoryInfo,
} from '../lib/components-data';
interface DocsSidebarProps {
selectedComponent: string;
selectedCategory: string | null;
}
export function DocsSidebar({
selectedComponent,
selectedCategory,
}: DocsSidebarProps) {
const [searchQuery, setSearchQuery] = useState('');
const router = useRouter();
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
selectedCategory ? c.category === selectedCategory : true,
)
.filter(
(c) =>
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.subcategory.toLowerCase().includes(searchQuery.toLowerCase()),
)
.sort((a, b) => a.name.localeCompare(b.name));
const onCategorySelect = (category: string | null) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('category', category || '');
router.push(`/components?${searchParams.toString()}`);
};
const onComponentSelect = (component: ComponentInfo) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('component', component.name);
router.push(`/components?${searchParams.toString()}`);
};
return (
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
{/* Header */}
<div className="flex-shrink-0 border-b p-4">
<div className="mb-2 flex items-center gap-2">
<Code2 className="text-primary h-6 w-6" />
<h1 className="text-xl font-bold">Components</h1>
</div>
<p className="text-muted-foreground text-sm">
This is the documentation for the components of the UI Kit.
</p>
</div>
{/* Controls */}
<div className="flex-shrink-0 space-y-2 border-b p-4">
{/* Category Select */}
<div className="space-y-2">
<Select
value={selectedCategory || 'all'}
onValueChange={(value) => {
const category = value === 'all' ? null : value;
onCategorySelect(category);
// Select first component in the filtered results
const firstComponent = category
? COMPONENTS_REGISTRY.find((c) => c.category === category)
: COMPONENTS_REGISTRY[0];
if (firstComponent) {
onComponentSelect(firstComponent);
}
}}
>
<SelectTrigger>
<SelectValue placeholder={'Select a category'} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
<div className="flex items-center gap-2">
<span>All Components</span>
<Badge variant="secondary" className="text-xs">
{COMPONENTS_REGISTRY.length}
</Badge>
</div>
</SelectItem>
{categories.map((category) => {
const categoryData =
categoryInfo[category as keyof typeof categoryInfo];
const categoryComponents = COMPONENTS_REGISTRY.filter(
(c) => c.category === category,
);
return (
<SelectItem key={category} value={category}>
<div className="flex items-center gap-2">
{categoryData && (
<categoryData.icon className="h-4 w-4" />
)}
<span>{category}</span>
<Badge variant="outline" className="text-xs">
{categoryComponents.length}
</Badge>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{/* Search Input */}
<div className="space-y-2">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder={'Search for a component'}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
</div>
{/* Components List - Scrollable */}
<div className="flex flex-1 flex-col overflow-y-auto">
<div className="flex-shrink-0 p-4 pb-2">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<FileText className="h-4 w-4" />
Components
<Badge variant="outline" className="text-xs">
{filteredComponents.length}
</Badge>
{selectedCategory && (
<Badge variant="secondary" className="text-xs">
{selectedCategory}
</Badge>
)}
</h3>
</div>
<ScrollArea className="flex-1">
<div className="px-4 pb-4">
<div className="space-y-1">
{filteredComponents.length === 0 ? (
<div className="text-muted-foreground py-8 text-center">
<p className="text-sm">No components found</p>
<p className="mt-1 text-xs">
Try adjusting your search or category filter
</p>
</div>
) : (
filteredComponents.map((item) => {
const IconComponent = item.icon;
return (
<ComponentItem
key={item.id}
item={item}
isSelected={item.name === selectedComponent}
onComponentSelect={onComponentSelect}
IconComponent={IconComponent}
/>
);
})
)}
</div>
</div>
</ScrollArea>
</div>
</div>
);
}
function ComponentItem({
item,
isSelected,
onComponentSelect,
IconComponent,
}: {
item: ComponentInfo;
IconComponent: React.ComponentType<{ className?: string }>;
isSelected: boolean;
onComponentSelect: (item: ComponentInfo) => void;
}) {
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isSelected) {
ref.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [isSelected]);
return (
<button
ref={ref}
key={item.id}
onClick={() => onComponentSelect(item)}
className={cn(
'w-full rounded-lg px-3 py-2.5 text-left text-sm transition-colors',
isSelected
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent hover:text-accent-foreground',
)}
>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<IconComponent className="h-4 w-4 flex-shrink-0" />
<span className="flex-1 truncate font-medium">{item.name}</span>
</div>
<p
className={cn(
'ml-6 line-clamp-1 text-xs',
isSelected ? 'opacity-90' : 'text-muted-foreground',
)}
>
{item.description}
</p>
<div className="ml-6 flex items-center gap-1.5 text-xs">
<span
className={cn(
'font-medium',
isSelected ? 'opacity-80' : 'text-muted-foreground',
)}
>
{item.subcategory}
</span>
<span
className={cn(
isSelected ? 'opacity-60' : 'text-muted-foreground/60',
)}
>
{item.props.length} props
</span>
</div>
</div>
</button>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,633 @@
'use client';
import { Bell, FileText, Package, Plus, Search, Users } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
EmptyState,
EmptyStateButton,
EmptyStateHeading,
EmptyStateText,
} from '@kit/ui/empty-state';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { StorySelect } from './story-select';
interface EmptyStateControls {
heading: string;
text: string;
showButton: boolean;
buttonText: string;
buttonVariant: 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive';
showExtraContent: boolean;
className: string;
minHeight: string;
}
export function EmptyStateStory() {
const { controls, updateControl } = useStoryControls<EmptyStateControls>({
heading: 'No projects yet',
text: 'Get started by creating your first project.',
showButton: true,
buttonText: 'Create Project',
buttonVariant: 'default',
showExtraContent: false,
className: '',
minHeight: '200px',
});
const generateCode = () => {
const containerProps = generatePropsString(
{
className: controls.className || `min-h-[${controls.minHeight}]`,
},
{
className: '',
},
);
let code = `<EmptyState${containerProps}>\n`;
code += ` <EmptyStateHeading>${controls.heading}</EmptyStateHeading>\n`;
code += ` <EmptyStateText>\n ${controls.text}\n </EmptyStateText>\n`;
if (controls.showButton) {
const buttonProps = generatePropsString(
{ variant: controls.buttonVariant },
{ variant: 'default' },
);
code += ` <EmptyStateButton${buttonProps}>${controls.buttonText}</EmptyStateButton>\n`;
}
if (controls.showExtraContent) {
code += ` <div className="mt-2">\n`;
code += ` <Button variant="link" size="sm">\n`;
code += ` Learn more\n`;
code += ` </Button>\n`;
code += ` </div>\n`;
}
code += `</EmptyState>`;
return code;
};
const buttonVariantOptions = [
{
value: 'default' as const,
label: 'Default',
description: 'Primary action button',
},
{
value: 'outline' as const,
label: 'Outline',
description: 'Secondary action',
},
{
value: 'secondary' as const,
label: 'Secondary',
description: 'Alternative style',
},
{
value: 'ghost' as const,
label: 'Ghost',
description: 'Minimal style',
},
{
value: 'destructive' as const,
label: 'Destructive',
description: 'Danger/delete action',
},
];
const renderPreview = () => (
<EmptyState
className={controls.className || `min-h-[${controls.minHeight}]`}
>
<EmptyStateHeading>{controls.heading}</EmptyStateHeading>
<EmptyStateText>{controls.text}</EmptyStateText>
{controls.showButton && (
<EmptyStateButton variant={controls.buttonVariant}>
{controls.buttonText}
</EmptyStateButton>
)}
{controls.showExtraContent && (
<div className="mt-2">
<Button variant="link" size="sm">
Learn more
</Button>
</div>
)}
</EmptyState>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="heading">Heading</Label>
<Input
id="heading"
value={controls.heading}
onChange={(e) => updateControl('heading', e.target.value)}
placeholder="Enter heading text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="text">Description Text</Label>
<Textarea
id="text"
value={controls.text}
onChange={(e) => updateControl('text', e.target.value)}
placeholder="Enter description text"
rows={2}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="showButton">Show Button</Label>
<Switch
id="showButton"
checked={controls.showButton}
onCheckedChange={(checked) => updateControl('showButton', checked)}
/>
</div>
{controls.showButton && (
<>
<div className="space-y-2">
<Label htmlFor="buttonText">Button Text</Label>
<Input
id="buttonText"
value={controls.buttonText}
onChange={(e) => updateControl('buttonText', e.target.value)}
placeholder="Enter button text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="buttonVariant">Button Variant</Label>
<StorySelect
value={controls.buttonVariant}
onValueChange={(value) => updateControl('buttonVariant', value)}
options={buttonVariantOptions}
/>
</div>
</>
)}
<div className="flex items-center justify-between">
<Label htmlFor="showExtraContent">Show Extra Content</Label>
<Switch
id="showExtraContent"
checked={controls.showExtraContent}
onCheckedChange={(checked) =>
updateControl('showExtraContent', checked)
}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="minHeight">Minimum Height</Label>
<Input
id="minHeight"
value={controls.minHeight}
onChange={(e) => updateControl('minHeight', e.target.value)}
placeholder="e.g. 200px, 300px"
/>
</div>
<div className="space-y-2">
<Label htmlFor="className">Custom Classes</Label>
<Input
id="className"
value={controls.className}
onChange={(e) => updateControl('className', e.target.value)}
placeholder="e.g. bg-gray-50"
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Common Use Cases</CardTitle>
<CardDescription>
Empty state patterns for different scenarios
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<EmptyState className="min-h-[200px]">
<EmptyStateHeading>No projects yet</EmptyStateHeading>
<EmptyStateText>
Get started by creating your first project.
</EmptyStateText>
<EmptyStateButton>
<Plus className="mr-2 h-4 w-4" />
Create Project
</EmptyStateButton>
</EmptyState>
<EmptyState className="min-h-[200px]">
<EmptyStateHeading>No results found</EmptyStateHeading>
<EmptyStateText>
Try adjusting your search or filter criteria.
</EmptyStateText>
<EmptyStateButton variant="outline">
<Search className="mr-2 h-4 w-4" />
Clear filters
</EmptyStateButton>
</EmptyState>
<EmptyState className="min-h-[200px]">
<EmptyStateHeading>No team members</EmptyStateHeading>
<EmptyStateText>
Invite team members to collaborate on your projects.
</EmptyStateText>
<EmptyStateButton>
<Users className="mr-2 h-4 w-4" />
Invite Members
</EmptyStateButton>
<div className="mt-2">
<Button variant="link" size="sm">
Learn more about teams
</Button>
</div>
</EmptyState>
<EmptyState className="min-h-[200px]">
<EmptyStateHeading>No notifications</EmptyStateHeading>
<EmptyStateText>
You're all caught up! Check back later.
</EmptyStateText>
</EmptyState>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>With Icons</CardTitle>
<CardDescription>
Empty states enhanced with descriptive icons
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<EmptyState className="min-h-[200px]">
<Package className="text-muted-foreground mb-4 h-12 w-12" />
<EmptyStateHeading>No products</EmptyStateHeading>
<EmptyStateText>
Add your first product to start selling.
</EmptyStateText>
<EmptyStateButton>Add Product</EmptyStateButton>
</EmptyState>
<EmptyState className="min-h-[200px]">
<FileText className="text-muted-foreground mb-4 h-12 w-12" />
<EmptyStateHeading>No documents</EmptyStateHeading>
<EmptyStateText>
Upload or create your first document.
</EmptyStateText>
<EmptyStateButton variant="outline">
Upload Document
</EmptyStateButton>
</EmptyState>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Different Styles</CardTitle>
<CardDescription>Various empty state presentations</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<EmptyState className="bg-muted/10 min-h-[150px] border-2 border-dashed">
<EmptyStateHeading>Drag and drop files here</EmptyStateHeading>
<EmptyStateText>Or click to browse your computer</EmptyStateText>
<EmptyStateButton variant="secondary">
Browse Files
</EmptyStateButton>
</EmptyState>
<EmptyState className="min-h-[150px] border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50">
<EmptyStateHeading className="text-blue-900">
Premium feature
</EmptyStateHeading>
<EmptyStateText className="text-blue-700">
Upgrade your plan to access this feature.
</EmptyStateText>
<EmptyStateButton className="bg-blue-600 hover:bg-blue-700">
Upgrade Now
</EmptyStateButton>
</EmptyState>
<EmptyState className="min-h-[150px] border-0 shadow-none">
<EmptyStateHeading>Coming soon</EmptyStateHeading>
<EmptyStateText>
This feature is under development. Stay tuned!
</EmptyStateText>
<EmptyStateButton variant="ghost">
<Bell className="mr-2 h-4 w-4" />
Get Notified
</EmptyStateButton>
</EmptyState>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>EmptyState Components</CardTitle>
<CardDescription>
Complete API reference for EmptyState components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* EmptyState */}
<div>
<h4 className="mb-3 text-lg font-semibold">EmptyState</h4>
<p className="text-muted-foreground mb-3 text-sm">
Container component that renders child components in a centered
layout with dashed border.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">
Content including EmptyStateHeading, EmptyStateText, and
EmptyStateButton
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* EmptyStateHeading */}
<div>
<h4 className="mb-3 text-lg font-semibold">EmptyStateHeading</h4>
<p className="text-muted-foreground mb-3 text-sm">
Heading text for the empty state. Renders as an h3 element.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3">Heading text content</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* EmptyStateText */}
<div>
<h4 className="mb-3 text-lg font-semibold">EmptyStateText</h4>
<p className="text-muted-foreground mb-3 text-sm">
Descriptive text explaining the empty state. Renders as a
paragraph element.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3">Description text content</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* EmptyStateButton */}
<div>
<h4 className="mb-3 text-lg font-semibold">EmptyStateButton</h4>
<p className="text-muted-foreground mb-3 text-sm">
Call-to-action button. Extends the Button component with all its
props.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">variant</td>
<td className="p-3 font-mono text-sm">
'default' | 'outline' | 'secondary' | 'ghost' |
'destructive'
</td>
<td className="p-3">Button style variant</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">size</td>
<td className="p-3 font-mono text-sm">
'default' | 'sm' | 'lg' | 'icon'
</td>
<td className="p-3">Button size</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">onClick</td>
<td className="p-3 font-mono text-sm">() =&gt; void</td>
<td className="p-3">Click event handler</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">...props</td>
<td className="p-3 font-mono text-sm">ButtonProps</td>
<td className="p-3">All other Button component props</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Empty States</CardTitle>
<CardDescription>
Best practices for empty state usage
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
✅ Use Empty States For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>• First-time user experiences</li>
<li>• Search results with no matches</li>
<li>• Empty lists or collections</li>
<li>• Filtered views with no results</li>
<li>• Error states where content cannot be loaded</li>
<li>• Features that require user action to populate</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
❌ Avoid Empty States For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>• Loading states (use skeletons or spinners)</li>
<li>• Form validation messages</li>
<li>• System notifications</li>
<li>• Content that will auto-populate</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Content Guidelines</CardTitle>
<CardDescription>
Writing effective empty state content
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Be Helpful</h4>
<p className="text-muted-foreground text-sm">
Explain why the area is empty and what the user can do about it.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Be Positive</h4>
<p className="text-muted-foreground text-sm">
Frame the message positively. Focus on what users can do, not
what's missing.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Provide Clear Actions</h4>
<p className="text-muted-foreground text-sm">
Include a primary call-to-action that helps users move forward.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keep It Brief</h4>
<p className="text-muted-foreground text-sm">
Use concise language. Users should understand the state at a
glance.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Design Considerations</CardTitle>
<CardDescription>Visual design best practices</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Use Appropriate Imagery</h4>
<p className="text-muted-foreground text-sm">
Icons or illustrations can make empty states more engaging and
help communicate the message.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Maintain Visual Hierarchy</h4>
<p className="text-muted-foreground text-sm">
The heading should be prominent, followed by descriptive text,
then the action button.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Consider Context</h4>
<p className="text-muted-foreground text-sm">
The empty state should feel integrated with the surrounding
interface, not jarring or out of place.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,598 @@
'use client';
import { useState } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { FileUploader } from '@kit/ui/file-uploader';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
// Mock Supabase client for the story
const createMockSupabaseClient = () => ({
storage: {
from: (bucket: string) => ({
upload: async (path: string, file: File, options: any) => {
// Simulate upload delay
await new Promise((resolve) =>
setTimeout(resolve, 1000 + Math.random() * 2000),
);
// Simulate occasional upload errors
if (Math.random() < 0.1) {
return {
error: {
message: 'Upload failed: Network error',
},
};
}
return {
error: null,
data: {
path: `${bucket}/${path}`,
},
};
},
}),
},
});
interface FileUploaderControls {
maxFiles: number;
maxFileSize: number; // in MB for easier control
allowedMimeTypes: 'images' | 'documents' | 'all';
showSuccessToast: boolean;
}
const maxFilesOptions = [
{ value: '1', label: '1 file', description: 'Single file upload' },
{ value: '3', label: '3 files', description: 'Small batch' },
{ value: '5', label: '5 files', description: 'Medium batch' },
{ value: '10', label: '10 files', description: 'Large batch' },
];
const maxFileSizeOptions = [
{ value: '1', label: '1 MB', description: 'Small files' },
{ value: '5', label: '5 MB', description: 'Medium files' },
{ value: '10', label: '10 MB', description: 'Large files' },
{ value: '50', label: '50 MB', description: 'Very large files' },
];
const mimeTypeOptions = [
{ value: 'images', label: 'Images only', description: 'image/* types' },
{
value: 'documents',
label: 'Documents',
description: 'pdf, doc, txt files',
},
{ value: 'all', label: 'All types', description: 'No restrictions' },
];
const getMimeTypes = (type: string): string[] => {
switch (type) {
case 'images':
return ['image/*'];
case 'documents':
return [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
];
case 'all':
default:
return [];
}
};
export function FileUploaderStory() {
const { controls, updateControl } = useStoryControls<FileUploaderControls>({
maxFiles: 3,
maxFileSize: 5, // MB
allowedMimeTypes: 'images',
showSuccessToast: true,
});
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [mockClient] = useState(() => createMockSupabaseClient());
const handleUploadSuccess = (files: string[]) => {
setUploadedFiles((prev) => [...prev, ...files]);
if (controls.showSuccessToast) {
toast.success(`Successfully uploaded ${files.length} file(s)!`);
}
};
const generateCode = () => {
const allowedMimeTypes = getMimeTypes(controls.allowedMimeTypes);
const maxFileSizeBytes = controls.maxFileSize * 1024 * 1024;
const propsString = generatePropsString(
{
maxFiles: controls.maxFiles,
bucketName: '"uploads"',
path: '"user-files"',
allowedMimeTypes: JSON.stringify(allowedMimeTypes),
maxFileSize: maxFileSizeBytes,
client: 'supabaseClient',
onUploadSuccess: 'handleUploadSuccess',
},
{
maxFiles: 1,
bucketName: '"uploads"',
path: undefined,
allowedMimeTypes: '[]',
maxFileSize: Number.POSITIVE_INFINITY,
client: undefined,
onUploadSuccess: undefined,
},
);
// Format props for better readability
const formattedProps = propsString
.trim()
.split(' ')
.map((prop) => ` ${prop}`)
.join('\n');
return `import { FileUploader } from '@kit/ui/file-uploader';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
const supabase = useSupabase();
const handleUploadSuccess = (files: string[]) => {
console.log('Uploaded files:', files);
};
<FileUploader
${formattedProps}
/>`;
};
const renderPreview = () => (
<div className="space-y-4">
<FileUploader
maxFiles={controls.maxFiles}
bucketName="demo-bucket"
path="user-files"
allowedMimeTypes={getMimeTypes(controls.allowedMimeTypes)}
maxFileSize={controls.maxFileSize * 1024 * 1024} // Convert MB to bytes
client={mockClient as any}
onUploadSuccess={handleUploadSuccess}
/>
{uploadedFiles.length > 0 && (
<div className="bg-muted/20 mt-6 rounded-lg border p-4">
<h4 className="mb-2 font-semibold">Successfully Uploaded Files:</h4>
<ul className="space-y-1">
{uploadedFiles.map((file, index) => (
<li
key={index}
className="text-muted-foreground flex items-center text-sm"
>
<span className="mr-2 inline-block h-2 w-2 rounded-full bg-green-500"></span>
{file}
</li>
))}
</ul>
</div>
)}
</div>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="maxFiles">Max Files</Label>
<SimpleStorySelect
value={controls.maxFiles.toString()}
onValueChange={(value) => updateControl('maxFiles', parseInt(value))}
options={maxFilesOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxFileSize">Max File Size</Label>
<SimpleStorySelect
value={controls.maxFileSize.toString()}
onValueChange={(value) =>
updateControl('maxFileSize', parseInt(value))
}
options={maxFileSizeOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="allowedMimeTypes">Allowed File Types</Label>
<SimpleStorySelect
value={controls.allowedMimeTypes}
onValueChange={(value) =>
updateControl('allowedMimeTypes', value as any)
}
options={mimeTypeOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="showSuccessToast">Show Success Toast</Label>
<Switch
id="showSuccessToast"
checked={controls.showSuccessToast}
onCheckedChange={(checked) =>
updateControl('showSuccessToast', checked)
}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Image Upload</CardTitle>
<CardDescription>
Configured for image files only with preview
</CardDescription>
</CardHeader>
<CardContent>
<FileUploader
maxFiles={1}
bucketName="images"
allowedMimeTypes={['image/*']}
maxFileSize={5 * 1024 * 1024} // 5MB
client={mockClient as any}
onUploadSuccess={(files) => toast.success('Image uploaded!')}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Document Upload</CardTitle>
<CardDescription>
Multiple document types with larger file size limit
</CardDescription>
</CardHeader>
<CardContent>
<FileUploader
maxFiles={5}
bucketName="documents"
path="user-docs"
allowedMimeTypes={[
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
]}
maxFileSize={10 * 1024 * 1024} // 10MB
client={mockClient as any}
onUploadSuccess={(files) =>
toast.success(`${files.length} documents uploaded!`)
}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Batch Upload</CardTitle>
<CardDescription>
Multiple files with no type restrictions
</CardDescription>
</CardHeader>
<CardContent>
<FileUploader
maxFiles={10}
bucketName="general"
allowedMimeTypes={[]} // No restrictions
maxFileSize={50 * 1024 * 1024} // 50MB
client={mockClient as any}
onUploadSuccess={(files) =>
toast.success(`Batch upload complete: ${files.length} files`)
}
/>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>FileUploader Component</CardTitle>
<CardDescription>
Complete API reference for FileUploader component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">FileUploader</h4>
<p className="text-muted-foreground mb-3 text-sm">
A drag-and-drop file uploader with preview, progress tracking, and
Supabase integration.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">maxFiles</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">1</td>
<td className="p-3">Maximum number of files allowed</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">bucketName</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Supabase storage bucket name</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">path</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">undefined</td>
<td className="p-3">
Optional path prefix for uploaded files
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">allowedMimeTypes</td>
<td className="p-3 font-mono text-sm">string[]</td>
<td className="p-3 font-mono text-sm">[]</td>
<td className="p-3">
Array of allowed MIME types (empty = all)
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">maxFileSize</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">Infinity</td>
<td className="p-3">Maximum file size in bytes</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">client</td>
<td className="p-3 font-mono text-sm">SupabaseClient</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Supabase client instance</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">onUploadSuccess</td>
<td className="p-3 font-mono text-sm">
{'(files: string[]) => void'}
</td>
<td className="p-3 font-mono text-sm">undefined</td>
<td className="p-3">Callback when upload succeeds</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">cacheControl</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">3600</td>
<td className="p-3">Cache control in seconds</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use FileUploader</CardTitle>
<CardDescription>
Best practices for file upload interfaces
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use FileUploader For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Profile picture and avatar uploads</li>
<li> Document attachment uploads</li>
<li> Image gallery uploads</li>
<li> File import features</li>
<li> Media content uploads</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Consider Alternatives For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Very large files (use chunked upload)</li>
<li> Real-time collaborative editing</li>
<li> Direct database uploads (use proper storage)</li>
<li> Temporary file sharing (use different patterns)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Configuration Guidelines</CardTitle>
<CardDescription>
How to configure FileUploader effectively
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">File Size Limits</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>
<strong>Images:</strong> 1-5MB for web, 10-20MB for high
quality
</li>
<li>
<strong>Documents:</strong> 10-50MB depending on content
</li>
<li>
<strong>Videos:</strong> 100MB+ (consider chunked upload)
</li>
<li>
<strong>Audio:</strong> 10-50MB for high quality
</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">MIME Type Patterns</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>
Use wildcards: <code>image/*</code>, <code>video/*</code>
</li>
<li>
Specific types: <code>application/pdf</code>
</li>
<li>
Multiple types: <code>['image/jpeg', 'image/png']</code>
</li>
<li> Empty array allows all file types</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Bucket Organization</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Use separate buckets for different content types</li>
<li>
Organize with path prefixes: <code>user-id/category</code>
</li>
<li> Consider public vs private bucket access</li>
<li> Set up proper RLS policies for security</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>User Experience Best Practices</CardTitle>
<CardDescription>
Creating intuitive upload experiences
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Visual Feedback</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Clear drag-and-drop zones with visual cues</li>
<li> Progress indicators during upload</li>
<li> Success/error states with appropriate messaging</li>
<li> File previews when possible (especially images)</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Error Handling</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Clear error messages for size/type violations</li>
<li> Retry mechanisms for network failures</li>
<li> Partial upload recovery when possible</li>
<li> Graceful degradation for unsupported browsers</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Accessibility</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Keyboard navigation support</li>
<li> Screen reader compatible labels</li>
<li> Focus management during upload process</li>
<li> Alternative input methods (click to select)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Security Considerations</CardTitle>
<CardDescription>Keeping file uploads secure</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">File Validation</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Always validate file types on both client and server</li>
<li> Check file contents, not just extensions</li>
<li> Scan for malware when possible</li>
<li> Limit file sizes to prevent DoS attacks</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Storage Security</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Use Row Level Security (RLS) policies</li>
<li> Separate public and private content</li>
<li> Generate unique file names to prevent conflicts</li>
<li> Set up proper bucket permissions</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">User Privacy</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Don't store sensitive files in public buckets</li>
<li> Implement file deletion capabilities</li>
<li> Consider data retention policies</li>
<li> Respect user privacy preferences</li>
</ul>
</div>
</CardContent>
</Card>
</div>
);
return (
<div className="space-y-4">
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<p className="text-sm text-yellow-800">
<strong>Note:</strong> This story uses a mock Supabase client for
demonstration. In your application, use a real Supabase client with
proper authentication and storage configuration.
</p>
</div>
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,552 @@
'use client';
import { useState } from 'react';
import { Badge } from '@kit/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Heading } from '@kit/ui/heading';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface HeadingStoryControls {
customClass: boolean;
showSample: boolean;
}
export default function HeadingStory() {
const [controls, setControls] = useState<HeadingStoryControls>({
customClass: false,
showSample: true,
});
const generateCode = () => {
const importStatement = generateImportStatement(
['Heading'],
'@kit/ui/heading',
);
const headings = [1, 2, 3, 4, 5, 6]
.map((level) => {
const sampleText = controls.showSample
? sampleTexts[level as keyof typeof sampleTexts]
: `Heading Level ${level}`;
const customClassName = controls.customClass
? level === 1
? 'text-primary'
: level === 2
? 'border-b-2 border-primary/20 pb-2'
: level === 3
? 'text-muted-foreground'
: 'text-accent-foreground'
: '';
const propsString = generatePropsString(
{
level: level,
className: customClassName || undefined,
},
{
level: 1,
},
);
return ` <Heading${propsString}>${sampleText}</Heading>`;
})
.join('\n');
const componentCode = `<div className="space-y-6">\n${headings}\n </div>`;
return `${importStatement}\n\n${componentCode}`;
};
const sampleTexts = {
1: 'Main Page Title',
2: 'Section Heading',
3: 'Subsection Title',
4: 'Component Title',
5: 'Minor Heading',
6: 'Small Heading',
};
const levelDescriptions = {
1: 'Primary page title - largest and most prominent',
2: 'Major section headings with bottom border',
3: 'Subsection headings for content organization',
4: 'Component or card titles',
5: 'Minor headings and labels',
6: 'Smallest heading level for subtle emphasis',
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>Heading Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2">
<Switch
id="customClass"
checked={controls.customClass}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, customClass: checked }))
}
/>
<label htmlFor="customClass" className="text-sm">
Add Custom Styling
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="showSample"
checked={controls.showSample}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showSample: checked }))
}
/>
<label htmlFor="showSample" className="text-sm">
Show Sample Text
</label>
</div>
</div>
<div className="space-y-3">
<p className="text-sm font-medium">Heading Level Descriptions:</p>
{Object.entries(levelDescriptions).map(([level, description]) => (
<div key={level} className="bg-muted/50 rounded-lg p-3">
<p className="text-sm font-medium">Level {level}:</p>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
))}
</div>
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardHeader>
<CardTitle>All Heading Levels</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-8">
{[1, 2, 3, 4, 5, 6].map((level) => (
<div key={level} className="space-y-3">
<div className="mb-2 flex items-center gap-2">
<Badge variant="outline">H{level}</Badge>
<span className="text-muted-foreground text-xs">
{levelDescriptions[level as keyof typeof levelDescriptions]}
</span>
</div>
<Heading
level={level as 1 | 2 | 3 | 4 | 5 | 6}
className={
controls.customClass
? 'text-primary border-primary/20 border-b-2 pb-2'
: undefined
}
>
{controls.showSample
? sampleTexts[level as keyof typeof sampleTexts]
: `Heading Level ${level}`}
</Heading>
<div className="bg-muted/50 rounded-lg p-3 text-xs">
<code>
{`<h${level} className="font-heading scroll-m-20 ${
level === 1
? 'text-3xl font-bold tracking-tight lg:text-4xl'
: level === 2
? 'text-2xl font-semibold tracking-tight pb-2'
: level === 3
? 'text-xl font-semibold tracking-tight lg:text-2xl'
: level === 4
? 'text-lg font-semibold tracking-tight lg:text-xl'
: level === 5
? 'text-base font-medium lg:text-lg'
: 'text-base font-medium'
}">`}
{controls.showSample
? sampleTexts[level as keyof typeof sampleTexts]
: `Heading Level ${level}`}
{`</h${level}>`}
</code>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive Heading"
previewDescription="Semantic heading component with responsive typography scaling"
controlsTitle="Configuration"
controlsDescription="Adjust heading level and styling options"
generatedCode={generateCode()}
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Heading Hierarchy</h3>
<Card>
<CardContent className="space-y-4 pt-6">
<Heading level={1}>Page Title (H1)</Heading>
<Heading level={2}>Major Section (H2)</Heading>
<Heading level={3}>Subsection (H3)</Heading>
<Heading level={4}>Component Title (H4)</Heading>
<Heading level={5}>Minor Heading (H5)</Heading>
<Heading level={6}>Small Heading (H6)</Heading>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Content Structure Example
</h3>
<Card>
<CardContent className="space-y-4 pt-6">
<Heading level={1}>Getting Started with React</Heading>
<p className="text-muted-foreground">
Learn the fundamentals of React development and build your
first application.
</p>
<Heading level={2}>Installation</Heading>
<p className="text-muted-foreground text-sm">
Before we begin, you'll need to set up your development
environment.
</p>
<Heading level={3}>Prerequisites</Heading>
<p className="text-muted-foreground text-sm">
Make sure you have Node.js installed on your system.
</p>
<Heading level={4}>Node.js Version</Heading>
<p className="text-muted-foreground text-sm">
We recommend using Node.js version 18 or higher.
</p>
<Heading level={3}>Creating Your Project</Heading>
<p className="text-muted-foreground text-sm">
Use Create React App to bootstrap your new project.
</p>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Styled Headings</h3>
<Card>
<CardContent className="space-y-4 pt-6">
<Heading
level={1}
className="text-gradient bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent"
>
Gradient Heading
</Heading>
<Heading level={2} className="border-l-4 border-blue-500 pl-4">
Accent Border Heading
</Heading>
<Heading
level={3}
className="bg-muted rounded-lg py-3 text-center"
>
Centered with Background
</Heading>
<Heading
level={4}
className="text-muted-foreground tracking-wider uppercase"
>
Uppercase Heading
</Heading>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Blog Post Layout</h3>
<Card>
<CardContent className="space-y-6 pt-6">
<div>
<Heading level={1}>The Future of Web Development</Heading>
<p className="text-muted-foreground mt-2 text-sm">
Published on March 15, 2024 • 5 min read
</p>
</div>
<div className="space-y-4">
<Heading level={2}>Introduction</Heading>
<p className="text-muted-foreground text-sm">
Web development continues to evolve at a rapid pace...
</p>
<Heading level={2}>Key Technologies</Heading>
<div className="space-y-3">
<Heading level={3}>Frontend Frameworks</Heading>
<p className="text-muted-foreground text-sm">
Modern frameworks are becoming more powerful...
</p>
<Heading level={4}>React and Next.js</Heading>
<p className="text-muted-foreground text-sm">
React continues to dominate the frontend landscape...
</p>
<Heading level={4}>Vue and Nuxt</Heading>
<p className="text-muted-foreground text-sm">
Vue.js offers a progressive approach to building UIs...
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Heading Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">level</td>
<td className="p-2 font-mono">1 | 2 | 3 | 4 | 5 | 6</td>
<td className="p-2">1</td>
<td className="p-2">Semantic heading level (h1-h6)</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">children</td>
<td className="p-2 font-mono">React.ReactNode</td>
<td className="p-2">-</td>
<td className="p-2">Heading content</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">className</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Additional CSS classes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Heading Levels</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Level</th>
<th className="p-2 text-left font-medium">Element</th>
<th className="p-2 text-left font-medium">Font Size</th>
<th className="p-2 text-left font-medium">Use Case</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">1</td>
<td className="p-2 font-mono">h1</td>
<td className="p-2 font-mono">text-3xl lg:text-4xl</td>
<td className="p-2">Page titles, main headings</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">2</td>
<td className="p-2 font-mono">h2</td>
<td className="p-2 font-mono">text-2xl lg:text-3xl</td>
<td className="p-2">Major section headings</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">3</td>
<td className="p-2 font-mono">h3</td>
<td className="p-2 font-mono">text-xl lg:text-2xl</td>
<td className="p-2">Subsection headings</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">4</td>
<td className="p-2 font-mono">h4</td>
<td className="p-2 font-mono">text-lg lg:text-xl</td>
<td className="p-2">Component titles, cards</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">5</td>
<td className="p-2 font-mono">h5</td>
<td className="p-2 font-mono">text-base lg:text-lg</td>
<td className="p-2">Minor headings, labels</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">6</td>
<td className="p-2 font-mono">h6</td>
<td className="p-2 font-mono">text-base</td>
<td className="p-2">Small headings, captions</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Typography Features</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Built-in Features</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Responsive sizing</Badge>
<Badge variant="secondary">Font heading family</Badge>
<Badge variant="secondary">Scroll margin</Badge>
<Badge variant="secondary">Semantic HTML</Badge>
<Badge variant="secondary">Tailwind classes</Badge>
</div>
</div>
<div className="space-y-2 text-sm">
<p>
• <strong>scroll-m-20:</strong> Provides space for sticky
headers when scrolling to anchors
</p>
<p>
• <strong>tracking-tight:</strong> Improved letter spacing for
headings
</p>
<p>
• <strong>font-heading:</strong> Uses the heading font family
from theme
</p>
<p>
• <strong>Responsive:</strong> Automatically scales on larger
screens
</p>
</div>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Usage</h3>
<p className="text-muted-foreground mb-4 text-sm">
Use semantic heading levels to create proper document structure
and accessibility.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { Heading } from '@kit/ui/heading';
function Page() {
return (
<div>
<Heading level={1}>Page Title</Heading>
<Heading level={2}>Section Heading</Heading>
<Heading level={3}>Subsection</Heading>
</div>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Custom Styling</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`<Heading
level={2}
className="text-primary border-b-2 border-primary/20 pb-2"
>
Custom Styled Heading
</Heading>
<Heading
level={3}
className="text-center bg-muted rounded-lg py-3"
>
Centered with Background
</Heading>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Accessibility Guidelines
</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Semantic Structure</h4>
<p>• Use heading levels in logical order (don't skip levels)</p>
<p> Start with H1 for the main page title</p>
<p> Use only one H1 per page</p>
<p> Structure headings hierarchically (H1 H2 H3)</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Content Guidelines</h4>
<p> Keep headings concise and descriptive</p>
<p> Avoid using headings just for styling</p>
<p> Use consistent terminology</p>
<p> Consider screen reader users</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">SEO Benefits</h4>
<p> Proper heading structure improves SEO</p>
<p> Search engines use headings to understand content</p>
<p> Headings help with page scanning and navigation</p>
<p> Important keywords in headings carry more weight</p>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Best Practices</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Structure</h4>
<p> Follow logical heading hierarchy</p>
<p> Don't skip heading levels</p>
<p> Use headings to create document outline</p>
<p> Keep headings short and descriptive</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Styling</h4>
<p> Use className prop for custom styles</p>
<p> Maintain visual hierarchy consistency</p>
<p> Consider responsive behavior</p>
<p> Test with different content lengths</p>
</div>
</div>
</div>
</div>
}
/>
);
}
export { HeadingStory };

View File

@@ -0,0 +1,813 @@
'use client';
import { useState } from 'react';
import { LockIcon, ShieldIcon, SmartphoneIcon } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from '@kit/ui/input-otp';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface InputOTPStoryControls {
maxLength: number;
pattern: 'digits' | 'alphanumeric' | 'letters';
disabled: boolean;
showSeparator: boolean;
groupSize: number;
autoSubmit: boolean;
showValue: boolean;
}
const PATTERN_REGEX = {
digits: /^[0-9]+$/,
alphanumeric: /^[a-zA-Z0-9]+$/,
letters: /^[a-zA-Z]+$/,
};
export default function InputOTPStory() {
const [controls, setControls] = useState<InputOTPStoryControls>({
maxLength: 6,
pattern: 'digits',
disabled: false,
showSeparator: true,
groupSize: 3,
autoSubmit: false,
showValue: true,
});
const [otpValue, setOtpValue] = useState('');
const [submittedValue, setSubmittedValue] = useState<string | null>(null);
const generateCode = () => {
const components = ['InputOTP', 'InputOTPGroup', 'InputOTPSlot'];
if (controls.showSeparator) {
components.push('InputOTPSeparator');
}
const importStatement = generateImportStatement(
components,
'@kit/ui/input-otp',
);
const stateImport = "const [value, setValue] = useState('');";
const patternProp =
controls.pattern !== 'digits'
? `REGEXP_ONLY_${controls.pattern.toUpperCase()}`
: undefined;
const otpProps = generatePropsString(
{
maxLength: controls.maxLength,
value: 'value',
onChange: 'setValue',
disabled: controls.disabled ? true : undefined,
pattern: patternProp,
},
{
maxLength: 6,
disabled: false,
},
);
// Generate slots with groups and separators
const totalSlots = controls.maxLength;
const groupSize = controls.groupSize;
const slots = [];
let currentGroupSlots = [];
for (let i = 0; i < totalSlots; i++) {
currentGroupSlots.push(` <InputOTPSlot index={${i}} />`);
// If we've reached group size or it's the last slot
if (currentGroupSlots.length === groupSize || i === totalSlots - 1) {
slots.push(
` <InputOTPGroup>\n${currentGroupSlots.join('\n')}\n </InputOTPGroup>`,
);
// Add separator if not the last group and separators are enabled
if (i < totalSlots - 1 && controls.showSeparator) {
slots.push(' <InputOTPSeparator />');
}
currentGroupSlots = [];
}
}
const otpStructure = `<InputOTP${otpProps}>\n${slots.join('\n')}\n </InputOTP>`;
let patternConstants = '';
if (controls.pattern !== 'digits') {
patternConstants = `\n// Pattern for ${controls.pattern} input\nconst REGEXP_ONLY_${controls.pattern.toUpperCase()} = /${controls.pattern === 'alphanumeric' ? '^[a-zA-Z0-9]+$' : '^[a-zA-Z]+$'}/;\n`;
}
const fullComponent = `${importStatement}\n\n${stateImport}${patternConstants}\n\nfunction OTPInput() {\n return (\n ${otpStructure}\n );\n}`;
return fullComponent;
};
const handleOTPChange = (value: string) => {
setOtpValue(value);
if (controls.autoSubmit && value.length === controls.maxLength) {
setSubmittedValue(value);
setTimeout(() => setSubmittedValue(null), 3000);
}
};
const handleSubmit = () => {
setSubmittedValue(otpValue);
setTimeout(() => setSubmittedValue(null), 3000);
};
const renderOTPSlots = () => {
const slots = [];
const groupSize = controls.groupSize;
const totalSlots = controls.maxLength;
for (let i = 0; i < totalSlots; i++) {
if (i > 0 && i % groupSize === 0 && controls.showSeparator) {
slots.push(<InputOTPSeparator key={`separator-${i}`} />);
}
slots.push(<InputOTPSlot key={i} index={i} />);
}
return slots;
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>OTP Input Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Max Length</label>
<Select
value={controls.maxLength.toString()}
onValueChange={(value) =>
setControls((prev) => ({ ...prev, maxLength: parseInt(value) }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="4">4 digits</SelectItem>
<SelectItem value="5">5 digits</SelectItem>
<SelectItem value="6">6 digits</SelectItem>
<SelectItem value="8">8 digits</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Pattern</label>
<Select
value={controls.pattern}
onValueChange={(value: InputOTPStoryControls['pattern']) =>
setControls((prev) => ({ ...prev, pattern: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="digits">Numbers only</SelectItem>
<SelectItem value="alphanumeric">Alphanumeric</SelectItem>
<SelectItem value="letters">Letters only</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Group Size</label>
<Select
value={controls.groupSize.toString()}
onValueChange={(value) =>
setControls((prev) => ({ ...prev, groupSize: parseInt(value) }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2 digits</SelectItem>
<SelectItem value="3">3 digits</SelectItem>
<SelectItem value="4">4 digits</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2">
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, disabled: checked }))
}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="showSeparator"
checked={controls.showSeparator}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showSeparator: checked }))
}
/>
<label htmlFor="showSeparator" className="text-sm">
Show Separator
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="autoSubmit"
checked={controls.autoSubmit}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, autoSubmit: checked }))
}
/>
<label htmlFor="autoSubmit" className="text-sm">
Auto Submit
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="showValue"
checked={controls.showValue}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showValue: checked }))
}
/>
<label htmlFor="showValue" className="text-sm">
Show Value
</label>
</div>
</div>
{controls.showValue && (
<div className="bg-muted/50 rounded-lg p-3">
<p className="mb-1 text-sm font-medium">Current Value:</p>
<p className="font-mono text-sm">
{otpValue || 'Empty'} ({otpValue.length}/{controls.maxLength})
</p>
</div>
)}
{submittedValue && (
<Card className="border-green-200 bg-green-50">
<CardContent className="pt-3">
<p className="text-sm font-medium text-green-800">
OTP Submitted!
</p>
<p className="font-mono text-sm text-green-700">
{submittedValue}
</p>
</CardContent>
</Card>
)}
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardHeader>
<CardTitle>OTP Input Preview</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="flex flex-col items-center space-y-4">
<InputOTP
maxLength={controls.maxLength}
value={otpValue}
onChange={handleOTPChange}
disabled={controls.disabled}
pattern={PATTERN_REGEX[controls.pattern]}
>
<InputOTPGroup>{renderOTPSlots()}</InputOTPGroup>
</InputOTP>
<div className="flex gap-2">
<Button
onClick={() => setOtpValue('')}
variant="outline"
size="sm"
disabled={controls.disabled || !otpValue}
>
Clear
</Button>
<Button
onClick={handleSubmit}
disabled={
controls.disabled || otpValue.length !== controls.maxLength
}
size="sm"
>
Verify OTP
</Button>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<h4 className="mb-2 font-semibold">Configuration:</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>Length: {controls.maxLength}</div>
<div>Pattern: {controls.pattern}</div>
<div>Group Size: {controls.groupSize}</div>
<div>Separator: {controls.showSeparator ? 'Yes' : 'No'}</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive OTP Input"
previewDescription="One-time password input with customizable length, patterns, and grouping"
controlsTitle="Configuration"
controlsDescription="Adjust OTP input length, pattern validation, and behavior"
generatedCode={generateCode()}
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Authentication Forms</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader className="text-center">
<LockIcon className="text-primary mx-auto mb-2 h-8 w-8" />
<CardTitle>Two-Factor Authentication</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
<p className="text-muted-foreground text-center text-sm">
Enter the 6-digit code from your authenticator app
</p>
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button className="w-full">Verify Code</Button>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<SmartphoneIcon className="text-primary mx-auto mb-2 h-8 w-8" />
<CardTitle>SMS Verification</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
<p className="text-muted-foreground text-center text-sm">
We sent a code to +1 (555) 123-****
</p>
<InputOTP maxLength={4}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
<div className="flex w-full gap-2">
<Button variant="outline" className="flex-1">
Resend Code
</Button>
<Button className="flex-1">Verify</Button>
</div>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Different Patterns</h3>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Numeric Only (Default)</CardTitle>
</CardHeader>
<CardContent className="flex justify-center">
<InputOTP maxLength={6} pattern={PATTERN_REGEX.digits}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Alphanumeric Code</CardTitle>
</CardHeader>
<CardContent className="flex justify-center">
<InputOTP maxLength={8} pattern={PATTERN_REGEX.alphanumeric}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
<InputOTPSlot index={6} />
<InputOTPSlot index={7} />
</InputOTPGroup>
</InputOTP>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Letters Only</CardTitle>
</CardHeader>
<CardContent className="flex justify-center">
<InputOTP maxLength={5} pattern={PATTERN_REGEX.letters}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
</InputOTPGroup>
</InputOTP>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Security Context</h3>
<Card>
<CardHeader className="text-center">
<ShieldIcon className="text-primary mx-auto mb-2 h-12 w-12" />
<CardTitle>Secure Payment Confirmation</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="rounded-lg border-amber-200 bg-amber-50 p-4">
<p className="text-sm text-amber-800">
<strong>Security Alert:</strong> Please confirm this payment
by entering your 6-digit security code.
</p>
</div>
<div className="space-y-2 text-center">
<p className="text-muted-foreground text-sm">
Payment Amount: <strong>$249.99</strong>
</p>
<p className="text-muted-foreground text-sm">
Merchant: TechStore Inc.
</p>
</div>
<div className="flex justify-center">
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1">
Cancel Payment
</Button>
<Button className="flex-1">Confirm Payment</Button>
</div>
</CardContent>
</Card>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">InputOTP Components</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Component</th>
<th className="p-2 text-left font-medium">Props</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">InputOTP</td>
<td className="p-2 font-mono">
maxLength, value, onChange, pattern, disabled
</td>
<td className="p-2">Root OTP input container</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">InputOTPGroup</td>
<td className="p-2 font-mono">className</td>
<td className="p-2">Groups OTP slots together</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">InputOTPSlot</td>
<td className="p-2 font-mono">index, className</td>
<td className="p-2">Individual character input slot</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">InputOTPSeparator</td>
<td className="p-2 font-mono">className</td>
<td className="p-2">Visual separator between groups</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">InputOTP Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">maxLength</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">6</td>
<td className="p-2">Maximum number of characters</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">value</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">''</td>
<td className="p-2">Current OTP value</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">onChange</td>
<td className="p-2 font-mono">
(value: string) ={'>'} void
</td>
<td className="p-2">-</td>
<td className="p-2">Callback when value changes</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">pattern</td>
<td className="p-2 font-mono">RegExp</td>
<td className="p-2">/^[0-9]+$/</td>
<td className="p-2">Pattern for input validation</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">disabled</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Disable the input</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">autoFocus</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Auto focus first slot</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Pattern Examples</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Common Patterns</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Numbers only</Badge>
<Badge variant="secondary">Alphanumeric</Badge>
<Badge variant="secondary">Letters only</Badge>
<Badge variant="secondary">Custom pattern</Badge>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// Numbers only (default)
pattern={/^[0-9]+$/}
// Alphanumeric
pattern={/^[a-zA-Z0-9]+$/}
// Letters only
pattern={/^[a-zA-Z]+$/}
// Custom: Numbers and dashes
pattern={/^[0-9-]+$/}`}
</pre>
</div>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Usage</h3>
<p className="text-muted-foreground mb-4 text-sm">
OTP inputs are commonly used for two-factor authentication, SMS
verification, and secure confirmations.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
} from '@kit/ui/input-otp';
function OTPForm() {
const [value, setValue] = useState('');
return (
<InputOTP
maxLength={6}
value={value}
onChange={setValue}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Form Integration</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { useForm } from 'react-hook-form';
function TwoFactorForm() {
const form = useForm();
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="otp"
render={({ field }) => (
<FormItem>
<FormLabel>Verification Code</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Enter the 6-digit code from your authenticator app.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Verify</Button>
</form>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Security Best Practices
</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Implementation</h4>
<p> Set appropriate maxLength for your use case</p>
<p> Use pattern validation to restrict input types</p>
<p> Implement auto-submit when complete</p>
<p> Provide clear feedback for invalid codes</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">User Experience</h4>
<p> Auto-focus the first input slot</p>
<p> Allow paste operations for convenience</p>
<p> Provide resend functionality with rate limiting</p>
<p> Clear visual feedback for active slots</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Accessibility</h4>
<p> Ensure proper keyboard navigation</p>
<p> Announce state changes to screen readers</p>
<p> Provide clear instructions and labels</p>
<p> Support standard form validation patterns</p>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Common Use Cases</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Authentication</h4>
<p> Two-factor authentication (6 digits)</p>
<p> SMS verification codes (4-6 digits)</p>
<p> Email verification (6-8 characters)</p>
<p> Backup codes (8-12 characters)</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Transactions</h4>
<p> Payment confirmations (6 digits)</p>
<p> Transfer authorizations (4-8 digits)</p>
<p> Account access PINs (4-6 digits)</p>
<p> Secure operations (variable length)</p>
</div>
</div>
</div>
</div>
}
/>
);
}
export { InputOTPStory };

View File

@@ -0,0 +1,784 @@
'use client';
import { useState } from 'react';
import {
Calendar,
CreditCard,
Eye,
EyeOff,
Lock,
Mail,
Phone,
Search,
User,
} from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface InputControls {
type:
| 'text'
| 'email'
| 'password'
| 'number'
| 'tel'
| 'url'
| 'search'
| 'date'
| 'time'
| 'datetime-local';
placeholder: string;
disabled: boolean;
required: boolean;
withLabel: boolean;
labelText: string;
withIcon: boolean;
iconPosition: 'left' | 'right';
// size: 'default' | 'sm' | 'lg'; // Size variants not implemented in component
error: boolean;
helperText: string;
maxLength?: number;
}
const typeOptions = [
{ value: 'text', label: 'Text', description: 'Plain text input' },
{ value: 'email', label: 'Email', description: 'Email address input' },
{
value: 'password',
label: 'Password',
description: 'Password input (hidden)',
},
{ value: 'number', label: 'Number', description: 'Numeric input' },
{ value: 'tel', label: 'Phone', description: 'Phone number input' },
{ value: 'url', label: 'URL', description: 'Website URL input' },
{ value: 'search', label: 'Search', description: 'Search query input' },
{ value: 'date', label: 'Date', description: 'Date picker input' },
{ value: 'time', label: 'Time', description: 'Time picker input' },
{
value: 'datetime-local',
label: 'DateTime',
description: 'Date and time input',
},
] as const;
const iconOptions = [
{ value: 'user', icon: User, label: 'User' },
{ value: 'mail', icon: Mail, label: 'Mail' },
{ value: 'lock', icon: Lock, label: 'Lock' },
{ value: 'search', icon: Search, label: 'Search' },
{ value: 'calendar', icon: Calendar, label: 'Calendar' },
{ value: 'phone', icon: Phone, label: 'Phone' },
{ value: 'card', icon: CreditCard, label: 'Credit Card' },
];
export function InputStory() {
const { controls, updateControl } = useStoryControls<InputControls>({
type: 'text',
placeholder: 'Enter text...',
disabled: false,
required: false,
withLabel: false,
labelText: 'Input Label',
withIcon: false,
iconPosition: 'left',
// size: 'default', // removed - not implemented
error: false,
helperText: '',
maxLength: undefined,
});
const [selectedIcon, setSelectedIcon] = useState('user');
const [showPassword, setShowPassword] = useState(false);
const [inputValue, setInputValue] = useState('');
const selectedIconData = iconOptions.find(
(opt) => opt.value === selectedIcon,
);
const IconComponent = selectedIconData?.icon || User;
const generateCode = () => {
const isPasswordWithToggle =
controls.type === 'password' && controls.withIcon;
const inputProps = {
type: isPasswordWithToggle && showPassword ? 'text' : controls.type,
placeholder: controls.placeholder,
disabled: controls.disabled,
required: controls.required,
maxLength: controls.maxLength,
className: cn(
controls.withIcon && controls.iconPosition === 'left' && 'pl-9',
controls.withIcon && controls.iconPosition === 'right' && 'pr-9',
controls.error && 'border-destructive focus-visible:ring-destructive',
),
};
const propsString = generatePropsString(inputProps, {
type: 'text',
placeholder: '',
disabled: false,
required: false,
maxLength: undefined,
className: '',
});
let code = '';
if (controls.withLabel) {
code += `<div className="space-y-2">\n`;
code += ` <Label htmlFor="input">${controls.labelText}${controls.required ? ' *' : ''}</Label>\n`;
}
if (controls.withIcon) {
code += ` <div className="relative">\n`;
if (controls.iconPosition === 'left') {
const iconName = selectedIconData?.icon.name || 'User';
code += ` <${iconName} className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />\n`;
}
code += ` <Input${propsString} />\n`;
if (controls.type === 'password' && controls.iconPosition === 'right') {
code += ` <Button\n`;
code += ` type="button"\n`;
code += ` variant="ghost"\n`;
code += ` size="sm"\n`;
code += ` className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"\n`;
code += ` onClick={() => setShowPassword(!showPassword)}\n`;
code += ` >\n`;
code += ` {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}\n`;
code += ` </Button>\n`;
} else if (controls.iconPosition === 'right') {
const iconName = selectedIconData?.icon.name || 'User';
code += ` <${iconName} className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />\n`;
}
code += ` </div>\n`;
} else {
const indent = controls.withLabel ? ' ' : '';
code += `${indent}<Input${propsString} />\n`;
}
if (controls.helperText) {
const indent = controls.withLabel ? ' ' : '';
const textColor = controls.error
? 'text-destructive'
: 'text-muted-foreground';
code += `${indent}<p className="${textColor} text-sm">${controls.helperText}</p>\n`;
}
if (controls.withLabel) {
code += `</div>`;
}
return code;
};
const renderPreview = () => {
const isPasswordWithToggle =
controls.type === 'password' && controls.withIcon;
const inputElement = (
<div className="relative">
{controls.withIcon && controls.iconPosition === 'left' && (
<IconComponent className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
)}
<Input
type={isPasswordWithToggle && showPassword ? 'text' : controls.type}
placeholder={controls.placeholder}
disabled={controls.disabled}
required={controls.required}
maxLength={controls.maxLength}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className={cn(
controls.size === 'sm' && 'h-8 text-sm',
controls.size === 'lg' && 'h-10',
controls.withIcon && controls.iconPosition === 'left' && 'pl-9',
controls.withIcon && controls.iconPosition === 'right' && 'pr-9',
controls.error &&
'border-destructive focus-visible:ring-destructive',
)}
/>
{controls.withIcon &&
controls.iconPosition === 'right' &&
(controls.type === 'password' ? (
<Button
type="button"
variant="ghost"
size="sm"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
) : (
<IconComponent className="text-muted-foreground absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2" />
))}
</div>
);
return (
<div className="w-full max-w-sm space-y-2">
{controls.withLabel && (
<Label htmlFor="input">
{controls.labelText}
{controls.required && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
)}
{inputElement}
{controls.helperText && (
<p
className={cn(
'text-sm',
controls.error ? 'text-destructive' : 'text-muted-foreground',
)}
>
{controls.helperText}
</p>
)}
</div>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="type">Input Type</Label>
<SimpleStorySelect
value={controls.type}
onValueChange={(value) => updateControl('type', value)}
options={typeOptions}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="placeholder">Placeholder</Label>
<Input
id="placeholder"
value={controls.placeholder}
onChange={(e) => updateControl('placeholder', e.target.value)}
placeholder="Enter placeholder text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="helperText">Helper Text</Label>
<Input
id="helperText"
value={controls.helperText}
onChange={(e) => updateControl('helperText', e.target.value)}
placeholder="Enter helper text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxLength">Max Length</Label>
<Input
id="maxLength"
type="number"
value={controls.maxLength || ''}
onChange={(e) =>
updateControl(
'maxLength',
e.target.value ? parseInt(e.target.value) : undefined,
)
}
placeholder="No limit"
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="withLabel">With Label</Label>
<Switch
id="withLabel"
checked={controls.withLabel}
onCheckedChange={(checked) => updateControl('withLabel', checked)}
/>
</div>
{controls.withLabel && (
<div className="space-y-2">
<Label htmlFor="labelText">Label Text</Label>
<Input
id="labelText"
value={controls.labelText}
onChange={(e) => updateControl('labelText', e.target.value)}
placeholder="Enter label text"
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="withIcon">With Icon</Label>
<Switch
id="withIcon"
checked={controls.withIcon}
onCheckedChange={(checked) => updateControl('withIcon', checked)}
/>
</div>
{controls.withIcon && (
<>
<div className="space-y-2">
<Label htmlFor="icon">Icon</Label>
<SimpleStorySelect
value={selectedIcon}
onValueChange={setSelectedIcon}
options={iconOptions.map((opt) => ({
value: opt.value,
label: opt.label,
description: `${opt.label} icon`,
}))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="iconPosition">Icon Position</Label>
<SimpleStorySelect
value={controls.iconPosition}
onValueChange={(value) => updateControl('iconPosition', value)}
options={[
{
value: 'left',
label: 'Left',
description: 'Icon before text',
},
{
value: 'right',
label: 'Right',
description: 'Icon after text',
},
]}
/>
</div>
</>
)}
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="required">Required</Label>
<Switch
id="required"
checked={controls.required}
onCheckedChange={(checked) => updateControl('required', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="disabled">Disabled</Label>
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) => updateControl('disabled', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="error">Error State</Label>
<Switch
id="error"
checked={controls.error}
onCheckedChange={(checked) => updateControl('error', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Input Types</CardTitle>
<CardDescription>
Different input types for various data
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="text-input">Text Input</Label>
<Input id="text-input" placeholder="Enter your name" />
</div>
<div className="space-y-2">
<Label htmlFor="email-input">Email Input</Label>
<Input
id="email-input"
type="email"
placeholder="Enter your email"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password-input">Password Input</Label>
<Input
id="password-input"
type="password"
placeholder="Enter password"
/>
</div>
<div className="space-y-2">
<Label htmlFor="number-input">Number Input</Label>
<Input
id="number-input"
type="number"
placeholder="Enter a number"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Input with Icons</CardTitle>
<CardDescription>
Enhanced inputs with icon indicators
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="user-input">Username</Label>
<div className="relative">
<User className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
id="user-input"
className="pl-9"
placeholder="Username"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="search-input">Search</Label>
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
id="search-input"
className="pl-9"
placeholder="Search..."
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email-icon-input">Email with Icon</Label>
<div className="relative">
<Mail className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
id="email-icon-input"
type="email"
className="pl-9"
placeholder="your@email.com"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="phone-input">Phone Number</Label>
<div className="relative">
<Phone className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
id="phone-input"
type="tel"
className="pl-9"
placeholder="(555) 123-4567"
/>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Input States</CardTitle>
<CardDescription>Different input states and feedback</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="success-input">Success State</Label>
<Input
id="success-input"
placeholder="Valid input"
className="border-green-500 focus-visible:ring-green-500"
/>
<p className="text-sm text-green-600">Looks good!</p>
</div>
<div className="space-y-2">
<Label htmlFor="error-input">Error State</Label>
<Input
id="error-input"
placeholder="Invalid input"
className="border-destructive focus-visible:ring-destructive"
/>
<p className="text-destructive text-sm">This field is required</p>
</div>
<div className="space-y-2">
<Label htmlFor="disabled-input">Disabled State</Label>
<Input id="disabled-input" placeholder="Cannot edit" disabled />
<p className="text-muted-foreground text-sm">Field is disabled</p>
</div>
<div className="space-y-2">
<Label htmlFor="readonly-input">Read-only</Label>
<Input
id="readonly-input"
value="Read-only value"
readOnly
className="bg-muted"
/>
<p className="text-muted-foreground text-sm">
Value cannot be changed
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Input Component</CardTitle>
<CardDescription>
Complete API reference for Input component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Input</h4>
<p className="text-muted-foreground mb-3 text-sm">
A form input element for collecting user data with various types
and states.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">type</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">'text'</td>
<td className="p-3">
HTML input type (text, email, password, etc.)
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">placeholder</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Placeholder text when empty</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">disabled</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Disable the input</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">required</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Make the input required</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">readOnly</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Make the input read-only</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">maxLength</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Maximum character length</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">value</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Controlled input value</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">onChange</td>
<td className="p-3 font-mono text-sm">function</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Change event handler</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Inputs</CardTitle>
<CardDescription>Best practices for input usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Inputs For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Single-line text entry (names, emails, passwords)</li>
<li> Numeric values (ages, prices, quantities)</li>
<li> Dates and times</li>
<li> Search queries</li>
<li> Form data collection</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Inputs For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Multi-line text (use textarea instead)</li>
<li> Multiple selections (use select or checkboxes)</li>
<li> Binary choices (use switches or radio buttons)</li>
<li> File uploads (use file input type)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>
Making inputs accessible to all users
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Labels and Descriptions</h4>
<p className="text-muted-foreground text-sm">
Always provide clear labels and helper text. Use required
indicators for mandatory fields.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Error Handling</h4>
<p className="text-muted-foreground text-sm">
Provide clear error messages that explain what went wrong and how
to fix it.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
<p className="text-muted-foreground text-sm">
Ensure inputs are keyboard accessible and follow logical tab
order.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Input Types Guide</CardTitle>
<CardDescription>Choosing the right input type</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="mb-2 text-sm font-semibold">Text Types</h4>
<ul className="space-y-1 text-sm">
<li>
<code>text</code> - General text input
</li>
<li>
<code>email</code> - Email addresses
</li>
<li>
<code>password</code> - Sensitive text
</li>
<li>
<code>url</code> - Website addresses
</li>
<li>
<code>tel</code> - Phone numbers
</li>
</ul>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold">Specialized Types</h4>
<ul className="space-y-1 text-sm">
<li>
<code>number</code> - Numeric values
</li>
<li>
<code>date</code> - Date selection
</li>
<li>
<code>time</code> - Time selection
</li>
<li>
<code>search</code> - Search queries
</li>
<li>
<code>file</code> - File uploads
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,20 @@
import { Loader2 } from 'lucide-react';
interface LoadingFallbackProps {
message?: string;
className?: string;
}
export function LoadingFallback({
message = 'Loading component...',
className = 'flex items-center justify-center py-12',
}: LoadingFallbackProps) {
return (
<div className={className}>
<div className="text-muted-foreground flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span>{message}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,315 @@
'use client';
import { useState } from 'react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface LoadingOverlayControls {
fullPage: boolean;
showChildren: boolean;
isLoading: boolean;
displayLogo: boolean;
}
export function LoadingOverlayStory() {
const { controls, updateControl } = useStoryControls<LoadingOverlayControls>({
fullPage: false,
showChildren: true,
isLoading: false,
displayLogo: false,
});
const [demoLoading, setDemoLoading] = useState(false);
const generateCode = () => {
const propsString = generatePropsString(
{
fullPage: controls.fullPage,
className: controls.fullPage ? undefined : 'h-48',
spinnerClassName: 'h-6 w-6',
},
{
fullPage: true,
className: undefined,
spinnerClassName: undefined,
},
);
const children = controls.showChildren ? '\n Loading your data...\n' : '';
return `<LoadingOverlay${propsString}>${children}</LoadingOverlay>`;
};
const renderPreview = () => {
if (controls.isLoading || demoLoading) {
return (
<LoadingOverlay
fullPage={controls.fullPage}
className={!controls.fullPage ? 'h-48' : undefined}
spinnerClassName="h-6 w-6"
>
{controls.showChildren && 'Loading your data...'}
</LoadingOverlay>
);
}
return (
<div className="bg-muted/20 flex h-48 items-center justify-center rounded-md border">
<div className="space-y-4 text-center">
<p className="text-muted-foreground">Content loaded!</p>
<Button onClick={() => setDemoLoading(true)}>
Show Loading Overlay
</Button>
</div>
</div>
);
};
const renderControls = () => (
<>
<div className="flex items-center justify-between">
<Label htmlFor="isLoading">Show Loading</Label>
<Switch
id="isLoading"
checked={controls.isLoading}
onCheckedChange={(checked) => updateControl('isLoading', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="fullPage">Full Page</Label>
<Switch
id="fullPage"
checked={controls.fullPage}
onCheckedChange={(checked) => updateControl('fullPage', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showChildren">Show Message</Label>
<Switch
id="showChildren"
checked={controls.showChildren}
onCheckedChange={(checked) => updateControl('showChildren', checked)}
/>
</div>
<Separator />
<div className="space-y-2">
<Label>Demo Controls</Label>
<Button
size="sm"
variant="outline"
onClick={() => {
setDemoLoading(true);
setTimeout(() => setDemoLoading(false), 3000);
}}
disabled={demoLoading}
>
{demoLoading ? 'Loading...' : 'Demo 3s Loading'}
</Button>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Container Loading</CardTitle>
<CardDescription>
Loading overlay within a specific container
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative">
<LoadingOverlay fullPage={false} className="h-32">
Processing request...
</LoadingOverlay>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Different Spinner Sizes</CardTitle>
<CardDescription>
Various spinner sizes for different contexts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2 text-center">
<div className="relative h-24 rounded border">
<LoadingOverlay fullPage={false} spinnerClassName="h-4 w-4">
Small
</LoadingOverlay>
</div>
</div>
<div className="space-y-2 text-center">
<div className="relative h-24 rounded border">
<LoadingOverlay fullPage={false} spinnerClassName="h-6 w-6">
Medium
</LoadingOverlay>
</div>
</div>
<div className="space-y-2 text-center">
<div className="relative h-24 rounded border">
<LoadingOverlay fullPage={false} spinnerClassName="h-8 w-8">
Large
</LoadingOverlay>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>LoadingOverlay Component</CardTitle>
<CardDescription>
Complete API reference for LoadingOverlay component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">LoadingOverlay</h4>
<p className="text-muted-foreground mb-3 text-sm">
A loading overlay component with spinner and optional message.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Loading message or content</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">fullPage</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">true</td>
<td className="p-3">Cover entire screen or container</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">spinnerClassName</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">CSS classes for spinner</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use LoadingOverlay</CardTitle>
<CardDescription>Best practices for loading overlays</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use LoadingOverlay For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Async operations that take more than 1-2 seconds</li>
<li> Form submissions and API calls</li>
<li> Page transitions and navigation</li>
<li> File uploads and downloads</li>
<li> Data processing operations</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid LoadingOverlay For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Very quick operations (&lt; 500ms)</li>
<li> Background tasks that don't block UI</li>
<li>• Real-time updates (use skeleton instead)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>UX Guidelines</CardTitle>
<CardDescription>Creating better loading experiences</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Provide Context</h4>
<p className="text-muted-foreground text-sm">
Always include a meaningful message about what's loading.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Use Appropriate Size</h4>
<p className="text-muted-foreground text-sm">
fullPage for navigation, container-scoped for component loading.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Timeout Handling</h4>
<p className="text-muted-foreground text-sm">
Consider showing error states for long-running operations.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,42 @@
'use client';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { cn } from '@kit/ui/utils';
interface PreviewCardProps {
title?: string;
description?: string;
children: React.ReactNode;
className?: string;
contentClassName?: string;
}
export function PreviewCard({
title = 'Preview',
description = 'Interactive component preview',
children,
className,
contentClassName,
}: PreviewCardProps) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<div
className={cn('bg-muted/30 rounded-lg border p-6', contentClassName)}
>
{children}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,897 @@
'use client';
import { useEffect, useState } from 'react';
import { Download, Pause, Play, RotateCcw, Upload, Zap } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Progress } from '@kit/ui/progress';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Slider } from '@kit/ui/slider';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface ProgressControlsProps {
value: number;
max: number;
className: string;
size: 'sm' | 'default' | 'lg';
variant: 'default' | 'success' | 'warning' | 'error';
animated: boolean;
showLabel: boolean;
indeterminate: boolean;
onValueChange: (value: number) => void;
onMaxChange: (max: number) => void;
onClassNameChange: (className: string) => void;
onSizeChange: (size: 'sm' | 'default' | 'lg') => void;
onVariantChange: (
variant: 'default' | 'success' | 'warning' | 'error',
) => void;
onAnimatedChange: (animated: boolean) => void;
onShowLabelChange: (showLabel: boolean) => void;
onIndeterminateChange: (indeterminate: boolean) => void;
}
const progressControls = [
{
name: 'value',
type: 'range',
min: 0,
max: 100,
step: 1,
description: 'Current progress value',
},
{
name: 'max',
type: 'range',
min: 50,
max: 200,
step: 10,
description: 'Maximum progress value',
},
{
name: 'size',
type: 'select',
options: ['sm', 'default', 'lg'],
description: 'Progress bar size',
},
{
name: 'variant',
type: 'select',
options: ['default', 'success', 'warning', 'error'],
description: 'Progress bar color variant',
},
{
name: 'animated',
type: 'boolean',
description: 'Enable animated progress bar',
},
{
name: 'showLabel',
type: 'boolean',
description: 'Show percentage label',
},
{
name: 'indeterminate',
type: 'boolean',
description: 'Indeterminate/loading state',
},
{
name: 'className',
type: 'text',
description: 'Additional CSS classes',
},
];
const sizeClasses = {
sm: 'h-1',
default: 'h-2',
lg: 'h-4',
};
const variantClasses = {
default: '',
success: '[&>*]:bg-green-500',
warning: '[&>*]:bg-yellow-500',
error: '[&>*]:bg-red-500',
};
function ProgressPlayground({
value,
max,
className,
size,
variant,
animated,
showLabel,
indeterminate,
}: ProgressControlsProps) {
const [animatedValue, setAnimatedValue] = useState(0);
useEffect(() => {
if (indeterminate) return;
const timer = setTimeout(() => setAnimatedValue(value), 100);
return () => clearTimeout(timer);
}, [value, indeterminate]);
const displayValue = indeterminate
? undefined
: animated
? animatedValue
: value;
const percentage = indeterminate
? 0
: Math.round((displayValue! / max) * 100);
return (
<div className="space-y-4">
{showLabel && !indeterminate && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Progress</span>
<span className="font-medium">{percentage}%</span>
</div>
)}
<Progress
value={displayValue}
max={max}
className={` ${sizeClasses[size]} ${variantClasses[variant]} ${indeterminate ? 'animate-pulse [&>*]:w-1/3 [&>*]:translate-x-0 [&>*]:animate-pulse' : ''} ${animated ? 'transition-all duration-500 ease-out' : ''} ${className} `}
/>
{showLabel && indeterminate && (
<div className="text-center text-sm">
<span className="text-muted-foreground">Loading...</span>
</div>
)}
</div>
);
}
const examples = [
{
title: 'File Upload Progress',
description: 'Track file upload with success state and percentage display',
component: () => {
const [progress, setProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const startUpload = () => {
setIsUploading(true);
setProgress(0);
const interval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
setIsUploading(false);
clearInterval(interval);
return 100;
}
return prev + Math.random() * 15;
});
}, 300);
};
const reset = () => {
setProgress(0);
setIsUploading(false);
};
return (
<Card className="w-full max-w-md">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Upload className="h-4 w-4" />
Upload Documents
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Progress</span>
<span className="font-medium">{Math.round(progress)}%</span>
</div>
<Progress
value={progress}
className={`h-2 transition-all duration-300 ${
progress === 100 ? '[&>*]:bg-green-500' : ''
}`}
/>
{progress === 100 && (
<p className="text-sm font-medium text-green-600">
Upload completed successfully!
</p>
)}
</div>
<div className="flex gap-2">
<Button
onClick={startUpload}
disabled={isUploading || progress === 100}
size="sm"
>
{isUploading ? 'Uploading...' : 'Start Upload'}
</Button>
<Button onClick={reset} variant="outline" size="sm">
<RotateCcw className="mr-1 h-3 w-3" />
Reset
</Button>
</div>
</CardContent>
</Card>
);
},
},
{
title: 'Download Manager',
description: 'Multiple progress bars with different states and sizes',
component: () => {
const downloads = [
{
id: 1,
name: 'Project Files.zip',
progress: 100,
size: 'default' as const,
variant: 'success' as const,
},
{
id: 2,
name: 'Software Update.dmg',
progress: 67,
size: 'default' as const,
variant: 'default' as const,
},
{
id: 3,
name: 'Documentation.pdf',
progress: 23,
size: 'default' as const,
variant: 'warning' as const,
},
{
id: 4,
name: 'Failed Download.exe',
progress: 45,
size: 'default' as const,
variant: 'error' as const,
},
];
return (
<Card className="w-full max-w-lg">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Download className="h-4 w-4" />
Download Manager
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{downloads.map((download) => (
<div key={download.id} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="truncate font-medium">{download.name}</span>
<span className="text-muted-foreground">
{download.progress}%
</span>
</div>
<Progress
value={download.progress}
className={`h-2 ${variantClasses[download.variant]}`}
/>
<div className="text-muted-foreground flex items-center justify-between text-xs">
<span>
{download.progress === 100
? 'Completed'
: download.variant === 'error'
? 'Failed'
: 'Downloading...'}
</span>
<span>{download.progress}/100</span>
</div>
</div>
))}
</CardContent>
</Card>
);
},
},
{
title: 'Skill Levels',
description:
'Progress bars showing different skill levels with custom styling',
component: () => {
const skills = [
{ name: 'React', level: 90, color: '[&>*]:bg-blue-500' },
{ name: 'TypeScript', level: 85, color: '[&>*]:bg-blue-600' },
{ name: 'Node.js', level: 75, color: '[&>*]:bg-green-600' },
{ name: 'Python', level: 60, color: '[&>*]:bg-yellow-500' },
{ name: 'Go', level: 40, color: '[&>*]:bg-cyan-500' },
];
return (
<Card className="w-full max-w-md">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Zap className="h-4 w-4" />
Skills Overview
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{skills.map((skill) => (
<div key={skill.name} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{skill.name}</span>
<span className="text-muted-foreground">{skill.level}%</span>
</div>
<Progress
value={skill.level}
className={`h-2 ${skill.color}`}
/>
</div>
))}
</CardContent>
</Card>
);
},
},
{
title: 'Loading States',
description: 'Indeterminate progress for unknown durations',
component: () => {
const [states, setStates] = useState({
processing: true,
analyzing: true,
syncing: true,
});
const toggleState = (key: keyof typeof states) => {
setStates((prev) => ({ ...prev, [key]: !prev[key] }));
};
return (
<Card className="w-full max-w-md">
<CardHeader className="pb-3">
<CardTitle className="text-base">Loading Operations</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Processing data...</span>
<Button
variant="ghost"
size="sm"
onClick={() => toggleState('processing')}
>
{states.processing ? (
<Pause className="h-3 w-3" />
) : (
<Play className="h-3 w-3" />
)}
</Button>
</div>
<Progress
value={states.processing ? undefined : 0}
className={`h-2 ${states.processing ? 'animate-pulse [&>*]:w-1/3 [&>*]:translate-x-0 [&>*]:animate-pulse' : ''}`}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Analyzing files...</span>
<Button
variant="ghost"
size="sm"
onClick={() => toggleState('analyzing')}
>
{states.analyzing ? (
<Pause className="h-3 w-3" />
) : (
<Play className="h-3 w-3" />
)}
</Button>
</div>
<Progress
value={states.analyzing ? undefined : 0}
className={`h-2 [&>*]:bg-orange-500 ${states.analyzing ? 'animate-pulse [&>*]:w-1/3 [&>*]:translate-x-0 [&>*]:animate-pulse' : ''}`}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Syncing changes...</span>
<Button
variant="ghost"
size="sm"
onClick={() => toggleState('syncing')}
>
{states.syncing ? (
<Pause className="h-3 w-3" />
) : (
<Play className="h-3 w-3" />
)}
</Button>
</div>
<Progress
value={states.syncing ? undefined : 0}
className={`h-2 [&>*]:bg-purple-500 ${states.syncing ? 'animate-pulse [&>*]:w-1/3 [&>*]:translate-x-0 [&>*]:animate-pulse' : ''}`}
/>
</div>
</CardContent>
</Card>
);
},
},
];
const apiReference = {
title: 'Progress API Reference',
description: 'Complete API documentation for the Progress component.',
props: [
{
name: 'value',
type: 'number | undefined',
default: 'undefined',
description:
'The current progress value. Use undefined for indeterminate state.',
},
{
name: 'max',
type: 'number',
default: '100',
description: 'The maximum progress value.',
},
{
name: 'className',
type: 'string',
description: 'Additional CSS classes to apply to the progress container.',
},
{
name: '...props',
type: 'React.ComponentProps<typeof ProgressPrimitive.Root>',
description:
'All props from Radix UI Progress.Root component including aria-label, aria-labelledby, aria-describedby, etc.',
},
],
examples: [
{
title: 'Basic Usage',
code: `import { Progress } from '@kit/ui/progress';
<Progress value={75} />`,
},
{
title: 'Custom Maximum',
code: `<Progress value={150} max={200} />`,
},
{
title: 'Indeterminate State',
code: `<Progress value={undefined} className="animate-pulse" />`,
},
{
title: 'Custom Styling',
code: `<Progress
value={60}
className="h-4 [&>*]:bg-gradient-to-r [&>*]:from-blue-500 [&>*]:to-purple-600"
/>`,
},
],
};
const usageGuidelines = {
title: 'Progress Usage Guidelines',
description:
'Best practices for implementing progress indicators effectively.',
guidelines: [
{
title: 'When to Use Progress',
items: [
'File uploads, downloads, or data transfers',
'Multi-step processes or forms',
'Loading operations with known duration',
'Skill levels or completion percentages',
'System resource usage (storage, memory)',
],
},
{
title: 'Progress Types',
items: [
'Determinate: Use when you know the total and current progress',
'Indeterminate: Use for unknown durations or when progress cannot be measured',
'Buffering: Show both loaded and buffered content (video players)',
'Stepped: Discrete progress through defined stages',
],
},
{
title: 'Visual Design',
items: [
'Use appropriate height: thin for subtle progress, thick for prominent indicators',
'Choose colors that match the context (success green, warning yellow, error red)',
'Consider animation for smooth visual feedback',
'Provide clear labels showing current state and percentage when helpful',
'Ensure sufficient contrast for accessibility',
],
},
{
title: 'User Experience',
items: [
'Always provide feedback during long operations',
'Show percentage or time estimates when possible',
'Allow users to cancel or pause lengthy operations',
'Use progress bars consistently across similar operations',
'Consider showing multiple progress indicators for complex operations',
],
},
{
title: 'Accessibility',
items: [
'Progress elements are automatically announced by screen readers',
'Provide meaningful aria-label or aria-labelledby attributes',
'Use role="progressbar" for semantic clarity',
'Include text alternatives for purely visual progress indicators',
'Ensure progress updates are announced to assistive technologies',
],
},
],
};
export default function ProgressStory() {
const [controls, setControls] = useState({
value: 65,
max: 100,
className: '',
size: 'default' as 'sm' | 'default' | 'lg',
variant: 'default' as 'default' | 'success' | 'warning' | 'error',
animated: true,
showLabel: true,
indeterminate: false,
});
const generateCode = () => {
const displayValue = controls.indeterminate ? undefined : controls.value;
const sizeClass = sizeClasses[controls.size];
const variantClass = variantClasses[controls.variant];
const animationClass = controls.indeterminate
? 'animate-pulse [&>*]:animate-pulse [&>*]:w-1/3 [&>*]:translate-x-0'
: '';
const transitionClass =
controls.animated && !controls.indeterminate
? 'transition-all duration-500 ease-out'
: '';
const className = [
sizeClass,
variantClass,
animationClass,
transitionClass,
controls.className,
]
.filter(Boolean)
.join(' ');
const propsString = generatePropsString(
{
value: displayValue,
max: controls.max !== 100 ? controls.max : undefined,
className: className || undefined,
},
{
value: undefined,
max: 100,
className: '',
},
);
const importStatement = generateImportStatement(
['Progress'],
'@kit/ui/progress',
);
const progressComponent = `<Progress${propsString} />`;
let fullExample = progressComponent;
// Add label wrapper if showLabel is enabled
if (controls.showLabel) {
const percentage = controls.indeterminate
? 0
: Math.round((displayValue! / controls.max) * 100);
const labelText = controls.indeterminate
? 'Loading...'
: `${percentage}%`;
fullExample = `<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Progress</span>
<span className="font-medium">${labelText}</span>
</div>
${progressComponent}
</div>`;
}
return `${importStatement}
${fullExample}`;
};
const controlsContent = (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="progress-value">Progress Value</Label>
<Slider
id="progress-value"
min={[0]}
max={[controls.max]}
step={[1]}
value={[controls.value]}
onValueChange={([value]) =>
setControls((prev) => ({ ...prev, value }))
}
disabled={controls.indeterminate}
/>
<div className="text-muted-foreground mt-1 flex justify-between text-xs">
<span>0</span>
<span>{controls.value}</span>
<span>{controls.max}</span>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="max-value">Maximum Value</Label>
<Slider
id="max-value"
min={[50]}
max={[200]}
step={[10]}
value={[controls.max]}
onValueChange={([max]) => setControls((prev) => ({ ...prev, max }))}
/>
<div className="text-muted-foreground mt-1 flex justify-between text-xs">
<span>50</span>
<span>{controls.max}</span>
<span>200</span>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<Select
value={controls.size}
onValueChange={(value: 'sm' | 'default' | 'lg') =>
setControls((prev) => ({ ...prev, size: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm">Small</SelectItem>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="lg">Large</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="variant">Variant</Label>
<Select
value={controls.variant}
onValueChange={(value: 'default' | 'success' | 'warning' | 'error') =>
setControls((prev) => ({ ...prev, variant: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="success">Success</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
<SelectItem value="error">Error</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="animated">Animated</Label>
<Switch
id="animated"
checked={controls.animated}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, animated: checked }))
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showLabel">Show Label</Label>
<Switch
id="showLabel"
checked={controls.showLabel}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, showLabel: checked }))
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="indeterminate">Indeterminate</Label>
<Switch
id="indeterminate"
checked={controls.indeterminate}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, indeterminate: checked }))
}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="className">Additional Classes</Label>
<Input
id="className"
value={controls.className}
onChange={(e) =>
setControls((prev) => ({ ...prev, className: e.target.value }))
}
placeholder="Additional CSS classes"
/>
</div>
</div>
);
const previewContent = (
<div className="p-6">
<ProgressPlayground
value={controls.value}
max={controls.max}
className={controls.className}
size={controls.size}
variant={controls.variant}
animated={controls.animated}
showLabel={controls.showLabel}
indeterminate={controls.indeterminate}
onValueChange={(value) => setControls((prev) => ({ ...prev, value }))}
onMaxChange={(max) => setControls((prev) => ({ ...prev, max }))}
onClassNameChange={(className) =>
setControls((prev) => ({ ...prev, className }))
}
onSizeChange={(size) => setControls((prev) => ({ ...prev, size }))}
onVariantChange={(variant) =>
setControls((prev) => ({ ...prev, variant }))
}
onAnimatedChange={(animated) =>
setControls((prev) => ({ ...prev, animated }))
}
onShowLabelChange={(showLabel) =>
setControls((prev) => ({ ...prev, showLabel }))
}
onIndeterminateChange={(indeterminate) =>
setControls((prev) => ({ ...prev, indeterminate }))
}
/>
</div>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive Progress"
previewDescription="Visual indicator showing task completion progress"
controlsTitle="Progress Configuration"
controlsDescription="Customize progress appearance and behavior"
generatedCode={generateCode()}
examples={
<div className="space-y-8">
{examples.map((example, index) => (
<div key={index}>
<h3 className="mb-4 text-lg font-semibold">{example.title}</h3>
<p className="text-muted-foreground mb-4 text-sm">
{example.description}
</p>
<div className="flex justify-center">
<example.component />
</div>
</div>
))}
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">{apiReference.title}</h3>
<p className="text-muted-foreground mb-6 text-sm">
{apiReference.description}
</p>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
{apiReference.props.map((prop, index) => (
<tr key={index} className="border-border/50 border-b">
<td className="p-2 font-mono">{prop.name}</td>
<td className="p-2 font-mono">{prop.type}</td>
<td className="p-2">{(prop as any).default || '-'}</td>
<td className="p-2">{prop.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="space-y-6">
<h3 className="text-lg font-semibold">Code Examples</h3>
{apiReference.examples.map((example, index) => (
<div key={index}>
<h4 className="mb-2 text-base font-medium">{example.title}</h4>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
<code>{example.code}</code>
</pre>
</div>
</div>
))}
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">
{usageGuidelines.title}
</h3>
<p className="text-muted-foreground mb-6 text-sm">
{usageGuidelines.description}
</p>
</div>
{usageGuidelines.guidelines.map((section, index) => (
<div key={index}>
<h4 className="mb-3 text-base font-semibold">{section.title}</h4>
<ul className="space-y-1 text-sm">
{section.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start">
<span className="mt-1.5 mr-2 h-1 w-1 flex-shrink-0 rounded-full bg-current" />
<span>{item}</span>
</li>
))}
</ul>
</div>
))}
</div>
}
/>
);
}

View File

@@ -0,0 +1,854 @@
'use client';
import { useState } from 'react';
import {
CreditCardIcon,
PlaneIcon,
SmartphoneIcon,
TruckIcon,
} from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import {
RadioGroup,
RadioGroupItem,
RadioGroupItemLabel,
} from '@kit/ui/radio-group';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface RadioGroupStoryControls {
orientation: 'vertical' | 'horizontal';
disabled: boolean;
useLabels: boolean;
showValue: boolean;
size: 'sm' | 'md' | 'lg';
}
const paymentMethods = [
{
value: 'card',
label: 'Credit Card',
icon: CreditCardIcon,
description: 'Pay with your credit or debit card',
},
{
value: 'bank',
label: 'Bank Transfer',
icon: CreditCardIcon,
description: 'Direct bank transfer',
},
{
value: 'mobile',
label: 'Mobile Payment',
icon: SmartphoneIcon,
description: 'Pay with mobile wallet',
},
];
const shippingOptions = [
{
value: 'standard',
label: 'Standard Shipping',
icon: TruckIcon,
description: '5-7 business days',
price: 'Free',
},
{
value: 'express',
label: 'Express Shipping',
icon: TruckIcon,
description: '2-3 business days',
price: '$9.99',
},
{
value: 'overnight',
label: 'Overnight Shipping',
icon: PlaneIcon,
description: '1 business day',
price: '$19.99',
},
];
export default function RadioGroupStory() {
const { controls, updateControl } = useStoryControls<RadioGroupStoryControls>(
{
orientation: 'vertical',
disabled: false,
useLabels: false,
showValue: true,
size: 'md',
},
);
const [selectedValue, setSelectedValue] = useState<string>('');
const generateCode = () => {
const propsString = generatePropsString(
{
value: selectedValue || 'option1',
onValueChange: 'setValue',
disabled: controls.disabled,
className:
controls.orientation === 'horizontal' ? 'flex gap-4' : 'space-y-2',
},
{
disabled: false,
},
);
const imports = generateImportStatement(
controls.useLabels
? ['RadioGroup', 'RadioGroupItem', 'RadioGroupItemLabel']
: ['RadioGroup', 'RadioGroupItem'],
'@kit/ui/radio-group',
);
const labelImport = controls.useLabels
? ''
: `\nimport { Label } from '@kit/ui/label';`;
const itemsCode = controls.useLabels
? ` {paymentMethods.map((method) => (
<RadioGroupItemLabel
key={method.value}
selected={selectedValue === method.value}
>
<RadioGroupItem value={method.value} />
<div className="flex items-center gap-3">
<method.icon className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">{method.label}</p>
<p className="text-sm text-muted-foreground">
{method.description}
</p>
</div>
</div>
</RadioGroupItemLabel>
))}`
: ` <div className="flex items-center space-x-2">
<RadioGroupItem value="option1" id="option1" />
<Label htmlFor="option1">Option 1</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option2" id="option2" />
<Label htmlFor="option2">Option 2</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option3" id="option3" />
<Label htmlFor="option3">Option 3</Label>
</div>`;
return `${imports}${labelImport}\n\nfunction PaymentForm() {\n const [selectedValue, setSelectedValue] = useState('${selectedValue || 'option1'}');\n\n return (\n <RadioGroup${propsString}>\n${itemsCode}\n </RadioGroup>\n );\n}`;
};
const sizeClasses = {
sm: 'h-3 w-3',
md: 'h-4 w-4',
lg: 'h-5 w-5',
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>Radio Group Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium">
Orientation
</label>
<Select
value={controls.orientation}
onValueChange={(value: RadioGroupStoryControls['orientation']) =>
updateControl('orientation', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="vertical">Vertical</SelectItem>
<SelectItem value="horizontal">Horizontal</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Size</label>
<Select
value={controls.size}
onValueChange={(value: RadioGroupStoryControls['size']) =>
updateControl('size', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm">Small</SelectItem>
<SelectItem value="md">Medium</SelectItem>
<SelectItem value="lg">Large</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2">
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) => updateControl('disabled', checked)}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="useLabels"
checked={controls.useLabels}
onCheckedChange={(checked) => updateControl('useLabels', checked)}
/>
<label htmlFor="useLabels" className="text-sm">
Enhanced Labels
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="showValue"
checked={controls.showValue}
onCheckedChange={(checked) => updateControl('showValue', checked)}
/>
<label htmlFor="showValue" className="text-sm">
Show Selected Value
</label>
</div>
</div>
{controls.showValue && selectedValue && (
<div className="bg-muted/50 rounded-lg p-3">
<p className="mb-1 text-sm font-medium">Selected Value:</p>
<p className="font-mono text-sm">{selectedValue}</p>
</div>
)}
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardHeader>
<CardTitle>Radio Group Preview</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<Label className="mb-3 block text-base font-semibold">
Choose your preferred option:
</Label>
<RadioGroup
value={selectedValue}
onValueChange={setSelectedValue}
disabled={controls.disabled}
className={
controls.orientation === 'horizontal'
? 'flex flex-wrap gap-4'
: 'space-y-2'
}
>
{controls.useLabels
? paymentMethods.slice(0, 3).map((method) => (
<RadioGroupItemLabel
key={method.value}
selected={selectedValue === method.value}
>
<RadioGroupItem
value={method.value}
className={sizeClasses[controls.size]}
/>
<div className="flex items-center gap-3">
<method.icon className="text-muted-foreground h-5 w-5" />
<div>
<p className="font-medium">{method.label}</p>
<p className="text-muted-foreground text-sm">
{method.description}
</p>
</div>
</div>
</RadioGroupItemLabel>
))
: ['Option 1', 'Option 2', 'Option 3'].map((option, index) => (
<div key={option} className="flex items-center space-x-2">
<RadioGroupItem
value={`option-${index + 1}`}
className={sizeClasses[controls.size]}
/>
<Label htmlFor={`option-${index + 1}`}>{option}</Label>
</div>
))}
</RadioGroup>
</div>
<div className="flex gap-2">
<Button
onClick={() => setSelectedValue('')}
variant="outline"
size="sm"
disabled={controls.disabled || !selectedValue}
>
Clear Selection
</Button>
<Button disabled={controls.disabled || !selectedValue} size="sm">
Confirm Choice
</Button>
</div>
</div>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
generatedCode={generateCode()}
previewTitle="Interactive Radio Group"
previewDescription="Single-selection input control with customizable styling and layouts"
controlsTitle="Configuration"
controlsDescription="Adjust orientation, size, labels, and behavior"
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Radio Groups</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Simple Options</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup defaultValue="option2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="option1" id="simple1" />
<Label htmlFor="simple1">Option 1</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option2" id="simple2" />
<Label htmlFor="simple2">Option 2</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option3" id="simple3" />
<Label htmlFor="simple3">Option 3</Label>
</div>
</RadioGroup>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Horizontal Layout</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup defaultValue="small" className="flex gap-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="small" id="size1" />
<Label htmlFor="size1">Small</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="medium" id="size2" />
<Label htmlFor="size2">Medium</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="large" id="size3" />
<Label htmlFor="size3">Large</Label>
</div>
</RadioGroup>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Payment Method Selection
</h3>
<Card>
<CardHeader>
<CardTitle>Choose Payment Method</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup defaultValue="card" className="space-y-3">
{paymentMethods.map((method) => (
<RadioGroupItemLabel key={method.value} selected={false}>
<RadioGroupItem value={method.value} />
<div className="flex items-center gap-3">
<method.icon className="text-muted-foreground h-5 w-5" />
<div>
<p className="font-medium">{method.label}</p>
<p className="text-muted-foreground text-sm">
{method.description}
</p>
</div>
</div>
</RadioGroupItemLabel>
))}
</RadioGroup>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Shipping Options</h3>
<Card>
<CardHeader>
<CardTitle>Select Shipping Method</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup defaultValue="standard" className="space-y-3">
{shippingOptions.map((option) => (
<RadioGroupItemLabel key={option.value} selected={false}>
<RadioGroupItem value={option.value} />
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-3">
<option.icon className="text-muted-foreground h-5 w-5" />
<div>
<p className="font-medium">{option.label}</p>
<p className="text-muted-foreground text-sm">
{option.description}
</p>
</div>
</div>
<div className="text-right">
<p className="font-semibold">{option.price}</p>
</div>
</div>
</RadioGroupItemLabel>
))}
</RadioGroup>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Settings Form</h3>
<Card>
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<Label className="mb-3 block text-base font-semibold">
Email Frequency
</Label>
<RadioGroup defaultValue="weekly" className="space-y-2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="daily" id="daily" />
<Label htmlFor="daily">Daily digest</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="weekly" id="weekly" />
<Label htmlFor="weekly">Weekly summary</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="monthly" id="monthly" />
<Label htmlFor="monthly">Monthly newsletter</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="never" id="never" />
<Label htmlFor="never">No emails</Label>
</div>
</RadioGroup>
</div>
<div>
<Label className="mb-3 block text-base font-semibold">
Theme Preference
</Label>
<RadioGroup defaultValue="system" className="space-y-2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="light" id="light" />
<Label htmlFor="light">Light theme</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dark" id="dark" />
<Label htmlFor="dark">Dark theme</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="system" id="system" />
<Label htmlFor="system">System preference</Label>
</div>
</RadioGroup>
</div>
</CardContent>
</Card>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">
Radio Group Components
</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Component</th>
<th className="p-2 text-left font-medium">Props</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">RadioGroup</td>
<td className="p-2 font-mono">
value, onValueChange, disabled, name
</td>
<td className="p-2">Root radio group container</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">RadioGroupItem</td>
<td className="p-2 font-mono">value, disabled, id</td>
<td className="p-2">Individual radio button</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">RadioGroupItemLabel</td>
<td className="p-2 font-mono">selected, className</td>
<td className="p-2">Enhanced label with styling</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">RadioGroup Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">value</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Currently selected value</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">onValueChange</td>
<td className="p-2 font-mono">
(value: string) ={'>'} void
</td>
<td className="p-2">-</td>
<td className="p-2">Callback when selection changes</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">defaultValue</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Default selected value</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">disabled</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Disable the entire group</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">name</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">
HTML name attribute for form submission
</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">required</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Mark as required field</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">dir</td>
<td className="p-2 font-mono">'ltr' | 'rtl'</td>
<td className="p-2">'ltr'</td>
<td className="p-2">
Text direction for internationalization
</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">loop</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">true</td>
<td className="p-2">
Whether keyboard navigation should loop
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">RadioGroupItem Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">value</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Value when this item is selected</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">disabled</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Disable this specific item</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">id</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">HTML id for label association</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Layout Options</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Layout Patterns</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Vertical (default)</Badge>
<Badge variant="secondary">Horizontal</Badge>
<Badge variant="secondary">Grid layout</Badge>
<Badge variant="secondary">Enhanced labels</Badge>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// Vertical (default)
<RadioGroup className="space-y-2">
// Horizontal
<RadioGroup className="flex gap-4">
// Grid
<RadioGroup className="grid grid-cols-2 gap-4">
// Enhanced labels
<RadioGroupItemLabel selected={selected}>
<RadioGroupItem value="option" />
<div>Enhanced content</div>
</RadioGroupItemLabel>`}
</pre>
</div>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Usage</h3>
<p className="text-muted-foreground mb-4 text-sm">
Radio groups allow users to select a single option from a list of
mutually exclusive choices.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { RadioGroup, RadioGroupItem } from '@kit/ui/radio-group';
import { Label } from '@kit/ui/label';
function PaymentForm() {
const [paymentMethod, setPaymentMethod] = useState('card');
return (
<RadioGroup value={paymentMethod} onValueChange={setPaymentMethod}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="card" id="card" />
<Label htmlFor="card">Credit Card</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="paypal" id="paypal" />
<Label htmlFor="paypal">PayPal</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="bank" id="bank" />
<Label htmlFor="bank">Bank Transfer</Label>
</div>
</RadioGroup>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Form Integration</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { useForm } from 'react-hook-form';
function SettingsForm() {
const form = useForm();
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Theme Preference</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="light" />
</FormControl>
<FormLabel className="font-normal">Light</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="dark" />
</FormControl>
<FormLabel className="font-normal">Dark</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="system" />
</FormControl>
<FormLabel className="font-normal">System</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Enhanced Labels</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { RadioGroupItemLabel } from '@kit/ui/radio-group';
<RadioGroup value={selected} onValueChange={setSelected}>
{options.map((option) => (
<RadioGroupItemLabel
key={option.value}
selected={selected === option.value}
>
<RadioGroupItem value={option.value} />
<div className="flex items-center gap-3">
<option.icon className="h-5 w-5" />
<div>
<p className="font-medium">{option.label}</p>
<p className="text-sm text-muted-foreground">
{option.description}
</p>
</div>
</div>
</RadioGroupItemLabel>
))}
</RadioGroup>`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Best Practices</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Selection Guidelines</h4>
<p>
Use radio groups for mutually exclusive choices (2-7
options)
</p>
<p>
For single true/false choices, consider using a checkbox or
switch
</p>
<p> For many options (8+), consider using a select dropdown</p>
<p> Always provide a default selection when appropriate</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Layout and Design</h4>
<p> Use vertical layout for better scannability</p>
<p> Group related options together</p>
<p> Provide clear, descriptive labels</p>
<p> Consider using enhanced labels for complex options</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Accessibility</h4>
<p>
Always associate labels with radio buttons using htmlFor/id
</p>
<p> Use fieldset and legend for grouped options</p>
<p> Ensure sufficient color contrast</p>
<p> Test with keyboard navigation and screen readers</p>
</div>
</div>
</div>
</div>
}
/>
);
}
export { RadioGroupStory };

View File

@@ -0,0 +1,841 @@
'use client';
import { useState } from 'react';
import { Crown, Shield, User } from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface SelectControls {
placeholder: string;
disabled: boolean;
required: boolean;
withLabel: boolean;
labelText: string;
size: 'default' | 'sm' | 'lg';
withGroups: boolean;
withSeparators: boolean;
withIcons: boolean;
error: boolean;
helperText: string;
position: 'popper' | 'item-aligned';
}
const sizeOptions = [
{ value: 'sm', label: 'Small', description: '32px height' },
{ value: 'default', label: 'Default', description: '36px height' },
{ value: 'lg', label: 'Large', description: '40px height' },
] as const;
const positionOptions = [
{ value: 'popper', label: 'Popper', description: 'Floating position' },
{
value: 'item-aligned',
label: 'Item Aligned',
description: 'Aligned with trigger',
},
] as const;
// Sample data
const frameworks = [
{ value: 'react', label: 'React', icon: '⚛️' },
{ value: 'vue', label: 'Vue.js', icon: '💚' },
{ value: 'angular', label: 'Angular', icon: '🅰️' },
{ value: 'svelte', label: 'Svelte', icon: '🧡' },
{ value: 'nextjs', label: 'Next.js', icon: '▲' },
];
const countries = [
{ value: 'us', label: 'United States', icon: '🇺🇸' },
{ value: 'uk', label: 'United Kingdom', icon: '🇬🇧' },
{ value: 'ca', label: 'Canada', icon: '🇨🇦' },
{ value: 'au', label: 'Australia', icon: '🇦🇺' },
{ value: 'de', label: 'Germany', icon: '🇩🇪' },
{ value: 'fr', label: 'France', icon: '🇫🇷' },
{ value: 'jp', label: 'Japan', icon: '🇯🇵' },
{ value: 'br', label: 'Brazil', icon: '🇧🇷' },
];
const priorities = [
{ value: 'low', label: 'Low', description: 'Not urgent' },
{ value: 'medium', label: 'Medium', description: 'Standard priority' },
{ value: 'high', label: 'High', description: 'Important' },
{ value: 'urgent', label: 'Urgent', description: 'Critical' },
];
const roles = [
{
group: 'System',
items: [
{ value: 'admin', label: 'Administrator', icon: Crown },
{ value: 'moderator', label: 'Moderator', icon: Shield },
],
},
{
group: 'Users',
items: [
{ value: 'editor', label: 'Editor', icon: User },
{ value: 'viewer', label: 'Viewer', icon: User },
{ value: 'guest', label: 'Guest', icon: User },
],
},
];
export function SelectStory() {
const { controls, updateControl } = useStoryControls<SelectControls>({
placeholder: 'Select an option...',
disabled: false,
required: false,
withLabel: false,
labelText: 'Select Label',
size: 'default',
withGroups: false,
withSeparators: false,
withIcons: false,
error: false,
helperText: '',
position: 'popper',
});
const [selectedValue, setSelectedValue] = useState<string>('');
const generateCode = () => {
const triggerProps = {
className: cn(
controls.size === 'sm' && 'h-8 text-sm',
controls.size === 'lg' && 'h-10',
controls.error && 'border-destructive focus:ring-destructive',
),
disabled: controls.disabled,
required: controls.required,
};
const triggerPropsString = generatePropsString(triggerProps, {
className: '',
disabled: false,
required: false,
});
const contentProps = {
position: controls.position,
};
const contentPropsString = generatePropsString(contentProps, {
position: 'popper',
});
let code = '';
if (controls.withLabel) {
code += `<div className="space-y-2">\n`;
code += ` <Label htmlFor="select">${controls.labelText}${controls.required ? ' *' : ''}</Label>\n`;
}
const indent = controls.withLabel ? ' ' : '';
code += `${indent}<Select value={selectedValue} onValueChange={setSelectedValue}>\n`;
code += `${indent} <SelectTrigger${triggerPropsString}>\n`;
code += `${indent} <SelectValue placeholder="${controls.placeholder}" />\n`;
code += `${indent} </SelectTrigger>\n`;
code += `${indent} <SelectContent${contentPropsString}>\n`;
if (controls.withGroups) {
code += `${indent} <SelectGroup>\n`;
code += `${indent} <SelectLabel>Framework</SelectLabel>\n`;
code += `${indent} <SelectItem value="react">React</SelectItem>\n`;
code += `${indent} <SelectItem value="vue">Vue.js</SelectItem>\n`;
code += `${indent} </SelectGroup>\n`;
if (controls.withSeparators) {
code += `${indent} <SelectSeparator />\n`;
}
code += `${indent} <SelectGroup>\n`;
code += `${indent} <SelectLabel>Meta Framework</SelectLabel>\n`;
code += `${indent} <SelectItem value="nextjs">Next.js</SelectItem>\n`;
code += `${indent} <SelectItem value="nuxt">Nuxt</SelectItem>\n`;
code += `${indent} </SelectGroup>\n`;
} else {
code += `${indent} <SelectItem value="react">React</SelectItem>\n`;
code += `${indent} <SelectItem value="vue">Vue.js</SelectItem>\n`;
code += `${indent} <SelectItem value="angular">Angular</SelectItem>\n`;
code += `${indent} <SelectItem value="svelte">Svelte</SelectItem>\n`;
}
code += `${indent} </SelectContent>\n`;
code += `${indent}</Select>\n`;
if (controls.helperText) {
const textColor = controls.error
? 'text-destructive'
: 'text-muted-foreground';
code += `${indent}<p className="${textColor} text-sm">${controls.helperText}</p>\n`;
}
if (controls.withLabel) {
code += `</div>`;
}
return code;
};
const renderPreview = () => {
return (
<div className="w-full max-w-sm space-y-2">
{controls.withLabel && (
<Label htmlFor="select">
{controls.labelText}
{controls.required && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
)}
<Select value={selectedValue} onValueChange={setSelectedValue}>
<SelectTrigger
className={cn(
controls.size === 'sm' && 'h-8 text-sm',
controls.size === 'lg' && 'h-10',
controls.error && 'border-destructive focus:ring-destructive',
)}
disabled={controls.disabled}
>
<SelectValue placeholder={controls.placeholder} />
</SelectTrigger>
<SelectContent position={controls.position}>
{controls.withGroups ? (
<>
<SelectGroup>
<SelectLabel>Frontend Frameworks</SelectLabel>
{frameworks.slice(0, 3).map((framework) => (
<SelectItem key={framework.value} value={framework.value}>
{controls.withIcons && (
<span className="mr-2">{framework.icon}</span>
)}
{framework.label}
</SelectItem>
))}
</SelectGroup>
{controls.withSeparators && <SelectSeparator />}
<SelectGroup>
<SelectLabel>Meta Frameworks</SelectLabel>
{frameworks.slice(3).map((framework) => (
<SelectItem key={framework.value} value={framework.value}>
{controls.withIcons && (
<span className="mr-2">{framework.icon}</span>
)}
{framework.label}
</SelectItem>
))}
</SelectGroup>
</>
) : (
frameworks.map((framework) => (
<SelectItem key={framework.value} value={framework.value}>
{controls.withIcons && (
<span className="mr-2">{framework.icon}</span>
)}
{framework.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
{controls.helperText && (
<p
className={cn(
'text-sm',
controls.error ? 'text-destructive' : 'text-muted-foreground',
)}
>
{controls.helperText}
</p>
)}
</div>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="position">Dropdown Position</Label>
<SimpleStorySelect
value={controls.position}
onValueChange={(value) => updateControl('position', value)}
options={positionOptions}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="placeholder">Placeholder</Label>
<input
id="placeholder"
className="border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-2xs focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
value={controls.placeholder}
onChange={(e) => updateControl('placeholder', e.target.value)}
placeholder="Enter placeholder text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="helperText">Helper Text</Label>
<input
id="helperText"
className="border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-2xs focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
value={controls.helperText}
onChange={(e) => updateControl('helperText', e.target.value)}
placeholder="Enter helper text"
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="withLabel">With Label</Label>
<Switch
id="withLabel"
checked={controls.withLabel}
onCheckedChange={(checked) => updateControl('withLabel', checked)}
/>
</div>
{controls.withLabel && (
<div className="space-y-2">
<Label htmlFor="labelText">Label Text</Label>
<input
id="labelText"
className="border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-2xs focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
value={controls.labelText}
onChange={(e) => updateControl('labelText', e.target.value)}
placeholder="Enter label text"
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="withGroups">With Groups</Label>
<Switch
id="withGroups"
checked={controls.withGroups}
onCheckedChange={(checked) => updateControl('withGroups', checked)}
/>
</div>
{controls.withGroups && (
<div className="flex items-center justify-between">
<Label htmlFor="withSeparators">With Separators</Label>
<Switch
id="withSeparators"
checked={controls.withSeparators}
onCheckedChange={(checked) =>
updateControl('withSeparators', checked)
}
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="withIcons">With Icons</Label>
<Switch
id="withIcons"
checked={controls.withIcons}
onCheckedChange={(checked) => updateControl('withIcons', checked)}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="required">Required</Label>
<Switch
id="required"
checked={controls.required}
onCheckedChange={(checked) => updateControl('required', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="disabled">Disabled</Label>
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) => updateControl('disabled', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="error">Error State</Label>
<Switch
id="error"
checked={controls.error}
onCheckedChange={(checked) => updateControl('error', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Select</CardTitle>
<CardDescription>Simple select with various options</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="basic-select">Choose Framework</Label>
<Select>
<SelectTrigger id="basic-select">
<SelectValue placeholder="Select framework" />
</SelectTrigger>
<SelectContent>
<SelectItem value="react">React</SelectItem>
<SelectItem value="vue">Vue.js</SelectItem>
<SelectItem value="angular">Angular</SelectItem>
<SelectItem value="svelte">Svelte</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="priority-select">Priority Level</Label>
<Select>
<SelectTrigger id="priority-select">
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
{priorities.map((priority) => (
<SelectItem key={priority.value} value={priority.value}>
<div className="flex flex-col">
<span>{priority.label}</span>
<span className="text-muted-foreground text-xs">
{priority.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Grouped Select</CardTitle>
<CardDescription>
Select with grouped options and separators
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="role-select">User Role</Label>
<Select>
<SelectTrigger id="role-select">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
{roles.map((roleGroup, groupIndex) => (
<div key={roleGroup.group}>
<SelectGroup>
<SelectLabel>{roleGroup.group}</SelectLabel>
{roleGroup.items.map((role) => (
<SelectItem key={role.value} value={role.value}>
<div className="flex items-center gap-2">
<role.icon className="h-4 w-4" />
{role.label}
</div>
</SelectItem>
))}
</SelectGroup>
{groupIndex < roles.length - 1 && <SelectSeparator />}
</div>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="country-select">Country</Label>
<Select>
<SelectTrigger id="country-select">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Popular</SelectLabel>
{countries.slice(0, 4).map((country) => (
<SelectItem key={country.value} value={country.value}>
<span className="mr-2">{country.icon}</span>
{country.label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Others</SelectLabel>
{countries.slice(4).map((country) => (
<SelectItem key={country.value} value={country.value}>
<span className="mr-2">{country.icon}</span>
{country.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Select States</CardTitle>
<CardDescription>Different states and sizes</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="error-select">Error State</Label>
<Select>
<SelectTrigger
id="error-select"
className="border-destructive focus:ring-destructive"
>
<SelectValue placeholder="Please select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>
<p className="text-destructive text-sm">This field is required</p>
</div>
<div className="space-y-2">
<Label htmlFor="disabled-select">Disabled State</Label>
<Select disabled>
<SelectTrigger id="disabled-select">
<SelectValue placeholder="Cannot select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground text-sm">Field is disabled</p>
</div>
<div className="space-y-2">
<Label htmlFor="small-select">Small Size</Label>
<Select>
<SelectTrigger id="small-select" className="h-8 text-sm">
<SelectValue placeholder="Small select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="small1">Small Option 1</SelectItem>
<SelectItem value="small2">Small Option 2</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="large-select">Large Size</Label>
<Select>
<SelectTrigger id="large-select" className="h-10">
<SelectValue placeholder="Large select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="large1">Large Option 1</SelectItem>
<SelectItem value="large2">Large Option 2</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Select Components</CardTitle>
<CardDescription>
Complete API reference for Select components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Select</h4>
<p className="text-muted-foreground mb-3 text-sm">
Root container for the select component. Contains all other select
parts.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">value</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Current selected value</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">onValueChange</td>
<td className="p-3 font-mono text-sm">function</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Callback when value changes</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">disabled</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Disable the select</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">required</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Make the select required</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">SelectTrigger</h4>
<p className="text-muted-foreground mb-3 text-sm">
The button that triggers the select dropdown. Shows the selected
value.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">disabled</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Disable the trigger</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">SelectContent</h4>
<p className="text-muted-foreground mb-3 text-sm">
The dropdown content that contains the selectable items.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 font-mono text-sm">position</td>
<td className="p-3 font-mono text-sm">
'popper' | 'item-aligned'
</td>
<td className="p-3 font-mono text-sm">'popper'</td>
<td className="p-3">Positioning strategy</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Select</CardTitle>
<CardDescription>Best practices for select usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Select For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Choosing one option from many (5+ options)</li>
<li> Space-constrained forms</li>
<li> Hierarchical or grouped options</li>
<li> Options with additional metadata</li>
<li> Searchable lists of items</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Select For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Few options (2-4 items - use radio buttons)</li>
<li> Binary choices (use switch or checkbox)</li>
<li> Multiple selections (use checkbox group)</li>
<li> Critical decisions that need to be visible</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>
Making selects accessible to all users
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
<p className="text-muted-foreground text-sm">
Space/Enter opens the select
<br />
Arrow keys navigate options
<br />
Escape closes the dropdown
<br /> Type to search/filter options
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Screen Readers</h4>
<p className="text-muted-foreground text-sm">
Use clear labels and provide helpful descriptions. Group related
options with SelectLabel.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Error Handling</h4>
<p className="text-muted-foreground text-sm">
Provide clear error messages and visual indicators when validation
fails.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Select Patterns</CardTitle>
<CardDescription>
Common select implementation patterns
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Simple Select</h4>
<p className="text-muted-foreground text-sm">
Basic selection from a flat list of options. Best for
straightforward choices.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Grouped Select</h4>
<p className="text-muted-foreground text-sm">
Organize related options into groups with labels and optional
separators.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Rich Options</h4>
<p className="text-muted-foreground text-sm">
Include icons, descriptions, or other metadata to help users make
informed choices.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Searchable Select</h4>
<p className="text-muted-foreground text-sm">
For long lists, implement search/filtering to help users find
options quickly.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,490 @@
'use client';
import { useMemo } from 'react';
import { faker } from '@faker-js/faker';
import { MoreHorizontal } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface User {
id: string;
name: string;
email: string;
role: 'Admin' | 'User' | 'Editor';
status: 'Active' | 'Inactive' | 'Pending';
createdAt: Date;
}
interface SimpleTableControls {
dataCount: number;
showActions: boolean;
showBadges: boolean;
showCaption: boolean;
}
export function SimpleDataTableStory() {
const { controls, updateControl } = useStoryControls<SimpleTableControls>({
dataCount: 10,
showActions: true,
showBadges: true,
showCaption: false,
});
// Generate stable mock data
const data = useMemo(() => {
faker.seed(controls.dataCount * 123);
return Array.from({ length: controls.dataCount }, (_, i) => ({
id: `user-${i + 1}`,
name: faker.person.fullName(),
email: faker.internet.email(),
role: faker.helpers.arrayElement(['Admin', 'User', 'Editor'] as const),
status: faker.helpers.arrayElement([
'Active',
'Inactive',
'Pending',
] as const),
createdAt: faker.date.past(),
}));
}, [controls.dataCount]);
const renderTable = () => (
<div className="rounded-md border">
<Table>
{controls.showCaption && (
<caption className="text-muted-foreground mt-4 text-sm">
A list of {data.length} users
</caption>
)}
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
{controls.showActions && <TableHead>Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell
colSpan={controls.showActions ? 6 : 5}
className="h-24 text-center"
>
No data available
</TableCell>
</TableRow>
) : (
data.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
{controls.showBadges ? (
<Badge variant="outline">{user.role}</Badge>
) : (
user.role
)}
</TableCell>
<TableCell>
{controls.showBadges ? (
<Badge
variant={
user.status === 'Active' ? 'default' : 'secondary'
}
>
{user.status}
</Badge>
) : (
user.status
)}
</TableCell>
<TableCell>{user.createdAt.toLocaleDateString()}</TableCell>
{controls.showActions && (
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => toast.success(`Editing ${user.name}`)}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => toast.success(`Viewing ${user.name}`)}
>
View Details
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
const generateCode = () => {
return `import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
interface User {
id: string;
name: string;
email: string;
role: string;
status: string;
createdAt: Date;
}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>{user.status}</TableCell>
<TableCell>{user.createdAt.toLocaleDateString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>`;
};
const dataCountOptions = [
{ value: '5', label: '5 records', description: 'Small dataset' },
{ value: '10', label: '10 records', description: 'Default size' },
{ value: '20', label: '20 records', description: 'Medium dataset' },
{ value: '50', label: '50 records', description: 'Large dataset' },
];
const renderPreview = () => renderTable();
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="dataCount">Data Count</Label>
<SimpleStorySelect
value={controls.dataCount.toString()}
onValueChange={(value) => updateControl('dataCount', parseInt(value))}
options={dataCountOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="showCaption">Show Caption</Label>
<Switch
id="showCaption"
checked={controls.showCaption}
onCheckedChange={(checked) => updateControl('showCaption', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showBadges">Show Badges</Label>
<Switch
id="showBadges"
checked={controls.showBadges}
onCheckedChange={(checked) => updateControl('showBadges', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="showActions">Show Actions</Label>
<Switch
id="showActions"
checked={controls.showActions}
onCheckedChange={(checked) => updateControl('showActions', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Table</CardTitle>
<CardDescription>
Simple table with minimal configuration using basic HTML table
components
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.slice(0, 3).map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Empty State</CardTitle>
<CardDescription>
How the table looks when there's no data
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={3} className="h-24 text-center">
No data available
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Table Components API</CardTitle>
<CardDescription>
Complete API reference for the basic table components
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="mb-2 text-sm font-semibold">Components</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left">Component</th>
<th className="p-2 text-left">Description</th>
<th className="p-2 text-left">HTML Element</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-2 font-mono">Table</td>
<td className="p-2">Root table container with styling</td>
<td className="p-2 font-mono">table</td>
</tr>
<tr className="border-b">
<td className="p-2 font-mono">TableHeader</td>
<td className="p-2">Table header section</td>
<td className="p-2 font-mono">thead</td>
</tr>
<tr className="border-b">
<td className="p-2 font-mono">TableBody</td>
<td className="p-2">Table body section</td>
<td className="p-2 font-mono">tbody</td>
</tr>
<tr className="border-b">
<td className="p-2 font-mono">TableRow</td>
<td className="p-2">Table row with hover effects</td>
<td className="p-2 font-mono">tr</td>
</tr>
<tr className="border-b">
<td className="p-2 font-mono">TableHead</td>
<td className="p-2">Table header cell</td>
<td className="p-2 font-mono">th</td>
</tr>
<tr className="border-b">
<td className="p-2 font-mono">TableCell</td>
<td className="p-2">Table data cell</td>
<td className="p-2 font-mono">td</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold">Usage Notes</h4>
<ul className="text-muted-foreground space-y-1 text-sm">
<li> These are basic styled HTML table components</li>
<li> No built-in sorting, filtering, or pagination logic</li>
<li> Use with manual state management for interactive features</li>
<li> All components accept standard HTML table attributes</li>
<li> Styling is handled via Tailwind CSS classes</li>
</ul>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Basic Table Components</CardTitle>
<CardDescription>
Best practices for using the basic table components
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Basic Table Components For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Simple data presentation</li>
<li> Static tables without complex interactions</li>
<li> Quick prototyping and demos</li>
<li> Custom table implementations</li>
<li> When you need full control over table behavior</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Use DataTable Component Instead For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Large datasets that need pagination</li>
<li> Built-in sorting and filtering requirements</li>
<li> Row selection and bulk operations</li>
<li> Column pinning and visibility controls</li>
<li> Advanced table interactions with TanStack Table</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Implementation Patterns</CardTitle>
<CardDescription>
Common patterns for using basic table components
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Basic Structure</h4>
<pre className="bg-muted text-muted-foreground rounded p-2 text-sm">
{`<Table>
<TableHeader>
<TableRow>
<TableHead>Column 1</TableHead>
<TableHead>Column 2</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.value1}</TableCell>
<TableCell>{item.value2}</TableCell>
</TableRow>
))}
</TableBody>
</Table>`}
</pre>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">With Border Styling</h4>
<pre className="bg-muted text-muted-foreground rounded p-2 text-sm">
{`<div className="rounded-md border">
<Table>
{/* table content */}
</Table>
</div>`}
</pre>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,646 @@
'use client';
import { useState } from 'react';
import { UserIcon } from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Skeleton } from '@kit/ui/skeleton';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface SkeletonStoryControls {
animating: boolean;
variant: 'default' | 'rounded' | 'circle';
size: 'sm' | 'md' | 'lg' | 'xl';
showDemo: boolean;
}
export default function SkeletonStory() {
const { controls, updateControl } = useStoryControls<SkeletonStoryControls>({
animating: true,
variant: 'default',
size: 'md',
showDemo: false,
});
const [isLoading, setIsLoading] = useState(true);
const generateCode = () => {
const propsString = generatePropsString(
{
className: `${sizeClasses[controls.size].width} ${sizeClasses[controls.size].height} ${variantClasses[controls.variant]}${!controls.animating ? ' animate-none' : ''}`,
},
{},
);
const imports = generateImportStatement(['Skeleton'], '@kit/ui/skeleton');
return `${imports}\n\nfunction LoadingComponent() {\n return (\n <div className="space-y-3">\n <Skeleton${propsString} />\n <Skeleton className="h-4 w-3/4" />\n <Skeleton className="h-4 w-1/2" />\n </div>\n );\n}`;
};
const sizeClasses = {
sm: { width: 'w-20', height: 'h-4' },
md: { width: 'w-32', height: 'h-5' },
lg: { width: 'w-48', height: 'h-6' },
xl: { width: 'w-64', height: 'h-8' },
};
const variantClasses = {
default: 'rounded-md',
rounded: 'rounded-lg',
circle: 'rounded-full aspect-square',
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>Skeleton Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Variant</label>
<Select
value={controls.variant}
onValueChange={(value: SkeletonStoryControls['variant']) =>
updateControl('variant', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="rounded">Rounded</SelectItem>
<SelectItem value="circle">Circle</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Size</label>
<Select
value={controls.size}
onValueChange={(value: SkeletonStoryControls['size']) =>
updateControl('size', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm">Small</SelectItem>
<SelectItem value="md">Medium</SelectItem>
<SelectItem value="lg">Large</SelectItem>
<SelectItem value="xl">Extra Large</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2">
<Switch
id="animating"
checked={controls.animating}
onCheckedChange={(checked) => updateControl('animating', checked)}
/>
<label htmlFor="animating" className="text-sm">
Animation
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="showDemo"
checked={controls.showDemo}
onCheckedChange={(checked) => updateControl('showDemo', checked)}
/>
<label htmlFor="showDemo" className="text-sm">
Show Loading Demo
</label>
</div>
</div>
{controls.showDemo && (
<div className="bg-muted/50 rounded-lg p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-sm font-medium">Loading Demo:</p>
<Button
size="sm"
variant="outline"
onClick={() => setIsLoading(!isLoading)}
>
{isLoading ? 'Show Content' : 'Show Loading'}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardHeader>
<CardTitle>Skeleton Preview</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<Label className="mb-3 block text-base font-semibold">
Basic Skeleton
</Label>
<div className="flex items-center gap-4">
<Skeleton
className={` ${sizeClasses[controls.size].width} ${sizeClasses[controls.size].height} ${variantClasses[controls.variant]} ${!controls.animating ? 'animate-none' : ''} `}
/>
<div className="text-muted-foreground text-sm">
{controls.variant} variant, {controls.size} size
</div>
</div>
</div>
{controls.showDemo && (
<div>
<Label className="mb-3 block text-base font-semibold">
Loading State Demo
</Label>
<Card>
<CardContent className="p-4">
{isLoading ? (
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-1/4" />
<Skeleton className="h-4 w-1/2" />
</div>
</div>
) : (
<div className="flex items-center space-x-4">
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-full">
<UserIcon className="text-primary h-6 w-6" />
</div>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium">John Doe</p>
<p className="text-muted-foreground text-sm">
Software Engineer
</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
)}
</div>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
generatedCode={generateCode()}
previewTitle="Interactive Skeleton"
previewDescription="Loading placeholder with customizable variants and animation states"
controlsTitle="Configuration"
controlsDescription="Adjust variant, size, and animation behavior"
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Skeletons</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Text Skeletons</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-2/3" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Shape Variants</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<span className="text-sm">Circle</span>
</div>
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-20 rounded-md" />
<span className="text-sm">Rectangle</span>
</div>
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-20 rounded-lg" />
<span className="text-sm">Rounded</span>
</div>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Card Layout Loading</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Profile Card</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4">
<Skeleton className="h-16 w-16 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-3 w-1/2" />
<Skeleton className="h-3 w-1/4" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Article Preview</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Skeleton className="h-32 w-full rounded-lg" />
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
<div className="flex justify-between">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-3 w-16" />
</div>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Data Table Loading</h3>
<Card>
<CardHeader>
<CardTitle>Table Skeleton</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{/* Table header */}
<div className="grid grid-cols-4 gap-4 border-b pb-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-18" />
</div>
{/* Table rows */}
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="grid grid-cols-4 gap-4">
<div className="flex items-center space-x-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-4 w-20" />
</div>
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-8 w-16 rounded" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Dashboard Loading</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4" />
</CardHeader>
<CardContent>
<Skeleton className="mb-2 h-8 w-20" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
</CardHeader>
<CardContent>
<Skeleton className="h-64 w-full rounded-lg" />
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Social Media Feed</h3>
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-4">
<div className="mb-4 flex items-center space-x-4">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<div className="mb-4 space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<Skeleton className="mb-4 h-48 w-full rounded-lg" />
<div className="flex justify-between">
<div className="flex space-x-4">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16" />
</div>
<Skeleton className="h-8 w-12" />
</div>
</CardContent>
</Card>
))}
</div>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Skeleton Component</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Component</th>
<th className="p-2 text-left font-medium">Props</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">Skeleton</td>
<td className="p-2 font-mono">
className, ...HTMLDivElement props
</td>
<td className="p-2">Loading placeholder component</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Skeleton Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">className</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Additional CSS classes</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">...props</td>
<td className="p-2 font-mono">HTMLDivElement</td>
<td className="p-2">-</td>
<td className="p-2">All standard div element props</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Animation States</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Built-in Classes</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">animate-pulse (default)</Badge>
<Badge variant="secondary">animate-none (disabled)</Badge>
<Badge variant="secondary">bg-primary/10 (background)</Badge>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// Default animated
<Skeleton className="h-4 w-32" />
// No animation
<Skeleton className="h-4 w-32 animate-none" />
// Custom background
<Skeleton className="h-4 w-32 bg-muted" />`}
</pre>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Common Shapes</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Shape Utilities</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">rounded-md (default)</Badge>
<Badge variant="secondary">
rounded-full (circle/avatar)
</Badge>
<Badge variant="secondary">rounded-lg (cards/images)</Badge>
<Badge variant="secondary">aspect-square (square)</Badge>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// Text skeleton
<Skeleton className="h-4 w-64" />
// Avatar skeleton
<Skeleton className="h-10 w-10 rounded-full" />
// Image skeleton
<Skeleton className="h-32 w-full rounded-lg" />
// Square skeleton
<Skeleton className="h-16 w-16 rounded-lg" />`}
</pre>
</div>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Usage</h3>
<p className="text-muted-foreground mb-4 text-sm">
Skeleton components provide visual placeholders during content
loading states, maintaining layout structure and improving
perceived performance.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { Skeleton } from '@kit/ui/skeleton';
function LoadingCard() {
return (
<div className="space-y-3">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-32 w-full rounded-lg" />
</div>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Loading State Patterns
</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`function ProfileCard({ isLoading, user }) {
if (isLoading) {
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-1/4" />
<Skeleton className="h-3 w-1/2" />
</div>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-4">
<Avatar src={user.avatar} />
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
</div>
</CardContent>
</Card>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">List Loading</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`function SkeletonList({ count = 5 }) {
return (
<div className="space-y-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-3 w-1/2" />
</div>
<Skeleton className="h-8 w-16" />
</div>
))}
</div>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Best Practices</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Layout Preservation</h4>
<p> Match skeleton dimensions to actual content</p>
<p> Maintain consistent spacing and alignment</p>
<p> Use similar border radius to final content</p>
<p> Preserve container structures</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Performance Considerations</h4>
<p> Use skeletons for content that takes {'>'}200ms to load</p>
<p> Avoid skeletons for instant state changes</p>
<p> Consider progressive loading for better UX</p>
<p> Limit skeleton complexity for performance</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Accessibility</h4>
<p> Add aria-label or aria-describedby for screen readers</p>
<p> Consider aria-live regions for dynamic loading states</p>
<p> Ensure sufficient color contrast</p>
<p> Test with screen reader and keyboard navigation</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Animation Guidelines</h4>
<p> Use default pulse animation for most cases</p>
<p> Disable animation for users who prefer reduced motion</p>
<p> Keep animation subtle and consistent</p>
<p> Consider wave or shimmer effects for long content</p>
</div>
</div>
</div>
</div>
}
/>
);
}
export { SkeletonStory };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
'use client';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import { Spinner } from '@kit/ui/spinner';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface SpinnerControls {
size: 'sm' | 'md' | 'lg' | 'xl';
}
const sizeOptions = [
{ value: 'sm', label: 'Small', description: '16px (h-4 w-4)' },
{ value: 'md', label: 'Medium', description: '24px (h-6 w-6)' },
{ value: 'lg', label: 'Large', description: '32px (h-8 w-8)' },
{ value: 'xl', label: 'Extra Large', description: '48px (h-12 w-12)' },
];
const sizeClassMap = {
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8',
xl: 'h-12 w-12',
};
export function SpinnerStory() {
const { controls, updateControl } = useStoryControls<SpinnerControls>({
size: 'md',
});
const generateCode = () => {
const className = sizeClassMap[controls.size];
const propsString = generatePropsString(
{ className },
{ className: undefined },
);
return `<Spinner${propsString} />`;
};
const renderPreview = () => (
<div className="flex items-center justify-center p-8">
<Spinner className={sizeClassMap[controls.size]} />
</div>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value as any)}
options={sizeOptions}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Spinner Sizes</CardTitle>
<CardDescription>
Different spinner sizes for various use cases
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
<div className="flex flex-col items-center space-y-2">
<Spinner className="h-4 w-4" />
<span className="text-muted-foreground text-xs">Small</span>
</div>
<div className="flex flex-col items-center space-y-2">
<Spinner className="h-6 w-6" />
<span className="text-muted-foreground text-xs">Medium</span>
</div>
<div className="flex flex-col items-center space-y-2">
<Spinner className="h-8 w-8" />
<span className="text-muted-foreground text-xs">Large</span>
</div>
<div className="flex flex-col items-center space-y-2">
<Spinner className="h-12 w-12" />
<span className="text-muted-foreground text-xs">Extra Large</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Usage Context</CardTitle>
<CardDescription>Spinners in different contexts</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Button Loading</h4>
<button className="bg-primary text-primary-foreground inline-flex items-center rounded-md px-4 py-2">
<Spinner className="mr-2 h-4 w-4" />
Loading...
</button>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Card Loading</h4>
<div className="flex items-center justify-center rounded-lg border p-6">
<div className="space-y-2 text-center">
<Spinner className="mx-auto h-6 w-6" />
<p className="text-muted-foreground text-sm">Loading data...</p>
</div>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Page Loading</h4>
<div className="flex items-center justify-center rounded-lg border p-16">
<div className="space-y-4 text-center">
<Spinner className="mx-auto h-8 w-8" />
<p className="text-muted-foreground">Please wait...</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Spinner Component</CardTitle>
<CardDescription>
Complete API reference for Spinner component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Spinner</h4>
<p className="text-muted-foreground mb-3 text-sm">
A spinning loading indicator with accessible markup.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">className</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Additional CSS classes for styling</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">children</td>
<td className="p-3 font-mono text-sm">ReactNode</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Content to display (usually none)</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Spinner</CardTitle>
<CardDescription>
Best practices for loading indicators
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Spinner For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>
Short loading states (button loading, small data fetches)
</li>
<li> Indeterminate progress indicators</li>
<li> Form submissions and API calls</li>
<li> Simple loading states without additional context</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Use Other Patterns For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Long loading processes (use progress bars)</li>
<li> Content loading (use skeleton screens)</li>
<li> File uploads with progress (use progress indicators)</li>
<li> Multi-step processes (use progress stepper)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Size Guidelines</CardTitle>
<CardDescription>Choosing the right spinner size</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Small (h-4 w-4)</h4>
<p className="text-muted-foreground text-sm">
Button loading states, inline loading indicators
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Medium (h-6 w-6)</h4>
<p className="text-muted-foreground text-sm">
Card loading, component-level loading states
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Large (h-8 w-8)</h4>
<p className="text-muted-foreground text-sm">
Page-level loading, important loading states
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Extra Large (h-12 w-12)</h4>
<p className="text-muted-foreground text-sm">
Full-page loading screens, splash screens
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility</CardTitle>
<CardDescription>Making spinners accessible</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Built-in Accessibility</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Includes role="status" for screen readers</li>
<li> SVG is properly hidden from screen readers</li>
<li> Inherits theme colors automatically</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Best Practices</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Provide context with surrounding text</li>
<li> Use aria-label or aria-describedby when needed</li>
<li> Ensure sufficient color contrast</li>
</ul>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,439 @@
'use client';
import { useState } from 'react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Stepper } from '@kit/ui/stepper';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface StepperControls {
variant: 'default' | 'numbers' | 'dots';
currentStep: number;
stepCount: number;
}
const variantOptions = [
{ value: 'default', label: 'Default', description: 'Progress bar style' },
{ value: 'numbers', label: 'Numbers', description: 'Numbered circles' },
{ value: 'dots', label: 'Dots', description: 'Simple dot indicators' },
];
const stepCountOptions = [
{ value: '3', label: '3 steps', description: 'Simple flow' },
{ value: '4', label: '4 steps', description: 'Standard flow' },
{ value: '5', label: '5 steps', description: 'Complex flow' },
{ value: '6', label: '6 steps', description: 'Multi-step process' },
];
export function StepperStory() {
const { controls, updateControl } = useStoryControls<StepperControls>({
variant: 'default',
currentStep: 1,
stepCount: 4,
});
const [interactiveStep, setInteractiveStep] = useState(0);
// Generate step labels based on step count
const generateSteps = (count: number) => {
const baseSteps = [
'Account Setup',
'Personal Info',
'Preferences',
'Review',
'Payment',
'Confirmation',
];
return baseSteps.slice(0, count);
};
const steps = generateSteps(controls.stepCount);
const generateCode = () => {
const propsString = generatePropsString(
{
steps: JSON.stringify(steps),
currentStep: controls.currentStep,
variant: controls.variant,
},
{
steps: undefined,
currentStep: 0,
variant: 'default',
},
);
return `<Stepper${propsString} />`;
};
const renderPreview = () => (
<div className="w-full space-y-4">
<Stepper
steps={steps}
currentStep={controls.currentStep}
variant={controls.variant}
/>
{controls.variant === 'numbers' && (
<div className="text-muted-foreground text-center text-sm">
Step {controls.currentStep + 1} of {steps.length}:{' '}
{steps[controls.currentStep]}
</div>
)}
</div>
);
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="variant">Variant</Label>
<SimpleStorySelect
value={controls.variant}
onValueChange={(value) => updateControl('variant', value as any)}
options={variantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="stepCount">Step Count</Label>
<SimpleStorySelect
value={controls.stepCount.toString()}
onValueChange={(value) => {
const count = parseInt(value);
updateControl('stepCount', count);
// Reset current step if it's beyond the new count
if (controls.currentStep >= count) {
updateControl('currentStep', count - 1);
}
}}
options={stepCountOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="currentStep">Current Step</Label>
<div className="flex flex-wrap gap-1">
{Array.from({ length: controls.stepCount }, (_, i) => (
<Button
key={i}
size="sm"
variant={controls.currentStep === i ? 'default' : 'outline'}
onClick={() => updateControl('currentStep', i)}
className="h-8 w-8 p-0"
>
{i + 1}
</Button>
))}
</div>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Stepper Variants</CardTitle>
<CardDescription>
Different visual styles for step indicators
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Default Progress Bar</h4>
<Stepper
steps={['Setup', 'Configuration', 'Review', 'Complete']}
currentStep={1}
variant="default"
/>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Numbered Steps</h4>
<Stepper
steps={['Setup', 'Configuration', 'Review', 'Complete']}
currentStep={1}
variant="numbers"
/>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Dot Indicators</h4>
<Stepper
steps={['Setup', 'Configuration', 'Review', 'Complete']}
currentStep={1}
variant="dots"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Interactive Stepper</CardTitle>
<CardDescription>
Navigate through steps with controls
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Stepper
steps={['Account', 'Profile', 'Settings', 'Review', 'Done']}
currentStep={interactiveStep}
variant="numbers"
/>
<div className="space-y-2 text-center">
<div className="text-muted-foreground text-sm">
Step {interactiveStep + 1} of 5:{' '}
{
['Account', 'Profile', 'Settings', 'Review', 'Done'][
interactiveStep
]
}
</div>
<div className="flex justify-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() =>
setInteractiveStep(Math.max(0, interactiveStep - 1))
}
disabled={interactiveStep === 0}
>
Previous
</Button>
<Button
size="sm"
onClick={() =>
setInteractiveStep(Math.min(4, interactiveStep + 1))
}
disabled={interactiveStep === 4}
>
Next
</Button>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Different Step Counts</CardTitle>
<CardDescription>
Steppers with various numbers of steps
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<h4 className="text-sm font-semibold">3-Step Process</h4>
<Stepper
steps={['Start', 'Configure', 'Finish']}
currentStep={2}
variant="numbers"
/>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">6-Step Process</h4>
<Stepper
steps={[
'Account',
'Profile',
'Preferences',
'Payment',
'Review',
'Complete',
]}
currentStep={3}
variant="dots"
/>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Stepper Component</CardTitle>
<CardDescription>
Complete API reference for Stepper component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Stepper</h4>
<p className="text-muted-foreground mb-3 text-sm">
A step indicator component for multi-step processes with various
visual styles.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">steps</td>
<td className="p-3 font-mono text-sm">string[]</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Array of step labels</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">currentStep</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">0</td>
<td className="p-3">Index of currently active step</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">variant</td>
<td className="p-3 font-mono text-sm">
'default' | 'numbers' | 'dots'
</td>
<td className="p-3 font-mono text-sm">'default'</td>
<td className="p-3">Visual style variant</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Stepper</CardTitle>
<CardDescription>Best practices for step indicators</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Stepper For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Multi-step forms and wizards</li>
<li> Onboarding processes</li>
<li> Setup and configuration flows</li>
<li> Checkout and registration processes</li>
<li> Sequential task completion</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Don't Use Stepper For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Single-page forms</li>
<li> Non-sequential processes</li>
<li> Navigation between unrelated sections</li>
<li> File upload progress (use progress bar)</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Choosing the Right Variant</CardTitle>
<CardDescription>Guidelines for stepper variants</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Default (Progress Bar)</h4>
<p className="text-muted-foreground text-sm">
Best for linear progress indication with clear visual completion.
Good for forms and simple workflows.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Numbers</h4>
<p className="text-muted-foreground text-sm">
Ideal for complex multi-step processes where step labels are
important. Shows clear progression and allows easy reference to
specific steps.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Dots</h4>
<p className="text-muted-foreground text-sm">
Perfect for compact spaces and when step sequence is more
important than labels. Great for onboarding screens and quick
setup flows.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>UX Best Practices</CardTitle>
<CardDescription>Creating effective step experiences</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Clear Labels</h4>
<p className="text-muted-foreground text-sm">
Use concise, descriptive labels that clearly indicate what each
step contains.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Optimal Step Count</h4>
<p className="text-muted-foreground text-sm">
Keep steps between 3-7 for optimal user comprehension. Break down
complex flows.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Progress Indication</h4>
<p className="text-muted-foreground text-sm">
Show completion status and allow users to understand their
position in the flow.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Navigation Support</h4>
<p className="text-muted-foreground text-sm">
Consider allowing backward navigation to previously completed
steps.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

View File

@@ -0,0 +1,135 @@
'use client';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kit/ui/tabs';
import { CodeCard } from './code-card';
import { ControlPanel } from './control-panel';
import { PreviewCard } from './preview-card';
export interface StoryTab {
id: string;
label: string;
content: React.ReactNode;
}
interface ComponentStoryLayoutProps {
// Playground tab props
preview: React.ReactNode;
controls?: React.ReactNode;
generatedCode?: string;
// Additional tabs
examples?: React.ReactNode;
apiReference?: React.ReactNode;
usageGuidelines?: React.ReactNode;
// Layout customization
defaultTab?: string;
className?: string;
previewClassName?: string;
controlsClassName?: string;
// Card titles
previewTitle?: string;
previewDescription?: string;
controlsTitle?: string;
controlsDescription?: string;
codeTitle?: string;
codeDescription?: string;
}
export function ComponentStoryLayout({
// Playground content
preview,
controls,
generatedCode,
// Tab content
examples,
apiReference,
usageGuidelines,
// Customization
defaultTab = 'playground',
className = '',
previewClassName,
controlsClassName,
// Card titles
previewTitle,
previewDescription,
controlsTitle,
controlsDescription,
codeTitle,
codeDescription,
}: ComponentStoryLayoutProps) {
return (
<div className={className}>
<Tabs defaultValue={defaultTab} className="w-full">
<TabsList>
<TabsTrigger value="playground">Playground</TabsTrigger>
{examples && <TabsTrigger value="examples">Examples</TabsTrigger>}
{apiReference && <TabsTrigger value="api">API Reference</TabsTrigger>}
{usageGuidelines && (
<TabsTrigger value="usage">Usage Guidelines</TabsTrigger>
)}
</TabsList>
{/* Playground Tab */}
<TabsContent value="playground" className="space-y-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Preview */}
<PreviewCard
title={previewTitle}
description={previewDescription}
className={`lg:col-span-2 ${previewClassName || ''}`}
>
{preview}
</PreviewCard>
{/* Controls */}
{controls && (
<ControlPanel
title={controlsTitle}
description={controlsDescription}
className={controlsClassName}
>
{controls}
</ControlPanel>
)}
</div>
{/* Generated Code */}
{generatedCode && (
<CodeCard
title={codeTitle}
description={codeDescription}
code={generatedCode}
/>
)}
</TabsContent>
{/* Examples Tab */}
{examples && (
<TabsContent value="examples" className="space-y-6">
{examples}
</TabsContent>
)}
{/* API Reference Tab */}
{apiReference && (
<TabsContent value="api" className="space-y-6">
{apiReference}
</TabsContent>
)}
{/* Usage Guidelines Tab */}
{usageGuidelines && (
<TabsContent value="usage" className="space-y-6">
{usageGuidelines}
</TabsContent>
)}
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,165 @@
'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@kit/ui/select';
import { cn } from '@kit/ui/utils';
import type { SelectOption } from '../lib/story-utils';
interface StorySelectProps<T = string> {
value: T;
onValueChange: (value: T) => void;
options: SelectOption<T>[];
placeholder?: string;
className?: string;
}
export function StorySelect<T extends string = string>({
value,
onValueChange,
options,
placeholder,
className,
}: StorySelectProps<T>) {
const selectedOption = options.find((opt) => opt.value === value);
return (
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger className={cn('min-h-[3.5rem] py-2', className)}>
<div className="flex w-full items-center">
{selectedOption ? (
<div
className={cn(
'flex w-full',
selectedOption.icon ? 'gap-3' : 'gap-0',
)}
>
{selectedOption.icon && (
<selectedOption.icon
className={cn(
'mt-0.5 h-4 w-4 flex-shrink-0',
selectedOption.color && `text-${selectedOption.color}`,
)}
/>
)}
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm leading-none font-medium">
{selectedOption.label}
</span>
<span className="text-muted-foreground text-xs leading-tight">
{selectedOption.description}
</span>
</div>
</div>
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
</div>
</SelectTrigger>
<SelectContent>
{options.map((option) => {
const Icon = option.icon;
return (
<SelectItem
key={option.value}
value={option.value}
className="min-h-[3.5rem] items-start py-2"
>
<div
className={cn(
'flex w-full',
Icon ? 'items-start gap-3' : 'items-center gap-0',
)}
>
{Icon && (
<Icon
className={cn(
'mt-0.5 h-4 w-4 flex-shrink-0',
option.color && `text-${option.color}`,
)}
/>
)}
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm leading-none font-medium">
{option.label}
</span>
<span className="text-muted-foreground text-xs leading-tight">
{option.description}
</span>
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
);
}
interface SimpleStorySelectProps<T = string> {
value: T;
onValueChange: (value: T) => void;
options: Array<{
value: T;
label: string;
description: string;
}>;
placeholder?: string;
className?: string;
}
export function SimpleStorySelect<T extends string = string>({
value,
onValueChange,
options,
placeholder,
className,
}: SimpleStorySelectProps<T>) {
const selectedOption = options.find((opt) => opt.value === value);
return (
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger className={cn('min-h-[3rem] py-2', className)}>
<div className="flex w-full items-center">
{selectedOption ? (
<div className="flex flex-col items-start gap-0.5">
<span className="text-sm leading-none font-medium">
{selectedOption.label}
</span>
<span className="text-muted-foreground text-xs leading-tight">
{selectedOption.description}
</span>
</div>
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
</div>
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem
key={option.value}
value={option.value}
className="min-h-[3rem] items-start py-2"
>
<div className="flex flex-col items-start gap-0.5">
<span className="text-sm leading-none font-medium">
{option.label}
</span>
<span className="text-muted-foreground text-xs leading-tight">
{option.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@@ -0,0 +1,859 @@
'use client';
import { useState } from 'react';
import {
Bell,
Eye,
Lock,
Mail,
Moon,
Shield,
Volume2,
Wifi,
} from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface SwitchControls {
checked: boolean;
disabled: boolean;
required: boolean;
withLabel: boolean;
labelText: string;
labelPosition: 'left' | 'right' | 'top' | 'bottom';
withDescription: boolean;
description: string;
withIcon: boolean;
size: 'default' | 'sm' | 'lg';
error: boolean;
helperText: string;
}
const sizeOptions = [
{ value: 'sm', label: 'Small', description: '16px height' },
{ value: 'default', label: 'Default', description: '20px height' },
{ value: 'lg', label: 'Large', description: '24px height' },
] as const;
const labelPositionOptions = [
{ value: 'left', label: 'Left', description: 'Label on the left' },
{ value: 'right', label: 'Right', description: 'Label on the right' },
{ value: 'top', label: 'Top', description: 'Label above switch' },
{ value: 'bottom', label: 'Bottom', description: 'Label below switch' },
] as const;
const iconOptions = [
{ value: 'bell', icon: Bell, label: 'Bell' },
{ value: 'mail', icon: Mail, label: 'Mail' },
{ value: 'shield', icon: Shield, label: 'Shield' },
{ value: 'moon', icon: Moon, label: 'Moon' },
{ value: 'wifi', icon: Wifi, label: 'Wifi' },
{ value: 'volume', icon: Volume2, label: 'Volume' },
{ value: 'eye', icon: Eye, label: 'Eye' },
{ value: 'lock', icon: Lock, label: 'Lock' },
];
export function SwitchStory() {
const { controls, updateControl } = useStoryControls<SwitchControls>({
checked: false,
disabled: false,
required: false,
withLabel: false,
labelText: 'Enable notifications',
labelPosition: 'right',
withDescription: false,
description: 'Receive push notifications on your device',
withIcon: false,
size: 'default',
error: false,
helperText: '',
});
const [selectedIcon, setSelectedIcon] = useState('bell');
const selectedIconData = iconOptions.find(
(opt) => opt.value === selectedIcon,
);
const IconComponent = selectedIconData?.icon || Bell;
const generateCode = () => {
const switchProps = {
checked: controls.checked,
disabled: controls.disabled,
required: controls.required,
className: cn(
controls.size === 'sm' && 'h-4 w-7',
controls.size === 'lg' && 'h-6 w-11',
controls.error && 'data-[state=checked]:bg-destructive',
),
};
const switchPropsString = generatePropsString(switchProps, {
checked: false,
disabled: false,
required: false,
className: '',
});
let code = '';
if (controls.withLabel) {
const isVertical =
controls.labelPosition === 'top' || controls.labelPosition === 'bottom';
const containerClass = isVertical
? 'space-y-2'
: 'flex items-center space-x-3';
if (controls.labelPosition === 'top') {
containerClass.replace('space-y-2', 'space-y-2');
} else if (controls.labelPosition === 'bottom') {
containerClass.replace(
'space-y-2',
'flex flex-col-reverse space-y-2 space-y-reverse',
);
} else if (controls.labelPosition === 'left') {
containerClass.replace(
'space-x-3',
'flex-row-reverse space-x-3 space-x-reverse',
);
}
code += `<div className="${containerClass}">\n`;
if (
controls.labelPosition === 'top' ||
controls.labelPosition === 'left'
) {
code += ` <div className="space-y-1">\n`;
code += ` <Label htmlFor="switch" className="${controls.labelPosition === 'left' ? 'text-sm font-medium' : ''}">\n`;
if (controls.withIcon) {
const iconName = selectedIconData?.icon.name || 'Bell';
code += ` <${iconName} className="mr-2 h-4 w-4 inline" />\n`;
}
code += ` ${controls.labelText}${controls.required ? ' *' : ''}\n`;
code += ` </Label>\n`;
if (controls.withDescription) {
code += ` <p className="text-muted-foreground text-sm">${controls.description}</p>\n`;
}
code += ` </div>\n`;
}
code += ` <Switch${switchPropsString} />\n`;
if (
controls.labelPosition === 'right' ||
controls.labelPosition === 'bottom'
) {
code += ` <div className="space-y-1">\n`;
code += ` <Label htmlFor="switch" className="${controls.labelPosition === 'right' ? 'text-sm font-medium' : ''}">\n`;
if (controls.withIcon) {
const iconName = selectedIconData?.icon.name || 'Bell';
code += ` <${iconName} className="mr-2 h-4 w-4 inline" />\n`;
}
code += ` ${controls.labelText}${controls.required ? ' *' : ''}\n`;
code += ` </Label>\n`;
if (controls.withDescription) {
code += ` <p className="text-muted-foreground text-sm">${controls.description}</p>\n`;
}
code += ` </div>\n`;
}
code += `</div>`;
} else {
code += `<Switch${switchPropsString} />`;
}
if (controls.helperText) {
const indent = controls.withLabel ? '' : '';
const textColor = controls.error
? 'text-destructive'
: 'text-muted-foreground';
code += `\n${indent}<p className="${textColor} text-sm">${controls.helperText}</p>`;
}
return code;
};
const renderPreview = () => {
const switchElement = (
<Switch
checked={controls.checked}
onCheckedChange={(checked) => updateControl('checked', checked)}
disabled={controls.disabled}
id="switch"
className={cn(
controls.size === 'sm' && 'h-4 w-7',
controls.size === 'lg' && 'h-6 w-11',
controls.error && 'data-[state=checked]:bg-destructive',
)}
/>
);
const labelElement = controls.withLabel && (
<div className="space-y-1">
<Label
htmlFor="switch"
className={cn(
controls.labelPosition === 'left' ||
controls.labelPosition === 'right'
? 'text-sm font-medium'
: '',
)}
>
{controls.withIcon && (
<IconComponent className="mr-2 inline h-4 w-4" />
)}
{controls.labelText}
{controls.required && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
{controls.withDescription && (
<p className="text-muted-foreground text-sm">
{controls.description}
</p>
)}
</div>
);
if (!controls.withLabel) {
return (
<div className="space-y-2">
{switchElement}
{controls.helperText && (
<p
className={cn(
'text-sm',
controls.error ? 'text-destructive' : 'text-muted-foreground',
)}
>
{controls.helperText}
</p>
)}
</div>
);
}
const isVertical =
controls.labelPosition === 'top' || controls.labelPosition === 'bottom';
const containerClass = cn(
isVertical ? 'space-y-2' : 'flex items-center space-x-3',
controls.labelPosition === 'bottom' && 'flex-col-reverse space-y-reverse',
controls.labelPosition === 'left' && 'flex-row-reverse space-x-reverse',
);
return (
<div className="space-y-2">
<div className={containerClass}>
{(controls.labelPosition === 'top' ||
controls.labelPosition === 'left') &&
labelElement}
{switchElement}
{(controls.labelPosition === 'right' ||
controls.labelPosition === 'bottom') &&
labelElement}
</div>
{controls.helperText && (
<p
className={cn(
'text-sm',
controls.error ? 'text-destructive' : 'text-muted-foreground',
)}
>
{controls.helperText}
</p>
)}
</div>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="checked">Checked State</Label>
<Switch
id="checked"
checked={controls.checked}
onCheckedChange={(checked) => updateControl('checked', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="withLabel">With Label</Label>
<Switch
id="withLabel"
checked={controls.withLabel}
onCheckedChange={(checked) => updateControl('withLabel', checked)}
/>
</div>
{controls.withLabel && (
<>
<div className="space-y-2">
<Label htmlFor="labelText">Label Text</Label>
<Input
id="labelText"
value={controls.labelText}
onChange={(e) => updateControl('labelText', e.target.value)}
placeholder="Enter label text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="labelPosition">Label Position</Label>
<SimpleStorySelect
value={controls.labelPosition}
onValueChange={(value) => updateControl('labelPosition', value)}
options={labelPositionOptions}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="withDescription">With Description</Label>
<Switch
id="withDescription"
checked={controls.withDescription}
onCheckedChange={(checked) =>
updateControl('withDescription', checked)
}
/>
</div>
{controls.withDescription && (
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={controls.description}
onChange={(e) => updateControl('description', e.target.value)}
placeholder="Enter description text"
/>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="withIcon">With Icon</Label>
<Switch
id="withIcon"
checked={controls.withIcon}
onCheckedChange={(checked) => updateControl('withIcon', checked)}
/>
</div>
{controls.withIcon && (
<div className="space-y-2">
<Label htmlFor="icon">Icon</Label>
<SimpleStorySelect
value={selectedIcon}
onValueChange={setSelectedIcon}
options={iconOptions.map((opt) => ({
value: opt.value,
label: opt.label,
description: `${opt.label} icon`,
}))}
/>
</div>
)}
</>
)}
<div className="space-y-2">
<Label htmlFor="helperText">Helper Text</Label>
<Input
id="helperText"
value={controls.helperText}
onChange={(e) => updateControl('helperText', e.target.value)}
placeholder="Enter helper text"
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="required">Required</Label>
<Switch
id="required"
checked={controls.required}
onCheckedChange={(checked) => updateControl('required', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="disabled">Disabled</Label>
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) => updateControl('disabled', checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="error">Error State</Label>
<Switch
id="error"
checked={controls.error}
onCheckedChange={(checked) => updateControl('error', checked)}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Switches</CardTitle>
<CardDescription>Simple on/off toggles</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-center space-x-3">
<Switch id="basic-1" />
<Label htmlFor="basic-1">Default switch</Label>
</div>
<div className="flex items-center space-x-3">
<Switch id="basic-2" defaultChecked />
<Label htmlFor="basic-2">Checked by default</Label>
</div>
<div className="flex items-center space-x-3">
<Switch id="basic-3" disabled />
<Label htmlFor="basic-3">Disabled switch</Label>
</div>
<div className="flex items-center space-x-3">
<Switch id="basic-4" disabled defaultChecked />
<Label htmlFor="basic-4">Disabled & checked</Label>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Switch Sizes</CardTitle>
<CardDescription>
Different sizes for various contexts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-6">
<div className="flex items-center space-x-2">
<Switch className="h-4 w-7" />
<Label className="text-sm">Small</Label>
</div>
<div className="flex items-center space-x-2">
<Switch />
<Label className="text-sm">Default</Label>
</div>
<div className="flex items-center space-x-2">
<Switch className="h-6 w-11" />
<Label className="text-sm">Large</Label>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Label Positions</CardTitle>
<CardDescription>Different label arrangements</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label>Label on top</Label>
<Switch />
</div>
<div className="flex flex-col-reverse space-y-2 space-y-reverse">
<Label>Label on bottom</Label>
<Switch />
</div>
<div className="flex items-center space-x-3">
<Switch />
<Label>Label on right</Label>
</div>
<div className="flex flex-row-reverse items-center space-x-3 space-x-reverse">
<Switch />
<Label>Label on left</Label>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Settings Panel</CardTitle>
<CardDescription>Real-world usage example</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center text-sm font-medium">
<Bell className="mr-2 h-4 w-4" />
Push Notifications
</Label>
<p className="text-muted-foreground text-sm">
Get notified about important updates
</p>
</div>
<Switch />
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center text-sm font-medium">
<Mail className="mr-2 h-4 w-4" />
Email Notifications
</Label>
<p className="text-muted-foreground text-sm">
Receive updates via email
</p>
</div>
<Switch defaultChecked />
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center text-sm font-medium">
<Moon className="mr-2 h-4 w-4" />
Dark Mode
</Label>
<p className="text-muted-foreground text-sm">
Switch to dark theme
</p>
</div>
<Switch />
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="flex items-center text-sm font-medium">
<Shield className="mr-2 h-4 w-4" />
Two-Factor Authentication
</Label>
<p className="text-muted-foreground text-sm">
Add an extra layer of security
</p>
</div>
<Switch />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Form Integration</CardTitle>
<CardDescription>Switches in forms with validation</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="terms">
Accept Terms & Conditions
<span className="text-destructive ml-1">*</span>
</Label>
<Switch id="terms" />
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="marketing">Subscribe to marketing emails</Label>
<Switch id="marketing" defaultChecked />
</div>
<p className="text-muted-foreground text-sm">
You can unsubscribe at any time
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="error-switch">
Enable feature (error state)
</Label>
<Switch
id="error-switch"
className="data-[state=checked]:bg-destructive"
/>
</div>
<p className="text-destructive text-sm">
This feature is currently unavailable
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Switch Component</CardTitle>
<CardDescription>
Complete API reference for Switch component
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">Switch</h4>
<p className="text-muted-foreground mb-3 text-sm">
A toggle switch component for boolean states. Built on Radix UI
Switch primitive.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">checked</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Controlled checked state</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">defaultChecked</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">
Default checked state (uncontrolled)
</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">onCheckedChange</td>
<td className="p-3 font-mono text-sm">function</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Callback when checked state changes</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">disabled</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Disable the switch</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">required</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">Make the switch required</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">name</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Name attribute for form submission</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">value</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">'on'</td>
<td className="p-3">
Value for form submission when checked
</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">id</td>
<td className="p-3 font-mono text-sm">string</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">HTML id attribute</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Switch</CardTitle>
<CardDescription>Best practices for switch usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Switch For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Binary on/off states (enable/disable features)</li>
<li> Settings and preferences</li>
<li> Immediate state changes with visible effect</li>
<li> Mobile-friendly toggle controls</li>
<li> When space is limited</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
Avoid Switch For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Multiple choice selections (use radio buttons)</li>
<li> Actions that require confirmation</li>
<li> States that need to be submitted in a form</li>
<li> When the change isn't immediate or obvious</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Switch vs Checkbox</CardTitle>
<CardDescription>When to use each component</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="mb-2 text-sm font-semibold">Use Switch When</h4>
<ul className="space-y-1 text-sm">
<li> Changes take effect immediately</li>
<li> Controlling system settings</li>
<li> Mobile interfaces</li>
<li> Binary state is obvious</li>
</ul>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold">Use Checkbox When</h4>
<ul className="space-y-1 text-sm">
<li> Part of a form submission</li>
<li> Multiple selections allowed</li>
<li> Requires explicit confirmation</li>
<li> Agreement/consent scenarios</li>
</ul>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>
Making switches accessible to all users
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
<p className="text-muted-foreground text-sm">
Tab to focus the switch
<br />
Space or Enter to toggle state
<br /> Arrow keys when part of a radio group
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Screen Reader Support</h4>
<p className="text-muted-foreground text-sm">
Always provide clear labels and descriptions. Use ARIA attributes
appropriately.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Visual Design</h4>
<p className="text-muted-foreground text-sm">
Ensure sufficient color contrast and provide visual feedback for
all states.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Common Patterns</CardTitle>
<CardDescription>
Typical switch implementation patterns
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Settings Panel</h4>
<p className="text-muted-foreground text-sm">
Group related switches with descriptive labels and help text.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Feature Toggles</h4>
<p className="text-muted-foreground text-sm">
Enable/disable application features with immediate visual
feedback.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Permission Controls</h4>
<p className="text-muted-foreground text-sm">
Control user permissions and privacy settings with clear labeling.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,964 @@
'use client';
import { useState } from 'react';
import {
EditIcon,
FileTextIcon,
MessageCircleIcon,
SendIcon,
StarIcon,
} from 'lucide-react';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import {
generateImportStatement,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface TextareaStoryControls {
disabled: boolean;
readonly: boolean;
required: boolean;
resize: 'none' | 'vertical' | 'horizontal' | 'both';
size: 'sm' | 'md' | 'lg';
showCharCount: boolean;
maxLength: number;
}
export default function TextareaStory() {
const { controls, updateControl } = useStoryControls<TextareaStoryControls>({
disabled: false,
readonly: false,
required: false,
resize: 'vertical',
size: 'md',
showCharCount: false,
maxLength: 500,
});
const [textValue, setTextValue] = useState('');
const [feedbackValue, setFeedbackValue] = useState('');
const [commentValue, setCommentValue] = useState('');
const generateCode = () => {
const propsString = generatePropsString(
{
placeholder: 'Type your message here...',
disabled: controls.disabled,
readOnly: controls.readonly,
required: controls.required,
maxLength: controls.showCharCount ? controls.maxLength : undefined,
className: `${sizeClasses[controls.size]} ${resizeClasses[controls.resize]}`,
},
{
disabled: false,
readOnly: false,
required: false,
},
);
const imports = generateImportStatement(['Textarea'], '@kit/ui/textarea');
const labelImport = `\nimport { Label } from '@kit/ui/label';`;
return `${imports}${labelImport}\n\nfunction MessageForm() {\n const [message, setMessage] = useState('');\n\n return (\n <div className="space-y-2">\n <Label htmlFor="message">Message</Label>\n <Textarea\n id="message"\n value={message}\n onChange={(e) => setMessage(e.target.value)}${propsString}\n />\n ${controls.showCharCount ? `<div className="text-xs text-muted-foreground text-right">\n {message.length} / ${controls.maxLength}\n </div>` : ''}\n </div>\n );\n}`;
};
const sizeClasses = {
sm: 'min-h-[50px] text-sm',
md: 'min-h-[80px] text-sm',
lg: 'min-h-[120px] text-base',
};
const resizeClasses = {
none: 'resize-none',
vertical: 'resize-y',
horizontal: 'resize-x',
both: 'resize',
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>Textarea Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Resize</label>
<Select
value={controls.resize}
onValueChange={(value: TextareaStoryControls['resize']) =>
updateControl('resize', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="vertical">Vertical</SelectItem>
<SelectItem value="horizontal">Horizontal</SelectItem>
<SelectItem value="both">Both</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Size</label>
<Select
value={controls.size}
onValueChange={(value: TextareaStoryControls['size']) =>
updateControl('size', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm">Small</SelectItem>
<SelectItem value="md">Medium</SelectItem>
<SelectItem value="lg">Large</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<label className="mb-2 block text-sm font-medium">
Max Length: {controls.maxLength}
</label>
<input
type="range"
min="50"
max="1000"
step="50"
value={controls.maxLength}
onChange={(e) => updateControl('maxLength', Number(e.target.value))}
className="w-full"
/>
</div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2">
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(checked) => updateControl('disabled', checked)}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="readonly"
checked={controls.readonly}
onCheckedChange={(checked) => updateControl('readonly', checked)}
/>
<label htmlFor="readonly" className="text-sm">
Readonly
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="required"
checked={controls.required}
onCheckedChange={(checked) => updateControl('required', checked)}
/>
<label htmlFor="required" className="text-sm">
Required
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="showCharCount"
checked={controls.showCharCount}
onCheckedChange={(checked) =>
updateControl('showCharCount', checked)
}
/>
<label htmlFor="showCharCount" className="text-sm">
Character Count
</label>
</div>
</div>
{controls.showCharCount && (
<div className="bg-muted/50 rounded-lg p-3">
<p className="mb-1 text-sm font-medium">Character Count:</p>
<p className="font-mono text-sm">
{textValue.length} / {controls.maxLength}
</p>
</div>
)}
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardHeader>
<CardTitle>Textarea Preview</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<Label className="mb-3 block text-base font-semibold">
Basic Textarea
</Label>
<div className="space-y-2">
<Textarea
placeholder="Type your message here..."
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
disabled={controls.disabled}
readOnly={controls.readonly}
required={controls.required}
maxLength={
controls.showCharCount ? controls.maxLength : undefined
}
className={`${sizeClasses[controls.size]} ${resizeClasses[controls.resize]}`}
/>
{controls.showCharCount && (
<div className="text-muted-foreground text-right text-xs">
{textValue.length} / {controls.maxLength}
</div>
)}
<div className="text-muted-foreground text-sm">
<p>
<strong>State:</strong>{' '}
{controls.disabled
? 'Disabled'
: controls.readonly
? 'Readonly'
: 'Active'}
</p>
<p>
<strong>Resize:</strong> {controls.resize}
</p>
<p>
<strong>Size:</strong> {controls.size}
</p>
</div>
</div>
</div>
<div>
<Label className="mb-3 block text-base font-semibold">
Quick Actions
</Label>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setTextValue('')}
disabled={controls.disabled || controls.readonly}
>
Clear
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
setTextValue(
'This is a sample text for testing the textarea component. You can edit this text to see how the component behaves with different content lengths and styling options.',
)
}
disabled={controls.disabled || controls.readonly}
>
Fill Sample
</Button>
<Button
size="sm"
disabled={controls.disabled || !textValue.trim()}
>
<SendIcon className="mr-2 h-4 w-4" />
Send
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
generatedCode={generateCode()}
previewTitle="Interactive Textarea"
previewDescription="Multi-line text input with customizable resize behavior and validation"
controlsTitle="Configuration"
controlsDescription="Adjust resize, size, validation, and behavior options"
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Textareas</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Default Textarea</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Label htmlFor="basic">Message</Label>
<Textarea id="basic" placeholder="Write your message..." />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>With Character Limit</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Label htmlFor="limited">Description (max 200 chars)</Label>
<Textarea
id="limited"
placeholder="Enter description..."
maxLength={200}
/>
<div className="text-muted-foreground text-right text-xs">
0 / 200
</div>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Form Examples</h3>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Contact Form</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Label htmlFor="name">Name *</Label>
<input
type="text"
id="name"
className="border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Your name"
required
/>
</div>
<div>
<Label htmlFor="email">Email *</Label>
<input
type="email"
id="email"
className="border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
placeholder="your@email.com"
required
/>
</div>
</div>
<div>
<Label htmlFor="subject">Subject</Label>
<input
type="text"
id="subject"
className="border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Subject line"
/>
</div>
<div>
<Label htmlFor="contact-message">Message *</Label>
<Textarea
id="contact-message"
placeholder="Tell us about your inquiry..."
className="min-h-[120px]"
required
/>
</div>
<Button className="w-full">
<SendIcon className="mr-2 h-4 w-4" />
Send Message
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Feedback Form</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="rating">Rating</Label>
<div className="mt-1 flex items-center space-x-2">
{Array.from({ length: 5 }).map((_, i) => (
<StarIcon
key={i}
className="h-5 w-5 fill-yellow-400 text-yellow-400"
/>
))}
<span className="text-muted-foreground ml-2 text-sm">
5/5 stars
</span>
</div>
</div>
<div>
<Label htmlFor="feedback">Your Feedback</Label>
<Textarea
id="feedback"
placeholder="What did you think about our service? Your feedback helps us improve..."
value={feedbackValue}
onChange={(e) => setFeedbackValue(e.target.value)}
className="min-h-[100px]"
/>
</div>
<div>
<Label htmlFor="suggestions">
Suggestions for Improvement
</Label>
<Textarea
id="suggestions"
placeholder="Any specific suggestions or features you'd like to see?"
className="min-h-[80px] resize-y"
/>
</div>
<Button>Submit Feedback</Button>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Different Sizes & States
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Size Variants</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Small (min-h-50px)</Label>
<Textarea
placeholder="Small textarea..."
className="min-h-[50px] text-sm"
/>
</div>
<div>
<Label>Medium (min-h-80px)</Label>
<Textarea
placeholder="Medium textarea..."
className="min-h-[80px]"
/>
</div>
<div>
<Label>Large (min-h-120px)</Label>
<Textarea
placeholder="Large textarea..."
className="min-h-[120px] text-base"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>States & Behaviors</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Disabled</Label>
<Textarea
placeholder="This textarea is disabled"
disabled
value="Cannot edit this text"
/>
</div>
<div>
<Label>Readonly</Label>
<Textarea
readOnly
value="This text is readonly and cannot be edited, but can be selected and copied."
className="cursor-default"
/>
</div>
<div>
<Label>No Resize</Label>
<Textarea
placeholder="This textarea cannot be resized"
className="resize-none"
/>
</div>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Comments & Discussion
</h3>
<Card>
<CardHeader>
<CardTitle>
<MessageCircleIcon className="mr-2 inline h-5 w-5" />
Add Comment
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-start space-x-3">
<div className="bg-primary/10 flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium">
U
</div>
<div className="flex-1 space-y-2">
<Textarea
placeholder="What are your thoughts?"
value={commentValue}
onChange={(e) => setCommentValue(e.target.value)}
className="min-h-[80px] resize-none"
/>
<div className="flex items-center justify-between">
<div className="text-muted-foreground text-xs">
{commentValue.length > 0 &&
`${commentValue.length} characters`}
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
Cancel
</Button>
<Button size="sm" disabled={!commentValue.trim()}>
Comment
</Button>
</div>
</div>
</div>
</div>
{/* Sample existing comments */}
<div className="space-y-4 border-t pt-4">
<div className="flex items-start space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600">
JD
</div>
<div className="flex-1">
<div className="text-sm font-medium">John Doe</div>
<div className="text-muted-foreground mt-1 text-sm">
Great article! Really helped me understand the concept
better.
</div>
<div className="text-muted-foreground mt-1 text-xs">
2 hours ago
</div>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 text-xs font-medium text-green-600">
SM
</div>
<div className="flex-1">
<div className="text-sm font-medium">Sarah Miller</div>
<div className="text-muted-foreground mt-1 text-sm">
Thanks for sharing! I have a question about the
implementation details...
</div>
<div className="text-muted-foreground mt-1 text-xs">
4 hours ago
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Content Creation</h3>
<Card>
<CardHeader>
<CardTitle>
<EditIcon className="mr-2 inline h-5 w-5" />
Write Article
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="article-title">Title</Label>
<input
type="text"
id="article-title"
className="border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Enter article title..."
/>
</div>
<div>
<Label htmlFor="article-excerpt">Excerpt</Label>
<Textarea
id="article-excerpt"
placeholder="Write a brief summary of your article..."
className="min-h-[60px] resize-y"
maxLength={200}
/>
<div className="text-muted-foreground text-right text-xs">
Max 200 characters
</div>
</div>
<div>
<Label htmlFor="article-content">Content</Label>
<Textarea
id="article-content"
placeholder="Write your article content here. You can use markdown for formatting..."
className="min-h-[200px] font-mono text-sm"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Badge variant="secondary">Markdown</Badge>
<Badge variant="outline">Auto-save enabled</Badge>
</div>
<div className="flex gap-2">
<Button variant="outline">
<FileTextIcon className="mr-2 h-4 w-4" />
Save Draft
</Button>
<Button>Publish</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Textarea Component</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Component</th>
<th className="p-2 text-left font-medium">Props</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">Textarea</td>
<td className="p-2 font-mono">
All HTMLTextAreaElement props
</td>
<td className="p-2">Multi-line text input component</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Common Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">value</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Controlled value</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">onChange</td>
<td className="p-2 font-mono">
(e: ChangeEvent) ={'>'} void
</td>
<td className="p-2">-</td>
<td className="p-2">Change event handler</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">placeholder</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Placeholder text</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">disabled</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Disable the textarea</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">readOnly</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Make textarea read-only</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">required</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Mark as required field</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">rows</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">-</td>
<td className="p-2">Number of visible rows</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">cols</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">-</td>
<td className="p-2">Number of visible columns</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">maxLength</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">-</td>
<td className="p-2">Maximum character limit</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">minLength</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">-</td>
<td className="p-2">Minimum character requirement</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Styling Classes</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">Size Variants</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">min-h-[50px] (small)</Badge>
<Badge variant="secondary">min-h-[80px] (default)</Badge>
<Badge variant="secondary">min-h-[120px] (large)</Badge>
</div>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Resize Options</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">resize-none</Badge>
<Badge variant="secondary">resize-y (vertical)</Badge>
<Badge variant="secondary">resize-x (horizontal)</Badge>
<Badge variant="secondary">resize (both)</Badge>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// Size variants
<Textarea className="min-h-[50px] text-sm" /> // Small
<Textarea className="min-h-[80px]" /> // Medium
<Textarea className="min-h-[120px] text-base" /> // Large
// Resize behavior
<Textarea className="resize-none" /> // No resize
<Textarea className="resize-y" /> // Vertical only
<Textarea className="resize-x" /> // Horizontal only
<Textarea className="resize" /> // Both directions
// Font styles
<Textarea className="font-mono" /> // Monospace font
<Textarea className="text-xs" /> // Extra small text
<Textarea className="text-lg" /> // Large text`}
</pre>
</div>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Usage</h3>
<p className="text-muted-foreground mb-4 text-sm">
The Textarea component is used for multi-line text input,
supporting all standard HTML textarea attributes and properties.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { Textarea } from '@kit/ui/textarea';
function CommentForm() {
const [comment, setComment] = useState('');
return (
<div className="space-y-2">
<Label htmlFor="comment">Comment</Label>
<Textarea
id="comment"
placeholder="Write your comment..."
value={comment}
onChange={(e) => setComment(e.target.value)}
className="min-h-[100px]"
/>
</div>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Form Integration</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const formSchema = z.object({
message: z.string().min(10, 'Message must be at least 10 characters'),
});
function MessageForm() {
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: { message: '' },
});
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder="Enter your message..."
className="min-h-[120px] resize-none"
{...field}
/>
</FormControl>
<FormDescription>
Your message will be reviewed before posting.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Send Message</Button>
</form>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Character Counting</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`function TextareaWithCount() {
const [text, setText] = useState('');
const maxLength = 280;
return (
<div className="space-y-2">
<Label>Tweet (max 280 characters)</Label>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
maxLength={maxLength}
placeholder="What's happening?"
className="resize-none"
/>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Share your thoughts with the community
</span>
<span className={\`
\${text.length > maxLength * 0.9 ? 'text-orange-500' : 'text-muted-foreground'}
\${text.length === maxLength ? 'text-red-500' : ''}
\`}>
{text.length}/{maxLength}
</span>
</div>
</div>
);
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Best Practices</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">When to Use Textarea</h4>
<p>
Multi-line text input (comments, messages, descriptions)
</p>
<p> Content that may exceed a single line</p>
<p> Free-form text where formatting isn't required</p>
<p> When users need to see their full input at once</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Sizing Guidelines</h4>
<p> Start with appropriate min-height for expected content</p>
<p> Allow vertical resizing for user preference</p>
<p> Consider fixed height for consistent layouts</p>
<p> Use resize-none for structured forms</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">User Experience</h4>
<p> Provide clear placeholder text with examples</p>
<p> Show character limits when they exist</p>
<p> Use proper labeling for accessibility</p>
<p> Consider auto-save for longer content</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Validation & Feedback</h4>
<p> Validate on blur rather than every keystroke</p>
<p> Show validation errors below the textarea</p>
<p> Use visual indicators for required fields</p>
<p> Provide helpful error messages with suggestions</p>
</div>
</div>
</div>
</div>
}
/>
);
}
export { TextareaStory };

View File

@@ -0,0 +1,933 @@
'use client';
import { useState } from 'react';
import {
AlertCircle,
CheckCircle,
Copy,
Download,
Heart,
HelpCircle,
Info,
Settings,
Share,
Star,
User,
} from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Checkbox } from '@kit/ui/checkbox';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@kit/ui/tooltip';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface TooltipControls {
content: string;
side: 'top' | 'bottom' | 'left' | 'right';
align: 'start' | 'center' | 'end';
sideOffset: number;
alignOffset: number;
delayDuration: number;
skipDelayDuration: number;
disableHoverableContent: boolean;
withArrow: boolean;
triggerType: 'button' | 'icon' | 'text' | 'input';
triggerVariant: 'default' | 'outline' | 'ghost' | 'destructive' | 'secondary';
}
const sideOptions = [
{ value: 'top', label: 'Top', description: 'Show above trigger' },
{ value: 'bottom', label: 'Bottom', description: 'Show below trigger' },
{ value: 'left', label: 'Left', description: 'Show to the left' },
{ value: 'right', label: 'Right', description: 'Show to the right' },
] as const;
const alignOptions = [
{ value: 'start', label: 'Start', description: 'Align to start edge' },
{ value: 'center', label: 'Center', description: 'Align to center' },
{ value: 'end', label: 'End', description: 'Align to end edge' },
] as const;
const triggerTypeOptions = [
{ value: 'button', label: 'Button', description: 'Button trigger' },
{ value: 'icon', label: 'Icon', description: 'Icon button trigger' },
{ value: 'text', label: 'Text', description: 'Text trigger' },
{ value: 'input', label: 'Input', description: 'Input field trigger' },
] as const;
const triggerVariantOptions = [
{ value: 'default', label: 'Default', description: 'Primary style' },
{ value: 'outline', label: 'Outline', description: 'Outlined style' },
{ value: 'ghost', label: 'Ghost', description: 'Minimal style' },
{ value: 'secondary', label: 'Secondary', description: 'Secondary style' },
{ value: 'destructive', label: 'Destructive', description: 'Danger style' },
] as const;
const iconOptions = [
{ value: 'info', icon: Info, label: 'Info' },
{ value: 'help', icon: HelpCircle, label: 'Help' },
{ value: 'alert', icon: AlertCircle, label: 'Alert' },
{ value: 'check', icon: CheckCircle, label: 'Check' },
{ value: 'star', icon: Star, label: 'Star' },
{ value: 'heart', icon: Heart, label: 'Heart' },
{ value: 'settings', icon: Settings, label: 'Settings' },
{ value: 'user', icon: User, label: 'User' },
];
function TooltipStory() {
const { controls, updateControl } = useStoryControls<TooltipControls>({
content: 'This is a helpful tooltip',
side: 'top',
align: 'center',
sideOffset: 4,
alignOffset: 0,
delayDuration: 700,
skipDelayDuration: 300,
disableHoverableContent: false,
withArrow: false,
triggerType: 'button',
triggerVariant: 'outline',
});
const [selectedIcon, setSelectedIcon] = useState('info');
const selectedIconData = iconOptions.find(
(opt) => opt.value === selectedIcon,
);
const IconComponent = selectedIconData?.icon || Info;
const generateCode = () => {
const providerProps = {
delayDuration: controls.delayDuration,
skipDelayDuration: controls.skipDelayDuration,
disableHoverableContent: controls.disableHoverableContent,
};
const providerPropsString = generatePropsString(providerProps, {
delayDuration: 700,
skipDelayDuration: 300,
disableHoverableContent: false,
});
const contentProps = {
side: controls.side,
align: controls.align,
sideOffset: controls.sideOffset,
alignOffset: controls.alignOffset,
};
const contentPropsString = generatePropsString(contentProps, {
side: 'top',
align: 'center',
sideOffset: 4,
alignOffset: 0,
});
let code = `<TooltipProvider${providerPropsString}>\n`;
code += ` <Tooltip>\n`;
code += ` <TooltipTrigger asChild>\n`;
if (controls.triggerType === 'button') {
code += ` <Button variant="${controls.triggerVariant}">Hover me</Button>\n`;
} else if (controls.triggerType === 'icon') {
code += ` <Button variant="${controls.triggerVariant}" size="icon">\n`;
const iconName = selectedIconData?.icon.name || 'Info';
code += ` <${iconName} className="h-4 w-4" />\n`;
code += ` </Button>\n`;
} else if (controls.triggerType === 'text') {
code += ` <span className="cursor-help underline decoration-dotted">Hover me</span>\n`;
} else if (controls.triggerType === 'input') {
code += ` <Input placeholder="Hover over this input" />\n`;
}
code += ` </TooltipTrigger>\n`;
code += ` <TooltipContent${contentPropsString}>\n`;
code += ` <p>${controls.content}</p>\n`;
code += ` </TooltipContent>\n`;
code += ` </Tooltip>\n`;
code += `</TooltipProvider>`;
return code;
};
const renderPreview = () => {
const trigger = (() => {
switch (controls.triggerType) {
case 'button':
return <Button variant={controls.triggerVariant}>Hover me</Button>;
case 'icon':
return (
<Button variant={controls.triggerVariant} size="icon">
<IconComponent className="h-4 w-4" />
</Button>
);
case 'text':
return (
<span className="cursor-help underline decoration-dotted">
Hover me
</span>
);
case 'input':
return <Input placeholder="Hover over this input" />;
default:
return <Button variant={controls.triggerVariant}>Hover me</Button>;
}
})();
return (
<div className="flex min-h-[200px] items-center justify-center">
<TooltipProvider
delayDuration={controls.delayDuration}
skipDelayDuration={controls.skipDelayDuration}
disableHoverableContent={controls.disableHoverableContent}
>
<Tooltip>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent
side={controls.side}
align={controls.align}
sideOffset={controls.sideOffset}
alignOffset={controls.alignOffset}
>
<p>{controls.content}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
};
const renderControls = () => (
<>
<div className="space-y-2">
<Label htmlFor="content">Tooltip Content</Label>
<Input
id="content"
value={controls.content}
onChange={(e) => updateControl('content', e.target.value)}
placeholder="Tooltip text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="triggerType">Trigger Type</Label>
<SimpleStorySelect
value={controls.triggerType}
onValueChange={(value) => updateControl('triggerType', value)}
options={triggerTypeOptions}
/>
</div>
{(controls.triggerType === 'button' ||
controls.triggerType === 'icon') && (
<div className="space-y-2">
<Label htmlFor="triggerVariant">Trigger Style</Label>
<SimpleStorySelect
value={controls.triggerVariant}
onValueChange={(value) => updateControl('triggerVariant', value)}
options={triggerVariantOptions}
/>
</div>
)}
{controls.triggerType === 'icon' && (
<div className="space-y-2">
<Label htmlFor="icon">Icon</Label>
<SimpleStorySelect
value={selectedIcon}
onValueChange={setSelectedIcon}
options={iconOptions.map((opt) => ({
value: opt.value,
label: opt.label,
description: `${opt.label} icon`,
}))}
/>
</div>
)}
<Separator />
<div className="space-y-2">
<Label htmlFor="side">Position</Label>
<SimpleStorySelect
value={controls.side}
onValueChange={(value) => updateControl('side', value)}
options={sideOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="align">Alignment</Label>
<SimpleStorySelect
value={controls.align}
onValueChange={(value) => updateControl('align', value)}
options={alignOptions}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="sideOffset">Side Offset</Label>
<Input
id="sideOffset"
type="number"
min="0"
max="50"
value={controls.sideOffset}
onChange={(e) =>
updateControl('sideOffset', parseInt(e.target.value) || 0)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="alignOffset">Align Offset</Label>
<Input
id="alignOffset"
type="number"
min="-50"
max="50"
value={controls.alignOffset}
onChange={(e) =>
updateControl('alignOffset', parseInt(e.target.value) || 0)
}
/>
</div>
</div>
<Separator />
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="delayDuration">Delay (ms)</Label>
<Input
id="delayDuration"
type="number"
min="0"
max="2000"
step="100"
value={controls.delayDuration}
onChange={(e) =>
updateControl('delayDuration', parseInt(e.target.value) || 0)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="skipDelayDuration">Skip Delay (ms)</Label>
<Input
id="skipDelayDuration"
type="number"
min="0"
max="1000"
step="100"
value={controls.skipDelayDuration}
onChange={(e) =>
updateControl('skipDelayDuration', parseInt(e.target.value) || 0)
}
/>
</div>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="disableHoverableContent">
Disable Hoverable Content
</Label>
<Switch
id="disableHoverableContent"
checked={controls.disableHoverableContent}
onCheckedChange={(checked) =>
updateControl('disableHoverableContent', checked)
}
/>
</div>
</>
);
const renderExamples = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Tooltips</CardTitle>
<CardDescription>
Simple tooltips with different triggers
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<TooltipProvider>
<div className="flex flex-wrap gap-4">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">
<Info className="mr-2 h-4 w-4" />
Info Button
</Button>
</TooltipTrigger>
<TooltipContent>
<p>This provides additional information</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<HelpCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Click for help documentation</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-help underline decoration-dotted">
Hover for explanation
</span>
</TooltipTrigger>
<TooltipContent>
<p>This term needs clarification for better understanding</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Input placeholder="Hover me" className="w-48" />
</TooltipTrigger>
<TooltipContent>
<p>Enter your email address here</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Tooltip Positions</CardTitle>
<CardDescription>Different positioning options</CardDescription>
</CardHeader>
<CardContent>
<TooltipProvider>
<div className="flex min-h-[300px] items-center justify-center">
<div className="grid grid-cols-3 gap-8">
{/* Top Row */}
<div></div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="sm">
Top
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Tooltip on top</p>
</TooltipContent>
</Tooltip>
<div></div>
{/* Middle Row */}
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="sm">
Left
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Tooltip on left</p>
</TooltipContent>
</Tooltip>
<div className="flex items-center justify-center">
<span className="text-muted-foreground text-sm">Center</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="sm">
Right
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Tooltip on right</p>
</TooltipContent>
</Tooltip>
{/* Bottom Row */}
<div></div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="sm">
Bottom
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Tooltip on bottom</p>
</TooltipContent>
</Tooltip>
<div></div>
</div>
</div>
</TooltipProvider>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Rich Content Tooltips</CardTitle>
<CardDescription>Tooltips with more complex content</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<TooltipProvider>
<div className="flex flex-wrap gap-4">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">
<Star className="mr-2 h-4 w-4" />
Premium Feature
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<div className="space-y-1">
<p className="font-semibold">Premium Feature</p>
<p className="text-xs">
This feature is only available to premium subscribers.
Upgrade your plan to unlock this functionality.
</p>
</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" />
Advanced Settings
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-1">
<p className="font-semibold">Keyboard Shortcut</p>
<p className="text-xs">
Press{' '}
<kbd className="bg-muted rounded px-1 py-0.5 text-xs">
Ctrl+Shift+S
</kbd>{' '}
to open
</p>
</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="destructive">
<AlertCircle className="mr-2 h-4 w-4" />
Delete Account
</Button>
</TooltipTrigger>
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
<div className="space-y-1">
<p className="font-semibold"> Destructive Action</p>
<p className="text-xs">
This action cannot be undone. All your data will be
permanently deleted.
</p>
</div>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Interactive Elements</CardTitle>
<CardDescription>Tooltips on various UI elements</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<TooltipProvider>
<div className="space-y-4">
<div className="flex items-center gap-4">
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy to clipboard</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<Download className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Download file</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<Share className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Share with others</p>
</TooltipContent>
</Tooltip>
</div>
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Tooltip>
<TooltipTrigger asChild>
<Input id="username" placeholder="Enter username" />
</TooltipTrigger>
<TooltipContent>
<p>Must be 3-20 characters, letters and numbers only</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center space-x-2">
<Tooltip>
<TooltipTrigger asChild>
<Checkbox id="terms" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>
By checking this, you agree to our Terms of Service and
Privacy Policy
</p>
</TooltipContent>
</Tooltip>
<Label htmlFor="terms" className="text-sm">
I agree to the terms and conditions
</Label>
</div>
</div>
</div>
</TooltipProvider>
</CardContent>
</Card>
</div>
);
const renderApiReference = () => (
<Card>
<CardHeader>
<CardTitle>Tooltip Components</CardTitle>
<CardDescription>
Complete API reference for Tooltip components
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-lg font-semibold">TooltipProvider</h4>
<p className="text-muted-foreground mb-3 text-sm">
Provides context for tooltip behavior. Wrap your app or section
using tooltips.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">delayDuration</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">700</td>
<td className="p-3">Delay before tooltip appears (ms)</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">skipDelayDuration</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">300</td>
<td className="p-3">
Delay to skip when moving between tooltips (ms)
</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">
disableHoverableContent
</td>
<td className="p-3 font-mono text-sm">boolean</td>
<td className="p-3 font-mono text-sm">false</td>
<td className="p-3">
Prevent tooltip from being hoverable
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">TooltipContent</h4>
<p className="text-muted-foreground mb-3 text-sm">
The content area of the tooltip with positioning options.
</p>
<div className="overflow-x-auto">
<table className="border-border w-full border-collapse border">
<thead>
<tr className="border-b">
<th className="p-3 text-left font-medium">Prop</th>
<th className="p-3 text-left font-medium">Type</th>
<th className="p-3 text-left font-medium">Default</th>
<th className="p-3 text-left font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-sm">side</td>
<td className="p-3 font-mono text-sm">
'top' | 'bottom' | 'left' | 'right'
</td>
<td className="p-3 font-mono text-sm">'top'</td>
<td className="p-3">Position relative to trigger</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">align</td>
<td className="p-3 font-mono text-sm">
'start' | 'center' | 'end'
</td>
<td className="p-3 font-mono text-sm">'center'</td>
<td className="p-3">Alignment relative to trigger</td>
</tr>
<tr className="border-b">
<td className="p-3 font-mono text-sm">sideOffset</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">4</td>
<td className="p-3">Distance from trigger (px)</td>
</tr>
<tr>
<td className="p-3 font-mono text-sm">alignOffset</td>
<td className="p-3 font-mono text-sm">number</td>
<td className="p-3 font-mono text-sm">0</td>
<td className="p-3">Alignment offset (px)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-lg font-semibold">Other Components</h4>
<ul className="space-y-2 text-sm">
<li>
<strong>Tooltip:</strong> Root container for tooltip state
</li>
<li>
<strong>TooltipTrigger:</strong> Element that triggers the
tooltip (use asChild prop)
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
);
const renderUsageGuidelines = () => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>When to Use Tooltips</CardTitle>
<CardDescription>Best practices for tooltip usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-green-700">
Use Tooltips For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Icon buttons and controls that need clarification</li>
<li>
Form fields with formatting requirements or validation rules
</li>
<li> Abbreviated text or truncated content</li>
<li> Keyboard shortcuts and accessibility information</li>
<li> Additional context that doesn't fit in the UI</li>
<li>• Help text for complex or unfamiliar features</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold text-red-700">
❌ Avoid Tooltips For
</h4>
<ul className="ml-4 space-y-1 text-sm">
<li>• Essential information users need to complete tasks</li>
<li>
• Long explanations (use dialogs or dedicated help sections)
</li>
<li>• Interactive content (tooltips dismiss on focus loss)</li>
<li>• Mobile interfaces (hover behavior is unreliable)</li>
<li>• Information that's already visible in the interface</li>
</ul>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Content Guidelines</CardTitle>
<CardDescription>Writing effective tooltip content</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Content Best Practices</h4>
<ul className="ml-4 space-y-1 text-sm">
<li> Keep content concise (ideally 1-2 lines)</li>
<li> Use sentence case, not title case</li>
<li> Don't repeat what's already visible</li>
<li> Be specific and actionable</li>
<li> Use active voice when possible</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Examples</h4>
<div className="space-y-2">
<div>
<p className="text-sm">
<span className="text-destructive">Bad:</span> "Click this
button"
</p>
<p className="text-sm">
<span className="text-green-600">Good:</span> "Save your
changes"
</p>
</div>
<div>
<p className="text-sm">
<span className="text-destructive">Bad:</span> "This field
is for your password"
</p>
<p className="text-sm">
<span className="text-green-600">Good:</span> "Must be 8+
characters with one number"
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>
Making tooltips accessible to all users
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Keyboard Support</h4>
<p className="text-muted-foreground text-sm">
Tooltips appear on focus and disappear on blur
<br />
Escape key dismisses tooltips
<br /> Tooltips don't trap focus or interfere with navigation
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Screen Reader Support</h4>
<p className="text-muted-foreground text-sm">
Tooltips are announced to screen readers when their trigger
elements receive focus. Essential information should not rely
solely on tooltips.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Mobile Considerations</h4>
<p className="text-muted-foreground text-sm">
Tooltips may not work reliably on touch devices. Consider
alternative approaches like expandable sections or inline help
text for mobile interfaces.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Common Patterns</CardTitle>
<CardDescription>
Typical tooltip implementation scenarios
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Icon Button Tooltips</h4>
<p className="text-muted-foreground text-sm">
Always provide tooltips for icon-only buttons to clarify their
function.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Form Field Help</h4>
<p className="text-muted-foreground text-sm">
Use tooltips to provide format requirements, examples, or
validation rules.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Status Indicators</h4>
<p className="text-muted-foreground text-sm">
Explain status badges, progress indicators, or system states.
</p>
</div>
<div className="space-y-2">
<h4 className="text-sm font-semibold">Truncated Content</h4>
<p className="text-muted-foreground text-sm">
Show full content when text is truncated due to space constraints.
</p>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={renderPreview()}
controls={renderControls()}
generatedCode={generateCode()}
examples={renderExamples()}
apiReference={renderApiReference()}
usageGuidelines={renderUsageGuidelines()}
/>
);
}
export default TooltipStory;

View File

@@ -0,0 +1,975 @@
'use client';
import React from 'react';
import dynamic from 'next/dynamic';
import {
AlertCircle,
AlertTriangle,
BarChart3,
Calendar as CalendarIcon,
ChevronDown,
ChevronRight,
CircleDot,
Command,
Cookie,
Database,
FileText,
Heading as HeadingIcon,
Info,
KeyRound,
Layers,
Layout,
Loader2,
MessageSquare,
MousePointer,
Navigation,
Package,
Palette,
PieChart,
Edit3 as TextIcon,
MessageSquare as ToastIcon,
ToggleLeft,
Type,
Upload,
} from 'lucide-react';
import { LoadingFallback } from '../components/loading-fallback';
const AlertStory = dynamic(
() =>
import('../components/alert-story').then((mod) => ({
default: mod.AlertStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const BorderedNavigationMenuStory = dynamic(
() =>
import('../components/bordered-navigation-menu-story').then((mod) => ({
default: mod.BorderedNavigationMenuStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const BadgeStory = dynamic(
() =>
import('../components/badge-story').then((mod) => ({
default: mod.BadgeStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const ButtonStory = dynamic(
() =>
import('../components/button-story').then((mod) => ({
default: mod.ButtonStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const CardStory = dynamic(
() =>
import('../components/card-story').then((mod) => ({
default: mod.CardStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const DataTableStory = dynamic(
() =>
import('../components/data-table-story').then((mod) => ({
default: mod.DataTableStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const DropdownMenuStory = dynamic(
() =>
import('../components/dropdown-menu-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const InputStory = dynamic(
() =>
import('../components/input-story').then((mod) => ({
default: mod.InputStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const SelectStory = dynamic(
() =>
import('../components/select-story').then((mod) => ({
default: mod.SelectStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const SwitchStory = dynamic(
() =>
import('../components/switch-story').then((mod) => ({
default: mod.SwitchStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const DialogStory = dynamic(
() =>
import('../components/dialog-story').then((mod) => ({
default: mod.DialogStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const AlertDialogStory = dynamic(
() =>
import('../components/alert-dialog-story').then((mod) => ({
default: mod.AlertDialogStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const TooltipStory = dynamic(
() =>
import('../components/tooltip-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const ProgressStory = dynamic(
() =>
import('../components/progress-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const CardButtonStory = dynamic(
() =>
import('../components/card-button-story').then((mod) => ({
default: mod.CardButtonStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const LoadingOverlayStory = dynamic(
() =>
import('../components/loading-overlay-story').then((mod) => ({
default: mod.LoadingOverlayStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const StepperStory = dynamic(
() =>
import('../components/stepper-story').then((mod) => ({
default: mod.StepperStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const CookieBannerStory = dynamic(
() =>
import('../components/cookie-banner-story').then((mod) => ({
default: mod.CookieBannerStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const FileUploaderStory = dynamic(
() =>
import('../components/file-uploader-story').then((mod) => ({
default: mod.FileUploaderStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const LoadingSpinnerStory = dynamic(
() =>
import('../components/spinner-story').then((mod) => ({
default: mod.SpinnerStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const TabsStory = dynamic(
() =>
import('../components/tabs-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const ChartStory = dynamic(
() =>
import('../components/chart-story').then((mod) => ({
default: mod.ChartStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const CalendarStory = dynamic(
() =>
import('../components/calendar-story').then((mod) => ({
default: mod.CalendarStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const BreadcrumbStory = dynamic(
() =>
import('../components/breadcrumb-story').then((mod) => ({
default: mod.BreadcrumbStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const FormStory = dynamic(
() =>
import('../components/form-story').then((mod) => ({
default: mod.FormStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const HeadingStory = dynamic(
() =>
import('../components/heading-story').then((mod) => ({
default: mod.HeadingStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const InputOTPStory = dynamic(
() =>
import('../components/input-otp-story').then((mod) => ({
default: mod.InputOTPStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const RadioGroupStory = dynamic(
() =>
import('../components/radio-group-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const SkeletonStory = dynamic(
() =>
import('../components/skeleton-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const SonnerStory = dynamic(
() =>
import('../components/sonner-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const TextareaStory = dynamic(
() =>
import('../components/textarea-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const CommandStory = dynamic(
() =>
import('../components/command-story').then((mod) => ({
default: mod.default,
})),
{
loading: () => <LoadingFallback />,
},
);
const SimpleTableStory = dynamic(
() =>
import('../components/simple-data-table-story').then((mod) => ({
default: mod.SimpleDataTableStory,
})),
{
loading: () => <LoadingFallback />,
},
);
// Component type definition
export interface ComponentInfo {
id: string;
name: string;
category: string;
subcategory: string;
description: string;
status: 'stable' | 'beta' | 'deprecated';
component: React.ComponentType;
sourceFile: string;
props: string[];
icon: React.ComponentType<{ className?: string }>;
}
// Component registry
export const COMPONENTS_REGISTRY: ComponentInfo[] = [
// Forms Components
{
id: 'input',
name: 'Input',
category: 'Forms',
subcategory: 'Fields',
description: 'Text input field for collecting user data',
status: 'stable',
component: InputStory,
sourceFile: '@kit/ui/input',
props: [
'type',
'placeholder',
'disabled',
'required',
'value',
'onChange',
'className',
],
icon: Type,
},
{
id: 'card-button',
name: 'Card Button',
category: 'Interaction',
subcategory: 'Controls',
description: 'Button component with card-like styling',
status: 'stable',
component: CardButtonStory,
sourceFile: '@kit/ui/card-button',
props: ['asChild', 'className', 'children', 'onClick', 'disabled'],
icon: MousePointer,
},
{
id: 'loading-overlay',
name: 'Loading Overlay',
category: 'Feedback',
subcategory: 'Controls',
description: 'Overlay component with loading spinner',
status: 'stable',
component: LoadingOverlayStory,
sourceFile: '@kit/ui/loading-overlay',
props: ['children', 'className', 'spinnerClassName', 'fullPage'],
icon: Loader2,
},
{
id: 'stepper',
name: 'Stepper',
category: 'Forms',
subcategory: 'Controls',
description: 'Stepper component with customizable steps',
status: 'stable',
component: StepperStory,
sourceFile: '@kit/ui/stepper',
props: ['steps', 'currentStep', 'variant'],
icon: ChevronDown,
},
{
id: 'bordered-navigation-menu',
name: 'Bordered Navigation Menu',
category: 'Navigation',
subcategory: 'Menus',
description: 'Bordered navigation menu component with customizable options',
status: 'stable',
component: BorderedNavigationMenuStory,
sourceFile: '@kit/ui/bordered-navigation-menu',
props: ['path', 'label', 'end', 'active', 'className', 'buttonClassName'],
icon: Navigation,
},
{
id: 'cookie-banner',
name: 'Cookie Banner',
category: 'Feedback',
subcategory: 'Modals',
description: 'Cookie banner component with customizable options',
status: 'stable',
component: CookieBannerStory,
sourceFile: '@kit/ui/cookie-banner',
props: [],
icon: Cookie,
},
{
id: 'file-uploader',
name: 'File Uploader',
category: 'Forms',
subcategory: 'Controls',
description: 'File uploader component with customizable options',
status: 'stable',
component: FileUploaderStory,
sourceFile: '@kit/ui/file-uploader',
props: [
'maxFiles',
'bucketName',
'path',
'allowedMimeTypes',
'maxFileSize',
'client',
'onUploadSuccess',
'cacheControl',
'className',
],
icon: Upload,
},
{
id: 'loading-spinner',
name: 'Loading Spinner',
category: 'Feedback',
subcategory: 'Controls',
description: 'Loading spinner component',
status: 'stable',
component: LoadingSpinnerStory,
sourceFile: '@kit/ui/spinner',
props: ['className', 'children'],
icon: Loader2,
},
{
id: 'select',
name: 'Select',
category: 'Forms',
subcategory: 'Fields',
description: 'Dropdown selection component with grouping support',
status: 'stable',
component: SelectStory,
sourceFile: '@kit/ui/select',
props: ['value', 'onValueChange', 'disabled', 'required', 'placeholder'],
icon: ChevronDown,
},
{
id: 'dropdown-menu',
name: 'Dropdown Menu',
category: 'Forms',
subcategory: 'Controls',
description: 'Dropdown menu component with customizable options',
status: 'stable',
component: DropdownMenuStory,
sourceFile: '@kit/ui/dropdown-menu',
props: ['value', 'onValueChange', 'disabled', 'required', 'placeholder'],
icon: ChevronDown,
},
{
id: 'switch',
name: 'Switch',
category: 'Forms',
subcategory: 'Controls',
description: 'Toggle switch for boolean states and settings',
status: 'stable',
component: SwitchStory,
sourceFile: '@kit/ui/switch',
props: [
'checked',
'onCheckedChange',
'disabled',
'required',
'name',
'value',
],
icon: ToggleLeft,
},
{
id: 'calendar',
name: 'Calendar',
category: 'Forms',
subcategory: 'Date & Time',
description:
'Date picker component for selecting single dates, date ranges, or multiple dates',
status: 'stable',
component: CalendarStory,
sourceFile: '@kit/ui/calendar',
props: [
'mode',
'selected',
'onSelect',
'captionLayout',
'numberOfMonths',
'showOutsideDays',
'showWeekNumber',
'disabled',
'buttonVariant',
],
icon: CalendarIcon,
},
{
id: 'form',
name: 'Form',
category: 'Forms',
subcategory: 'Validation',
description:
'Form components with React Hook Form integration and Zod validation',
status: 'stable',
component: FormStory,
sourceFile: '@kit/ui/form',
props: ['control', 'name', 'render', 'defaultValue', 'rules'],
icon: FileText,
},
{
id: 'input-otp',
name: 'Input OTP',
category: 'Forms',
subcategory: 'Security',
description:
'One-time password input with customizable length, patterns, and grouping',
status: 'stable',
component: InputOTPStory,
sourceFile: '@kit/ui/input-otp',
props: [
'maxLength',
'value',
'onChange',
'pattern',
'disabled',
'autoFocus',
],
icon: KeyRound,
},
{
id: 'radio-group',
name: 'Radio Group',
category: 'Forms',
subcategory: 'Controls',
description:
'Single-selection input control with enhanced labels and customizable layouts',
status: 'stable',
component: RadioGroupStory,
sourceFile: '@kit/ui/radio-group',
props: [
'value',
'onValueChange',
'disabled',
'name',
'required',
'orientation',
],
icon: CircleDot,
},
{
id: 'skeleton',
name: 'Skeleton',
category: 'Feedback',
subcategory: 'Loading',
description:
'Animated loading placeholder that preserves layout during content loading',
status: 'stable',
component: SkeletonStory,
sourceFile: '@kit/ui/skeleton',
props: ['className'],
icon: Loader2,
},
{
id: 'sonner',
name: 'Sonner',
category: 'Feedback',
subcategory: 'Notifications',
description:
'Toast notification system with promise support and rich interactions',
status: 'stable',
component: SonnerStory,
sourceFile: '@kit/ui/sonner',
props: ['position', 'theme', 'richColors', 'expand', 'visibleToasts'],
icon: ToastIcon,
},
{
id: 'textarea',
name: 'Textarea',
category: 'Forms',
subcategory: 'Fields',
description:
'Multi-line text input with customizable resize behavior and validation support',
status: 'stable',
component: TextareaStory,
sourceFile: '@kit/ui/textarea',
props: [
'value',
'onChange',
'placeholder',
'disabled',
'readOnly',
'required',
'rows',
'cols',
'maxLength',
],
icon: TextIcon,
},
// Feedback Components
{
id: 'alert',
name: 'Alert',
category: 'Feedback',
subcategory: 'Messages',
description: 'Contextual feedback messages for user actions',
status: 'stable',
component: AlertStory,
sourceFile: '@kit/ui/alert',
props: ['variant', 'className', 'children'],
icon: AlertCircle,
},
{
id: 'dialog',
name: 'Dialog',
category: 'Feedback',
subcategory: 'Modals',
description: 'Modal dialog for forms, content, and user interactions',
status: 'stable',
component: DialogStory,
sourceFile: '@kit/ui/dialog',
props: ['open', 'onOpenChange', 'modal'],
icon: MessageSquare,
},
{
id: 'alert-dialog',
name: 'Alert Dialog',
category: 'Feedback',
subcategory: 'Modals',
description: 'Modal dialog for critical confirmations and alerts',
status: 'stable',
component: AlertDialogStory,
sourceFile: '@kit/ui/alert-dialog',
props: ['open', 'onOpenChange'],
icon: AlertTriangle,
},
{
id: 'command',
name: 'Command',
category: 'Forms',
subcategory: 'Controls',
description:
'Command palette for executing actions and commands with search and keyboard navigation',
status: 'stable',
component: CommandStory,
sourceFile: '@kit/ui/command',
props: [
'value',
'onValueChange',
'filter',
'shouldFilter',
'loop',
'vimBindings',
'defaultValue',
],
icon: Command,
},
{
id: 'tooltip',
name: 'Tooltip',
category: 'Feedback',
subcategory: 'Overlays',
description: 'Contextual information overlay triggered by hover or focus',
status: 'stable',
component: TooltipStory,
sourceFile: '@kit/ui/tooltip',
props: [
'delayDuration',
'skipDelayDuration',
'disableHoverableContent',
'open',
'onOpenChange',
],
icon: Info,
},
{
id: 'progress',
name: 'Progress',
category: 'Feedback',
subcategory: 'Status',
description: 'Visual indicator showing completion progress of tasks',
status: 'stable',
component: ProgressStory,
sourceFile: '@kit/ui/progress',
props: ['value', 'max', 'getValueLabel', 'className'],
icon: BarChart3,
},
// Display Components
{
id: 'chart',
name: 'Chart',
category: 'Display',
subcategory: 'Data Visualization',
description: 'Data visualization components built on top of Recharts',
status: 'stable',
component: ChartStory,
sourceFile: '@kit/ui/chart',
props: ['config', 'children', 'className'],
icon: PieChart,
},
{
id: 'heading',
name: 'Heading',
category: 'Display',
subcategory: 'Typography',
description:
'Semantic heading component with responsive typography scaling',
status: 'stable',
component: HeadingStory,
sourceFile: '@kit/ui/heading',
props: ['level', 'children', 'className'],
icon: HeadingIcon,
},
// Interaction Components
{
id: 'button',
name: 'Button',
category: 'Interaction',
subcategory: 'Actions',
description: 'Clickable element that triggers actions',
status: 'stable',
component: ButtonStory,
sourceFile: '@kit/ui/button',
props: ['variant', 'size', 'disabled', 'onClick', 'className', 'children'],
icon: MousePointer,
},
// Layout Components
{
id: 'card',
name: 'Card',
category: 'Layout',
subcategory: 'Containers',
description: 'Container for content with optional header and footer',
status: 'stable',
component: CardStory,
sourceFile: '@kit/ui/card',
props: ['className', 'children'],
icon: Layout,
},
{
id: 'badge',
name: 'Badge',
category: 'Display',
subcategory: 'Indicators',
description: 'Small labeled status or category indicator',
status: 'stable',
component: BadgeStory,
sourceFile: '@kit/ui/badge',
props: ['variant', 'className', 'children'],
icon: Palette,
},
// Data Components
{
id: 'simple-data-table',
name: 'Table',
category: 'Data',
subcategory: 'Tables',
description: 'Simple table component with basic TanStack Table features',
status: 'stable',
component: SimpleTableStory,
sourceFile: '@kit/ui/data-table',
props: ['data', 'columns'],
icon: Database,
},
{
id: 'data-table',
name: 'Data Table',
category: 'Data',
subcategory: 'Tables',
description: 'Advanced table with sorting, filtering, and pagination',
status: 'stable',
component: DataTableStory,
sourceFile: '@kit/ui/enhanced-data-table',
props: ['data', 'columns', 'pageSize', 'sorting', 'filtering'],
icon: Database,
},
// Navigation Components
{
id: 'breadcrumb',
name: 'Breadcrumb',
category: 'Navigation',
subcategory: 'Hierarchy',
description:
'Navigation component showing the hierarchical path to the current page',
status: 'stable',
component: BreadcrumbStory,
sourceFile: '@kit/ui/breadcrumb',
props: ['separator', 'asChild', 'href', 'className'],
icon: ChevronRight,
},
{
id: 'empty-state',
name: 'Empty State',
category: 'Feedback',
subcategory: 'Messages',
description:
'Empty state component for displaying when no data is available',
status: 'stable',
component: dynamic(
() =>
import('../components/empty-state-story').then((mod) => ({
default: mod.EmptyStateStory,
})),
{
loading: () => <LoadingFallback />,
},
),
sourceFile: '@kit/ui/empty-state',
props: ['className', 'children'],
icon: Package,
},
{
id: 'tabs',
name: 'Tabs',
category: 'Navigation',
subcategory: 'Organization',
description: 'Tabbed navigation interface for organizing related content',
status: 'stable',
component: TabsStory,
sourceFile: '@kit/ui/tabs',
props: [
'defaultValue',
'value',
'onValueChange',
'orientation',
'dir',
'activationMode',
],
icon: Layers,
},
];
// Enhanced category system with icons and descriptions
export const categoryInfo = {
Forms: {
icon: Type,
description: 'Components for collecting and validating user input',
color: 'bg-cyan-500',
},
Interaction: {
icon: MousePointer,
description: 'Components that handle user interactions',
color: 'bg-blue-500',
},
Layout: {
icon: Layout,
description: 'Components for structuring and organizing content',
color: 'bg-green-500',
},
Display: {
icon: Palette,
description: 'Components for displaying information and status',
color: 'bg-purple-500',
},
Data: {
icon: Database,
description: 'Components for displaying and manipulating data',
color: 'bg-orange-500',
},
Feedback: {
icon: AlertCircle,
description: 'Components for providing user feedback',
color: 'bg-red-500',
},
Navigation: {
icon: Navigation,
description: 'Components for site and app navigation',
color: 'bg-indigo-500',
},
} as const;
export const categories = [
...new Set(COMPONENTS_REGISTRY.map((c) => c.category)),
];

View File

@@ -0,0 +1,155 @@
'use client';
import { useState } from 'react';
import { toast } from '@kit/ui/sonner';
/**
* Generic hook for managing component story controls
*/
export function useStoryControls<T extends Record<string, any>>(
initialState: T,
) {
const [controls, setControls] = useState<T>(initialState);
const updateControl = <K extends keyof T>(key: K, value: T[K]) => {
setControls((prev) => ({ ...prev, [key]: value }));
};
const resetControls = () => {
setControls(initialState);
};
return {
controls,
updateControl,
resetControls,
};
}
/**
* Hook for managing copy-to-clipboard functionality
*/
export function useCopyCode() {
const [copiedCode, setCopiedCode] = useState(false);
const copyCode = async (code: string) => {
try {
await navigator.clipboard.writeText(code);
setCopiedCode(true);
toast.success('Code copied to clipboard!');
setTimeout(() => setCopiedCode(false), 2000);
} catch (error) {
toast.error('Failed to copy code');
console.error('Failed to copy code:', error);
}
};
return {
copiedCode,
copyCode,
};
}
/**
* Utility to generate props string from control values
*/
export function generatePropsString(
controls: Record<string, any>,
defaults: Record<string, any> = {},
excludeKeys: string[] = [],
): string {
const props: string[] = [];
Object.entries(controls).forEach(([key, value]) => {
if (excludeKeys.includes(key)) return;
// Skip undefined values - omit the prop entirely
if (value === undefined) return;
const defaultValue = defaults[key];
const hasDefault = defaultValue !== undefined;
// Only include prop if it's different from default or there's no default
if (!hasDefault || value !== defaultValue) {
if (typeof value === 'boolean') {
if (value) {
props.push(key);
}
} else if (typeof value === 'string') {
if (value) {
props.push(`${key}="${value}"`);
}
} else {
props.push(`${key}={${JSON.stringify(value)}}`);
}
}
});
return props.length > 0 ? ` ${props.join(' ')}` : '';
}
/**
* Common tab configuration for component stories
*/
export interface StoryTab {
id: string;
label: string;
content: React.ReactNode;
}
export const defaultStoryTabs = [
{ id: 'playground', label: 'Playground' },
{ id: 'examples', label: 'Examples' },
{ id: 'api', label: 'API Reference' },
{ id: 'usage', label: 'Usage Guidelines' },
];
/**
* Option type for select components with descriptions
*/
export interface SelectOption<T = string> {
value: T;
label: string;
description: string;
icon?: React.ComponentType<{ className?: string }>;
color?: string;
}
/**
* Utility to format component imports for code generation
*/
export function generateImportStatement(
components: string[],
source: string,
): string {
if (components.length === 1) {
return `import { ${components[0]} } from '${source}';`;
}
if (components.length <= 3) {
return `import { ${components.join(', ')} } from '${source}';`;
}
// Multi-line for many imports
return `import {\n ${components.join(',\n ')}\n} from '${source}';`;
}
/**
* Utility to create formatted code blocks
*/
export function formatCodeBlock(
code: string,
imports: string[] = [],
language: 'tsx' | 'jsx' | 'javascript' | 'typescript' = 'tsx',
): string {
let formattedCode = '';
if (imports.length > 0) {
formattedCode += imports.join('\n') + '\n\n';
}
formattedCode += code;
return formattedCode;
}

View File

@@ -0,0 +1,32 @@
import { withI18n } from '../../lib/i18n/with-i18n';
import { DocsContent } from './components/docs-content';
import { DocsHeader } from './components/docs-header';
import { DocsSidebar } from './components/docs-sidebar';
type ComponentDocsPageProps = {
searchParams: Promise<{
component: string;
category: string;
}>;
};
async function ComponentDocsPage(props: ComponentDocsPageProps) {
let { component, category } = await props.searchParams;
if (!component) {
component = 'Input';
}
return (
<div className="bg-background flex h-screen">
<DocsSidebar selectedComponent={component} selectedCategory={category} />
<div className="flex flex-1 flex-col">
<DocsHeader selectedComponent={component} />
<DocsContent selectedComponent={component} />
</div>
</div>
);
}
export default withI18n(ComponentDocsPage);

View File

@@ -32,7 +32,7 @@ export default async function DashboardPage(props: DashboardPageProps) {
<EnvModeSelector mode={mode} />
</PageHeader>
<PageBody className={'py-2'}>
<PageBody className={'space-y-8 py-2'}>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<ServiceCard name={'Supabase API'} status={supabaseStatus} />
<ServiceCard name={'Supabase Admin'} status={supabaseAdminStatus} />