docs(storybook): simplify sidebar Story pages around the UI

Drop the category kicker from the 83 sidebar Stories (kicker is now optional;
hidden verification Stories keep it) and shorten or remove repeated, obvious
or internal page/section descriptions. Required specs, limits, virtual-data
and undecided-value notices stay. Story IDs, exports, fixtures, shared state
notes and package code are unchanged. Record: docs/reports/storybook-ui-simplification-2026-10-22.md
This commit is contained in:
shkim4 committed 2026-10-22 16:13:58 +09:00
1 parent aa8b7f8df1
commit da0e647b49
70 files changed
+305 -333

No files matched your search

+4 -5
View File
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative "Accordion" Story puts each Radix mode on one page with real, clickable items. The same sections also exist
// as Stories tagged `verification`, which the sidebar hides by default (see .storybook/main.ts). Modes use the Radix API
@@ -126,12 +126,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Accordion"
description="제목을 눌러 관련 내용을 펼치고 접는 목록입니다. 동작은 Radix API(type, collapsible, defaultValue, disabled)로 정하며 HM variant prop은 없습니다."
description="동작은 Radix API · HM variant prop 없음"
/>
<StorySection title="Overview" description="기본 Accordion입니다.">{board("single")}</StorySection>
<StorySection title="Variants" description="모두 접을 수 있는 single과, 여러 항목을 동시에 여는 multiple입니다.">
<StorySection title="Overview">{board("single")}</StorySection>
<StorySection title="Variants">
{board("collapsible-single", "multiple")}
</StorySection>
<StorySection title="States">{board("disabled")}</StorySection>
+4 -5
View File
@@ -11,7 +11,7 @@ import {
AlertDialogTrigger,
Button,
} from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Alert Dialog is a modal Radix overlay (Portal) for a decision that must be confirmed or cancelled: no Close (X), no outside
// dismiss. The representative Story opens the real dialogs; the same cases start open in the verification Stories.
@@ -74,14 +74,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Alert Dialog"
description="반드시 확인하거나 취소해야 하는 결정을 묻는 모달입니다. 닫기(X)와 바깥 클릭 닫기가 없습니다."
description="닫기(X)·바깥 클릭 닫기 없음"
/>
<StorySection title="Overview" description="일반 확인창은 같은 크기의 Solid 버튼 세트입니다. 버튼을 눌러 엽니다.">
<StorySection title="Overview" description="같은 크기 Solid 버튼 세트">
<Confirmation />
</StorySection>
<StorySection title="Variants" description="삭제 확인창은 취소와 삭제 모두 같은 크기의 Standard Solid 세트입니다.">
<StorySection title="Variants" description="취소·삭제 같은 크기 Standard Solid">
<Destructive />
</StorySection>
</StoryPage>
+3 -4
View File
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { InfoIcon, TriangleAlertIcon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
const meta = {
title: "Components/Feedback/Alert",
@@ -27,9 +27,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Alert"
description="화면 안에 놓이는 인라인 메시지입니다(Toast 아님). variant는 default(neutral)와 destructive만 두며 닫기 동작은 없습니다."
description="인라인 메시지(Toast 아님) · default / destructive만 · 닫기 없음"
/>
<StorySection title="Overview">
<StateBoard
@@ -67,7 +66,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Usage" description="아이콘 없음, 제목만, 긴 내용 구성입니다.">
<StorySection title="Usage">
<StateBoard
min={320}
states={[
+4 -5
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { AspectRatio, Card, CardContent } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// AspectRatio is a layout primitive: it keeps the ratio and nothing else. The image styling (object-fit, radius) in these Stories is
// the consumer's composition on the child, not part of AspectRatio. Images are inline data URIs (no network). The Card example only
@@ -68,13 +68,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Aspect Ratio"
description="콘텐츠를 지정한 가로세로 비율로 유지하는 컨테이너입니다. 폭이 바뀌어도 비율대로 높이가 정해집니다."
description="폭에 따라 비율대로 높이 결정"
/>
<StorySection title="Overview" description="ratio = 16 / 9.">{board("r169")}</StorySection>
<StorySection title="Variants" description="ratio = 4 / 3, 1과 영상 자리 표시입니다.">{board("r43", "r11", "video")}</StorySection>
<StorySection title="Usage" description="Card 안의 이미지 비율 조합입니다.">{board("card")}</StorySection>
<StorySection title="Variants" description="ratio 4 / 3 · 1 · 영상 자리">{board("r43", "r11", "video")}</StorySection>
<StorySection title="Usage">{board("card")}</StorySection>
</StoryPage>
),
}
+3 -4
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Avatar, AvatarFallback, AvatarImage } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Images are inline data URIs so the Story does not depend on the network. "Broken" uses an address that cannot load so the
// Radix fallback shows. Radix renders the image only after it loads.
@@ -86,13 +86,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Avatar"
description="사용자를 나타내는 원형 이미지입니다. 이미지를 불러오지 못하면 fallback 글자를 표시합니다."
description="이미지 실패 시 fallback 글자"
/>
<StorySection title="Overview">{board("image")}</StorySection>
<StorySection title="States">{board("fallback")}</StorySection>
<StorySection title="Usage" description="이름·소속과 함께 쓰는 소비 조합입니다.">{board("composition")}</StorySection>
<StorySection title="Usage">{board("composition")}</StorySection>
</StoryPage>
),
}
+4 -5
View File
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { CheckIcon } from "lucide-react"
import { Badge } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
const variants = ["default", "secondary", "destructive", "outline"] as const
@@ -29,11 +29,10 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Badge"
description="상태나 분류를 짧게 표시하는 라벨입니다. Button variant를 재사용하지 않는 별도 컴포넌트입니다."
description="Button variant와 별도인 컴포넌트"
/>
<StorySection title="Overview" description="variant를 생략한 기본 Badge(primary)입니다.">
<StorySection title="Overview" description="기본값: primary">
<Badge>Badge</Badge>
</StorySection>
<StorySection title="Variants" description="default(primary) / secondary(neutral) / destructive / outline.">
@@ -70,7 +69,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Usage" description="긴 텍스트와 링크(asChild)입니다.">
<StorySection title="Usage">
<StateBoard
min={280}
states={[
+3 -4
View File
@@ -14,7 +14,7 @@ import {
DropdownMenuTrigger,
} from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative "Breadcrumb" Story shows each case on one page (real links with sample hrefs); the same specimens also exist
// as Stories tagged `verification` (hidden from the sidebar). BreadcrumbEllipsis is presentational: the Dropdown Menu example is
@@ -133,12 +133,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Breadcrumb"
description="현재 페이지까지의 상위 경로를 보여주는 내비게이션입니다. 마지막 항목은 현재 페이지입니다."
description="마지막 항목 = 현재 페이지"
/>
<StorySection title="Overview">{board("basic")}</StorySection>
<StorySection title="Variants" description="중간 경로 생략(Ellipsis)과 사용자 링크 · 구분자입니다.">{board("ellipsis", "custom")}</StorySection>
<StorySection title="Variants" description="Ellipsis · 사용자 링크 · 구분자">{board("ellipsis", "custom")}</StorySection>
<StorySection title="Usage">{board("long")}</StorySection>
</StoryPage>
),
+6 -10
View File
@@ -3,7 +3,7 @@ import { DownloadIcon } from "lucide-react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Button, type ButtonAppearance } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Story structure follows ui_viz (implementations/web/storybook/Button.stories.jsx):
// one representative "Button" Story shows kinds, sizes and states on a single screen; the same sections also exist
@@ -222,21 +222,17 @@ export const All: Story = {
parameters: { layout: "fullscreen" },
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Button"
description="사용자가 동작을 실행하는 기본 컨트롤입니다. variant는 역할(색), appearance는 Brand / Standard Solid / Standard Outline, size는 높이를 정합니다."
/>
<StorySection title="Overview" description="props를 생략한 기본 Button: Brand · Primary(Green) · Medium 34px.">
<StoryPageHeader title="Button" description="variant = 역할(색) · appearance = Brand / Standard Solid / Standard Outline · size = 높이" />
<StorySection title="Overview" description="기본값: Brand · Primary · Medium 34px">
<Button>Button</Button>
</StorySection>
<StorySection title="Sizes" description="Small 28 / Medium 34 / Large 42px · 글자 12/18 · 13/20 · 15/22. 위는 Brand, 아래는 Standard Solid입니다.">
<StorySection title="Sizes" description="28 / 34 / 42px · 위 Brand, 아래 Standard Solid">
<SizesBoard />
</StorySection>
<StorySection title="Variants" description="appearance별로 허용된 역할 조합입니다.">
<StorySection title="Variants" description="appearance별 허용 역할 조합">
<RolesBoard />
</StorySection>
<StorySection title="States" description="Default / Hover / Pressed / Disabled. Hover·Pressed는 Storybook이 고정한 상태입니다.">
<StorySection title="States" description="Hover·Pressed는 Storybook 고정 상태">
<StateBoard
min={1200}
states={[
+3 -4
View File
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import type { DateRange } from "react-day-picker"
import { Calendar } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative "Calendar" Story shows each mode with real, clickable calendars; the same specimens also exist as Stories
// tagged `verification` (hidden from the sidebar). `today` is pinned to 2026-10-02 so the Story looks the same on any day; this
@@ -70,13 +70,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Calendar"
description="날짜를 고르는 달력입니다. 기본 locale은 한국어이며, 이 화면의 오늘은 2026-10-02로 고정되어 있습니다."
description="기본 locale 한국어 · 오늘은 2026-10-02로 고정"
/>
<StorySection title="Overview" description='mode="single"'>{board("single")}</StorySection>
<StorySection title="Variants" description='mode="range"와 여러 달 표시(numberOfMonths=2)입니다.'>{board("range", "months")}</StorySection>
<StorySection title="States" description="선택할 수 없는 날짜(disabled)입니다.">{board("disabled")}</StorySection>
<StorySection title="States">{board("disabled")}</StorySection>
</StoryPage>
),
}
+4 -5
View File
@@ -4,7 +4,7 @@ import { Button } from "@hm-design/ui"
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardSurface } from "@hm-design/ui"
import { Input } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Story structure follows Button / Input / Select: one representative "Card" Story shows the ui_viz cases on one page,
// the same specimens also exist as separate Stories tagged `verification` (hidden from the sidebar, see .storybook/main.ts).
@@ -71,14 +71,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Card"
description="제목과 본문을 하나의 면으로 묶는 콘텐츠 컨테이너입니다(ui_viz Content Card). Default 상태만 정의되어 있습니다."
description="Default 상태만 정의"
/>
<StorySection title="Overview" description="surface를 생략한 기본 Card(White)입니다.">
<StorySection title="Overview" description="기본값: White">
<StateBoard min={360} states={[{ id: "surface", label: "Surface · White (기본)", children: <Specimen /> }]} />
</StorySection>
<StorySection title="Variants" description="surface: content(Beige)와 제목 옆 Action입니다.">
<StorySection title="Variants" description="content(Beige) · 제목 옆 Action">
<StateBoard
min={360}
states={[
+4 -5
View File
@@ -2,7 +2,7 @@ import { useEffect, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { AspectRatio, Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative Story shows real, draggable carousels; the same specimens also exist as Stories tagged `verification`
// (hidden from the sidebar). Slide widths (`basis-*`) are the consumer's `className` on CarouselItem; Card and AspectRatio are
@@ -152,13 +152,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Carousel"
description="여러 슬라이드를 끌기·버튼으로 넘겨 보는 영역입니다. 슬라이드 폭(basis-*)은 CarouselItem의 className으로 정합니다."
description="슬라이드 폭(basis-*)은 CarouselItem className"
/>
<StorySection title="Overview">{board("basic")}</StorySection>
<StorySection title="Variants" description="여러 항목 표시, 반복(loop), 세로 방향입니다.">{board("multiple", "loop", "vertical")}</StorySection>
<StorySection title="Usage" description="AspectRatio와 조합한 이미지 슬라이드입니다.">{board("media")}</StorySection>
<StorySection title="Variants" description="여러 항목 · loop · 세로">{board("multiple", "loop", "vertical")}</StorySection>
<StorySection title="Usage">{board("media")}</StorySection>
</StoryPage>
),
}
+3 -4
View File
@@ -2,7 +2,7 @@ import { useId, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Checkbox, Label } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Current HM Checkbox. ui-viz has no Checkbox spec, so it was reference only.
// The option label is the HM Label; its foreground role for Checkbox options is still TBD in ui_viz.
@@ -48,9 +48,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Checkbox"
description="항목을 선택하거나 해제하는 컨트롤입니다. 18px 박스이며 Checked·Indeterminate는 state-active 색입니다."
description="18px 박스 · Checked·Indeterminate는 state-active 색"
/>
<StorySection title="Overview">
<StateBoard
@@ -82,7 +81,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시합니다.">
<StorySection title="Validation" description="aria-invalid → Error 테두리">
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Option label="Option">{(id) => <Checkbox id={id} aria-invalid />}</Option> }]} />
</StorySection>
<StorySection title="Usage">
+3 -4
View File
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { ChevronDownIcon } from "lucide-react"
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative "Collapsible" Story shows one independent disclosure per state with real, clickable triggers. The same
// sections also exist as Stories tagged `verification` (hidden from the sidebar). Collapsible is a single disclosure: no divider
@@ -81,13 +81,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Collapsible"
description="하나의 영역을 펼치고 접는 단일 disclosure입니다. 구분선 체계와 애니메이션은 없습니다."
description="구분선·애니메이션 없음"
/>
<StorySection title="Overview">{board("closed")}</StorySection>
<StorySection title="States">{board("open", "disabled")}</StorySection>
<StorySection title="Usage" description="긴 콘텐츠와, Button을 trigger로 쓰는 제어형 조합입니다.">{board("long", "controlled")}</StorySection>
<StorySection title="Usage">{board("long", "controlled")}</StorySection>
</StoryPage>
),
}
+4 -5
View File
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Combobox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, type ComboboxOption } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Combobox is a Popover + Command composition (search + single choice); Select stays for short lists without search.
// The representative Story shows real comboboxes; the same specimens also exist as Stories tagged `verification` (hidden from the
@@ -122,12 +122,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Combobox"
description="검색해서 하나를 고르는 Popover + Command 조합입니다. 검색이 필요 없는 짧은 목록은 Select를 씁니다."
description="Popover + Command · 짧은 목록은 Select 사용"
/>
<StorySection title="Overview" description="Trigger는 dialog 팝업을 열고, 내부 검색 입력이 listbox를 제어합니다. Enter/Space로 열기, 방향키·Enter로 선택, Escape로 닫기 후 Trigger로 focus가 복귀합니다.">{board("empty")}</StorySection>
<StorySection title="Sizes" description="size를 생략하면 Large입니다. 같은 size의 Select와 나란히 비교합니다.">
<StorySection title="Overview" description="Escape로 닫으면 Trigger로 focus 복귀">{board("empty")}</StorySection>
<StorySection title="Sizes" description="기본값: Large · 같은 size의 Select와 비교">
<StateBoard
min={320}
states={[
+2 -3
View File
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { CalendarIcon, SettingsIcon, UserIcon } from "lucide-react"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative "Command" Story shows real, typeable lists; the same specimens also exist as Stories tagged `verification`
// (hidden from the sidebar). Filtering is cmdk's. The bordered box is Story composition (Command draws no border of its own).
@@ -100,9 +100,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Command"
description="입력한 검색어로 항목을 걸러 고르는 목록입니다. Combobox와 명령 팔레트의 기반입니다."
description="Combobox와 명령 팔레트의 기반"
/>
<StorySection title="Overview">{board("basic")}</StorySection>
<StorySection title="Variants">{board("grouped")}</StorySection>
+3 -4
View File
@@ -16,7 +16,7 @@ import {
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Context Menu opens from pointer context (right click, the context-menu key / Shift+F10, long press) on its trigger area.
// The representative Story is a target area: right click it. Radix owns keyboard navigation, focus return and Escape.
@@ -83,11 +83,10 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Context Menu"
description="영역에서 오른쪽 클릭, 컨텍스트 메뉴 키 / Shift+F10, 길게 누르기로 여는 메뉴입니다."
description="오른쪽 클릭 · Shift+F10 · 길게 누르기로 열기"
/>
<StorySection title="Overview" description="항목 · Disabled · 단축키 · Checkbox · Radio Group · 하위 메뉴 · 위험 항목을 한 메뉴에서 확인합니다. 점선 영역에서 오른쪽 클릭합니다.">
<StorySection title="Overview" description="점선 영역에서 오른쪽 클릭">
<Full />
</StorySection>
</StoryPage>
+4 -5
View File
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { DatePicker } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// DatePicker is a Popover + Calendar composition packaged as a component (value handling, trigger text, close on select).
// The representative Story shows real pickers; the same specimens also exist as Stories tagged `verification` (hidden from the
@@ -116,14 +116,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Date Picker"
description="Trigger를 눌러 달력에서 날짜를 고르는 Popover + Calendar 조합 컴포넌트입니다. 날짜를 고르면 닫힙니다."
description="Popover + Calendar · 날짜 선택 시 닫힘"
/>
<StorySection title="Overview">{board("empty")}</StorySection>
<StorySection title="Sizes" description="size를 생략하면 Large입니다.">{board("medium")}</StorySection>
<StorySection title="Sizes" description="기본값: Large">{board("medium")}</StorySection>
<StorySection title="States">{board("selected", "controlled", "disabled-dates", "disabled")}</StorySection>
<StorySection title="Usage" description="기본 표시 형식과 사용자 형식 지정입니다.">{board("format")}</StorySection>
<StorySection title="Usage">{board("format")}</StorySection>
</StoryPage>
),
}
+4 -6
View File
@@ -4,7 +4,7 @@ import { Button } from "@hm-design/ui"
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@hm-design/ui"
import { Input } from "@hm-design/ui"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Dialog is an overlay (Radix Portal), so it does not fit the StateBoard. The representative Story is a row of triggers that open
// the real Dialog for each ui_viz case (dialog.md §9 Reference Viewer examples); the same cases also exist as separate Stories tagged
@@ -182,21 +182,19 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Dialog"
description="현재 화면 위에 내용을 띄워 확인·입력을 받는 모달입니다. 각 버튼을 눌러 실제 Dialog를 엽니다."
/>
<StorySection title="Overview" description="짧은 내용의 Dialog는 내용 기준 크기입니다.">
<StorySection title="Overview" description="내용 기준 크기">
<Basic />
</StorySection>
<StorySection title="Variants" description="내용이 길거나 넓으면 Body 안에서만 scroll 합니다. 본문 스크롤은 공통 Header 70px / Footer 48px를 뺀 높이를 씁니다.">
<StorySection title="Variants" description="긴 내용은 Body만 scroll · Header 70 / Footer 48px 제외">
<div className="flex flex-wrap gap-3">
<LongContent />
<WideContent />
<BodyScroll />
</div>
</StorySection>
<StorySection title="Usage" description="일반 확인과 삭제 확인 모두 취소·실행을 Standard Solid 세트로 구성합니다.">
<StorySection title="Usage" description="취소·실행은 Standard Solid 세트">
<div className="flex flex-wrap gap-3">
<DeleteConfirmation />
<FormContent />
+4 -5
View File
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
// Usage: preserved native label fixtures do not define a new-screen exception. Use public Label
// for supported fields (docs/components/label.md §5); copy only actions requested by the screen.
import { Button, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, Input } from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Drawer is a modal vaul overlay (Portal). The representative Story opens the real Drawer from its triggers; drag the handle or
// the panel down to dismiss. The same cases start open in the verification Stories. vaul owns the drag, Radix the focus and ARIA.
@@ -101,14 +101,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Drawer"
description="화면 아래에서 올라오는 모달 패널입니다. 핸들이나 패널을 아래로 끌어 닫습니다."
description="핸들·패널을 아래로 끌어 닫기"
/>
<StorySection title="Overview" description="데스크톱: 하단 우측 가로 취소 → 실행. 모바일(640px 이하): 전체 폭 세로 실행 위 / 취소 아래. 같은 Drawer를 화면 폭에 맞춰 확인합니다.">
<StorySection title="Overview" description="데스크톱: 우측 가로 취소 → 실행 · 640px 이하: 세로 실행 위 / 취소 아래">
<Basic />
</StorySection>
<StorySection title="Usage" description="폼 입력 조합과, 내용이 길 때 Body만 scroll 하는 경우입니다.">
<StorySection title="Usage" description="긴 내용은 Body만 scroll">
<div className="flex flex-wrap gap-3">
<FormCase />
<LongCase />
+2 -4
View File
@@ -17,7 +17,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Dropdown Menu is a Radix overlay (Portal), so the representative Story is a trigger that opens the real menu, the same way as
// Dialog. The verification Story starts open. Radix owns keyboard navigation, focus return and Escape.
@@ -85,11 +85,9 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Dropdown Menu"
description="버튼으로 여는 작업 메뉴입니다. 키보드 이동, Esc 닫기, 포커스 복귀는 Radix가 담당합니다."
/>
<StorySection title="Overview" description="항목 · Disabled · 구분선 · Checkbox · Radio Group · 하위 메뉴 · 위험 항목을 한 메뉴에서 확인합니다. 버튼을 눌러 엽니다.">
<StorySection title="Overview" description="항목 · Disabled · 구분선 · Checkbox · Radio · 하위 메뉴 · 위험 항목">
<Full />
</StorySection>
</StoryPage>
+4 -5
View File
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Hover Card is a Radix overlay (Portal) for a richer preview than Tooltip. The representative Story shows real triggers
// (hover or focus to open); the verification Story starts one open. Radix owns the open / close delay and the hover bridge.
@@ -54,14 +54,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Hover Card"
description="링크에 포인터를 올리거나 포커스하면 Tooltip보다 풍부한 미리보기를 띄우는 overlay입니다."
description="링크 hover / focus 시 미리보기"
/>
<StorySection title="Overview" description="링크 위에 포인터를 올려 미리보기를 엽니다.">
<StorySection title="Overview">
<Preview />
</StorySection>
<StorySection title="Usage" description="긴 콘텐츠는 카드 폭에서 줄바꿈되고 높이만 늘어납니다.">
<StorySection title="Usage" description="카드 폭 고정 · 높이만 늘어남">
<LongContent />
</StorySection>
</StoryPage>
+6 -7
View File
@@ -2,7 +2,7 @@ import { useId, type ComponentProps, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Input, type InputSize } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Story structure follows ui_viz (implementations/web/storybook/Input.stories.jsx and InputSizes.stories.jsx):
// one representative "Input" Story shows the sizes and every state on one page; the same specimens also exist as
@@ -126,17 +126,16 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Input"
description="한 줄 텍스트를 입력하는 컨트롤입니다. Label·Helper·Error 문구는 Input이 아니라 소비 조합입니다."
description="Label·Helper·Error 문구는 소비 조합"
/>
<StorySection title="Overview" description="size를 생략한 기본 Input: Large 42px.">
<StorySection title="Overview" description="기본값: Large 42px">
<StateBoard states={[{ id: "default", label: "Default · size 생략(Large)", children: <Specimen placeholder={placeholder} /> }]} />
</StorySection>
<StorySection title="Sizes" description="Small 28 / Medium 34 / Large 42px. 업무 폼·조회 조합은 Medium을 씁니다.">
<StorySection title="Sizes" description="28 / 34 / 42px · 업무 폼·조회는 Medium">
<SizeComparison />
</StorySection>
<StorySection title="States" description="Focus와 Error는 1.5px 테두리로 구분하며 Hover는 없습니다. Disabled는 흰 면을 유지하고 컨트롤과 소비 Label 각각 60%로 표시합니다. Readonly Label은 100%입니다.">
<StorySection title="States" description="Focus·Error 1.5px 테두리 · Hover 없음 · Disabled 60%">
<StateBoard
states={[
{ id: "focus", label: "Focus", children: <Specimen size="medium" placeholder={placeholder} /> },
@@ -145,7 +144,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시하고, 오류 문구는 소비 조합으로 연결합니다.">
<StorySection title="Validation" description="aria-invalid → Error 테두리 · 오류 문구는 소비 조합">
<StateBoard
states={[
{ id: "error", label: "Error", children: <Specimen size="medium" error defaultValue="Invalid value" /> },
+3 -4
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Input, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Label has no standalone ui_viz spec. Its typography and color are the Field Label role owned by Input / Textarea
// (14/20/500, color.fg.secondary). Required star and messages are composed here as a consumer would (not part of Label).
@@ -28,9 +28,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Label"
description="Label은 항목명이고 Input은 입력칸입니다. Field Label 14/20/500을 쓰며, htmlFor로 연결하면 라벨 클릭 시 해당 Input이나 Select로 초점이 이동합니다."
description="Field Label 14/20/500 · htmlFor 연결 시 클릭으로 focus 이동"
/>
<StorySection title="Overview">
<StateBoard
@@ -70,7 +69,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Usage" description="Required 별표는 Label이 아닌 소비 조합입니다. 긴 라벨은 줄바꿈됩니다.">
<StorySection title="Usage" description="Required 별표는 소비 조합">
<div className="mb-4 grid max-w-sm gap-2">
<Label htmlFor="label-select">부서 선택</Label>
<Select><SelectTrigger id="label-select" size="medium"><SelectValue placeholder="부서를 선택하세요" /></SelectTrigger>
+3 -4
View File
@@ -17,7 +17,7 @@ import {
MenubarSubTrigger,
MenubarTrigger,
} from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Menubar is a desktop menu bar (Left / Right between top menus). The representative Story is the real bar and opens the real
// menus; Radix owns the keyboard model and focus. Hidden verification Stories isolate trigger focus and open + focus.
@@ -97,11 +97,10 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Menubar"
description="데스크톱 앱 형태의 상단 메뉴 막대입니다. 메뉴가 열린 상태에서 좌우 방향키로 이웃 메뉴로 이동합니다."
description="열린 상태에서 좌우 방향키로 이웃 메뉴 이동"
/>
<StorySection title="Overview" description="파일(단축키 · Disabled · 하위 메뉴 · 위험 항목), 편집, 보기(Label · Checkbox · Radio)입니다. 메뉴를 눌러 엽니다.">
<StorySection title="Overview" description="단축키 · Disabled · 하위 메뉴 · 위험 항목 · Checkbox · Radio">
<div style={{ minHeight: 384 }}>
<Specimen />
</div>
+2 -3
View File
@@ -10,7 +10,7 @@ import {
NavigationMenuTrigger,
} from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Navigation Menu is for destinations (links), not commands. The representative Story opens the real menus with hover, click or
// keyboard; the same specimens also exist as Stories tagged `verification` (hidden from the sidebar). Title + description children
@@ -102,9 +102,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Navigation Menu"
description="목적지(링크)로 이동하는 메뉴입니다. 명령 실행에는 쓰지 않습니다. hover, 클릭 또는 키보드로 엽니다."
description="링크 이동용 · 명령 실행에는 쓰지 않음"
/>
<StorySection title="Overview">
<StateBoard
+4 -5
View File
@@ -10,7 +10,7 @@ import {
PaginationPrevious,
} from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Pagination is presentational: the consumer owns the page and the hrefs. The Story keeps the page in local state and uses
// `#page-N` hrefs with preventDefault. The representative Story shows each case on one page; the same specimens also exist as
@@ -89,13 +89,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Pagination"
description="여러 페이지로 나뉜 목록을 이동하는 내비게이션입니다. 현재 페이지와 href는 소비 코드가 소유합니다."
description="현재 페이지와 href는 소비 코드가 소유"
/>
<StorySection title="Overview">{board("middle")}</StorySection>
<StorySection title="States" description="첫 페이지와 마지막 페이지에서는 이전·다음이 aria-disabled입니다.">{board("first", "last")}</StorySection>
<StorySection title="Usage" description="페이지가 많으면 생략 부호로 줄입니다.">{board("ellipsis")}</StorySection>
<StorySection title="States" description="처음·끝에서 이전·다음 aria-disabled">{board("first", "last")}</StorySection>
<StorySection title="Usage">{board("ellipsis")}</StorySection>
</StoryPage>
),
}
+5 -6
View File
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Button, Input, Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Popover is a non-modal Radix overlay (Portal). The representative Story opens the real panels from their triggers;
// the verification Story starts one open. Radix owns outside click / Escape close and focus return.
@@ -84,17 +84,16 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Popover"
description="Trigger 옆에 상호작용 가능한 패널을 띄우는 비모달 overlay입니다. 바깥 클릭이나 Esc로 닫힙니다."
description="비모달 · 바깥 클릭이나 Esc로 닫힘"
/>
<StorySection title="Overview" description="버튼을 눌러 안내 패널을 엽니다.">
<StorySection title="Overview">
<Info />
</StorySection>
<StorySection title="Variants" description="align: start / center / end.">
<StorySection title="Variants">
<Aligned />
</StorySection>
<StorySection title="Usage" description="패널 안의 입력 폼 조합입니다. 취소·저장은 PopoverClose로 닫습니다.">
<StorySection title="Usage" description="취소·저장은 PopoverClose로 닫음">
<FormPanel />
</StorySection>
</StoryPage>
+4 -5
View File
@@ -2,7 +2,7 @@ import { useState } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Button, Progress } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Progress follows `value` (0..max). The representative Story shows fixed values plus one interactive value; the same specimens
// also exist as Stories tagged `verification` (hidden from the sidebar). No status colors are defined, so there is one look.
@@ -59,13 +59,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Progress"
description="작업 진행률을 막대로 표시합니다. value(0..max)를 따라 채워집니다."
description="value(0..max) 비율로 채움"
/>
<StorySection title="Overview">{board("middle")}</StorySection>
<StorySection title="States" description="value 0과 100입니다.">{board("zero", "full")}</StorySection>
<StorySection title="Usage" description="max를 바꾼 범위와, value를 바꾸는 실제 조작입니다.">{board("custom-maximum", "interactive")}</StorySection>
<StorySection title="States">{board("zero", "full")}</StorySection>
<StorySection title="Usage">{board("custom-maximum", "interactive")}</StorySection>
</StoryPage>
),
}
+3 -4
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Label, RadioGroup, RadioGroupItem } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Current HM Radio Group. ui-viz has no Radio spec, so it was reference only.
@@ -54,9 +54,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Radio Group"
description="여러 선택지 중 하나를 고르는 컨트롤입니다. 그룹 안에서는 방향키로 이동·선택합니다(Radix roving focus)."
description="그룹 안에서 방향키로 이동·선택"
/>
<StorySection title="Overview">
<StateBoard min={240} states={[{ id: "vertical", label: "Vertical (default) · Checked / Unchecked", children: <Group prefix="v" defaultValue="b" /> }]} />
@@ -75,7 +74,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시합니다.">
<StorySection title="Validation" description="aria-invalid → Error 테두리">
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Group prefix="e" invalid /> }]} />
</StorySection>
</StoryPage>
+3 -5
View File
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative Story shows real, draggable layouts; the same specimens also exist as Stories tagged `verification` (hidden
// from the sidebar). react-resizable-panels v4 sizes: a string without a unit ("30") or with "%" is a percentage, a number is
@@ -110,13 +110,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Resizable"
description="경계선을 끌어 인접한 패널의 크기를 조절하는 레이아웃입니다."
/>
<StorySection title="Overview">{board("horizontal")}</StorySection>
<StorySection title="Variants" description="세로 방향과 grip 표시입니다.">{board("vertical", "handle")}</StorySection>
<StorySection title="Usage" description="최소·최대 크기와 접을 수 있는 패널입니다.">{board("minmax", "collapsible")}</StorySection>
<StorySection title="Variants">{board("vertical", "handle")}</StorySection>
<StorySection title="Usage" description="최소·최대 크기 · 접기">{board("minmax", "collapsible")}</StorySection>
</StoryPage>
),
}
+3 -4
View File
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { ScrollArea, ScrollBar } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Scroll Area is Radix scrolling with a styled scrollbar (type "hover": the bar shows while hovering or scrolling). Sizes below
// are Story layout. The representative Story shows vertical, horizontal, both axes and a table-like content.
@@ -88,12 +88,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Scroll Area"
description="정해진 크기 안에서 내용을 스크롤하는 영역입니다. 스크롤바는 포인터를 올리거나 스크롤할 때 나타납니다."
description="스크롤바는 hover·스크롤 시 표시"
/>
<StorySection title="Overview">{board("vertical")}</StorySection>
<StorySection title="Variants" description="가로와 양방향 스크롤입니다.">{board("horizontal", "both")}</StorySection>
<StorySection title="Variants">{board("horizontal", "both")}</StorySection>
<StorySection title="Usage">{board("table-like")}</StorySection>
</StoryPage>
),
+6 -7
View File
@@ -2,7 +2,7 @@ import { useId, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, type SelectSize } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Story structure follows ui_viz (implementations/web/storybook/Select.stories.jsx and SelectSizes.stories.jsx):
// one representative "Select" Story shows the default, selected, disabled and open states on one page; the same specimens
@@ -151,12 +151,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Select"
description="목록에서 하나의 값을 고르는 컨트롤입니다. Option은 Trigger의 size를 따르고 목록 폭은 Trigger와 같습니다."
description="Option은 Trigger size를 따르고 목록 폭 = Trigger 폭"
/>
{/* Open stays in the first section: the list opens on load, so its trigger must be on the first screen. */}
<StorySection title="Overview" description="size를 생략한 기본 Select(Large 44px)와 열린 목록입니다. 목록은 Escape 또는 바깥 클릭으로 닫히고, Trigger를 클릭하면 다시 열립니다.">
<StorySection title="Overview" description="기본값: Large 44px · 열린 목록">
<StateBoard
fixed
states={[
@@ -173,10 +172,10 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Sizes" description="Medium 34 / Large 44px. Input과 독립된 두 크기입니다.">
<StorySection title="Sizes" description="34 / 44px · Input과 별도 크기">
<SizeComparison />
</StorySection>
<StorySection title="States" description="값이 선택된 상태와 Disabled입니다.">
<StorySection title="States">
<StateBoard
fixed
states={[
@@ -185,7 +184,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Invalid / Focus" description="aria-invalid는 Error 테두리로 표시합니다. Tab으로 Trigger에 포커스해 Invalid + Focus를 확인합니다.">
<StorySection title="Invalid / Focus" description="aria-invalid → Error 테두리">
<StateBoard
fixed
states={[
+2 -3
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Separator } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
const meta = {
title: "Components/Data & Display/Separator",
@@ -26,9 +26,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Separator"
description="콘텐츠 사이를 나누는 선입니다. 제목과 설명을 하나의 묶음으로 두고 구분선은 묶음 뒤 한 곳에 사용합니다. 기본은 장식용(decorative)입니다."
description="구분선은 묶음 뒤 한 곳 · 기본 decorative"
/>
<StorySection title="Overview">
<StateBoard
+4 -6
View File
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
// Usage: preserved native label fixtures do not define a new-screen exception. Use public Label
// for supported fields (docs/components/label.md §5); copy only actions requested by the screen.
import { Button, Input, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Sheet is a modal Radix overlay (Portal), so the representative Story is a row of triggers that open the real Sheet for each
// side and content case. The same cases start open in the verification Stories. Radix owns focus trap, Escape and focus return.
@@ -99,21 +99,19 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Sheet"
description="화면 가장자리에서 열리는 보조 모달 패널입니다. 각 버튼을 눌러 실제 Sheet를 엽니다."
/>
<StorySection title="Overview" description="side를 생략한 기본 방향(right)입니다.">
<StorySection title="Overview" description="기본값: right">
<SideCase side="right" />
</StorySection>
<StorySection title="Variants" description="side: left / top / bottom.">
<StorySection title="Variants">
<div className="flex flex-wrap gap-3">
<SideCase side="left" />
<SideCase side="top" />
<SideCase side="bottom" />
</div>
</StorySection>
<StorySection title="Usage" description="폼 입력 조합과, 내용이 길 때 Body만 scroll 하는 경우입니다.">
<StorySection title="Usage" description="긴 내용은 Body만 scroll">
<div className="flex flex-wrap gap-3">
<FormCase />
<LongCase />
+3 -4
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Card, CardContent, CardHeader, Skeleton } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Skeleton is a visual placeholder; size and shape come from `className` (layout classes here are Story composition).
// There is no data fetching. The representative Story shows text lines, an avatar, and a card / list loading composition.
@@ -64,12 +64,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Skeleton"
description="콘텐츠를 불러오는 동안 자리를 표시하는 placeholder입니다. 크기와 모양은 className으로 정합니다."
description="크기·모양은 className으로 지정"
/>
<StorySection title="Overview">{board("text")}</StorySection>
<StorySection title="Usage" description="Avatar 행과 카드·목록 로딩 조합입니다.">{board("avatar", "card")}</StorySection>
<StorySection title="Usage">{board("avatar", "card")}</StorySection>
</StoryPage>
),
}
+3 -4
View File
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Slider } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative Story shows each Slider mode with real, draggable controls; the same specimens also exist as Stories tagged
// `verification` (hidden from the sidebar). The value label is the Story's composition (Slider has no built-in label).
@@ -82,12 +82,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Slider"
description="막대 위의 thumb을 끌어 범위 안의 값을 고르는 컨트롤입니다. 화살표 키로도 값을 바꿉니다."
description="끌기 또는 화살표 키로 값 변경"
/>
<StorySection title="Overview">{board("single")}</StorySection>
<StorySection title="Variants" description="두 thumb의 범위, step, 세로 방향입니다.">{board("range", "step", "vertical")}</StorySection>
<StorySection title="Variants">{board("range", "step", "vertical")}</StorySection>
<StorySection title="States">{board("disabled")}</StorySection>
</StoryPage>
),
+4 -5
View File
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Button, toast, Toaster } from "@hm-design/ui"
import { StateBoard } from "./story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "./story-page"
import { StoryPage, StoryPageHeader, StorySection } from "./story-page"
// Toast is created with the real Sonner API (`toast`, re-exported by @hm-design/ui); nothing is a static specimen.
// One <Toaster /> is mounted in .storybook/preview.tsx for every Story (an app mounts it once at its root).
@@ -125,14 +125,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Sonner"
description="작업 결과를 잠시 띄우는 Toast 알림입니다. 실제 Sonner API(toast)로 만들며, Toaster는 앱 루트에 한 번 둡니다."
description="Toaster는 앱 루트에 한 번"
/>
<StorySection title="Variants" description="버튼을 눌러 각 유형의 Toast를 띄웁니다.">
<StorySection title="Variants">
<Board ids={["types"]} />
</StorySection>
<StorySection title="Usage" description="Action / Cancel / Close, Promise, 여러 개 쌓기와 긴 메시지입니다.">
<StorySection title="Usage">
<Board ids={["action", "promise", "stack"]} />
</StorySection>
</StoryPage>
+4 -2
View File
@@ -13,10 +13,12 @@ export function StoryPage({ className, children }: { className?: string; childre
return <div className={className ? `sb-page ${className}` : "sb-page"}><div className="sb-page-inner">{children}</div></div>
}
export function StoryPageHeader({ kicker, title, description }: { kicker: string; title: string; description?: ReactNode }) {
// Representative (sidebar) Stories omit `kicker`: the sidebar path already names the category.
// Hidden verification Stories keep passing it so their page structure stays unchanged.
export function StoryPageHeader({ kicker, title, description }: { kicker?: string; title: string; description?: ReactNode }) {
return (
<header className="sb-page-header">
<p className="sb-page-kicker">{kicker}</p>
{kicker ? <p className="sb-page-kicker">{kicker}</p> : null}
<h1 className="sb-page-title">{title}</h1>
{description ? <p className="sb-page-description">{description}</p> : null}
</header>
+2 -3
View File
@@ -2,7 +2,7 @@ import { useId, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Label, Switch } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
function Option({ label, children }: { label: string; children: (id: string) => ReactNode }) {
const id = useId()
@@ -41,9 +41,8 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Switch"
description="설정을 즉시 켜고 끄는 컨트롤입니다. 트랙 36×20, 썸 16이며 On은 state-active 색입니다."
description="트랙 36×20 · 썸 16 · On은 state-active 색"
/>
<StorySection title="Overview">
<StateBoard
+3 -4
View File
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Badge, Checkbox, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Table is a static display primitive, so the representative Story is a board of specimens. Checkbox and Badge appear only as
// cell content (Story composition); Table has no dependency on them and no selection, sorting or paging behavior.
@@ -153,13 +153,12 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Table"
description="행과 열로 데이터를 보여주는 정적 표시 primitive입니다. 정렬·선택·페이지 로직은 Data Table Pattern이 담당합니다."
description="정적 표시 · 정렬·선택·페이지는 Data Table Pattern"
/>
<StorySection title="Overview">{board("basic")}</StorySection>
<StorySection title="States">{board("selected")}</StorySection>
<StorySection title="Usage" description="긴 텍스트와 가로 넘침입니다.">{board("long", "wide")}</StorySection>
<StorySection title="Usage">{board("long", "wide")}</StorySection>
</StoryPage>
),
}
+9 -10
View File
@@ -6,7 +6,7 @@ import {
Tabs, TabsContent, TabsList, TabsTrigger,
} from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Representative "Tabs" Story shows default Primary, explicit API comparisons and Secondary; Legacy stays verification-only.
// D-01 applies to every Fill specimen: flat panels use the actual parent surface and padding 24px 0.
@@ -147,14 +147,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Tabs"
description="같은 영역의 콘텐츠를 전환하는 탭입니다. 선택된 탭은 하단으로 콘텐츠 면과 이어지며, 넘칠 때만 이전/다음 이동 버튼이 나타납니다."
description="선택 탭은 콘텐츠 면과 이어짐 · 넘칠 때만 이전/다음 버튼"
/>
<StorySection title="Overview">
<StateBoard
states={[
{ id: "primary", label: "Primary · 기본 TabsList", noteAfter: true, note: "fill / panelSurface 생략 → Primary. 선택 면이 위 White alpha 0.8 → 아래 content panel색 gradient로 콘텐츠와 이어집니다. 탭 전체 opacity는 1이며 비선택 hover는 기존 Black 20% overlay입니다.", children: <Specimen /> },
{ id: "primary", label: "Primary · 기본 TabsList", noteAfter: true, note: "fill / panelSurface 생략 → Primary · 비선택 Hover Black 20%", children: <Specimen /> },
]}
/>
</StorySection>
@@ -169,24 +168,24 @@ export const All: Story = {
<StorySection title="Variants">
<StateBoard
states={[
{ id: "secondary", label: "Secondary · Fill + panelSurface surface", noteAfter: true, note: "서브형. Surface(White) 위에서 사용하며 선택 면이 White로 이어집니다.", children: <Specimen variant="secondary" /> },
{ id: "secondary", label: "Secondary · Fill + panelSurface surface", noteAfter: true, note: "서브형 · Surface(White) 위에서 사용", children: <Specimen variant="secondary" /> },
]}
/>
</StorySection>
<StorySection title="States">
<StateBoard
states={[
{ id: "overflow", wide: true, label: "Overflow · Primary + 이동 버튼", noteAfter: true, note: "일반 화면 폭에서 20개 탭의 넘침을 확인합니다. 이전/다음 버튼은 선택을 바꾸지 않고 목록만 이동합니다. 모바일에서도 같은 내부 스크롤을 유지합니다.", children: <div className="w-full min-w-0 max-w-4xl"><Specimen count={20} /></div> },
{ id: "overflow", wide: true, label: "Overflow · Primary + 이동 버튼", noteAfter: true, note: "20개 탭 · 이전/다음은 선택을 바꾸지 않고 목록만 이동", children: <div className="w-full min-w-0 max-w-4xl"><Specimen count={20} /></div> },
]}
/>
</StorySection>
<StorySection title="Usage" description="실제 상위 표면에 panelSurface를 맞추고 TabsContent의 중복 박스를 제거합니다. px-0 py-6는 이 조합의 예시이며 공통 spacing 규칙이 아닙니다.">
<StorySection title="Usage" description="panelSurface를 상위 표면에 맞춤 · px-0 py-6은 예시이며 공통 규칙 아님">
<StateBoard states={[
{ id: "usage-card", label: "Card 내부 · Secondary", noteAfter: true, note: "바깥 Card의 배경·테두리·기본 여백 24px은 유지합니다. 내부 패널은 배경 transparent·border 0·padding 24px 0이며 입력 요소의 테두리를 보존합니다.", children: <SurfaceUsage inCard /> },
{ id: "usage-page", label: "페이지 직접 배치 · Primary", noteAfter: true, note: "기존 Layout의 content 배경을 사용하는 조합입니다. 추가 흰색 박스를 만들지 않습니다. 일정 탭의 Table은 고유 표면과 내부 스크롤을 유지하고 입력값은 탭 전환 후에도 보존됩니다.", children: <SurfaceUsage /> },
{ id: "usage-card", label: "Card 내부 · Secondary", noteAfter: true, note: "바깥 Card 유지 · 내부 패널 transparent · border 0 · padding 24px 0", children: <SurfaceUsage inCard /> },
{ id: "usage-page", label: "페이지 직접 배치 · Primary", noteAfter: true, note: "Layout content 배경 사용 · 추가 흰색 박스 없음", children: <SurfaceUsage /> },
]} />
</StorySection>
<StorySection title="Do / Don't" description="Card 전용 Token을 화면 전체 간격에 사용하거나 모든 제목·설명 간격을 24px로 통일하지 않습니다.">
<StorySection title="Do / Don't" description="Card Token을 화면 전체 간격에 쓰지 않음">
<p className="text-sm text-muted-foreground">Do: Card 또는 Layout이 외곽 여백을 소유하고 내부 Input·Table·Alert의 고유 스타일을 유지합니다. Don't: 패널 박스 제거를 모든 border 제거로 해석하거나, 그라데이션 alpha 조정을 탭 전체 opacity 변경으로 구현하지 않습니다. 실제 패널 계산값·키보드 동작·모바일 넘침을 확인합니다.</p>
</StorySection>
</StoryPage>
+5 -6
View File
@@ -2,7 +2,7 @@ import { useId, type ComponentProps, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Textarea } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Story structure follows Input: one representative "Textarea" Story shows every state on one page; the same specimens
// also exist as separate Stories tagged `verification`, hidden from the sidebar by default (see .storybook/main.ts).
@@ -87,14 +87,13 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Textarea"
description="여러 줄 텍스트를 입력하는 컨트롤입니다. 최소 높이 112px, resize 기본 vertical이며 size 체계는 없습니다. Label·Helper·Error 문구는 소비 조합입니다."
description="최소 높이 112px · resize vertical · size 없음 · Label·Helper·Error는 소비 조합"
/>
<StorySection title="Overview">
<StateBoard states={[{ id: "default", label: "Default", children: <Specimen placeholder={placeholder} /> }]} />
</StorySection>
<StorySection title="States" description="Focus는 1.5px 테두리로 구분하며 shadow·glow를 쓰지 않습니다. Hover는 Default와 같습니다.">
<StorySection title="States" description="Focus 1.5px 테두리(shadow 없음) · Hover = Default">
<StateBoard
states={[
{ id: "hover", label: "Hover (= Default)", note: hoverNote, noteAfter: true, children: <Specimen defaultValue="Hover over this field" /> },
@@ -104,7 +103,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시하고, 오류 문구는 소비 조합으로 연결합니다.">
<StorySection title="Validation" description="aria-invalid → Error 테두리 · 오류 문구는 소비 조합">
<StateBoard
states={[
{ id: "error", label: "Error", children: <Specimen error defaultValue="Invalid value" /> },
@@ -112,7 +111,7 @@ export const All: Story = {
]}
/>
</StorySection>
<StorySection title="Usage" description="줄바꿈 유지와 resize 해제입니다.">
<StorySection title="Usage">
<StateBoard
states={[
{ id: "multiline", label: "Multiline · Wrap", children: <Specimen defaultValue={multiline} /> },
+4 -5
View File
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"
import { ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Toggle Group is a group of pressed-state items (toolbar-style), not a content switcher (that is Tabs). The representative Story
// shows single / multiple / icon-only / disabled / vertical / spaced groups with real controls; the same specimens also exist as
@@ -137,15 +137,14 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Toggle Group"
description="눌림 상태를 갖는 항목 묶음(툴바형)입니다. 내용을 전환하는 용도는 Tabs를 씁니다."
description="툴바형 눌림 항목 묶음 · 내용 전환은 Tabs 사용"
/>
<StorySection title="Overview">{board("single")}</StorySection>
<StorySection title="Sizes" description="Icon-only / Icon + Text / Text-only.">
<StorySection title="Sizes">
<Sizes />
</StorySection>
<StorySection title="Variants" description="다중 선택, 세로 방향, 항목 간격(spacing)입니다.">{board("multiple", "vertical", "spaced")}</StorySection>
<StorySection title="Variants">{board("multiple", "vertical", "spaced")}</StorySection>
<StorySection title="States">{board("disabled")}</StorySection>
<StorySection title="Usage">{board("many")}</StorySection>
</StoryPage>
+5 -6
View File
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { BoldIcon, ItalicIcon, StarIcon } from "lucide-react"
import { Toggle, ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Toggle is a persistent pressed-state control. The representative Story shows each state with real, clickable toggles; the same
// specimens also exist as Stories tagged `verification` (hidden from the sidebar). Icon-only toggles carry an `aria-label`.
@@ -108,12 +108,11 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Toggle"
description="Toggle과 Toggle Group은 눌림(On) 상태를 공유합니다. Default·Outline, Off·On·Disabled와 키보드 Focus를 같은 기준으로 비교합니다."
description="Toggle Group과 같은 눌림(On) 상태 기준"
/>
<StorySection title="Overview">{board("offon")}</StorySection>
<StorySection title="단독 · 연결 그룹" description="단독 Toggle은 독립 선택입니다. spacing=0 그룹은 연결 형태이며 single은 하나, multiple은 여러 항목을 선택합니다.">
<StorySection title="단독 · 연결 그룹" description="단독 = 독립 선택 · spacing=0 그룹 = 연결 형태">
<div className="grid gap-4">
<div><p className="mb-2">단독 · 독립 선택</p><Toggle variant="outline">알림</Toggle></div>
<div><p className="mb-2">연결 그룹 · 단일 선택</p><ToggleGroup type="single" variant="outline" defaultValue="left" aria-label="정렬"><ToggleGroupItem value="left">왼쪽</ToggleGroupItem><ToggleGroupItem value="center">가운데</ToggleGroupItem><ToggleGroupItem value="right">오른쪽</ToggleGroupItem></ToggleGroup></div>
@@ -123,9 +122,9 @@ export const All: Story = {
<StorySection title="Sizes">
<Sizes />
</StorySection>
<StorySection title="Variants" description="아이콘만, 아이콘 + 텍스트입니다.">{board("icon")}</StorySection>
<StorySection title="Variants">{board("icon")}</StorySection>
<StorySection title="States">{board("disabled", "controlled")}</StorySection>
<StorySection title="Usage" description="className 확장입니다.">{board("extended")}</StorySection>
<StorySection title="Usage">{board("extended")}</StorySection>
</StoryPage>
),
}
+5 -6
View File
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hm-design/ui"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Tooltip is a Radix overlay (Portal) with short, non-interactive text. The representative Story shows real triggers
// (hover or focus to open); the verification Story starts one open. Interactive content belongs in Popover / Hover Card.
@@ -77,17 +77,16 @@ export const All: Story = {
<TooltipProvider>
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.components}
title="Tooltip"
description="짧고 상호작용 없는 설명을 띄우는 overlay입니다. Trigger에 포인터를 올리거나 포커스하면 열립니다."
description="상호작용 없는 짧은 설명 · hover 또는 focus로 열림"
/>
<StorySection title="Overview" description="기본 Tooltip입니다.">
<StorySection title="Overview">
<Basic />
</StorySection>
<StorySection title="Variants" description="side: top / right / bottom / left.">
<StorySection title="Variants">
<Placements />
</StorySection>
<StorySection title="Usage" description="긴 문구는 줄바꿈됩니다. Disabled 버튼은 포인터 이벤트가 없으므로 감싼 요소를 Trigger로 씁니다.">
<StorySection title="Usage" description="Disabled 버튼은 감싼 요소를 Trigger로 사용">
<div className="flex flex-wrap gap-3">
<LongText />
<Disabled />