* 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.
635 lines
20 KiB
TypeScript
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 };
|