[WEB-5083] chore: add stories to propel #7888

This commit is contained in:
Aaron
2025-10-06 08:18:52 -07:00
committed by GitHub
parent 0cca31ef2e
commit cbcb026e9a
38 changed files with 6786 additions and 874 deletions

View File

@@ -11,7 +11,7 @@ function getAbsolutePath(value: string) {
}
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: ["@storybook/addon-designs", "@storybook/addon-docs"],
addons: [getAbsolutePath("@storybook/addon-designs"), getAbsolutePath("@storybook/addon-docs")],
framework: {
name: getAbsolutePath("@storybook/react-vite"),
options: {},

View File

@@ -55,8 +55,8 @@
"@plane/types": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"cmdk": "^1.1.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"frimousse": "^0.3.0",
"lucide-react": "catalog:",
"react": "catalog:",
@@ -71,12 +71,12 @@
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@storybook/addon-designs": "10.0.2",
"@storybook/addon-docs": "9.1.2",
"@storybook/react-vite": "9.1.2",
"@storybook/addon-docs": "9.1.10",
"@storybook/react-vite": "9.1.10",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"eslint-plugin-storybook": "9.1.2",
"storybook": "9.1.2",
"eslint-plugin-storybook": "9.1.10",
"storybook": "9.1.10",
"tsdown": "catalog:",
"typescript": "catalog:"
}

View File

@@ -0,0 +1,198 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Accordion } from "./accordion";
const meta = {
title: "Components/Accordion",
component: Accordion.Root,
parameters: {
layout: "centered",
controls: { disable: true },
},
tags: ["autodocs"],
subcomponents: {
Item: Accordion.Item,
Trigger: Accordion.Trigger,
Content: Accordion.Content,
},
args: {
children: null,
},
} satisfies Meta<typeof Accordion.Root>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render() {
return (
<Accordion.Root className="w-96">
<Accordion.Item value="item-1">
<Accordion.Trigger>What is Plane?</Accordion.Trigger>
<Accordion.Content>
Plane is an open-source project management tool designed for developers and teams to plan, track, and manage
their work efficiently.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>How do I get started?</Accordion.Trigger>
<Accordion.Content>
You can get started by signing up for an account, creating your first workspace, and inviting your team
members to collaborate.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Trigger>Is it free to use?</Accordion.Trigger>
<Accordion.Content>
Plane offers both free and paid plans. The free plan includes essential features for small teams, while paid
plans unlock advanced functionality.
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
},
};
export const SingleOpen: Story = {
render() {
return (
<Accordion.Root defaultValue={["item-1"]} className="w-96">
<Accordion.Item value="item-1">
<Accordion.Trigger>Section 1</Accordion.Trigger>
<Accordion.Content>Content for section 1. Only one section can be open at a time.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>Section 2</Accordion.Trigger>
<Accordion.Content>Content for section 2.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Trigger>Section 3</Accordion.Trigger>
<Accordion.Content>Content for section 3.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
},
};
export const AllowMultiple: Story = {
render() {
return (
<Accordion.Root allowMultiple defaultValue={["item-1", "item-2"]} className="w-96">
<Accordion.Item value="item-1">
<Accordion.Trigger>First Section</Accordion.Trigger>
<Accordion.Content>Multiple sections can be open at the same time.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>Second Section</Accordion.Trigger>
<Accordion.Content>This section is also open by default.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Trigger>Third Section</Accordion.Trigger>
<Accordion.Content>You can open this section while keeping the others open.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
},
};
export const WithDisabledItem: Story = {
render() {
return (
<Accordion.Root className="w-96">
<Accordion.Item value="item-1">
<Accordion.Trigger>Enabled Section</Accordion.Trigger>
<Accordion.Content>This section can be toggled.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2" disabled>
<Accordion.Trigger>Disabled Section</Accordion.Trigger>
<Accordion.Content>This content cannot be accessed.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Trigger>Another Enabled Section</Accordion.Trigger>
<Accordion.Content>This section can also be toggled.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
},
};
export const CustomIcon: Story = {
render() {
return (
<Accordion.Root className="w-96">
<Accordion.Item value="item-1">
<Accordion.Trigger
icon={
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-transform group-data-[panel-open]:rotate-180"
>
<path d="m6 9 6 6 6-6" />
</svg>
}
>
Custom Chevron Icon
</Accordion.Trigger>
<Accordion.Content>
This accordion uses a custom chevron icon instead of the default plus icon.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger
icon={
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-transform group-data-[panel-open]:rotate-180"
>
<path d="m6 9 6 6 6-6" />
</svg>
}
>
Another Section
</Accordion.Trigger>
<Accordion.Content>All items in this accordion use the custom icon.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
},
};
export const AsChildTrigger: Story = {
render() {
return (
<Accordion.Root className="w-96">
<Accordion.Item value="item-1">
<Accordion.Trigger asChild>
<button className="w-full rounded-md bg-blue-500 px-4 py-2 text-left text-white hover:bg-blue-600">
Custom Button Trigger
</button>
</Accordion.Trigger>
<Accordion.Content>
When using asChild, you can completely customize the trigger element without the default icon wrapper.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger asChild>
<button className="w-full rounded-md bg-green-500 px-4 py-2 text-left text-white hover:bg-green-600">
Another Custom Trigger
</button>
</Accordion.Trigger>
<Accordion.Content>This gives you full control over the trigger styling and behavior.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
},
};

View File

@@ -2,21 +2,21 @@ import * as React from "react";
import { Accordion as BaseAccordion } from "@base-ui-components/react";
import { PlusIcon } from "lucide-react";
interface AccordionRootProps {
export interface AccordionRootProps {
defaultValue?: string[];
allowMultiple?: boolean;
className?: string;
children: React.ReactNode;
}
interface AccordionItemProps {
export interface AccordionItemProps {
value: string;
disabled?: boolean;
className?: string;
children: React.ReactNode;
}
interface AccordionTriggerProps {
export interface AccordionTriggerProps {
className?: string;
icon?: React.ReactNode;
children: React.ReactNode;
@@ -24,7 +24,7 @@ interface AccordionTriggerProps {
iconClassName?: string;
}
interface AccordionContentProps {
export interface AccordionContentProps {
className?: string;
contentWrapperClassName?: string;
children: React.ReactNode;

View File

@@ -1,55 +1,333 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AnimatedCounter } from "./animated-counter";
const meta: Meta<typeof AnimatedCounter> = {
const meta = {
title: "Components/AnimatedCounter",
component: AnimatedCounter,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
size: {
control: { type: "select" },
options: ["sm", "md", "lg"],
},
args: {
size: "md",
count: 0,
},
};
} satisfies Meta<typeof AnimatedCounter>;
export default meta;
type Story = StoryObj<typeof AnimatedCounter>;
const AnimatedCounterDemo = (args: React.ComponentProps<typeof AnimatedCounter>) => {
const [count, setCount] = useState(args.count || 0);
return (
<div className="space-y-6 p-4">
<div className="flex items-center justify-center gap-6">
<button
className="px-4 py-2 bg-red-500 text-white font-medium rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2 transition-colors shadow-md"
onClick={() => setCount((prev) => Math.max(0, prev - 1))}
>
-1
</button>
<div className="flex items-center justify-center min-w-[60px] h-12 bg-gray-50 border border-gray-200 rounded-lg">
<AnimatedCounter {...args} count={count} />
</div>
<button
className="px-4 py-2 bg-green-500 text-white font-medium rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors shadow-md"
onClick={() => setCount((prev) => prev + 1)}
>
+1
</button>
</div>
</div>
);
};
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: (args) => <AnimatedCounterDemo {...args} />,
args: {
count: 5,
size: "md",
render(args) {
const [count, setCount] = useState(0);
return (
<div className="space-y-6 p-4">
<div className="flex items-center justify-center gap-6">
<button
className="px-4 py-2 bg-red-500 text-white font-medium rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2 transition-colors shadow-md"
onClick={() => setCount((prev) => Math.max(0, prev - 1))}
>
-1
</button>
<div className="flex items-center justify-center min-w-[60px] h-12 bg-gray-50 border border-gray-200 rounded-lg">
<AnimatedCounter {...args} count={count} />
</div>
<button
className="px-4 py-2 bg-green-500 text-white font-medium rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors shadow-md"
onClick={() => setCount((prev) => prev + 1)}
>
+1
</button>
</div>
</div>
);
},
};
export const Sizes: Story = {
render() {
const [count, setCount] = useState(42);
return (
<div className="space-y-6 p-4">
<div className="flex items-center gap-4">
<button
className="px-3 py-1 bg-custom-background-80 text-sm rounded hover:bg-custom-background-90"
onClick={() => setCount((prev) => Math.max(0, prev - 1))}
>
-1
</button>
<button
className="px-3 py-1 bg-custom-background-80 text-sm rounded hover:bg-custom-background-90"
onClick={() => setCount((prev) => prev + 1)}
>
+1
</button>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
<span className="text-sm text-custom-text-400 w-20">Small:</span>
<div className="flex items-center justify-center min-w-[40px] h-8 bg-custom-background-80 border border-custom-border-200 rounded">
<AnimatedCounter count={count} size="sm" />
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-custom-text-400 w-20">Medium:</span>
<div className="flex items-center justify-center min-w-[50px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
<AnimatedCounter count={count} size="md" />
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-custom-text-400 w-20">Large:</span>
<div className="flex items-center justify-center min-w-[60px] h-12 bg-custom-background-80 border border-custom-border-200 rounded">
<AnimatedCounter count={count} size="lg" />
</div>
</div>
</div>
</div>
);
},
};
export const LargeNumbers: Story = {
render() {
const [count, setCount] = useState(1234567);
return (
<div className="space-y-6 p-4">
<div className="flex items-center gap-4">
<button
className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600"
onClick={() => setCount((prev) => Math.max(0, prev - 1000))}
>
-1000
</button>
<button
className="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600"
onClick={() => setCount((prev) => prev + 1000)}
>
+1000
</button>
</div>
<div className="flex items-center justify-center min-w-[100px] h-12 bg-custom-background-80 border border-custom-border-200 rounded-lg">
<AnimatedCounter count={count} size="lg" />
</div>
</div>
);
},
};
export const Countdown: Story = {
render() {
const [count, setCount] = useState(10);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (isRunning && count > 0) {
const timer = setTimeout(() => setCount((prev) => prev - 1), 1000);
return () => clearTimeout(timer);
}
if (count === 0) {
setIsRunning(false);
}
}, [count, isRunning]);
const handleStart = () => {
setCount(10);
setIsRunning(true);
};
return (
<div className="space-y-6 p-4">
<div className="flex flex-col items-center gap-4">
<div className="flex items-center justify-center min-w-[60px] h-16 bg-custom-background-80 border-2 border-custom-border-200 rounded-lg">
<AnimatedCounter count={count} size="lg" className="text-2xl" />
</div>
<button
className="px-6 py-2 bg-custom-primary-100 text-white font-medium rounded-lg hover:bg-custom-primary-200"
onClick={handleStart}
disabled={isRunning}
>
{isRunning ? "Counting..." : "Start Countdown"}
</button>
</div>
</div>
);
},
};
export const LiveCounter: Story = {
render() {
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (isRunning) {
const timer = setInterval(() => setCount((prev) => prev + 1), 500);
return () => clearInterval(timer);
}
}, [isRunning]);
return (
<div className="space-y-6 p-4">
<div className="flex flex-col items-center gap-4">
<div className="flex items-center justify-center min-w-[80px] h-16 bg-custom-background-80 border-2 border-custom-border-200 rounded-lg">
<AnimatedCounter count={count} size="lg" className="text-2xl" />
</div>
<div className="flex gap-2">
<button
className="px-4 py-2 bg-green-500 text-white font-medium rounded hover:bg-green-600"
onClick={() => setIsRunning(true)}
disabled={isRunning}
>
Start
</button>
<button
className="px-4 py-2 bg-red-500 text-white font-medium rounded hover:bg-red-600"
onClick={() => setIsRunning(false)}
disabled={!isRunning}
>
Stop
</button>
<button
className="px-4 py-2 bg-gray-500 text-white font-medium rounded hover:bg-gray-600"
onClick={() => {
setIsRunning(false);
setCount(0);
}}
>
Reset
</button>
</div>
</div>
</div>
);
},
};
export const MultipleCounters: Story = {
render() {
const [likes, setLikes] = useState(42);
const [comments, setComments] = useState(15);
const [shares, setShares] = useState(8);
return (
<div className="space-y-6 p-4">
<div className="max-w-md border border-custom-border-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium">Engagement Stats</h3>
</div>
<div className="flex gap-4">
<div className="flex-1 flex flex-col items-center gap-2">
<div className="text-custom-text-400 text-sm">Likes</div>
<div className="flex items-center gap-2">
<button
className="w-8 h-8 flex items-center justify-center bg-custom-background-80 rounded hover:bg-custom-background-90"
onClick={() => setLikes((prev) => prev + 1)}
>
+
</button>
<div className="flex items-center justify-center min-w-[40px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
<AnimatedCounter count={likes} size="md" />
</div>
</div>
</div>
<div className="flex-1 flex flex-col items-center gap-2">
<div className="text-custom-text-400 text-sm">Comments</div>
<div className="flex items-center gap-2">
<button
className="w-8 h-8 flex items-center justify-center bg-custom-background-80 rounded hover:bg-custom-background-90"
onClick={() => setComments((prev) => prev + 1)}
>
+
</button>
<div className="flex items-center justify-center min-w-[40px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
<AnimatedCounter count={comments} size="md" />
</div>
</div>
</div>
<div className="flex-1 flex flex-col items-center gap-2">
<div className="text-custom-text-400 text-sm">Shares</div>
<div className="flex items-center gap-2">
<button
className="w-8 h-8 flex items-center justify-center bg-custom-background-80 rounded hover:bg-custom-background-90"
onClick={() => setShares((prev) => prev + 1)}
>
+
</button>
<div className="flex items-center justify-center min-w-[40px] h-10 bg-custom-background-80 border border-custom-border-200 rounded">
<AnimatedCounter count={shares} size="md" />
</div>
</div>
</div>
</div>
</div>
</div>
);
},
};
export const InBadge: Story = {
render() {
const [notifications, setNotifications] = useState(3);
return (
<div className="space-y-6 p-4">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded-lg">
Notifications
</button>
<div className="absolute -top-2 -right-2 min-w-[24px] h-6 flex items-center justify-center bg-red-500 text-white rounded-full px-1.5">
<AnimatedCounter count={notifications} size="sm" className="text-xs font-medium" />
</div>
</div>
<button
className="px-4 py-2 bg-custom-primary-100 text-white rounded hover:bg-custom-primary-200"
onClick={() => setNotifications((prev) => prev + 1)}
>
Add Notification
</button>
</div>
</div>
);
},
};
export const FastAnimation: Story = {
render() {
const [count, setCount] = useState(0);
const incrementFast = () => {
for (let i = 1; i <= 10; i++) {
setTimeout(() => setCount((prev) => prev + 1), i * 50);
}
};
return (
<div className="space-y-6 p-4">
<div className="flex flex-col items-center gap-4">
<div className="flex items-center justify-center min-w-[60px] h-12 bg-custom-background-80 border border-custom-border-200 rounded-lg">
<AnimatedCounter count={count} size="lg" />
</div>
<div className="flex gap-2">
<button
className="px-4 py-2 bg-custom-primary-100 text-white rounded hover:bg-custom-primary-200"
onClick={incrementFast}
>
+10 Fast
</button>
<button
className="px-4 py-2 bg-custom-background-80 rounded hover:bg-custom-background-90"
onClick={() => setCount(0)}
>
Reset
</button>
</div>
</div>
</div>
);
},
};

View File

@@ -0,0 +1,171 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Avatar } from "./avatar";
const meta = {
title: "Components/Avatar",
component: Avatar,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
name: "John Doe",
src: "https://i.pravatar.cc/150?img=1",
},
} satisfies Meta<typeof Avatar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WithName: Story = {
args: {
name: "Jane Smith",
src: "https://i.pravatar.cc/150?img=5",
},
};
export const Fallback: Story = {
args: {
name: "Alice Johnson",
src: "invalid-url",
},
};
export const FallbackWithCustomColor: Story = {
args: {
name: "Bob Wilson",
src: "invalid-url",
fallbackBackgroundColor: "#3b82f6",
fallbackTextColor: "#ffffff",
},
};
export const FallbackWithCustomText: Story = {
args: {
fallbackText: "AB",
src: "invalid-url",
fallbackBackgroundColor: "#10b981",
fallbackTextColor: "#ffffff",
},
};
export const Small: Story = {
args: {
name: "Small Avatar",
src: "https://i.pravatar.cc/150?img=2",
size: "sm",
},
};
export const Medium: Story = {
args: {
name: "Medium Avatar",
src: "https://i.pravatar.cc/150?img=3",
size: "md",
},
};
export const Base: Story = {
args: {
name: "Base Avatar",
src: "https://i.pravatar.cc/150?img=4",
size: "base",
},
};
export const Large: Story = {
args: {
name: "Large Avatar",
src: "https://i.pravatar.cc/150?img=6",
size: "lg",
},
};
export const CircleShape: Story = {
args: {
name: "Circle Avatar",
src: "https://i.pravatar.cc/150?img=7",
shape: "circle",
},
};
export const SquareShape: Story = {
args: {
name: "Square Avatar",
src: "https://i.pravatar.cc/150?img=8",
shape: "square",
},
};
export const AllSizes: Story = {
parameters: {
controls: { disable: true },
},
render() {
return (
<div className="flex items-center gap-4">
<Avatar name="Small" src="https://i.pravatar.cc/150?img=10" size="sm" />
<Avatar name="Medium" src="https://i.pravatar.cc/150?img=11" size="md" />
<Avatar name="Base" src="https://i.pravatar.cc/150?img=12" size="base" />
<Avatar name="Large" src="https://i.pravatar.cc/150?img=13" size="lg" />
</div>
);
},
};
export const AllShapes: Story = {
parameters: {
controls: { disable: true },
},
render() {
return (
<div className="flex items-center gap-4">
<Avatar name="Circle" src="https://i.pravatar.cc/150?img=14" shape="circle" />
<Avatar name="Square" src="https://i.pravatar.cc/150?img=15" shape="square" />
</div>
);
},
};
export const FallbackVariations: Story = {
parameters: {
controls: { disable: true },
},
render() {
return (
<div className="flex items-center gap-4">
<Avatar name="Alice" src="invalid-url" fallbackBackgroundColor="#ef4444" fallbackTextColor="#ffffff" />
<Avatar name="Bob" src="invalid-url" fallbackBackgroundColor="#f59e0b" fallbackTextColor="#ffffff" />
<Avatar name="Charlie" src="invalid-url" fallbackBackgroundColor="#10b981" fallbackTextColor="#ffffff" />
<Avatar name="David" src="invalid-url" fallbackBackgroundColor="#3b82f6" fallbackTextColor="#ffffff" />
<Avatar name="Eve" src="invalid-url" fallbackBackgroundColor="#8b5cf6" fallbackTextColor="#ffffff" />
</div>
);
},
};
export const AvatarGroup: Story = {
parameters: {
controls: { disable: true },
},
render() {
return (
<div className="flex -space-x-2">
<Avatar name="User 1" src="https://i.pravatar.cc/150?img=20" size="md" className="ring-2 ring-white" />
<Avatar name="User 2" src="https://i.pravatar.cc/150?img=21" size="md" className="ring-2 ring-white" />
<Avatar name="User 3" src="https://i.pravatar.cc/150?img=22" size="md" className="ring-2 ring-white" />
<Avatar name="User 4" src="https://i.pravatar.cc/150?img=23" size="md" className="ring-2 ring-white" />
<Avatar
fallbackText="+5"
src="invalid-url"
size="md"
fallbackBackgroundColor="#6b7280"
fallbackTextColor="#ffffff"
className="ring-2 ring-white"
/>
</div>
);
},
};

View File

@@ -1,56 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./button";
import type { TButtonVariant, TButtonSizes } from "./helper";
const buttonVariants: TButtonVariant[] = [
"primary",
"accent-primary",
"outline-primary",
"neutral-primary",
"link-primary",
"danger",
"accent-danger",
"outline-danger",
"link-danger",
"tertiary-danger",
"link-neutral",
];
const buttonSizes: TButtonSizes[] = ["sm", "md", "lg", "xl"];
const meta: Meta<typeof Button> = {
const meta = {
title: "Components/Button",
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: buttonVariants,
},
size: {
control: "select",
options: buttonSizes,
},
loading: {
control: "boolean",
},
disabled: {
control: "boolean",
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
args: {
children: "Button",
},
};
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Primary: Story = {
args: {
@@ -212,62 +178,68 @@ export const WithAppendIcon: Story = {
};
export const AllVariants: Story = {
render: () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Primary Variants</h3>
<div className="flex flex-wrap gap-2">
<Button variant="primary">Primary</Button>
<Button variant="accent-primary">Accent Primary</Button>
<Button variant="outline-primary">Outline Primary</Button>
<Button variant="neutral-primary">Neutral Primary</Button>
<Button variant="link-primary">Link Primary</Button>
render() {
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Primary Variants</h3>
<div className="flex flex-wrap gap-2">
<Button variant="primary">Primary</Button>
<Button variant="accent-primary">Accent Primary</Button>
<Button variant="outline-primary">Outline Primary</Button>
<Button variant="neutral-primary">Neutral Primary</Button>
<Button variant="link-primary">Link Primary</Button>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Danger Variants</h3>
<div className="flex flex-wrap gap-2">
<Button variant="danger">Danger</Button>
<Button variant="accent-danger">Accent Danger</Button>
<Button variant="outline-danger">Outline Danger</Button>
<Button variant="link-danger">Link Danger</Button>
<Button variant="tertiary-danger">Tertiary Danger</Button>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Other Variants</h3>
<div className="flex flex-wrap gap-2">
<Button variant="link-neutral">Link Neutral</Button>
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Danger Variants</h3>
<div className="flex flex-wrap gap-2">
<Button variant="danger">Danger</Button>
<Button variant="accent-danger">Accent Danger</Button>
<Button variant="outline-danger">Outline Danger</Button>
<Button variant="link-danger">Link Danger</Button>
<Button variant="tertiary-danger">Tertiary Danger</Button>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Other Variants</h3>
<div className="flex flex-wrap gap-2">
<Button variant="link-neutral">Link Neutral</Button>
</div>
</div>
</div>
),
);
},
};
export const AllSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
render() {
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
</div>
</div>
</div>
),
);
},
};
export const AllStates: Story = {
render: () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Button States</h3>
<div className="flex flex-wrap gap-2">
<Button>Default</Button>
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
render() {
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Button States</h3>
<div className="flex flex-wrap gap-2">
<Button>Default</Button>
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
</div>
</div>
</div>
</div>
),
);
},
};

View File

