fix(sw): preserve theme token inheritance across viewer components

This commit is contained in:
shkim4 committed 2026-10-20 10:38:28 +09:00
1 parent 40c5b89814
commit 41e963a12a
17 files changed
+445 -21

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
import { useState, type CSSProperties, type ReactNode } from "react"
import { createPortal } from "react-dom"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWFooter, SWFooterCommandLine, SWHeader, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerSection,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { AUX_ITEMS, CATEGORIES, HISTORY } from "./sw-footer-fixture"
import { LayerPanel, OptionsPanel, SAMPLES, useViewerDemo } from "./sw-viewer-fixture"
import "./sw-stories.css"
import "./sw-viewer-stories.css"
// SW theme token inheritance (2026-10-20). The official way to change SW tokens: set them through `style` (or the
// consumer's own class) on the OUTERMOST SW scope — <SWTheme> around the screen, or the component root when it is
// used alone. Nested SW roots (viewer menus, More, panels, footer command line) inherit the value.
// Dotum is used here ONLY to make the inheritance visible: it is NOT the SW default font (design approval pending).
const KICKER = "HM DESIGN SYSTEM / SW"
const VERIFY_FONT = 'Dotum, "돋움", sans-serif'
const VERIFY_THEME = { "--hm-sw-header-font-family": VERIFY_FONT } as CSSProperties
const NESTED_THEME = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' } as CSSProperties
const meta = {
title: "SW/Theme",
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
function Frame({ width, height, header, children, ...rest }: { width: number; height: number; header: ReactNode; children?: ReactNode; [data: `data-${string}`]: string }) {
return (
<div className="sw-story-scroller">
<div className="sw-viewer-story-frame" style={{ width: "100%", maxWidth: width, height }} {...rest}>
{header}
<div className="sw-viewer-story-canvas" aria-label="도면 영역 (예시)">{children}</div>
</div>
</div>
)
}
function ViewerArea({ width, panels, ...rest }: { width: number; panels?: boolean; [data: `data-${string}`]: string }) {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: panels ? ["layers", "options"] : [] })
return (
<Frame width={width} height={panels ? 600 : 220} {...rest}
header={<SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
{panels && <>
<LayerPanel demo={demo} strategy="absolute" defaultPosition="top-start" />
<OptionsPanel demo={demo} strategy="absolute" />
</>}
</Frame>
)
}
function PortalDemo() {
const [target, setTarget] = useState<HTMLDivElement | null>(null)
const content = (label: string) => (
<SWViewerSection title={label}>
<SWViewerHint>Portal로 테마 영역 밖에 렌더링한 SW 요소</SWViewerHint>
<SWViewerButton>확인</SWViewerButton>
</SWViewerSection>
)
return (
<div style={{ display: "grid", gap: 12 }}>
{/* The portals are created inside the themed scope, but their DOM lands in the target below (outside it). */}
<SWTheme style={VERIFY_THEME} data-theme-case="portal-source">
<SWViewerHint>이 영역(테마 지정)에서 아래 대상 영역으로 Portal을 만듭니다.</SWViewerHint>
{target && createPortal(<SWTheme data-portal="unthemed">{content("SWTheme만 (테마 미전달 → 기본 글꼴)")}</SWTheme>, target)}
{target && createPortal(<SWTheme style={VERIFY_THEME} data-portal="themed">{content("SWTheme + 같은 theme style (테마 전달)")}</SWTheme>, target)}
</SWTheme>
<div ref={setTarget} data-portal-target="" style={{ display: "grid", gridTemplateColumns: "repeat(2, 320px)", gap: 12, padding: 12, background: "#e4dbc9" }} />
</div>
)
}
export const ThemeInheritance: Story = {
name: "Theme Inheritance",
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW — 테마 토큰 상속"
description={<>SW 토큰은 가장 바깥 SW 범위(<code>&lt;SWTheme&gt;</code> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 <code>style</code>로만 바꿉니다. 아래 Dotum은 <strong>상속 확인용 검증 값</strong>이며 SW 기본 글꼴(Noto Sans KR)은 바뀌지 않습니다(Dotum 기본 적용은 디자인 승인 대기).</>} />
<StorySection title="1. 기본 (테마 지정 없음)" description="단독 SWViewerHeader. 기본 글꼴 Noto Sans KR. 1000px 폭이라 일부 항목이 More(⋯)로 이동합니다.">
<ViewerArea width={1000} data-theme-case="default" />
</StorySection>
<StorySection title="2. 뷰어 모드 — SWTheme style로 글꼴 지정" description="<SWTheme style={{ '--hm-sw-header-font-family': … }}> 안의 Header·Dropdown·More·패널(내부 컨트롤 포함)이 같은 글꼴을 씁니다.">
<SWTheme style={VERIFY_THEME} data-theme-case="viewer-themed">
<ViewerArea width={1000} panels />
</SWTheme>
</StorySection>
<StorySection title="3. 일반 모드 — SWHeader · SWFooter · 명령창" description="SWHeader Dropdown·창 이동 Tooltip, SWFooter의 명령창(SWFooterCommandLine)·명령 등록 안내(Tooltip)가 바깥 SWTheme의 값을 상속합니다.">
<SWTheme style={VERIFY_THEME} data-theme-case="general-themed">
<div className="sw-story-scroller" style={{ display: "grid", gap: 12, paddingBottom: 300 }}>
<div style={{ width: 1440 }}>
<SWHeader documentTitle="C0020101-006-평면및종단면도(06)" layer={{ name: "0_Layer_Cludmark" }} layerMenuOpen
layers={[{ id: "0", name: "0_Layer_Cludmark" }, { id: "1", name: "A-WALL" }]} />
</div>
<div style={{ width: 1440, marginTop: 240 }}>
<SWFooter categories={CATEGORIES} defaultCategoryId="modify" auxItems={AUX_ITEMS} onSlotAdd={() => {}}
command={<SWFooterCommandLine history={HISTORY} expanded />} />
</div>
</div>
</SWTheme>
</StorySection>
<StorySection title="4. Portal" description="Portal은 DOM 위치를 따르므로 테마 영역 밖으로 나간 요소에는 상속되지 않습니다. Portal 내용을 같은 theme style의 <SWTheme>으로 감싸면 같은 값이 적용됩니다.">
<PortalDemo />
</StorySection>
<StorySection title="5. 여러 테마 영역" description="옆 영역끼리 값이 섞이지 않고, 안쪽 SWTheme의 style은 그 안에서만 적용됩니다.">
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 12 }}>
<SWTheme style={VERIFY_THEME} data-theme-case="area-a"><SWViewerHint>영역 A — 검증 글꼴(Dotum)</SWViewerHint></SWTheme>
<SWTheme data-theme-case="area-b"><SWViewerHint>영역 B — 기본(Noto Sans KR)</SWViewerHint></SWTheme>
<SWTheme style={VERIFY_THEME} data-theme-case="area-c">
<SWViewerHint>영역 C 바깥 — Dotum</SWViewerHint>
<SWTheme style={NESTED_THEME} data-theme-case="area-c-nested"><SWViewerHint>영역 C 안쪽 — Malgun Gothic</SWViewerHint></SWTheme>
</SWTheme>
</div>
</StorySection>
</StoryPage>
),
}