Files
myeasycms-v2/apps/dev-tool/app/components/components/checkbox-story.tsx
Giancarlo Buomprisco ad427365c9 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.
2025-08-22 07:35:44 +08:00

1356 lines
45 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
Bell,
Eye,
FileText,
Globe,
Image,
Settings,
Shield,
Star,
Users,
Zap,
} 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 { Checkbox } from '@kit/ui/checkbox';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
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 {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface CheckboxControlsProps {
checked: boolean;
indeterminate: boolean;
disabled: boolean;
required: boolean;
size: 'sm' | 'default' | 'lg';
className: string;
onCheckedChange: (checked: boolean) => void;
onIndeterminateChange: (indeterminate: boolean) => void;
onDisabledChange: (disabled: boolean) => void;
onRequiredChange: (required: boolean) => void;
onSizeChange: (size: 'sm' | 'default' | 'lg') => void;
onClassNameChange: (className: string) => void;
}
const sizeClasses = {
sm: 'h-3 w-3',
default: 'h-4 w-4',
lg: 'h-5 w-5',
};
function CheckboxPlayground({
checked,
indeterminate,
disabled,
required,
size,
className,
onCheckedChange,
}: CheckboxControlsProps) {
return (
<div className="flex min-h-32 items-center justify-center">
<div className="flex items-center space-x-2">
<Checkbox
id="playground-checkbox"
checked={indeterminate ? 'indeterminate' : checked}
onCheckedChange={(value) => {
if (typeof value === 'boolean') {
onCheckedChange(value);
}
}}
disabled={disabled}
required={required}
className={cn(sizeClasses[size], className)}
/>
<Label
htmlFor="playground-checkbox"
className={cn(
'text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
disabled && 'opacity-70',
)}
>
{indeterminate
? 'Indeterminate checkbox'
: checked
? 'Checked checkbox'
: 'Unchecked checkbox'}
{required && <span className="ml-1 text-red-500">*</span>}
</Label>
</div>
</div>
);
}
const examples = [
{
title: 'Basic Checkbox States',
description:
'Standard checkbox states including checked, unchecked, indeterminate, and disabled',
component: () => {
const [basicChecked, setBasicChecked] = useState(false);
const [indeterminateChecked, setIndeterminateChecked] = useState<
boolean | 'indeterminate'
>('indeterminate');
return (
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle className="text-base">Checkbox States</CardTitle>
<CardDescription>
Different states and behaviors of checkboxes
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox id="unchecked" checked={false} />
<Label htmlFor="unchecked">Unchecked</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="checked" checked={true} />
<Label htmlFor="checked">Checked</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="indeterminate"
checked={indeterminateChecked}
onCheckedChange={setIndeterminateChecked}
/>
<Label htmlFor="indeterminate">
Indeterminate (partially selected)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="disabled-unchecked" disabled checked={false} />
<Label htmlFor="disabled-unchecked" className="opacity-70">
Disabled unchecked
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="disabled-checked" disabled checked={true} />
<Label htmlFor="disabled-checked" className="opacity-70">
Disabled checked
</Label>
</div>
<Separator />
<div className="flex items-center space-x-2">
<Checkbox
id="interactive"
checked={basicChecked}
onCheckedChange={setBasicChecked}
/>
<Label htmlFor="interactive">
Interactive checkbox - {basicChecked ? 'Checked' : 'Unchecked'}
</Label>
</div>
</CardContent>
</Card>
);
},
},
{
title: 'Form Integration',
description:
'Checkboxes in forms with validation, grouping, and different use cases',
component: () => {
const [formData, setFormData] = useState({
acceptTerms: false,
receiveNewsletter: false,
receivePromotions: false,
enableNotifications: true,
enableTwoFactor: false,
});
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
setTimeout(() => setSubmitted(false), 3000);
};
const isValid = formData.acceptTerms;
return (
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle className="text-base">Account Settings Form</CardTitle>
<CardDescription>
Configure your account preferences and privacy settings
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-3">
<h4 className="text-sm font-medium">Email Preferences</h4>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter"
checked={formData.receiveNewsletter}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
receiveNewsletter: !!checked,
}))
}
/>
<Label htmlFor="newsletter">Receive newsletter</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="promotions"
checked={formData.receivePromotions}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
receivePromotions: !!checked,
}))
}
/>
<Label htmlFor="promotions">Receive promotional emails</Label>
</div>
</div>
<Separator />
<div className="space-y-3">
<h4 className="text-sm font-medium">Security</h4>
<div className="flex items-center space-x-2">
<Checkbox
id="notifications"
checked={formData.enableNotifications}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
enableNotifications: !!checked,
}))
}
/>
<Label htmlFor="notifications">
Enable push notifications
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="two-factor"
checked={formData.enableTwoFactor}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
enableTwoFactor: !!checked,
}))
}
/>
<Label htmlFor="two-factor">
Enable two-factor authentication
<Badge variant="secondary" className="ml-2 text-xs">
Recommended
</Badge>
</Label>
</div>
</div>
<Separator />
<div className="flex items-start space-x-2">
<Checkbox
id="terms"
checked={formData.acceptTerms}
onCheckedChange={(checked) =>
setFormData((prev) => ({ ...prev, acceptTerms: !!checked }))
}
required
/>
<Label htmlFor="terms" className="text-sm">
I accept the{' '}
<span className="cursor-pointer underline">
Terms of Service
</span>{' '}
and{' '}
<span className="cursor-pointer underline">
Privacy Policy
</span>
<span className="ml-1 text-red-500">*</span>
</Label>
</div>
<Button type="submit" disabled={!isValid} className="w-full">
{submitted ? 'Settings Saved!' : 'Save Settings'}
</Button>
{submitted && (
<div className="rounded-md border border-green-200 bg-green-50 p-3">
<p className="text-sm text-green-800">
Your settings have been successfully updated!
</p>
</div>
)}
</form>
</CardContent>
</Card>
);
},
},
{
title: 'Hierarchical Selection',
description:
'Parent-child checkbox relationships with indeterminate states',
component: () => {
const [selections, setSelections] = useState({
all: false as boolean | 'indeterminate',
documents: {
parent: false as boolean | 'indeterminate',
children: {
reports: false,
presentations: true,
spreadsheets: false,
},
},
media: {
parent: true as boolean | 'indeterminate',
children: {
images: true,
videos: true,
audio: true,
},
},
settings: {
parent: false as boolean | 'indeterminate',
children: {
preferences: false,
security: false,
notifications: false,
},
},
});
const updateParentState = (
category:
| keyof typeof selections.documents
| keyof typeof selections.media
| keyof typeof selections.settings,
) => {
const categoryData = selections[
category as keyof typeof selections
] as {
parent: boolean | 'indeterminate';
children: Record<string, boolean>;
};
const children = Object.values(categoryData.children);
const checkedCount = children.filter(Boolean).length;
let parentState: boolean | 'indeterminate';
if (checkedCount === 0) parentState = false;
else if (checkedCount === children.length) parentState = true;
else parentState = 'indeterminate';
return parentState;
};
const updateAllState = () => {
const allCategories = [
selections.documents,
selections.media,
selections.settings,
];
const allCheckedStates = allCategories.map((cat) => cat.parent);
const trueCount = allCheckedStates.filter(
(state) => state === true,
).length;
const indeterminateCount = allCheckedStates.filter(
(state) => state === 'indeterminate',
).length;
if (trueCount === allCategories.length) return true;
if (trueCount === 0 && indeterminateCount === 0) return false;
return 'indeterminate';
};
const handleChildChange = (
category: 'documents' | 'media' | 'settings',
child: string,
checked: boolean,
) => {
const newSelections = { ...selections };
(newSelections[category].children as any)[child] = checked;
newSelections[category].parent = updateParentState(category);
newSelections.all = updateAllState();
setSelections(newSelections);
};
const handleParentChange = (
category: 'documents' | 'media' | 'settings',
checked: boolean | 'indeterminate',
) => {
if (checked === 'indeterminate') return;
const newSelections = { ...selections };
newSelections[category].parent = checked;
Object.keys(newSelections[category].children).forEach((key) => {
(newSelections[category].children as any)[key] = checked;
});
newSelections.all = updateAllState();
setSelections(newSelections);
};
const handleAllChange = (checked: boolean | 'indeterminate') => {
if (checked === 'indeterminate') return;
const newSelections = { ...selections };
newSelections.all = checked;
(['documents', 'media', 'settings'] as const).forEach((category) => {
newSelections[category].parent = checked;
Object.keys(newSelections[category].children).forEach((key) => {
(newSelections[category].children as any)[key] = checked;
});
});
setSelections(newSelections);
};
// Update parent states based on children
React.useEffect(() => {
const newSelections = { ...selections };
(['documents', 'media', 'settings'] as const).forEach((category) => {
newSelections[category].parent = updateParentState(category);
});
newSelections.all = updateAllState();
setSelections(newSelections);
}, []);
return (
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle className="text-base">File Backup Selection</CardTitle>
<CardDescription>
Choose which files to include in your backup
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2 font-medium">
<Checkbox
id="select-all"
checked={selections.all}
onCheckedChange={handleAllChange}
/>
<Label htmlFor="select-all">Select All</Label>
</div>
<div className="ml-6 space-y-4">
{/* Documents */}
<div>
<div className="mb-2 flex items-center space-x-2">
<Checkbox
id="documents"
checked={selections.documents.parent}
onCheckedChange={(checked) =>
handleParentChange('documents', checked)
}
/>
<Label htmlFor="documents" className="font-medium">
<FileText className="mr-1 inline h-4 w-4" />
Documents
</Label>
</div>
<div className="ml-6 space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="reports"
checked={selections.documents.children.reports}
onCheckedChange={(checked) =>
handleChildChange('documents', 'reports', !!checked)
}
/>
<Label htmlFor="reports" className="text-sm">
Reports (15 files)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="presentations"
checked={selections.documents.children.presentations}
onCheckedChange={(checked) =>
handleChildChange(
'documents',
'presentations',
!!checked,
)
}
/>
<Label htmlFor="presentations" className="text-sm">
Presentations (8 files)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="spreadsheets"
checked={selections.documents.children.spreadsheets}
onCheckedChange={(checked) =>
handleChildChange(
'documents',
'spreadsheets',
!!checked,
)
}
/>
<Label htmlFor="spreadsheets" className="text-sm">
Spreadsheets (12 files)
</Label>
</div>
</div>
</div>
{/* Media */}
<div>
<div className="mb-2 flex items-center space-x-2">
<Checkbox
id="media"
checked={selections.media.parent}
onCheckedChange={(checked) =>
handleParentChange('media', checked)
}
/>
<Label htmlFor="media" className="font-medium">
<Image className="mr-1 inline h-4 w-4" />
Media
</Label>
</div>
<div className="ml-6 space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="images"
checked={selections.media.children.images}
onCheckedChange={(checked) =>
handleChildChange('media', 'images', !!checked)
}
/>
<Label htmlFor="images" className="text-sm">
Images (245 files)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="videos"
checked={selections.media.children.videos}
onCheckedChange={(checked) =>
handleChildChange('media', 'videos', !!checked)
}
/>
<Label htmlFor="videos" className="text-sm">
Videos (23 files)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="audio"
checked={selections.media.children.audio}
onCheckedChange={(checked) =>
handleChildChange('media', 'audio', !!checked)
}
/>
<Label htmlFor="audio" className="text-sm">
Audio (67 files)
</Label>
</div>
</div>
</div>
{/* Settings */}
<div>
<div className="mb-2 flex items-center space-x-2">
<Checkbox
id="settings"
checked={selections.settings.parent}
onCheckedChange={(checked) =>
handleParentChange('settings', checked)
}
/>
<Label htmlFor="settings" className="font-medium">
<Settings className="mr-1 inline h-4 w-4" />
Settings
</Label>
</div>
<div className="ml-6 space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="preferences"
checked={selections.settings.children.preferences}
onCheckedChange={(checked) =>
handleChildChange('settings', 'preferences', !!checked)
}
/>
<Label htmlFor="preferences" className="text-sm">
Preferences
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="security"
checked={selections.settings.children.security}
onCheckedChange={(checked) =>
handleChildChange('settings', 'security', !!checked)
}
/>
<Label htmlFor="security" className="text-sm">
Security
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="notifications"
checked={selections.settings.children.notifications}
onCheckedChange={(checked) =>
handleChildChange(
'settings',
'notifications',
!!checked,
)
}
/>
<Label htmlFor="notifications" className="text-sm">
Notifications
</Label>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
},
},
{
title: 'Feature Toggle List',
description:
'Feature flags and settings with descriptions and visual indicators',
component: () => {
const [features, setFeatures] = useState({
darkMode: true,
notifications: false,
autoSave: true,
collaboration: false,
analytics: true,
betaFeatures: false,
offlineMode: true,
advancedSearch: false,
});
const handleFeatureToggle = (feature: keyof typeof features) => {
setFeatures((prev) => ({ ...prev, [feature]: !prev[feature] }));
};
const featureList = [
{
key: 'darkMode' as keyof typeof features,
name: 'Dark Mode',
description: 'Switch between light and dark themes',
icon: Eye,
category: 'Appearance',
},
{
key: 'notifications' as keyof typeof features,
name: 'Push Notifications',
description: 'Receive real-time notifications',
icon: Bell,
category: 'Notifications',
},
{
key: 'autoSave' as keyof typeof features,
name: 'Auto Save',
description: 'Automatically save your work',
icon: Zap,
category: 'Productivity',
},
{
key: 'collaboration' as keyof typeof features,
name: 'Real-time Collaboration',
description: 'Work together with team members',
icon: Users,
category: 'Collaboration',
beta: true,
},
{
key: 'analytics' as keyof typeof features,
name: 'Usage Analytics',
description: 'Help improve the product',
icon: Globe,
category: 'Data',
},
{
key: 'betaFeatures' as keyof typeof features,
name: 'Beta Features',
description: 'Access experimental features',
icon: Star,
category: 'Advanced',
beta: true,
},
{
key: 'offlineMode' as keyof typeof features,
name: 'Offline Mode',
description: 'Work without internet connection',
icon: Shield,
category: 'Sync',
},
{
key: 'advancedSearch' as keyof typeof features,
name: 'Advanced Search',
description: 'Enhanced search capabilities',
icon: Settings,
category: 'Search',
beta: true,
},
];
const enabledCount = Object.values(features).filter(Boolean).length;
return (
<Card className="mx-auto w-full max-w-lg">
<CardHeader>
<CardTitle className="text-base">Feature Settings</CardTitle>
<CardDescription>
Customize your experience with these feature toggles
<Badge variant="outline" className="ml-2">
{enabledCount}/{Object.keys(features).length} enabled
</Badge>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{featureList.map((feature) => {
const IconComponent = feature.icon;
const isEnabled = features[feature.key];
return (
<div
key={feature.key}
className="flex items-start space-x-3 rounded-md border p-3"
>
<Checkbox
id={feature.key}
checked={isEnabled}
onCheckedChange={() => handleFeatureToggle(feature.key)}
className="mt-0.5"
/>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
<IconComponent className="text-muted-foreground h-4 w-4" />
<Label
htmlFor={feature.key}
className="cursor-pointer font-medium"
>
{feature.name}
</Label>
{feature.beta && (
<Badge variant="secondary" className="text-xs">
Beta
</Badge>
)}
</div>
<p className="text-muted-foreground text-sm">
{feature.description}
</p>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{feature.category}
</Badge>
{isEnabled && (
<Badge
variant="secondary"
className="bg-green-50 text-xs text-green-700"
>
Active
</Badge>
)}
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
);
},
},
{
title: 'Size Variations',
description: 'Different checkbox sizes for various use cases and contexts',
component: () => {
const [sizes, setSizes] = useState({
small: false,
default: true,
large: false,
});
return (
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle className="text-base">Checkbox Sizes</CardTitle>
<CardDescription>
Different sizes for different contexts and visual hierarchy
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="flex items-center space-x-3">
<Checkbox
id="small"
checked={sizes.small}
onCheckedChange={(checked) =>
setSizes((prev) => ({ ...prev, small: !!checked }))
}
className="h-3 w-3"
/>
<Label htmlFor="small" className="text-sm">
Small checkbox (12px) - Used in compact lists and tables
</Label>
</div>
<div className="flex items-center space-x-3">
<Checkbox
id="default"
checked={sizes.default}
onCheckedChange={(checked) =>
setSizes((prev) => ({ ...prev, default: !!checked }))
}
/>
<Label htmlFor="default">
Default checkbox (16px) - Standard size for most forms
</Label>
</div>
<div className="flex items-center space-x-3">
<Checkbox
id="large"
checked={sizes.large}
onCheckedChange={(checked) =>
setSizes((prev) => ({ ...prev, large: !!checked }))
}
className="h-5 w-5"
/>
<Label htmlFor="large" className="text-base">
Large checkbox (20px) - For touch interfaces and accessibility
</Label>
</div>
</div>
<Separator />
<div className="space-y-3">
<h4 className="text-sm font-medium">Usage Guidelines:</h4>
<ul className="text-muted-foreground space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1 w-1 rounded-full bg-current" />
Small: Dense data tables, list items with many options
</li>
<li className="flex items-center gap-2">
<div className="h-1 w-1 rounded-full bg-current" />
Default: Forms, settings pages, general UI interactions
</li>
<li className="flex items-center gap-2">
<div className="h-1 w-1 rounded-full bg-current" />
Large: Touch interfaces, accessibility requirements,
prominently featured options
</li>
</ul>
</div>
</CardContent>
</Card>
);
},
},
];
const apiReference = {
title: 'Checkbox API Reference',
description: 'Complete API documentation for the Checkbox component.',
props: [
{
name: 'checked',
type: 'boolean | "indeterminate"',
default: 'false',
description: 'The controlled checked state of the checkbox.',
},
{
name: 'defaultChecked',
type: 'boolean',
default: 'false',
description: 'The initial checked state when uncontrolled.',
},
{
name: 'onCheckedChange',
type: '(checked: boolean | "indeterminate") => void',
description: 'Event handler called when the checked state changes.',
},
{
name: 'disabled',
type: 'boolean',
default: 'false',
description:
'When true, prevents the user from interacting with the checkbox.',
},
{
name: 'required',
type: 'boolean',
default: 'false',
description:
'When true, indicates that the user must check the checkbox before submitting.',
},
{
name: 'name',
type: 'string',
description: 'The name of the checkbox submitted with form data.',
},
{
name: 'value',
type: 'string',
default: '"on"',
description: 'The value given as data when submitted with a name.',
},
{
name: 'id',
type: 'string',
description: 'The id attribute of the checkbox element.',
},
{
name: 'className',
type: 'string',
description: 'Additional CSS classes to apply to the checkbox.',
},
{
name: '...props',
type: 'React.ComponentPropsWithRef<typeof CheckboxPrimitive.Root>',
description:
'All props from Radix UI Checkbox.Root component including form, data attributes, etc.',
},
],
examples: [
{
title: 'Basic Usage',
code: `import { Checkbox } from '@kit/ui/checkbox';
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<label htmlFor="terms">
Accept terms and conditions
</label>
</div>`,
},
{
title: 'Controlled Checkbox',
code: `const [checked, setChecked] = useState(false);
<Checkbox
id="newsletter"
checked={checked}
onCheckedChange={setChecked}
/>`,
},
{
title: 'Indeterminate State',
code: `const [checked, setChecked] = useState<boolean | 'indeterminate'>('indeterminate');
<Checkbox
checked={checked}
onCheckedChange={setChecked}
/>`,
},
{
title: 'With Label Component',
code: `import { Label } from '@kit/ui/label';
<div className="items-top flex space-x-2">
<Checkbox id="terms1" />
<div className="grid gap-1.5 leading-none">
<Label
htmlFor="terms1"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions
</Label>
<p className="text-xs text-muted-foreground">
You agree to our Terms of Service and Privacy Policy.
</p>
</div>
</div>`,
},
],
};
const usageGuidelines = {
title: 'Checkbox Usage Guidelines',
description:
'Best practices for implementing checkboxes that provide excellent user experience.',
guidelines: [
{
title: 'When to Use Checkboxes',
items: [
'Allowing users to select multiple options from a list',
'Single binary choice that can be toggled on or off',
'Terms and conditions acceptance in forms',
'Feature toggles and preference settings',
'Bulk selection in lists and data tables (with parent-child relationships)',
],
},
{
title: 'Checkbox vs. Other Controls',
items: [
'Use checkboxes for multiple selections, radio buttons for single selection',
'Use switches for immediate state changes, checkboxes for form submissions',
'Prefer checkboxes over dropdowns when options are few and visible space allows',
'Use checkboxes with labels, not as standalone interactive elements',
'Consider toggle switches for settings that take effect immediately',
],
},
{
title: 'Labeling and Content',
items: [
'Always provide clear, concise labels for checkboxes',
'Use positive language (what will happen when checked)',
'Place labels to the right of checkboxes for better readability',
'Make labels clickable by associating them with checkbox IDs',
'Provide additional context or help text when needed',
],
},
{
title: 'Visual Design',
items: [
'Use consistent checkbox sizes throughout your interface',
'Ensure sufficient color contrast for accessibility (4.5:1 minimum)',
'Provide clear visual feedback for hover, focus, and disabled states',
'Use indeterminate state for parent checkboxes when children are partially selected',
'Maintain adequate spacing between checkboxes and labels',
],
},
{
title: 'Accessibility',
items: [
'Checkboxes automatically include proper ARIA attributes',
'Use fieldset and legend for groups of related checkboxes',
'Ensure keyboard navigation works (Space to toggle, Tab to navigate)',
'Provide sufficient touch targets for mobile interfaces (44px minimum)',
'Announce state changes clearly to screen readers',
],
},
{
title: 'Form Integration',
items: [
'Always validate required checkboxes before form submission',
'Provide clear error messaging for failed validation',
'Maintain checkbox state during form submission errors',
'Use proper name attributes for server-side form handling',
'Group related checkboxes logically with visual separators',
],
},
],
};
export default function CheckboxStory() {
const [controls, setControls] = useState({
checked: false,
indeterminate: false,
disabled: false,
required: false,
size: 'default' as 'sm' | 'default' | 'lg',
className: '',
});
const generateCode = () => {
const checkedValue = controls.indeterminate
? '"indeterminate"'
: controls.checked.toString();
const sizeClass =
controls.size !== 'default' ? sizeClasses[controls.size] : '';
const className = [sizeClass, controls.className].filter(Boolean).join(' ');
const propsString = generatePropsString(
{
id: 'checkbox',
checked: controls.indeterminate
? '"indeterminate"'
: controls.checked
? 'true'
: 'false',
onCheckedChange: 'setChecked',
disabled: controls.disabled ? true : undefined,
required: controls.required ? true : undefined,
className: className || undefined,
},
{
checked: 'false',
disabled: false,
required: false,
},
);
const importStatement = generateImportStatement(
['Checkbox'],
'@kit/ui/checkbox',
);
const labelImport = generateImportStatement(['Label'], '@kit/ui/label');
const stateImport = 'const [checked, setChecked] = useState(false);';
const checkboxComponent = `<div className="flex items-center space-x-2">
<Checkbox${propsString} />
<Label htmlFor="checkbox">
Accept terms and conditions
</Label>
</div>`;
return `${importStatement}\n${labelImport}\n\n${stateImport}\n\n${checkboxComponent}`;
};
const controlsContent = (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="checked">Checked</Label>
<Switch
id="checked"
checked={controls.checked}
onCheckedChange={(checked) =>
setControls((prev) => ({ ...prev, checked, indeterminate: false }))
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="indeterminate">Indeterminate</Label>
<Switch
id="indeterminate"
checked={controls.indeterminate}
onCheckedChange={(indeterminate) =>
setControls((prev) => ({ ...prev, indeterminate, checked: false }))
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="disabled">Disabled</Label>
<Switch
id="disabled"
checked={controls.disabled}
onCheckedChange={(disabled) =>
setControls((prev) => ({ ...prev, disabled }))
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="required">Required</Label>
<Switch
id="required"
checked={controls.required}
onCheckedChange={(required) =>
setControls((prev) => ({ ...prev, required }))
}
/>
</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="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">
<div className="flex items-center space-x-2">
<Checkbox
id="demo-checkbox"
checked={controls.indeterminate ? 'indeterminate' : controls.checked}
onCheckedChange={(checked) => {
if (checked === 'indeterminate') {
setControls((prev) => ({
...prev,
indeterminate: true,
checked: false,
}));
} else {
setControls((prev) => ({
...prev,
checked: !!checked,
indeterminate: false,
}));
}
}}
disabled={controls.disabled}
required={controls.required}
className={`${sizeClasses[controls.size]} ${controls.className}`}
/>
<Label
htmlFor="demo-checkbox"
className={controls.disabled ? 'opacity-50' : ''}
>
Accept terms and conditions{' '}
{controls.required && <span className="text-red-500">*</span>}
</Label>
</div>
</div>
);
return (
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
previewTitle="Interactive Checkbox"
previewDescription="Toggle between different checkbox states"
controlsTitle="Checkbox Configuration"
controlsDescription="Customize checkbox 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>
}
/>
);
}