mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 09:00:18 -06:00
chore: 742 storybook setup and cursor rule (#6220)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description:
|
||||
description: Migrate deprecated UI components to a unified component
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
177
.cursor/rules/storybook-create-new-story.mdc
Normal file
177
.cursor/rules/storybook-create-new-story.mdc
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
description: Create a story in Storybook for a given component
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Formbricks Storybook Stories
|
||||
|
||||
## When generating Storybook stories for Formbricks components:
|
||||
|
||||
### 1. **File Structure**
|
||||
- Create `stories.tsx` (not `.stories.tsx`) in component directory
|
||||
- Use exact import: `import { Meta, StoryObj } from "@storybook/react-vite";`
|
||||
- Import component from `"./index"`
|
||||
|
||||
### 2. **Story Structure Template**
|
||||
```tsx
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { ComponentName } from "./index";
|
||||
|
||||
// For complex components with configurable options
|
||||
// consider this as an example the options need to reflect the props types
|
||||
interface StoryOptions {
|
||||
showIcon: boolean;
|
||||
numberOfElements: number;
|
||||
customLabels: string[];
|
||||
}
|
||||
|
||||
type StoryProps = React.ComponentProps<typeof ComponentName> & StoryOptions;
|
||||
|
||||
const meta: Meta<StoryProps> = {
|
||||
title: "UI/ComponentName",
|
||||
component: ComponentName,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component: "The **ComponentName** component provides [description].",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
// Organize in exactly these categories: Behavior, Appearance, Content
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ComponentName> & { args: StoryOptions };
|
||||
```
|
||||
|
||||
### 3. **ArgTypes Organization**
|
||||
Organize ALL argTypes into exactly three categories:
|
||||
- **Behavior**: disabled, variant, onChange, etc.
|
||||
- **Appearance**: size, color, layout, styling, etc.
|
||||
- **Content**: text, icons, numberOfElements, etc.
|
||||
|
||||
Format:
|
||||
```tsx
|
||||
argTypes: {
|
||||
propName: {
|
||||
control: "select" | "boolean" | "text" | "number",
|
||||
options: ["option1", "option2"], // for select
|
||||
description: "Clear description",
|
||||
table: {
|
||||
category: "Behavior" | "Appearance" | "Content",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "default" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Required Stories**
|
||||
Every component must include:
|
||||
- `Default`: Most common use case
|
||||
- `Disabled`: If component supports disabled state
|
||||
- `WithIcon`: If component supports icons
|
||||
- Variant stories for each variant (Primary, Secondary, Error, etc.)
|
||||
- Edge case stories (ManyElements, LongText, CustomStyling)
|
||||
|
||||
### 5. **Story Format**
|
||||
```tsx
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Props with realistic values
|
||||
},
|
||||
};
|
||||
|
||||
export const EdgeCase: Story = {
|
||||
args: { /* ... */ },
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use this when [specific scenario].",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 6. **Dynamic Content Pattern**
|
||||
For components with dynamic content, create render function:
|
||||
```tsx
|
||||
const renderComponent = (args: StoryProps) => {
|
||||
const { numberOfElements, showIcon, customLabels } = args;
|
||||
|
||||
// Generate dynamic content
|
||||
const elements = Array.from({ length: numberOfElements }, (_, i) => ({
|
||||
id: `element-${i}`,
|
||||
label: customLabels[i] || `Element ${i + 1}`,
|
||||
icon: showIcon ? <IconComponent /> : undefined,
|
||||
}));
|
||||
|
||||
return <ComponentName {...args} elements={elements} />;
|
||||
};
|
||||
|
||||
export const Dynamic: Story = {
|
||||
render: renderComponent,
|
||||
args: {
|
||||
numberOfElements: 3,
|
||||
showIcon: true,
|
||||
customLabels: ["First", "Second", "Third"],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 7. **State Management**
|
||||
For interactive components:
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
|
||||
const ComponentWithState = (args: any) => {
|
||||
const [value, setValue] = useState(args.defaultValue);
|
||||
|
||||
return (
|
||||
<ComponentName
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue);
|
||||
args.onChange?.(newValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
render: ComponentWithState,
|
||||
args: { defaultValue: "initial" },
|
||||
};
|
||||
```
|
||||
|
||||
### 8. **Quality Requirements**
|
||||
- Include component description in parameters.docs
|
||||
- Add story documentation for non-obvious use cases
|
||||
- Test edge cases (overflow, empty states, many elements)
|
||||
- Ensure no TypeScript errors
|
||||
- Use realistic prop values
|
||||
- Include at least 3-5 story variants
|
||||
- Example values need to be in the context of survey application
|
||||
|
||||
### 9. **Naming Conventions**
|
||||
- **Story titles**: "UI/ComponentName"
|
||||
- **Story exports**: PascalCase (Default, WithIcon, ManyElements)
|
||||
- **Categories**: "Behavior", "Appearance", "Content" (exact spelling)
|
||||
- **Props**: camelCase matching component props
|
||||
|
||||
### 10. **Special Cases**
|
||||
- **Generic components**: Remove `component` from meta if type conflicts
|
||||
- **Form components**: Include Invalid, WithValue stories
|
||||
- **Navigation**: Include ManyItems stories
|
||||
- **Modals, Dropdowns and Popups **: Include trigger and content structure
|
||||
|
||||
## Generate stories that are comprehensive, well-documented, and reflect all component states and edge cases.
|
||||
@@ -19,9 +19,13 @@ const meta: Meta<StoryProps> = {
|
||||
component: Alert,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
controls: {
|
||||
sort: "requiredFirst",
|
||||
exclude: [],
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **Alert** component displays important messages to users with various styles and optional actions. It supports different variants for different message types and can include icons and buttons.",
|
||||
},
|
||||
},
|
||||
},
|
||||
// These argTypes are for story controls, not component props
|
||||
@@ -61,10 +65,10 @@ const meta: Meta<StoryProps> = {
|
||||
control: "boolean",
|
||||
description: "Whether to show action buttons",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
},
|
||||
order: 4,
|
||||
order: 1,
|
||||
},
|
||||
title: {
|
||||
control: "text",
|
||||
@@ -91,7 +95,7 @@ const meta: Meta<StoryProps> = {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 2,
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -160,7 +164,7 @@ export const Small: Story = {
|
||||
};
|
||||
|
||||
// With custom icon
|
||||
export const withButtonAndIcon: Story = {
|
||||
export const WithButtonAndIcon: Story = {
|
||||
render: renderAlert,
|
||||
args: {
|
||||
variant: "default",
|
||||
@@ -170,6 +174,13 @@ export const withButtonAndIcon: Story = {
|
||||
showButton: true,
|
||||
actionButtonText: "Learn more",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you need both visual emphasis and actionable content.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Error variant
|
||||
@@ -186,7 +197,7 @@ export const Destructive: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Only use if the user needs to take immediate action or there is a critical error.",
|
||||
story: "Use for critical errors that need immediate attention or action.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -206,7 +217,7 @@ export const Warning: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use this to make the user aware of potential issues.",
|
||||
story: "Use to make the user aware of potential issues or risks.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -226,7 +237,7 @@ export const Info: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use this to give contextual information and support the user.",
|
||||
story: "Use to give contextual information and support the user.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -246,7 +257,46 @@ export const Success: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use this to give positive feedback.",
|
||||
story: "Use to give positive feedback and confirm successful actions.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: renderAlert,
|
||||
args: {
|
||||
variant: "info",
|
||||
title: "Information",
|
||||
description: "This alert has an icon for better visual hierarchy.",
|
||||
showIcon: true,
|
||||
showButton: false,
|
||||
actionButtonText: "",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use icons to improve visual hierarchy and message clarity.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongContent: Story = {
|
||||
render: renderAlert,
|
||||
args: {
|
||||
variant: "warning",
|
||||
title: "Long Alert Title That Might Wrap to Multiple Lines",
|
||||
description:
|
||||
"This is a very long alert description that demonstrates how the alert component handles longer content. It should wrap gracefully and maintain proper spacing and readability even with extensive text content.",
|
||||
showIcon: true,
|
||||
showButton: true,
|
||||
actionButtonText: "Acknowledge",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows how the alert handles longer content with proper text wrapping.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Badge } from "./index";
|
||||
|
||||
const meta = {
|
||||
title: "ui/Badge",
|
||||
component: Badge,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["warning", "success", "error", "gray"],
|
||||
},
|
||||
size: { control: "select", options: ["small", "normal", "large"] },
|
||||
className: { control: "text" },
|
||||
},
|
||||
} satisfies Meta<typeof Badge>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
text: "Warning",
|
||||
type: "warning",
|
||||
size: "normal",
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
text: "Success",
|
||||
type: "success",
|
||||
size: "normal",
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
text: "Error",
|
||||
type: "error",
|
||||
size: "normal",
|
||||
},
|
||||
};
|
||||
|
||||
export const Gray: Story = {
|
||||
args: {
|
||||
text: "Gray",
|
||||
type: "gray",
|
||||
size: "normal",
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeWarning: Story = {
|
||||
args: {
|
||||
text: "Warning",
|
||||
type: "warning",
|
||||
size: "large",
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeSuccess: Story = {
|
||||
args: {
|
||||
text: "Success",
|
||||
type: "success",
|
||||
size: "large",
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeError: Story = {
|
||||
args: {
|
||||
text: "Error",
|
||||
type: "error",
|
||||
size: "large",
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeGray: Story = {
|
||||
args: {
|
||||
text: "Gray",
|
||||
type: "gray",
|
||||
size: "large",
|
||||
},
|
||||
};
|
||||
|
||||
export const TinyWarning: Story = {
|
||||
args: {
|
||||
text: "Warning",
|
||||
type: "warning",
|
||||
size: "tiny",
|
||||
},
|
||||
};
|
||||
|
||||
export const TinySuccess: Story = {
|
||||
args: {
|
||||
text: "Success",
|
||||
type: "success",
|
||||
size: "tiny",
|
||||
},
|
||||
};
|
||||
|
||||
export const TinyError: Story = {
|
||||
args: {
|
||||
text: "Error",
|
||||
type: "error",
|
||||
size: "tiny",
|
||||
},
|
||||
};
|
||||
|
||||
export const TinyGray: Story = {
|
||||
args: {
|
||||
text: "Gray",
|
||||
type: "gray",
|
||||
size: "tiny",
|
||||
},
|
||||
};
|
||||
217
apps/web/modules/ui/components/badge/stories.tsx
Normal file
217
apps/web/modules/ui/components/badge/stories.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Badge } from "./index";
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: "UI/Badge",
|
||||
component: Badge,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **Badge** component displays small status indicators or labels with different colors and sizes. Use it to highlight important information or show status states.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["warning", "success", "error", "gray"],
|
||||
description: "Color variant of the badge",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "gray" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["tiny", "normal", "large"],
|
||||
description: "Size of the badge",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "normal" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
role: {
|
||||
control: "text",
|
||||
description: "Accessibility role attribute",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
text: {
|
||||
control: "text",
|
||||
description: "Badge content text",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Badge>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
text: "Badge",
|
||||
type: "gray",
|
||||
size: "normal",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
text: "Warning",
|
||||
type: "warning",
|
||||
size: "normal",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for warnings or actions that need attention.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
text: "Success",
|
||||
type: "success",
|
||||
size: "normal",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use to indicate successful operations or positive states.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
text: "Error",
|
||||
type: "error",
|
||||
size: "normal",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for errors or failed operations.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Gray: Story = {
|
||||
args: {
|
||||
text: "Gray",
|
||||
type: "gray",
|
||||
size: "normal",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for neutral information or inactive states.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Tiny: Story = {
|
||||
args: {
|
||||
text: "Tiny",
|
||||
type: "gray",
|
||||
size: "tiny",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when space is very limited or for subtle indicators.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
text: "Large",
|
||||
type: "gray",
|
||||
size: "large",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for prominent badges or when more visibility is needed.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
text: "Very Long Badge Text",
|
||||
type: "warning",
|
||||
size: "normal",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Badge handles longer text content gracefully.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
args: {
|
||||
text: "Custom",
|
||||
type: "gray",
|
||||
size: "normal",
|
||||
className: "bg-purple-100 border-purple-200 text-purple-800",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can override the default styling with custom CSS classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithRole: Story = {
|
||||
args: {
|
||||
text: "Status",
|
||||
type: "success",
|
||||
size: "normal",
|
||||
role: "status",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use role attribute for better accessibility, especially for dynamic status updates.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { Button } from "./index";
|
||||
|
||||
const meta = {
|
||||
title: "ui/Button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["outline", "default", "secondary", "ghost", "destructive", "link"],
|
||||
},
|
||||
size: { control: "select", options: ["sm", "lg", "fab", "icon"] },
|
||||
},
|
||||
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
|
||||
args: { onClick: fn() },
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Minimal: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "ghost",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warn: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "destructive",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "default",
|
||||
loading: true,
|
||||
},
|
||||
};
|
||||
253
apps/web/modules/ui/components/button/stories.tsx
Normal file
253
apps/web/modules/ui/components/button/stories.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { Button } from "./index";
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "UI/Button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **Button** component provides clickable actions with multiple variants and sizes. It supports loading states, different visual styles, and can be used as a child component wrapper.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
loading: {
|
||||
control: "boolean",
|
||||
description: "Shows loading spinner and disables interaction",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Disables the button interaction",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
asChild: {
|
||||
control: "boolean",
|
||||
description: "Render as a child component using Slot",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
onClick: {
|
||||
action: "clicked",
|
||||
description: "Click handler function",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "function" },
|
||||
},
|
||||
order: 4,
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["default", "destructive", "outline", "secondary", "ghost", "link"],
|
||||
description: "Visual style variant of the button",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "default" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["default", "sm", "lg", "icon"],
|
||||
description: "Size of the button",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "default" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
children: {
|
||||
control: "text",
|
||||
description: "Button content",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "React.ReactNode" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
args: { onClick: fn() },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
children: "Delete",
|
||||
variant: "destructive",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for actions that are destructive or potentially harmful, like deleting data.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "outline",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for secondary actions or when you need a button with less visual weight.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "secondary",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for secondary actions that are less important than the primary action.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "ghost",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for subtle actions or when you need minimal visual impact.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Link: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "link",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you want button functionality but link appearance.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
children: "Small Button",
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: "Large Button",
|
||||
size: "lg",
|
||||
},
|
||||
};
|
||||
|
||||
export const Icon: Story = {
|
||||
args: {
|
||||
children: "×",
|
||||
size: "icon",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for icon-only buttons. The button will be square-shaped.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Loading...",
|
||||
variant: "default",
|
||||
loading: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use to show loading state during async operations. The button becomes disabled and shows a spinner.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: "Disabled Button",
|
||||
disabled: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when the button action is temporarily unavailable.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
args: {
|
||||
children: "Custom Button",
|
||||
className: "bg-purple-500 hover:bg-purple-600 text-white",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can override the default styling with custom CSS classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,68 +1,132 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { BellRing } from "lucide-react";
|
||||
import { Card } from "./index";
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Button } from "../button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./index";
|
||||
|
||||
const meta = {
|
||||
interface CardStoryProps {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
footerContent: string;
|
||||
showFooter: boolean;
|
||||
className?: string;
|
||||
footerButton?: boolean;
|
||||
}
|
||||
|
||||
const meta: Meta<CardStoryProps> = {
|
||||
title: "UI/Card",
|
||||
component: Card,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha" },
|
||||
docs: {
|
||||
description: {
|
||||
component: `The **card** component is used to display a card with a label, description, and optional icon. It can also display a status and buttons for connecting and viewing documentation.`,
|
||||
component:
|
||||
"The **Card** component is a flexible container for content with consistent styling. It includes subcomponents like CardHeader, CardTitle, CardDescription, CardContent, and CardFooter for structured layouts.",
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
icon: { control: "text" },
|
||||
},
|
||||
argTypes: {
|
||||
title: {
|
||||
control: "text",
|
||||
description: "Card title",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
description: {
|
||||
control: "text",
|
||||
description: "Card description",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
content: {
|
||||
control: "text",
|
||||
description: "Main content of the card",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
footerContent: {
|
||||
control: "text",
|
||||
description: "Content for the card footer",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
showFooter: {
|
||||
control: "boolean",
|
||||
description: "Toggle footer visibility",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
footerButton: {
|
||||
control: "boolean",
|
||||
description: "Show a button in the footer",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes",
|
||||
table: { category: "Appearance" },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Card>;
|
||||
render: ({ title, description, content, footerContent, showFooter, footerButton, className }) => (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{content}</p>
|
||||
</CardContent>
|
||||
{showFooter && (
|
||||
<CardFooter>{footerButton ? <Button>{footerContent}</Button> : <p>{footerContent}</p>}</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<CardStoryProps>;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Card Label",
|
||||
description: "This is the description of the card.",
|
||||
connectText: "Connect",
|
||||
connectHref: "#",
|
||||
connectNewTab: false,
|
||||
docsText: "Docs",
|
||||
docsHref: "#",
|
||||
docsNewTab: false,
|
||||
connected: true,
|
||||
statusText: "Connected",
|
||||
title: "Default Card",
|
||||
description: "This is the default card description.",
|
||||
content: "This is the main content area of the card. You can put any React node here.",
|
||||
footerContent: "Footer content",
|
||||
showFooter: true,
|
||||
footerButton: false,
|
||||
className: "w-96",
|
||||
},
|
||||
};
|
||||
|
||||
export const Disconnected: Story = {
|
||||
export const HeaderOnly: Story = {
|
||||
args: {
|
||||
label: "Card Label",
|
||||
description: "This is the description of the card.",
|
||||
connectText: "Connect",
|
||||
connectHref: "#",
|
||||
connectNewTab: false,
|
||||
docsText: "Docs",
|
||||
docsHref: "#",
|
||||
docsNewTab: false,
|
||||
connected: false,
|
||||
statusText: "Disconnected",
|
||||
...Default.args,
|
||||
title: "Header Only",
|
||||
description: "This card only has a header.",
|
||||
content: "",
|
||||
showFooter: false,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "A card that only displays a header. Useful for short announcements or titles.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
label: "Card Label",
|
||||
description: "This is the description of the card.",
|
||||
connectText: "Connect",
|
||||
connectHref: "#",
|
||||
connectNewTab: false,
|
||||
docsText: "Docs",
|
||||
docsHref: "#",
|
||||
docsNewTab: false,
|
||||
connected: true,
|
||||
statusText: "Connected",
|
||||
icon: <BellRing />,
|
||||
...Default.args,
|
||||
title: "Card with Long Content",
|
||||
description: "This card demonstrates how longer content is handled.",
|
||||
content:
|
||||
"This is a card with a longer content section to demonstrate how the card component handles extensive text content. The card will expand to accommodate the content while maintaining proper spacing and readability. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
footerContent: "Read More",
|
||||
showFooter: true,
|
||||
footerButton: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows how the card handles longer content with proper spacing and layout.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useArgs } from "storybook/preview-api";
|
||||
import { fn } from "storybook/test";
|
||||
import { ColorPicker } from "./index";
|
||||
|
||||
const meta: Meta<typeof ColorPicker> = {
|
||||
title: "ui/ColorPicker",
|
||||
title: "UI/ColorPicker",
|
||||
component: ColorPicker,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Button } from "../button";
|
||||
import {
|
||||
@@ -44,14 +44,17 @@ const DefaultBodyContent = (elementCount: number): React.ReactNode => {
|
||||
};
|
||||
|
||||
const meta: Meta<StoryProps> = {
|
||||
title: "UI/Modal",
|
||||
title: "UI/Dialog",
|
||||
component: DialogContent,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: {
|
||||
sort: "requiredFirst",
|
||||
exclude: [],
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **Dialog** component provides modal dialogs for displaying content over the main interface. It supports customizable headers, body content, footers, and various interaction patterns.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { FileIcon, FolderIcon, ImageIcon } from "lucide-react";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { InputCombobox } from "./index";
|
||||
|
||||
const meta = {
|
||||
title: "UI/InputCombobox",
|
||||
component: InputCombobox,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
The \`InputCombobox\` component is a versatile combination of an input field and a dropdown menu.
|
||||
It supports various features such as:
|
||||
- Searchable options
|
||||
- Grouped options
|
||||
- Multi-select
|
||||
- Icons for options
|
||||
- Clearable selection
|
||||
- Custom input props
|
||||
- Handling both dropdown and input modes
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof InputCombobox>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const commonOptions = [
|
||||
{ label: "File", value: "file", icon: FileIcon },
|
||||
{ label: "Folder", value: "folder", icon: FolderIcon },
|
||||
{ label: "Image", value: "image", icon: ImageIcon },
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
id: "input-combobox-default",
|
||||
showSearch: true,
|
||||
searchPlaceholder: "Search...",
|
||||
options: commonOptions,
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
clearable: true,
|
||||
withInput: false,
|
||||
allowMultiSelect: false,
|
||||
showCheckIcon: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInput: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
withInput: true,
|
||||
inputProps: {
|
||||
placeholder: "Type or select an option",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GroupedOptions: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
groupedOptions: [
|
||||
{
|
||||
label: "Common",
|
||||
value: "common",
|
||||
options: commonOptions,
|
||||
},
|
||||
{
|
||||
label: "Advanced",
|
||||
value: "advanced",
|
||||
options: [
|
||||
{ label: "Database", value: "database" },
|
||||
{ label: "Network", value: "network" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiSelect: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
allowMultiSelect: true,
|
||||
value: ["file", "image"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Clearable: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
value: "folder",
|
||||
clearable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutSearch: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
showSearch: false,
|
||||
},
|
||||
};
|
||||
319
apps/web/modules/ui/components/input-combo-box/stories.tsx
Normal file
319
apps/web/modules/ui/components/input-combo-box/stories.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { FileIcon, FolderIcon, ImageIcon } from "lucide-react";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { InputCombobox } from "./index";
|
||||
|
||||
interface StoryOptions {
|
||||
numberOfOptions: number;
|
||||
showCustomIcons: boolean;
|
||||
enableMultiSelect: boolean;
|
||||
enableClearable: boolean;
|
||||
}
|
||||
|
||||
type StoryProps = React.ComponentProps<typeof InputCombobox> & StoryOptions;
|
||||
|
||||
const meta: Meta<StoryProps> = {
|
||||
title: "UI/InputCombobox",
|
||||
component: InputCombobox,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **InputCombobox** component is a versatile combination of an input field and a dropdown menu. It supports searchable options, grouped options, multi-select, icons, clearable selection, and both dropdown and input modes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
allowMultiSelect: {
|
||||
control: "boolean",
|
||||
description: "Allow selecting multiple options",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
clearable: {
|
||||
control: "boolean",
|
||||
description: "Allow clearing the selection",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
showSearch: {
|
||||
control: "boolean",
|
||||
description: "Show search input in dropdown",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "true" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
withInput: {
|
||||
control: "boolean",
|
||||
description: "Include input field alongside dropdown",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 4,
|
||||
},
|
||||
showCheckIcon: {
|
||||
control: "boolean",
|
||||
description: "Show check icon for selected items",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 5,
|
||||
},
|
||||
onChangeValue: {
|
||||
action: "value changed",
|
||||
description: "Callback when value changes",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "function" },
|
||||
},
|
||||
order: 6,
|
||||
},
|
||||
comboboxClasses: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes for the combobox",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text for search input",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "Search..." },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
emptyDropdownText: {
|
||||
control: "text",
|
||||
description: "Text to show when no options found",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
id: {
|
||||
control: "text",
|
||||
description: "Unique identifier for the component",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InputCombobox>;
|
||||
|
||||
const commonOptions = [
|
||||
{ label: "File", value: "file", icon: FileIcon },
|
||||
{ label: "Folder", value: "folder", icon: FolderIcon },
|
||||
{ label: "Image", value: "image", icon: ImageIcon },
|
||||
];
|
||||
|
||||
const simpleOptions = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
id: "input-combobox-default",
|
||||
showSearch: true,
|
||||
searchPlaceholder: "Search...",
|
||||
options: commonOptions,
|
||||
value: commonOptions[0].value,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
clearable: false,
|
||||
withInput: false,
|
||||
allowMultiSelect: false,
|
||||
showCheckIcon: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInput: Story = {
|
||||
args: {
|
||||
id: "input-combobox-with-input",
|
||||
showSearch: true,
|
||||
options: commonOptions,
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
withInput: true,
|
||||
inputProps: {
|
||||
placeholder: "Type or select an option",
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you need both dropdown selection and free text input functionality.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiSelect: Story = {
|
||||
args: {
|
||||
id: "input-combobox-multi-select",
|
||||
options: commonOptions,
|
||||
value: ["file", "image"],
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
allowMultiSelect: true,
|
||||
showCheckIcon: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when users need to select multiple options from the dropdown.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Clearable: Story = {
|
||||
args: {
|
||||
id: "input-combobox-clearable",
|
||||
options: commonOptions,
|
||||
value: "folder",
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
clearable: true,
|
||||
showCheckIcon: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when users should be able to clear their selection.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GroupedOptions: Story = {
|
||||
args: {
|
||||
id: "input-combobox-grouped",
|
||||
groupedOptions: [
|
||||
{
|
||||
label: "Common",
|
||||
value: "common",
|
||||
options: commonOptions,
|
||||
},
|
||||
{
|
||||
label: "Advanced",
|
||||
value: "advanced",
|
||||
options: [
|
||||
{ label: "Database", value: "database" },
|
||||
{ label: "Network", value: "network" },
|
||||
{ label: "Security", value: "security" },
|
||||
],
|
||||
},
|
||||
],
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
showCheckIcon: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you need to organize options into logical groups.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutSearch: Story = {
|
||||
args: {
|
||||
id: "input-combobox-no-search",
|
||||
options: simpleOptions,
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
showSearch: false,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you have a small number of options and don't need search functionality.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyOptions: Story = {
|
||||
args: {
|
||||
id: "input-combobox-many-options",
|
||||
options: Array.from({ length: 50 }, (_, i) => ({
|
||||
label: `Option ${i + 1}`,
|
||||
value: `option${i + 1}`,
|
||||
})),
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
showSearch: true,
|
||||
searchPlaceholder: "Search from 50 options...",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you have many options and need search functionality for better UX.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
args: {
|
||||
id: "input-combobox-custom",
|
||||
options: commonOptions,
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
comboboxClasses: "border-blue-300 hover:border-blue-400",
|
||||
showCheckIcon: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can customize the appearance with custom CSS classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyState: Story = {
|
||||
args: {
|
||||
id: "input-combobox-empty",
|
||||
options: [],
|
||||
value: null,
|
||||
onChangeValue: (value, option) => logger.debug({ value, option }, "onChangeValue"),
|
||||
emptyDropdownText: "No options available",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows how the component handles empty options list.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,87 +1,277 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Input } from "./index";
|
||||
|
||||
const meta = {
|
||||
title: "ui/Input",
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "UI/Input",
|
||||
component: Input,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
The \`Input\` component is used to input the form fields.
|
||||
It supports all standard HTML input attributes, along with some additional props to handle specific use cases:
|
||||
- \`isInvalid\`: Adds a visual indicator for invalid input.
|
||||
- \`crossOrigin\`: Specifies how the element handles cross-origin requests.
|
||||
- \`dangerouslySetInnerHTML\`: Allows setting inner HTML content directly, similar to the native \`dangerouslySetInnerHTML\` in React.`,
|
||||
component:
|
||||
"The **Input** component is a versatile form input field that supports all standard HTML input attributes. It includes visual indicators for invalid states and supports various input types with consistent styling.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Input>;
|
||||
argTypes: {
|
||||
isInvalid: {
|
||||
control: "boolean",
|
||||
description: "Shows invalid state with red border",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Disables the input field",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Makes the input required",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
readOnly: {
|
||||
control: "boolean",
|
||||
description: "Makes the input read-only",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
order: 4,
|
||||
},
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["text", "email", "password", "number", "tel", "url", "search"],
|
||||
description: "Input type",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "text" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
description: "Input value",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 2,
|
||||
},
|
||||
defaultValue: {
|
||||
control: "text",
|
||||
description: "Default input value",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
type Story = StoryObj<typeof Input>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: "",
|
||||
className: "",
|
||||
isInvalid: false,
|
||||
disabled: false,
|
||||
placeholder: "Enter text",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue: Story = {
|
||||
args: {
|
||||
value: "Sample text",
|
||||
placeholder: "Enter text",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you need to display a pre-filled value.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Invalid: Story = {
|
||||
args: {
|
||||
isInvalid: true,
|
||||
disabled: false,
|
||||
placeholder: "Invalid input",
|
||||
value: "Invalid value",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use to show validation errors with red border styling.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
isInvalid: false,
|
||||
disabled: true,
|
||||
placeholder: "Disabled input",
|
||||
value: "Cannot edit this",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when the input should not be editable.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue: Story = {
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
value: "Prefilled text",
|
||||
isInvalid: false,
|
||||
disabled: false,
|
||||
placeholder: "Enter text",
|
||||
readOnly: true,
|
||||
value: "Read-only value",
|
||||
placeholder: "Read-only input",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use when you want to display data that cannot be edited but can be selected.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomClass: Story = {
|
||||
export const Required: Story = {
|
||||
args: {
|
||||
className: "rounded-lg bg-slate-50 text-base",
|
||||
isInvalid: false,
|
||||
disabled: false,
|
||||
placeholder: "Input with custom class",
|
||||
required: true,
|
||||
placeholder: "Required field *",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for mandatory form fields.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Password: Story = {
|
||||
args: {
|
||||
type: "password",
|
||||
value: "abcd",
|
||||
isInvalid: false,
|
||||
disabled: false,
|
||||
placeholder: "Enter password",
|
||||
value: "secretpassword",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for password input fields with hidden text.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Email: Story = {
|
||||
args: {
|
||||
type: "email",
|
||||
isInvalid: false,
|
||||
disabled: false,
|
||||
placeholder: "john.doe@email.com",
|
||||
placeholder: "john.doe@example.com",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for email input with built-in validation.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NumberInput: Story = {
|
||||
args: {
|
||||
type: "number",
|
||||
placeholder: "Enter number",
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for numeric input with optional min/max validation.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Search: Story = {
|
||||
args: {
|
||||
type: "search",
|
||||
placeholder: "Search...",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for search input fields with search-specific styling.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomStyling: Story = {
|
||||
args: {
|
||||
className: "rounded-lg bg-slate-50 text-base border-2 border-blue-300 focus:border-blue-500",
|
||||
placeholder: "Custom styled input",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can customize the appearance with additional CSS classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
placeholder:
|
||||
"This is a very long placeholder text that demonstrates how the input handles overflow content gracefully",
|
||||
value: "This is a very long input value that shows how the text scrolls within the input field",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows how the input handles long content with scrolling.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { BellRing } from "lucide-react";
|
||||
import { Card } from "./index";
|
||||
|
||||
const meta = {
|
||||
title: "ui/IntegrationCard",
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: "UI/IntegrationCard",
|
||||
component: Card,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
@@ -12,8 +12,60 @@ const meta = {
|
||||
component: `The **card** component is used to display a card with a label, description, and optional icon. It can also display a status and buttons for connecting and viewing documentation.`,
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
icon: { control: "text" },
|
||||
},
|
||||
argTypes: {
|
||||
// Behavior
|
||||
connectHref: {
|
||||
control: "text",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
connectNewTab: {
|
||||
control: "boolean",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
docsHref: {
|
||||
control: "text",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
docsNewTab: {
|
||||
control: "boolean",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
connected: {
|
||||
control: "boolean",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
table: { category: "Behavior" },
|
||||
},
|
||||
|
||||
// Content
|
||||
label: {
|
||||
control: "text",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
description: {
|
||||
control: "text",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
connectText: {
|
||||
control: "text",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
docsText: {
|
||||
control: "text",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
statusText: {
|
||||
control: "text",
|
||||
table: { category: "Content" },
|
||||
},
|
||||
|
||||
// Appearance
|
||||
icon: {
|
||||
control: false,
|
||||
table: { category: "Appearance" },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Card>;
|
||||
|
||||
@@ -1,29 +1,53 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Label } from "./index";
|
||||
|
||||
const meta = {
|
||||
title: "ui/Label",
|
||||
const meta: Meta<typeof Label> = {
|
||||
title: "UI/Label",
|
||||
component: Label,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
The **Label** component is used to label the form fields.
|
||||
`,
|
||||
component:
|
||||
"The **Label** component is used to label form fields and provide accessible names for interactive elements. It's built on Radix UI Label primitive and supports all standard label attributes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
className: { control: "text" },
|
||||
children: { control: "text" },
|
||||
htmlFor: {
|
||||
control: "text",
|
||||
description: "Associates the label with a form control",
|
||||
table: {
|
||||
category: "Behavior",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
children: {
|
||||
control: "text",
|
||||
description: "Label text content",
|
||||
table: {
|
||||
category: "Content",
|
||||
type: { summary: "React.ReactNode" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Label>;
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
type Story = StoryObj<typeof Label>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -31,9 +55,176 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomClass: Story = {
|
||||
args: {
|
||||
children: "Label with Custom Class",
|
||||
className: "text-red-500",
|
||||
export const WithInput: Story = {
|
||||
render: (args) => (
|
||||
<div className="space-y-2">
|
||||
<Label {...args} htmlFor="email">
|
||||
Email Address
|
||||
</Label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use with form inputs to provide accessible labels. The htmlFor attribute should match the input's id.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
render: (args) => (
|
||||
<div className="space-y-2">
|
||||
<Label {...args} htmlFor="required-field">
|
||||
Required Field <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<input
|
||||
id="required-field"
|
||||
type="text"
|
||||
placeholder="Required input"
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use to indicate required form fields with appropriate visual indicators.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCheckbox: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input id="terms" type="checkbox" className="h-4 w-4 rounded border-slate-300 text-blue-600" />
|
||||
<Label {...args} htmlFor="terms">
|
||||
I agree to the terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
),
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use with checkboxes and radio buttons for proper accessibility.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithRadio: Story = {
|
||||
render: (args) => (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input id="option1" type="radio" name="options" className="h-4 w-4 border-slate-300 text-blue-600" />
|
||||
<Label {...args} htmlFor="option1">
|
||||
Option 1
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input id="option2" type="radio" name="options" className="h-4 w-4 border-slate-300 text-blue-600" />
|
||||
<Label {...args} htmlFor="option2">
|
||||
Option 2
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use with radio button groups for proper accessibility and interaction.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyling: Story = {
|
||||
args: {
|
||||
children: "Custom Styled Label",
|
||||
className: "text-lg font-bold text-blue-600",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can customize the label appearance with additional CSS classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: (args) => (
|
||||
<div className="space-y-2">
|
||||
<Label {...args} htmlFor="disabled-input" className="opacity-50">
|
||||
Disabled Field
|
||||
</Label>
|
||||
<input
|
||||
id="disabled-input"
|
||||
type="text"
|
||||
placeholder="Disabled input"
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2 opacity-50"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use with disabled form controls to maintain visual consistency.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
children:
|
||||
"This is a very long label that demonstrates how labels handle extended text content gracefully",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows how the label handles longer text content with proper wrapping.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHelpText: Story = {
|
||||
render: (args) => (
|
||||
<div className="space-y-2">
|
||||
<Label {...args} htmlFor="password">
|
||||
Password
|
||||
</Label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2"
|
||||
/>
|
||||
<p className="text-sm text-slate-500">Password must be at least 8 characters long</p>
|
||||
</div>
|
||||
),
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use with additional help text to provide context and guidance.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { LoadingSpinner } from "./index";
|
||||
|
||||
const meta: Meta<typeof LoadingSpinner> = {
|
||||
title: "ui/LoadingSpinner",
|
||||
component: LoadingSpinner,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
115
apps/web/modules/ui/components/loading-spinner/stories.tsx
Normal file
115
apps/web/modules/ui/components/loading-spinner/stories.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { LoadingSpinner } from "./index";
|
||||
|
||||
const meta: Meta<typeof LoadingSpinner> = {
|
||||
title: "UI/LoadingSpinner",
|
||||
component: LoadingSpinner,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: { sort: "alpha", exclude: [] },
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The **LoadingSpinner** component displays an animated spinner to indicate loading states. It's centered within its container and can be customized with different sizes and colors.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
className: {
|
||||
control: "text",
|
||||
description: "Additional CSS classes for styling the spinner",
|
||||
table: {
|
||||
category: "Appearance",
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "h-6 w-6" },
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof LoadingSpinner>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
className: "h-4 w-4",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for small loading indicators or when space is limited.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
className: "h-10 w-10",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for prominent loading states or when more visibility is needed.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
className: "h-16 w-16",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Use for full-page loading states or very prominent loading indicators.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ColorVariants: Story = {
|
||||
args: {
|
||||
className: "h-8 w-8 text-blue-500",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can customize the color using text color classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SlowAnimation: Story = {
|
||||
args: {
|
||||
className: "h-8 w-8 animate-spin animate-duration-2000",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "You can customize the animation speed with Tailwind animation classes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomStyles: Story = {
|
||||
args: {
|
||||
className: "h-12 w-12 text-green-600 drop-shadow-lg",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Combine multiple utility classes for custom styling effects.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
const meta = {
|
||||
title: "ui/PageHeader",
|
||||
const meta: Meta<typeof PageHeader> = {
|
||||
title: "UI/PageHeader",
|
||||
component: PageHeader,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
|
||||
Reference in New Issue
Block a user