147 lines
6.5 KiB
TypeScript
147 lines
6.5 KiB
TypeScript
import type { ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import {
|
|
NavigationMenu,
|
|
NavigationMenuContent,
|
|
NavigationMenuIndicator,
|
|
NavigationMenuItem,
|
|
NavigationMenuLink,
|
|
NavigationMenuList,
|
|
NavigationMenuTrigger,
|
|
} from "@hm-design/ui"
|
|
import { StateBoard } from "@/components/story-board"
|
|
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
|
|
// inside a content link are marked with `data-description` for the secondary text.
|
|
|
|
function Destination({ href, title, description, active }: { href: string; title: string; description?: string; active?: boolean }) {
|
|
return (
|
|
<NavigationMenuLink href={href} active={active}>
|
|
<span>{title}</span>
|
|
{description ? <span data-description="">{description}</span> : null}
|
|
</NavigationMenuLink>
|
|
)
|
|
}
|
|
|
|
function Specimen({ viewport = true }: { viewport?: boolean }) {
|
|
return (
|
|
<NavigationMenu viewport={viewport}>
|
|
<NavigationMenuList>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuTrigger>제품</NavigationMenuTrigger>
|
|
<NavigationMenuContent>
|
|
<ul className="sm:w-[min(360px,calc(100cqw-18px))]" style={{ display: "grid", gap: 4 }}>
|
|
<li>
|
|
<Destination href="#analytics" title="분석" description="노선과 시설물 데이터를 한눈에 확인합니다." />
|
|
</li>
|
|
<li>
|
|
<Destination href="#maps" title="지도" description="공간 정보를 지도 위에서 관리합니다." />
|
|
</li>
|
|
<li>
|
|
<Destination href="#reports" title="리포트" description="점검 결과를 보고서로 만듭니다." />
|
|
</li>
|
|
</ul>
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuTrigger>자료</NavigationMenuTrigger>
|
|
<NavigationMenuContent>
|
|
<ul className="sm:w-[min(300px,calc(100cqw-18px))]" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
|
|
<li>
|
|
<Destination href="#docs" title="문서" />
|
|
</li>
|
|
<li>
|
|
<Destination href="#guides" title="가이드" />
|
|
</li>
|
|
<li>
|
|
<Destination href="#api" title="API" />
|
|
</li>
|
|
<li>
|
|
<Destination href="#blog" title="블로그" />
|
|
</li>
|
|
</ul>
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink href="#company" active>
|
|
회사
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink href="#contact">문의</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuTrigger disabled>준비 중</NavigationMenuTrigger>
|
|
</NavigationMenuItem>
|
|
</NavigationMenuList>
|
|
{viewport ? <NavigationMenuIndicator /> : null}
|
|
</NavigationMenu>
|
|
)
|
|
}
|
|
|
|
// Size specimens to their review area (including the panel's 8px padding + 1px border on each side).
|
|
// Mobile uses the content's natural minimum width instead of a fixed desktop specimen width.
|
|
// `minHeight: 256` reserves room for the open panel (about 232px below the menu bar) so it never covers what follows.
|
|
// `compact` (representative page only) drops that reservation on PC (iframe width 1000px and up), where the two examples sit side by
|
|
// side and nothing is below them; in the one-column layout below 1000px the 256px stays so an open menu does not cover the next example.
|
|
const frame = (node: ReactNode, compact = false) => (
|
|
<div
|
|
style={{ width: "100%", containerType: "inline-size", ...(compact ? {} : { minHeight: 256 }) }}
|
|
className={`max-md:[&_[data-viewport=false]_[data-slot=navigation-menu-content]]:absolute${compact ? " max-[999px]:min-h-64" : ""}`}
|
|
>
|
|
{node}
|
|
</div>
|
|
)
|
|
|
|
const meta = { title: "Components/Navigation/Navigation Menu", component: NavigationMenu, parameters: { layout: "padded" } } satisfies Meta<typeof NavigationMenu>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
export const All: Story = {
|
|
name: "Navigation Menu",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Navigation Menu" />
|
|
{/* Overview is merged into Variants (Viewport is its first item). Two equal columns on PC (iframe width 1000px and up), one
|
|
column below that; each specimen sits in its own single-column StateBoard (min larger than any column). */}
|
|
<StorySection title="Variants">
|
|
<div className="grid items-start gap-6 min-[1000px]:grid-cols-2">
|
|
<StateBoard min={10000} states={[{ id: "viewport", label: "Viewport", children: frame(<Specimen />, true) }]} />
|
|
<StateBoard min={10000} states={[{ id: "no-viewport", label: "No viewport", children: frame(<Specimen viewport={false} />, true) }]} />
|
|
</div>
|
|
</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const Default: Story = { tags: ["verification"], render: () => frame(<Specimen />) }
|
|
export const NoViewport: Story = { tags: ["verification"], name: "No Viewport", render: () => frame(<Specimen viewport={false} />) }
|
|
|
|
// Three enabled triggers with different widths exercise first / middle / last offsets in both positioning modes.
|
|
export const IndicatorAlignment: Story = {
|
|
tags: ["verification"],
|
|
render: () => (
|
|
<StateBoard min={520} states={[true, false].map((viewport) => ({
|
|
id: viewport ? "viewport" : "no-viewport",
|
|
label: `Indicator alignment · viewport=${viewport}`,
|
|
children: frame(
|
|
<NavigationMenu viewport={viewport}>
|
|
<NavigationMenuList>
|
|
{["제품", "기술 자료", "지원 센터"].map((label) => (
|
|
<NavigationMenuItem key={label}>
|
|
<NavigationMenuTrigger>{label}</NavigationMenuTrigger>
|
|
<NavigationMenuContent><Destination href="#verification" title={`${label} 안내`} /></NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
))}
|
|
</NavigationMenuList>
|
|
{viewport ? <NavigationMenuIndicator /> : null}
|
|
</NavigationMenu>
|
|
),
|
|
}))} />
|
|
),
|
|
}
|
|
|