@@ -1,10 +1,9 @@
import { ComponentProps, useState } from "react";
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { DateRange } from "react-day-picker";
import { Calendar } from "./root";
type CalendarProps = ComponentProps<typeof Calendar>;
const meta: Meta<CalendarProps> = {
const meta = {
title: "Components/Calendar",
component: Calendar,
parameters: {
@@ -13,14 +12,13 @@ const meta: Meta<CalendarProps> = {
args: {
showOutsideDays: true,
},
};
} satisfies Meta<typeof Calendar>;
export default meta;
type Story = StoryObj<CalendarProps>;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
render: (args: CalendarProps) => {
export const SingleDate: Story = {
render(args) {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
@@ -30,3 +28,167 @@ export const Default: Story = {
);
},
};
export const MultipleDates: Story = {
render(args) {
const [dates, setDates] = useState<Date[] | undefined>([
new Date(2024, 0, 15),
new Date(2024, 0, 20),
new Date(2024, 0, 25),
]);
return (
<div className="p-4">
<Calendar {...args} mode="multiple" selected={dates} onSelect={setDates} className="rounded-md border" />
</div>
);
},
};
export const RangeSelection: Story = {
render(args) {
const [range, setRange] = useState<DateRange | undefined>({
from: new Date(2024, 0, 10),
to: new Date(2024, 0, 20),
});
return (
<div className="p-4">
<Calendar {...args} mode="range" selected={range} onSelect={setRange} className="rounded-md border" />
</div>
);
},
};
export const DisabledDates: Story = {
render(args) {
const [date, setDate] = useState<Date | undefined>();
const disabledDays = [new Date(2024, 0, 5), new Date(2024, 0, 12), new Date(2024, 0, 19), new Date(2024, 0, 26)];
return (
<div className="p-4">
<Calendar
{...args}
mode="single"
selected={date}
onSelect={setDate}
disabled={disabledDays}
className="rounded-md border"
/>
</div>
);
},
};
export const DisabledWeekends: Story = {
render(args) {
const [date, setDate] = useState<Date | undefined>();
return (
<div className="p-4">
<Calendar
{...args}
mode="single"
selected={date}
onSelect={setDate}
disabled={(date) => date.getDay() === 0 || date.getDay() === 6}
className="rounded-md border"
/>
</div>
);
},
};
export const MinMaxDates: Story = {
render(args) {
const [date, setDate] = useState<Date | undefined>();
const today = new Date();
const tenDaysAgo = new Date(today);
tenDaysAgo.setDate(today.getDate() - 10);
const tenDaysFromNow = new Date(today);
tenDaysFromNow.setDate(today.getDate() + 10);
return (
<div className="p-4">
<Calendar
{...args}
mode="single"
selected={date}
onSelect={setDate}
disabled={(date) => date < tenDaysAgo || date > tenDaysFromNow}
className="rounded-md border"
/>
</div>
);
},
};
export const WeekStartsOnMonday: Story = {
render(args) {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<div className="p-4">
<Calendar
{...args}
mode="single"
selected={date}
onSelect={setDate}
weekStartsOn={1}
className="rounded-md border"
/>
</div>
);
},
};
export const WithoutOutsideDays: Story = {
render(args) {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<div className="p-4">
<Calendar
{...args}
mode="single"
selected={date}
onSelect={setDate}
showOutsideDays={false}
className="rounded-md border"
/>
</div>
);
},
};
export const TwoMonths: Story = {
render(args) {
const [range, setRange] = useState<DateRange | undefined>({
from: new Date(2024, 0, 10),
to: new Date(2024, 1, 15),
});
return (
<div className="p-4">
<Calendar
{...args}
mode="range"
selected={range}
onSelect={setRange}
numberOfMonths={2}
className="rounded-md border"
/>
</div>
);
},
};
export const Uncontrolled: Story = {
render(args) {
return (
<div className="p-4">
<Calendar {...args} mode="single" defaultMonth={new Date(2024, 0)} className="rounded-md border" />
</div>
);
},
};

View File

@@ -0,0 +1,211 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Card, ECardVariant, ECardSpacing, ECardDirection } from "./card";
const meta = {
title: "Components/Card",
component: Card,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
children: (
<>
<h3 className="text-lg font-semibold">Card Title</h3>
<p className="text-sm text-gray-600">This is a default card with shadow and large spacing.</p>
</>
),
},
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WithShadow: Story = {
args: {
variant: ECardVariant.WITH_SHADOW,
children: (
<>
<h3 className="text-lg font-semibold">Card with Shadow</h3>
<p className="text-sm text-gray-600">Hover over this card to see the shadow effect.</p>
</>
),
},
};
export const WithoutShadow: Story = {
args: {
variant: ECardVariant.WITHOUT_SHADOW,
children: (
<>
<h3 className="text-lg font-semibold">Card without Shadow</h3>
<p className="text-sm text-gray-600">This card has no shadow effect on hover.</p>
</>
),
},
};
export const SmallSpacing: Story = {
args: {
spacing: ECardSpacing.SM,
children: (
<>
<h3 className="text-lg font-semibold">Small Spacing</h3>
<p className="text-sm text-gray-600">This card uses small spacing (p-4).</p>
</>
),
},
};
export const LargeSpacing: Story = {
args: {
spacing: ECardSpacing.LG,
children: (
<>
<h3 className="text-lg font-semibold">Large Spacing</h3>
<p className="text-sm text-gray-600">This card uses large spacing (p-6).</p>
</>
),
},
};
export const ColumnDirection: Story = {
args: {
direction: ECardDirection.COLUMN,
children: (
<>
<h3 className="text-lg font-semibold">Column Direction</h3>
<p className="text-sm text-gray-600">Content is arranged vertically.</p>
<button className="rounded bg-blue-500 px-4 py-2 text-white">Action</button>
</>
),
},
};
export const RowDirection: Story = {
args: {
direction: ECardDirection.ROW,
children: (
<>
<div className="flex-shrink-0">
<div className="h-12 w-12 rounded bg-blue-500" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold">Row Direction</h3>
<p className="text-sm text-gray-600">Content is arranged horizontally.</p>
</div>
</>
),
},
};
export const ProductCard: Story = {
args: {
variant: ECardVariant.WITH_SHADOW,
spacing: ECardSpacing.LG,
direction: ECardDirection.COLUMN,
children: (
<>
<div className="h-48 w-full rounded bg-gray-200" />
<h3 className="text-xl font-bold">Product Name</h3>
<p className="text-sm text-gray-600">A brief description of the product goes here.</p>
<div className="flex items-center justify-between">
<span className="text-lg font-semibold">$99.99</span>
<button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">Add to Cart</button>
</div>
</>
),
},
};
export const UserCard: Story = {
args: {
variant: ECardVariant.WITH_SHADOW,
spacing: ECardSpacing.LG,
direction: ECardDirection.ROW,
children: (
<>
<div className="h-16 w-16 flex-shrink-0 rounded-full bg-blue-500" />
<div className="flex-1">
<h3 className="text-lg font-semibold">John Doe</h3>
<p className="text-sm text-gray-600">Software Engineer</p>
<p className="text-xs text-gray-500">john.doe@example.com</p>
</div>
</>
),
},
};
export const NotificationCard: Story = {
args: {
variant: ECardVariant.WITHOUT_SHADOW,
spacing: ECardSpacing.SM,
direction: ECardDirection.COLUMN,
children: (
<>
<div className="flex items-start justify-between">
<h4 className="font-semibold">New Message</h4>
<span className="text-xs text-gray-500">2m ago</span>
</div>
<p className="text-sm text-gray-600">You have received a new message from Alice.</p>
</>
),
},
};
export const AllVariants: Story = {
render() {
return (
<div className="space-y-4">
<Card variant={ECardVariant.WITH_SHADOW}>
<h3 className="font-semibold">With Shadow</h3>
<p className="text-sm text-gray-600">Hover to see the shadow effect</p>
</Card>
<Card variant={ECardVariant.WITHOUT_SHADOW}>
<h3 className="font-semibold">Without Shadow</h3>
<p className="text-sm text-gray-600">No shadow on hover</p>
</Card>
</div>
);
},
};
export const AllSpacings: Story = {
render() {
return (
<div className="space-y-4">
<Card spacing={ECardSpacing.SM}>
<h3 className="font-semibold">Small Spacing (p-4)</h3>
<p className="text-sm text-gray-600">Compact padding</p>
</Card>
<Card spacing={ECardSpacing.LG}>
<h3 className="font-semibold">Large Spacing (p-6)</h3>
<p className="text-sm text-gray-600">More generous padding</p>
</Card>
</div>
);
},
};
export const AllDirections: Story = {
render() {
return (
<div className="space-y-4">
<Card direction={ECardDirection.COLUMN}>
<h3 className="font-semibold">Column Direction</h3>
<p className="text-sm text-gray-600">Vertical layout</p>
<button className="w-fit rounded bg-blue-500 px-4 py-2 text-white">Button</button>
</Card>
<Card direction={ECardDirection.ROW}>
<div className="h-12 w-12 flex-shrink-0 rounded bg-blue-500" />
<div>
<h3 className="font-semibold">Row Direction</h3>
<p className="text-sm text-gray-600">Horizontal layout</p>
</div>
</Card>
</div>
);
},
};

View File

@@ -0,0 +1,174 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ChevronDown } from "lucide-react";
import { useArgs } from "storybook/preview-api";
import { Collapsible } from "./collapsible";
const meta = {
title: "Components/Collapsible",
component: Collapsible.CollapsibleRoot,
subcomponents: {
CollapsibleTrigger: Collapsible.CollapsibleTrigger,
CollapsibleContent: Collapsible.CollapsibleContent,
},
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
children: null,
isOpen: false,
onToggle: () => {},
},
render(args) {
const [{ isOpen }, updateArgs] = useArgs();
const toggleOpen = () => updateArgs({ isOpen: !isOpen });
return (
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={toggleOpen} className="w-96">
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
<span className="font-semibold">Click to toggle</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="rounded-md border border-gray-200 p-4">
<p className="text-sm">This is the collapsible content that can be shown or hidden.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
);
},
} satisfies Meta<typeof Collapsible.CollapsibleRoot>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const DefaultOpen: Story = {
args: { isOpen: true },
};
export const Controlled: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="space-y-4">
<div className="flex gap-2">
<button onClick={() => setIsOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-sm text-white">
Open
</button>
<button onClick={() => setIsOpen(false)} className="rounded bg-gray-500 px-4 py-2 text-sm text-white">
Close
</button>
<button onClick={() => setIsOpen(!isOpen)} className="rounded bg-green-500 px-4 py-2 text-sm text-white">
Toggle
</button>
</div>
<Collapsible.CollapsibleRoot isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
<span className="font-semibold">Controlled Collapsible</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="rounded-md border border-gray-200 p-4">
<p className="text-sm">This collapsible is controlled by external state.</p>
<p className="mt-2 text-sm">Current state: {isOpen ? "Open" : "Closed"}</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
</div>
);
},
};
export const NestedContent: Story = {
render(args) {
const [isOpen, setIsOpen] = useState(args.isOpen);
return (
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
<span className="font-semibold">Collapsible with Nested Content</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="space-y-2 rounded-md border border-gray-200 p-4">
<h4 className="font-semibold">Section 1</h4>
<p className="text-sm">This is some content in the first section.</p>
<h4 className="font-semibold">Section 2</h4>
<p className="text-sm">This is some content in the second section.</p>
<ul className="list-inside list-disc text-sm">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
);
},
};
export const CustomStyling: Story = {
render(args) {
const [isOpen, setIsOpen] = useState(args.isOpen);
return (
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-gradient-to-r from-purple-500 to-pink-500 px-6 py-3 text-white shadow-lg transition-all hover:shadow-xl">
<span className="text-lg font-bold">Custom Styled Trigger</span>
<ChevronDown className="h-5 w-5 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-4">
<div className="rounded-lg bg-gradient-to-br from-purple-100 to-pink-100 p-6 shadow-md">
<p className="text-purple-900">This collapsible has custom styling with gradients, shadows, and colors.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
);
},
};
export const MultipleCollapsibles: Story = {
render() {
return (
<div className="w-96 space-y-2">
<Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
<span className="font-semibold">First Item</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="rounded-md border border-gray-200 p-4">
<p className="text-sm">Content for the first item.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
<span className="font-semibold">Second Item</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="rounded-md border border-gray-200 p-4">
<p className="text-sm">Content for the second item.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleTrigger className="flex w-full items-center justify-between rounded-md bg-gray-100 px-4 py-2 hover:bg-gray-200">
<span className="font-semibold">Third Item</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="rounded-md border border-gray-200 p-4">
<p className="text-sm">Content for the third item.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
</div>
);
},
};

View File

@@ -0,0 +1,260 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Check, ChevronsUpDown } from "lucide-react";
import { useArgs } from "storybook/preview-api";
import { Combobox } from "./combobox";
const frameworks = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
{ value: "svelte", label: "Svelte" },
{ value: "solid", label: "Solid" },
{ value: "next", label: "Next.js" },
{ value: "nuxt", label: "Nuxt" },
{ value: "remix", label: "Remix" },
];
const meta = {
title: "Components/Combobox",
component: Combobox,
subcomponents: {
ComboboxButton: Combobox.Button,
ComboboxOptions: Combobox.Options,
ComboboxOption: Combobox.Option,
},
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
children: null,
value: "",
onValueChange: () => {},
},
render(args) {
const [{ value }, updateArgs] = useArgs();
const setValue = (newValue: string | string[]) => updateArgs({ value: newValue });
return (
<Combobox {...args} value={value} onValueChange={(v) => setValue(v as string)}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value === framework.value && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
} satisfies Meta<typeof Combobox>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WithoutSearch: Story = {
render() {
const [value, setValue] = useState("");
return (
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value === framework.value && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
};
export const MultiSelect: Story = {
render() {
const [value, setValue] = useState<string[]>([]);
return (
<Combobox multiSelect value={value} onValueChange={(v) => setValue(v as string[])}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span className="truncate">{value.length > 0 ? `${value.length} selected` : "Select frameworks..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value.includes(framework.value) && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
};
export const MultiSelectWithLimit: Story = {
render() {
const [value, setValue] = useState<string[]>([]);
return (
<div className="space-y-2">
<Combobox multiSelect maxSelections={3} value={value} onValueChange={(v) => setValue(v as string[])}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span className="truncate">
{value.length > 0 ? `${value.length}/3 selected` : "Select up to 3 frameworks..."}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value.includes(framework.value) && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
<p className="text-xs text-gray-500">Maximum 3 selections allowed</p>
</div>
);
},
};
export const Disabled: Story = {
args: { disabled: true },
render() {
const [value, setValue] = useState("");
return (
<Combobox disabled value={value} onValueChange={(v) => setValue(v as string)}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-gray-100 px-4 py-2 opacity-50">
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value === framework.value && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
};
export const DisabledOptions: Story = {
render() {
const [value, setValue] = useState("");
return (
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options showSearch searchPlaceholder="Search framework..." className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
disabled={framework.value === "angular" || framework.value === "svelte"}
className="flex items-center gap-2 px-4 py-2"
>
{value === framework.value && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
};
export const CustomMaxHeight: Story = {
render() {
const [value, setValue] = useState("");
return (
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options showSearch searchPlaceholder="Search framework..." maxHeight="sm" className="w-72">
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value === framework.value && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
};
export const CustomEmptyMessage: Story = {
render() {
const [value, setValue] = useState("");
return (
<Combobox value={value} onValueChange={(v) => setValue(v as string)}>
<Combobox.Button className="flex w-72 items-center justify-between rounded-md border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<span>{value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Combobox.Button>
<Combobox.Options
showSearch
searchPlaceholder="Search framework..."
emptyMessage="No frameworks found. Try a different search."
className="w-72"
>
{frameworks.map((framework) => (
<Combobox.Option
key={framework.value}
value={framework.value}
className="flex items-center gap-2 px-4 py-2"
>
{value === framework.value && <Check className="h-4 w-4" />}
<span>{framework.label}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
},
};

View File

@@ -0,0 +1,203 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { File, Folder, Settings, User } from "lucide-react";
import { Command } from "./command";
const meta = {
title: "Components/Command",
component: Command,
subcomponents: {
CommandInput: Command.Input,
CommandList: Command.List,
CommandItem: Command.Item,
CommandEmpty: Command.Empty,
},
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof Command>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.Input placeholder="Search..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
<Command.List className="max-h-80 overflow-auto py-2">
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Item 1</Command.Item>
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Item 2</Command.Item>
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Item 3</Command.Item>
</Command.List>
<Command.Empty className="py-6 text-center text-sm text-gray-500">No results found.</Command.Empty>
</Command>
);
},
};
export const WithIcons: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.Input
placeholder="Search files and folders..."
className="h-9 w-full bg-transparent py-3 text-sm outline-none"
/>
<Command.List className="max-h-80 overflow-auto py-2">
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<Folder className="h-4 w-4" />
<span>Documents</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<Folder className="h-4 w-4" />
<span>Downloads</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<File className="h-4 w-4" />
<span>README.md</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<File className="h-4 w-4" />
<span>package.json</span>
</Command.Item>
</Command.List>
<Command.Empty className="py-6 text-center text-sm text-gray-500">No files or folders found.</Command.Empty>
</Command>
);
},
};
export const WithCategories: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.Input
placeholder="Search commands..."
className="h-9 w-full bg-transparent py-3 text-sm outline-none"
/>
<Command.List className="max-h-80 overflow-auto py-2">
<div className="px-2 py-1.5 text-xs font-semibold text-gray-500">User</div>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<User className="h-4 w-4" />
<span>Profile</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<Settings className="h-4 w-4" />
<span>Settings</span>
</Command.Item>
<div className="mt-2 px-2 py-1.5 text-xs font-semibold text-gray-500">Files</div>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<Folder className="h-4 w-4" />
<span>Open Folder</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<File className="h-4 w-4" />
<span>New File</span>
</Command.Item>
</Command.List>
<Command.Empty className="py-6 text-center text-sm text-gray-500">No commands found.</Command.Empty>
</Command>
);
},
};
export const EmptyState: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.Input placeholder="Search..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
<Command.List className="max-h-80 overflow-auto py-2">{/* No items - will show empty state */}</Command.List>
<Command.Empty className="py-6 text-center text-sm text-gray-500">
<p className="font-semibold">No results found</p>
<p className="mt-1 text-xs">Try searching for something else</p>
</Command.Empty>
</Command>
);
},
};
export const LongList: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.Input placeholder="Search items..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
<Command.List className="max-h-60 overflow-auto py-2">
{Array.from({ length: 20 }, (_, i) => (
<Command.Item key={i} className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">
Item {i + 1}
</Command.Item>
))}
</Command.List>
<Command.Empty className="py-6 text-center text-sm text-gray-500">No results found.</Command.Empty>
</Command>
);
},
};
export const WithoutSearch: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.List className="max-h-80 overflow-auto py-2">
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<User className="h-4 w-4" />
<span>Profile</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<Settings className="h-4 w-4" />
<span>Settings</span>
</Command.Item>
<Command.Item className="flex cursor-pointer items-center gap-2 rounded px-3 py-2 hover:bg-gray-100">
<Folder className="h-4 w-4" />
<span>Files</span>
</Command.Item>
</Command.List>
</Command>
);
},
};
export const CustomStyling: Story = {
render() {
return (
<Command className="w-96 rounded-lg border-2 border-blue-300 bg-blue-50 p-2 shadow-lg">
<Command.Input
placeholder="Search with custom styling..."
className="h-9 w-full bg-transparent py-3 text-sm text-blue-900 outline-none placeholder:text-blue-400"
/>
<Command.List className="max-h-80 overflow-auto py-2">
<Command.Item className="cursor-pointer rounded px-3 py-2 text-blue-900 hover:bg-blue-200">
Custom Item 1
</Command.Item>
<Command.Item className="cursor-pointer rounded px-3 py-2 text-blue-900 hover:bg-blue-200">
Custom Item 2
</Command.Item>
<Command.Item className="cursor-pointer rounded px-3 py-2 text-blue-900 hover:bg-blue-200">
Custom Item 3
</Command.Item>
</Command.List>
<Command.Empty className="py-6 text-center text-sm text-blue-500">No matching items found.</Command.Empty>
</Command>
);
},
};
export const DisabledItems: Story = {
render() {
return (
<Command className="w-96 rounded-lg border border-gray-200 p-2">
<Command.Input placeholder="Search..." className="h-9 w-full bg-transparent py-3 text-sm outline-none" />
<Command.List className="max-h-80 overflow-auto py-2">
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Active Item 1</Command.Item>
<Command.Item disabled className="cursor-not-allowed rounded px-3 py-2 opacity-50">
Disabled Item
</Command.Item>
<Command.Item className="cursor-pointer rounded px-3 py-2 hover:bg-gray-100">Active Item 2</Command.Item>
</Command.List>
<Command.Empty className="py-6 text-center text-sm text-gray-500">No results found.</Command.Empty>
</Command>
);
},
};

View File

@@ -1,9 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Copy, Download, Edit, Share, Trash, ChevronRight, Star, Archive } from "lucide-react";
import { ContextMenu } from "./context-menu";
// cannot use satisfies here because base-ui does not have portable types.
const meta: Meta<typeof ContextMenu> = {
title: "Components/ContextMenu",
component: ContextMenu,
subcomponents: {
ContextMenuTrigger: ContextMenu.Trigger,
ContextMenuPortal: ContextMenu.Portal,
ContextMenuContent: ContextMenu.Content,
ContextMenuItem: ContextMenu.Item,
ContextMenuSeparator: ContextMenu.Separator,
ContextMenuSubmenu: ContextMenu.Submenu,
ContextMenuSubmenuTrigger: ContextMenu.SubmenuTrigger,
},
args: {
children: null,
},
parameters: {
layout: "centered",
},
@@ -11,25 +25,354 @@ const meta: Meta<typeof ContextMenu> = {
};
export default meta;
type Story = StoryObj<typeof ContextMenu>;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>Back</ContextMenu.Item>
<ContextMenu.Item>Forward</ContextMenu.Item>
<ContextMenu.Item>Reload</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>More Tools</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
),
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>Back</ContextMenu.Item>
<ContextMenu.Item>Forward</ContextMenu.Item>
<ContextMenu.Item>Reload</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>More Tools</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const WithIcons: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy
</ContextMenu.Item>
<ContextMenu.Item>
<Edit className="mr-2 h-4 w-4" />
Edit
</ContextMenu.Item>
<ContextMenu.Item>
<Download className="mr-2 h-4 w-4" />
Download
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>
<Share className="mr-2 h-4 w-4" />
Share
</ContextMenu.Item>
<ContextMenu.Item>
<Trash className="mr-2 h-4 w-4 text-red-500" />
<span className="text-red-500">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const WithSubmenus: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy
</ContextMenu.Item>
<ContextMenu.Item>
<Edit className="mr-2 h-4 w-4" />
Edit
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Submenu>
<ContextMenu.SubmenuTrigger>
<Share className="mr-2 h-4 w-4" />
Share
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenu.SubmenuTrigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>Email</ContextMenu.Item>
<ContextMenu.Item>Message</ContextMenu.Item>
<ContextMenu.Item>Copy Link</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Submenu>
<ContextMenu.Separator />
<ContextMenu.Item>
<Trash className="mr-2 h-4 w-4 text-red-500" />
<span className="text-red-500">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const DisabledItems: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy
</ContextMenu.Item>
<ContextMenu.Item disabled>
<Edit className="mr-2 h-4 w-4" />
Edit (Disabled)
</ContextMenu.Item>
<ContextMenu.Item>
<Download className="mr-2 h-4 w-4" />
Download
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item disabled>
<Share className="mr-2 h-4 w-4" />
Share (Disabled)
</ContextMenu.Item>
<ContextMenu.Item>
<Trash className="mr-2 h-4 w-4 text-red-500" />
<span className="text-red-500">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const OnFileCard: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="w-64 p-4 border border-custom-border-200 rounded-lg hover:bg-custom-background-80 cursor-pointer">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-custom-primary-100 rounded flex items-center justify-center text-white text-lg">
📄
</div>
<div className="flex-1">
<div className="font-medium">Document.pdf</div>
<div className="text-sm text-custom-text-400">2.4 MB</div>
</div>
</div>
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Download className="mr-2 h-4 w-4" />
Download
</ContextMenu.Item>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy Link
</ContextMenu.Item>
<ContextMenu.Item>
<Star className="mr-2 h-4 w-4" />
Add to Favorites
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>
<Archive className="mr-2 h-4 w-4" />
Archive
</ContextMenu.Item>
<ContextMenu.Item>
<Trash className="mr-2 h-4 w-4 text-red-500" />
<span className="text-red-500">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const OnImage: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="relative w-80 h-56 bg-custom-background-80 rounded-lg overflow-hidden cursor-pointer">
<div className="absolute inset-0 flex items-center justify-center text-custom-text-400">
Image Placeholder
</div>
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Download className="mr-2 h-4 w-4" />
Save Image
</ContextMenu.Item>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy Image
</ContextMenu.Item>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy Image URL
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>Open Image in New Tab</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const OnText: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="w-96 p-6 border border-custom-border-200 rounded-lg">
<h3 className="text-lg font-semibold mb-2">Context Menu on Text</h3>
<p className="text-custom-text-300">
Right click anywhere on this text area to see the context menu. This demonstrates how context menus can be
applied to text content areas.
</p>
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy
</ContextMenu.Item>
<ContextMenu.Item>
<Edit className="mr-2 h-4 w-4" />
Edit
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>Select All</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const NestedSubmenus: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>New File</ContextMenu.Item>
<ContextMenu.Item>New Folder</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Submenu>
<ContextMenu.SubmenuTrigger>
Import
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenu.SubmenuTrigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>From File</ContextMenu.Item>
<ContextMenu.Item>From URL</ContextMenu.Item>
<ContextMenu.Submenu>
<ContextMenu.SubmenuTrigger>
From Cloud
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenu.SubmenuTrigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>Google Drive</ContextMenu.Item>
<ContextMenu.Item>Dropbox</ContextMenu.Item>
<ContextMenu.Item>OneDrive</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Submenu>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Submenu>
<ContextMenu.Separator />
<ContextMenu.Item>
<Trash className="mr-2 h-4 w-4 text-red-500" />
<span className="text-red-500">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};
export const WithKeyboardShortcuts: Story = {
render() {
return (
<ContextMenu>
<ContextMenu.Trigger>
<div className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-custom-border-300 text-sm">
Right click here
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item>
<Copy className="mr-2 h-4 w-4" />
Copy
<span className="ml-auto text-xs text-custom-text-400">C</span>
</ContextMenu.Item>
<ContextMenu.Item>
<Edit className="mr-2 h-4 w-4" />
Edit
<span className="ml-auto text-xs text-custom-text-400">E</span>
</ContextMenu.Item>
<ContextMenu.Item>
<Download className="mr-2 h-4 w-4" />
Download
<span className="ml-auto text-xs text-custom-text-400">D</span>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>
<Trash className="mr-2 h-4 w-4 text-red-500" />
<span className="text-red-500">Delete</span>
<span className="ml-auto text-xs text-custom-text-400"></span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
);
},
};

