From 6f0993b01e9ecafb60e1edc42edf76e46ec03f8d Mon Sep 17 00:00:00 2001 From: Lee Hye Won Date: Thu, 2 Jul 2026 16:32:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(tailwind-config):=20shadow-timo=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 드롭다운 open 상태 그림자용 --shadow-timo CSS 변수를 추가했습니다. Co-Authored-By: Claude Sonnet 4.6 --- packages/tailwind-config/theme.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/tailwind-config/theme.css b/packages/tailwind-config/theme.css index 5c2c7ba..bdbbc4f 100644 --- a/packages/tailwind-config/theme.css +++ b/packages/tailwind-config/theme.css @@ -1,4 +1,8 @@ @import "./tokens/colors.css"; @import "./tokens/typography.css"; @import "./tokens/radius.css"; -@import "./scrollbar.css"; \ No newline at end of file +@import "./scrollbar.css"; + +@theme { + --shadow-timo: 0px 0px 3px 0px rgba(0, 0, 0, 0.12); +} \ No newline at end of file From 762c92fe21bc63e100885ccb9767a89a96a5023e Mon Sep 17 00:00:00 2001 From: Lee Hye Won Date: Thu, 2 Jul 2026 16:33:35 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(design-system):=20Dropdown=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 피그마 디자인 스펙에 맞춰 Dropdown 컴포넌트를 구현했습니다. Co-Authored-By: Claude Sonnet 4.6 --- .../components/dropdown/Dropdown.stories.tsx | 24 ++++ .../src/components/dropdown/Dropdown.tsx | 115 ++++++++++++++++++ .../src/components/index.ts | 1 + 3 files changed, 140 insertions(+) create mode 100644 packages/timo-design-system/src/components/dropdown/Dropdown.stories.tsx create mode 100644 packages/timo-design-system/src/components/dropdown/Dropdown.tsx diff --git a/packages/timo-design-system/src/components/dropdown/Dropdown.stories.tsx b/packages/timo-design-system/src/components/dropdown/Dropdown.stories.tsx new file mode 100644 index 0000000..6d75508 --- /dev/null +++ b/packages/timo-design-system/src/components/dropdown/Dropdown.stories.tsx @@ -0,0 +1,24 @@ +import { useState } from "react"; + +import { Dropdown } from "./Dropdown"; + +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/Dropdown", + component: Dropdown, + parameters: { layout: "centered" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const DefaultStory = () => { + const [value, setValue] = useState(""); + return ; +}; + +export const Default: Story = { + args: { items: [], value: "", onChange: () => {} }, + render: () => , +}; diff --git a/packages/timo-design-system/src/components/dropdown/Dropdown.tsx b/packages/timo-design-system/src/components/dropdown/Dropdown.tsx new file mode 100644 index 0000000..c36c43a --- /dev/null +++ b/packages/timo-design-system/src/components/dropdown/Dropdown.tsx @@ -0,0 +1,115 @@ +import { cn } from "@lib"; +import { useEffect, useRef, useState } from "react"; + +export interface DropdownProps { + items: string[]; + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +} + +export const Dropdown = ({ + items, + value, + onChange, + placeholder = "기본", + className, +}: DropdownProps) => { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + const handleSelect = (item: string) => { + onChange(item); + setOpen(false); + }; + + useEffect(() => { + if (!open) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(false); + }; + const handleClickOutside = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + setOpen(false); + } + }; + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [open]); + + return ( +
+ + + {open && ( +
    + {items.map((item, i) => ( +
  • + handleSelect(item)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") handleSelect(item); + }} + className="typo-body-m-12 text-timo-gray-900 cursor-pointer" + > + {item} + + {i < items.length - 1 && ( +
    + )} +
  • + ))} +
+ )} +
+ ); +}; diff --git a/packages/timo-design-system/src/components/index.ts b/packages/timo-design-system/src/components/index.ts index 7917669..17c5644 100644 --- a/packages/timo-design-system/src/components/index.ts +++ b/packages/timo-design-system/src/components/index.ts @@ -1,4 +1,5 @@ export { Checkbox } from "@components/checkbox/Checkbox"; +export { Dropdown } from "@components/dropdown/Dropdown"; export { Color } from "@components/color/Color"; export { Typography } from "@components/typography/Typography"; export { Tag } from "@components/tag/Tag"; From db3ca5605a64e0b997353f7891424dfb0753dfd2 Mon Sep 17 00:00:00 2001 From: Lee Hye Won Date: Thu, 2 Jul 2026 16:59:04 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor(design-system):=20Dropdown=20boole?= =?UTF-8?q?an=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B0=8F=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit open을 isOpen으로 변경하고 닫힐 때 트리거 버튼으로 포커스를 복귀했습니다. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/dropdown/Dropdown.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/timo-design-system/src/components/dropdown/Dropdown.tsx b/packages/timo-design-system/src/components/dropdown/Dropdown.tsx index c36c43a..cc51879 100644 --- a/packages/timo-design-system/src/components/dropdown/Dropdown.tsx +++ b/packages/timo-design-system/src/components/dropdown/Dropdown.tsx @@ -16,25 +16,31 @@ export const Dropdown = ({ placeholder = "기본", className, }: DropdownProps) => { - const [open, setOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); + const triggerRef = useRef(null); const handleSelect = (item: string) => { onChange(item); - setOpen(false); + setIsOpen(false); + }; + + const closeWithFocus = () => { + setIsOpen(false); + triggerRef.current?.focus(); }; useEffect(() => { - if (!open) return; + if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpen(false); + if (e.key === "Escape") closeWithFocus(); }; const handleClickOutside = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { - setOpen(false); + closeWithFocus(); } }; document.addEventListener("keydown", handleKeyDown); @@ -43,22 +49,23 @@ export const Dropdown = ({ document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("mousedown", handleClickOutside); }; - }, [open]); + }, [isOpen]); return (
- {open && ( + {isOpen && (