style(storybook): refine overlay story layout
This commit is contained in:
1 parent
bef164489b
commit
3bebe8a660
9 files changed
+198
-77
No files matched your search
@@ -11,6 +11,7 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
Button,
|
Button,
|
||||||
} from "@hm-design/ui"
|
} from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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
|
// Alert Dialog is a modal Radix overlay (Portal) for a decision that must be confirmed or cancelled: no Close (X), no outside
|
||||||
@@ -68,17 +69,34 @@ export default meta
|
|||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
// Representative Story: the common Story page; each trigger opens the real dialog.
|
// Representative Story: the common Story page; each trigger opens the real dialog.
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: Overview and Variants are merged into one Variants section.
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Alert Dialog",
|
name: "Alert Dialog",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Alert Dialog" />
|
<StoryPageHeader title="Alert Dialog" />
|
||||||
<StorySection title="Overview">
|
|
||||||
<Confirmation />
|
|
||||||
</StorySection>
|
|
||||||
<StorySection title="Variants">
|
<StorySection title="Variants">
|
||||||
<Destructive />
|
<SpecimenGrid
|
||||||
|
states={[
|
||||||
|
{ id: "confirmation", label: "Confirmation", children: <Confirmation /> },
|
||||||
|
{ id: "destructive", label: "Destructive", children: <Destructive /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const All: Story = {
|
|||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Context Menu" />
|
<StoryPageHeader title="Context Menu" />
|
||||||
<StorySection title="Overview">
|
<StorySection title="Basic">
|
||||||
<Full />
|
<Full />
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from "@hm-design/ui"
|
|||||||
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } 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 { Input } from "@hm-design/ui"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@hm-design/ui"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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
|
// Dialog is an overlay (Radix Portal), so it does not fit the StateBoard. The representative Story is a row of triggers that open
|
||||||
@@ -161,7 +162,7 @@ function FormContent({ defaultOpen }: { defaultOpen?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보" actions={confirmActions}>
|
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보" actions={confirmActions}>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<label htmlFor="dialog-form-name" className="text-sm font-medium text-muted-foreground">
|
<label htmlFor="dialog-form-name" className="text-sm font-medium text-foreground">
|
||||||
프로젝트명
|
프로젝트명
|
||||||
</label>
|
</label>
|
||||||
<Input id="dialog-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
<Input id="dialog-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
||||||
@@ -176,29 +177,44 @@ type Story = StoryObj<typeof meta>
|
|||||||
|
|
||||||
// Representative Story: the common Story page; every case opens from its trigger. Named after the component, as in ui_viz.
|
// Representative Story: the common Story page; every case opens from its trigger. Named after the component, as in ui_viz.
|
||||||
// It is the only Dialog entry in the default sidebar, so Storybook shows Dialog as a single 1-depth item.
|
// It is the only Dialog entry in the default sidebar, so Storybook shows Dialog as a single 1-depth item.
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: the common Story page. Overview is merged into Variants (Basic is its first item).
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Dialog",
|
name: "Dialog",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader
|
<StoryPageHeader title="Dialog" />
|
||||||
title="Dialog"
|
|
||||||
/>
|
|
||||||
<StorySection title="Overview">
|
|
||||||
<Basic />
|
|
||||||
</StorySection>
|
|
||||||
<StorySection title="Variants">
|
<StorySection title="Variants">
|
||||||
<div className="flex flex-wrap gap-3">
|
<SpecimenGrid
|
||||||
<LongContent />
|
states={[
|
||||||
<WideContent />
|
{ id: "basic", label: "Basic", children: <Basic /> },
|
||||||
<BodyScroll />
|
{ id: "long-content", label: "Long Content", children: <LongContent /> },
|
||||||
</div>
|
{ id: "wide-content", label: "Wide Content", children: <WideContent /> },
|
||||||
|
{ id: "body-scroll", label: "Body Scroll", children: <BodyScroll /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
<StorySection title="Usage">
|
<StorySection title="Usage">
|
||||||
<div className="flex flex-wrap gap-3">
|
<SpecimenGrid
|
||||||
<DeleteConfirmation />
|
states={[
|
||||||
<FormContent />
|
{ id: "delete-confirmation", label: "Delete Confirmation", children: <DeleteConfirmation /> },
|
||||||
</div>
|
{ id: "form", label: "Form", children: <FormContent /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +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
|
// 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.
|
// 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 { Button, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, Input } from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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
|
// Drawer is a modal vaul overlay (Portal). The representative Story opens the real Drawer from its triggers; drag the handle or
|
||||||
@@ -70,7 +71,7 @@ function FormCase({ defaultOpen }: { defaultOpen?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보" actions={actions}>
|
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보" actions={actions}>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<label htmlFor="drawer-form-name" className="text-sm font-medium text-muted-foreground">
|
<label htmlFor="drawer-form-name" className="text-sm font-medium text-foreground">
|
||||||
프로젝트명
|
프로젝트명
|
||||||
</label>
|
</label>
|
||||||
<Input id="drawer-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
<Input id="drawer-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
||||||
@@ -95,20 +96,35 @@ const meta = { title: "Components/Overlay/Drawer", component: Drawer, parameters
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: Overview and Usage are merged into one Usage section (Basic, Form, Long Content).
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Drawer",
|
name: "Drawer",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Drawer" />
|
<StoryPageHeader title="Drawer" />
|
||||||
<StorySection title="Overview">
|
|
||||||
<Basic />
|
|
||||||
</StorySection>
|
|
||||||
<StorySection title="Usage">
|
<StorySection title="Usage">
|
||||||
<div className="flex flex-wrap gap-3">
|
<SpecimenGrid
|
||||||
<FormCase />
|
states={[
|
||||||
<LongCase />
|
{ id: "basic", label: "Basic", children: <Basic /> },
|
||||||
</div>
|
{ id: "form", label: "Form", children: <FormCase /> },
|
||||||
|
{ id: "long-content", label: "Long Content", children: <LongCase /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const All: Story = {
|
|||||||
<StoryPageHeader
|
<StoryPageHeader
|
||||||
title="Dropdown Menu"
|
title="Dropdown Menu"
|
||||||
/>
|
/>
|
||||||
<StorySection title="Overview">
|
<StorySection title="Basic">
|
||||||
<Full />
|
<Full />
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hm-design/ui"
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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 Card is a Radix overlay (Portal) for a richer preview than Tooltip. The representative Story shows real triggers
|
||||||
@@ -48,17 +49,34 @@ const meta = { title: "Components/Overlay/Hover Card", component: HoverCard, par
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: Overview and Usage are merged into one Variants section (Basic, Long Content).
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Hover Card",
|
name: "Hover Card",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Hover Card" />
|
<StoryPageHeader title="Hover Card" />
|
||||||
<StorySection title="Overview">
|
<StorySection title="Variants">
|
||||||
<Preview />
|
<SpecimenGrid
|
||||||
</StorySection>
|
states={[
|
||||||
<StorySection title="Usage">
|
{ id: "basic", label: "Basic", children: <Preview /> },
|
||||||
<LongContent />
|
{ id: "long-content", label: "Long Content", children: <LongContent /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
import { Button, Input, Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@hm-design/ui"
|
import { Button, Input, Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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;
|
// Popover is a non-modal Radix overlay (Portal). The representative Story opens the real panels from their triggers;
|
||||||
@@ -33,7 +34,7 @@ function FormPanel() {
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="start" aria-label="프로젝트 이름 변경">
|
<PopoverContent align="start" aria-label="프로젝트 이름 변경">
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<label htmlFor="popover-name" className="text-sm font-medium text-muted-foreground">
|
<label htmlFor="popover-name" className="text-sm font-medium text-foreground">
|
||||||
프로젝트명
|
프로젝트명
|
||||||
</label>
|
</label>
|
||||||
<Input id="popover-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
<Input id="popover-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
||||||
@@ -55,11 +56,9 @@ function FormPanel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Aligned() {
|
function Aligned({ align }: { align: "start" | "center" | "end" }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full max-w-md justify-between gap-3">
|
<Popover>
|
||||||
{(["start", "center", "end"] as const).map((align) => (
|
|
||||||
<Popover key={align}>
|
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="primary" appearance="outline" size="sm">
|
<Button variant="primary" appearance="outline" size="sm">
|
||||||
{`align ${align}`}
|
{`align ${align}`}
|
||||||
@@ -69,8 +68,6 @@ function Aligned() {
|
|||||||
<p>{`align="${align}"`}</p>
|
<p>{`align="${align}"`}</p>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,20 +75,39 @@ const meta = { title: "Components/Overlay/Popover", component: Popover, paramete
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: Overview is merged into Variants (Basic is its first item); each align trigger sits in its own column.
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Popover",
|
name: "Popover",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Popover" />
|
<StoryPageHeader title="Popover" />
|
||||||
<StorySection title="Overview">
|
|
||||||
<Info />
|
|
||||||
</StorySection>
|
|
||||||
<StorySection title="Variants">
|
<StorySection title="Variants">
|
||||||
<Aligned />
|
<SpecimenGrid
|
||||||
|
states={[
|
||||||
|
{ id: "basic", label: "Basic", children: <Info /> },
|
||||||
|
{ id: "align-start", label: "Align Start", children: <Aligned align="start" /> },
|
||||||
|
{ id: "align-center", label: "Align Center", children: <Aligned align="center" /> },
|
||||||
|
{ id: "align-end", label: "Align End", children: <Aligned align="end" /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
<StorySection title="Usage">
|
<StorySection title="Usage">
|
||||||
<FormPanel />
|
<SpecimenGrid states={[{ id: "form", label: "Form", children: <FormPanel /> }]} />
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +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
|
// 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.
|
// 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 { Button, Input, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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
|
// Sheet is a modal Radix overlay (Portal), so the representative Story is a row of triggers that open the real Sheet for each
|
||||||
@@ -68,7 +69,7 @@ function FormCase({ defaultOpen }: { defaultOpen?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보">
|
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<label htmlFor="sheet-form-name" className="text-sm font-medium text-muted-foreground">
|
<label htmlFor="sheet-form-name" className="text-sm font-medium text-foreground">
|
||||||
프로젝트명
|
프로젝트명
|
||||||
</label>
|
</label>
|
||||||
<Input id="sheet-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
<Input id="sheet-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
||||||
@@ -93,29 +94,44 @@ const meta = { title: "Components/Overlay/Sheet", component: Sheet, parameters:
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: the common Story page. Overview is merged into Variants (Right is its first item).
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Sheet",
|
name: "Sheet",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader
|
<StoryPageHeader title="Sheet" />
|
||||||
title="Sheet"
|
|
||||||
/>
|
|
||||||
<StorySection title="Overview">
|
|
||||||
<SideCase side="right" />
|
|
||||||
</StorySection>
|
|
||||||
<StorySection title="Variants">
|
<StorySection title="Variants">
|
||||||
<div className="flex flex-wrap gap-3">
|
<SpecimenGrid
|
||||||
<SideCase side="left" />
|
states={[
|
||||||
<SideCase side="top" />
|
{ id: "right", label: "Right", children: <SideCase side="right" /> },
|
||||||
<SideCase side="bottom" />
|
{ id: "left", label: "Left", children: <SideCase side="left" /> },
|
||||||
</div>
|
{ id: "top", label: "Top", children: <SideCase side="top" /> },
|
||||||
|
{ id: "bottom", label: "Bottom", children: <SideCase side="bottom" /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
<StorySection title="Usage">
|
<StorySection title="Usage">
|
||||||
<div className="flex flex-wrap gap-3">
|
<SpecimenGrid
|
||||||
<FormCase />
|
states={[
|
||||||
<LongCase />
|
{ id: "form", label: "Form", children: <FormCase /> },
|
||||||
</div>
|
{ id: "long-content", label: "Long Content", children: <LongCase /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hm-design/ui"
|
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hm-design/ui"
|
||||||
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||||
import { 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
|
// Tooltip is a Radix overlay (Portal) with short, non-interactive text. The representative Story shows real triggers
|
||||||
@@ -20,11 +21,9 @@ function Basic({ defaultOpen }: { defaultOpen?: boolean }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Placements() {
|
function Placement({ side }: { side: "top" | "right" | "bottom" | "left" }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3">
|
<Tooltip>
|
||||||
{(["top", "right", "bottom", "left"] as const).map((side) => (
|
|
||||||
<Tooltip key={side}>
|
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="primary" appearance="outline" size="sm">
|
<Button variant="primary" appearance="outline" size="sm">
|
||||||
{side}
|
{side}
|
||||||
@@ -32,8 +31,6 @@ function Placements() {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side={side}>{`side="${side}"`}</TooltipContent>
|
<TooltipContent side={side}>{`side="${side}"`}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +67,21 @@ const meta = { title: "Components/Overlay/Tooltip", component: Tooltip, paramete
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that
|
||||||
|
// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider
|
||||||
|
// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its
|
||||||
|
// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them.
|
||||||
|
function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) {
|
||||||
|
return (
|
||||||
|
<div className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${columns === 5 ? "min-[1000px]:grid-cols-5" : "min-[1000px]:grid-cols-4"}`}>
|
||||||
|
{states.map((state) => (
|
||||||
|
<StateBoard key={state.id} min={10000} states={[{ ...state, children: <div>{state.children}</div> }]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representative Story: Overview is merged into Variants (Basic is its first item, then the four sides): 5 columns on PC.
|
||||||
export const All: Story = {
|
export const All: Story = {
|
||||||
name: "Tooltip",
|
name: "Tooltip",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
@@ -77,17 +89,26 @@ export const All: Story = {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Tooltip" />
|
<StoryPageHeader title="Tooltip" />
|
||||||
<StorySection title="Overview">
|
|
||||||
<Basic />
|
|
||||||
</StorySection>
|
|
||||||
<StorySection title="Variants">
|
<StorySection title="Variants">
|
||||||
<Placements />
|
<SpecimenGrid
|
||||||
|
columns={5}
|
||||||
|
states={[
|
||||||
|
{ id: "basic", label: "Basic", children: <Basic /> },
|
||||||
|
{ id: "top", label: "Top", children: <Placement side="top" /> },
|
||||||
|
{ id: "right", label: "Right", children: <Placement side="right" /> },
|
||||||
|
{ id: "bottom", label: "Bottom", children: <Placement side="bottom" /> },
|
||||||
|
{ id: "left", label: "Left", children: <Placement side="left" /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
<StorySection title="Usage">
|
<StorySection title="Usage">
|
||||||
<div className="flex flex-wrap gap-3">
|
<SpecimenGrid
|
||||||
<LongText />
|
columns={5}
|
||||||
<Disabled />
|
states={[
|
||||||
</div>
|
{ id: "long-text", label: "Long Text", children: <LongText /> },
|
||||||
|
{ id: "disabled", label: "Disabled", children: <Disabled /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
Reference in new issue
Block a user