View File

@@ -0,0 +1,399 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { X } from "lucide-react";
import { useArgs } from "storybook/preview-api";
import { Dialog, EDialogWidth } from "./root";
const meta = {
title: "Components/Dialog",
component: Dialog,
subcomponents: {
DialogPanel: Dialog.Panel,
DialogTitle: Dialog.Title,
},
args: {
children: null,
open: false,
onOpenChange: () => {},
},
parameters: {
layout: "centered",
},
tags: ["autodocs"],
render(args) {
const [{ open }, updateArgs] = useArgs();
const setOpen = (value: boolean) => updateArgs({ open: value });
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Dialog
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel>
<div className="p-6">
<Dialog.Title>Dialog Title</Dialog.Title>
<div className="mt-4">
<p className="text-sm text-gray-600">This is the dialog content. You can put any content here.</p>
</div>
<div className="mt-6 flex justify-end gap-2">
<button
onClick={() => setOpen(false)}
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={() => setOpen(false)}
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
>
Confirm
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: null,
},
};
export const TopPosition: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Dialog (Top)
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel position="top">
<div className="p-6">
<Dialog.Title>Top Positioned Dialog</Dialog.Title>
<div className="mt-4">
<p className="text-sm text-gray-600">
This dialog appears at the top of the screen instead of centered.
</p>
</div>
<div className="mt-6 flex justify-end gap-2">
<button
onClick={() => setOpen(false)}
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
>
Close
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const SmallWidth: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Small Dialog
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel width={EDialogWidth.SM}>
<div className="p-6">
<Dialog.Title>Small Dialog</Dialog.Title>
<div className="mt-4">
<p className="text-sm text-gray-600">This is a small dialog.</p>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setOpen(false)}
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
>
Close
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const LargeWidth: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Large Dialog
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel width={EDialogWidth.XXXXL}>
<div className="p-6">
<Dialog.Title>Large Dialog</Dialog.Title>
<div className="mt-4">
<p className="text-sm text-gray-600">
This is a large dialog with more horizontal space for content.
</p>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setOpen(false)}
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
>
Close
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const WithCloseButton: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Dialog with Close Button
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel>
<div className="p-6">
<div className="flex items-start justify-between">
<Dialog.Title>Dialog with Close Button</Dialog.Title>
<button onClick={() => setOpen(false)} className="rounded-full p-1 hover:bg-gray-100">
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-4">
<p className="text-sm text-gray-600">This dialog has a close button in the header.</p>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const ConfirmationDialog: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
const handleConfirm = () => {
alert("Confirmed!");
setOpen(false);
};
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600">
Delete Item
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel width={EDialogWidth.SM}>
<div className="p-6">
<Dialog.Title>Confirm Deletion</Dialog.Title>
<div className="mt-4">
<p className="text-sm text-gray-600">
Are you sure you want to delete this item? This action cannot be undone.
</p>
</div>
<div className="mt-6 flex justify-end gap-2">
<button
onClick={() => setOpen(false)}
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={handleConfirm}
className="rounded bg-red-500 px-4 py-2 text-sm text-white hover:bg-red-600"
>
Delete
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const FormDialog: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert("Form submitted!");
setOpen(false);
};
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Form
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel width={EDialogWidth.MD}>
<form onSubmit={handleSubmit} className="p-6">
<Dialog.Title>Create New Item</Dialog.Title>
<div className="mt-4 space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Name
</label>
<input
type="text"
id="name"
className="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm"
placeholder="Enter name"
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
</label>
<textarea
id="description"
rows={3}
className="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm"
placeholder="Enter description"
/>
</div>
</div>
<div className="mt-6 flex justify-end gap-2">
<button
type="button"
onClick={() => setOpen(false)}
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
>
Cancel
</button>
<button type="submit" className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600">
Create
</button>
</div>
</form>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const ScrollableContent: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<>
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Scrollable Dialog
</button>
{open && (
<Dialog {...args} open={open} onOpenChange={setOpen}>
<Dialog.Panel width={EDialogWidth.MD}>
<div className="p-6">
<Dialog.Title>Scrollable Content</Dialog.Title>
<div className="mt-4 max-h-96 overflow-y-auto">
{Array.from({ length: 20 }, (_, i) => (
<p key={i} className="mb-2 text-sm text-gray-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</p>
))}
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setOpen(false)}
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
>
Close
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
)}
</>
);
},
};
export const AllWidths: Story = {
render() {
const [openWidth, setOpenWidth] = useState<EDialogWidth | null>(null);
const widths = [
{ width: EDialogWidth.SM, label: "Small" },
{ width: EDialogWidth.MD, label: "Medium" },
{ width: EDialogWidth.LG, label: "Large" },
{ width: EDialogWidth.XL, label: "XL" },
{ width: EDialogWidth.XXL, label: "2XL" },
{ width: EDialogWidth.XXXL, label: "3XL" },
{ width: EDialogWidth.XXXXL, label: "4XL" },
];
return (
<div className="flex flex-wrap gap-2">
{widths.map(({ width, label }) => (
<button
key={width}
onClick={() => setOpenWidth(width)}
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
>
{label}
</button>
))}
{widths.map(({ width, label }) => (
<Dialog key={width} open={openWidth === width} onOpenChange={() => setOpenWidth(null)}>
<Dialog.Panel width={width}>
<div className="p-6">
<Dialog.Title>{label} Dialog</Dialog.Title>
<div className="mt-4">
<p className="text-sm text-gray-600">This dialog uses the {label} width variant.</p>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setOpenWidth(null)}
className="rounded bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
>
Close
</button>
</div>
</div>
</Dialog.Panel>
</Dialog>
))}
</div>
);
},
};

View File

@@ -1,6 +1,6 @@
"use client";
import * as React from "react";
import { forwardRef, memo, useMemo } from "react";
import { Dialog as BaseDialog } from "@base-ui-components/react";
import { cn } from "../utils/classname";
@@ -41,44 +41,41 @@ const OVERLAY_CLASSNAME = cn("fixed inset-0 z-backdrop bg-custom-backdrop");
const BASE_CLASSNAME = "relative text-left bg-custom-background-100 rounded-lg shadow-md w-full z-modal";
// Utility functions
const getPositionClassNames = React.useCallback(
(position: DialogPosition) =>
cn("isolate fixed z-modal", {
"top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2": position === "center",
"top-8 left-1/2 -translate-x-1/2": position === "top",
}),
[]
);
const getPositionClassNames = (position: DialogPosition) =>
cn("isolate fixed z-modal", {
"top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2": position === "center",
"top-8 left-1/2 -translate-x-1/2": position === "top",
});
const DialogPortal = React.memo<React.ComponentProps<typeof BaseDialog.Portal>>(({ children, ...props }) => (
const DialogPortal = memo<React.ComponentProps<typeof BaseDialog.Portal>>(({ children, ...props }) => (
<BaseDialog.Portal data-slot="dialog-portal" {...props}>
{children}
</BaseDialog.Portal>
));
DialogPortal.displayName = "DialogPortal";
const DialogOverlay = React.memo<React.ComponentProps<typeof BaseDialog.Backdrop>>(({ className, ...props }) => (
const DialogOverlay = memo<React.ComponentProps<typeof BaseDialog.Backdrop>>(({ className, ...props }) => (
<BaseDialog.Backdrop data-slot="dialog-overlay" className={cn(OVERLAY_CLASSNAME, className)} {...props} />
));
DialogOverlay.displayName = "DialogOverlay";
const DialogComponent = React.memo<DialogProps>(({ children, ...props }) => (
const DialogComponent = memo<DialogProps>(({ children, ...props }) => (
<BaseDialog.Root data-slot="dialog" {...props}>
{children}
</BaseDialog.Root>
));
DialogComponent.displayName = "Dialog";
const DialogTrigger = React.memo<React.ComponentProps<typeof BaseDialog.Trigger>>(({ children, ...props }) => (
const DialogTrigger = memo<React.ComponentProps<typeof BaseDialog.Trigger>>(({ children, ...props }) => (
<BaseDialog.Trigger data-slot="dialog-trigger" {...props}>
{children}
</BaseDialog.Trigger>
));
DialogTrigger.displayName = "DialogTrigger";
const DialogPanel = React.forwardRef<React.ElementRef<typeof BaseDialog.Popup>, DialogPanelProps>(
const DialogPanel = forwardRef<React.ElementRef<typeof BaseDialog.Popup>, DialogPanelProps>(
({ className, width = EDialogWidth.XXL, children, position = "center", ...props }, ref) => {
const positionClassNames = React.useMemo(() => getPositionClassNames(position), [position]);
const positionClassNames = useMemo(() => getPositionClassNames(position), [position]);
return (
<DialogPortal>
<DialogOverlay />
@@ -98,7 +95,7 @@ const DialogPanel = React.forwardRef<React.ElementRef<typeof BaseDialog.Popup>,
);
DialogPanel.displayName = "DialogPanel";
const DialogTitle = React.memo<DialogTitleProps>(({ className, children, ...props }) => (
const DialogTitle = memo<DialogTitleProps>(({ className, children, ...props }) => (
<BaseDialog.Title data-slot="dialog-title" className={cn("text-lg leading-none font-semibold", className)} {...props}>
{children}
</BaseDialog.Title>

View File

@@ -3,44 +3,367 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import { EmojiPicker } from "./emoji-picker";
import { EmojiIconPickerTypes, TChangeHandlerProps } from "./helper";
const meta: Meta<typeof EmojiPicker> = {
const meta = {
title: "Components/Emoji/EmojiPicker",
component: EmojiPicker,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
} satisfies Meta<typeof EmojiPicker>;
export default meta;
type Story = StoryObj<typeof EmojiPicker>;
const EmojiPickerWithState = (args: React.ComponentProps<typeof EmojiPicker>) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4">
<EmojiPicker
{...args}
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={(value) => {
setSelectedValue(value);
}}
/>
{selectedValue && <div className="text-sm text-gray-600">Selected: {JSON.stringify(selectedValue, null, 2)}</div>}
</div>
);
};
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: (args: React.ComponentProps<typeof EmojiPicker>) => <EmojiPickerWithState {...args} />,
args: {
label: "😊 Pick an emoji or icon",
defaultOpen: EmojiIconPickerTypes.EMOJI,
closeOnSelect: true,
searchPlaceholder: "Search emojis...",
iconType: "lucide",
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="😊 Pick an emoji or icon"
defaultOpen={EmojiIconPickerTypes.EMOJI}
closeOnSelect
/>
{selectedValue && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
<div className="font-medium mb-2">Selected:</div>
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
</div>
)}
</div>
);
},
};
export const OpenToEmojiTab: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="😊 Choose Emoji"
defaultOpen={EmojiIconPickerTypes.EMOJI}
closeOnSelect
/>
{selectedValue && (
<div className="text-sm">Selected: {selectedValue.type === "emoji" ? selectedValue.value : "Icon"}</div>
)}
</div>
);
},
};
export const OpenToIconTab: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="🎨 Choose Icon"
defaultOpen={EmojiIconPickerTypes.ICON}
closeOnSelect
/>
{selectedValue && (
<div className="text-sm">
Selected:{" "}
{selectedValue.type === "icon" && typeof selectedValue.value === "object"
? selectedValue.value.name
: "Emoji"}
</div>
)}
</div>
);
},
};
export const LucideIcons: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="Lucide Icons"
defaultOpen={EmojiIconPickerTypes.ICON}
closeOnSelect
iconType="lucide"
/>
{selectedValue && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
<div className="font-medium mb-2">Selected Icon:</div>
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
</div>
)}
</div>
);
},
};
export const MaterialIcons: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="Material Icons"
defaultOpen={EmojiIconPickerTypes.ICON}
closeOnSelect
iconType="material"
/>
{selectedValue && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
<div className="font-medium mb-2">Selected Icon:</div>
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
</div>
)}
</div>
);
},
};
export const CloseOnSelectDisabled: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValues, setSelectedValues] = useState<TChangeHandlerProps[]>([]);
const handleChange = (value: TChangeHandlerProps) => {
setSelectedValues((prev) => [...prev, value]);
};
return (
<div className="space-y-4 p-4">
<div className="flex gap-2 items-center">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={handleChange}
label="Select Multiple (Stays Open)"
defaultOpen={EmojiIconPickerTypes.EMOJI}
closeOnSelect={false}
/>
<button
className="px-3 py-1.5 text-sm bg-custom-background-80 rounded hover:bg-custom-background-90"
onClick={() => setSelectedValues([])}
>
Clear
</button>
</div>
{selectedValues.length > 0 && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
<div className="font-medium mb-2">Selected ({selectedValues.length}):</div>
<div className="flex gap-2 flex-wrap">
{selectedValues.map((val, idx) => (
<span key={idx} className="text-lg">
{val.type === "emoji" ? val.value : "🎨"}
</span>
))}
</div>
</div>
)}
</div>
);
},
};
export const CustomSearchPlaceholder: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="Custom Search"
defaultOpen={EmojiIconPickerTypes.EMOJI}
closeOnSelect
searchPlaceholder="Type to find emojis..."
/>
{selectedValue && <div className="text-sm">Selected: {JSON.stringify(selectedValue)}</div>}
</div>
);
},
};
export const SearchDisabled: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="No Search"
defaultOpen={EmojiIconPickerTypes.EMOJI}
closeOnSelect
searchDisabled
/>
{selectedValue && <div className="text-sm">Selected: {JSON.stringify(selectedValue)}</div>}
</div>
);
},
};
export const CustomIconColor: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedValue}
label="Custom Icon Color"
defaultOpen={EmojiIconPickerTypes.ICON}
closeOnSelect
defaultIconColor="#FF5733"
/>
{selectedValue && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
<pre className="text-xs">{JSON.stringify(selectedValue, null, 2)}</pre>
</div>
)}
</div>
);
},
};
export const DifferentPlacements: Story = {
render() {
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const [isOpen3, setIsOpen3] = useState(false);
const [isOpen4, setIsOpen4] = useState(false);
return (
<div className="p-8 space-y-8">
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Bottom Start:</span>
<EmojiPicker
isOpen={isOpen1}
handleToggle={setIsOpen1}
onChange={() => {}}
label="Bottom Start"
placement="bottom-start"
/>
</div>
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Bottom End:</span>
<EmojiPicker
isOpen={isOpen2}
handleToggle={setIsOpen2}
onChange={() => {}}
label="Bottom End"
placement="bottom-end"
/>
</div>
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Top Start:</span>
<EmojiPicker
isOpen={isOpen3}
handleToggle={setIsOpen3}
onChange={() => {}}
label="Top Start"
placement="top-start"
/>
</div>
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Top End:</span>
<EmojiPicker
isOpen={isOpen4}
handleToggle={setIsOpen4}
onChange={() => {}}
label="Top End"
placement="top-end"
/>
</div>
</div>
);
},
};
export const InFormContext: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [formData, setFormData] = useState({
title: "",
emoji: null as TChangeHandlerProps | null,
});
const handleEmojiChange = (value: TChangeHandlerProps) => {
setFormData((prev) => ({ ...prev, emoji: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert(`Form submitted:\n${JSON.stringify(formData, null, 2)}`);
};
return (
<div className="max-w-md p-4">
<form onSubmit={handleSubmit} className="space-y-4 p-6 border border-custom-border-200 rounded-lg">
<div>
<label className="block text-sm font-medium mb-2">Project Title</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
className="w-full px-3 py-2 bg-custom-background-80 border border-custom-border-200 rounded"
placeholder="Enter project title"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Project Icon</label>
<EmojiPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={handleEmojiChange}
label={formData.emoji && formData.emoji.type === "emoji" ? formData.emoji.value : "Click to select icon"}
defaultOpen={EmojiIconPickerTypes.EMOJI}
closeOnSelect
buttonClassName="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90 w-full text-left"
/>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-custom-primary-100 text-white rounded hover:bg-custom-primary-200"
>
Create Project
</button>
</form>
</div>
);
},
};

View File

@@ -2,48 +2,342 @@ import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { SmilePlus } from "lucide-react";
import { stringToEmoji } from "../emoji-icon-picker";
import { EmojiReactionGroup, EmojiReactionType } from "./emoji-reaction";
import { EmojiReactionPicker } from "./emoji-reaction-picker";
const meta: Meta<typeof EmojiReactionPicker> = {
const meta = {
title: "Components/Emoji/EmojiReactionPicker",
component: EmojiReactionPicker,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
} satisfies Meta<typeof EmojiReactionPicker>;
export default meta;
type Story = StoryObj<typeof EmojiReactionPicker>;
const EmojiPickerDemo = (args: React.ComponentProps<typeof EmojiReactionPicker>) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiReactionPicker
{...args}
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={(emoji) => {
setSelectedEmoji(emoji);
console.log("Selected emoji:", emoji);
}}
label={
<span className={`flex items-center justify-center rounded-md px-2 size-8 text-xl`}>
{selectedEmoji ? stringToEmoji(selectedEmoji) : <SmilePlus className="h-6 text-custom-text-100" />}
</span>
}
/>
</div>
);
};
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: (args) => <EmojiPickerDemo {...args} />,
args: {
closeOnSelect: true,
searchPlaceholder: "Search emojis...",
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedEmoji}
closeOnSelect
label={
<span className="flex items-center justify-center rounded-md px-2 size-8 text-xl">
{selectedEmoji ? stringToEmoji(selectedEmoji) : <SmilePlus className="h-6 text-custom-text-100" />}
</span>
}
/>
{selectedEmoji && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
Selected: {selectedEmoji}
</div>
)}
</div>
);
},
};
export const WithCustomLabel: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedEmoji}
closeOnSelect
label={
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90 flex items-center gap-2">
{selectedEmoji ? stringToEmoji(selectedEmoji) : <SmilePlus className="h-4 w-4" />}
<span className="text-sm">Add Reaction</span>
</button>
}
/>
{selectedEmoji && <div className="text-sm">Selected: {selectedEmoji}</div>}
</div>
);
},
};
export const InlineReactions: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [reactions, setReactions] = useState<EmojiReactionType[]>([
{ emoji: "👍", count: 3, reacted: false, users: ["Alice", "Bob", "Charlie"] },
{ emoji: "❤️", count: 2, reacted: true, users: ["You", "David"] },
]);
const handleReactionAdd = (emoji: string) => {
setReactions((prev) => {
const existing = prev.find((r) => r.emoji === emoji);
if (existing) {
return prev.map((r) => (r.emoji === emoji ? { ...r, count: r.count + 1, reacted: true } : r));
}
return [...prev, { emoji, count: 1, reacted: true, users: ["You"] }];
});
};
const handleReactionClick = (emoji: string) => {
setReactions((prev) =>
prev.map((r) => {
if (r.emoji === emoji) {
return {
...r,
reacted: !r.reacted,
count: r.reacted ? r.count - 1 : r.count + 1,
};
}
return r;
})
);
};
return (
<div className="p-4">
<EmojiReactionGroup
reactions={reactions}
onReactionClick={handleReactionClick}
onAddReaction={() => setIsOpen(true)}
showAddButton={false}
/>
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={handleReactionAdd}
closeOnSelect
label={
<button className="inline-flex items-center justify-center rounded-full border border-dashed border-custom-border-300 bg-custom-background-100 text-custom-text-400 transition-all duration-200 hover:border-custom-primary-100 hover:text-custom-primary-100 hover:bg-custom-primary-100/5 h-7 w-7">
<SmilePlus className="h-3.5 w-3.5" />
</button>
}
/>
</div>
);
},
};
export const DifferentPlacements: Story = {
render() {
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const [isOpen3, setIsOpen3] = useState(false);
const [isOpen4, setIsOpen4] = useState(false);
return (
<div className="p-8 space-y-8">
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Bottom Start:</span>
<EmojiReactionPicker
isOpen={isOpen1}
handleToggle={setIsOpen1}
onChange={() => {}}
placement="bottom-start"
label={<SmilePlus className="h-6 w-6" />}
/>
</div>
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Bottom End:</span>
<EmojiReactionPicker
isOpen={isOpen2}
handleToggle={setIsOpen2}
onChange={() => {}}
placement="bottom-end"
label={<SmilePlus className="h-6 w-6" />}
/>
</div>
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Top Start:</span>
<EmojiReactionPicker
isOpen={isOpen3}
handleToggle={setIsOpen3}
onChange={() => {}}
placement="top-start"
label={<SmilePlus className="h-6 w-6" />}
/>
</div>
<div className="flex gap-4 items-center">
<span className="text-sm w-32">Top End:</span>
<EmojiReactionPicker
isOpen={isOpen4}
handleToggle={setIsOpen4}
onChange={() => {}}
placement="top-end"
label={<SmilePlus className="h-6 w-6" />}
/>
</div>
</div>
);
},
};
export const SearchDisabled: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedEmoji}
closeOnSelect
searchDisabled
label={
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90">
No Search
</button>
}
/>
{selectedEmoji && <div className="text-sm">Selected: {selectedEmoji}</div>}
</div>
);
},
};
export const CustomSearchPlaceholder: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
return (
<div className="space-y-4 p-4">
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={setSelectedEmoji}
closeOnSelect
searchPlaceholder="Find your emoji..."
label={
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90">
Custom Search
</button>
}
/>
{selectedEmoji && <div className="text-sm">Selected: {selectedEmoji}</div>}
</div>
);
},
};
export const CloseOnSelectDisabled: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [selectedEmojis, setSelectedEmojis] = useState<string[]>([]);
const handleChange = (emoji: string) => {
setSelectedEmojis((prev) => [...prev, emoji]);
};
return (
<div className="space-y-4 p-4">
<div className="flex gap-2 items-center">
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={handleChange}
closeOnSelect={false}
label={
<button className="px-4 py-2 bg-custom-background-80 border border-custom-border-200 rounded hover:bg-custom-background-90">
Select Multiple (Stays Open)
</button>
}
/>
<button
className="px-3 py-1.5 text-sm bg-custom-background-80 rounded hover:bg-custom-background-90"
onClick={() => setSelectedEmojis([])}
>
Clear
</button>
</div>
{selectedEmojis.length > 0 && (
<div className="text-sm p-4 bg-custom-background-80 rounded border border-custom-border-200">
<div className="font-medium mb-2">Selected ({selectedEmojis.length}):</div>
<div className="flex gap-2 flex-wrap">
{selectedEmojis.map((emoji, idx) => (
<span key={idx} className="text-xl">
{emoji}
</span>
))}
</div>
</div>
)}
</div>
);
},
};
export const InMessageContext: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
const [reactions, setReactions] = useState<EmojiReactionType[]>([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
]);
const handleReactionAdd = (emoji: string) => {
setReactions((prev) => {
const existing = prev.find((r) => r.emoji === emoji);
if (existing) {
return prev.map((r) => (r.emoji === emoji ? { ...r, count: r.count + 1, reacted: true } : r));
}
return [...prev, { emoji, count: 1, reacted: true, users: ["You"] }];
});
};
const handleReactionClick = (emoji: string) => {
setReactions((prev) =>
prev.map((r) => {
if (r.emoji === emoji) {
return {
...r,
reacted: !r.reacted,
count: r.reacted ? r.count - 1 : r.count + 1,
};
}
return r;
})
);
};
return (
<div className="max-w-md border border-custom-border-200 rounded-lg p-4 space-y-3">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-custom-primary-100 flex items-center justify-center text-white text-sm">
AB
</div>
<div className="flex-1">
<div className="font-medium text-sm">Alice Brown</div>
<div className="text-sm text-custom-text-300 mt-1">
Just finished the design for the new dashboard! Would love to hear your thoughts.
</div>
</div>
</div>
<div className="flex gap-2">
<EmojiReactionGroup reactions={reactions} onReactionClick={handleReactionClick} showAddButton={false} />
<EmojiReactionPicker
isOpen={isOpen}
handleToggle={setIsOpen}
onChange={handleReactionAdd}
closeOnSelect
label={
<button className="inline-flex items-center justify-center rounded-full border border-dashed border-custom-border-300 bg-custom-background-100 text-custom-text-400 transition-all duration-200 hover:border-custom-primary-100 hover:text-custom-primary-100 hover:bg-custom-primary-100/5 h-7 w-7">
<SmilePlus className="h-3.5 w-3.5" />
</button>
}
/>
</div>
</div>
);
},
};

