156 lines
6.2 KiB
TypeScript
156 lines
6.2 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 { STORY_KICKER, 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.
|
|
const frame = (node: ReactNode) => (
|
|
<div
|
|
style={{ minHeight: 256, width: "100%", containerType: "inline-size" }}
|
|
className="max-md:[&_[data-viewport=false]_[data-slot=navigation-menu-content]]:absolute"
|
|
>
|
|
{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
|
|
kicker={STORY_KICKER.components}
|
|
title="Navigation Menu"
|
|
description="목적지(링크)로 이동하는 메뉴입니다. 명령 실행에는 쓰지 않습니다. hover, 클릭 또는 키보드로 엽니다."
|
|
/>
|
|
<StorySection title="Overview">
|
|
<StateBoard
|
|
min={520}
|
|
states={[
|
|
{ id: "viewport", label: "Viewport (default)", note: "제품 / 자료는 content, 회사는 현재 위치(active), 문의는 일반 링크, 준비 중은 disabled trigger", children: frame(<Specimen />) },
|
|
]}
|
|
/>
|
|
</StorySection>
|
|
<StorySection title="Variants">
|
|
<StateBoard
|
|
min={520}
|
|
states={[
|
|
{ id: "no-viewport", label: "viewport={false}", note: "각 content가 자기 trigger 아래에 열립니다", children: frame(<Specimen viewport={false} />) },
|
|
]}
|
|
/>
|
|
</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>
|
|
),
|
|
}))} />
|
|
),
|
|
}
|
|
|