mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? feat(storybook): Storybook with Calendar and Modal components #9869 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@ -27,6 +27,21 @@ const config: StorybookConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.less$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,6 +14,12 @@ const sizeClasses = {
|
|||||||
large: 'w-8 h-8',
|
large: 'w-8 h-8',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const minSizeClasses = {
|
||||||
|
small: 'min-w-4 min-h-4',
|
||||||
|
default: 'min-w-6 min-h-6',
|
||||||
|
large: 'min-w-8 min-h-8',
|
||||||
|
};
|
||||||
|
|
||||||
export const Spin: React.FC<SpinProps> = ({
|
export const Spin: React.FC<SpinProps> = ({
|
||||||
spinning = true,
|
spinning = true,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
@ -32,7 +38,12 @@ export const Spin: React.FC<SpinProps> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{spinning && (
|
{spinning && (
|
||||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-text-primary/30 ">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute inset-0 z-10 flex items-center justify-center bg-text-primary/30',
|
||||||
|
minSizeClasses[size],
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full border-muted-foreground border-2 border-t-transparent animate-spin',
|
'rounded-full border-muted-foreground border-2 border-t-transparent animate-spin',
|
||||||
|
|||||||
296
web/src/stories/calendar.stories.tsx
Normal file
296
web/src/stories/calendar.stories.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import { Calendar } from '@/components/originui/calendar';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||||
|
const meta = {
|
||||||
|
title: 'Example/Calendar',
|
||||||
|
component: Calendar,
|
||||||
|
parameters: {
|
||||||
|
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
layout: 'centered',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
## Calendar Component
|
||||||
|
|
||||||
|
Calendar is a date picker component based on react-day-picker that allows users to select dates or date ranges. It provides a clean and customizable interface for date selection with support for various customization options.
|
||||||
|
|
||||||
|
### Import Path
|
||||||
|
\`\`\`typescript
|
||||||
|
import { Calendar } from '@/components/originui/calendar';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
\`\`\`tsx
|
||||||
|
import { Calendar } from '@/components/originui/calendar';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
className="rounded-md border"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Single date selection
|
||||||
|
- Date range selection
|
||||||
|
- Customizable styling with className prop
|
||||||
|
- Navigation between months
|
||||||
|
- Today highlighting
|
||||||
|
- Disabled dates support
|
||||||
|
- Customizable components
|
||||||
|
- Built with Tailwind CSS
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||||
|
tags: ['autodocs'],
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||||
|
argTypes: {
|
||||||
|
mode: {
|
||||||
|
description: 'Selection mode - single date or range',
|
||||||
|
control: { type: 'radio' },
|
||||||
|
options: ['single', 'range'],
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
description: 'Selected date or date range',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
onSelect: {
|
||||||
|
description: 'Callback function when date is selected',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'Additional CSS classes for styling',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
classNames: {
|
||||||
|
description: 'Custom class names for internal elements',
|
||||||
|
control: { type: 'object' },
|
||||||
|
},
|
||||||
|
showOutsideDays: {
|
||||||
|
description: 'Whether to show outside days',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
description: 'Custom components for calendar elements',
|
||||||
|
control: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Calendar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
showOutsideDays: true,
|
||||||
|
className: 'rounded-md border',
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
showOutsideDays={true}
|
||||||
|
className="rounded-md border"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Default Calendar
|
||||||
|
|
||||||
|
Shows the basic calendar with single date selection mode.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
className="rounded-md border"
|
||||||
|
showOutsideDays={true}
|
||||||
|
/>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RangeSelection: Story = {
|
||||||
|
args: {
|
||||||
|
showOutsideDays: true,
|
||||||
|
className: 'rounded-md border',
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [range, setRange] = useState<{
|
||||||
|
from: Date | undefined;
|
||||||
|
to?: Date | undefined;
|
||||||
|
}>({
|
||||||
|
from: new Date(),
|
||||||
|
to: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
selected={range}
|
||||||
|
onSelect={(range) =>
|
||||||
|
setRange(range as { from: Date | undefined; to?: Date | undefined })
|
||||||
|
}
|
||||||
|
showOutsideDays={true}
|
||||||
|
className="rounded-md border"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Range Selection Calendar
|
||||||
|
|
||||||
|
Shows the calendar with date range selection mode.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [range, setRange] = useState<{ from: Date | undefined; to?: Date | undefined }>({
|
||||||
|
from: new Date(),
|
||||||
|
to: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
selected={range}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (!range.from) {
|
||||||
|
setRange({ from: date });
|
||||||
|
} else if (!range.to && date && date > range.from) {
|
||||||
|
setRange({ from: range.from, to: date });
|
||||||
|
} else {
|
||||||
|
setRange({ from: date });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="rounded-md border"
|
||||||
|
showOutsideDays={true}
|
||||||
|
/>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutOutsideDays: Story = {
|
||||||
|
args: {
|
||||||
|
showOutsideDays: false,
|
||||||
|
className: 'rounded-md border',
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
showOutsideDays={false}
|
||||||
|
className="rounded-md border"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Calendar without Outside Days
|
||||||
|
|
||||||
|
Shows the calendar without displaying days from previous/next months.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
className="rounded-md border"
|
||||||
|
showOutsideDays={false}
|
||||||
|
/>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomStyling: Story = {
|
||||||
|
args: {
|
||||||
|
showOutsideDays: true,
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
showOutsideDays={true}
|
||||||
|
className="rounded-md border-2 border-primary bg-secondary"
|
||||||
|
classNames={{
|
||||||
|
caption_label: 'text-lg font-bold text-primary',
|
||||||
|
day_button: 'size-10 rounded-full hover:bg-primary/20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Custom Styled Calendar
|
||||||
|
|
||||||
|
Shows the calendar with custom styling using className and classNames props.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
className="rounded-md border-2 border-primary bg-secondary"
|
||||||
|
classNames={{
|
||||||
|
caption_label: 'text-lg font-bold text-primary',
|
||||||
|
day_button: 'size-10 rounded-full hover:bg-primary/20',
|
||||||
|
}}
|
||||||
|
showOutsideDays={true}
|
||||||
|
/>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
738
web/src/stories/modal.stories.tsx
Normal file
738
web/src/stories/modal.stories.tsx
Normal file
@ -0,0 +1,738 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||||
|
const meta = {
|
||||||
|
title: 'Example/Modal',
|
||||||
|
component: Modal,
|
||||||
|
parameters: {
|
||||||
|
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
layout: 'centered',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
## Modal Component
|
||||||
|
|
||||||
|
The Modal component is a dialog overlay that can be used to present content in a modal window. It provides a flexible way to display information, forms, or any other content on top of the main page content.
|
||||||
|
|
||||||
|
### Import Path
|
||||||
|
\`\`\`typescript
|
||||||
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
\`\`\`tsx
|
||||||
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setOpen(true)}>Open Modal</button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Modal Title"
|
||||||
|
>
|
||||||
|
<p>Modal content goes here</p>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Multiple sizes: small, default, and large
|
||||||
|
- Customizable header with title and close button
|
||||||
|
- Customizable footer with default OK/Cancel buttons
|
||||||
|
- Support for controlled and uncontrolled usage
|
||||||
|
- Loading state for confirmation button
|
||||||
|
- Keyboard navigation support (ESC to close)
|
||||||
|
- Click outside to close functionality
|
||||||
|
- Full screen mode option
|
||||||
|
- Built with Radix UI primitives for accessibility
|
||||||
|
- Customizable styling with className props
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||||
|
tags: ['autodocs'],
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||||
|
argTypes: {
|
||||||
|
open: {
|
||||||
|
description: 'Whether the modal is open or not',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
onOpenChange: {
|
||||||
|
description:
|
||||||
|
'Callback function that is called when the open state changes',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
description: 'Title of the modal',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
titleClassName: {
|
||||||
|
description: 'Additional CSS classes for the title container',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
description: 'Content to be displayed inside the modal',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
description:
|
||||||
|
'Custom footer content. If not provided, default buttons will be shown',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
footerClassName: {
|
||||||
|
description: 'Additional CSS classes for the footer container',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
showfooter: {
|
||||||
|
description: 'Whether to show the footer or not',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'Additional CSS classes for the modal container',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'Size of the modal',
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['small', 'default', 'large'],
|
||||||
|
},
|
||||||
|
closable: {
|
||||||
|
description: 'Whether to show the close button in the header',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
description: 'Custom close icon',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
maskClosable: {
|
||||||
|
description: 'Whether to close the modal when clicking on the mask',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
destroyOnClose: {
|
||||||
|
description: 'Whether to unmount the modal content when closed',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
description: 'Whether the modal should take the full screen',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
confirmLoading: {
|
||||||
|
description: 'Whether the confirm button should show a loading state',
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
cancelText: {
|
||||||
|
description: 'Text for the cancel button',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
okText: {
|
||||||
|
description: 'Text for the OK button',
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
onOk: {
|
||||||
|
description: 'Callback function for the OK button',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
onCancel: {
|
||||||
|
description: 'Callback function for the Cancel button',
|
||||||
|
control: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Modal>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Default Modal',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal Content</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This is the default modal with standard size and functionality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Default Modal</Button>
|
||||||
|
<Modal open={open} onOpenChange={setOpen} title={args.title}>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Default Modal
|
||||||
|
|
||||||
|
Shows the basic modal with default size and standard header/footer.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Default Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Default Modal"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal Content</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This is the default modal with standard size and functionality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Small Modal',
|
||||||
|
size: 'small',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Small Modal</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This is a small modal, suitable for simple confirmations or short
|
||||||
|
messages.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Small Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
size={args.size}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Small Modal
|
||||||
|
|
||||||
|
Shows a small-sized modal, ideal for confirmations or brief messages.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Small Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Small Modal"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Small Modal</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This is a small modal, suitable for simple confirmations or short messages.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Large Modal',
|
||||||
|
size: 'large',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Large Modal</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
This is a large modal with more content. It can accommodate forms,
|
||||||
|
tables, or other complex content.
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted p-4 rounded-md">
|
||||||
|
<p>Additional content area</p>
|
||||||
|
<p className="mt-2">You can put any content here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Large Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
size={args.size}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Large Modal
|
||||||
|
|
||||||
|
Shows a large-sized modal, suitable for complex content like forms or data tables.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Large Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Large Modal"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Large Modal</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
This is a large modal with more content. It can accommodate forms, tables, or other complex content.
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted p-4 rounded-md">
|
||||||
|
<p>Additional content area</p>
|
||||||
|
<p className="mt-2">You can put any content here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithCustomFooter: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Custom Footer',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal with Custom Footer</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This modal has a custom footer with multiple buttons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>
|
||||||
|
Open Modal with Custom Footer
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-between w-full">
|
||||||
|
<Button variant="outline">Secondary Action</Button>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button>Primary Action</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Custom Footer
|
||||||
|
|
||||||
|
Shows a modal with a custom footer. You can provide your own footer content instead of using the default OK/Cancel buttons.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Modal with Custom Footer</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Custom Footer"
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-between w-full">
|
||||||
|
<Button variant="outline">Secondary Action</Button>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
|
||||||
|
<Button>Primary Action</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal with Custom Footer</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This modal has a custom footer with multiple buttons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutFooter: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'No Footer',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal without Footer</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This modal has no footer. The content area extends to the bottom of
|
||||||
|
the modal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Modal without Footer</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
showfooter={false}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Without Footer
|
||||||
|
|
||||||
|
Shows a modal without a footer. Useful when you want to include action buttons within the content area or don't need any footer actions.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Modal without Footer</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="No Footer"
|
||||||
|
showfooter={false}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal without Footer</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This modal has no footer. The content area extends to the bottom of the modal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullScreen: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Full Screen Modal',
|
||||||
|
children: (
|
||||||
|
<div className="h-96 flex flex-col">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Full Screen Modal</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
This modal takes up the full screen. Useful for complex workflows or
|
||||||
|
when you need maximum space.
|
||||||
|
</p>
|
||||||
|
<div className="flex-grow bg-muted rounded-md p-4">
|
||||||
|
<p>Content area that can expand to fill available space</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Full Screen Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
full={true}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Full Screen Modal
|
||||||
|
|
||||||
|
Shows a full screen modal that takes up the entire viewport. Useful for complex workflows or when maximum space is needed.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Full Screen Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Full Screen Modal"
|
||||||
|
full={true}
|
||||||
|
>
|
||||||
|
<div className="h-96 flex flex-col">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Full Screen Modal</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
This modal takes up the full screen. Useful for complex workflows or when you need maximum space.
|
||||||
|
</p>
|
||||||
|
<div className="flex-grow bg-muted rounded-md p-4">
|
||||||
|
<p>Content area that can expand to fill available space</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoadingState: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Loading State',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal with Loading State</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The OK button shows a loading spinner when confirmLoading is true.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setOpen(false);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Loading State Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
confirmLoading={loading}
|
||||||
|
onOk={handleOk}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Loading State
|
||||||
|
|
||||||
|
Shows a modal with the confirm button in a loading state. This is useful when performing async operations after clicking OK.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setOpen(false);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Loading State Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Loading State"
|
||||||
|
confirmLoading={loading}
|
||||||
|
onOk={handleOk}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Modal with Loading State</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The OK button shows a loading spinner when confirmLoading is true.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interactive example showing how to use the modal in a real component
|
||||||
|
|
||||||
|
export const Interactive: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
title: 'Interactive Modal',
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Interactive Modal</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Click OK to see the loading state, or click Cancel/Close to close the
|
||||||
|
modal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Interactive Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={args.title}
|
||||||
|
onOk={() => {
|
||||||
|
// Simulate API call
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{args.children}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: `
|
||||||
|
### Interactive Example
|
||||||
|
|
||||||
|
This is a fully interactive example showing how to use the modal in a real component with state management.
|
||||||
|
|
||||||
|
\`\`\`tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
|
|
||||||
|
function InteractiveModal() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)}>Open Interactive Modal</Button>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title="Interactive Modal"
|
||||||
|
onOk={() => {
|
||||||
|
// Simulate API call
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Interactive Modal</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Click OK to see the loading state, or click Cancel/Close to close the modal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user