View File

@@ -1,24 +1,25 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { EmojiReaction } from "./emoji-reaction";
import { EmojiReaction, EmojiReactionGroup, EmojiReactionButton, EmojiReactionType } from "./emoji-reaction";
const meta: Meta<typeof EmojiReaction> = {
const meta = {
title: "Components/Emoji/EmojiReaction",
component: EmojiReaction,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
} satisfies Meta<typeof EmojiReaction>;
export default meta;
type Story = StoryObj<typeof EmojiReaction>;
type Story = StoryObj<typeof meta>;
export const Single: Story = {
args: {
emoji: "👍",
count: 5,
reacted: false,
users: ["User 1", "User 2", "User 3"],
users: ["Alice", "Bob", "Charlie"],
},
};
@@ -27,6 +28,256 @@ export const Reacted: Story = {
emoji: "❤️",
count: 12,
reacted: true,
users: ["User 1", "User 2", "User 3", "User 4", "User 5", "User 6"],
users: ["Alice", "Bob", "Charlie", "David", "Emma", "Frank"],
},
};
export const Interactive: Story = {
render() {
const [reacted, setReacted] = useState(false);
const [count, setCount] = useState(5);
const handleClick = () => {
setReacted(!reacted);
setCount(reacted ? count - 1 : count + 1);
};
return (
<div className="flex flex-col gap-4 items-center">
<EmojiReaction
emoji="👍"
count={count}
reacted={reacted}
users={["Alice", "Bob", "Charlie"]}
onReactionClick={handleClick}
/>
<p className="text-sm text-custom-text-400">Click to toggle reaction</p>
</div>
);
},
};
export const WithTooltip: Story = {
args: {
emoji: "🎉",
count: 8,
reacted: false,
users: ["Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Grace", "Henry"],
},
};
export const WithoutCount: Story = {
args: {
emoji: "🔥",
count: 0,
reacted: false,
showCount: false,
},
};
export const Sizes: Story = {
render() {
return (
<div className="flex flex-col gap-4 items-center">
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-text-400">Small</span>
<EmojiReaction emoji="👍" count={5} size="sm" users={["Alice", "Bob"]} />
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-text-400">Medium (default)</span>
<EmojiReaction emoji="👍" count={5} size="md" users={["Alice", "Bob"]} />
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-text-400">Large</span>
<EmojiReaction emoji="👍" count={5} size="lg" users={["Alice", "Bob"]} />
</div>
</div>
);
},
};
export const MultipleReactions: Story = {
render() {
const [reactions, setReactions] = useState<EmojiReactionType[]>([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
{ emoji: "❤️", count: 12, reacted: true, users: ["David", "Emma", "Frank"] },
{ emoji: "🎉", count: 3, reacted: false, users: ["Grace"] },
]);
const handleReactionClick = (emoji: string) => {
setReactions((prev) =>
prev.map((r) => {
if (r.emoji === emoji) {
return {
...r,
reacted: !r.reacted,
count: r.reacted ? r.count - 1 : r.count + 1,
};
}
return r;
})
);
};
return (
<div className="flex gap-2">
{reactions.map((reaction) => (
<EmojiReaction
key={reaction.emoji}
emoji={reaction.emoji}
count={reaction.count}
reacted={reaction.reacted}
users={reaction.users}
onReactionClick={handleReactionClick}
/>
))}
</div>
);
},
};
export const AddButton: Story = {
render() {
const handleAdd = () => {
alert("Add reaction clicked");
};
return <EmojiReactionButton onAddReaction={handleAdd} />;
},
};
export const AddButtonSizes: Story = {
render() {
return (
<div className="flex gap-4 items-center">
<div className="flex flex-col gap-2 items-center">
<span className="text-xs text-custom-text-400">Small</span>
<EmojiReactionButton size="sm" />
</div>
<div className="flex flex-col gap-2 items-center">
<span className="text-xs text-custom-text-400">Medium</span>
<EmojiReactionButton size="md" />
</div>
<div className="flex flex-col gap-2 items-center">
<span className="text-xs text-custom-text-400">Large</span>
<EmojiReactionButton size="lg" />
</div>
</div>
);
},
};
export const ReactionGroup: Story = {
render() {
const [reactions, setReactions] = useState<EmojiReactionType[]>([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
{ emoji: "❤️", count: 12, reacted: true, users: ["David", "Emma", "Frank"] },
{ emoji: "🎉", count: 3, reacted: false, users: ["Grace"] },
{ emoji: "🔥", count: 8, reacted: false, users: ["Henry", "Ivy"] },
]);
const handleReactionClick = (emoji: string) => {
setReactions((prev) =>
prev.map((r) => {
if (r.emoji === emoji) {
return {
...r,
reacted: !r.reacted,
count: r.reacted ? r.count - 1 : r.count + 1,
};
}
return r;
})
);
};
const handleAddReaction = () => {
alert("Add reaction clicked");
};
return (
<EmojiReactionGroup
reactions={reactions}
onReactionClick={handleReactionClick}
onAddReaction={handleAddReaction}
/>
);
},
};
export const ReactionGroupSizes: Story = {
render() {
const reactions: EmojiReactionType[] = [
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob"] },
{ emoji: "❤️", count: 12, reacted: true, users: ["Charlie", "David"] },
{ emoji: "🎉", count: 3, reacted: false, users: ["Emma"] },
];
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-text-400">Small</span>
<EmojiReactionGroup reactions={reactions} size="sm" />
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-text-400">Medium</span>
<EmojiReactionGroup reactions={reactions} size="md" />
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-text-400">Large</span>
<EmojiReactionGroup reactions={reactions} size="lg" />
</div>
</div>
);
},
};
export const InMessageContext: Story = {
render() {
const [reactions, setReactions] = useState<EmojiReactionType[]>([
{ emoji: "👍", count: 5, reacted: false, users: ["Alice", "Bob", "Charlie"] },
{ emoji: "❤️", count: 2, reacted: true, users: ["You", "David"] },
]);
const handleReactionClick = (emoji: string) => {
setReactions((prev) =>
prev.map((r) => {
if (r.emoji === emoji) {
return {
...r,
reacted: !r.reacted,
count: r.reacted ? r.count - 1 : r.count + 1,
};
}
return r;
})
);
};
return (
<div className="max-w-md border border-custom-border-200 rounded-lg p-4 space-y-3">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-custom-primary-100 flex items-center justify-center text-white text-sm">
AB
</div>
<div className="flex-1">
<div className="font-medium text-sm">Alice Brown</div>
<div className="text-sm text-custom-text-300 mt-1">
Hey everyone! Just wanted to share some exciting news about our project launch next week!
</div>
</div>
</div>
<EmojiReactionGroup reactions={reactions} onReactionClick={handleReactionClick} />
</div>
);
},
};
export const ManyUsers: Story = {
args: {
emoji: "🎉",
count: 47,
reacted: true,
users: ["Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Grace", "Henry", "Ivy", "Jack", "Kate", "Liam"],
},
};

View File

@@ -0,0 +1,261 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Settings, User, LogOut, Mail, Bell, HelpCircle } from "lucide-react";
import { Menu } from "./menu";
const meta = {
title: "Components/Menu",
component: Menu,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
subcomponents: {
MenuItem: Menu.MenuItem,
SubMenu: Menu.SubMenu,
},
args: {
children: null,
},
} satisfies Meta<typeof Menu>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render() {
return (
<Menu label="Options">
<Menu.MenuItem onClick={() => alert("Option 1 clicked")}>Option 1</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Option 2 clicked")}>Option 2</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Option 3 clicked")}>Option 3</Menu.MenuItem>
</Menu>
);
},
};
export const WithIcons: Story = {
render() {
return (
<Menu label="Account">
<Menu.MenuItem onClick={() => alert("Profile")}>
<div className="flex items-center gap-2">
<User className="h-4 w-4" />
<span>Profile</span>
</div>
</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Settings")}>
<div className="flex items-center gap-2">
<Settings className="h-4 w-4" />
<span>Settings</span>
</div>
</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Messages")}>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4" />
<span>Messages</span>
</div>
</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Logout")}>
<div className="flex items-center gap-2">
<LogOut className="h-4 w-4" />
<span>Logout</span>
</div>
</Menu.MenuItem>
</Menu>
);
},
};
export const Ellipsis: Story = {
render() {
return (
<Menu ellipsis>
<Menu.MenuItem onClick={() => alert("Edit")}>Edit</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Duplicate")}>Duplicate</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Delete")}>Delete</Menu.MenuItem>
</Menu>
);
},
};
export const VerticalEllipsis: Story = {
render() {
return (
<Menu verticalEllipsis>
<Menu.MenuItem onClick={() => alert("Edit")}>Edit</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Duplicate")}>Duplicate</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Delete")}>Delete</Menu.MenuItem>
</Menu>
);
},
};
export const NoBorder: Story = {
render() {
return (
<Menu label="Actions" noBorder>
<Menu.MenuItem onClick={() => alert("Action 1")}>Action 1</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Action 2")}>Action 2</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Action 3")}>Action 3</Menu.MenuItem>
</Menu>
);
},
};
export const NoChevron: Story = {
render() {
return (
<Menu label="Menu" noChevron>
<Menu.MenuItem onClick={() => alert("Item 1")}>Item 1</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Item 2")}>Item 2</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Item 3")}>Item 3</Menu.MenuItem>
</Menu>
);
},
};
export const Disabled: Story = {
render() {
return (
<Menu label="Disabled Menu" disabled>
<Menu.MenuItem onClick={() => alert("Item 1")}>Item 1</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Item 2")}>Item 2</Menu.MenuItem>
</Menu>
);
},
};
export const DisabledItems: Story = {
render() {
return (
<Menu label="Options">
<Menu.MenuItem onClick={() => alert("Enabled")}>Enabled Item</Menu.MenuItem>
<Menu.MenuItem disabled>Disabled Item</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Enabled")}>Another Enabled Item</Menu.MenuItem>
</Menu>
);
},
};
export const CustomButton: Story = {
render() {
return (
<Menu
customButton={
<button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">Custom Button</button>
}
>
<Menu.MenuItem onClick={() => alert("Option 1")}>Option 1</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Option 2")}>Option 2</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Option 3")}>Option 3</Menu.MenuItem>
</Menu>
);
},
};
export const WithSubmenu: Story = {
render() {
return (
<Menu label="File">
<Menu.MenuItem onClick={() => alert("New File")}>New File</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Open")}>Open</Menu.MenuItem>
<Menu.SubMenu
trigger="Export"
className="min-w-[12rem] rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg"
>
<Menu.MenuItem onClick={() => alert("Export as PDF")}>Export as PDF</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Export as CSV")}>Export as CSV</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Export as JSON")}>Export as JSON</Menu.MenuItem>
</Menu.SubMenu>
<Menu.MenuItem onClick={() => alert("Close")}>Close</Menu.MenuItem>
</Menu>
);
},
};
export const MaxHeightSmall: Story = {
render() {
return (
<Menu label="Small Height" maxHeight="sm">
{Array.from({ length: 10 }, (_, i) => (
<Menu.MenuItem key={i} onClick={() => alert(`Item ${i + 1}`)}>
Item {i + 1}
</Menu.MenuItem>
))}
</Menu>
);
},
};
export const MaxHeightLarge: Story = {
render() {
return (
<Menu label="Large Height" maxHeight="lg">
{Array.from({ length: 15 }, (_, i) => (
<Menu.MenuItem key={i} onClick={() => alert(`Item ${i + 1}`)}>
Item {i + 1}
</Menu.MenuItem>
))}
</Menu>
);
},
};
export const ComplexMenu: Story = {
render() {
return (
<Menu label="More Actions" buttonClassName="bg-gray-100">
<Menu.MenuItem onClick={() => alert("Notifications")}>
<div className="flex items-center gap-2">
<Bell className="h-4 w-4" />
<span>Notifications</span>
<span className="ml-auto rounded bg-red-500 px-2 py-0.5 text-xs text-white">3</span>
</div>
</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Help")}>
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4" />
<span>Help Center</span>
</div>
</Menu.MenuItem>
<Menu.SubMenu
trigger="Settings"
className="min-w-[12rem] rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg"
>
<Menu.MenuItem onClick={() => alert("General Settings")}>General</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Privacy Settings")}>Privacy</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Security Settings")}>Security</Menu.MenuItem>
</Menu.SubMenu>
<div className="my-1 border-t border-gray-200" />
<Menu.MenuItem onClick={() => alert("Logout")}>
<div className="flex items-center gap-2 text-red-500">
<LogOut className="h-4 w-4" />
<span>Logout</span>
</div>
</Menu.MenuItem>
</Menu>
);
},
};
export const CustomStyles: Story = {
render() {
return (
<Menu
label="Styled Menu"
buttonClassName="bg-purple-500 text-white hover:bg-purple-600"
optionsClassName="bg-purple-50 border-purple-300"
>
<Menu.MenuItem onClick={() => alert("Item 1")} className="hover:bg-purple-200">
Item 1
</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Item 2")} className="hover:bg-purple-200">
Item 2
</Menu.MenuItem>
<Menu.MenuItem onClick={() => alert("Item 3")} className="hover:bg-purple-200">
Item 3
</Menu.MenuItem>
</Menu>
);
},
};

View File

@@ -1,33 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Pill, EPillVariant, EPillSize } from "./pill";
const meta: Meta<typeof Pill> = {
const meta = {
title: "Components/Pill",
component: Pill,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: Object.values(EPillVariant),
},
size: {
control: "select",
options: Object.values(EPillSize),
},
},
};
export default meta;
type Story = StoryObj<typeof Pill>;
export const Default: Story = {
args: {
children: "Default",
},
};
} satisfies Meta<typeof Pill>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Primary: Story = {
args: {
@@ -86,58 +75,66 @@ export const Large: Story = {
};
export const AllVariants: Story = {
render: () => (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Pill variant={EPillVariant.DEFAULT}>Default</Pill>
<Pill variant={EPillVariant.PRIMARY}>Primary</Pill>
<Pill variant={EPillVariant.SUCCESS}>Success</Pill>
<Pill variant={EPillVariant.WARNING}>Warning</Pill>
<Pill variant={EPillVariant.ERROR}>Error</Pill>
<Pill variant={EPillVariant.INFO}>Info</Pill>
render() {
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Pill variant={EPillVariant.DEFAULT}>Default</Pill>
<Pill variant={EPillVariant.PRIMARY}>Primary</Pill>
<Pill variant={EPillVariant.SUCCESS}>Success</Pill>
<Pill variant={EPillVariant.WARNING}>Warning</Pill>
<Pill variant={EPillVariant.ERROR}>Error</Pill>
<Pill variant={EPillVariant.INFO}>Info</Pill>
</div>
</div>
</div>
),
);
},
};
export const AllSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Pill size={EPillSize.SM}>Small</Pill>
<Pill size={EPillSize.MD}>Medium</Pill>
<Pill size={EPillSize.LG}>Large</Pill>
render() {
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Pill size={EPillSize.SM}>Small</Pill>
<Pill size={EPillSize.MD}>Medium</Pill>
<Pill size={EPillSize.LG}>Large</Pill>
</div>
</div>
</div>
),
);
},
};
export const WithNumbers: Story = {
render: () => (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Pill variant={EPillVariant.PRIMARY}>3</Pill>
<Pill variant={EPillVariant.SUCCESS}>12</Pill>
<Pill variant={EPillVariant.WARNING}>99+</Pill>
<Pill variant={EPillVariant.ERROR}>!</Pill>
render() {
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Pill variant={EPillVariant.PRIMARY}>3</Pill>
<Pill variant={EPillVariant.SUCCESS}>12</Pill>
<Pill variant={EPillVariant.WARNING}>99+</Pill>
<Pill variant={EPillVariant.ERROR}>!</Pill>
</div>
</div>
</div>
),
);
},
};
export const StatusExamples: Story = {
render: () => (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-sm font-medium">Task Status</h3>
<div className="flex flex-wrap gap-2">
<Pill variant={EPillVariant.DEFAULT}>Draft</Pill>
<Pill variant={EPillVariant.WARNING}>In Progress</Pill>
<Pill variant={EPillVariant.INFO}>In Review</Pill>
<Pill variant={EPillVariant.SUCCESS}>Completed</Pill>
<Pill variant={EPillVariant.ERROR}>Blocked</Pill>
render() {
return (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-sm font-medium">Task Status</h3>
<div className="flex flex-wrap gap-2">
<Pill variant={EPillVariant.DEFAULT}>Draft</Pill>
<Pill variant={EPillVariant.WARNING}>In Progress</Pill>
<Pill variant={EPillVariant.INFO}>In Review</Pill>
<Pill variant={EPillVariant.SUCCESS}>Completed</Pill>
<Pill variant={EPillVariant.ERROR}>Blocked</Pill>
</div>
</div>
</div>
</div>
),
);
},
};

View File

@@ -0,0 +1,311 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { X } from "lucide-react";
import { useArgs } from "storybook/preview-api";
import { Popover } from "./root";
// cannot use satifies here because base-ui does not have portable types.
const meta: Meta<typeof Popover> = {
title: "Components/Popover",
component: Popover,
subcomponents: {
PopoverButton: Popover.Button,
PopoverPanel: Popover.Panel,
},
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
children: null,
open: undefined,
onOpenChange: () => {},
},
render(args) {
const [{ open }, updateArgs] = useArgs();
const setOpen = (value: boolean | undefined) => updateArgs({ open: value });
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Popover
</Popover.Button>
<Popover.Panel className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Popover Title</h3>
<p className="mt-2 text-sm text-gray-600">This is the popover content. You can put any content here.</p>
</Popover.Panel>
</Popover>
);
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: null,
},
};
export const Controlled: Story = {
render() {
const [open, setOpen] = useState(false);
return (
<div className="space-y-4">
<div className="flex gap-2">
<button onClick={() => setOpen(true)} className="rounded bg-blue-500 px-3 py-1.5 text-sm text-white">
Open
</button>
<button onClick={() => setOpen(false)} className="rounded bg-gray-500 px-3 py-1.5 text-sm text-white">
Close
</button>
</div>
<Popover open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Controlled Popover
</Popover.Button>
<Popover.Panel className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<div className="flex items-start justify-between">
<h3 className="text-sm font-semibold">Controlled State</h3>
<button onClick={() => setOpen(false)} className="rounded-full p-1 hover:bg-gray-100">
<X className="h-4 w-4" />
</button>
</div>
<p className="mt-2 text-sm text-gray-600">Current state: {open ? "Open" : "Closed"}</p>
</Popover.Panel>
</Popover>
</div>
);
},
};
export const SideTop: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Above
</Popover.Button>
<Popover.Panel side="top" className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Top Positioned</h3>
<p className="mt-2 text-sm text-gray-600">This popover appears above the button.</p>
</Popover.Panel>
</Popover>
);
},
};
export const SideBottom: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Below
</Popover.Button>
<Popover.Panel side="bottom" className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Bottom Positioned</h3>
<p className="mt-2 text-sm text-gray-600">This popover appears below the button.</p>
</Popover.Panel>
</Popover>
);
},
};
export const SideLeft: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Left
</Popover.Button>
<Popover.Panel side="left" className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Left Positioned</h3>
<p className="mt-2 text-sm text-gray-600">This popover appears to the left of the button.</p>
</Popover.Panel>
</Popover>
);
},
};
export const SideRight: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Right
</Popover.Button>
<Popover.Panel side="right" className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Right Positioned</h3>
<p className="mt-2 text-sm text-gray-600">This popover appears to the right of the button.</p>
</Popover.Panel>
</Popover>
);
},
};
export const AlignStart: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Align Start
</Popover.Button>
<Popover.Panel align="start" className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Start Aligned</h3>
<p className="mt-2 text-sm text-gray-600">This popover is aligned to the start.</p>
</Popover.Panel>
</Popover>
);
},
};
export const AlignEnd: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Align End
</Popover.Button>
<Popover.Panel align="end" className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">End Aligned</h3>
<p className="mt-2 text-sm text-gray-600">This popover is aligned to the end.</p>
</Popover.Panel>
</Popover>
);
},
};
export const CustomOffset: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Custom Offset
</Popover.Button>
<Popover.Panel sideOffset={20} className="w-64 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Custom Side Offset</h3>
<p className="mt-2 text-sm text-gray-600">This popover has a custom side offset of 20px.</p>
</Popover.Panel>
</Popover>
);
},
};
export const WithForm: Story = {
render(args) {
const [open, setOpen] = useState(args.open ?? false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert("Form submitted!");
setOpen(false);
};
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Open Form
</Popover.Button>
<Popover.Panel className="w-72 rounded-lg border border-gray-200 bg-white p-4 shadow-lg">
<h3 className="text-sm font-semibold">Quick Form</h3>
<form onSubmit={handleSubmit} className="mt-3 space-y-3">
<div>
<label htmlFor="name" className="block text-xs font-medium text-gray-700">
Name
</label>
<input
type="text"
id="name"
className="mt-1 w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
placeholder="Enter name"
/>
</div>
<div>
<label htmlFor="email" className="block text-xs font-medium text-gray-700">
Email
</label>
<input
type="email"
id="email"
className="mt-1 w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
placeholder="Enter email"
/>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setOpen(false)}
className="rounded bg-gray-200 px-3 py-1.5 text-xs hover:bg-gray-300"
>
Cancel
</button>
<button type="submit" className="rounded bg-blue-500 px-3 py-1.5 text-xs text-white hover:bg-blue-600">
Submit
</button>
</div>
</form>
</Popover.Panel>
</Popover>
);
},
};
export const WithList: Story = {
render(args) {
const [open, setOpen] = useState(args.open);
return (
<Popover {...args} open={open} onOpenChange={setOpen}>
<Popover.Button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Show Options
</Popover.Button>
<Popover.Panel className="w-56 rounded-lg border border-gray-200 bg-white shadow-lg">
<div className="p-2">
<h3 className="px-2 py-1.5 text-xs font-semibold text-gray-500">Options</h3>
<button className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-gray-100">Option 1</button>
<button className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-gray-100">Option 2</button>
<button className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-gray-100">Option 3</button>
</div>
</Popover.Panel>
</Popover>
);
},
};
export const ColorPicker: Story = {
render() {
const [selectedColor, setSelectedColor] = useState("#3b82f6");
const colors = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#8b5cf6", "#ec4899", "#6b7280", "#000000", "#ffffff"];
return (
<Popover>
<Popover.Button className="flex items-center gap-2 rounded border border-gray-300 bg-white px-4 py-2 hover:bg-gray-50">
<div className="h-4 w-4 rounded" style={{ backgroundColor: selectedColor }} />
<span className="text-sm">Pick Color</span>
</Popover.Button>
<Popover.Panel className="w-48 rounded-lg border border-gray-200 bg-white p-3 shadow-lg">
<h3 className="mb-2 text-xs font-semibold">Select Color</h3>
<div className="grid grid-cols-5 gap-2">
{colors.map((color) => (
<button
key={color}
onClick={() => setSelectedColor(color)}
className="h-8 w-8 rounded border-2 transition-transform hover:scale-110"
style={{
backgroundColor: color,
borderColor: selectedColor === color ? "#000" : "transparent",
}}
/>
))}
</div>
</Popover.Panel>
</Popover>
);
},
};

