Initial commit: HMDesignSystem baseline

This commit is contained in:
shkim4 committed 2026-10-08 16:58:09 +09:00
commit b6a0a61103
486 files changed
+68432

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
import type { ComponentType } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { TunnelFacility } from "@hm-design/center-graphic"
import {
BridgeReferenceDemo,
IntersectionReferenceDemo,
InterchangeReferenceDemo,
JunctionReferenceDemo,
LinkedFacilityReferenceDemo,
RoadsideReferenceDemo,
TunnelReferenceDemo,
} from "@hm-design/center-graphic/reference"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { CENTER_BOUNDARY, 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: "Center Graphic/Facility",
component: TunnelFacility,
parameters: { layout: "padded" },
} satisfies Meta<typeof TunnelFacility>
export default meta
type Story = StoryObj<typeof meta>
// Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic.
const page = (name: string, Demo: ComponentType): Story => ({
name,
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.center}
title={name}
description="CENTER 시설물 그래픽입니다. 승인 SVG Reference를 위치도(Position Map)와 실사화(Actual / Detail)로 표시합니다."
/>
<StorySection title="Overview" description={CENTER_BOUNDARY}>
{withRepresentation(() => <Demo />)}
</StorySection>
</StoryPage>
),
})
export const Tunnel: Story = page("터널", TunnelReferenceDemo)
export const Bridge: Story = page("교량", BridgeReferenceDemo)
export const Intersection: Story = page("교차로", IntersectionReferenceDemo)
export const Junction: Story = page("분기점", JunctionReferenceDemo)
export const Interchange: Story = page("나들목", InterchangeReferenceDemo)
export const Roadside: Story = page("휴게소 · 졸음쉼터", RoadsideReferenceDemo)
// Linked facilities are a standalone section heading followed by item cards.
// Keep the reference data/artwork and road-class grouping owned by the package.
export const Linked: Story = {
...page("연계 시설물", LinkedFacilityReferenceDemo),
render: () => (
<StoryPage className="sb-center-linked-page">
<StoryPageHeader
kicker={STORY_KICKER.center}
title="연계 시설물"
description={
<>
CENTER 시설물 그래픽입니다. 승인 SVG Reference를 위치도(Position Map)와 실사화(Actual / Detail)로 표시합니다.
<br />
{CENTER_BOUNDARY}
</>
}
/>
{withRepresentation(() => <LinkedFacilityReferenceDemo />)}
</StoryPage>
),
}
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"] } },
}
+34
View File
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { CenterInformationDisplayReferenceDemo } from "@hm-design/center-graphic/reference"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
// Owner: ui-viz docs/patterns/center-information-presentation.md and center-information-{box,table,emphasis}.md.
// Single-line ellipsis is resolved; multi-line overflow, exact placement and the presentation scale (Q3) are not.
const meta = {
title: "Center Graphic/Information",
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
// Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic.
export const Information: Story = {
name: "Information",
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.center}
title="Information"
description="CENTER 정보 표시(Box · Table · Emphasis) Reference입니다. 단일행 말줄임만 해결된 상태이며 배율(Q3)은 정해지지 않았습니다."
/>
<StorySection title="Overview" description={CENTER_BOUNDARY}>
{withRepresentation(() => (
<div className="sb-center-information">
<CenterInformationDisplayReferenceDemo />
</div>
))}
</StorySection>
</StoryPage>
),
}
+46
View File
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { CENTER_BOUNDARY, CenterFrame, SCALE_NOTE, StatusTable } from "./center-shared"
// Mirrors the ui-viz docs/governance/center-domain.md §5 registry rows. The values stay in the owner documents.
const STATUS: [string, string, string, string][] = [
["Representation Scale (7680×2160 = 1.00 / 3840×2160 = 0.75)", "CONFIRMED_BY_DESIGN", "foundation/center-scale.md §3.5", "CenterRepresentation. 다른 viewport는 PREVIEW_ONLY 1.00"],
["FACILITY_PRESENTATION_SCALE (Q2b)", "HOLD — OWNER_DECISION_REQUIRED", "visualization/facility.md §14.1", "구현하지 않음. scale 값·preset을 만들지 않음"],
["INFORMATION_PRESENTATION_SCALE (Q3)", "HOLD — OWNER_DECISION_REQUIRED", "patterns/center-information-presentation.md §5.7", "구현하지 않음"],
["Title Resolution B 수치 / per-Type palette", "TBD / SCOPED_DESIGN_APPROVAL", "components/center-title.md §6.7 · §8.1", "Reference 그대로"],
["Information body-kind · padding · multi-line · KD-1/KD-4", "TBD", "patterns/center-information-presentation.md §5.3 · §8", "단일행 말줄임(해결)만 표시"],
["Information Box badge anchor · §5.2.2 ink-position", "OWNER_DECISION_REQUIRED / Candidate (authored)", "components/center-information-box.md §5.2", "Reference 그대로. 승격 없음"],
["Facility §12.24 rules (ink basis · LABEL_REGION …)", "OWNER_REVIEW (Q2c)", "visualization/facility.md §12.24", "Reference 동작 표시. 검증 완료로 표현하지 않음"],
["Facility asset manifest (128 references)", "Confirmed current mapping · stable-ID/alias policy PARTIAL", "visualization/facility-assets.md", "승인 SVG를 바이트 그대로 사용"],
["Spatial · Title · Information source SVG 65종", "CANONICAL_ASSET_CANDIDATE (64 RECHECK · 1 ADOPTED)", "visualization/spatial-title-information-assets.md", "runtime 미사용 · 규칙 기반 재구성"],
["Route layer order Shadow → Outline → Primary Stroke", "CONFIRMED", "visualization/composition.md §5.1 · route.md §7.10", "Spatial 노선"],
["전체 Visualization layer stack · collision algorithm", "Observed Candidate / TBD", "visualization/composition.md §6 · §7.5", "구현하지 않음"],
["Railway outline width · line join", "TBD", "visualization/route.md §4.1 · §6.4", "Reference 값 표시, 확정 아님"],
["Spatial §6.2 tile layer · 읍·면·동 radius · §8.1 paint order", "Candidate (authored) · OWNER_REVIEW (R2)", "visualization/spatial-map.md", "Reference 그대로"],
["Color Candidate application", "TBD", "foundation/center-color.md §6–7", "Reference 그대로"],
]
const meta = { title: "Center Graphic/Overview", parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
export const Overview: Story = {
name: "Overview",
render: () => (
<StoryPage>
<StoryPageHeader kicker={STORY_KICKER.center} title="Overview" description={CENTER_BOUNDARY} />
<CenterFrame>
<div className="sb-center-overview">
<StorySection title="Representation Scale" description="제작 해상도별 CENTER 그래픽 표시 배율입니다.">
<p className="sb-center-note">{SCALE_NOTE}</p>
</StorySection>
<StorySection title="Status" description="ui-viz center-domain.md §5 registry의 항목별 상태와 이 Storybook에서의 표현입니다.">
<div className="sb-center-scroll">
<StatusTable rows={STATUS} />
</div>
</StorySection>
</div>
</CenterFrame>
</StoryPage>
),
}
+52
View File
@@ -0,0 +1,52 @@
import type { ReactNode } from "react"
import { CenterRepresentation, type CenterTargetResolution } from "@hm-design/center-graphic"
import "./center-stories.css"
// CENTER is a separate domain from the UI Component System. These stories present the packaged CENTER components and the
// reference demo pages; they do not own, create or promote CENTER contract values (see docs/center-graphic/README.md).
export const CENTER_BOUNDARY =
"CENTER Web Reference 렌더링입니다. Canonical 값·상태는 ui-viz CENTER owner 문서가 소유하며 HOLD / TBD / Candidate / OWNER_DECISION_REQUIRED를 이 화면이 확정하지 않습니다."
export const SCALE_NOTE =
"제작 대상이 7680 × 2160이면 CENTER 그래픽을 100%, 3840 × 2160이면 75%로 표시합니다. Storybook 화면 크기는 배율을 정하지 않으며, 다른 제작 해상도의 배율은 정해지지 않았습니다. 제작 대상은 주소에 centerResolution=3840x2160 을 붙여 바꿉니다."
export const CenterFrame = ({ children }: { children: ReactNode }) => (
<div className="sb-center" lang="ko">
{children}
</div>
)
// A shareable Storybook URL states the production target explicitly; the default is the 7680 reference.
export const centerTargetResolution = (): CenterTargetResolution => {
const search = window.parent !== window ? window.parent.location.search : window.location.search
return new URLSearchParams(search).get("centerResolution") === "3840x2160" ? "3840x2160" : "7680x2160"
}
export const withRepresentation = (Story: () => ReactNode) => (
<CenterFrame>
<CenterRepresentation targetResolution={centerTargetResolution()}>
<Story />
</CenterRepresentation>
</CenterFrame>
)
export function BoundaryNote({ children }: { children?: ReactNode }) {
return <p className="sb-center-note">{children ?? CENTER_BOUNDARY}</p>
}
export function StatusTable({ rows, label = "CENTER 상태" }: { rows: [string, string, string, string][]; label?: string }) {
return (
<table className="sb-center-status" aria-label={label}>
<thead>
<tr><th>항목</th><th>상태</th><th>Owner (ui-viz)</th><th>이 Storybook에서의 표현</th></tr>
</thead>
<tbody>
{rows.map(([item, status, owner, presentation]) => (
<tr key={item} data-center-status={status}>
<td>{item}</td><td><code>{status}</code></td><td>{owner}</td><td>{presentation}</td>
</tr>
))}
</tbody>
</table>
)
}
+32
View File
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { RouteLine } from "@hm-design/center-graphic"
import { CenterSpatialInformationReferenceDemo } from "@hm-design/center-graphic/reference"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
// Owners: ui-viz docs/visualization/route.md, spatial-map.md, spatial-section.md, composition.md.
// Route layer order Shadow -> Outline -> Primary Stroke is CONFIRMED; the whole-map layer order is Candidate and line join is TBD.
const meta = {
title: "Center Graphic/Spatial",
component: RouteLine,
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta<typeof RouteLine>
export default meta
type Story = StoryObj<typeof meta>
// Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic.
export const Spatial: Story = {
name: "Spatial",
render: () => (
<StoryPage className="sb-center-spatial-page">
<StoryPageHeader
kicker={STORY_KICKER.center}
title="Spatial"
description="CENTER 노선·공간 정보 Reference입니다. 노선 레이어 순서(Shadow → Outline → Primary Stroke)는 확정이며, 전체 지도 레이어 순서는 Candidate, line join은 TBD입니다."
/>
<StorySection title="Overview" description={CENTER_BOUNDARY}>
{withRepresentation(() => <CenterSpatialInformationReferenceDemo />)}
</StorySection>
</StoryPage>
),
}
+251
View File
@@ -0,0 +1,251 @@
/* Storybook review chrome only; never select the CENTER artwork itself.
Existing package demo classes are adapters to this common card frame, so
@hm-design/center-graphic and its /reference API remain unchanged. */
.sb-center {
--color-fg-secondary: #6e6759;
--color-stroke-subtle: #e0dace;
--color-bg-content: #f5f0e5;
--color-action-primary: #123328;
/* Measured from TunnelReferenceDemo at 1280px before consolidation.
Local presentation values, not new Foundation / runtime theme tokens. */
--sb-center-card-background: rgba(217, 217, 217, .48);
--sb-center-card-border: 1px solid rgba(64, 5, 0, .14);
--sb-center-card-radius: 8px;
--sb-center-header-background: rgba(255, 255, 255, .62);
--sb-center-header-border: 1px solid rgba(64, 5, 0, .12);
--sb-center-header-padding: 8px 14px;
--sb-center-header-gap: 12px;
--sb-center-content-padding: 24px;
--sb-center-section-gap: 18px;
--sb-center-column-gap: 14px;
box-sizing: border-box;
width: 100%;
min-width: 0;
padding: 8px 0;
}
.sb-center-note { max-width: 880px; margin: 0; color: var(--hm-sem-fg-secondary); font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); overflow-wrap: anywhere; }
.sb-center-status { width: 100%; border-collapse: collapse; font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); }
.sb-center-status th, .sb-center-status td { border: 1px solid #e0dace; padding: 6px 10px; text-align: left; vertical-align: top; }
.sb-center-status th { background: #f4f1ea; font-weight: 600; }
.sb-center-status code { font-size: inherit; }
.sb-center-scroll { min-width: 0; max-width: 100%; overflow-x: auto; }
.sb-center-information { min-width: 0; width: 100%; }
/* One frame for all ordinary reference cards, including Linked item cards.
Overview reuses StorySection rather than introducing another React helper. */
.sb-center :is(.tunnel-reference__sample, .facility-reference__sample,
.center-title-reference__sample, .cid-demo__block, .linked-facility__card,
.sb-page-section, .csi-panel) {
box-sizing: border-box;
min-width: 0;
max-width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: var(--sb-center-card-background);
border: var(--sb-center-card-border);
border-radius: var(--sb-center-card-radius);
}
.sb-center :is(.tunnel-reference, .facility-reference, .center-title-reference,
.cid-demo, .linked-facility, .csi-demo, .sb-center-overview) {
display: grid;
min-width: 0;
gap: var(--sb-center-section-gap);
}
.sb-center-overview { margin-top: 32px; }
.sb-center :is(.tunnel-reference__variant-grid, .facility-reference__variant-grid) {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--sb-center-column-gap);
}
.sb-center :is(.tunnel-reference__sample, .facility-reference__sample,
.center-title-reference__sample, .cid-demo__block,
.linked-facility__card, .sb-page-section) > header {
box-sizing: border-box;
min-width: 0;
min-height: 42px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: var(--sb-center-header-gap);
margin: 0;
padding: var(--sb-center-header-padding);
background: var(--sb-center-header-background);
border-bottom: var(--sb-center-header-border);
overflow-wrap: anywhere;
}
.sb-center .center-title-reference__sample > header > div {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--sb-center-header-gap);
}
.sb-center :is(.tunnel-reference__sample, .facility-reference__sample,
.center-title-reference__sample, .cid-demo__block, .linked-facility__group,
.linked-facility__card, .sb-page-section) > header :is(strong, h2, h3, .sb-page-section-title) {
margin: 0;
color: var(--hm-sem-fg-primary);
font-size: var(--hm-label-font-size);
line-height: var(--hm-label-line-height);
font-weight: var(--hm-font-weight-bold);
}
.sb-center :is(.tunnel-reference__sample, .facility-reference__sample,
.center-title-reference__sample, .cid-demo__block, .linked-facility__group,
.linked-facility__card, .sb-page-section) > header :is(span, small, p, .sb-page-section-description) {
min-width: 0;
margin: 0;
color: var(--hm-sem-fg-secondary);
font-size: var(--hm-button-font-size-sm);
line-height: var(--hm-button-line-height-sm);
font-weight: var(--hm-input-font-weight);
text-align: left;
}
/* No common height: keep each demo's content-driven height / existing minimum.
Safe centering keeps the start of wide specimens reachable by local scroll. */
.sb-center :is(.tunnel-reference__stage, .facility-reference__stage,
.center-title-reference__stage, .cid-demo__stage,
.sb-center-overview .sb-page-section-body, .linked-facility__variant) {
box-sizing: border-box;
min-width: 0;
max-width: 100%;
padding: var(--sb-center-content-padding);
overflow-x: auto;
}
.sb-center :is(.tunnel-reference__stage, .facility-reference__stage) {
grid-template-columns: minmax(0, 1fr);
justify-content: normal;
justify-items: safe center;
}
.sb-center :is(.center-title-reference__stage, .cid-demo__stage) {
border: 0;
border-radius: 0;
background: transparent;
justify-content: safe center;
}
.sb-center .cid-demo__stage > * { flex: 0 0 auto; }
.sb-center .linked-facility__grid {
min-width: 0;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: var(--sb-center-column-gap);
padding: 0;
align-items: start;
}
/* Linked is Section Header + Common Card List. Road-class groups and context
are plain sections, so an item card never sits inside another visual card. */
.sb-center-linked-page .sb-center {
padding-top: var(--sb-center-section-gap);
}
.sb-center .linked-facility :is(.linked-facility__group, .linked-facility__context, .linked-facility__pending) {
min-width: 0;
margin: 0;
padding: 0;
overflow: visible;
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
}
.sb-center .linked-facility__group {
display: grid;
gap: var(--sb-center-section-gap);
}
.sb-center .linked-facility__group > header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sb-center-header-gap);
min-height: 0;
padding: 0;
background: transparent;
border: 0;
}
.sb-center .linked-facility__context {
gap: var(--sb-center-section-gap);
overflow-wrap: anywhere;
}
.sb-center .linked-facility__card {
container: sb-linked-card / inline-size;
}
/* 56px label + 10px gap + measured 251px artwork + 48px common padding.
Stack the label when that row cannot fit; preserve artwork size and local scroll. */
@container sb-linked-card (max-width: 365px) {
.sb-center .linked-facility__variant {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto;
justify-items: safe center;
gap: var(--sb-center-header-gap);
}
.sb-center .linked-facility__variant > small {
grid-row: auto;
}
}
.sb-center .sb-center-status { min-width: 720px; }
/* Spatial cards reuse the common frame above. Contrast belongs to the inner
specimen canvas, never the card, heading, description or controls. */
.sb-center-spatial-page .csi-panel {
grid-template-columns: minmax(0, 1fr);
padding: var(--sb-center-content-padding);
gap: var(--sb-center-header-gap);
overflow: hidden;
}
.sb-center-spatial-page .csi-panel > h4 {
margin: calc(-1 * var(--sb-center-content-padding)) calc(-1 * var(--sb-center-content-padding)) 0;
padding: var(--sb-center-header-padding);
background: var(--sb-center-header-background);
border-bottom: var(--sb-center-header-border);
color: var(--hm-sem-fg-primary);
font-size: var(--hm-label-font-size);
line-height: var(--hm-label-line-height);
font-weight: var(--hm-font-weight-bold);
overflow-wrap: anywhere;
}
.sb-center-spatial-page .csi-panel > :is(.csi-legend, .csi-note) {
color: var(--hm-sem-fg-primary);
font-size: var(--hm-button-font-size-sm);
line-height: var(--hm-button-line-height-sm);
overflow-wrap: anywhere;
}
/* Direct demo divs are canvas/control groups, not nested cards. Keep wide
swatches, compass rows and the fixed 840px extent fixture locally scrollable;
headings and explanatory text stay within the card at every viewport. */
.sb-center-spatial-page .csi-panel > div {
box-sizing: border-box;
min-width: 0;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
}
.sb-center-spatial-page .csi-panel :is(.csi-row-label, .csi-controls label) {
color: var(--hm-sem-fg-primary);
}
/* These fluid-width route canvases already fit the card. Their marker/shadow
overhang must not create a subpixel scrollbar or change the content height. */
.sb-center-spatial-page :is(#csi-area-project-route, #csi-area-direction) > div {
overflow: visible;
}
/* District and boundary canvases stay transparent on the common surface.
Only their review labels use Foundation black; artwork colours stay intact. */
.sb-center-spatial-page :is(#csi-area-administrative, #csi-area-zone-boundary) .csi-row-label {
color: var(--hm-sem-palette-black);
}
/* Preserve the extent fixture's white terminal lines and translucent washes
on its original light canvas. The compass needs no separate light backing. */
.sb-center-spatial-page #csi-area-spatial-section > .csi-spatial-specimens {
background: #efefef;
}
/* The reference demo authors white state captions inline for its dark canvas.
This exact caption selector changes review text only, never semantic artwork. */
.sb-center-spatial-page #csi-area-route .csi-swatchgrid > div > div > div:last-child {
color: var(--hm-sem-fg-primary) !important;
}
@media (max-width: 760px) {
.sb-center :is(.tunnel-reference__variant-grid, .facility-reference__variant-grid) {
grid-template-columns: minmax(0, 1fr);
}
}
+46
View File
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { CenterTitle, CENTER_TITLE_COMPOSITIONS, type CenterTitleComposition } from "@hm-design/center-graphic"
import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
// Owner: ui-viz docs/components/center-title.md. Title Resolution B numeric output is TBD; the per-Type palette is SCOPED_DESIGN_APPROVAL.
const meta = {
title: "Center Graphic/Title",
component: CenterTitle,
parameters: { layout: "padded" },
} satisfies Meta<typeof CenterTitle>
export default meta
type Story = StoryObj<typeof meta>
// Representative Story: the four title types. Named after the area so Storybook shows it as a single sidebar item.
// Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic.
export const Compositions: Story = {
name: "Title",
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader
kicker={STORY_KICKER.center}
title="Title"
description="CENTER 제목 그래픽의 네 가지 Type Reference입니다. Resolution B 수치는 TBD, Type별 palette는 SCOPED_DESIGN_APPROVAL입니다."
/>
<StorySection title="Overview" description={CENTER_BOUNDARY}>
{withRepresentation(() => <CenterTitleReferenceDemo />)}
</StorySection>
</StoryPage>
),
}
export const Playground: Story = {
tags: ["verification"],
decorators: [withRepresentation],
args: { composition: "routeProjectNameRoute", context: "서산영덕선", projectName: "대산~당진 건설공사", routeClassification: "고속국도 제30호", projectRoute: "L=25.36km" },
argTypes: {
composition: {
control: { type: "select", labels: Object.fromEntries(Object.entries(CENTER_TITLE_COMPOSITIONS).map(([key, { type, label }]) => [key, `${type} · ${label}`])) },
options: Object.keys(CENTER_TITLE_COMPOSITIONS) as CenterTitleComposition[],
},
},
render: (args) => <CenterTitle {...args} />,
}