Skip to content
This repository was archived by the owner on Jul 20, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/components/Drawer/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Drawer } from "./Drawer";

const meta: Meta<typeof Drawer> = {
title: "Components/Drawer",
component: Drawer,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: "A sliding drawer component that can appear from any side (left, right, top, bottom), commonly used for sub-menus and additional content.",
},
},
},
argTypes: {
direction: {
control: { type: "select" },
options: ["left", "right", "top", "bottom"],
description: "The direction from which the drawer slides in",
},
},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedTheme, setSelectedTheme] = useState("light");

const themes = [
{ id: "light", label: "Light", description: "Light theme for daytime use" },
{ id: "dark", label: "Dark", description: "Dark theme for reduced eye strain" },
{ id: "auto", label: "Auto", description: "Automatically switch based on system settings" },
];

return (
<div className="relative h-96 overflow-hidden border rounded-lg bg-white dark:bg-gray-900">
<button
onClick={() => setIsOpen(true)}
className="m-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Open Theme Drawer (Right)
</button>

<Drawer
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Theme"
direction="right"
>
<div className="flex flex-col">
{themes.map((theme) => (
<button
key={theme.id}
onClick={() => {
setSelectedTheme(theme.id);
console.log(`Theme selected: ${theme.id}`);
}}
className={`
flex items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-800
${selectedTheme === theme.id ? "bg-gray-50 dark:bg-gray-800" : ""}
`}
>
<div className="flex flex-col items-start">
<span className="font-medium">{theme.label}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">
{theme.description}
</span>
</div>
{selectedTheme === theme.id && (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6667 5L7.5 14.1667L3.33333 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</button>
))}
</div>
</Drawer>
</div>
);
},
};

167 changes: 167 additions & 0 deletions src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React from "react";
import { twJoin } from "tailwind-merge";

type DrawerDirection = "left" | "right" | "top" | "bottom";

interface DrawerProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
className?: string;
showBackButton?: boolean;
backIcon?: React.ReactNode;
direction?: DrawerDirection;
}

export const Drawer: React.FC<DrawerProps> = ({
isOpen,
onClose,
title,
children,
className = "",
showBackButton = true,
backIcon,
direction = "right",
}) => {
const getTransformClasses = () => {
switch (direction) {
case "left":
return isOpen ? "translate-x-0" : "-translate-x-full";
case "right":
return isOpen ? "translate-x-0" : "translate-x-full";
case "top":
return isOpen ? "translate-y-0" : "-translate-y-full";
case "bottom":
return isOpen ? "translate-y-0" : "translate-y-full";
default:
return isOpen ? "translate-x-0" : "translate-x-full";
}
};

const getPositionClasses = () => {
switch (direction) {
case "left":
return "left-0 top-0 bottom-0 w-80";
case "right":
return "right-0 top-0 bottom-0 w-80";
case "top":
return "top-0 left-0 right-0 h-80";
case "bottom":
return "bottom-0 left-0 right-0 h-80";
default:
return "right-0 top-0 bottom-0 w-80";
}
};

const getBackIcon = () => {
if (backIcon) return backIcon;

switch (direction) {
case "left":
return (
<svg
Copy link
Preview

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] SVG markup is duplicated for each direction. You could extract a base arrow icon component and apply CSS rotation or transform props instead of repeating nearly identical SVG blocks.

Copilot uses AI. Check for mistakes.

width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-accent-primary"
>
<path
d="M6 4L10 8L6 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
case "top":
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-accent-primary"
>
<path
d="M4 10L8 6L12 10"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
case "bottom":
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-accent-primary"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
default: // right
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-accent-primary"
>
<path
d="M10 12L6 8L10 4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
};

return (
<div
Copy link
Preview

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drawer container lacks ARIA roles and attributes. Consider adding role="dialog", aria-modal="true", and an aria-labelledby referencing the title header to improve screen‐reader support.

Copilot uses AI. Check for mistakes.

className={twJoin(
"absolute transform transition-transform duration-300 ease-in-out rounded-3xl md:rounded-lg",
"bg-[#FFFFFF] dark:bg-[#252525]",
getPositionClasses(),
getTransformClasses(),
className,
)}
>
{/* Header */}
<div className="flex items-center gap-3 p-4 border-b border-[#38708533] dark:border-[#404040]">
{showBackButton && (
<button
onClick={onClose}
className="flex items-center justify-center w-8 h-8 rounded hover:bg-accent-secondary/10 transition-colors"
aria-label="Go back"
>
{getBackIcon()}
</button>
)}
<h3 className="text-sm font-medium text-accent-primary">{title}</h3>
</div>

<div className="flex-1 overflow-y-auto">{children}</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/Drawer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Drawer } from "./Drawer";