fix(sw): preserve theme token inheritance across viewer components
This commit is contained in:
1 parent
40c5b89814
commit
41e963a12a
17 files changed
+445
-21
No files matched your search
@@ -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><SWTheme></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>
|
||||
),
|
||||
}
|
||||
Reference in new issue
Block a user