import type { ComponentType, ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { CompactFacilityConnection, FACILITY_CONFIGS, LocalAssetFacility, RoadsideFacilityTag, TunnelFacility, type FacilityConfig, type RoadsideEntity, } from "@hm-design/center-graphic" import { LinkedFacilityReferenceDemo } from "@hm-design/center-graphic/reference" import { StoryPage, StoryPageHeader } from "@/components/story-page" import { withRepresentation } from "./center-shared" // Owners: ui-viz docs/visualization/facility.md and facility-assets.md (128 approved SVG references, shipped unchanged). // The display scale (Q2b) is HOLD and Facility has no Selected / Hover state. const meta = { title: "노선도/시설물", id: "center-graphic-facility", component: TunnelFacility, parameters: { layout: "padded" }, } satisfies Meta export default meta type Story = StoryObj // Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic. // 2026-11-13: no visible "개요" section title (사업명 page rule); the specimens keep their indent via the common `sb-page-indent`. const page = (name: string, Demo: ComponentType): Story => ({ name, parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => (
{withRepresentation(() => )}
), }) // 터널 page layout (2026-11-13): composed here from the public API (same props as the package TunnelReferenceDemo) so the // specimens can be grouped — one width-behaviour section with a Position Map column (SEPARATED → SHORT → MEDIUM → LONG) and an // Actual / Detail column (SEPARATED → ACTUAL SHORT → REF → LONG), same steps on the same row (1 column below a 1180px content // width), and the compact applied example full width below. The former standalone Position Map / Actual examples are the // MEDIUM / ACTUAL REF rows (same default name and length). Package classes are reused so the common title row, 12px title gap and the // 8px local scrollbar still apply; the layout lives in center-stories.css (`.sb-center-tunnel*`). const TUNNEL_WIDTH_NAMES: [string, string, string][] = [ ["SHORT", "터널", "L=80m"], ["MEDIUM", "대산2터널", "L=750m"], ["LONG", "서해안고속도로 대산연결 제2터널", "L=12,480m"], ] const TUNNEL_ACTUAL_LABELS = ["ACTUAL SHORT", "ACTUAL REF", "ACTUAL LONG"] function TunnelSample({ title, note, children }: { title: string; note?: string; children: ReactNode }) { return (
{title} {note ? {note} : null}
{children}
) } function TunnelSpecimens() { return (
{/* 2026-11-13: no "너비 동작" title row; each column carries the common specimen title (English + Korean note). */}
{(["positionMap", "actualDetail"] as const).map((variant) => (
{variant === "positionMap" ? "Position Map" : "Actual / Detail"} {variant === "positionMap" ? "위치도" : "실사화"}
SEPARATED
{TUNNEL_WIDTH_NAMES.map(([size, name, length], index) => (
{variant === "positionMap" ? size : TUNNEL_ACTUAL_LABELS[index]}
))}
))}
) } // 교량 · 교차로 · 분기점 · 나들목 page layout (2026-11-13): the 터널 layout applied to the package FacilityReferenceDemo // specimens, composed from the public API with the same props. Position Map | Actual / Detail columns (SEPARATED first, then // SHORT / MEDIUM / LONG and ACTUAL SHORT / REF / LONG on the same rows), the compact applied example full width below. The former // standalone Position Map / Actual examples are the MEDIUM / ACTUAL REF rows (default name and secondary text) and the former // Separated example is each column's first row. `data-sb-compare-at` raises the 2-column breakpoint when the widest row needs it // (교량 LONG 511px artwork: 1210px); without it the 터널 1180px rule applies. function facilityWidthNames(config: FacilityConfig) { return [ config.id === "bridge" ? "1교" : "신시", config.defaultName, config.id === "bridge" ? "서해안고속도로 대산연결 제2교량" : `서해안고속도로 ${config.defaultName}`, ] } function FacilitySpecimens({ config, compareAt }: { config: FacilityConfig; compareAt?: "1210" }) { const widthNames = facilityWidthNames(config) return (
{(["positionMap", "actualDetail"] as const).map((variant) => (
{variant === "positionMap" ? "Position Map" : "Actual / Detail"} {variant === "positionMap" ? "위치도" : "실사화"}
SEPARATED
{widthNames.map((name, index) => (
{variant === "positionMap" ? ["SHORT", "MEDIUM", "LONG"][index] : `ACTUAL ${["SHORT", "REF", "LONG"][index]}`}
))}
))}
) } // 연계 노선 page layout (2026-11-13): the package RoadsideReferenceDemo specimens in the same 2-column structure and the same // single-line row labels as the other facility pages (SHORT / MEDIUM / LONG, ACTUAL SHORT / REF / LONG; the former two-line // "휴게소 · SHORT" meta is replaced — the entity stays in the tag text and its accessible name). Widest row 64px + 16px + 450px // LONG tag, so the 터널 1180px rule applies. Separated exists for Actual / Detail only, so it is that column's first row; at // 2 columns the Position Map column keeps an empty first row so the steps stay on the same rows, at 1 column the empty row is // not rendered. The 연계 / 노선 맥락 example is full width below. const ROADSIDE_TESTS: { size: string; actual: string; entity: RoadsideEntity; label: string }[] = [ { size: "SHORT", actual: "ACTUAL SHORT", entity: "rest-area", label: "휴게소" }, { size: "MEDIUM", actual: "ACTUAL REF", entity: "drowsy-shelter", label: "졸음쉼터" }, { size: "LONG", actual: "ACTUAL LONG", entity: "drowsy-shelter", label: "서해안고속도로 대산방향 졸음쉼터" }, ] function RoadsideSpecimens() { return (
Position Map위치도
Actual / Detail실사화
SEPARATED
{ROADSIDE_TESTS.map((test) => (
{test.actual}
))}
) } // `name` is repeated as a literal because Storybook's static indexer cannot read it through page(); it drives the sidebar label. export const Tunnel: Story = { ...page("터널", TunnelSpecimens), name: "터널" } export const Bridge: Story = { ...page("교량", () => ), name: "교량" } export const Intersection: Story = { ...page("교차로", () => ), name: "교차로" } export const Junction: Story = { ...page("분기점", () => ), name: "분기점" } export const Interchange: Story = { ...page("나들목", () => ), name: "나들목" } export const Roadside: Story = { ...page("휴게소 · 졸음쉼터", RoadsideSpecimens), name: "연계 노선" } // Linked facilities are a standalone section heading followed by item cards. // Keep the reference data/artwork and road-class grouping owned by the package. // 2026-11-13: same `sb-page-indent` start as the other facility pages; the page class scopes the Linked-only CSS // (hidden context notice / READY count / duplicate card name, card title and POSITION · ACTUAL label style). export const Linked: Story = { ...page("연계 시설물", LinkedFacilityReferenceDemo), name: "연계 시설물", render: () => (
{withRepresentation(() => )}
), } export const TunnelPlayground: Story = { tags: ["verification"], name: "직접 테스트 (터널)", decorators: [withRepresentation], args: { name: "대산2터널", secondaryText: "L=750m", variant: "positionMap", separated: false }, argTypes: { variant: { control: "inline-radio", options: ["positionMap", "actualDetail"] } }, }