diff --git a/packages/timo-design-system/src/components/index.ts b/packages/timo-design-system/src/components/index.ts index 7917669..6a98357 100644 --- a/packages/timo-design-system/src/components/index.ts +++ b/packages/timo-design-system/src/components/index.ts @@ -1,7 +1,9 @@ export { Checkbox } from "@components/checkbox/Checkbox"; -export { Color } from "@components/color/Color"; -export { Typography } from "@components/typography/Typography"; -export { Tag } from "@components/tag/Tag"; -export { PriorityIcon } from "@components/priority-icon/PriorityIcon"; +export { Color } from "@components/tokens/color/Color"; +export { Typography } from "@components/tokens/typography/Typography"; +export { TagIcon } from "@components/tag/tag-icon/TagIcon"; +export { TagSelector } from "@components/tag/tag-selector/TagSelector"; +export { PriorityIcon } from "@components/priority/priority-icon/PriorityIcon"; +export { PrioritySelector } from "@components/priority/priority-selector/PrioritySelector"; export { CreateButton } from "@components/button/create-button/CreateButton"; export { TodayBadge } from "@components/badge/today-badge/TodayBadge"; diff --git a/packages/timo-design-system/src/components/layout/dropdown/Dropdown.stories.tsx b/packages/timo-design-system/src/components/layout/dropdown/Dropdown.stories.tsx new file mode 100644 index 0000000..4618c68 --- /dev/null +++ b/packages/timo-design-system/src/components/layout/dropdown/Dropdown.stories.tsx @@ -0,0 +1,44 @@ +import { Dropdown } from "./Dropdown"; + +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/layout/Dropdown", + parameters: { + layout: "centered", + backgrounds: { + default: "light-gray", + values: [ + { name: "light-gray", value: "#F5F5F5" }, + { name: "dark", value: "#333333" }, + { name: "white", value: "#FFFFFF" }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const SAMPLE_ITEMS = ["옵션 1", "옵션 2", "옵션 3"]; + +export const Default: Story = { + name: "Basic Usage", + render: () => ( + + + 트리거 + + + + {SAMPLE_ITEMS.map((item) => ( + + + {item} + + + ))} + + + ), +}; diff --git a/packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx b/packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx new file mode 100644 index 0000000..b8e2db1 --- /dev/null +++ b/packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx @@ -0,0 +1,133 @@ +import { cn } from "@lib"; +import { + createContext, + useContext, + useEffect, + useRef, + useState, + type ButtonHTMLAttributes, + type ReactNode, +} from "react"; + +interface DropdownContextValue { + isOpen: boolean; + toggle: () => void; + close: () => void; +} + +const DropdownContext = createContext(null); + +const useDropdownContext = (): DropdownContextValue => { + const context = useContext(DropdownContext); + + if (!context) { + throw new Error( + "Dropdown.Trigger, Dropdown.Panel은 Dropdown 내부에서만 사용할 수 있습니다.", + ); + } + + return context; +}; + +export interface DropdownProps { + children: ReactNode; + className?: string; +} + +const DropdownRoot = ({ children, className }: DropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const rootRef = useRef(null); + + useEffect(() => { + if (!isOpen) return; + + const handleOutsideClick = (event: MouseEvent) => { + if (!rootRef.current?.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + const handleEscapeKeydown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsOpen(false); + } + }; + + document.addEventListener("mousedown", handleOutsideClick); + document.addEventListener("keydown", handleEscapeKeydown); + + return () => { + document.removeEventListener("mousedown", handleOutsideClick); + document.removeEventListener("keydown", handleEscapeKeydown); + }; + }, [isOpen]); + + const toggle = () => setIsOpen((prev) => !prev); + const close = () => setIsOpen(false); + + return ( + +
+ {children} +
+
+ ); +}; + +export interface DropdownTriggerProps { + children: ReactNode; + className?: string; +} + +const DropdownTrigger = ({ children, className }: DropdownTriggerProps) => { + const { toggle } = useDropdownContext(); + + return ( + + ); +}; + +export interface DropdownPanelProps { + children: ReactNode; + className?: string; +} + +const DropdownPanel = ({ children, className }: DropdownPanelProps) => { + const { isOpen } = useDropdownContext(); + + if (!isOpen) return null; + + return ( +
+ {children} +
+ ); +}; + +export type DropdownItemProps = ButtonHTMLAttributes; + +const DropdownItem = ({ className, ...rest }: DropdownItemProps) => { + return ( +