feat(sw): refine viewer model layout design and theme tokens
Deploy Storybook / deploy (push) Successful in 1m13s
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:
1 parent
7a5236f3ad
commit
325ec25e37
6 files changed
+197
-56
No files matched your search
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user