View File

@@ -1,10 +1,11 @@
import React, { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button, EButtonVariant, EButtonSize } from "../button/button";
import { Button } from "../button/button";
import type { TButtonVariant } from "../button/helper";
import { EPortalWidth, EPortalPosition } from "./constants";
import { ModalPortal, PortalWrapper } from "./";
const meta: Meta<typeof ModalPortal> = {
const meta = {
title: "Components/Portal/ModalPortal",
component: ModalPortal,
parameters: {
@@ -19,48 +20,34 @@ Perfect for modals, drawers, overlays, and any UI that needs to appear above oth
},
},
tags: ["autodocs"],
argTypes: {
width: {
control: "select",
options: Object.values(EPortalWidth),
description: "Modal width preset",
},
position: {
control: "select",
options: Object.values(EPortalPosition),
description: "Modal position on screen",
},
fullScreen: {
control: "boolean",
description: "Render modal in fullscreen mode",
},
showOverlay: {
control: "boolean",
description: "Show/hide background overlay",
},
closeOnOverlayClick: {
control: "boolean",
description: "Close modal when clicking overlay",
},
closeOnEscape: {
control: "boolean",
description: "Close modal when pressing Escape",
},
args: {
isOpen: false,
children: null,
},
};
render(args) {
return (
<ModalDemo {...args} buttonText="Open Modal">
<ModalContent
title="Default Modal"
description="A standard modal with all default settings. Demonstrates focus management, keyboard navigation, and accessibility features."
/>
</ModalDemo>
);
},
} satisfies Meta<typeof ModalPortal>;
export default meta;
type Story = StoryObj<typeof ModalPortal>;
type Story = StoryObj<typeof meta>;
// Helper component for interactive stories
const ModalDemo = ({
children,
buttonText = "Open Modal",
buttonVariant = EButtonVariant.PRIMARY,
buttonVariant = "primary",
...modalProps
}: Omit<Parameters<typeof ModalPortal>[0], "isOpen" | "onClose"> & {
buttonText?: string;
buttonVariant?: Parameters<typeof Button>[0]["variant"];
buttonVariant?: TButtonVariant;
}) => {
const [isOpen, setIsOpen] = useState(false);
@@ -94,7 +81,7 @@ const ModalContent = ({
<p className="text-sm text-gray-500 mt-1">Modal demonstration</p>
</div>
{showCloseButton && onClose && (
<Button variant={EButtonVariant.GHOST} size={EButtonSize.SM} onClick={onClose} aria-label="Close modal">
<Button variant="link-neutral" size="sm" onClick={onClose} aria-label="Close modal">
</Button>
)}
@@ -115,27 +102,18 @@ const ModalContent = ({
</div>
);
export const Default: Story = {
render: () => (
<ModalDemo buttonText="Open Modal">
<ModalContent
title="Default Modal"
description="A standard modal with all default settings. Demonstrates focus management, keyboard navigation, and accessibility features."
/>
</ModalDemo>
),
};
export const Default: Story = {};
export const Positions: Story = {
name: "Different Positions",
render: () => {
render() {
const [activeModal, setActiveModal] = useState<EPortalPosition | null>(null);
return (
<div className="flex gap-3">
{Object.values(EPortalPosition).map((position) => (
<React.Fragment key={position}>
<Button variant={EButtonVariant.OUTLINE} onClick={() => setActiveModal(position)}>
<Button variant="outline-primary" onClick={() => setActiveModal(position)}>
{position.charAt(0).toUpperCase() + position.slice(1)}
</Button>
<ModalPortal
@@ -159,14 +137,14 @@ export const Positions: Story = {
export const Widths: Story = {
name: "Different Widths",
render: () => {
render() {
const [activeModal, setActiveModal] = useState<EPortalWidth | null>(null);
return (
<div className="flex gap-3">
{Object.values(EPortalWidth).map((width) => (
<React.Fragment key={width}>
<Button variant={EButtonVariant.SECONDARY} onClick={() => setActiveModal(width)}>
<Button variant="neutral-primary" onClick={() => setActiveModal(width)}>
{width.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase())}
</Button>
<ModalPortal
@@ -188,10 +166,19 @@ export const Widths: Story = {
},
};
// PortalWrapper Stories
const PortalWrapperMeta: Meta<typeof PortalWrapper> = {
title: "Components/Portal/PortalWrapper",
component: PortalWrapper,
export const BasicPortal: Story = {
render() {
return (
<div className="relative">
<p>This content renders in the normal document flow.</p>
<PortalWrapper portalId="storybook-portal">
<div className="fixed top-4 right-4 p-4 bg-blue-500 text-white rounded shadow-lg z-50">
This content is rendered in a portal!
</div>
</PortalWrapper>
</div>
);
},
parameters: {
layout: "centered",
docs: {
@@ -202,21 +189,4 @@ It's used internally by ModalPortal but can also be used directly for custom por
},
},
},
tags: ["autodocs"],
};
export const BasicPortal: StoryObj<typeof PortalWrapper> = {
render: () => (
<div className="relative">
<p>This content renders in the normal document flow.</p>
<PortalWrapper portalId="storybook-portal">
<div className="fixed top-4 right-4 p-4 bg-blue-500 text-white rounded shadow-lg z-50">
This content is rendered in a portal!
</div>
</PortalWrapper>
</div>
),
parameters: {
...PortalWrapperMeta.parameters,
},
};

View File

@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ScrollArea } from "./scrollarea";
const meta: Meta<typeof ScrollArea> = {
const meta = {
title: "Components/ScrollArea",
component: ScrollArea,
parameters: {
@@ -13,88 +13,291 @@ const meta: Meta<typeof ScrollArea> = {
},
},
},
argTypes: {
orientation: {
control: { type: "select" },
options: ["vertical", "horizontal"],
description: "Orientation of the scrollbar",
table: {
type: { summary: "ScrollAreaOrientation" },
defaultValue: { summary: "vertical" },
},
},
size: {
control: { type: "select" },
options: ["sm", "md", "lg"],
description: "Size variant of the scrollbar",
table: {
type: { summary: "ScrollAreaSize" },
defaultValue: { summary: "md" },
},
},
scrollType: {
control: { type: "select" },
options: ["always", "scroll", "hover"],
description: "When to show the scrollbar",
table: {
type: { summary: "ScrollAreaScrollType" },
defaultValue: { summary: "always" },
},
},
className: {
control: { type: "text" },
description: "Additional CSS classes",
},
},
};
export default meta;
type Story = StoryObj<typeof ScrollArea>;
// Sample content components for stories
const LongTextContent = () => (
<div className="p-4 space-y-4">
<h3 className="text-lg font-semibold">Long Text Content</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
</p>
<p>
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores
eos qui ratione voluptatem sequi nesciunt.
</p>
<p>
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam
eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.
</p>
<p>
Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea
commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae
consequatur.
</p>
</div>
);
// Default story
export const Default: Story = {
args: {
className: "h-64 w-80 border rounded-lg",
size: "md",
scrollType: "always",
orientation: "vertical",
},
render: (args) => (
<ScrollArea {...args}>
<LongTextContent />
</ScrollArea>
),
} satisfies Meta<typeof ScrollArea>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render(args) {
return (
<ScrollArea {...args} className="h-64 w-80 border rounded-lg">
<div className="p-4 space-y-4">
<h3 className="text-lg font-semibold">Long Text Content</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
</p>
<p>
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni
dolores eos qui ratione voluptatem sequi nesciunt.
</p>
</div>
</ScrollArea>
);
},
};
export const Sizes: Story = {
render() {
const content = (
<div className="p-4 space-y-2">
{[...Array(10)].map((_, i) => (
<p key={i}>Line {i + 1}: This is some scrollable content to demonstrate different sizes.</p>
))}
</div>
);
return (
<div className="flex flex-col gap-6">
<div className="space-y-2">
<div className="text-sm font-medium">Small</div>
<ScrollArea className="h-48 w-80 border rounded-lg" size="sm">
{content}
</ScrollArea>
</div>
<div className="space-y-2">
<div className="text-sm font-medium">Medium</div>
<ScrollArea className="h-48 w-80 border rounded-lg" size="md">
{content}
</ScrollArea>
</div>
<div className="space-y-2">
<div className="text-sm font-medium">Large</div>
<ScrollArea className="h-48 w-80 border rounded-lg" size="lg">
{content}
</ScrollArea>
</div>
</div>
);
},
};
export const ScrollTypeAlways: Story = {
render() {
return (
<ScrollArea className="h-64 w-80 border rounded-lg" scrollType="always">
<div className="p-4 space-y-2">
<h3 className="text-lg font-semibold">Always Visible Scrollbar</h3>
{[...Array(15)].map((_, i) => (
<p key={i}>Line {i + 1}: The scrollbar is always visible.</p>
))}
</div>
</ScrollArea>
);
},
};
export const ScrollTypeScroll: Story = {
render() {
return (
<ScrollArea className="h-64 w-80 border rounded-lg" scrollType="scroll">
<div className="p-4 space-y-2">
<h3 className="text-lg font-semibold">Scroll to Show</h3>
<p className="text-sm text-custom-text-400">Scrollbar appears when scrolling</p>
{[...Array(15)].map((_, i) => (
<p key={i}>Line {i + 1}: Try scrolling to see the scrollbar appear.</p>
))}
</div>
</ScrollArea>
);
},
};
export const ScrollTypeHover: Story = {
render() {
return (
<ScrollArea className="h-64 w-80 border rounded-lg" scrollType="hover">
<div className="p-4 space-y-2">
<h3 className="text-lg font-semibold">Hover to Show</h3>
<p className="text-sm text-custom-text-400">Scrollbar appears on hover</p>
{[...Array(15)].map((_, i) => (
<p key={i}>Line {i + 1}: Hover over the area to see the scrollbar.</p>
))}
</div>
</ScrollArea>
);
},
};
export const HorizontalScroll: Story = {
render() {
return (
<ScrollArea className="h-32 w-96 border rounded-lg" orientation="horizontal">
<div className="flex gap-4 p-4 w-[1200px]">
{[...Array(12)].map((_, i) => (
<div
key={i}
className="flex-shrink-0 w-32 h-20 bg-custom-background-80 rounded flex items-center justify-center"
>
Item {i + 1}
</div>
))}
</div>
</ScrollArea>
);
},
};
export const BothDirections: Story = {
render() {
return (
<ScrollArea className="h-64 w-96 border rounded-lg">
<div className="w-[800px] p-4 space-y-2">
<h3 className="text-lg font-semibold">Both Directions</h3>
<p className="text-sm text-custom-text-400">Content scrolls both vertically and horizontally</p>
{[...Array(20)].map((_, i) => (
<p key={i}>
Line {i + 1}: This line is very long and extends beyond the container width to demonstrate horizontal
scrolling along with vertical scrolling.
</p>
))}
</div>
</ScrollArea>
);
},
};
export const ListExample: Story = {
render() {
return (
<ScrollArea className="h-80 w-96 border rounded-lg">
<div className="p-4">
<h3 className="text-lg font-semibold mb-4">User List</h3>
<div className="space-y-2">
{[...Array(25)].map((_, i) => (
<div
key={i}
className="flex items-center gap-3 p-3 bg-custom-background-80 rounded hover:bg-custom-background-90 cursor-pointer"
>
<div className="w-10 h-10 rounded-full bg-custom-primary-100 flex items-center justify-center text-white font-medium">
{String.fromCharCode(65 + (i % 26))}
</div>
<div>
<div className="font-medium">User {i + 1}</div>
<div className="text-sm text-custom-text-400">user{i + 1}@example.com</div>
</div>
</div>
))}
</div>
</div>
</ScrollArea>
);
},
};
export const CodeBlock: Story = {
render() {
const code = `function fibonacci(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
const temp = a + b;
a = b;
b = temp;
}
return b;
}
// Example usage
console.log(fibonacci(10)); // 55
console.log(fibonacci(20)); // 6765
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}`;
return (
<ScrollArea className="h-96 w-full max-w-2xl border rounded-lg bg-custom-background-100">
<pre className="p-4 text-sm">
<code>{code}</code>
</pre>
</ScrollArea>
);
},
};
export const ChatMessages: Story = {
render() {
return (
<ScrollArea className="h-96 w-full max-w-md border rounded-lg">
<div className="p-4 space-y-4">
{[...Array(20)].map((_, i) => (
<div key={i} className={`flex ${i % 3 === 0 ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[70%] p-3 rounded-lg ${
i % 3 === 0 ? "bg-custom-primary-100 text-white" : "bg-custom-background-80"
}`}
>
<div className="text-sm">{i % 3 === 0 ? "You" : `User ${i + 1}`}</div>
<div className="mt-1">Message content for message number {i + 1}</div>
</div>
</div>
))}
</div>
</ScrollArea>
);
},
};
export const DataTable: Story = {
render() {
return (
<ScrollArea className="h-96 w-full max-w-3xl border rounded-lg">
<table className="w-full">
<thead className="bg-custom-background-80 sticky top-0">
<tr>
<th className="px-4 py-2 text-left">ID</th>
<th className="px-4 py-2 text-left">Name</th>
<th className="px-4 py-2 text-left">Email</th>
<th className="px-4 py-2 text-left">Status</th>
</tr>
</thead>
<tbody>
{[...Array(50)].map((_, i) => (
<tr key={i} className="border-t border-custom-border-200 hover:bg-custom-background-80">
<td className="px-4 py-2">#{i + 1}</td>
<td className="px-4 py-2">User {i + 1}</td>
<td className="px-4 py-2">user{i + 1}@example.com</td>
<td className="px-4 py-2">
<span
className={`px-2 py-1 rounded text-xs ${i % 3 === 0 ? "bg-green-500/20 text-green-500" : "bg-gray-500/20 text-gray-500"}`}
>
{i % 3 === 0 ? "Active" : "Inactive"}
</span>
</td>
</tr>
))}
</tbody>
</table>
</ScrollArea>
);
},
};

View File

@@ -1,50 +1,56 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Separator } from "./separator";
const meta: Meta<typeof Separator> = {
const meta = {
title: "Components/Separator",
component: Separator,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
};
} satisfies Meta<typeof Separator>;
export default meta;
type Story = StoryObj<typeof Separator>;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<div className="w-[300px] space-y-4">
<div>Content Above</div>
<Separator />
<div>Content Below</div>
</div>
),
render() {
return (
<div className="w-[300px] space-y-4">
<div>Content Above</div>
<Separator />
<div>Content Below</div>
</div>
);
},
};
export const Vertical: Story = {
render: () => (
<div className="flex h-[100px] items-center space-x-4">
<div>Left Content</div>
<Separator orientation="vertical" />
<div>Right Content</div>
</div>
),
render() {
return (
<div className="flex h-[100px] items-center space-x-4">
<div>Left Content</div>
<Separator orientation="vertical" />
<div>Right Content</div>
</div>
);
},
};
export const WithinContainer: Story = {
render: () => (
<div className="w-[300px] rounded-lg border p-6 space-y-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Section 1</h4>
<p className="text-sm text-muted-foreground">Description for section 1</p>
render() {
return (
<div className="w-[300px] rounded-lg border p-6 space-y-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Section 1</h4>
<p className="text-sm text-muted-foreground">Description for section 1</p>
</div>
<Separator />
<div className="space-y-2">
<h4 className="font-medium leading-none">Section 2</h4>
<p className="text-sm text-muted-foreground">Description for section 2</p>
</div>
</div>
<Separator />
<div className="space-y-2">
<h4 className="font-medium leading-none">Section 2</h4>
<p className="text-sm text-muted-foreground">Description for section 2</p>
</div>
</div>
),
);
},
};

View File

@@ -1,33 +1,194 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Skeleton } from "./index";
const meta: Meta<typeof Skeleton> = {
const meta = {
title: "Components/Skeleton",
component: Skeleton,
parameters: {
layout: "centered",
},
};
args: {
children: null,
},
} satisfies Meta<typeof Skeleton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<Skeleton className="w-80 flex flex-col gap-2">
<Skeleton.Item height="40px" width="100%" />
</Skeleton>
),
render() {
return (
<Skeleton className="w-80 flex flex-col gap-2">
<Skeleton.Item height="40px" width="100%" />
</Skeleton>
);
},
};
export const Card: Story = {
render: () => (
<Skeleton className="w-80 flex flex-col gap-4">
<Skeleton.Item height="200px" width="100%" />
<div className="flex flex-col gap-2">
<Skeleton.Item height="20px" width="50%" />
<Skeleton.Item height="20px" width="30%" />
</div>
</Skeleton>
),
render() {
return (
<Skeleton className="w-80 flex flex-col gap-4">
<Skeleton.Item height="200px" width="100%" />
<div className="flex flex-col gap-2">
<Skeleton.Item height="20px" width="60%" />
<Skeleton.Item height="16px" width="40%" />
</div>
</Skeleton>
);
},
};
export const List: Story = {
render() {
return (
<Skeleton className="w-96 flex flex-col gap-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="flex gap-3">
<Skeleton.Item height="40px" width="40px" className="rounded-full" />
<div className="flex-1 flex flex-col gap-2">
<Skeleton.Item height="16px" width="70%" />
<Skeleton.Item height="12px" width="50%" />
</div>
</div>
))}
</Skeleton>
);
},
};
export const Table: Story = {
render() {
return (
<Skeleton className="w-full flex flex-col gap-3">
<div className="flex gap-4">
<Skeleton.Item height="20px" width="150px" />
<Skeleton.Item height="20px" width="200px" />
<Skeleton.Item height="20px" width="120px" />
</div>
{[...Array(5)].map((_, i) => (
<div key={i} className="flex gap-4">
<Skeleton.Item height="40px" width="150px" />
<Skeleton.Item height="40px" width="200px" />
<Skeleton.Item height="40px" width="120px" />
</div>
))}
</Skeleton>
);
},
};
export const Profile: Story = {
render() {
return (
<Skeleton className="w-80 flex flex-col gap-4">
<div className="flex items-center gap-4">
<Skeleton.Item height="80px" width="80px" className="rounded-full" />
<div className="flex-1 flex flex-col gap-2">
<Skeleton.Item height="20px" width="60%" />
<Skeleton.Item height="16px" width="40%" />
</div>
</div>
<div className="flex flex-col gap-2">
<Skeleton.Item height="16px" width="100%" />
<Skeleton.Item height="16px" width="90%" />
<Skeleton.Item height="16px" width="70%" />
</div>
</Skeleton>
);
},
};
export const Avatar: Story = {
render() {
return (
<Skeleton className="flex gap-2">
<Skeleton.Item height="40px" width="40px" className="rounded-full" />
</Skeleton>
);
},
};
export const AvatarGroup: Story = {
render() {
return (
<Skeleton className="flex -space-x-2">
{[...Array(4)].map((_, i) => (
<Skeleton.Item key={i} height="40px" width="40px" className="rounded-full border-2 border-white" />
))}
</Skeleton>
);
},
};
export const Text: Story = {
render() {
return (
<Skeleton className="w-96 flex flex-col gap-2">
<Skeleton.Item height="16px" width="100%" />
<Skeleton.Item height="16px" width="95%" />
<Skeleton.Item height="16px" width="90%" />
<Skeleton.Item height="16px" width="60%" />
</Skeleton>
);
},
};
export const Button: Story = {
render() {
return (
<Skeleton className="inline-flex">
<Skeleton.Item height="40px" width="120px" className="rounded-md" />
</Skeleton>
);
},
};
export const Input: Story = {
render() {
return (
<Skeleton className="w-80 flex flex-col gap-2">
<Skeleton.Item height="14px" width="80px" />
<Skeleton.Item height="40px" width="100%" className="rounded-md" />
</Skeleton>
);
},
};
export const Form: Story = {
render() {
return (
<Skeleton className="w-96 flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Skeleton.Item height="14px" width="80px" />
<Skeleton.Item height="40px" width="100%" className="rounded-md" />
</div>
<div className="flex flex-col gap-2">
<Skeleton.Item height="14px" width="100px" />
<Skeleton.Item height="40px" width="100%" className="rounded-md" />
</div>
<div className="flex flex-col gap-2">
<Skeleton.Item height="14px" width="60px" />
<Skeleton.Item height="80px" width="100%" className="rounded-md" />
</div>
<Skeleton.Item height="40px" width="120px" className="rounded-md" />
</Skeleton>
);
},
};
export const ProductCard: Story = {
render() {
return (
<Skeleton className="w-72 flex flex-col gap-3 p-4 border rounded-lg">
<Skeleton.Item height="200px" width="100%" className="rounded-md" />
<div className="flex flex-col gap-2">
<Skeleton.Item height="20px" width="80%" />
<Skeleton.Item height="16px" width="60%" />
<Skeleton.Item height="24px" width="40%" />
</div>
<Skeleton.Item height="40px" width="100%" className="rounded-md" />
</Skeleton>
);
},
};

View File

@@ -0,0 +1,136 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CircularBarSpinner } from "./circular-bar-spinner";
const meta = {
title: "Components/CircularBarSpinner",
component: CircularBarSpinner,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
height: "16px",
width: "16px",
},
} satisfies Meta<typeof CircularBarSpinner>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Small: Story = {
args: {
height: "12px",
width: "12px",
},
};
export const Medium: Story = {
args: {
height: "24px",
width: "24px",
},
};
export const Large: Story = {
args: {
height: "32px",
width: "32px",
},
};
export const ExtraLarge: Story = {
args: {
height: "48px",
width: "48px",
},
};
export const CustomColor: Story = {
args: {
className: "text-green-500",
},
};
export const AllSizes: Story = {
render() {
return (
<div className="flex items-center gap-6">
<div className="text-center">
<CircularBarSpinner height="12px" width="12px" />
<p className="mt-2 text-xs text-gray-600">Small</p>
</div>
<div className="text-center">
<CircularBarSpinner height="16px" width="16px" />
<p className="mt-2 text-xs text-gray-600">Default</p>
</div>
<div className="text-center">
<CircularBarSpinner height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Medium</p>
</div>
<div className="text-center">
<CircularBarSpinner height="32px" width="32px" />
<p className="mt-2 text-xs text-gray-600">Large</p>
</div>
<div className="text-center">
<CircularBarSpinner height="48px" width="48px" />
<p className="mt-2 text-xs text-gray-600">XL</p>
</div>
</div>
);
},
};
export const ColorVariations: Story = {
render() {
return (
<div className="flex items-center gap-6">
<div className="text-center">
<CircularBarSpinner className="text-blue-500" height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Blue</p>
</div>
<div className="text-center">
<CircularBarSpinner className="text-green-500" height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Green</p>
</div>
<div className="text-center">
<CircularBarSpinner className="text-red-500" height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Red</p>
</div>
<div className="text-center">
<CircularBarSpinner className="text-purple-500" height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Purple</p>
</div>
<div className="text-center">
<CircularBarSpinner className="text-orange-500" height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Orange</p>
</div>
</div>
);
},
};
export const InButton: Story = {
render() {
return (
<button className="flex items-center gap-2 rounded bg-green-500 px-4 py-2 text-white">
<CircularBarSpinner height="16px" width="16px" />
<span>Processing...</span>
</button>
);
},
};
export const CenteredInCard: Story = {
render() {
return (
<div className="w-96 rounded-lg border border-gray-200 bg-white p-8 shadow-md">
<div className="flex flex-col items-center justify-center space-y-4">
<CircularBarSpinner height="48px" width="48px" />
<p className="text-sm text-gray-600">Processing data...</p>
</div>
</div>
);
},
};

View File

@@ -0,0 +1,136 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Spinner } from "./circular-spinner";
const meta = {
title: "Components/Spinner",
component: Spinner,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
height: "32px",
width: "32px",
},
} satisfies Meta<typeof Spinner>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Small: Story = {
args: {
height: "16px",
width: "16px",
},
};
export const Medium: Story = {
args: {
height: "24px",
width: "24px",
},
};
export const Large: Story = {
args: {
height: "48px",
width: "48px",
},
};
export const ExtraLarge: Story = {
args: {
height: "64px",
width: "64px",
},
};
export const CustomColor: Story = {
args: {
className: "text-blue-500",
},
};
export const AllSizes: Story = {
render() {
return (
<div className="flex items-center gap-6">
<div className="text-center">
<Spinner height="16px" width="16px" />
<p className="mt-2 text-xs text-gray-600">Small</p>
</div>
<div className="text-center">
<Spinner height="24px" width="24px" />
<p className="mt-2 text-xs text-gray-600">Medium</p>
</div>
<div className="text-center">
<Spinner height="32px" width="32px" />
<p className="mt-2 text-xs text-gray-600">Default</p>
</div>
<div className="text-center">
<Spinner height="48px" width="48px" />
<p className="mt-2 text-xs text-gray-600">Large</p>
</div>
<div className="text-center">
<Spinner height="64px" width="64px" />
<p className="mt-2 text-xs text-gray-600">XL</p>
</div>
</div>
);
},
};
export const ColorVariations: Story = {
render() {
return (
<div className="flex items-center gap-6">
<div className="text-center">
<Spinner className="text-blue-500" />
<p className="mt-2 text-xs text-gray-600">Blue</p>
</div>
<div className="text-center">
<Spinner className="text-green-500" />
<p className="mt-2 text-xs text-gray-600">Green</p>
</div>
<div className="text-center">
<Spinner className="text-red-500" />
<p className="mt-2 text-xs text-gray-600">Red</p>
</div>
<div className="text-center">
<Spinner className="text-purple-500" />
<p className="mt-2 text-xs text-gray-600">Purple</p>
</div>
<div className="text-center">
<Spinner className="text-orange-500" />
<p className="mt-2 text-xs text-gray-600">Orange</p>
</div>
</div>
);
},
};
export const InButton: Story = {
render() {
return (
<button className="flex items-center gap-2 rounded bg-blue-500 px-4 py-2 text-white">
<Spinner height="16px" width="16px" />
<span>Loading...</span>
</button>
);
},
};
export const CenteredInCard: Story = {
render() {
return (
<div className="w-96 rounded-lg border border-gray-200 bg-white p-8 shadow-md">
<div className="flex flex-col items-center justify-center space-y-4">
<Spinner height="48px" width="48px" />
<p className="text-sm text-gray-600">Loading content...</p>
</div>
</div>
);
},
};

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { Switch as BaseSwitch } from "@base-ui-components/react/switch";
import { cn } from "../utils/classname";
interface IToggleSwitchProps {
export interface IToggleSwitchProps {
value: boolean;
onChange: (value: boolean) => void;
label?: string;

View File

@@ -0,0 +1,241 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useArgs } from "storybook/preview-api";
import { Switch } from "./root";
const meta = {
title: "Components/Switch",
component: Switch,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: { value: false, onChange: () => {} },
render(args) {
const [{ value }, updateArgs] = useArgs();
const setValue = (newValue: boolean) => updateArgs({ value: newValue });
return <Switch {...args} value={value} onChange={setValue} />;
},
} satisfies Meta<typeof Switch>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Checked: Story = {
args: { value: true },
};
export const WithLabel: Story = {
render(args) {
const [value, setValue] = useState(args.value);
return (
<div className="flex items-center gap-2">
<Switch {...args} value={value} onChange={setValue} label="Enable notifications" />
<label className="text-sm">Enable notifications</label>
</div>
);
},
};
export const Small: Story = {
args: { size: "sm" },
};
export const Medium: Story = {
args: { size: "md" },
};
export const Large: Story = {
args: { size: "lg" },
};
export const Disabled: Story = {
args: { disabled: true },
};
export const DisabledChecked: Story = {
args: { value: true, disabled: true },
};
export const AllSizes: Story = {
render() {
const [small, setSmall] = useState(false);
const [medium, setMedium] = useState(false);
const [large, setLarge] = useState(false);
return (
<div className="flex items-center gap-6">
<div className="text-center">
<Switch value={small} onChange={setSmall} size="sm" />
<p className="mt-2 text-xs text-gray-600">Small</p>
</div>
<div className="text-center">
<Switch value={medium} onChange={setMedium} size="md" />
<p className="mt-2 text-xs text-gray-600">Medium</p>
</div>
<div className="text-center">
<Switch value={large} onChange={setLarge} size="lg" />
<p className="mt-2 text-xs text-gray-600">Large</p>
</div>
</div>
);
},
};
export const AllStates: Story = {
render() {
const [unchecked, setUnchecked] = useState(false);
const [checked, setChecked] = useState(true);
const [disabledUnchecked] = useState(false);
const [disabledChecked] = useState(true);
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
<Switch value={unchecked} onChange={setUnchecked} />
<span className="text-sm text-gray-600">Unchecked</span>
</div>
<div className="flex items-center gap-4">
<Switch value={checked} onChange={setChecked} />
<span className="text-sm text-gray-600">Checked</span>
</div>
<div className="flex items-center gap-4">
<Switch value={disabledUnchecked} onChange={() => {}} disabled />
<span className="text-sm text-gray-600">Disabled Unchecked</span>
</div>
<div className="flex items-center gap-4">
<Switch value={disabledChecked} onChange={() => {}} disabled />
<span className="text-sm text-gray-600">Disabled Checked</span>
</div>
</div>
);
},
};
export const InForm: Story = {
render() {
const [notifications, setNotifications] = useState(true);
const [marketing, setMarketing] = useState(false);
const [updates, setUpdates] = useState(true);
return (
<div className="w-80 rounded-lg border border-gray-200 bg-white p-6 shadow-md">
<h3 className="text-lg font-semibold">Notification Settings</h3>
<div className="mt-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Push Notifications</p>
<p className="text-xs text-gray-500">Receive push notifications on your device</p>
</div>
<Switch value={notifications} onChange={setNotifications} size="md" />
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Marketing Emails</p>
<p className="text-xs text-gray-500">Receive emails about new features</p>
</div>
<Switch value={marketing} onChange={setMarketing} size="md" />
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Product Updates</p>
<p className="text-xs text-gray-500">Get notified about product updates</p>
</div>
<Switch value={updates} onChange={setUpdates} size="md" />
</div>
</div>
</div>
);
},
};
export const WithDescription: Story = {
render() {
const [enabled, setEnabled] = useState(false);
return (
<div className="w-96 rounded-lg border border-gray-200 bg-white p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="text-sm font-semibold">Enable Two-Factor Authentication</h4>
<p className="mt-1 text-xs text-gray-500">
Add an extra layer of security to your account by enabling two-factor authentication.
</p>
</div>
<Switch value={enabled} onChange={setEnabled} size="md" className="ml-4" />
</div>
</div>
);
},
};
export const Interactive: Story = {
render() {
const [enabled, setEnabled] = useState(false);
return (
<div className="w-80 space-y-4 rounded-lg border border-gray-200 bg-white p-6">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Feature Toggle</span>
<Switch value={enabled} onChange={setEnabled} size="md" />
</div>
<div className="rounded bg-gray-50 p-4">
<p className="text-sm text-gray-700">
Status: <span className="font-semibold">{enabled ? "Enabled" : "Disabled"}</span>
</p>
{enabled && <p className="mt-2 text-xs text-green-600">Feature is now active and ready to use!</p>}
</div>
</div>
);
},
};
export const CustomStyles: Story = {
render() {
const [value, setValue] = useState(false);
return (
<div className="flex items-center gap-4">
<Switch
value={value}
onChange={setValue}
size="lg"
className="border-2 border-purple-300 data-[state=checked]:bg-purple-500"
/>
<span className="text-sm">Custom styled switch</span>
</div>
);
},
};
export const MultipleControls: Story = {
render() {
const [settings, setSettings] = useState({
feature1: true,
feature2: false,
feature3: true,
feature4: false,
feature5: true,
});
const toggleSetting = (key: keyof typeof settings) => {
setSettings((prev) => ({ ...prev, [key]: !prev[key] }));
};
return (
<div className="w-96 rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Feature Flags</h3>
<div className="space-y-3">
{Object.entries(settings).map(([key, value]) => (
<div key={key} className="flex items-center justify-between">
<span className="text-sm capitalize">{key.replace(/([A-Z])/g, " $1").trim()}</span>
<Switch value={value} onChange={() => toggleSetting(key as keyof typeof settings)} size="sm" />
</div>
))}
</div>
</div>
);
},
};

View File

@@ -0,0 +1,376 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } from "./core";
const meta = {
title: "Components/Table",
component: Table,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof Table>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>John Doe</TableCell>
<TableCell>john@example.com</TableCell>
<TableCell>Admin</TableCell>
</TableRow>
<TableRow>
<TableCell>Jane Smith</TableCell>
<TableCell>jane@example.com</TableCell>
<TableCell>User</TableCell>
</TableRow>
<TableRow>
<TableCell>Bob Wilson</TableCell>
<TableCell>bob@example.com</TableCell>
<TableCell>Moderator</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const WithCaption: Story = {
render() {
return (
<Table>
<TableCaption>A list of recent users</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Alice Johnson</TableCell>
<TableCell>alice@example.com</TableCell>
<TableCell>Active</TableCell>
</TableRow>
<TableRow>
<TableCell>Charlie Brown</TableCell>
<TableCell>charlie@example.com</TableCell>
<TableCell>Inactive</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const WithFooter: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead className="text-right">Quantity</TableHead>
<TableHead className="text-right">Price</TableHead>
<TableHead className="text-right">Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Product A</TableCell>
<TableCell className="text-right">2</TableCell>
<TableCell className="text-right">$10.00</TableCell>
<TableCell className="text-right">$20.00</TableCell>
</TableRow>
<TableRow>
<TableCell>Product B</TableCell>
<TableCell className="text-right">1</TableCell>
<TableCell className="text-right">$25.00</TableCell>
<TableCell className="text-right">$25.00</TableCell>
</TableRow>
<TableRow>
<TableCell>Product C</TableCell>
<TableCell className="text-right">3</TableCell>
<TableCell className="text-right">$15.00</TableCell>
<TableCell className="text-right">$45.00</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3} className="font-semibold">
Total
</TableCell>
<TableCell className="text-right font-semibold">$90.00</TableCell>
</TableRow>
</TableFooter>
</Table>
);
},
};
export const WithActions: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>John Doe</TableCell>
<TableCell>john@example.com</TableCell>
<TableCell>Admin</TableCell>
<TableCell className="text-right">
<button className="mr-2 text-blue-500 hover:underline">Edit</button>
<button className="text-red-500 hover:underline">Delete</button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Jane Smith</TableCell>
<TableCell>jane@example.com</TableCell>
<TableCell>User</TableCell>
<TableCell className="text-right">
<button className="mr-2 text-blue-500 hover:underline">Edit</button>
<button className="text-red-500 hover:underline">Delete</button>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const WithBadges: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Status</TableHead>
<TableHead>Priority</TableHead>
<TableHead>Assignee</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Website Redesign</TableCell>
<TableCell>
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800">In Progress</span>
</TableCell>
<TableCell>
<span className="rounded-full bg-red-100 px-2 py-1 text-xs text-red-800">High</span>
</TableCell>
<TableCell>John Doe</TableCell>
</TableRow>
<TableRow>
<TableCell>Mobile App</TableCell>
<TableCell>
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs text-blue-800">Planned</span>
</TableCell>
<TableCell>
<span className="rounded-full bg-yellow-100 px-2 py-1 text-xs text-yellow-800">Medium</span>
</TableCell>
<TableCell>Jane Smith</TableCell>
</TableRow>
<TableRow>
<TableCell>API Integration</TableCell>
<TableCell>
<span className="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-800">Completed</span>
</TableCell>
<TableCell>
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800">Low</span>
</TableCell>
<TableCell>Bob Wilson</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const WithCheckboxes: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>
<input type="checkbox" className="h-4 w-4" aria-label="Select all rows" />
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<input type="checkbox" className="h-4 w-4" aria-label="Select row for John Doe" />
</TableCell>
<TableCell>John Doe</TableCell>
<TableCell>john@example.com</TableCell>
<TableCell>Admin</TableCell>
</TableRow>
<TableRow>
<TableCell>
<input type="checkbox" className="h-4 w-4" aria-label="Select row for Jane Smith" />
</TableCell>
<TableCell>Jane Smith</TableCell>
<TableCell>jane@example.com</TableCell>
<TableCell>User</TableCell>
</TableRow>
<TableRow>
<TableCell>
<input type="checkbox" className="h-4 w-4" aria-label="Select row for Bob Wilson" />
</TableCell>
<TableCell>Bob Wilson</TableCell>
<TableCell>bob@example.com</TableCell>
<TableCell>Moderator</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const EmptyState: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={3} className="h-24 text-center">
No results found.
</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const LargeDataset: Story = {
render() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Department</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 15 }, (_, i) => (
<TableRow key={i}>
<TableCell>{1000 + i}</TableCell>
<TableCell>User {i + 1}</TableCell>
<TableCell>user{i + 1}@example.com</TableCell>
<TableCell>{["Engineering", "Sales", "Marketing", "Support"][i % 4]}</TableCell>
<TableCell>
<span
className={`rounded-full px-2 py-1 text-xs ${
i % 2 === 0 ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
}`}
>
{i % 2 === 0 ? "Active" : "Inactive"}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
},
};
export const CustomStyling: Story = {
render() {
return (
<Table className="border-2 border-blue-200">
<TableHeader>
<TableRow className="bg-blue-100">
<TableHead className="text-blue-900">Name</TableHead>
<TableHead className="text-blue-900">Email</TableHead>
<TableHead className="text-blue-900">Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow className="hover:bg-blue-50">
<TableCell className="font-semibold">John Doe</TableCell>
<TableCell>john@example.com</TableCell>
<TableCell>Admin</TableCell>
</TableRow>
<TableRow className="hover:bg-blue-50">
<TableCell className="font-semibold">Jane Smith</TableCell>
<TableCell>jane@example.com</TableCell>
<TableCell>User</TableCell>
</TableRow>
</TableBody>
</Table>
);
},
};
export const ResponsiveTable: Story = {
render() {
return (
<div className="w-full max-w-4xl">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="hidden sm:table-cell">Email</TableHead>
<TableHead className="hidden md:table-cell">Department</TableHead>
<TableHead className="hidden lg:table-cell">Location</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>John Doe</TableCell>
<TableCell className="hidden sm:table-cell">john@example.com</TableCell>
<TableCell className="hidden md:table-cell">Engineering</TableCell>
<TableCell className="hidden lg:table-cell">New York</TableCell>
<TableCell>Active</TableCell>
</TableRow>
<TableRow>
<TableCell>Jane Smith</TableCell>
<TableCell className="hidden sm:table-cell">jane@example.com</TableCell>
<TableCell className="hidden md:table-cell">Marketing</TableCell>
<TableCell className="hidden lg:table-cell">San Francisco</TableCell>
<TableCell>Active</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
);
},
};

