feat: integrate latest HMDesign design system updates
This commit is contained in:
1 parent
99faec189e
commit
1c4e0bb9a1
209 files changed
+8845
-552
No files matched your search
@@ -53,8 +53,8 @@ function Destructive({ defaultOpen }: { defaultOpen?: boolean }) {
|
||||
<AlertDialogDescription>삭제하면 되돌릴 수 없습니다. 프로젝트의 모든 데이터가 함께 삭제됩니다.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel appearance="outline">취소</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" appearance="outline">
|
||||
<AlertDialogCancel appearance="solid">취소</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" appearance="solid">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
@@ -81,7 +81,7 @@ export const All: Story = {
|
||||
<StorySection title="Overview" description="일반 확인창은 같은 크기의 Solid 버튼 세트입니다. 버튼을 눌러 엽니다.">
|
||||
<Confirmation />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="삭제 확인창은 취소와 삭제 모두 같은 크기의 Outline 세트입니다.">
|
||||
<StorySection title="Variants" description="삭제 확인창은 취소와 삭제 모두 같은 크기의 Standard Solid 세트입니다.">
|
||||
<Destructive />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -25,7 +25,7 @@ const rolesFor = (appearance: ButtonAppearance) =>
|
||||
appearance === "brand"
|
||||
? ["primary", "secondary", "modify"].includes(role)
|
||||
: appearance === "solid"
|
||||
? role !== "destructive"
|
||||
? true
|
||||
: ["primary", "secondary", "destructive", "cancel"].includes(role),
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ const meta = {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: roles.map(([role]) => role),
|
||||
description: "역할(Role): primary Green, secondary Brown, modify Orange, destructive Red, cancel Gray. destructive는 outline만, modify는 brand/solid만, cancel은 solid/outline만 지원합니다. docs/components/button.md §6.1 참조.",
|
||||
description: "역할(Role): primary Green, secondary Brown, modify Orange, destructive Red, cancel Gray. destructive는 solid/outline, modify는 brand/solid만, cancel은 solid/outline만 지원합니다. docs/components/button.md §6.1 참조.",
|
||||
},
|
||||
appearance: {
|
||||
control: "inline-radio",
|
||||
@@ -205,10 +205,10 @@ function LongLabelBoard() {
|
||||
function DestructiveBoard() {
|
||||
return (
|
||||
<div className={row}>
|
||||
<Button variant="cancel" appearance="outline">
|
||||
<Button variant="cancel" appearance="solid">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" appearance="outline">
|
||||
<Button variant="destructive" appearance="solid">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
@@ -282,7 +282,7 @@ export const Destructive: Story = {
|
||||
tags: ["verification"],
|
||||
render: () => <DestructiveBoard />,
|
||||
parameters: {
|
||||
docs: { description: { story: "검증 전용 삭제·취소 세트: Cancel은 Gray Outline, Delete는 Red Outline이며 appearance와 size를 통일합니다. Red Hover/Pressed 면은 Red 8%입니다." } },
|
||||
docs: { description: { story: "검증 전용 삭제·취소 세트: Cancel은 Gray Solid, Delete는 Red Solid이며 appearance와 size를 통일합니다. Solid Hover/Pressed는 기존 Black 20% overlay이며 Red 토큰을 사용합니다." } },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export const All: Story = {
|
||||
title="Combobox"
|
||||
description="검색해서 하나를 고르는 Popover + Command 조합입니다. 검색이 필요 없는 짧은 목록은 Select를 씁니다."
|
||||
/>
|
||||
<StorySection title="Overview">{board("empty")}</StorySection>
|
||||
<StorySection title="Overview" description="Trigger는 dialog 팝업을 열고, 내부 검색 입력이 listbox를 제어합니다. Enter/Space로 열기, 방향키·Enter로 선택, Escape로 닫기 후 Trigger로 focus가 복귀합니다.">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes" description="size를 생략하면 Large입니다. 같은 size의 Select와 나란히 비교합니다.">
|
||||
<StateBoard
|
||||
min={320}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/compon
|
||||
// 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
|
||||
// `verification` that start open. Pseudo-state helpers are not used to fake an open Dialog.
|
||||
// Usage: native label below is a preserved Reference fixture. New HM React fields use public Label
|
||||
// with the actual control id (docs/components/label.md §5); fixture actions are not screen requirements.
|
||||
|
||||
const longText = "긴 텍스트는 줄바꿈으로 전체를 표시합니다. 공백 없는 긴 문자열도 Dialog 너비를 밀어내지 않고 줄바꿈합니다. ".repeat(8)
|
||||
|
||||
@@ -66,12 +68,12 @@ const confirmActions = (
|
||||
const deleteActions = (
|
||||
<>
|
||||
<DialogClose asChild>
|
||||
<Button variant="cancel" appearance="outline">
|
||||
<Button variant="cancel" appearance="solid">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button variant="destructive" appearance="outline">
|
||||
<Button variant="destructive" appearance="solid">
|
||||
삭제
|
||||
</Button>
|
||||
</DialogClose>
|
||||
@@ -150,7 +152,7 @@ function WideContent({ defaultOpen }: { defaultOpen?: boolean }) {
|
||||
function DeleteConfirmation({ defaultOpen }: { defaultOpen?: boolean }) {
|
||||
return (
|
||||
<Frame trigger="삭제 확인" defaultOpen={defaultOpen} title="항목 삭제" description="삭제하면 되돌릴 수 없습니다." actions={deleteActions}>
|
||||
<p>선택한 항목을 삭제합니다. 취소와 삭제 실행은 같은 크기의 Outline 세트입니다.</p>
|
||||
<p>선택한 항목을 삭제합니다. 취소와 삭제 실행은 같은 크기의 Standard Solid 세트입니다.</p>
|
||||
</Frame>
|
||||
)
|
||||
}
|
||||
@@ -194,7 +196,7 @@ export const All: Story = {
|
||||
<BodyScroll />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="일반 확인은 Solid 세트, 삭제 확인은 취소·삭제 모두 Outline 세트입니다.">
|
||||
<StorySection title="Usage" description="일반 확인과 삭제 확인 모두 취소·실행을 Standard Solid 세트로 구성합니다.">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<DeleteConfirmation />
|
||||
<FormContent />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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"
|
||||
|
||||
@@ -44,13 +46,13 @@ function Frame({
|
||||
const actions = (
|
||||
<>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="primary" appearance="solid">
|
||||
확인
|
||||
<Button variant="cancel" appearance="solid">
|
||||
취소
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="cancel" appearance="solid">
|
||||
취소
|
||||
<Button variant="primary" appearance="solid">
|
||||
실행
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</>
|
||||
@@ -103,7 +105,7 @@ export const All: Story = {
|
||||
title="Drawer"
|
||||
description="화면 아래에서 올라오는 모달 패널입니다. 핸들이나 패널을 아래로 끌어 닫습니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="기본 Drawer입니다. 버튼을 눌러 엽니다.">
|
||||
<StorySection title="Overview" description="데스크톱: 하단 우측 가로 취소 → 실행. 모바일(640px 이하): 전체 폭 세로 실행 위 / 취소 아래. 같은 Drawer를 화면 폭에 맞춰 확인합니다.">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="폼 입력 조합과, 내용이 길 때 Body만 scroll 하는 경우입니다.">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Input, Label } from "@hm-design/ui"
|
||||
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"
|
||||
|
||||
@@ -30,7 +30,7 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Label"
|
||||
description="입력 컨트롤의 이름입니다. Field Label 14/20/500을 쓰며, htmlFor로 연결하면 라벨 클릭 시 컨트롤로 focus가 이동합니다."
|
||||
description="Label은 항목명이고 Input은 입력칸입니다. Field Label 14/20/500을 쓰며, htmlFor로 연결하면 라벨 클릭 시 해당 Input이나 Select로 초점이 이동합니다."
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
@@ -71,6 +71,12 @@ export const All: Story = {
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="Required 별표는 Label이 아닌 소비 조합입니다. 긴 라벨은 줄바꿈됩니다.">
|
||||
<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>
|
||||
<SelectContent><SelectItem value="design">디자인</SelectItem><SelectItem value="engineering">개발</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<StateBoard
|
||||
states={[
|
||||
{
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Checkbox, Progress, ToggleGroup, ToggleGroupItem, Combobox, DatePicker, FilterBarSearch, Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem } from "@hm-design/ui"
|
||||
|
||||
const meta = { title: "Verification/Publisher Regression", tags: ["verification"], parameters: { layout: "padded" } } satisfies Meta
|
||||
export default meta
|
||||
function Consumers() {
|
||||
const [checked, setChecked] = useState(false)
|
||||
const [query, setQuery] = useState("Editable")
|
||||
return <div className="grid max-w-2xl gap-4">
|
||||
<Checkbox asChild checked={checked} onCheckedChange={value => setChecked(value === true)} aria-label="Consumer checkbox"><button id="consumer-checkbox"><span data-consumer-child>Consumer child</span></button></Checkbox>
|
||||
<Progress asChild value={35} aria-label="Consumer progress"><section id="consumer-progress"><span data-consumer-child>Consumer progress</span></section></Progress>
|
||||
<ToggleGroup type="multiple" spacing={3} style={{ color: "var(--hm-sem-fg-primary)" }} aria-label="Spacing merge"><ToggleGroupItem value="one">One</ToggleGroupItem><ToggleGroupItem value="two">Two</ToggleGroupItem></ToggleGroup>
|
||||
<Combobox aria-label="Department" options={[{ value: "design", label: "Design" }]} />
|
||||
<Combobox aria-label="Owner" options={[{ value: "hm", label: "Hanmac" }]} />
|
||||
<DatePicker aria-label="Start date" /><DatePicker aria-label="End date" />
|
||||
<FilterBarSearch value="Read only" readOnly onValueChange={() => { throw Error("readOnly mutated") }} />
|
||||
<FilterBarSearch value="Disabled" disabled onValueChange={() => { throw Error("disabled mutated") }} />
|
||||
<FilterBarSearch value={query} onValueChange={setQuery} />
|
||||
<Menubar><MenubarMenu><MenubarTrigger>File regression</MenubarTrigger><MenubarContent><MenubarItem>New regression</MenubarItem></MenubarContent></MenubarMenu><MenubarMenu><MenubarTrigger>Edit regression</MenubarTrigger><MenubarContent><MenubarItem>Undo regression</MenubarItem></MenubarContent></MenubarMenu></Menubar>
|
||||
</div>
|
||||
}
|
||||
export const ConsumersRegression: StoryObj<typeof meta> = {
|
||||
render: () => <Consumers />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const doc = canvasElement.ownerDocument
|
||||
const key = (target: HTMLElement, value: string) => { target.focus(); target.dispatchEvent(new KeyboardEvent("keydown", { key: value, bubbles: true })) }
|
||||
const settle = () => new Promise(resolve => setTimeout(resolve, 160))
|
||||
key(canvasElement.querySelector<HTMLElement>('[data-slot="menubar-trigger"]')!, "ArrowDown")
|
||||
await settle()
|
||||
key(doc.querySelector<HTMLElement>('[data-slot="menubar-item"]')!, "ArrowRight")
|
||||
await settle()
|
||||
const edit = canvasElement.querySelectorAll<HTMLElement>('[data-slot="menubar-trigger"]')[1]
|
||||
if (edit.getAttribute("aria-expanded") !== "true" || !doc.querySelector('[data-slot="menubar-content"]')?.textContent?.includes("Undo regression")) throw Error("Menubar switch closed the next popup")
|
||||
key(doc.querySelector<HTMLElement>('[data-slot="menubar-item"]')!, "Escape")
|
||||
},
|
||||
}
|
||||
@@ -273,10 +273,10 @@ export const OpenTopLong: Story = {
|
||||
// that SelectContent already forwards, so no Select API exists for it. Both columns share options, selected value and width.
|
||||
function ListSizeColumn({ title, listSize, width }: { title: string; listSize: SelectSize; width: number }) {
|
||||
return (
|
||||
<section className="grid content-start gap-2" style={{ width }} data-list-size={listSize}>
|
||||
<section className="grid min-w-0 content-start gap-2" style={{ width, maxWidth: "100%" }} data-list-size={listSize}>
|
||||
<h3 className="m-0 text-sm font-semibold">{title}</h3>
|
||||
<div className="min-h-56">
|
||||
<SelectSpecimen size="medium" listSize={listSize} defaultOpen defaultValue="long" options={longOpenOptions} label={"Trigger medium · " + width + "px"} />
|
||||
<SelectSpecimen size="medium" listSize={listSize} defaultOpen defaultValue="long" options={longOpenOptions} label={"Trigger medium · 최대 " + width + "px"} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -286,7 +286,7 @@ export const ListSizeComparison: Story = {
|
||||
name: "Medium Trigger / List Size Comparison",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<div className="grid gap-8 [grid-template-columns:repeat(auto-fit,minmax(280px,max-content))]">
|
||||
<div className="grid gap-8 [grid-template-columns:repeat(auto-fit,minmax(min(100%,384px),max-content))]">
|
||||
<ListSizeColumn title="A · Medium Trigger + Large List" listSize="large" width={280} />
|
||||
<ListSizeColumn title="B · Medium Trigger + Medium List" listSize="medium" width={280} />
|
||||
<ListSizeColumn title="A · Medium Trigger + Large List" listSize="large" width={384} />
|
||||
|
||||
@@ -28,7 +28,7 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Separator"
|
||||
description="콘텐츠 사이를 나누는 선입니다. 기본은 장식용(decorative)이며 접근성 트리에 노출되지 않습니다."
|
||||
description="콘텐츠 사이를 나누는 선입니다. 제목과 설명을 하나의 묶음으로 두고 구분선은 묶음 뒤 한 곳에 사용합니다. 기본은 장식용(decorative)입니다."
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
@@ -39,7 +39,7 @@ export const All: Story = {
|
||||
label: "Horizontal",
|
||||
children: (
|
||||
<div className="grid gap-3 text-sm">
|
||||
<span>Above</span>
|
||||
<div><strong>제목</strong><p>제목에 연결된 설명입니다.</p></div>
|
||||
<Separator />
|
||||
<span>Below</span>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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"
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ export function StateBoard({ states, min = 320, fixed = false }: { states: Board
|
||||
data-overview-state={id}
|
||||
aria-label={label}
|
||||
>
|
||||
<Label className="border-b border-black/10 pb-2 text-sm font-medium text-muted-foreground">{label}</Label>
|
||||
{note && !noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
|
||||
<header className="grid gap-2 border-b border-black/10 pb-2">
|
||||
<Label className="text-sm font-medium text-muted-foreground">{label}</Label>
|
||||
{note && !noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
|
||||
</header>
|
||||
{children}
|
||||
{note && noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
|
||||
</section>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import type { ComponentProps } from "react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@hm-design/ui"
|
||||
import { useId, useState, type ComponentProps } from "react"
|
||||
import {
|
||||
Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea,
|
||||
Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Representative "Tabs" Story shows default Primary, explicit API comparisons and Secondary; Legacy stays verification-only.
|
||||
// Selected connects to the content surface, so every specimen puts the Tabs on a plain bordered panel supplied by the consumer
|
||||
// (TabsContent here). The panel is layout, not part of Tabs.
|
||||
// D-01 applies to every Fill specimen: flat panels use the actual parent surface and padding 24px 0.
|
||||
// D-02 is shipped by the public Tabs theme mapping (Primary top White alpha .8). Legacy stays compatibility-only.
|
||||
|
||||
type Variant = "primary" | "secondary"
|
||||
type ListProps = Pick<ComponentProps<typeof TabsList>, "fill" | "panelSurface">
|
||||
|
||||
// Primary = Fill + panelSurface="content", Secondary = Fill + panelSurface="surface" (2026-10-06 사용자 결정).
|
||||
const variantProps = {
|
||||
primary: { listProps: {}, panel: "border bg-background" },
|
||||
secondary: { listProps: { fill: true, panelSurface: "surface" }, panel: "border bg-card" },
|
||||
primary: { listProps: {}, surface: "bg-background" },
|
||||
secondary: { listProps: { fill: true, panelSurface: "surface" }, surface: "bg-card" },
|
||||
} as const
|
||||
|
||||
function Specimen({
|
||||
@@ -32,12 +36,14 @@ function Specimen({
|
||||
labels?: string[]
|
||||
listProps?: ListProps
|
||||
}) {
|
||||
const { listProps: defaults, panel } = variantProps[variant]
|
||||
const { listProps: defaults, surface } = variantProps[variant]
|
||||
const effectiveListProps: ListProps = listProps ?? defaults
|
||||
const legacy = effectiveListProps.fill === false
|
||||
const items = labels?.map((label, i) => ({ value: `menu${i + 1}`, label }))
|
||||
?? Array.from({ length: count }, (_, i) => ({ value: `menu${i + 1}`, label: `메뉴 ${i + 1}` }))
|
||||
return (
|
||||
<Tabs defaultValue={defaultValue}>
|
||||
<TabsList aria-label="업무 필터" {...(listProps ?? defaults)}>
|
||||
<Tabs defaultValue={defaultValue} className={legacy ? undefined : `min-w-0 ${surface}`}>
|
||||
<TabsList aria-label="업무 필터" {...effectiveListProps}>
|
||||
{items.map(({ value, label }, i) => (
|
||||
<TabsTrigger key={value} value={value} disabled={disabledSecond && i === 1}>
|
||||
{label}
|
||||
@@ -45,7 +51,7 @@ function Specimen({
|
||||
))}
|
||||
</TabsList>
|
||||
{items.map(({ value }, i) => (
|
||||
<TabsContent key={value} value={value} className={`rounded-b p-4 text-sm ${panel}`}>
|
||||
<TabsContent key={value} value={value} className={legacy ? "rounded-b p-4 text-sm border bg-card" : "px-0 py-6 text-sm"}>
|
||||
메뉴 {i + 1} 콘텐츠
|
||||
</TabsContent>
|
||||
))}
|
||||
@@ -55,6 +61,47 @@ function Specimen({
|
||||
|
||||
const longLabels = ["기본 정보", "보상 협의 및 계약 진행 현황", "토지 및 지장물 조사 내역"]
|
||||
|
||||
// Component composition reference, not a new application screen. All visual controls use public HM exports.
|
||||
// Input state lives above the panels so the draft survives Radix's inactive-panel unmount.
|
||||
function SurfaceUsage({ inCard = false }: { inCard?: boolean }) {
|
||||
const id = useId()
|
||||
const [project, setProject] = useState("")
|
||||
const [purpose, setPurpose] = useState("")
|
||||
const tabs = (
|
||||
<Tabs defaultValue="information" className="min-w-0">
|
||||
<TabsList panelSurface={inCard ? "surface" : "content"} aria-label={inCard ? "Card 내부 프로젝트 정보" : "페이지 프로젝트 정보"}>
|
||||
<TabsTrigger value="information">기본 정보</TabsTrigger>
|
||||
<TabsTrigger value="schedule">일정·산출물</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="information" className="px-0 py-6">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`${id}-project`}>프로젝트명 (필수)</Label>
|
||||
<Input id={`${id}-project`} required value={project} onChange={event => setProject(event.target.value)} placeholder="프로젝트명을 입력하세요" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`${id}-purpose`}>목적 및 배경</Label>
|
||||
<Textarea id={`${id}-purpose`} value={purpose} onChange={event => setPurpose(event.target.value)} placeholder="프로젝트의 목적을 입력하세요" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="schedule" className="px-0 py-6">
|
||||
<Table>
|
||||
<TableCaption>프로젝트 산출물 일정 예시</TableCaption>
|
||||
<TableHeader><TableRow><TableHead scope="col">산출물</TableHead><TableHead scope="col">마감일</TableHead></TableRow></TableHeader>
|
||||
<TableBody><TableRow><TableCell>화면 설계서</TableCell><TableCell>10월 14일</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
return inCard ? (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader><CardTitle>프로젝트 정보</CardTitle></CardHeader>
|
||||
<CardContent className="min-w-0">{tabs}</CardContent>
|
||||
</Card>
|
||||
) : <div className="min-w-0 bg-background">{tabs}</div>
|
||||
}
|
||||
|
||||
function LongLabelSpecimens() {
|
||||
return (
|
||||
<div className="grid min-w-0 max-w-full gap-5">
|
||||
@@ -86,7 +133,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"기본 TabsList는 Primary(fill=true, panelSurface=\"content\")입니다. 대표 Story는 기본 API와 명시적 Primary, Secondary(fill + panelSurface=\"surface\"), Primary Overflow를 비교합니다. Legacy non-fill은 fill={false}로 유지하며 숨김 Compatibility에서 확인합니다. 높이 34, 좌우 16, 상단 radius 4, 16/24/700과 8px 곡선 연결은 기존 값 그대로입니다. Selected는 패널 배경과 이어지고 Hover는 비선택에만 Black 20%입니다. 넘칠 때만 이전/다음 이동 버튼이 나타납니다. 곡선 반경 8px과 비선택 합성색의 과거 REVIEW / TEMPORARY 이력은 유지합니다.",
|
||||
"기본 TabsList는 Primary(fill=true, panelSurface=\"content\")입니다. 대표 Story는 기본 API와 명시적 Primary, Secondary(fill + panelSurface=\"surface\"), Primary Overflow를 비교합니다. 모든 Fill 예제는 중첩 박스 없는 TabsContent(px-0 py-6)를 사용하며 Usage는 Card 내부 surface와 페이지 content 위의 조합입니다. panelSurface는 List의 선택 면만 바꾸므로 실제 상위 배경에 맞춰 선택합니다. Primary 선택 탭의 gradient는 상단 White alpha 0.8 → content이며 전체 opacity는 1입니다. Secondary는 기존 불투명 White를 유지합니다. Legacy non-fill은 fill={false}로 유지하며 숨김 Compatibility에서 확인합니다. 높이 34, 좌우 16, 상단 radius 4, 16/24/700과 8px 곡선 연결은 기존 값 그대로입니다. Hover는 비선택에만 Black 20%이며 넘칠 때만 이전/다음 이동 버튼이 나타납니다. aria-label과 연결된 입력 Label을 제공하고, 방향키·Home/End·포커스·Disabled·모바일 내부 스크롤을 확인합니다. 곡선 반경 8px과 비선택 합성색의 과거 REVIEW / TEMPORARY 이력은 유지합니다.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,7 +154,7 @@ export const All: Story = {
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "primary", label: "Primary · 기본 TabsList", noteAfter: true, note: "fill / panelSurface 생략 → Primary. 선택 면이 위 White → 아래 content panel색 gradient로 콘텐츠와 이어집니다. 비선택 탭에 hover하면 Black 20% overlay가 적용됩니다.", children: <Specimen /> },
|
||||
{ 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 /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
@@ -129,10 +176,19 @@ export const All: Story = {
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "overflow", label: "Overflow · Primary + 이동 버튼", noteAfter: true, note: "Primary Tabs에 탭이 많거나 가용 폭이 좁으면 이전/다음 이동 버튼이 나타납니다. 버튼은 선택을 바꾸지 않고 목록만 한 항목씩 이동합니다. 260px 폭은 Story wrapper의 검증 폭이며 Tabs 자체 폭이 아닙니다.", children: <div className="max-w-[260px]"><Specimen count={8} /></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 규칙이 아닙니다.">
|
||||
<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 /> },
|
||||
]} />
|
||||
</StorySection>
|
||||
<StorySection title="Do / Don't" description="Card 전용 Token을 화면 전체 간격에 사용하거나 모든 제목·설명 간격을 24px로 통일하지 않습니다.">
|
||||
<p className="text-sm text-muted-foreground">Do: Card 또는 Layout이 외곽 여백을 소유하고 내부 Input·Table·Alert의 고유 스타일을 유지합니다. Don't: 패널 박스 제거를 모든 border 제거로 해석하거나, 그라데이션 alpha 조정을 탭 전체 opacity 변경으로 구현하지 않습니다. 실제 패널 계산값·키보드 동작·모바일 넘침을 확인합니다.</p>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { BoldIcon, ItalicIcon, StarIcon } from "lucide-react"
|
||||
import { Toggle } from "@hm-design/ui"
|
||||
import { Toggle, ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
@@ -110,9 +110,16 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Toggle"
|
||||
description="눌림(On) 상태를 유지하는 단일 버튼입니다."
|
||||
description="Toggle과 Toggle Group은 눌림(On) 상태를 공유합니다. Default·Outline, Off·On·Disabled와 키보드 Focus를 같은 기준으로 비교합니다."
|
||||
/>
|
||||
<StorySection title="Overview">{board("offon")}</StorySection>
|
||||
<StorySection title="단독 · 연결 그룹" description="단독 Toggle은 독립 선택입니다. spacing=0 그룹은 연결 형태이며 single은 하나, multiple은 여러 항목을 선택합니다.">
|
||||
<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>
|
||||
<div><p className="mb-2">연결 그룹 · 다중 선택</p><ToggleGroup type="multiple" variant="outline" defaultValue={["bold"]} aria-label="글자 스타일"><ToggleGroupItem value="bold">굵게</ToggleGroupItem><ToggleGroupItem value="italic">기울임</ToggleGroupItem></ToggleGroup></div>
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes">
|
||||
<Sizes />
|
||||
</StorySection>
|
||||
|
||||
Reference in new issue
Block a user