Files
myeasycms-v2/apps/dev-tool/app/components/components/calendar-story.tsx
Giancarlo Buomprisco ad427365c9 Storybook (#328)
* feat(docs): add interactive examples and API references for Button, Card, and LoadingFallback components
- Updated dependencies
- Set `retries` to a fixed value of 3 for consistent test retries across environments.
- Increased `timeout` from 60 seconds to 120 seconds to allow more time for tests to complete.
- Reduced `expect` timeout from 10 seconds to 5 seconds for quicker feedback on assertions.
2025-08-22 07:35:44 +08:00

635 lines
20 KiB
TypeScript

'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 };