View File

@@ -1,5 +1,6 @@
import { ComponentProps } from "react";
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Home, Settings, User, Bell } from "lucide-react";
import { Tabs } from "./tabs";
type TabOption = {
@@ -10,21 +11,24 @@ type TabOption = {
const tabOptions: TabOption[] = [
{ label: "Account", value: "account" },
{ label: "Password", value: "password" },
{ label: "Notifications", value: "notifications" },
];
interface StoryProps extends ComponentProps<typeof Tabs> {
options: TabOption[];
}
const meta: Meta<StoryProps> = {
// cannot use satisfies here because base-ui does not have portable types.
const meta: Meta<typeof Tabs> = {
title: "Components/Tabs",
component: Tabs,
subcomponents: {
TabsList: Tabs.List,
TabsTrigger: Tabs.Trigger,
TabsContent: Tabs.Content,
TabsIndicator: Tabs.Indicator,
},
parameters: {
layout: "centered",
},
args: {
defaultValue: "account",
options: tabOptions,
},
};
@@ -32,35 +36,24 @@ export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
options: [
{
label: "Account",
value: "account",
},
{
label: "Password",
value: "password",
},
],
},
render: ({ defaultValue, options }) => {
const safeDefault = options?.some((o) => o.value === defaultValue) ? defaultValue : options?.[0]?.value;
render({ defaultValue }) {
return (
<div className="w-[400px]">
<Tabs defaultValue={safeDefault}>
<Tabs defaultValue={defaultValue}>
<Tabs.List>
{options.map((option) => (
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{options.map((option) => (
{tabOptions.map((option) => (
<Tabs.Content key={option.value} value={option.value} className="p-4">
{option.label} content goes here
<div className="text-sm">
<h3 className="font-medium mb-2">{option.label}</h3>
<p className="text-custom-text-300">Content for the {option.label.toLowerCase()} tab.</p>
</div>
</Tabs.Content>
))}
</Tabs>
@@ -70,7 +63,7 @@ export const Basic: Story = {
};
export const Sizes: Story = {
render: ({ defaultValue, options }) => {
render({ defaultValue }) {
const sizes = ["sm", "md", "lg"] as const;
const sizeLabels: Record<(typeof sizes)[number], string> = {
sm: "Small",
@@ -81,14 +74,15 @@ export const Sizes: Story = {
<div className="w-[400px] grid gap-4">
{sizes.map((size) => (
<div key={size} className="flex flex-col gap-2">
<div className="text-lg">{sizeLabels[size]}</div>
<div className="text-sm font-medium">{sizeLabels[size]}</div>
<Tabs defaultValue={defaultValue}>
<Tabs.List>
{options.map((option) => (
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value} size={size}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
</Tabs>
</div>
@@ -97,3 +91,268 @@ export const Sizes: Story = {
);
},
};
export const Controlled: Story = {
render() {
const [activeTab, setActiveTab] = useState("account");
return (
<div className="w-[400px]">
<div className="mb-4 text-sm">
Active tab: <span className="font-medium">{activeTab}</span>
</div>
<Tabs value={activeTab} onValueChange={(value) => value && setActiveTab(value)}>
<Tabs.List>
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabOptions.map((option) => (
<Tabs.Content key={option.value} value={option.value} className="p-4">
<div className="text-sm">Content for {option.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const DisabledTab: Story = {
render({ defaultValue }) {
return (
<div className="w-[400px]">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="password" disabled>
Password
</Tabs.Trigger>
<Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content value="account" className="p-4">
<div className="text-sm">Account content</div>
</Tabs.Content>
<Tabs.Content value="password" className="p-4">
<div className="text-sm">Password content (disabled)</div>
</Tabs.Content>
<Tabs.Content value="notifications" className="p-4">
<div className="text-sm">Notifications content</div>
</Tabs.Content>
</Tabs>
</div>
);
},
};
export const WithIcons: Story = {
render({ defaultValue }) {
const tabsWithIcons = [
{ label: "Home", value: "home", icon: Home },
{ label: "Profile", value: "profile", icon: User },
{ label: "Settings", value: "settings", icon: Settings },
{ label: "Notifications", value: "notifications", icon: Bell },
];
return (
<div className="w-[500px]">
<Tabs defaultValue={defaultValue || "home"}>
<Tabs.List>
{tabsWithIcons.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}>
<tab.icon className="w-4 h-4 mr-2" />
{tab.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabsWithIcons.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4">
<div className="text-sm">Content for {tab.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const IconsOnly: Story = {
render({ defaultValue }) {
const iconTabs = [
{ value: "home", icon: Home },
{ value: "profile", icon: User },
{ value: "settings", icon: Settings },
{ value: "notifications", icon: Bell },
];
return (
<div className="w-[300px]">
<Tabs defaultValue={defaultValue || "home"}>
<Tabs.List>
{iconTabs.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}>
<tab.icon className="w-4 h-4" />
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{iconTabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4">
<div className="text-sm">Content for {tab.value}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const DynamicTabs: Story = {
render() {
const [tabs, setTabs] = useState([
{ label: "Tab 1", value: "tab1" },
{ label: "Tab 2", value: "tab2" },
]);
const [activeTab, setActiveTab] = useState("tab1");
const addTab = () => {
const newTabNum = tabs.length + 1;
setTabs([...tabs, { label: `Tab ${newTabNum}`, value: `tab${newTabNum}` }]);
};
const removeTab = (valueToRemove: string) => {
const newTabs = tabs.filter((tab) => tab.value !== valueToRemove);
setTabs(newTabs);
if (activeTab === valueToRemove && newTabs.length > 0) {
setActiveTab(newTabs[0].value);
}
};
return (
<div className="w-[500px]">
<div className="mb-4">
<button
onClick={addTab}
className="px-3 py-1.5 text-sm bg-custom-background-80 rounded hover:bg-custom-background-90"
>
Add Tab
</button>
</div>
<Tabs value={activeTab} onValueChange={(value) => value && setActiveTab(value)}>
<Tabs.List>
{tabs.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}>
{tab.label}
{tabs.length > 1 && (
<button
onClick={(e) => {
e.stopPropagation();
removeTab(tab.value);
}}
className="ml-2 hover:text-red-500"
>
×
</button>
)}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4">
<div className="text-sm">Content for {tab.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const FullWidth: Story = {
render({ defaultValue }) {
return (
<div className="w-full max-w-2xl">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
<Tabs.Trigger value="account" className="flex-1">
Account
</Tabs.Trigger>
<Tabs.Trigger value="password" className="flex-1">
Password
</Tabs.Trigger>
<Tabs.Trigger value="notifications" className="flex-1">
Notifications
</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
{tabOptions.map((option) => (
<Tabs.Content key={option.value} value={option.value} className="p-4">
<div className="text-sm">Content for {option.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const WithComplexContent: Story = {
render({ defaultValue }) {
return (
<div className="w-[600px]">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content value="account" className="p-4">
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Username</label>
<input type="text" className="mt-1 w-full px-3 py-2 bg-custom-background-80 rounded" />
</div>
<div>
<label className="text-sm font-medium">Email</label>
<input type="email" className="mt-1 w-full px-3 py-2 bg-custom-background-80 rounded" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="password" className="p-4">
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Current Password</label>
<input type="password" className="mt-1 w-full px-3 py-2 bg-custom-background-80 rounded" />
</div>
<div>
<label className="text-sm font-medium">New Password</label>
<input type="password" className="mt-1 w-full px-3 py-2 bg-custom-background-80 rounded" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="notifications" className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">Email notifications</span>
<input type="checkbox" />
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Push notifications</span>
<input type="checkbox" />
</div>
</div>
</Tabs.Content>
</Tabs>
</div>
);
},
};

View File

@@ -0,0 +1,381 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Toast, setToast, updateToast, setPromiseToast, TOAST_TYPE } from "./toast";
const meta = {
title: "Components/Toast",
component: Toast,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
theme: "light",
},
} satisfies Meta<typeof Toast>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Provider: Story = {
render() {
return (
<div>
<Toast theme="light" />
<div className="space-y-2">
<p className="text-sm text-gray-600">
Toast provider is required to display toasts. It should be added to your app root.
</p>
<code className="block rounded bg-gray-100 p-2 text-xs">{`<Toast theme="light" />`}</code>
</div>
</div>
);
},
};
export const Success: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Your changes have been saved successfully.",
})
}
className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
>
Show Success Toast
</button>
</>
);
},
};
export const Error: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: "Something went wrong. Please try again.",
})
}
className="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600"
>
Show Error Toast
</button>
</>
);
},
};
export const Warning: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.WARNING,
title: "Warning",
message: "This action cannot be undone.",
})
}
className="rounded bg-yellow-500 px-4 py-2 text-white hover:bg-yellow-600"
>
Show Warning Toast
</button>
</>
);
},
};
export const Info: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.INFO,
title: "Information",
message: "Here's some helpful information for you.",
})
}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
Show Info Toast
</button>
</>
);
},
};
export const Loading: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.LOADING,
title: "Loading...",
})
}
className="rounded bg-gray-500 px-4 py-2 text-white hover:bg-gray-600"
>
Show Loading Toast
</button>
</>
);
},
};
export const WithActionItems: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "File uploaded",
message: "Your file has been uploaded successfully.",
actionItems: (
<button className="rounded bg-blue-500 px-3 py-1 text-xs text-white hover:bg-blue-600">
View File
</button>
),
})
}
className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
>
Show Toast with Action
</button>
</>
);
},
};
export const UpdateToast: Story = {
render() {
const handleUpdate = () => {
const id = setToast({
type: TOAST_TYPE.LOADING,
title: "Processing...",
});
setTimeout(() => {
updateToast(id, {
type: TOAST_TYPE.SUCCESS,
title: "Complete!",
message: "The operation has finished successfully.",
});
}, 2000);
};
return (
<>
<Toast theme="light" />
<button onClick={handleUpdate} className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Update Toast After 2s
</button>
</>
);
},
};
export const PromiseToast: Story = {
render() {
const handlePromise = () => {
const promise = new Promise<{ name?: string; error?: string }>((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve({ name: "Success data" }) : reject({ error: "Failed" });
}, 2000);
});
setPromiseToast(promise, {
loading: "Processing request...",
success: {
title: "Request completed!",
message: (data) => `Successfully processed: ${data.name}`,
},
error: {
title: "Request failed",
message: (error) => `Error: ${error.error}`,
},
});
};
return (
<>
<Toast theme="light" />
<button onClick={handlePromise} className="rounded bg-purple-500 px-4 py-2 text-white hover:bg-purple-600">
Show Promise Toast
</button>
</>
);
},
};
export const AllTypes: Story = {
render() {
return (
<>
<Toast theme="light" />
<div className="flex flex-wrap gap-2">
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "Operation successful",
})
}
className="rounded bg-green-500 px-3 py-2 text-sm text-white hover:bg-green-600"
>
Success
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: "Operation failed",
})
}
className="rounded bg-red-500 px-3 py-2 text-sm text-white hover:bg-red-600"
>
Error
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.WARNING,
title: "Warning",
message: "Please be careful",
})
}
className="rounded bg-yellow-500 px-3 py-2 text-sm text-white hover:bg-yellow-600"
>
Warning
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.INFO,
title: "Info",
message: "Here's some info",
})
}
className="rounded bg-blue-500 px-3 py-2 text-sm text-white hover:bg-blue-600"
>
Info
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.LOADING,
title: "Loading",
})
}
className="rounded bg-gray-500 px-3 py-2 text-sm text-white hover:bg-gray-600"
>
Loading
</button>
</div>
</>
);
},
};
export const MultipleToasts: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "First toast",
message: "This is the first toast",
});
setTimeout(() => {
setToast({
type: TOAST_TYPE.INFO,
title: "Second toast",
message: "This is the second toast",
});
}, 500);
setTimeout(() => {
setToast({
type: TOAST_TYPE.WARNING,
title: "Third toast",
message: "This is the third toast",
});
}, 1000);
}}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
Show Multiple Toasts
</button>
</>
);
},
};
export const TitleOnly: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Saved!",
})
}
className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
>
Show Title Only
</button>
</>
);
},
};
export const LongMessage: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.INFO,
title: "Important Information",
message:
"This is a longer message that provides more detailed information about what happened and what the user should do next.",
})
}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
Show Long Message
</button>
</>
);
},
};

