feat(sw): refine viewer model layout design and theme tokens
Deploy Storybook / deploy (push) Successful in 1m13s

- Model / Layout tabs follow Figma 7652:40344: gradient faces, gold / green
  gradient borders, blurred outline, inner shadow and top gloss, with every
  color in its own --hm-sw-viewer-tab(s)-* token (consumer-overridable)
- Box height matches the adjacent field group (42px, from the control height
  token); padding 1px 1px 1.5px, button side padding 5px, 1px right margin
  for a tabs-only first group; 378px max width, equal buttons and the
  measured More steps are unchanged; existing text-shadow kept
- Storybook: SW/Viewer > Model / Layout story (states and token override);
  Logo story listed before Responsive More
- AI docs (VIEWER.md, DESIGN_RULES.md) and viewer / text-weight test
  expectations updated
This commit is contained in:
shkim4 committed 2026-10-23 16:27:41 +09:00
1 parent 7a5236f3ad
commit 325ec25e37
6 files changed
+197 -56

No files matched your search

+67 -30
View File
@@ -1,7 +1,7 @@
import { useRef, useState, type ReactNode } from "react"
import { useRef, useState, type CSSProperties, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWHeader, SWHeaderWindowControls, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
SWHeader, SWHeaderWindowControls, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
type SWViewerHeaderGroup, type SWViewerMenuEntry,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
@@ -175,6 +175,43 @@ export const States: Story = {
},
}
// 2026-10-23 Figma 7652:40344: the Model / Layout tabs alone, each selected state, Disabled and a color token override
// (CSS variables on SWTheme, as a consuming program sets them). Hover / Focus: point at or Tab to a button.
function TabsRow({ initial, disabled = false, style }: { initial: string; disabled?: boolean; style?: CSSProperties }) {
const [value, setValue] = useState(initial)
const groups: SWViewerHeaderGroup[] = [{
id: "space", label: "도면 공간", surface: "field", items: [
{ type: "tabs", id: "space", label: "모델과 배치 선택", value, disabled, onValueChange: setValue, options: [{ value: "model", label: "Model", count: 17377 }, { value: "layout", label: "Layout", count: 7377 }] },
],
}]
const header = <SWViewerHeader documentTitle="Model Layout.dwg" groups={groups} />
// Overrides go on the outermost SW scope (SWTheme), where the package declares the tokens.
return <div className="sw-story-scroller" data-tabs-case={initial + (disabled ? "-disabled" : style ? "-override" : "")}><div style={{ width: 1440 }}>{style ? <SWTheme style={style}>{header}</SWTheme> : header}</div></div>
}
const TAB_OVERRIDE = {
"--hm-sw-viewer-tabs-outline-color": "#4a7fd0",
"--hm-sw-viewer-tabs-glow-color": "rgba(74, 127, 208, 0.6)",
"--hm-sw-viewer-tab-gradient": "linear-gradient(180deg, #0b1830, #16305c 50%, #0b1830)",
"--hm-sw-viewer-tab-active-border": "linear-gradient(180deg, #b35f00, #ffb347 40%, #b35f00)",
"--hm-sw-viewer-tab-active-text": "#ffb347",
"--hm-sw-viewer-tab-inactive-border": "linear-gradient(#3d6bb3, #3d6bb3)",
"--hm-sw-viewer-tab-inactive-text": "#dbe7ff",
"--hm-sw-viewer-tab-highlight-color": "#5b8fe0",
} as CSSProperties
export const ModelLayoutTabs: Story = {
name: "Model / Layout",
render: () => (
<StoryPage>
<StoryPageHeader title="SW Viewer — Model / Layout" />
<StorySection title="Model 선택"><TabsRow initial="model" /></StorySection>
<StorySection title="Layout 선택"><TabsRow initial="layout" /></StorySection>
<StorySection title="Disabled"><TabsRow initial="model" disabled /></StorySection>
<StorySection title="색상 토큰 Override (--hm-sw-viewer-tab(s)-*)"><TabsRow initial="model" style={TAB_OVERRIDE} /></StorySection>
</StoryPage>
),
}
const MENU_ENTRIES: SWViewerMenuEntry[] = [
{ type: "label", id: "l-file", label: "파일" },
{ type: "item", id: "open", label: "파일 열기", icon: <FolderOpen size={16} />, shortcut: "Ctrl+O" },
@@ -326,6 +363,34 @@ export const VariableSize: Story = {
},
}
export const Logo: Story = {
name: "Logo",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
{ title: "로고 없음 (기본)", props: {} },
{ title: "logoSrc (이미지 URL)", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } },
{ title: "logo (요소)", props: { logo: <svg viewBox="0 0 32 17" width="32" height="17" aria-hidden="true"><rect x="0.5" y="0.5" width="31" height="16" rx="3" fill="none" stroke="#fff" /><text x="16" y="12.5" fill="#fff" fontSize="10" fontWeight="700" textAnchor="middle">LOGO</text></svg>, logoAlt: "예시 로고" } },
]
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 로고" />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
{/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */}
<div style={{ width: "100%", maxWidth: 1440 }} data-logo-case={row.title}>
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
</div>
</StorySection>
))}
<Log lines={demo.log} />
</div>
</StoryPage>
)
},
}
export const Responsive: Story = {
name: "Responsive More",
render: () => {
@@ -391,34 +456,6 @@ export const CompareWithSWHeader: Story = {
),
}
export const Logo: Story = {
name: "Logo",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
{ title: "로고 없음 (기본)", props: {} },
{ title: "logoSrc (이미지 URL)", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } },
{ title: "logo (요소)", props: { logo: <svg viewBox="0 0 32 17" width="32" height="17" aria-hidden="true"><rect x="0.5" y="0.5" width="31" height="16" rx="3" fill="none" stroke="#fff" /><text x="16" y="12.5" fill="#fff" fontSize="10" fontWeight="700" textAnchor="middle">LOGO</text></svg>, logoAlt: "예시 로고" } },
]
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 로고" />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
{/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */}
<div style={{ width: "100%", maxWidth: 1440 }} data-logo-case={row.title}>
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
</div>
</StorySection>
))}
<Log lines={demo.log} />
</div>
</StoryPage>
)
},
}
function ViewerOnly() {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
// 2026-10-23: the same placeholder logo as the SWHeader row, so the two logo slots compare at the same width.