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:
committed by
GitHub
parent
360ea30f4b
commit
ad427365c9
@@ -3,16 +3,19 @@ description: UI Components API reference and guidelines
|
||||
globs: **/*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# UI Components
|
||||
|
||||
- Reusable UI components are defined in the "packages/ui" package named "@kit/ui".
|
||||
- By exporting the component from the "exports" field, we can import it using the "@kit/ui/{component-name}" format.
|
||||
|
||||
## Styling
|
||||
|
||||
- Styling is done using Tailwind CSS. We use the "cn" function from the "@kit/ui/utils" package to generate class names.
|
||||
- Avoid fixes classes such as "bg-gray-500". Instead, use Shadcn classes such as "bg-background", "text-secondary-foreground", "text-muted-foreground", etc.
|
||||
|
||||
Makerkit leverages two sets of UI components:
|
||||
|
||||
1. **Shadcn UI Components**: Base components from the Shadcn UI library
|
||||
2. **Makerkit-specific Components**: Custom components built on top of Shadcn UI
|
||||
|
||||
@@ -22,86 +25,87 @@ Makerkit leverages two sets of UI components:
|
||||
// Import Shadcn UI components
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card } from '@kit/ui/card';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
// Import Makerkit-specific components
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
```
|
||||
|
||||
## Core Shadcn UI Components
|
||||
|
||||
| Component | Description | Import Path |
|
||||
|-----------|-------------|-------------|
|
||||
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
|
||||
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
|
||||
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
||||
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
|
||||
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
|
||||
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
|
||||
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
|
||||
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
|
||||
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
|
||||
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
|
||||
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
|
||||
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
|
||||
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
|
||||
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
|
||||
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
|
||||
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
|
||||
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
|
||||
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
|
||||
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
|
||||
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
|
||||
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
|
||||
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.tsx) |
|
||||
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
|
||||
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
|
||||
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
|
||||
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/shadcn-sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) |
|
||||
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
|
||||
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
|
||||
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
|
||||
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
|
||||
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
|
||||
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
|
||||
| Component | Description | Import Path |
|
||||
| ---------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
|
||||
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
|
||||
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
||||
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
|
||||
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
|
||||
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
|
||||
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
|
||||
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
|
||||
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
|
||||
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
|
||||
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
|
||||
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
|
||||
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
|
||||
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
|
||||
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
|
||||
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
|
||||
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
|
||||
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
|
||||
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
|
||||
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
|
||||
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
|
||||
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.tsx) |
|
||||
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
|
||||
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
|
||||
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
|
||||
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/shadcn-sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) |
|
||||
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
|
||||
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
|
||||
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
|
||||
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
|
||||
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
|
||||
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
|
||||
|
||||
## Makerkit-specific Components
|
||||
|
||||
| Component | Description | Import Path |
|
||||
|-----------|-------------|-------------|
|
||||
| `If` | Conditional rendering component | `@kit/ui/if` [if.tsx](mdc:packages/ui/src/makerkit/if.tsx) |
|
||||
| `Trans` | Internationalization text component | `@kit/ui/trans` [trans.tsx](mdc:packages/ui/src/makerkit/trans.tsx) |
|
||||
| `Page` | Page layout with navigation | `@kit/ui/page` [page.tsx](mdc:packages/ui/src/makerkit/page.tsx) |
|
||||
| `GlobalLoader` | Full-page loading indicator | `@kit/ui/global-loader` [global-loader.tsx](mdc:packages/ui/src/makerkit/global-loader.tsx) |
|
||||
| `ImageUploader` | Image upload component | `@kit/ui/image-uploader` [image-uploader.tsx](mdc:packages/ui/src/makerkit/image-uploader.tsx) |
|
||||
| `ProfileAvatar` | User avatar with fallback | `@kit/ui/profile-avatar` [profile-avatar.tsx](mdc:packages/ui/src/makerkit/profile-avatar.tsx) |
|
||||
| `DataTable` (Enhanced) | Extended data table with pagination | `@kit/ui/enhanced-data-table` [data-table.tsx](mdc:packages/ui/src/makerkit/data-table.tsx) |
|
||||
| `Stepper` | Multi-step process indicator | `@kit/ui/stepper` [stepper.tsx](mdc:packages/ui/src/makerkit/stepper.tsx) |
|
||||
| `CookieBanner` | GDPR-compliant cookie notice | `@kit/ui/cookie-banner` [cookie-banner.tsx](mdc:packages/ui/src/makerkit/cookie-banner.tsx) |
|
||||
| `CardButton` | Card-styled button | `@kit/ui/card-button` [card-button.tsx](mdc:packages/ui/src/makerkit/card-button.tsx) |
|
||||
| `MultiStepForm` | Form with multiple steps | `@kit/ui/multi-step-form` [multi-step-form.tsx](mdc:packages/ui/src/makerkit/multi-step-form.tsx) |
|
||||
| `EmptyState` | Empty data placeholder | `@kit/ui/empty-state` [empty-state.tsx](mdc:packages/ui/src/makerkit/empty-state.tsx) |
|
||||
| `AppBreadcrumbs` | Application path breadcrumbs | `@kit/ui/app-breadcrumbs` [app-breadcrumbs.tsx](mdc:packages/ui/src/makerkit/app-breadcrumbs.tsx) |
|
||||
| Component | Description | Import Path |
|
||||
| ---------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `If` | Conditional rendering component | `@kit/ui/if` [if.tsx](mdc:packages/ui/src/makerkit/if.tsx) |
|
||||
| `Trans` | Internationalization text component | `@kit/ui/trans` [trans.tsx](mdc:packages/ui/src/makerkit/trans.tsx) |
|
||||
| `Page` | Page layout with navigation | `@kit/ui/page` [page.tsx](mdc:packages/ui/src/makerkit/page.tsx) |
|
||||
| `GlobalLoader` | Full-page loading indicator | `@kit/ui/global-loader` [global-loader.tsx](mdc:packages/ui/src/makerkit/global-loader.tsx) |
|
||||
| `ImageUploader` | Image upload component | `@kit/ui/image-uploader` [image-uploader.tsx](mdc:packages/ui/src/makerkit/image-uploader.tsx) |
|
||||
| `ProfileAvatar` | User avatar with fallback | `@kit/ui/profile-avatar` [profile-avatar.tsx](mdc:packages/ui/src/makerkit/profile-avatar.tsx) |
|
||||
| `DataTable` (Enhanced) | Extended data table with pagination | `@kit/ui/enhanced-data-table` [data-table.tsx](mdc:packages/ui/src/makerkit/data-table.tsx) |
|
||||
| `Stepper` | Multi-step process indicator | `@kit/ui/stepper` [stepper.tsx](mdc:packages/ui/src/makerkit/stepper.tsx) |
|
||||
| `CookieBanner` | GDPR-compliant cookie notice | `@kit/ui/cookie-banner` [cookie-banner.tsx](mdc:packages/ui/src/makerkit/cookie-banner.tsx) |
|
||||
| `CardButton` | Card-styled button | `@kit/ui/card-button` [card-button.tsx](mdc:packages/ui/src/makerkit/card-button.tsx) |
|
||||
| `MultiStepForm` | Form with multiple steps | `@kit/ui/multi-step-form` [multi-step-form.tsx](mdc:packages/ui/src/makerkit/multi-step-form.tsx) |
|
||||
| `EmptyState` | Empty data placeholder | `@kit/ui/empty-state` [empty-state.tsx](mdc:packages/ui/src/makerkit/empty-state.tsx) |
|
||||
| `AppBreadcrumbs` | Application path breadcrumbs | `@kit/ui/app-breadcrumbs` [app-breadcrumbs.tsx](mdc:packages/ui/src/makerkit/app-breadcrumbs.tsx) |
|
||||
|
||||
## Marketing Components
|
||||
|
||||
Import all marketing components with:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Hero,
|
||||
HeroTitle,
|
||||
GradientText,
|
||||
// etc.
|
||||
Hero,
|
||||
HeroTitle,
|
||||
} from '@kit/ui/marketing';
|
||||
```
|
||||
|
||||
Key marketing components:
|
||||
|
||||
- `Hero` - Hero sections [hero.tsx](mdc:packages/ui/src/makerkit/marketing/hero.tsx)
|
||||
- `SecondaryHero` [secondary-hero.tsx](mdc:packages/ui/src/makerkit/marketing/secondary-hero.tsx)
|
||||
- `FeatureCard`, `FeatureGrid` - Feature showcases [feature-card.tsx](mdc:packages/ui/src/makerkit/marketing/feature-card.tsx)
|
||||
- `Footer` - Page Footer [footer.tsx](mdc:packages/ui/src/makerkit/marketing/footer.tsx)
|
||||
- `Header` - Page Header [header.tsx](mdc:packages/ui/src/makerkit/marketing/header.tsx)
|
||||
- `NewsletterSignup` - Email collection [newsletter-signup-container.tsx](mdc:packages/ui/src/makerkit/marketing/newsletter-signup-container.tsx)
|
||||
- `ComingSoon` - Coming soon page template [coming-soon.tsx](mdc:packages/ui/src/makerkit/marketing/coming-soon.tsx)
|
||||
- `ComingSoon` - Coming soon page template [coming-soon.tsx](mdc:packages/ui/src/makerkit/marketing/coming-soon.tsx)
|
||||
|
||||
937
apps/dev-tool/app/components/components/alert-dialog-story.tsx
Normal file
937
apps/dev-tool/app/components/components/alert-dialog-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
656
apps/dev-tool/app/components/components/alert-story.tsx
Normal file
656
apps/dev-tool/app/components/components/alert-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
430
apps/dev-tool/app/components/components/badge-story.tsx
Normal file
430
apps/dev-tool/app/components/components/badge-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
619
apps/dev-tool/app/components/components/breadcrumb-story.tsx
Normal file
619
apps/dev-tool/app/components/components/breadcrumb-story.tsx
Normal 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 };
|
||||
501
apps/dev-tool/app/components/components/button-story.tsx
Normal file
501
apps/dev-tool/app/components/components/button-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
634
apps/dev-tool/app/components/components/calendar-story.tsx
Normal file
634
apps/dev-tool/app/components/components/calendar-story.tsx
Normal 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 };
|
||||
482
apps/dev-tool/app/components/components/card-button-story.tsx
Normal file
482
apps/dev-tool/app/components/components/card-button-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
619
apps/dev-tool/app/components/components/card-story.tsx
Normal file
619
apps/dev-tool/app/components/components/card-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
688
apps/dev-tool/app/components/components/chart-story.tsx
Normal file
688
apps/dev-tool/app/components/components/chart-story.tsx
Normal 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 };
|
||||
1355
apps/dev-tool/app/components/components/checkbox-story.tsx
Normal file
1355
apps/dev-tool/app/components/components/checkbox-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
60
apps/dev-tool/app/components/components/code-card.tsx
Normal file
60
apps/dev-tool/app/components/components/code-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1072
apps/dev-tool/app/components/components/command-story.tsx
Normal file
1072
apps/dev-tool/app/components/components/command-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
33
apps/dev-tool/app/components/components/control-panel.tsx
Normal file
33
apps/dev-tool/app/components/components/control-panel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
505
apps/dev-tool/app/components/components/cookie-banner-story.tsx
Normal file
505
apps/dev-tool/app/components/components/cookie-banner-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1108
apps/dev-tool/app/components/components/data-table-story.tsx
Normal file
1108
apps/dev-tool/app/components/components/data-table-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
917
apps/dev-tool/app/components/components/dialog-story.tsx
Normal file
917
apps/dev-tool/app/components/components/dialog-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
30
apps/dev-tool/app/components/components/docs-content.tsx
Normal file
30
apps/dev-tool/app/components/components/docs-content.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
58
apps/dev-tool/app/components/components/docs-header.tsx
Normal file
58
apps/dev-tool/app/components/components/docs-header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
95
apps/dev-tool/app/components/components/docs-provider.tsx
Normal file
95
apps/dev-tool/app/components/components/docs-provider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
271
apps/dev-tool/app/components/components/docs-sidebar.tsx
Normal file
271
apps/dev-tool/app/components/components/docs-sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1101
apps/dev-tool/app/components/components/dropdown-menu-story.tsx
Normal file
1101
apps/dev-tool/app/components/components/dropdown-menu-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
633
apps/dev-tool/app/components/components/empty-state-story.tsx
Normal file
633
apps/dev-tool/app/components/components/empty-state-story.tsx
Normal 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">() => 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
598
apps/dev-tool/app/components/components/file-uploader-story.tsx
Normal file
598
apps/dev-tool/app/components/components/file-uploader-story.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1156
apps/dev-tool/app/components/components/form-story.tsx
Normal file
1156
apps/dev-tool/app/components/components/form-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
552
apps/dev-tool/app/components/components/heading-story.tsx
Normal file
552
apps/dev-tool/app/components/components/heading-story.tsx
Normal 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 };
|
||||
813
apps/dev-tool/app/components/components/input-otp-story.tsx
Normal file
813
apps/dev-tool/app/components/components/input-otp-story.tsx
Normal 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 };
|
||||
784
apps/dev-tool/app/components/components/input-story.tsx
Normal file
784
apps/dev-tool/app/components/components/input-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
apps/dev-tool/app/components/components/loading-fallback.tsx
Normal file
20
apps/dev-tool/app/components/components/loading-fallback.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 (< 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
42
apps/dev-tool/app/components/components/preview-card.tsx
Normal file
42
apps/dev-tool/app/components/components/preview-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
897
apps/dev-tool/app/components/components/progress-story.tsx
Normal file
897
apps/dev-tool/app/components/components/progress-story.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
854
apps/dev-tool/app/components/components/radio-group-story.tsx
Normal file
854
apps/dev-tool/app/components/components/radio-group-story.tsx
Normal 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 };
|
||||
841
apps/dev-tool/app/components/components/select-story.tsx
Normal file
841
apps/dev-tool/app/components/components/select-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
646
apps/dev-tool/app/components/components/skeleton-story.tsx
Normal file
646
apps/dev-tool/app/components/components/skeleton-story.tsx
Normal 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 };
|
||||
1114
apps/dev-tool/app/components/components/sonner-story.tsx
Normal file
1114
apps/dev-tool/app/components/components/sonner-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
292
apps/dev-tool/app/components/components/spinner-story.tsx
Normal file
292
apps/dev-tool/app/components/components/spinner-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
439
apps/dev-tool/app/components/components/stepper-story.tsx
Normal file
439
apps/dev-tool/app/components/components/stepper-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
135
apps/dev-tool/app/components/components/story-layout.tsx
Normal file
135
apps/dev-tool/app/components/components/story-layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
165
apps/dev-tool/app/components/components/story-select.tsx
Normal file
165
apps/dev-tool/app/components/components/story-select.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
859
apps/dev-tool/app/components/components/switch-story.tsx
Normal file
859
apps/dev-tool/app/components/components/switch-story.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1327
apps/dev-tool/app/components/components/tabs-story.tsx
Normal file
1327
apps/dev-tool/app/components/components/tabs-story.tsx
Normal file
File diff suppressed because it is too large
Load Diff
964
apps/dev-tool/app/components/components/textarea-story.tsx
Normal file
964
apps/dev-tool/app/components/components/textarea-story.tsx
Normal 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 };
|
||||
933
apps/dev-tool/app/components/components/tooltip-story.tsx
Normal file
933
apps/dev-tool/app/components/components/tooltip-story.tsx
Normal 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;
|
||||
975
apps/dev-tool/app/components/lib/components-data.tsx
Normal file
975
apps/dev-tool/app/components/lib/components-data.tsx
Normal 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)),
|
||||
];
|
||||
155
apps/dev-tool/app/components/lib/story-utils.ts
Normal file
155
apps/dev-tool/app/components/lib/story-utils.ts
Normal 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;
|
||||
}
|
||||
32
apps/dev-tool/app/components/page.tsx
Normal file
32
apps/dev-tool/app/components/page.tsx
Normal 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);
|
||||
@@ -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} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
|
||||
|
||||
import {
|
||||
BoltIcon,
|
||||
ComponentIcon,
|
||||
LanguagesIcon,
|
||||
LayoutDashboardIcon,
|
||||
MailIcon,
|
||||
@@ -32,6 +33,11 @@ const routes = [
|
||||
path: '/variables',
|
||||
Icon: BoltIcon,
|
||||
},
|
||||
{
|
||||
label: 'Components',
|
||||
path: '/components',
|
||||
Icon: ComponentIcon,
|
||||
},
|
||||
{
|
||||
label: 'Emails',
|
||||
path: '/emails',
|
||||
|
||||
@@ -4,10 +4,18 @@ import { useState } from 'react';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { I18nProvider } from '@kit/i18n/provider';
|
||||
import { Toaster } from '@kit/ui/sonner';
|
||||
|
||||
export function RootProviders({ children }: React.PropsWithChildren) {
|
||||
return <ReactQueryProvider>{children}</ReactQueryProvider>;
|
||||
import { i18nResolver } from '../lib/i18n/i18n.resolver';
|
||||
import { getI18nSettings } from '../lib/i18n/i18n.settings';
|
||||
|
||||
export function RootProviders(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<I18nProvider settings={getI18nSettings('en')} resolver={i18nResolver}>
|
||||
<ReactQueryProvider>{props.children}</ReactQueryProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ReactQueryProvider(props: React.PropsWithChildren) {
|
||||
|
||||
31
apps/dev-tool/lib/i18n/i18n.resolver.ts
Normal file
31
apps/dev-tool/lib/i18n/i18n.resolver.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
/**
|
||||
* @name i18nResolver
|
||||
* @description Resolve the translation file for the given language and namespace in the dev-tool application.
|
||||
* @param language
|
||||
* @param namespace
|
||||
*/
|
||||
export async function i18nResolver(language: string, namespace: string) {
|
||||
const logger = await getLogger();
|
||||
|
||||
try {
|
||||
const data = await import(
|
||||
`../../../web/public/locales/${language}/${namespace}.json`
|
||||
);
|
||||
|
||||
return data as Record<string, string>;
|
||||
} catch (error) {
|
||||
console.group(
|
||||
`Error while loading translation file: ${language}/${namespace}`,
|
||||
);
|
||||
logger.error(error instanceof Error ? error.message : error);
|
||||
logger.warn(
|
||||
`Please create a translation file for this language at "public/locales/${language}/${namespace}.json"`,
|
||||
);
|
||||
console.groupEnd();
|
||||
|
||||
// return an empty object if the file could not be loaded to avoid loops
|
||||
return {};
|
||||
}
|
||||
}
|
||||
10
apps/dev-tool/lib/i18n/i18n.server.ts
Normal file
10
apps/dev-tool/lib/i18n/i18n.server.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { initializeServerI18n } from '@kit/i18n/server';
|
||||
|
||||
import { i18nResolver } from './i18n.resolver';
|
||||
import { getI18nSettings } from './i18n.settings';
|
||||
|
||||
export function createI18nServerInstance(language?: string) {
|
||||
const settings = getI18nSettings(language);
|
||||
|
||||
return initializeServerI18n(settings, i18nResolver);
|
||||
}
|
||||
52
apps/dev-tool/lib/i18n/i18n.settings.ts
Normal file
52
apps/dev-tool/lib/i18n/i18n.settings.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createI18nSettings } from '@kit/i18n';
|
||||
|
||||
/**
|
||||
* The default language of the application.
|
||||
* This is used as a fallback language when the selected language is not supported.
|
||||
*/
|
||||
const defaultLanguage = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en';
|
||||
|
||||
/**
|
||||
* The list of supported languages.
|
||||
* By default, only the default language is supported.
|
||||
* Add more languages here if needed.
|
||||
*/
|
||||
export const languages: string[] = [defaultLanguage];
|
||||
|
||||
/**
|
||||
* The name of the cookie that stores the selected language.
|
||||
*/
|
||||
export const I18N_COOKIE_NAME = 'lang';
|
||||
|
||||
/**
|
||||
* The default array of Internationalization (i18n) namespaces.
|
||||
* These namespaces are commonly used in the application for translation purposes.
|
||||
*/
|
||||
export const defaultI18nNamespaces = ['common'];
|
||||
|
||||
/**
|
||||
* Get the i18n settings for the given language and namespaces.
|
||||
* If the language is not supported, it will fall back to the default language.
|
||||
* @param language
|
||||
* @param ns
|
||||
*/
|
||||
export function getI18nSettings(
|
||||
language: string | undefined,
|
||||
ns: string | string[] = defaultI18nNamespaces,
|
||||
) {
|
||||
let lng = language ?? defaultLanguage;
|
||||
|
||||
if (!languages.includes(lng)) {
|
||||
console.warn(
|
||||
`Language "${lng}" is not supported. Falling back to "${defaultLanguage}"`,
|
||||
);
|
||||
|
||||
lng = defaultLanguage;
|
||||
}
|
||||
|
||||
return createI18nSettings({
|
||||
language: lng,
|
||||
namespaces: ns,
|
||||
languages,
|
||||
});
|
||||
}
|
||||
13
apps/dev-tool/lib/i18n/with-i18n.tsx
Normal file
13
apps/dev-tool/lib/i18n/with-i18n.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createI18nServerInstance } from './i18n.server';
|
||||
|
||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||
|
||||
export function withI18n<Params extends object>(
|
||||
Component: LayoutOrPageComponent<Params>,
|
||||
) {
|
||||
return async function I18nServerComponentWrapper(params: Params) {
|
||||
await createI18nServerInstance();
|
||||
|
||||
return <Component {...params} />;
|
||||
};
|
||||
}
|
||||
@@ -8,12 +8,13 @@
|
||||
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.16",
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@tanstack/react-query": "5.85.5",
|
||||
"ai": "5.0.16",
|
||||
"ai": "5.0.21",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"nodemailer": "^7.0.5",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
@@ -21,18 +22,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/email-templates": "workspace:*",
|
||||
"@kit/i18n": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/nodemailer": "7.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"pino-pretty": "13.0.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"recharts": "2.15.3",
|
||||
"tailwindcss": "4.1.12",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.2",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@types/node": "^24.3.0",
|
||||
"dotenv": "17.2.1",
|
||||
"node-html-parser": "^7.0.1",
|
||||
|
||||
@@ -5,7 +5,8 @@ dotenvConfig();
|
||||
dotenvConfig({ path: '.env.local' });
|
||||
|
||||
const enableBillingTests = process.env.ENABLE_BILLING_TESTS === 'true';
|
||||
const enableTeamAccountTests = (process.env.ENABLE_TEAM_ACCOUNT_TESTS ?? 'true') === 'true';
|
||||
const enableTeamAccountTests =
|
||||
(process.env.ENABLE_TEAM_ACCOUNT_TESTS ?? 'true') === 'true';
|
||||
|
||||
const testIgnore: string[] = [];
|
||||
|
||||
@@ -44,7 +45,7 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 3 : 1,
|
||||
retries: 3,
|
||||
/* Limit parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
@@ -61,13 +62,13 @@ export default defineConfig({
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
navigationTimeout: 5000,
|
||||
navigationTimeout: 15_000,
|
||||
},
|
||||
// test timeout set to 1 minutes
|
||||
timeout: 60 * 1000,
|
||||
// test timeout set to 2 minutes
|
||||
timeout: 120 * 1000,
|
||||
expect: {
|
||||
// expect timeout set to 10 seconds
|
||||
timeout: 10 * 1000,
|
||||
// expect timeout set to 5 seconds
|
||||
timeout: 5 * 1000,
|
||||
},
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
||||
@@ -113,13 +113,23 @@ test.describe('Admin', () => {
|
||||
// Try with invalid confirmation
|
||||
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
||||
await page.getByRole('button', { name: 'Ban User' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Ban User' }),
|
||||
).toBeVisible(); // Dialog should still be open
|
||||
|
||||
// Confirm with correct text
|
||||
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||
await page.getByRole('button', { name: 'Ban User' }).click();
|
||||
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Ban User' }).click(),
|
||||
page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/admin/accounts') &&
|
||||
response.status() === 200,
|
||||
),
|
||||
]);
|
||||
|
||||
await expect(page.getByText('Banned')).toBeVisible();
|
||||
|
||||
await page.context().clearCookies();
|
||||
@@ -156,7 +166,15 @@ test.describe('Admin', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||
await page.getByRole('button', { name: 'Reactivate User' }).click();
|
||||
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Reactivate User' }).click(),
|
||||
page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/admin/accounts') &&
|
||||
response.status() === 200,
|
||||
),
|
||||
]);
|
||||
|
||||
// Verify ban badge is removed
|
||||
await expect(page.getByText('Banned')).not.toBeVisible();
|
||||
@@ -192,26 +210,31 @@ test.describe('Admin', () => {
|
||||
|
||||
test('delete user flow', async ({ page }) => {
|
||||
await page.getByTestId('admin-delete-account-button').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete User' }),
|
||||
).toBeVisible();
|
||||
|
||||
// Try with invalid confirmation
|
||||
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete User' }),
|
||||
).toBeVisible(); // Dialog should still be open
|
||||
|
||||
// Confirm with correct text
|
||||
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
// Should redirect to admin dashboard
|
||||
await expect(page).toHaveURL('/admin/accounts');
|
||||
await page.waitForURL('/admin/accounts');
|
||||
|
||||
// Log out
|
||||
await page.context().clearCookies();
|
||||
await page.waitForURL('/');
|
||||
|
||||
// Verify user can't log in
|
||||
await page.goto('/auth/sign-in');
|
||||
@@ -231,7 +254,10 @@ test.describe('Admin', () => {
|
||||
});
|
||||
|
||||
test.describe('Team Account Management', () => {
|
||||
test.skip(process.env.ENABLE_TEAM_ACCOUNT_TESTS !== 'true', 'Team account tests are disabled');
|
||||
test.skip(
|
||||
process.env.ENABLE_TEAM_ACCOUNT_TESTS !== 'true',
|
||||
'Team account tests are disabled',
|
||||
);
|
||||
|
||||
let testUserEmail: string;
|
||||
let teamName: string;
|
||||
@@ -358,6 +384,7 @@ async function createUser(
|
||||
async function filterAccounts(page: Page, email: string) {
|
||||
await page
|
||||
.locator('[data-test="admin-accounts-table-filter-input"]')
|
||||
.first()
|
||||
.fill(email);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
@@ -366,4 +393,6 @@ async function filterAccounts(page: Page, email: string) {
|
||||
|
||||
async function selectAccount(page: Page, email: string) {
|
||||
await page.getByRole('link', { name: email.split('@')[0] }).click();
|
||||
await page.waitForURL(new RegExp(`/admin/accounts/[a-z0-9-]+`));
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ import { Trans } from '@kit/ui/trans';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
const ModeToggle = dynamic(() =>
|
||||
import('@kit/ui/mode-toggle').then((mod) => ({
|
||||
default: mod.ModeToggle,
|
||||
})),
|
||||
const ModeToggle = dynamic(
|
||||
() =>
|
||||
import('@kit/ui/mode-toggle').then((mod) => ({
|
||||
default: mod.ModeToggle,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const MobileModeToggle = dynamic(() =>
|
||||
|
||||
@@ -52,6 +52,7 @@ const config = {
|
||||
experimental: {
|
||||
mdxRs: true,
|
||||
reactCompiler: ENABLE_REACT_COMPILER,
|
||||
clientSegmentCache: true,
|
||||
optimizePackageImports: [
|
||||
'recharts',
|
||||
'lucide-react',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"clean": "git clean -xdf .next .turbo node_modules",
|
||||
"dev": "next dev --turbo | pino-pretty -c",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "next lint --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"",
|
||||
"start": "next start",
|
||||
"start:test": "NODE_ENV=test next start",
|
||||
@@ -61,13 +61,13 @@
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.1",
|
||||
"recharts": "2.15.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^3.25.74"
|
||||
@@ -76,7 +76,7 @@
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@next/bundle-analyzer": "15.4.7",
|
||||
"@next/bundle-analyzer": "15.5.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "19.1.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-supabase-saas-kit-turbo",
|
||||
"version": "2.12.3",
|
||||
"version": "2.13.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
|
||||
@@ -8,4 +8,4 @@ export const analytics: AnalyticsManager = createAnalyticsManager({
|
||||
providers: {
|
||||
null: () => NullAnalyticsService,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
"@types/react": "19.1.10",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.1",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@types/react": "19.1.10",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@kit/ui": "workspace:*",
|
||||
"@types/react": "19.1.10",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.5.0"
|
||||
"@react-email/components": "0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.1",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/react": "19.1.10",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
|
||||
@@ -151,7 +151,7 @@ async function PersonalAccountPage(props: { account: Account }) {
|
||||
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
||||
<Heading level={6}>Teams</Heading>
|
||||
|
||||
<div>
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<AdminMembershipsTable memberships={memberships} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,7 +216,9 @@ async function TeamAccountPage(props: {
|
||||
<div className={'flex flex-col gap-y-2.5'}>
|
||||
<Heading level={6}>Team Members</Heading>
|
||||
|
||||
<AdminMembersTable members={members} />
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<AdminMembersTable members={members} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,13 +63,15 @@ export function AdminAccountsTable(
|
||||
<AccountsTableFilters filters={props.filters} />
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
pageSize={props.pageSize}
|
||||
pageIndex={props.page - 1}
|
||||
pageCount={props.pageCount}
|
||||
data={props.data}
|
||||
columns={getColumns()}
|
||||
/>
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<DataTable
|
||||
pageSize={props.pageSize}
|
||||
pageIndex={props.page - 1}
|
||||
pageCount={props.pageCount}
|
||||
data={props.data}
|
||||
columns={getColumns()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
@@ -71,9 +73,14 @@ export function AdminDeleteUserDialog(
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteUserAction(data);
|
||||
|
||||
setError(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
if (isRedirectError(error)) {
|
||||
// Do nothing
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
})}
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
"@tanstack/react-query": "5.85.5",
|
||||
"@types/react": "19.1.10",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.1",
|
||||
"sonner": "^2.0.7",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"lucide-react": "^0.540.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-i18next": "^15.6.1"
|
||||
"react-i18next": "^15.7.1"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"typesVersions": {
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.1",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@tanstack/react-query": "5.85.5",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-i18next": "^15.6.1"
|
||||
"react-i18next": "^15.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.3.6",
|
||||
"i18next": "25.4.0",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@kit/mailers-shared": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@types/nodemailer": "7.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@supabase/supabase-js": "2.55.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@supabase/supabase-js": "2.55.0",
|
||||
"@tanstack/react-query": "5.85.5",
|
||||
"@types/react": "19.1.10",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"react": "19.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"zod": "^3.25.74"
|
||||
|
||||
@@ -33,12 +33,12 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint": "^9.33.0",
|
||||
"next": "15.4.7",
|
||||
"next": "15.5.0",
|
||||
"next-themes": "0.4.6",
|
||||
"prettier": "^3.6.2",
|
||||
"react-day-picker": "^9.9.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwindcss": "4.1.12",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -68,9 +68,11 @@
|
||||
"./input": "./src/shadcn/input.tsx",
|
||||
"./label": "./src/shadcn/label.tsx",
|
||||
"./popover": "./src/shadcn/popover.tsx",
|
||||
"./progress": "./src/shadcn/progress.tsx",
|
||||
"./scroll-area": "./src/shadcn/scroll-area.tsx",
|
||||
"./select": "./src/shadcn/select.tsx",
|
||||
"./sheet": "./src/shadcn/sheet.tsx",
|
||||
"./slider": "./src/shadcn/slider.tsx",
|
||||
"./table": "./src/shadcn/table.tsx",
|
||||
"./tabs": "./src/shadcn/tabs.tsx",
|
||||
"./tooltip": "./src/shadcn/tooltip.tsx",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
29
packages/ui/src/shadcn/slider.tsx
Normal file
29
packages/ui/src/shadcn/slider.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { Slider as SliderPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full touch-none items-center select-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full">
|
||||
<SliderPrimitive.Range className="bg-primary absolute h-full" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
@@ -6,20 +6,20 @@ const Table: React.FC<React.HTMLAttributes<HTMLTableElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<div
|
||||
className={cn('bg-background relative flex flex-1 flex-col overflow-auto')}
|
||||
>
|
||||
<table
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
Table.displayName = 'Table';
|
||||
|
||||
const TableHeader: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <thead className={cn('[&_tr]:border-b', className)} {...props} />;
|
||||
TableHeader.displayName = 'TableHeader';
|
||||
|
||||
const TableBody: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({
|
||||
className,
|
||||
@@ -27,7 +27,6 @@ const TableBody: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({
|
||||
}) => (
|
||||
<tbody className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
);
|
||||
TableBody.displayName = 'TableBody';
|
||||
|
||||
const TableFooter: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({
|
||||
className,
|
||||
@@ -35,13 +34,12 @@ const TableFooter: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({
|
||||
}) => (
|
||||
<tfoot
|
||||
className={cn(
|
||||
'bg-muted/50 border-t font-medium last:[&>tr]:border-b-0',
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
TableFooter.displayName = 'TableFooter';
|
||||
|
||||
const TableRow: React.FC<React.HTMLAttributes<HTMLTableRowElement>> = ({
|
||||
className,
|
||||
@@ -49,13 +47,12 @@ const TableRow: React.FC<React.HTMLAttributes<HTMLTableRowElement>> = ({
|
||||
}) => (
|
||||
<tr
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted group/row border-b transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
TableRow.displayName = 'TableRow';
|
||||
|
||||
const TableHead: React.FC<React.ThHTMLAttributes<HTMLTableCellElement>> = ({
|
||||
className,
|
||||
@@ -63,13 +60,12 @@ const TableHead: React.FC<React.ThHTMLAttributes<HTMLTableCellElement>> = ({
|
||||
}) => (
|
||||
<th
|
||||
className={cn(
|
||||
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
'text-muted-foreground h-8 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
TableHead.displayName = 'TableHead';
|
||||
|
||||
const TableCell: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = ({
|
||||
className,
|
||||
@@ -77,13 +73,12 @@ const TableCell: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = ({
|
||||
}) => (
|
||||
<td
|
||||
className={cn(
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
'px-2 py-1.5 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
TableCell.displayName = 'TableCell';
|
||||
|
||||
const TableCaption: React.FC<React.HTMLAttributes<HTMLTableCaptionElement>> = ({
|
||||
className,
|
||||
@@ -94,7 +89,6 @@ const TableCaption: React.FC<React.HTMLAttributes<HTMLTableCaptionElement>> = ({
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
||||
export {
|
||||
Table,
|
||||
|
||||
483
pnpm-lock.yaml
generated
483
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,9 @@
|
||||
"format": "prettier --check \"**/*.{js,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.4.7",
|
||||
"@next/eslint-plugin-next": "15.5.0",
|
||||
"@types/eslint": "9.6.1",
|
||||
"eslint-config-next": "15.4.7",
|
||||
"eslint-config-next": "15.5.0",
|
||||
"eslint-config-turbo": "^2.5.6",
|
||||
"typescript-eslint": "8.40.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user