Files
myeasycms-v2/apps/dev-tool/app/components/components/button-story.tsx
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

489 lines
16 KiB
TypeScript

'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;
}
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,
});
const generateCode = () => {
const propsString = generatePropsString(
{
variant: controls.variant,
size: controls.size,
disabled: controls.disabled,
className: controls.fullWidth ? 'w-full' : '',
},
{
variant: 'default',
size: 'default',
disabled: 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>
</>
);
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()}
/>
);
}