View File

@@ -19,105 +19,114 @@ import {
} from "lucide-react";
import { Toolbar } from "./toolbar";
const meta: Meta<typeof Toolbar> = {
const meta = {
title: "Components/Toolbar",
component: Toolbar,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
};
args: {
children: null,
},
} satisfies Meta<typeof Toolbar>;
export default meta;
type Story = StoryObj<typeof Toolbar>;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<div className="p-4 space-y-4">
<div className="w-96 border rounded">
render() {
return (
<div className="p-4 space-y-4">
<div className="w-96 border rounded">
<Toolbar>
<Toolbar.Group isFirst>
<Toolbar.Item icon={Undo} tooltip="Undo" />
<Toolbar.Item icon={Redo} tooltip="Redo" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={Bold} tooltip="Bold" />
<Toolbar.Item icon={Italic} tooltip="Italic" />
<Toolbar.Item icon={Underline} tooltip="Underline" />
<Toolbar.Item icon={Strikethrough} tooltip="Strikethrough" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={AlignLeft} tooltip="Align Left" />
<Toolbar.Item icon={AlignCenter} tooltip="Align Center" />
<Toolbar.Item icon={AlignRight} tooltip="Align Right" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={Link} tooltip="Link" />
<Toolbar.Item icon={Code} tooltip="Code" />
</Toolbar.Group>
</Toolbar>
</div>
</div>
);
},
};
export const WithActiveStates: Story = {
render() {
return (
<div className="p-4">
<Toolbar>
<Toolbar.Group isFirst>
<Toolbar.Item icon={Undo} tooltip="Undo" />
<Toolbar.Item icon={Redo} tooltip="Redo" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={Bold} tooltip="Bold" />
<Toolbar.Item icon={Italic} tooltip="Italic" />
<Toolbar.Item icon={Underline} tooltip="Underline" />
<Toolbar.Item icon={Strikethrough} tooltip="Strikethrough" />
<Toolbar.Item icon={Bold} tooltip="Bold" shortcut={["Cmd", "B"]} isActive />
<Toolbar.Item icon={Italic} tooltip="Italic" shortcut={["Cmd", "I"]} />
<Toolbar.Item icon={Underline} tooltip="Underline" shortcut={["Cmd", "U"]} isActive />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" isActive />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={AlignLeft} tooltip="Align Left" />
<Toolbar.Item icon={AlignCenter} tooltip="Align Center" />
<Toolbar.Item icon={AlignCenter} tooltip="Align Center" isActive />
<Toolbar.Item icon={AlignRight} tooltip="Align Right" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={Link} tooltip="Link" />
<Toolbar.Item icon={Code} tooltip="Code" />
</Toolbar.Group>
</Toolbar>
</div>
</div>
),
};
export const WithActiveStates: Story = {
render: () => (
<div className="p-4">
<Toolbar>
<Toolbar.Group isFirst>
<Toolbar.Item icon={Bold} tooltip="Bold" shortcut={["Cmd", "B"]} isActive />
<Toolbar.Item icon={Italic} tooltip="Italic" shortcut={["Cmd", "I"]} />
<Toolbar.Item icon={Underline} tooltip="Underline" shortcut={["Cmd", "U"]} isActive />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" isActive />
<Toolbar.Item icon={Quote} tooltip="Quote" />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={AlignLeft} tooltip="Align Left" />
<Toolbar.Item icon={AlignCenter} tooltip="Align Center" isActive />
<Toolbar.Item icon={AlignRight} tooltip="Align Right" />
</Toolbar.Group>
</Toolbar>
</div>
),
);
},
};
export const CommentToolbar: Story = {
render: () => (
<div className="p-4 space-y-4">
<h3 className="text-sm font-medium">Comment Toolbar with Access Control</h3>
<div className="rounded border-[0.5px] border-custom-border-200 p-1">
<Toolbar>
{/* Access Specifier */}
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
<Toolbar.Item icon={Lock} tooltip="Private" isActive />
<Toolbar.Item icon={Globe2} tooltip="Public" />
</div>
<div className="flex w-full items-stretch justify-between gap-2 rounded border-[0.5px] border-custom-border-200 p-1">
<div className="flex items-stretch">
<Toolbar.Group isFirst>
<Toolbar.Item icon={Bold} tooltip="Bold" shortcut={["Cmd", "B"]} />
<Toolbar.Item icon={Italic} tooltip="Italic" shortcut={["Cmd", "I"]} />
<Toolbar.Item icon={Code} tooltip="Code" shortcut={["Cmd", "`"]} />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
</Toolbar.Group>
render() {
return (
<div className="p-4 space-y-4">
<h3 className="text-sm font-medium">Comment Toolbar with Access Control</h3>
<div className="rounded border-[0.5px] border-custom-border-200 p-1">
<Toolbar>
{/* Access Specifier */}
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
<Toolbar.Item icon={Lock} tooltip="Private" isActive />
<Toolbar.Item icon={Globe2} tooltip="Public" />
</div>
<Toolbar.SubmitButton>Comment</Toolbar.SubmitButton>
</div>
</Toolbar>
<div className="flex w-full items-stretch justify-between gap-2 rounded border-[0.5px] border-custom-border-200 p-1">
<div className="flex items-stretch">
<Toolbar.Group isFirst>
<Toolbar.Item icon={Bold} tooltip="Bold" shortcut={["Cmd", "B"]} />
<Toolbar.Item icon={Italic} tooltip="Italic" shortcut={["Cmd", "I"]} />
<Toolbar.Item icon={Code} tooltip="Code" shortcut={["Cmd", "`"]} />
</Toolbar.Group>
<Toolbar.Group>
<Toolbar.Item icon={List} tooltip="Bullet List" />
<Toolbar.Item icon={ListOrdered} tooltip="Numbered List" />
</Toolbar.Group>
</div>
<Toolbar.SubmitButton>Comment</Toolbar.SubmitButton>
</div>
</Toolbar>
</div>
</div>
</div>
),
);
},
};

View File

@@ -0,0 +1,303 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { HelpCircle } from "lucide-react";
import { Tooltip } from "./root";
const meta = {
title: "Components/Tooltip",
component: Tooltip,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof Tooltip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
tooltipContent: "This is a tooltip",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Hover me</button>,
},
};
export const WithHeading: Story = {
args: {
tooltipHeading: "Tooltip Title",
tooltipContent: "This is the tooltip content with a heading.",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Hover me</button>,
},
};
export const PositionTop: Story = {
args: {
tooltipContent: "Tooltip on top",
position: "top",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Top</button>,
},
};
export const PositionBottom: Story = {
args: {
tooltipContent: "Tooltip on bottom",
position: "bottom",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Bottom</button>,
},
};
export const PositionLeft: Story = {
args: {
tooltipContent: "Tooltip on left",
position: "left",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Left</button>,
},
};
export const PositionRight: Story = {
args: {
tooltipContent: "Tooltip on right",
position: "right",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Right</button>,
},
};
export const WithIcon: Story = {
args: {
tooltipContent: "Click here for help",
children: (
<button className="rounded-full p-2 hover:bg-gray-100">
<HelpCircle className="h-5 w-5 text-gray-600" />
</button>
),
},
};
export const Disabled: Story = {
args: {
tooltipContent: "This tooltip is disabled",
disabled: true,
children: <button className="rounded bg-gray-400 px-4 py-2 text-white">Hover me (disabled)</button>,
},
};
export const LongContent: Story = {
args: {
tooltipHeading: "Important Information",
tooltipContent:
"This is a longer tooltip with more detailed information that wraps to multiple lines. It provides comprehensive details about the element.",
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Long content</button>,
},
};
export const CustomDelay: Story = {
args: {
tooltipContent: "This tooltip has a custom delay",
openDelay: 1000,
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Custom delay (1s)</button>,
},
};
export const CustomOffset: Story = {
args: {
tooltipContent: "Custom offset tooltip",
sideOffset: 20,
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">Custom offset</button>,
},
};
export const AllPositions: Story = {
args: {
children: <div />,
},
render() {
return (
<div className="flex flex-col items-center gap-4">
<Tooltip tooltipContent="Top position" position="top">
<button className="rounded bg-blue-500 px-4 py-2 text-sm text-white">Top</button>
</Tooltip>
<div className="flex gap-4">
<Tooltip tooltipContent="Left position" position="left">
<button className="rounded bg-blue-500 px-4 py-2 text-sm text-white">Left</button>
</Tooltip>
<Tooltip tooltipContent="Right position" position="right">
<button className="rounded bg-blue-500 px-4 py-2 text-sm text-white">Right</button>
</Tooltip>
</div>
<Tooltip tooltipContent="Bottom position" position="bottom">
<button className="rounded bg-blue-500 px-4 py-2 text-sm text-white">Bottom</button>
</Tooltip>
</div>
);
},
};
export const OnText: Story = {
args: {
children: <div />,
},
render() {
return (
<p className="text-sm text-gray-700">
This is some text with a{" "}
<Tooltip tooltipContent="Additional information about this word" position="top">
<span className="cursor-help border-b border-dashed border-blue-500 text-blue-500">tooltip</span>
</Tooltip>{" "}
in it.
</p>
);
},
};
export const OnDisabledButton: Story = {
args: {
children: <div />,
},
render() {
return (
<Tooltip tooltipContent="This feature is currently unavailable" position="top">
<button className="cursor-not-allowed rounded bg-gray-300 px-4 py-2 text-gray-500" disabled>
Disabled Button
</button>
</Tooltip>
);
},
};
export const ComplexContent: Story = {
args: {
tooltipHeading: "User Information",
tooltipContent: (
<div className="space-y-1">
<p className="font-semibold">John Doe</p>
<p className="text-xs">john@example.com</p>
<p className="text-xs text-gray-400">Last seen: 2 hours ago</p>
</div>
),
children: <button className="rounded bg-blue-500 px-4 py-2 text-white">View User</button>,
},
};
export const WithCustomStyling: Story = {
args: {
tooltipContent: "Custom styled tooltip",
className: "bg-purple-500 text-white",
children: <button className="rounded bg-purple-500 px-4 py-2 text-white">Custom style</button>,
},
};
export const MultipleTooltips: Story = {
args: {
children: <div />,
},
render() {
return (
<div className="flex gap-4">
<Tooltip tooltipContent="Save your work" position="top">
<button className="rounded bg-green-500 px-4 py-2 text-sm text-white">Save</button>
</Tooltip>
<Tooltip tooltipContent="Discard changes" position="top">
<button className="rounded bg-red-500 px-4 py-2 text-sm text-white">Cancel</button>
</Tooltip>
<Tooltip tooltipContent="Export to PDF" position="top">
<button className="rounded bg-blue-500 px-4 py-2 text-sm text-white">Export</button>
</Tooltip>
<Tooltip tooltipContent="Share with team" position="top">
<button className="rounded bg-purple-500 px-4 py-2 text-sm text-white">Share</button>
</Tooltip>
</div>
);
},
};
export const IconButtons: Story = {
args: {
children: <div />,
},
render() {
return (
<div className="flex gap-2">
<Tooltip tooltipContent="Edit" position="top">
<button className="rounded p-2 hover:bg-gray-100">
<svg
className="h-5 w-5 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
</Tooltip>
<Tooltip tooltipContent="Delete" position="top">
<button className="rounded p-2 hover:bg-gray-100">
<svg
className="h-5 w-5 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</Tooltip>
<Tooltip tooltipContent="Share" position="top">
<button className="rounded p-2 hover:bg-gray-100">
<svg
className="h-5 w-5 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
/>
</svg>
</button>
</Tooltip>
</div>
);
},
};
export const InFormField: Story = {
args: {
children: <div />,
},
render() {
return (
<div className="w-80">
<label className="mb-1 flex items-center gap-2 text-sm font-medium text-gray-700">
Email Address
<Tooltip
tooltipHeading="Email Requirements"
tooltipContent="Enter a valid email address that you have access to. We'll send a verification link."
position="right"
>
<HelpCircle className="h-4 w-4 cursor-help text-gray-400" />
</Tooltip>
</label>
<input
type="email"
className="w-full rounded border border-gray-300 px-3 py-2 text-sm"
placeholder="you@example.com"
/>
</div>
);
},
};

View File

