Merge pull request 'Feat/storybook ui refinement' (#20) from feat/storybook-ui-refinement into main
Deploy Storybook / deploy (push) Successful in 57s

Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
shkim4 committed 2026-11-09 18:27:44 +09:00
commit 0be30d34ab
25 files changed
+104 -91

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@ addons.setConfig({
},
theme: create({
base: 'light',
brandTitle: 'Hanmac Design System',
brandTitle: '한맥 디자인 시스템',
brandUrl: './',
brandTarget: '_self',
fontBase: "'Hanmac Manager Noto Sans KR', 'Noto Sans KR', 'Malgun Gothic', sans-serif",
+3 -3
View File
@@ -25,7 +25,7 @@ const preview: Preview = {
includeNames: true,
order: [
"Foundation",
["Hanmac Color", "Company Colors", "Typography", "Layout"],
["한맥 컬러", "가족사 컬러", "Typography"],
"Components",
[
"Form",
@@ -47,8 +47,8 @@ const preview: Preview = {
["Data", ["Data Table", "Filter Bar"], "Layout", ["GNB", "LNB", "Dashboard Layout", "Loading Screen"]],
"SW",
["Header", ["SW Header", "*"], "Footer", ["SW Footer", "*"], "Popup", "Viewer", "Theme"],
"OrgChart",
["Overview", "Department Colors", "Full Composition", "Expanded OrgChart"],
"조직도",
["개요", "부서별 색상", "전체 구성", "확장 조직도"],
"노선도",
"*",
// With includeNames, keeps the Loader group after the Runtime Contract Stories (its place without names).
+10 -8
View File
@@ -16,7 +16,7 @@ import { 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: "노선도/Facility",
title: "노선도/시설물",
id: "center-graphic-facility",
component: TunnelFacility,
parameters: { layout: "padded" },
@@ -31,23 +31,25 @@ const page = (name: string, Demo: ComponentType): Story => ({
render: () => (
<StoryPage>
<StoryPageHeader title={name} />
<StorySection title="Overview">
<StorySection title="개요">
{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)
// `name` is repeated as a literal because Storybook's static indexer cannot read it through page(); it drives the sidebar label.
export const Tunnel: Story = { ...page("터널", TunnelReferenceDemo), name: "터널" }
export const Bridge: Story = { ...page("교량", BridgeReferenceDemo), name: "교량" }
export const Intersection: Story = { ...page("교차로", IntersectionReferenceDemo), name: "교차로" }
export const Junction: Story = { ...page("분기점", JunctionReferenceDemo), name: "분기점" }
export const Interchange: Story = { ...page("나들목", InterchangeReferenceDemo), name: "나들목" }
export const Roadside: Story = { ...page("휴게소 · 졸음쉼터", RoadsideReferenceDemo), name: "휴게소 · 졸음쉼터" }
// 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),
name: "연계 시설물",
render: () => (
<StoryPage className="sb-center-linked-page">
<StoryPageHeader title="연계 시설물" />
+4 -4
View File
@@ -6,7 +6,7 @@ import { 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: "노선도/Information",
title: "노선도/정보",
id: "center-graphic-information",
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta
@@ -15,11 +15,11 @@ 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",
name: "정보",
render: () => (
<StoryPage>
<StoryPageHeader title="Information" />
<StorySection title="Overview">
<StoryPageHeader title="정보" />
<StorySection title="개요">
{withRepresentation(() => (
<div className="sb-center-information">
<CenterInformationDisplayReferenceDemo />
+4 -4
View File
@@ -7,7 +7,7 @@ import { 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: "노선도/Spatial",
title: "노선도/노선·공간",
id: "center-graphic-spatial",
component: RouteLine,
parameters: { layout: "fullscreen", controls: { disable: true } },
@@ -17,11 +17,11 @@ 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",
name: "노선·공간",
render: () => (
<StoryPage className="sb-center-spatial-page">
<StoryPageHeader title="Spatial" />
<StorySection title="Overview">
<StoryPageHeader title="노선·공간" />
<StorySection title="개요">
{withRepresentation(() => <CenterSpatialInformationReferenceDemo />)}
</StorySection>
</StoryPage>
+4 -4
View File
@@ -6,7 +6,7 @@ import { 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: "노선도/Title",
title: "노선도/제목",
id: "center-graphic-title",
component: CenterTitle,
parameters: { layout: "padded" },
@@ -17,12 +17,12 @@ 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",
name: "제목",
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader title="Title" />
<StorySection title="Overview">
<StoryPageHeader title="제목" />
<StorySection title="개요">
{withRepresentation(() => <CenterTitleReferenceDemo />)}
</StorySection>
</StoryPage>
+4 -4
View File
@@ -35,13 +35,13 @@ function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: Co
function ColorReferencePage() {
const values = useResolvedProperties(colorProperties)
return <ReferencePage title="Hanmac Color">
return <ReferencePage title="한맥 컬러">
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="">
<div className="hm-brand-color-row">{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} values={values} />)}</div>
</ReferenceSection>)}
</ReferencePage>
}
// The sidebar name is "Hanmac Color"; `id` keeps the existing Story ID (foundation-color--all) stable.
const meta = { title: "Foundation/Hanmac Color", id: "foundation-color", parameters: { layout: "fullscreen" } } satisfies Meta
// The sidebar name is "한맥 컬러"; `id` keeps the existing Story ID (foundation-color--all) stable.
const meta = { title: "Foundation/한맥 컬러", id: "foundation-color", parameters: { layout: "fullscreen" } } satisfies Meta
export default meta
export const All: StoryObj<typeof meta> = { name: "Hanmac Color", render: () => <ColorReferencePage /> }
export const All: StoryObj<typeof meta> = { name: "한맥 컬러", render: () => <ColorReferencePage /> }
+14 -5
View File
@@ -4,7 +4,15 @@ import { ReferencePage, ReferenceSection } from "./reference"
// Company colors come from the existing OrgChart presets; HEX values are never duplicated here.
// The chip markup and classes are shared with Foundation/Color (foundation.css).
const companies = Object.values(companyColorPresets)
// Display order only. PC rows: 한맥기술/삼안 · 장헌/피티씨 · 장헌산업/한라산업개발/바론컨설턴트.
// Row starts (jangheon, jangheon-industry) are pinned to column 1 in foundation.css via the section ids; companies not listed follow in preset order.
const companyRows = [["hanmac", "saman"], ["jangheon", "ptc"], ["jangheon-industry", "halla", "baron"]]
const presetCompanies = Object.values(companyColorPresets)
const listedIds = companyRows.flat()
const companies = [
...companyRows.flatMap(ids => ids.flatMap(id => presetCompanies.filter(company => company.companyId === id))),
...presetCompanies.filter(company => !listedIds.includes(company.companyId)),
]
function CompanyColorChip({ label, hex, main }: { label: string; hex: string; main: boolean }) {
return <article className="hm-brand-color-chip" data-company-color={hex} data-base={main ? "" : undefined}>
@@ -14,9 +22,9 @@ function CompanyColorChip({ label, hex, main }: { label: string; hex: string; ma
}
function CompanyColorsPage() {
return <ReferencePage title="Company Colors">
return <ReferencePage title="가족사 컬러">
<div className="hm-company-grid">
{companies.map(company => <ReferenceSection key={company.companyId} title={company.companyName} note="">
{companies.map(company => <ReferenceSection key={company.companyId} title={company.companyName} note="" id={`company-${company.companyId}`}>
<div className="hm-brand-color-row" data-company-id={company.companyId}>
<CompanyColorChip label="메인" hex={company.primary} main />
{company.secondary.map((hex, index) => <CompanyColorChip key={hex} label={`서브 ${index + 1}`} hex={hex} main={false} />)}
@@ -25,6 +33,7 @@ function CompanyColorsPage() {
</div>
</ReferencePage>
}
const meta = { title: "Foundation/Company Colors", parameters: { layout: "fullscreen" } } satisfies Meta
// The sidebar name is "가족사 컬러"; `id` keeps the existing Story ID (foundation-company-colors--all) stable.
const meta = { title: "Foundation/가족사 컬러", id: "foundation-company-colors", parameters: { layout: "fullscreen" } } satisfies Meta
export default meta
export const All: StoryObj<typeof meta> = { name: "Company Colors", render: () => <CompanyColorsPage /> }
export const All: StoryObj<typeof meta> = { name: "가족사 컬러", render: () => <CompanyColorsPage /> }
+3
View File
@@ -41,6 +41,9 @@
chips up to 3 per row (min 100px each, 3 -> 2 -> 1). Chip size stays at one third even when a company has fewer chips. */
.hm-foundation .hm-company-grid { display: grid; gap: 48px 32px; grid-template-columns: repeat(auto-fill, minmax(max(316px, (100% - 64px) / 3), 1fr)); }
.hm-foundation .hm-company-grid > .sb-page-section { min-width: 0; margin-top: 0; }
/* Row starts for the 2 / 2 / 3 company layout (한맥·삼안 / 장헌·피티씨 / 장헌산업·한라산업개발·바론컨설턴트). */
.hm-foundation .hm-company-grid > #company-jangheon,
.hm-foundation .hm-company-grid > #company-jangheon-industry { grid-column-start: 1; }
.hm-foundation .hm-brand-color-row[data-company-id] { display: grid; gap: 8px; overflow: visible; padding-bottom: 0; grid-template-columns: repeat(auto-fill, minmax(max(100px, (100% - 16px) / 3), 1fr)); }
/* Typography: compact specification page. Font Family one line, Font Weight small cards, Specimens as comparison tables
+2 -1
View File
@@ -30,6 +30,7 @@ function LayoutReferencePage() {
</div>
</ReferencePage>
}
const meta = { title: "Foundation/Layout", parameters: { layout: "fullscreen" } } satisfies Meta
// Hidden from the sidebar with the verification tag; the Story ID and direct URL (foundation-layout--all) stay available.
const meta = { title: "Foundation/Layout", tags: ["verification"], parameters: { layout: "fullscreen" } } satisfies Meta
export default meta
export const All: StoryObj<typeof meta> = { name: "Layout", render: () => <LayoutReferencePage /> }
+7 -1
View File
@@ -1,7 +1,7 @@
/* Department Colors Storybook adapter only. The actual Tunnel sample/stage
frame comes from center-stories.css; this sheet never selects employee cards. */
.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--sb-center-column-gap);
}
@@ -43,6 +43,12 @@
background: transparent;
}
@media (max-width: 960px) {
.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
grid-template-columns: minmax(0, 1fr);
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { CoreComponentPreview } from './core-component-preview';
import { organizationStylePresets } from '../../packages/org-chart/src/core/style-presets';
const meta = { title: 'OrgChart/Components/Cards', tags: ['internal'], component: CoreComponentPreview, parameters: { layout: 'padded', controls: { disable: true } } } satisfies Meta<typeof CoreComponentPreview>;
const meta = { title: '조직도/Components/Cards', id: 'orgchart-components-cards', tags: ['internal'], component: CoreComponentPreview, parameters: { layout: 'padded', controls: { disable: true } } } satisfies Meta<typeof CoreComponentPreview>;
export default meta;
type Story = StoryObj<typeof meta>;
function organization(variant: number): Story {
@@ -2,7 +2,7 @@ import { StrictMode } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { DepartmentTypographyPreview } from './org-chart-page-previews';
const meta = { title: 'OrgChart/Department Colors', component: DepartmentTypographyPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof DepartmentTypographyPreview>;
const meta = { title: '조직도/부서별 색상', component: DepartmentTypographyPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof DepartmentTypographyPreview>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DepartmentColors: Story = { name: 'Department Colors', parameters: { __id: 'orgchart--department-colors' } };
export const DepartmentColors: Story = { name: '부서별 색상', parameters: { __id: 'orgchart--department-colors' } };
+3 -3
View File
@@ -50,7 +50,7 @@ function ExpandedPreview() {
})), []);
return <StoryPage className="hm-orgchart-story-page hm-orgchart-story-expanded">
<StoryPageHeader title="Expanded OrgChart" />
<StoryPageHeader title="확장 조직도" />
{/* Preview first: the chart sits right under the page title; its own header already names it, so no section header. */}
<div className="hm-orgchart-story hm-orgchart-story-lead">
{error ? <p role="alert" className="hm-orgchart-story-note">{error}</p> : null}
@@ -61,7 +61,7 @@ function ExpandedPreview() {
</StoryPage>;
}
const meta = { title: 'OrgChart/Expanded OrgChart', component: ExpandedPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof ExpandedPreview>;
const meta = { title: '조직도/확장 조직도', component: ExpandedPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof ExpandedPreview>;
export default meta;
type Story = StoryObj<typeof meta>;
export const ExpandedOrgChart: Story = { name: 'Expanded OrgChart', parameters: { __id: 'orgchart--expanded-org-chart' } };
export const ExpandedOrgChart: Story = { name: '확장 조직도', parameters: { __id: 'orgchart--expanded-org-chart' } };
@@ -2,7 +2,7 @@ import { StrictMode } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { CompositionPreview } from './org-chart-page-previews';
const meta = { title: 'OrgChart/Full Composition', component: CompositionPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof CompositionPreview>;
const meta = { title: '조직도/전체 구성', component: CompositionPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof CompositionPreview>;
export default meta;
type Story = StoryObj<typeof meta>;
export const FullComposition: Story = { name: 'Full Composition', parameters: { __id: 'orgchart--full-composition' } };
export const FullComposition: Story = { name: '전체 구성', parameters: { __id: 'orgchart--full-composition' } };
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { CoreComponentPreview } from './core-component-preview';
import './org-chart-stories.css';
const meta = { title: 'OrgChart/Verification/Overview', component: CoreComponentPreview, tags: ['internal'], parameters: { layout: 'padded', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof CoreComponentPreview>;
const meta = { title: '조직도/Verification/Overview', component: CoreComponentPreview, tags: ['internal'], parameters: { layout: 'padded', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof CoreComponentPreview>;
export default meta;
type Story = StoryObj<typeof meta>;
// Literal IDs preserve every existing direct URL after moving the hidden CSF entries.
+2 -2
View File
@@ -2,7 +2,7 @@ import { StrictMode } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { OverviewPreview } from './org-chart-page-previews';
const meta = { title: 'OrgChart/Overview', component: OverviewPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof OverviewPreview>;
const meta = { title: '조직도/개요', component: OverviewPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof OverviewPreview>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LogoPersonnelSummary: Story = { name: 'Overview', parameters: { __id: 'orgchart--logo-personnel-summary' } };
export const LogoPersonnelSummary: Story = { name: '개요', parameters: { __id: 'orgchart--logo-personnel-summary' } };
+3 -3
View File
@@ -19,7 +19,7 @@ export function CompositionPreview() {
const host = useRef<HTMLElement>(null);
useLayoutEffect(() => centerCompositionPreview(host.current!), []);
return <StoryPage className="hm-orgchart-story-page">
<StoryPageHeader title="Full Composition" />
<StoryPageHeader title="전체 구성" />
<StorySection title="고정형 조직도">
<section ref={host} className="hm-orgchart-story hm-orgchart-story-full" aria-label="Full Composition">
<OrgChart dataSource={compositionSource} stylesheetUrls={compositionStylesheets} displayMode="readOnly" className="hm-orgchart-story-chart" />
@@ -30,7 +30,7 @@ export function CompositionPreview() {
export function DepartmentTypographyPreview() {
return <StoryPage className="hm-orgchart-story-page">
<StoryPageHeader title="Department Colors" />
<StoryPageHeader title="부서별 색상" />
<StorySection title="조직별 컬러 · 카드 미리보기">
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="DepartmentColors" presentation="design" /></div>
</StorySection>
@@ -39,7 +39,7 @@ export function DepartmentTypographyPreview() {
export function OverviewPreview() {
return <StoryPage className="hm-orgchart-story-page">
<StoryPageHeader title="Overview" />
<StoryPageHeader title="개요" />
<StorySection title="관계사·회사 로고">
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="Logo" hideLogoHeading /></div>
</StorySection>
+2 -1
View File
@@ -5,7 +5,8 @@ import { CoreComponentPreview } from "./core-component-preview"
// and automatic Raw Count. Acquired logo mapping stays in the shared Core/asset
// layer; Stories do not duplicate summary UI or add employee data fields.
const meta = {
title: "OrgChart/Components/Summaries",
title: "조직도/Components/Summaries",
id: "orgchart-components-summaries",
tags: ["internal"],
component: CoreComponentPreview,
args: { kind: "PositionSummary" },
+7 -6
View File
@@ -167,7 +167,8 @@ function MultipleContent() {
}
const meta = {
title: "OrgChart/Content",
title: "조직도/콘텐츠",
id: "orgchart-content",
tags: ["internal"],
component: OrgChart,
parameters: {
@@ -190,28 +191,28 @@ export default meta
type Story = StoryObj<typeof meta>
export const BasicOrganization: Story = {
name: "Basic Organization",
name: "기본 조직도",
render: args => <Content note="HTML 공통 v1: 조직 3개, 고유 직원 4명 (Root 1 / Team A 2 / Team B 1). 정책 미주입 시 원본 집계만 표시하고 업무상 총원은 미확정입니다."><OrgChart {...args} className="hm-orgchart-story-chart" /></Content>,
}
export const OrganizationHierarchy: Story = {
name: "Organization Hierarchy",
name: "조직 계층",
args: { dataSource: hierarchySource },
render: args => <Content note="공통 v1의 parentId 계층·조직장과 연결선을 확인합니다. showEmployees/showSummary만 숨기며 원본 소속과 집계는 유지합니다. 하위 조직 접기/펼치기는 Core 버튼의 키보드 Enter/Space로 실행합니다."><OrgChart {...args} className="hm-orgchart-story-chart" /></Content>,
}
export const EmployeeCards: Story = {
name: "Employee Cards",
name: "직원 카드",
args: { dataSource: employeeSource },
render: args => <Content note="공통 v1에서 긴 조직명·직원명·직위·직책만 파생한 가상 데이터입니다. null 프로필 이미지는 Core의 첫 글자 대체 표시를 사용합니다. 재직·파견·휴직 상태와 조직장 구분은 동일 Core 노드 UI입니다."><OrgChart {...args} className="hm-orgchart-story-chart" /></Content>,
}
export const OrganizationSummary: Story = {
name: "Organization Summary",
name: "조직 요약",
render: args => <Content note="공통 v1 원본 직원 전체 4명, 직접 인원 Root 1 / Team A 2 / Team B 1. 하위 포함 인원은 서로 합산하지 않습니다. 승인 집계 정책을 임의 생성하지 않으며 업무상 총원은 정책 승인 필요로 표시합니다."><OrgChart {...args} className="hm-orgchart-story-chart" /></Content>,
}
export const JSONDataUpdate: Story = { name: "JSON Data Update", render: () => <DataUpdateContent /> }
export const LoadingEmptyError: Story = { name: "Loading / Empty / Error", render: () => <LoadingContent /> }
export const LoadingEmptyError: Story = { name: "로딩 / 비어 있음 / 오류", render: () => <LoadingContent /> }
export const ResponsiveContainer: Story = { name: "Responsive Container", render: () => <ResponsiveContent /> }
export const MultipleInstances: Story = { name: "Multiple Instances", render: () => <MultipleContent /> }
+8 -4
View File
@@ -450,7 +450,7 @@ const LOGO_CASES: { title: string; props: Partial<SWHeaderProps> }[] = [
]
export const LogoSlot: Story = {
name: "Logo",
name: "로고",
parameters: { controls: { disable: true } },
render: () => (
<StoryPage>
@@ -780,17 +780,21 @@ function StyleHeader({ onLog, showState, ...props }: StyleHostProps) {
const MORE_WIDTHS = [1920, 1440, 1280, 950, 860, 760]
export const ResponsiveMore: Story = {
name: "Responsive More",
name: "화면 너비",
parameters: { controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader title="SW Header — 화면 너비" />
<div className="sw-story-responsive">
<div className="sw-story-stack sw-story-responsive">
{MORE_WIDTHS.map(width => (
// 2026-10-23: the 1920 row stays inside the Story column (as SW Viewer › Responsive) and is hidden on narrow pages.
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined}>
<StorySection title={`${width}px`}>
<MenuFrame width={width} fluid><StyleHeader /></MenuFrame>
{/* Same structure as the Logo page (no black frame). This page only shows widths, so the five field dropdowns are
held closed with the public controlled props; the open callbacks and header data are unchanged. */}
<div style={{ width: "100%", maxWidth: width }} data-frame-width={width}>
<StyleHeader layerMenuOpen={false} colorMenuOpen={false} lineStyleMenuOpen={false} textStyleMenuOpen={false} dimensionStyleMenuOpen={false} />
</div>
</StorySection>
</div>
))}
+6 -7
View File
@@ -141,7 +141,10 @@ const FIGMA_FRAMES: { node: string; step: string; documentTitle?: string }[] = [
{ node: "873:17421", step: "body-plan" },
]
// One page (2026-11-03): the Default header, then the three Multi Step frames; each header keeps its own state.
// The representative page keeps only the middle frame (project open); the three frames stay in the MultiStep verification story.
const OPEN_PROJECT_FRAME = FIGMA_FRAMES[1]
// One page: the Default header, then the project-open Multi Step frame; each header keeps its own state.
export const ProcessHeader: Story = {
name: "Step Header",
render: () => (
@@ -151,11 +154,7 @@ export const ProcessHeader: Story = {
<LiveHeader />
</StorySection>
<StorySection title="Multi Step">
<div className="sw-story-stack">
{FIGMA_FRAMES.map(frame => (
<Row key={frame.node}><div data-figma-node={frame.node}><LiveMultiHeader step={frame.step} documentTitle={frame.documentTitle} /></div></Row>
))}
</div>
<Row><div data-figma-node={OPEN_PROJECT_FRAME.node}><LiveMultiHeader step={OPEN_PROJECT_FRAME.step} documentTitle={OPEN_PROJECT_FRAME.documentTitle} /></div></Row>
</StorySection>
</StoryPage>
),
@@ -164,7 +163,7 @@ export const ProcessHeader: Story = {
const WIDTHS = [1920, 1366, 1100]
export const Responsive: Story = {
name: "Responsive More",
name: "화면 너비",
render: () => (
<StoryPage>
<StoryPageHeader title="SW Step Header — 화면 너비" />
+1 -1
View File
@@ -12,6 +12,6 @@ const meta = {
export default meta
export const PropertyPanelCompare: StoryObj<typeof meta> = {
name: "Property Panel", // 2026-10-22 (user request): was "Property Panel · 4개 비교"; ID unchanged
name: "속성창", // 2026-10-22 (user request): was "Property Panel · 4개 비교"; ID unchanged
render: () => <PropertyPanelComparePage />,
}
-2
View File
@@ -46,5 +46,3 @@
.sw-viewer-story-steps { margin: 0; padding-left: 20px; font-size: 14px; }
.sw-viewer-story-context { display: flex; align-items: center; justify-content: center; height: 160px; border: 1px dashed #9a9284; border-radius: 4px; background: #f5f0e5; color: #3e3523; font-size: 13px; }
.sw-viewer-story-mode { display: flex; gap: 16px; margin-bottom: 8px; font-size: 13px; }
/* Room below a single header row so its logo menu / document list are not cut by the scroller. */
.sw-viewer-story-menu-row { padding-bottom: 120px; }
+10 -21
View File
@@ -1,7 +1,7 @@
import { useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWHeader, SWHeaderWindowControls, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
type SWViewerHeaderGroup, type SWViewerMenuEntry,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
@@ -59,21 +59,12 @@ function AboutPanel({ demo }: { demo: ViewerDemo }) {
}
// 2026-10-22 (user request): the Viewer Header screens have no 1440px maximum; they fill the Story column.
function ViewerScreen({ width = null as number | null, initialDocument = SAMPLES[1].name as string | null, open = [] as ("layers" | "options" | "properties")[], windowControls = false }) {
function ViewerScreen({ width = null as number | null, initialDocument = SAMPLES[1].name as string | null, open = [] as ("layers" | "options" | "properties")[] }) {
const demo = useViewerDemo({ initialDocument, initialPanels: open })
// 2026-10-21 new viewer version: SWHeader's own minimize / close (SWHeaderWindowControls, same icons, size, hover)
// at the right end after the existing trailing items (the `trailing` slot, no new API). The program owns the real
// window actions (onMinimize / onClose, as in SWHeader); the Story only logs them.
const trailing = windowControls ? (
<>
{demo.trailing}
<SWHeaderWindowControls showWindowMove={false} onMinimize={() => demo.record("minimize")} onClose={() => demo.record("close")} />
</>
) : demo.trailing
return (
<>
<Log lines={demo.log} />
<ViewerFrame width={width} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={trailing} onCommand={demo.onCommand} />}>
<ViewerFrame width={width} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<LayerPanel demo={demo} strategy="absolute" />
<OptionsPanel demo={demo} strategy="absolute" inset={{ side: 356 }} />
<PropertiesPanel demo={demo} strategy="absolute" inset={{ side: 24, top: 488 }} />
@@ -87,15 +78,12 @@ export const ViewerHeader: Story = {
name: "Viewer Header",
render: () => (
<StoryPage>
{/* 2026-10-22 (user request): page title and the two sections only. The minimize / close version keeps
SWHeaderWindowControls at the right end of `trailing`; the program owns the real window actions. */}
{/* The minimize / close example was removed from this Story; the component keeps supporting it
(SWHeaderWindowControls at the right end of `trailing`, VIEWER.md §2.1). */}
<StoryPageHeader title="SW Viewer Header" />
<StorySection title="기본형">
<ViewerScreen />
</StorySection>
<StorySection title="최소화 · 닫기 버튼">
<div data-viewer-window-controls=""><ViewerScreen windowControls /></div>
</StorySection>
</StoryPage>
),
}
@@ -280,7 +268,7 @@ export const SettingsPanel: Story = {
export const ContentVariants: Story = {
// 2026-10-21: the one settings-popup Story (former 설정창 and 같은 프레임, 다른 내부 콘텐츠 pages); ID kept.
// 2026-10-22 (user request): the 축척 지정 demo popup was removed.
name: "Settings Popup",
name: "설정창",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] })
return (
@@ -327,7 +315,7 @@ export const VariableSize: Story = {
}
export const Logo: Story = {
name: "Logo",
name: "로고",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
@@ -355,7 +343,7 @@ export const Logo: Story = {
}
export const Responsive: Story = {
name: "Responsive More",
name: "화면 너비",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
return (
@@ -369,7 +357,8 @@ export const Responsive: Story = {
// as the SW Header / Footer 1920 rows (sw-story-responsive-hidden, sw-stories.css), e.g. a 1920px screen.
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined}><StorySection title={`${width}px`}>
{/* The row width is a maximum (2026-10-22): on a narrower screen the header shrinks with it instead of scrolling. */}
<div className="sw-story-scroller sw-viewer-story-menu-row"><div style={{ width: "100%", maxWidth: width }} data-width={width}><SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div></div>
{/* Same structure as the Logo page (no scroller / bottom padding), so the spacing matches it. */}
<div style={{ width: "100%", maxWidth: width }} data-width={width}><SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div>
</StorySection></div>
))}
</div>