Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
546 lines
22 KiB
TypeScript
546 lines
22 KiB
TypeScript
import { useEffect, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import { BellIcon, ChartColumnIcon, SearchIcon, SettingsIcon, type LucideIcon } from "lucide-react"
|
|
import {
|
|
Avatar,
|
|
AvatarFallback,
|
|
AvatarImage,
|
|
Badge,
|
|
Button,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
GNB,
|
|
GNBBrand,
|
|
GNBMobileGroup,
|
|
GNBMobileLink,
|
|
GNBMobileMenu,
|
|
GNBNav,
|
|
GNBUser,
|
|
GNBUtilities,
|
|
LNB,
|
|
LNBGroup,
|
|
LNBItem,
|
|
LNBMobileGroup,
|
|
LNBMobileLink,
|
|
LNBNav,
|
|
NavigationMenu,
|
|
NavigationMenuContent,
|
|
NavigationMenuItem,
|
|
NavigationMenuLink,
|
|
NavigationMenuList,
|
|
NavigationMenuTrigger,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@hm-design/ui"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// GNB is a composition pattern. Layout visual reference is the ui-viz GNB (green 70px bar, centered menu, Beige current item joined to
|
|
// the content, White 2-depth panel, text-only profile); every part inside is an HM component. The menu data, the user and the current
|
|
// location below are local sample data for this Story only (no network, no auth, no router). The desktop / mobile switch follows the
|
|
// viewport (1024px): narrow the Story to 390px to see the hamburger and the Sheet menu.
|
|
// GNBUser's default accessible name includes both the visible name and description; explicit labels must preserve them.
|
|
|
|
type Destination = { id: string; label: string }
|
|
type NavItem = { id: string; label: string; children?: Destination[] }
|
|
|
|
const navItems: NavItem[] = [
|
|
{ id: "dashboard", label: "대시보드" },
|
|
{
|
|
id: "projects",
|
|
label: "프로젝트",
|
|
children: [
|
|
{ id: "projects/active", label: "진행 중 프로젝트" },
|
|
{ id: "projects/done", label: "완료 프로젝트" },
|
|
{ id: "projects/templates", label: "프로젝트 템플릿 관리와 기준 설정을 위한 긴 하위 메뉴 이름" },
|
|
],
|
|
},
|
|
{ id: "reports", label: "리포트" },
|
|
{
|
|
id: "admin",
|
|
label: "관리",
|
|
children: [
|
|
{ id: "admin/users", label: "사용자 관리" },
|
|
{ id: "admin/org", label: "조직 관리" },
|
|
],
|
|
},
|
|
]
|
|
|
|
const longNavItems: NavItem[] = [
|
|
{ id: "dashboard", label: "통합 대시보드와 현황 요약" },
|
|
{ id: "projects", label: "전사 프로젝트 포트폴리오 관리", children: [{ id: "projects/active", label: "진행 중 프로젝트" }] },
|
|
{ id: "reports", label: "정기 점검 결과 리포트" },
|
|
]
|
|
|
|
const photo =
|
|
"data:image/svg+xml;utf8," +
|
|
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80"><rect width="80" height="80" fill="#266651"/><circle cx="40" cy="31" r="14" fill="#bddfd4"/><rect x="14" y="50" width="52" height="40" rx="22" fill="#bddfd4"/></svg>')
|
|
// An address that cannot load, so the Radix fallback shows.
|
|
const brokenPhoto = "data:image/png;base64,broken"
|
|
|
|
const user = { image: photo, name: "김한맥", description: "프로젝트 관리자" }
|
|
const longUser = { image: brokenPhoto, name: "김한맥 (한맥기술 인프라사업본부 스마트시설팀)", description: "통합 시스템 관리자 · 인프라사업본부" }
|
|
|
|
// A test logo (not a Hanmac asset), only to check the logo layout.
|
|
const testLogo =
|
|
"data:image/svg+xml;utf8," +
|
|
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="120" height="32"><rect width="120" height="32" rx="4" fill="#fff" fill-opacity=".9"/><text x="60" y="21" font-size="13" font-weight="700" text-anchor="middle" fill="#123328">TEST LOGO</text></svg>')
|
|
|
|
// The content that follows the bar: positioned, later in the DOM, top corners rounded by the join, so the bar's green continues behind it.
|
|
const contentStyle: CSSProperties = {
|
|
position: "relative",
|
|
minHeight: 220,
|
|
padding: 34,
|
|
borderRadius: "var(--hm-gnb-join) var(--hm-gnb-join) 0 0",
|
|
background: "var(--hm-sem-bg-content)",
|
|
}
|
|
|
|
function isCurrent(current: string, id: string) {
|
|
return current === id || current.startsWith(`${id}/`)
|
|
}
|
|
|
|
function SearchAction() {
|
|
return (
|
|
<Popover>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<PopoverTrigger asChild>
|
|
<Button type="button" variant="ghost" size="icon" aria-label="검색">
|
|
<SearchIcon aria-hidden="true" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent>검색</TooltipContent>
|
|
</Tooltip>
|
|
<PopoverContent align="end" style={{ "--hm-popover-padding": "0", "--hm-popover-width": "320px" } as CSSProperties}>
|
|
<Command>
|
|
<CommandInput placeholder="메뉴·프로젝트 검색" />
|
|
<CommandList>
|
|
<CommandEmpty>결과가 없습니다.</CommandEmpty>
|
|
<CommandGroup heading="바로가기">
|
|
<CommandItem>진행 중 프로젝트</CommandItem>
|
|
<CommandItem>월간 리포트</CommandItem>
|
|
<CommandItem>사용자 관리</CommandItem>
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
|
|
function NotificationAction({ count = 3 }: { count?: number }) {
|
|
return (
|
|
<Popover>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<PopoverTrigger asChild>
|
|
<Button type="button" variant="ghost" size="icon" aria-label={`알림 ${count}건`} style={{ position: "relative" }}>
|
|
<BellIcon aria-hidden="true" />
|
|
<Badge
|
|
variant="destructive"
|
|
aria-hidden="true"
|
|
className="size-6"
|
|
style={{ position: "absolute", top: -8, right: -8, padding: 0, borderRadius: "50%" }}
|
|
>
|
|
{count}
|
|
</Badge>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent>알림</TooltipContent>
|
|
</Tooltip>
|
|
<PopoverContent align="end" style={{ "--hm-popover-width": "300px" } as CSSProperties}>
|
|
<div className="grid gap-2">
|
|
<p className="font-bold">새 알림</p>
|
|
<p>1호선 구간 점검 결과가 등록되었습니다.</p>
|
|
<p>월간 리포트 승인이 요청되었습니다.</p>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
|
|
function UserMenu({ person, defaultOpen }: { person: { name: string; description: string; image?: string }; defaultOpen?: boolean }) {
|
|
return (
|
|
<DropdownMenu defaultOpen={defaultOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<GNBUser name={person.name} description={person.description}>
|
|
<Avatar>
|
|
{person.image ? <AvatarImage src={person.image} alt="" /> : null}
|
|
<AvatarFallback>{person.name.slice(0, 1)}</AvatarFallback>
|
|
</Avatar>
|
|
</GNBUser>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>{person.name}</DropdownMenuLabel>
|
|
<DropdownMenuItem>프로필</DropdownMenuItem>
|
|
<DropdownMenuItem>설정</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem variant="destructive">로그아웃</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
|
|
function Bar({
|
|
items = navItems,
|
|
person = user,
|
|
eyebrow = "한맥 업무 시스템",
|
|
brand = "프로젝트 대시보드",
|
|
logo,
|
|
initial = "dashboard",
|
|
openMenu,
|
|
userOpen,
|
|
mobileOpen,
|
|
sticky,
|
|
utilities,
|
|
onCurrent,
|
|
current: currentProp,
|
|
mobileChildren,
|
|
mobileMenuOpen,
|
|
onMobileMenuOpenChange,
|
|
}: {
|
|
items?: NavItem[]
|
|
person?: { name: string; description: string }
|
|
eyebrow?: string
|
|
brand?: string
|
|
logo?: string
|
|
initial?: string
|
|
openMenu?: string
|
|
userOpen?: boolean
|
|
mobileOpen?: boolean
|
|
sticky?: boolean
|
|
utilities?: boolean
|
|
onCurrent?: (id: string) => void
|
|
current?: string
|
|
mobileChildren?: ReactNode
|
|
mobileMenuOpen?: boolean
|
|
onMobileMenuOpenChange?: (open: boolean) => void
|
|
}) {
|
|
const [localCurrent, setCurrent] = useState(initial)
|
|
const current = currentProp ?? localCurrent
|
|
const go = (id: string) => (event: MouseEvent) => {
|
|
event.preventDefault()
|
|
if (currentProp === undefined) setCurrent(id)
|
|
onCurrent?.(id)
|
|
}
|
|
return (
|
|
<GNB sticky={sticky}>
|
|
<GNBMobileMenu title="메뉴" defaultOpen={mobileOpen} open={mobileMenuOpen} onOpenChange={onMobileMenuOpenChange}>
|
|
{mobileChildren ?? items.map((item) =>
|
|
item.children ? (
|
|
<GNBMobileGroup key={item.id} label={item.label}>
|
|
{item.children.map((child) => (
|
|
<GNBMobileLink key={child.id} href={`#${child.id}`} active={current === child.id} onClick={go(child.id)}>
|
|
{child.label}
|
|
</GNBMobileLink>
|
|
))}
|
|
</GNBMobileGroup>
|
|
) : (
|
|
<GNBMobileLink key={item.id} href={`#${item.id}`} active={current === item.id} onClick={go(item.id)}>
|
|
{item.label}
|
|
</GNBMobileLink>
|
|
)
|
|
)}
|
|
</GNBMobileMenu>
|
|
<GNBBrand href="#dashboard" onClick={go("dashboard")}>
|
|
{logo ? <img src={logo} alt="" /> : null}
|
|
<span>
|
|
{eyebrow ? <small>{eyebrow}</small> : null}
|
|
<strong>{brand}</strong>
|
|
</span>
|
|
</GNBBrand>
|
|
<GNBNav>
|
|
<NavigationMenu viewport={false} aria-label="주 메뉴" defaultValue={openMenu}>
|
|
<NavigationMenuList>
|
|
{items.map((item) =>
|
|
item.children ? (
|
|
<NavigationMenuItem key={item.id} value={item.id}>
|
|
<NavigationMenuTrigger aria-current={isCurrent(current, item.id) ? "page" : undefined}>{item.label}</NavigationMenuTrigger>
|
|
<NavigationMenuContent>
|
|
<ul>
|
|
{item.children.map((child) => (
|
|
<li key={child.id}>
|
|
<NavigationMenuLink href={`#${child.id}`} active={current === child.id} onClick={go(child.id)}>
|
|
{child.label}
|
|
</NavigationMenuLink>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
) : (
|
|
<NavigationMenuItem key={item.id}>
|
|
<NavigationMenuLink href={`#${item.id}`} active={current === item.id} onClick={go(item.id)}>
|
|
{item.label}
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
)
|
|
)}
|
|
</NavigationMenuList>
|
|
</NavigationMenu>
|
|
</GNBNav>
|
|
<GNBUtilities>
|
|
{utilities ? (
|
|
<>
|
|
<SearchAction />
|
|
<NotificationAction />
|
|
</>
|
|
) : null}
|
|
<UserMenu person={person} defaultOpen={userOpen} />
|
|
</GNBUtilities>
|
|
</GNB>
|
|
)
|
|
}
|
|
|
|
// The bar with the content that follows it (the GNB Mode page owns this part; here it only shows the join).
|
|
function Page({ children, label }: { children: ReactNode; label?: string }) {
|
|
return (
|
|
<div>
|
|
{children}
|
|
<div style={contentStyle}>
|
|
<p className="text-sm text-muted-foreground">{label ?? "연결된 콘텐츠 영역: GNB는 자기 높이와 12px 접점까지만 차지합니다."}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StickyPage() {
|
|
return (
|
|
<div
|
|
role="region"
|
|
aria-label="Sticky GNB 스크롤 예시"
|
|
tabIndex={0}
|
|
style={{ height: 360, overflow: "auto", scrollbarWidth: "none" }}
|
|
>
|
|
<Bar sticky />
|
|
<div style={{ ...contentStyle, minHeight: 900 }} className="grid content-start gap-3 text-sm text-muted-foreground">
|
|
{Array.from({ length: 24 }, (_, i) => (
|
|
<p key={i}>스크롤 본문 {i + 1}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Consumer-owned data and route state; every menu face/disclosure is an existing public LNB part.
|
|
type SidebarNode = { id: string; label: string; icon?: LucideIcon; children?: SidebarNode[] }
|
|
const sidebarNodes: SidebarNode[] = [
|
|
{ id: "dashboard", label: "대시보드" },
|
|
{
|
|
id: "projects", label: "프로젝트 관리", children: [
|
|
{ id: "projects/design", label: "디자인 관리", children: [
|
|
{ id: "projects/design/tokens", label: "토큰 관리" },
|
|
{ id: "projects/design/components", label: "컴포넌트 설계와 사용 규칙을 검토하는 긴 메뉴 이름" },
|
|
{ id: "projects/design/layout", label: "레이아웃 관리" },
|
|
] },
|
|
{ id: "projects/tasks", label: "업무 목록" },
|
|
{ id: "projects/schedule", label: "일정 관리" },
|
|
...Array.from({ length: 8 }, (_, i) => ({ id: `projects/extra-${i + 1}`, label: `프로젝트 점검 메뉴 ${i + 1}` })),
|
|
],
|
|
},
|
|
{ id: "reports", label: "현황 보고", icon: ChartColumnIcon },
|
|
{ id: "admin", label: "관리", children: [
|
|
{ id: "admin/users", label: "사용자 관리" },
|
|
{ id: "admin/org", label: "조직 관리" },
|
|
] },
|
|
{ id: "settings", label: "설정", icon: SettingsIcon },
|
|
]
|
|
const sidebarGnbItems: NavItem[] = navItems.map(item => item.id === "projects" ? {
|
|
...item, children: [
|
|
{ id: "projects/design/tokens", label: "토큰 관리" },
|
|
{ id: "projects/tasks", label: "업무 목록" },
|
|
],
|
|
} : item)
|
|
|
|
// `showCurrent={false}` keeps the representative page UI-only; the verification Story keeps the raw route readout.
|
|
function SidebarPage({ fullHeight = false, color = "green", showCurrent = true }: { fullHeight?: boolean; color?: "green" | "brown"; showCurrent?: boolean }) {
|
|
const [current, setCurrent] = useState("projects/design/tokens")
|
|
const [groups, setGroups] = useState<Record<string, boolean>>({ projects: true, "projects/design": true })
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
// Same 1024/1023 contract as LNBMobileMenu; the sole Sheet belongs to the GNB header.
|
|
useEffect(() => {
|
|
const desktop = window.matchMedia("(min-width: 1024px)")
|
|
const closeOnDesktop = () => { if (desktop.matches) setMobileOpen(false) }
|
|
desktop.addEventListener("change", closeOnDesktop)
|
|
return () => desktop.removeEventListener("change", closeOnDesktop)
|
|
}, [])
|
|
const go = (id: string) => (event: MouseEvent) => {
|
|
event.preventDefault()
|
|
setCurrent(id)
|
|
// LNBMobileLink has its own context: close this GNB-owned Sheet in the shared route handler.
|
|
setMobileOpen(false)
|
|
}
|
|
const desktopMenu = (nodes: SidebarNode[]): ReactNode => nodes.map(node => node.children ? (
|
|
<LNBGroup key={node.id} label={node.label} current={isCurrent(current, node.id)}
|
|
open={groups[node.id] ?? false} onOpenChange={open => setGroups(state => ({ ...state, [node.id]: open }))}>
|
|
{desktopMenu(node.children)}
|
|
</LNBGroup>
|
|
) : (
|
|
<LNBItem key={node.id} href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>
|
|
{node.icon ? <node.icon aria-hidden="true" /> : null}{node.label}
|
|
</LNBItem>
|
|
))
|
|
// LNB's existing mobile view is an expanded link list, not a new disclosure menu.
|
|
const mobileMenu = (nodes: SidebarNode[]): ReactNode => nodes.map(node => node.children ? (
|
|
<LNBMobileGroup key={node.id} label={node.label}>{mobileMenu(node.children)}</LNBMobileGroup>
|
|
) : (
|
|
<LNBMobileLink key={node.id} href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>{node.label}</LNBMobileLink>
|
|
))
|
|
return (
|
|
<div data-hm-gnb-layout="sidebar" data-hm-color={color} style={{ height: fullHeight ? "100dvh" : 560 }}>
|
|
<Bar items={sidebarGnbItems} current={current} onCurrent={setCurrent}
|
|
mobileMenuOpen={mobileOpen} onMobileMenuOpenChange={setMobileOpen}
|
|
mobileChildren={<div data-slot="lnb-mobile-nav">{mobileMenu(sidebarNodes)}</div>} />
|
|
<div data-hm-gnb-body>
|
|
<LNB data-hm-gnb-sidebar>
|
|
<LNBNav aria-label="서비스 메뉴">{desktopMenu(sidebarNodes)}</LNBNav>
|
|
</LNB>
|
|
<main data-hm-gnb-main>
|
|
<div style={{ padding: contentStyle.padding }}>
|
|
<h2 className="text-lg font-bold">프로젝트 업무 화면</h2>
|
|
{showCurrent ? <p className="mt-2 text-sm text-muted-foreground" data-current-menu>선택: {current}</p> : null}
|
|
<p className="mt-4 text-sm">선택한 메뉴의 콘텐츠를 표시합니다.</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const meta = { title: "Patterns/Layout/GNB", component: GNB, parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta<typeof GNB>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Representative Story: the green bar with a current item, an open 2-depth panel, long labels, a logo, the optional utility icons and
|
|
// sticky. Menus and the user menu open for real. Narrow the page to 390px for the mobile bar (hamburger → Sheet).
|
|
// Each GNB fills its specimen parent inside the same Inner as the page and section headings.
|
|
// The representative page names the connected area only; the verification Stories keep the default 12px join note.
|
|
const contentLabel = "콘텐츠 영역"
|
|
export const All: Story = {
|
|
name: "GNB",
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="GNB" />
|
|
<StorySection title="Overview">
|
|
<Page label={contentLabel}>
|
|
<Bar />
|
|
</Page>
|
|
</StorySection>
|
|
<StorySection title="States · Navigation Menu Open">
|
|
<div style={{ minHeight: 360 }}>
|
|
<Page label="프로젝트 > 진행 중 프로젝트">
|
|
<Bar initial="projects/active" openMenu="projects" />
|
|
</Page>
|
|
</div>
|
|
</StorySection>
|
|
<StorySection title="Variants · Long Labels">
|
|
<Page label={contentLabel}>
|
|
<Bar items={longNavItems} person={longUser} eyebrow="한맥기술 인프라 통합 시스템" brand="스마트 시설물 통합 관리 시스템" />
|
|
</Page>
|
|
</StorySection>
|
|
<StorySection title="Variants · With Logo">
|
|
<Page label={contentLabel}>
|
|
<Bar logo={testLogo} />
|
|
</Page>
|
|
</StorySection>
|
|
<StorySection title="Variants · Utilities">
|
|
<Page label={contentLabel}>
|
|
<Bar utilities />
|
|
</Page>
|
|
</StorySection>
|
|
<StorySection title="Usage · Sticky">
|
|
<StickyPage />
|
|
</StorySection>
|
|
<StorySection title="Usage · GNB Sidebar">
|
|
<SidebarPage showCurrent={false} />
|
|
</StorySection>
|
|
<StorySection title="Usage · Brown 콘텐츠 LNB"><SidebarPage color="brown" showCurrent={false} /></StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const Default: Story = { tags: ["verification"], render: () => <Page><Bar /></Page> }
|
|
export const Sticky: Story = { tags: ["verification"], render: () => <StickyPage /> }
|
|
export const Sidebar: Story = { tags: ["verification"], name: "GNB Sidebar", render: () => <SidebarPage fullHeight /> }
|
|
export const NavigationMenuOpen: Story = {
|
|
tags: ["verification"],
|
|
name: "Navigation Menu Open",
|
|
render: () => (
|
|
<div style={{ minHeight: 360 }}>
|
|
<Page label="프로젝트 > 진행 중 프로젝트">
|
|
<Bar initial="projects/active" openMenu="projects" />
|
|
</Page>
|
|
</div>
|
|
),
|
|
}
|
|
export const ShortDepthOpen: Story = {
|
|
tags: ["verification"],
|
|
render: () => <Page><Bar openMenu="admin" /></Page>,
|
|
}
|
|
export const RightEdgeLongDepth: Story = {
|
|
tags: ["verification"],
|
|
render: () => <Page><Bar openMenu="admin" items={navItems.map((item) => item.id === "admin" ? { ...item, children: navItems[1].children } : item)} /></Page>,
|
|
}
|
|
|
|
// First / middle / last parents, each with the same short / long 2Depth labels.
|
|
// Position and width remain Production GNB's; Stories only supply menu data.
|
|
const depthAlignmentItems = (long: boolean): NavItem[] => ["first", "middle", "last"].map((id, index) => ({
|
|
id,
|
|
label: ["첫 메뉴", "중간 메뉴", "마지막 메뉴"][index],
|
|
children: long ? navItems[1].children : [{ id: `${id}/short`, label: "짧은 메뉴" }],
|
|
}))
|
|
const depthAlignmentRender = (openMenu: string, long: boolean) => () => <Page><Bar items={depthAlignmentItems(long)} openMenu={openMenu} /></Page>
|
|
export const FirstDepthShort: Story = { tags: ["verification"], render: depthAlignmentRender("first", false) }
|
|
export const FirstDepthLong: Story = { tags: ["verification"], render: depthAlignmentRender("first", true) }
|
|
export const MiddleDepthShort: Story = { tags: ["verification"], render: depthAlignmentRender("middle", false) }
|
|
export const MiddleDepthLong: Story = { tags: ["verification"], render: depthAlignmentRender("middle", true) }
|
|
export const LastDepthShort: Story = { tags: ["verification"], render: depthAlignmentRender("last", false) }
|
|
export const LastDepthLong: Story = { tags: ["verification"], render: depthAlignmentRender("last", true) }
|
|
export const EdgeFirstLong: Story = {
|
|
tags: ["verification"],
|
|
render: () => <div style={{ width: 600, maxWidth: "100%" }}><Page><Bar items={depthAlignmentItems(true)} openMenu="first" /></Page></div>,
|
|
}
|
|
export const EdgeLastLong: Story = {
|
|
tags: ["verification"],
|
|
render: () => <div style={{ width: 600, maxWidth: "100%", marginLeft: "auto" }}><Page><Bar items={depthAlignmentItems(true)} openMenu="last" /></Page></div>,
|
|
}
|
|
export const UserMenuOpen: Story = { tags: ["verification"], name: "User Menu Open", render: () => <Page><Bar userOpen /></Page> }
|
|
export const LongLabels: Story = {
|
|
tags: ["verification"],
|
|
name: "Long Labels",
|
|
render: () => (
|
|
<Page>
|
|
<Bar items={longNavItems} person={longUser} eyebrow="한맥기술 인프라 통합 시스템" brand="스마트 시설물 통합 관리 시스템" />
|
|
</Page>
|
|
),
|
|
}
|
|
export const WithLogo: Story = { tags: ["verification"], name: "With Logo", render: () => <Page><Bar logo={testLogo} /></Page> }
|
|
export const Utilities: Story = { tags: ["verification"], render: () => <Page><Bar utilities /></Page> }
|
|
// Mobile verification: use a 390px viewport (the switch is a viewport media query, so it cannot be shown inside a narrow container).
|
|
export const Mobile: Story = { tags: ["verification"], globals: { viewport: { value: "hmMobile390", isRotated: false } }, render: () => <Page><Bar initial="projects/active" utilities /></Page> }
|
|
export const MobileMenuOpen: Story = {
|
|
tags: ["verification"],
|
|
name: "Mobile Menu Open",
|
|
globals: { viewport: { value: "hmMobile390", isRotated: false } },
|
|
render: () => (
|
|
<Page>
|
|
<Bar initial="projects/active" mobileOpen />
|
|
</Page>
|
|
),
|
|
}
|