@@ -9,16 +9,6 @@ const meta: Meta<typeof Breadcrumbs> = {
title: "UI/Breadcrumbs",
component: Breadcrumbs,
tags: ["autodocs"],
argTypes: {
isLoading: {
control: "boolean",
description: "Shows loading state of breadcrumbs",
},
onBack: {
action: "onBack",
description: "Callback function when back button is clicked",
},
},
};
type TBreadcrumbBlockProps = {

View File

@@ -1,44 +1,38 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { PopoverMenu } from "./popover-menu";
const meta: Meta<typeof PopoverMenu> = {
title: "PopoverMenu",
component: PopoverMenu,
};
export default meta;
// types
type TPopoverMenu = {
id: number;
name: string;
};
type Story = StoryObj<typeof PopoverMenu<TPopoverMenu>>;
// data
const data: TPopoverMenu[] = [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
{ id: 3, name: "John Smith" },
{ id: 4, name: "Jane Smith" },
];
// components
const PopoverMenuItemRender = (item: TPopoverMenu) => (
<div className="text-sm text-gray-600 hover:text-gray-700 rounded-sm cursor-pointer hover:bg-gray-200 transition-all px-1.5 py-0.5 capitalize">
{item.name}
</div>
);
// stories
export const Default: Story = {
const meta: Meta<typeof PopoverMenu<TPopoverMenu>> = {
title: "Components/PopoverMenu",
component: PopoverMenu,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
popperPosition: "bottom-start",
panelClassName: "rounded bg-gray-100 p-2",
data: data,
data: [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
{ id: 3, name: "John Smith" },
{ id: 4, name: "Jane Smith" },
],
keyExtractor: (item, index: number) => `${item.id}-${index}`,
render: (item) => PopoverMenuItemRender(item),
render: (item: TPopoverMenu) => (
<div className="text-sm text-gray-600 hover:text-gray-700 rounded-sm cursor-pointer hover:bg-gray-200 transition-all px-1.5 py-0.5 capitalize">
{item.name}
</div>
),
},
};
export default meta;
type Story = StoryObj<typeof PopoverMenu<TPopoverMenu>>;
export const Default: Story = {};

View File

@@ -2,26 +2,19 @@ import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Sortable } from "./sortable";
const meta: Meta<typeof Sortable> = {
type StoryItem = { id: string; name: string };
const meta: Meta<typeof Sortable<StoryItem>> = {
title: "Sortable",
component: Sortable,
};
export default meta;
type StoryItem = { id: string; name: string };
type Story = StoryObj<typeof Sortable<StoryItem>>;
const data = [
{ id: "1", name: "John Doe" },
{ id: "2", name: "Satish" },
{ id: "3", name: "Alice" },
{ id: "4", name: "Bob" },
{ id: "5", name: "Charlie" },
];
export const Default: Story = {
args: {
data,
data: [
{ id: "1", name: "John Doe" },
{ id: "2", name: "Satish" },
{ id: "3", name: "Alice" },
{ id: "4", name: "Bob" },
{ id: "5", name: "Charlie" },
],
render: (item: StoryItem) => (
// <Draggable data={item} className="rounded-lg">
<div className="border ">{item.name}</div>
@@ -31,3 +24,9 @@ export const Default: Story = {
keyExtractor: (item) => item.id,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

373
pnpm-lock.yaml generated
View File

@@ -296,7 +296,7 @@ importers:
version: 3.0.5
'@types/node':
specifier: ^20.14.9
version: 20.19.11
version: 20.19.17
'@types/pino-http':
specifier: ^5.8.4
version: 5.8.4
@@ -686,7 +686,7 @@ importers:
version: 4.17.23
'@types/node':
specifier: ^20.14.9
version: 20.19.11
version: 20.19.17
'@types/ws':
specifier: ^8.5.10
version: 8.18.1
@@ -704,7 +704,7 @@ importers:
dependencies:
'@floating-ui/dom':
specifier: ^1.7.1
version: 1.7.2
version: 1.7.3
'@floating-ui/react':
specifier: ^0.26.4
version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -773,7 +773,7 @@ importers:
version: 3.6.2
'@tiptap/react':
specifier: ^3.5.3
version: 3.6.2(@floating-ui/dom@1.7.2)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 3.6.2(@floating-ui/dom@1.7.3)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/starter-kit':
specifier: ^3.5.3
version: 3.6.2
@@ -824,7 +824,7 @@ importers:
version: 9.0.12(yjs@13.6.27)
y-prosemirror:
specifier: ^1.2.15
version: 1.3.6(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
version: 1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
y-protocols:
specifier: ^1.0.6
version: 1.0.6(yjs@13.6.27)
@@ -984,7 +984,7 @@ importers:
version: 4.17.23
'@types/node':
specifier: ^20.14.9
version: 20.19.11
version: 20.19.17
tsdown:
specifier: 'catalog:'
version: 0.14.2(typescript@5.8.3)
@@ -1054,13 +1054,13 @@ importers:
version: link:../typescript-config
'@storybook/addon-designs':
specifier: 10.0.2
version: 10.0.2(@storybook/addon-docs@9.1.2(@types/react@18.3.11)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
version: 10.0.2(@storybook/addon-docs@9.1.10(@types/react@18.3.11)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/addon-docs':
specifier: 9.1.2
version: 9.1.2(@types/react@18.3.11)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
specifier: 9.1.10
version: 9.1.10(@types/react@18.3.11)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/react-vite':
specifier: 9.1.2
version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.0)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
specifier: 9.1.10
version: 9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.0)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
'@types/react':
specifier: 'catalog:'
version: 18.3.11
@@ -1068,11 +1068,11 @@ importers:
specifier: 'catalog:'
version: 18.3.1
eslint-plugin-storybook:
specifier: 9.1.2
version: 9.1.2(eslint@8.57.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)
specifier: 9.1.10
version: 9.1.10(eslint@8.57.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)
storybook:
specifier: 9.1.2
version: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
specifier: 9.1.10
version: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
tsdown:
specifier: 'catalog:'
version: 0.14.2(typescript@5.8.3)
@@ -1324,7 +1324,7 @@ importers:
version: 4.17.12
'@types/node':
specifier: ^20.5.2
version: 20.19.11
version: 20.19.17
'@types/react':
specifier: 'catalog:'
version: 18.3.11
@@ -1633,9 +1633,6 @@ packages:
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
'@emnapi/runtime@1.4.5':
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
@@ -1875,9 +1872,6 @@ packages:
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.7.2':
resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
'@floating-ui/dom@1.7.3':
resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
@@ -2218,8 +2212,8 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@napi-rs/wasm-runtime@1.0.6':
resolution: {integrity: sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==}
'@napi-rs/wasm-runtime@1.0.5':
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==}
'@next/env@14.2.32':
resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==}
@@ -2309,8 +2303,8 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@oxc-project/types@0.93.0':
resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==}
'@oxc-project/types@0.92.0':
resolution: {integrity: sha512-PDLfCbwgXjGdTBxzcuDOUxJYNBl6P8dOp3eDKWw54dYvqONan9rwGDRQU0zrkdEMiItfXQQUOI17uOcMX5Zm7A==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
@@ -2577,91 +2571,91 @@ packages:
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rolldown/binding-android-arm64@1.0.0-beta.41':
resolution: {integrity: sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==}
'@rolldown/binding-android-arm64@1.0.0-beta.40':
resolution: {integrity: sha512-9Ii9phC7QU6Lb+ncMfG1Xlosq0NBB1N/4sw+EGZ3y0BBWGy02TOb5ghWZalphAKv9rn1goqo5WkBjyd2YvsLmA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-beta.41':
resolution: {integrity: sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==}
'@rolldown/binding-darwin-arm64@1.0.0-beta.40':
resolution: {integrity: sha512-5O6d0y2tBQTL+ecQY3qXIwSnF1/Zik8q7LZMKeyF+VJ9l194d0IdMhl2zUF0cqWbYHuF4Pnxplk4OhurPQ/Z9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-beta.41':
resolution: {integrity: sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==}
'@rolldown/binding-darwin-x64@1.0.0-beta.40':
resolution: {integrity: sha512-izB9jygt3miPQbOTZfSu5K51isUplqa8ysByOKQqcJHgrBWmbTU8TM9eouv6tRmBR0kjcEcID9xhmA1CeZ1VIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-beta.41':
resolution: {integrity: sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==}
'@rolldown/binding-freebsd-x64@1.0.0-beta.40':
resolution: {integrity: sha512-2fdpEpKT+wwP0vig9dqxu+toTeWmVSjo3psJQVDeLJ51rO+GXcCJ1IkCXjhMKVEevNtZS7B8T8Z2vvmRV9MAdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41':
resolution: {integrity: sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==}
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40':
resolution: {integrity: sha512-HP2lo78OWULN+8TewpLbS9PS00jh0CaF04tA2u8z2I+6QgVgrYOYKvX+T0hlO5smgso4+qb3YchzumWJl3yCPQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41':
resolution: {integrity: sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==}
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40':
resolution: {integrity: sha512-ng00gfr9BhA2NPAOU5RWAlTiL+JcwAD+L+4yUD1sbBy6tgHdLiNBOvKtHISIF9RM9/eQeS0tAiWOYZGIH9JMew==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.41':
resolution: {integrity: sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==}
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.40':
resolution: {integrity: sha512-mF0R1l9kLcaag/9cLEiYYdNZ4v1uuX4jklSDZ1s6vJE4RB3LirUney0FavdVRwCJ5sDvfvsPgXgtBXWYr2M2tQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.41':
resolution: {integrity: sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==}
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.40':
resolution: {integrity: sha512-+wi08S7wT5iLPHRZb0USrS6n+T6m+yY++dePYedE5uvKIpWCJJioFTaRtWjpm0V6dVNLcq2OukrvfdlGtH9Wgg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.41':
resolution: {integrity: sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==}
'@rolldown/binding-linux-x64-musl@1.0.0-beta.40':
resolution: {integrity: sha512-W5qBGAemUocIBKCcOsDjlV9GUt28qhl/+M6etWBeLS5gQK0J6XDg0YVzfOQdvq57ZGjYNP0NvhYzqhOOnEx+4g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.41':
resolution: {integrity: sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==}
'@rolldown/binding-openharmony-arm64@1.0.0-beta.40':
resolution: {integrity: sha512-vJwoDehtt+yqj2zacq1AqNc2uE/oh7mnRGqAUbuldV6pgvU01OSQUJ7Zu+35hTopnjFoDNN6mIezkYlGAv5RFA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-beta.41':
resolution: {integrity: sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==}
'@rolldown/binding-wasm32-wasi@1.0.0-beta.40':
resolution: {integrity: sha512-Oj3YyqVUPurr1FlMpEE/bJmMC+VWAWPM/SGUfklO5KUX97bk5Q/733nPg4RykK8q8/TluJoQYvRc05vL/B74dw==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41':
resolution: {integrity: sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==}
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40':
resolution: {integrity: sha512-0ZtO6yN8XjVoFfN4HDWQj4nDu3ndMybr7jIM00DJqOmc+yFhly7rdOy7fNR9Sky3leCpBtsXfepVqRmVpYKPVA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41':
resolution: {integrity: sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==}
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40':
resolution: {integrity: sha512-BPl1inoJXPpIe38Ja46E4y11vXlJyuleo+9Rmu//pYL5fIDYJkXUj/oAXqjSuwLcssrcwnuPgzvzvlz9++cr3w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.41':
resolution: {integrity: sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==}
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.40':
resolution: {integrity: sha512-UguA4ltbAk+nbwHRxqaUP/etpTbR0HjyNlsu4Zjbh/ytNbFsbw8CA4tEBkwDyjgI5NIPea6xY11zpl7R2/ddVA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-beta.41':
resolution: {integrity: sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==}
'@rolldown/pluginutils@1.0.0-beta.40':
resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==}
'@rollup/pluginutils@5.2.0':
resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
@@ -2829,10 +2823,10 @@ packages:
peerDependencies:
storybook: ^8.6.14
'@storybook/addon-docs@9.1.2':
resolution: {integrity: sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==}
'@storybook/addon-docs@9.1.10':
resolution: {integrity: sha512-LYK3oXy/0PgY39FhkYVd9D0bzatLGTsMhaPPwSjBOmZgK0f0yBLqaePy7urUFeHYm/rjwAaRmDJNBqUnGTVoig==}
peerDependencies:
storybook: ^9.1.2
storybook: ^9.1.10
'@storybook/addon-essentials@8.6.14':
resolution: {integrity: sha512-5ZZSHNaW9mXMOFkoPyc3QkoNGdJHETZydI62/OASR0lmPlJ1065TNigEo5dJddmZNn0/3bkE8eKMAzLnO5eIdA==}
@@ -2904,10 +2898,10 @@ packages:
react-dom:
optional: true
'@storybook/builder-vite@9.1.2':
resolution: {integrity: sha512-5Y7e5wnSzFxCGP63UNRRZVoxHe1znU4dYXazJBobAlEcUPBk7A0sH2716tA6bS4oz92oG9tgvn1g996hRrw4ow==}
'@storybook/builder-vite@9.1.10':
resolution: {integrity: sha512-0ogI+toZJYaFptcFpRcRPOZ9/NrFUYhiaI09ggeEB1FF9ygHMVsobp4eaj4HjZI6V3x7cQwkd2ZmxAMQDBQuMA==}
peerDependencies:
storybook: ^9.1.2
storybook: ^9.1.10
vite: 7.0.7
'@storybook/builder-webpack5@8.6.14':
@@ -2942,10 +2936,10 @@ packages:
peerDependencies:
storybook: ^8.6.14
'@storybook/csf-plugin@9.1.2':
resolution: {integrity: sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==}
'@storybook/csf-plugin@9.1.10':
resolution: {integrity: sha512-247F/JU0Naxm/RIUnQYpqXeCL0wG8UNJkZe+/GkLjdqzsyML0lb+8OwBsWFfG8zfj6fkjmRU2mF44TnNkzoQcg==}
peerDependencies:
storybook: ^9.1.2
storybook: ^9.1.10
'@storybook/global@5.0.0':
resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
@@ -3002,20 +2996,20 @@ packages:
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^8.6.14
'@storybook/react-dom-shim@9.1.2':
resolution: {integrity: sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==}
'@storybook/react-dom-shim@9.1.10':
resolution: {integrity: sha512-cxy8GTj73RMJIFPrgqdnMXePGX5iFohM5pDCZ63Te5m5GtzKqsILRXtBBLO6Ouexm/ZYRVznkKiwNKX/Fu24fQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.2
storybook: ^9.1.10
'@storybook/react-vite@9.1.2':
resolution: {integrity: sha512-dv3CBjOzmMoSyIotMtdmsBRjB25i19OjFP0IZqauLeUoVm6QddILW7JRcZVLrzhATyBEn+sEAdWQ4j79Z11HAg==}
'@storybook/react-vite@9.1.10':
resolution: {integrity: sha512-k0wWlfoWakoHL3NZ1+38oxRH3WYyprFFX+WUb/4W8axrvpKogvdnxKCul/YB1HH5FcTagIfguamsPjKwB1ZkJg==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.2
storybook: ^9.1.10
vite: 7.0.7
'@storybook/react-webpack5@8.6.14':
@@ -3045,13 +3039,13 @@ packages:
typescript:
optional: true
'@storybook/react@9.1.2':
resolution: {integrity: sha512-VVXu1HrhDExj/yj+heFYc8cgIzBruXy1UYT3LW0WiJyadgzYz3J41l/Lf/j2FCppyxwlXb19Uv51plb1F1C77w==}
'@storybook/react@9.1.10':
resolution: {integrity: sha512-flG3Gn3EHZnxn92C7vrA2U4aGqpOKdf85fL43+J/2k9HF5AIyOFGlcv4LGVyKZ3LOAow/nGBVSXL9961h+ICRA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.2
storybook: ^9.1.10
typescript: 5.8.3
peerDependenciesMeta:
typescript:
@@ -3589,9 +3583,6 @@ packages:
'@types/node@18.16.1':
resolution: {integrity: sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==}
'@types/node@20.19.11':
resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==}
'@types/node@20.19.17':
resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==}
@@ -4058,10 +4049,6 @@ packages:
resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==}
engines: {node: '>=14'}
ansis@4.2.0:
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
engines: {node: '>=14'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -5063,12 +5050,12 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
eslint-plugin-storybook@9.1.2:
resolution: {integrity: sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==}
eslint-plugin-storybook@9.1.10:
resolution: {integrity: sha512-HAVQ9HTMydcFj5KjnzsETOwPe19eIViwRBhc47lvU04YEFTgEg2rlXN1xozxHUlQ+XkkoKYkIUYoqo7KgGhkIA==}
engines: {node: '>=20.0.0'}
peerDependencies:
eslint: '>=8'
storybook: ^9.1.2
storybook: ^9.1.10
eslint-plugin-turbo@1.13.4:
resolution: {integrity: sha512-82GfMzrewI/DJB92Bbch239GWbGx4j1zvjk1lqb06lxIlMPnVwUHVwPbAnLfyLG3JuhLv9whxGkO/q1CL18JTg==}
@@ -7207,8 +7194,8 @@ packages:
vue-tsc:
optional: true
rolldown@1.0.0-beta.41:
resolution: {integrity: sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==}
rolldown@1.0.0-beta.40:
resolution: {integrity: sha512-VqEHbKpOgTPmQrZ4fVn4eshDQS/6g/fRpNE7cFSJY+eQLDZn4B9X61J6L+hnlt1u2uRI+pF7r1USs6S5fuWCvw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -7425,8 +7412,8 @@ packages:
prettier:
optional: true
storybook@9.1.2:
resolution: {integrity: sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==}
storybook@9.1.10:
resolution: {integrity: sha512-4+U7gF9hMpGilQmdVJwQaVZZEkD7XwC4ZDmBa51mobaPYelELEMoMfNM2hLyvB2x12gk1IJui1DnwOE4t+MXhw==}
hasBin: true
peerDependencies:
prettier: ^2 || ^3
@@ -7638,10 +7625,6 @@ packages:
tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -8205,16 +8188,6 @@ packages:
peerDependencies:
yjs: ^13.0.0
y-prosemirror@1.3.6:
resolution: {integrity: sha512-vtS2rv8+ll/TBQRqwUiqflgSuN/DhfvUQX0r5O3o5i0pO6K4pSNgFtVkOKtNWPBVkS6l9BDQjbtnDNftZnxq7Q==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
prosemirror-model: ^1.7.1
prosemirror-state: ^1.2.3
prosemirror-view: 1.40.0
y-protocols: ^1.0.1
yjs: ^13.5.38
y-prosemirror@1.3.7:
resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
@@ -8568,11 +8541,6 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.4.5':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.5.0':
dependencies:
tslib: 2.8.1
@@ -8778,11 +8746,6 @@ snapshots:
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.2':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.3':
dependencies:
'@floating-ui/core': 1.7.3
@@ -8993,7 +8956,7 @@ snapshots:
'@img/sharp-wasm32@0.33.5':
dependencies:
'@emnapi/runtime': 1.4.5
'@emnapi/runtime': 1.5.0
optional: true
'@img/sharp-win32-ia32@0.33.5':
@@ -9150,7 +9113,7 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@napi-rs/wasm-runtime@1.0.6':
'@napi-rs/wasm-runtime@1.0.5':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
@@ -9212,7 +9175,7 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@oxc-project/types@0.93.0': {}
'@oxc-project/types@0.92.0': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -9539,51 +9502,51 @@ snapshots:
'@remirror/core-constants@3.0.0': {}
'@rolldown/binding-android-arm64@1.0.0-beta.41':
'@rolldown/binding-android-arm64@1.0.0-beta.40':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.41':
'@rolldown/binding-darwin-arm64@1.0.0-beta.40':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.41':
'@rolldown/binding-darwin-x64@1.0.0-beta.40':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.41':
'@rolldown/binding-freebsd-x64@1.0.0-beta.40':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41':
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41':
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.41':
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.40':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.41':
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.40':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.41':
'@rolldown/binding-linux-x64-musl@1.0.0-beta.40':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.41':
'@rolldown/binding-openharmony-arm64@1.0.0-beta.40':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.41':
'@rolldown/binding-wasm32-wasi@1.0.0-beta.40':
dependencies:
'@napi-rs/wasm-runtime': 1.0.6
'@napi-rs/wasm-runtime': 1.0.5
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41':
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40':
optional: true
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41':
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.41':
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.40':
optional: true
'@rolldown/pluginutils@1.0.0-beta.41': {}
'@rolldown/pluginutils@1.0.0-beta.40': {}
'@rollup/pluginutils@5.2.0(rollup@4.52.0)':
dependencies:
@@ -9690,12 +9653,12 @@ snapshots:
storybook: 8.6.14(prettier@3.6.2)
ts-dedent: 2.2.0
'@storybook/addon-designs@10.0.2(@storybook/addon-docs@9.1.2(@types/react@18.3.11)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
'@storybook/addon-designs@10.0.2(@storybook/addon-docs@9.1.10(@types/react@18.3.11)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
dependencies:
'@figspec/react': 1.0.4(react@18.3.1)
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
optionalDependencies:
'@storybook/addon-docs': 9.1.2(@types/react@18.3.11)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/addon-docs': 9.1.10(@types/react@18.3.11)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -9712,15 +9675,15 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
'@storybook/addon-docs@9.1.2(@types/react@18.3.11)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
'@storybook/addon-docs@9.1.10(@types/react@18.3.11)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
dependencies:
'@mdx-js/react': 3.1.0(@types/react@18.3.11)(react@18.3.1)
'@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/csf-plugin': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/react-dom-shim': 9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
ts-dedent: 2.2.0
transitivePeerDependencies:
- '@types/react'
@@ -9812,10 +9775,10 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@storybook/builder-vite@9.1.2(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))':
'@storybook/builder-vite@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))':
dependencies:
'@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
'@storybook/csf-plugin': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
ts-dedent: 2.2.0
vite: 7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)
@@ -9890,9 +9853,9 @@ snapshots:
storybook: 8.6.14(prettier@3.6.2)
unplugin: 1.16.1
'@storybook/csf-plugin@9.1.2(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
'@storybook/csf-plugin@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
dependencies:
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
unplugin: 1.16.1
'@storybook/global@5.0.0': {}
@@ -9966,25 +9929,25 @@ snapshots:
react-dom: 18.3.1(react@18.3.1)
storybook: 8.6.14(prettier@3.6.2)
'@storybook/react-dom-shim@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
'@storybook/react-dom-shim@9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))':
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
'@storybook/react-vite@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.0)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))':
'@storybook/react-vite@9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.0)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
'@rollup/pluginutils': 5.2.0(rollup@4.52.0)
'@storybook/builder-vite': 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
'@storybook/react': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)
'@storybook/builder-vite': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
'@storybook/react': 9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)
find-up: 7.0.0
magic-string: 0.30.17
react: 18.3.1
react-docgen: 8.0.0
react-dom: 18.3.1(react@18.3.1)
resolve: 1.22.10
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
tsconfig-paths: 4.2.0
vite: 7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)
transitivePeerDependencies:
@@ -10026,13 +9989,13 @@ snapshots:
'@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.6.2))
typescript: 5.8.3
'@storybook/react@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)':
'@storybook/react@9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3)':
dependencies:
'@storybook/global': 5.0.0
'@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
'@storybook/react-dom-shim': 9.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
optionalDependencies:
typescript: 5.8.3
@@ -10238,9 +10201,9 @@ snapshots:
transitivePeerDependencies:
- emojibase
'@tiptap/extension-floating-menu@3.6.2(@floating-ui/dom@1.7.2)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)':
'@tiptap/extension-floating-menu@3.6.2(@floating-ui/dom@1.7.3)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)':
dependencies:
'@floating-ui/dom': 1.7.2
'@floating-ui/dom': 1.7.3
'@tiptap/core': 3.6.2(@tiptap/pm@3.6.2)
'@tiptap/pm': 3.6.2
optional: true
@@ -10363,7 +10326,7 @@ snapshots:
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
'@tiptap/react@3.6.2(@floating-ui/dom@1.7.2)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@tiptap/react@3.6.2(@floating-ui/dom@1.7.3)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tiptap/core': 3.6.2(@tiptap/pm@3.6.2)
'@tiptap/pm': 3.6.2
@@ -10376,7 +10339,7 @@ snapshots:
use-sync-external-store: 1.5.0(react@18.3.1)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.6.2(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)
'@tiptap/extension-floating-menu': 3.6.2(@floating-ui/dom@1.7.2)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)
'@tiptap/extension-floating-menu': 3.6.2(@floating-ui/dom@1.7.3)(@tiptap/core@3.6.2(@tiptap/pm@3.6.2))(@tiptap/pm@3.6.2)
transitivePeerDependencies:
- '@floating-ui/dom'
@@ -10464,7 +10427,7 @@ snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/chai@5.2.2':
dependencies:
@@ -10473,15 +10436,15 @@ snapshots:
'@types/compression@1.8.1':
dependencies:
'@types/express': 4.17.23
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/connect@3.4.38':
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/cors@2.8.19':
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/d3-array@3.2.1': {}
@@ -10531,14 +10494,14 @@ snapshots:
'@types/express-serve-static-core@4.19.6':
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.5
'@types/express-serve-static-core@5.0.7':
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.5
@@ -10612,10 +10575,6 @@ snapshots:
'@types/node@18.16.1': {}
'@types/node@20.19.11':
dependencies:
undici-types: 6.21.0
'@types/node@20.19.17':
dependencies:
undici-types: 6.21.0
@@ -10642,7 +10601,7 @@ snapshots:
'@types/pino@6.3.12':
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/pino-pretty': 5.0.0
'@types/pino-std-serializers': 4.0.0
sonic-boom: 2.8.0
@@ -10682,12 +10641,12 @@ snapshots:
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/serve-static@1.15.8':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 20.19.11
'@types/node': 20.19.17
'@types/send': 0.17.5
'@types/triple-beam@1.3.5': {}
@@ -10708,7 +10667,7 @@ snapshots:
'@types/ws@8.18.1':
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
'@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)':
dependencies:
@@ -11122,8 +11081,6 @@ snapshots:
ansis@4.1.0: {}
ansis@4.2.0: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
@@ -12177,7 +12134,7 @@ snapshots:
get-tsconfig: 4.10.1
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.14
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
@@ -12273,11 +12230,11 @@ snapshots:
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
eslint-plugin-storybook@9.1.2(eslint@8.57.1)(storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3):
eslint-plugin-storybook@9.1.10(eslint@8.57.1)(storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)))(typescript@5.8.3):
dependencies:
'@typescript-eslint/utils': 8.38.0(eslint@8.57.1)(typescript@5.8.3)
eslint: 8.57.1
storybook: 9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
storybook: 9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1))
transitivePeerDependencies:
- supports-color
- typescript
@@ -13123,7 +13080,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 20.19.11
'@types/node': 20.19.17
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -13991,7 +13948,7 @@ snapshots:
pretty-hrtime: 1.0.3
read-cache: 1.0.0
slash: 5.1.0
tinyglobby: 0.2.14
tinyglobby: 0.2.15
yargs: 17.7.2
transitivePeerDependencies:
- jiti
@@ -14623,7 +14580,7 @@ snapshots:
dependencies:
glob: 7.2.3
rolldown-plugin-dts@0.15.10(rolldown@1.0.0-beta.41)(typescript@5.8.3):
rolldown-plugin-dts@0.15.10(rolldown@1.0.0-beta.40)(typescript@5.8.3):
dependencies:
'@babel/generator': 7.28.3
'@babel/parser': 7.28.3
@@ -14633,33 +14590,33 @@ snapshots:
debug: 4.4.1
dts-resolver: 2.1.2
get-tsconfig: 4.10.1
rolldown: 1.0.0-beta.41
rolldown: 1.0.0-beta.40
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
- oxc-resolver
- supports-color
rolldown@1.0.0-beta.41:
rolldown@1.0.0-beta.40:
dependencies:
'@oxc-project/types': 0.93.0
'@rolldown/pluginutils': 1.0.0-beta.41
ansis: 4.2.0
'@oxc-project/types': 0.92.0
'@rolldown/pluginutils': 1.0.0-beta.40
ansis: 4.1.0
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-beta.41
'@rolldown/binding-darwin-arm64': 1.0.0-beta.41
'@rolldown/binding-darwin-x64': 1.0.0-beta.41
'@rolldown/binding-freebsd-x64': 1.0.0-beta.41
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.41
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.41
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.41
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.41
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.41
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.41
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.41
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.41
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.41
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.41
'@rolldown/binding-android-arm64': 1.0.0-beta.40
'@rolldown/binding-darwin-arm64': 1.0.0-beta.40
'@rolldown/binding-darwin-x64': 1.0.0-beta.40
'@rolldown/binding-freebsd-x64': 1.0.0-beta.40
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.40
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.40
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.40
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.40
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.40
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.40
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.40
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.40
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.40
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.40
rollup@4.52.0:
dependencies:
@@ -14955,7 +14912,7 @@ snapshots:
- supports-color
- utf-8-validate
storybook@9.1.2(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)):
storybook@9.1.10(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@7.0.0(@types/node@22.17.2)(jiti@2.5.1)(terser@5.43.1)(yaml@2.8.1)):
dependencies:
'@storybook/global': 5.0.0
'@testing-library/jest-dom': 6.6.3
@@ -15208,11 +15165,6 @@ snapshots:
tinyexec@1.0.1: {}
tinyglobby@0.2.14:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -15321,11 +15273,11 @@ snapshots:
diff: 8.0.2
empathic: 2.0.0
hookable: 5.5.3
rolldown: 1.0.0-beta.41
rolldown-plugin-dts: 0.15.10(rolldown@1.0.0-beta.41)(typescript@5.8.3)
rolldown: 1.0.0-beta.40
rolldown-plugin-dts: 0.15.10(rolldown@1.0.0-beta.40)(typescript@5.8.3)
semver: 7.7.2
tinyexec: 1.0.1
tinyglobby: 0.2.14
tinyglobby: 0.2.15
tree-kill: 1.2.2
unconfig: 7.3.3
optionalDependencies:
@@ -15833,15 +15785,6 @@ snapshots:
lib0: 0.2.114
yjs: 13.6.27
y-prosemirror@1.3.6(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27):
dependencies:
lib0: 0.2.114
prosemirror-model: 1.25.3
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
y-protocols: 1.0.6(yjs@13.6.27)
yjs: 13.6.27
y-prosemirror@1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27):
dependencies:
lib0: 0.2.114