style(storybook): unify page hierarchy and compact layouts
This commit is contained in:
1 parent
cda60840d8
commit
ff55a805d5
21 files changed
+115
-85
No files matched your search
@@ -1,5 +1,13 @@
|
||||
# Working Context
|
||||
|
||||
## 2026-11-11 — Storybook 페이지 위계·여백 공통화 (미커밋)
|
||||
|
||||
- 공통 프레임 `src/components/story-page.css`: 위·아래 여백 16–24px, H1 28/36/700 margin 0, 설명 Page Header description 토큰(8px·보조색), 제목→첫 내용·섹션 사이 24px(`--sb-page-section-gap`, header margin이 다음 블록 margin과 상쇄), H2 16/24/700(`--sb-page-section-title-*` = dialog body 토큰), 섹션 제목 구분선 8 + 12px. 좌우 여백·SW 기준 폭(2040/1125/787)은 그대로.
|
||||
- `StorySectionGrid`(story-page.tsx) 추가: 나란히 놓인 섹션의 row gap을 grid가 소유. Chart 7개·Toggle이 사용. `StateBoard` 라벨 구분선 제거, 행 간격 40 → 24px.
|
||||
- Button `compactPage`·Tabs `py-4!` 덮어쓰기 제거, Foundation Layout 중복 규칙 제거, 폰트 Weight 카드 박스 제거.
|
||||
- Chart 섹션명을 카드 제목과 겹치지 않는 변형명(Basic·Label·Solid 등)으로 통일, SW 제목 2개(`SW Popup — 속성창`, `SW Viewer — 설정창`) 사이드바 기준으로 정리. SW Story 전용 묶음(`sw-story-stack` 섹션 묶음, Footer 너비 행)은 공통 섹션 간격을 쓰도록 조정 — SW 컴포넌트 CSS는 변경 없음.
|
||||
- 기준 문서: `docs/guidelines/프로젝트-지침.md` §6.3 Storybook 페이지 프레임 항목.
|
||||
|
||||
## 2026-11-05 — Step Header 프로세스 영역 양 끝 직각
|
||||
|
||||
- 사용자 결정: `SWProcessHeader appearance="multi"`만 프로세스 영역(파일명 옆·창 영역 옆) 위 모서리를 직각으로(`default`는 Strana 6px·덮개 5px 유지 — 처음 두 타입 모두로 들었다가 사용자 정정). `sw-process-header.css` multi 블록에 바탕 `::before`·양 끝 덮개 `border-radius: 0` 1줄. 단계 탭 캡슐·화살표·사다리꼴 면·색·높이 변경 없음. Header 최외곽은 원래 직각.
|
||||
|
||||
@@ -157,6 +157,7 @@ HM 지정값이 있는 색상, radius, control height, typography 등은 Theme T
|
||||
- 컴포넌트가 실제 지원하는 default, variant, size, disabled 및 주요 상태를 확인한다. invalid, open 등 해당 컴포넌트에 실제로 있는 상태만 다루고, 없는 상태를 문서화를 위해 만들지 않는다.
|
||||
- Storybook은 구현 Reference Catalog이자 컴포넌트 단위의 검수와 사용법 문서화 도구다. 신규 UI 구현 전 유사 대표/PRIMARY Story를 먼저 확인한다. `Demo/App` 화면은 여러 컴포넌트를 조합한 적용 결과 확인용이며, Demo의 페이지·타일 UI를 Story의 기준으로 쓰지 않는다.
|
||||
- 컴포넌트 Story는 현재 HM 대표 Story의 공통 구조를 따른다. 컴포넌트 이름의 대표 Story 하나에 종류·크기·상태를 한 화면으로 모으고, 같은 구역을 `verification` 태그 Story(예: Roles, Sizes, Brand States)로도 둔다. `verification` Story는 메뉴와 검색에서 기본으로 숨기고(`.storybook/main.ts`의 `tags`) 직접 URL과 사이드바 태그 필터로 연다. HM 전용 Story(Playground, Compatibility)도 같은 태그로 숨긴다. 기본 사이드바에는 컴포넌트가 하위 메뉴 없는 1뎁스 항목 하나로만 보이고(대표 Story 하나만 노출되면 Storybook이 컴포넌트 항목에 합친다), 클릭하면 한 페이지에서 전체 검수 내용이 열린다. Story 이름과 순서, 행·열 구성은 현재 HM 유사 대표 Story의 공통 구성을 먼저 따른다. 외부 ui_viz Story는 제공된 경우에만 충돌하지 않는 참고로 사용한다.
|
||||
- Storybook 페이지 프레임(2026-11-11 확정)은 `src/components/story-page.tsx`·`story-page.css`의 `StoryPage`·`StoryPageHeader`·`StorySection`·`StorySectionGrid`가 공통으로 소유한다. Web·Chart·OrgChart(Admin)의 문서 위계는 H1 페이지 제목 28/36/700 → 설명 14/20/400 보조색(제목 아래 8px) → 24px → H2 섹션 제목 16/24/700과 얇은 구분선 → 12px → 예제이며, 섹션 사이는 24px, `StateBoard`의 H3 라벨은 14/20/500 보조색이고 자체 구분선을 두지 않는다. 위·아래 여백은 16–24px, 좌우 여백은 기존 값(480px까지 16px, 768px부터 60px)을 유지한다. 값은 기존 Component Token을 `--sb-page-*`로 참조하며 새 Design Token이 아니다. 섹션을 나란히 놓을 때는 `StorySectionGrid`를 쓰고, Story별 `!important`·`[&_.sb-page-*]` 덮어쓰기로 간격을 바꾸지 않는다. SW와 노선도(CENTER)는 이 페이지 프레임과 섹션 간격만 공유하고 컴포넌트 크기·글꼴·캔버스·카드 규격은 각자 유지한다. 페이지 제목은 사이드바 이름을 따르며, 한 그룹에 Story가 여럿이면 `그룹 — Story 이름`(예: `SW Header — 화면 너비`)으로 쓴다. 섹션 제목은 페이지 제목과 예제 카드 제목을 반복하지 않는 짧은 변형명(예: Chart `Basic`, `Label`, `Solid`)으로 쓴다.
|
||||
- 대표 Story의 Focus 구역은 pseudo-state 복제 대신 `play` 함수로 실제 요소에 focus를 준다(ui_viz도 Focus는 정적 specimen을 만들지 않는다). hover 같은 상태가 ui_viz에 없으면 `.storybook/pseudo-states.ts`를 쓰지 않는다. 구역 배치 helper는 `src/components/story-board.tsx`의 `StateBoard`를 쓴다.
|
||||
- hover, pressed 같은 pseudo state는 Storybook이 `:hover`/`:active` 규칙을 `data-sb-hover`/`data-sb-active` 규칙으로 복제해 고정한다(`.storybook/pseudo-states.ts`). 복제만 하고 값을 새로 정의하지 않으므로 실제 디자인을 보정하는 용도로 쓰지 않는다. Story에서는 `data-sb-*` 속성만 붙인다.
|
||||
- ui_viz에서 값이 `TBD`인 상태(예: Button Focus)는 별도 specimen을 만들지 않고 키보드로 확인하는 안내만 둔다. ui_viz에 없는 상태(예: Button `aria-invalid`)와 `REFERENCE_ONLY` 항목은 검수 Story에 넣지 않는다. shadcn 호환 API는 `Compatibility` Story로 분리한다.
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
AreaStraightColorLabelExample,
|
||||
AreaStraightExample,
|
||||
} from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Sample data only. The eight examples share the Line Chart data: connection styles as a plain area (left) and with Dots and
|
||||
// value Labels (right), then Stacked (running total) and Expanded (each month normalized to 100%).
|
||||
@@ -26,7 +26,7 @@ export const Area: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Area Chart" />
|
||||
{/* Same grid and order as Line Chart, plus a Stacked | Expanded row. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Curve">
|
||||
<AreaCurveExample />
|
||||
</StorySection>
|
||||
@@ -51,7 +51,7 @@ export const Area: Story = {
|
||||
<StorySection title="Expanded">
|
||||
<AreaExpandedExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SignedBarChartExample,
|
||||
StackedBarChartExample,
|
||||
} from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// shadcn/ui Chart (Recharts) with HM tokens, from the public @hm-design/chart entries. All values are sample data.
|
||||
// Narrow widths: use the toolbar Viewport presets (HM Mobile · 390). The chart is fluid.
|
||||
@@ -24,7 +24,7 @@ export const Bar: Story = {
|
||||
{/* Two sections per row from 1024px (HM desktop boundary), one below. Source order is the mobile order; on desktop it reads
|
||||
row by row: Grouped | Horizontal, then Stacked | Positive / Negative. On desktop every row has the same height and each
|
||||
card fills its section, so card tops, bottoms and chart areas line up. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Grouped">
|
||||
<BarChartExample />
|
||||
</StorySection>
|
||||
@@ -37,7 +37,7 @@ export const Bar: Story = {
|
||||
<StorySection title="Positive / Negative">
|
||||
<SignedBarChartExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DonutPlainExample,
|
||||
DonutSolidExample,
|
||||
} from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Sample data only. The four examples share one data set, color mapping and item order.
|
||||
const meta = {
|
||||
@@ -22,11 +22,11 @@ export const Donut: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Donut Chart" />
|
||||
{/* Same grid as Bar and Line Chart: two per row from 1024px, one below; source order is the mobile order. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="기본형">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Basic">
|
||||
<DonutPlainExample />
|
||||
</StorySection>
|
||||
<StorySection title="기본형 + 텍스트">
|
||||
<StorySection title="Basic · Center Text">
|
||||
<DonutCenterExample />
|
||||
</StorySection>
|
||||
<StorySection title="Active">
|
||||
@@ -35,10 +35,10 @@ export const Donut: Story = {
|
||||
<StorySection title="Interactive">
|
||||
<DonutInteractiveExample />
|
||||
</StorySection>
|
||||
<StorySection title="입체형">
|
||||
<StorySection title="Solid">
|
||||
<DonutSolidExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
LineStraightColorLabelExample,
|
||||
LineStraightExample,
|
||||
} from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Sample data only. All six examples share one data set and axis range, so connection styles compare directly: curve, straight
|
||||
// and step, each as a plain line (left) and with Dots and value labels (right).
|
||||
@@ -25,7 +25,7 @@ export const Line: Story = {
|
||||
<StoryPageHeader title="Line Chart" />
|
||||
{/* Same grid as Bar Chart: two per row from 1024px, one below. Source order is the mobile order; desktop reads row by row:
|
||||
Curve | Curve + labels, Straight | Straight + colored labels, Step | Step + labels. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Curve">
|
||||
<LineCurveExample />
|
||||
</StorySection>
|
||||
@@ -44,7 +44,7 @@ export const Line: Story = {
|
||||
<StorySection title="Step · Dot + Label">
|
||||
<LineStepDotLabelExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { PieLabelExample, PieLabelListExample, PiePlainExample, PieSolidExample, PieStackedExample } from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Sample data only. All pies share one item list and color mapping; the stacked pie draws two series as concentric rings.
|
||||
const meta = {
|
||||
@@ -16,23 +16,23 @@ export const Pie: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Pie Chart" />
|
||||
{/* Same grid as Bar, Line and Donut Chart: two per row from 1024px, one below; source order is the mobile order. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="기본 Pie">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Basic">
|
||||
<PiePlainExample />
|
||||
</StorySection>
|
||||
<StorySection title="Pie + Label">
|
||||
<StorySection title="Label">
|
||||
<PieLabelExample />
|
||||
</StorySection>
|
||||
<StorySection title="Pie + Label List">
|
||||
<StorySection title="Label List">
|
||||
<PieLabelListExample />
|
||||
</StorySection>
|
||||
<StorySection title="Pie + Stacked">
|
||||
<StorySection title="Stacked">
|
||||
<PieStackedExample />
|
||||
</StorySection>
|
||||
<StorySection title="입체형">
|
||||
<StorySection title="Solid">
|
||||
<PieSolidExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { RadarCustomLabelExample, RadarDotExample, RadarLinesOnlyExample, RadarPlainExample } from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Sample data only. All radars share one data set (six axes, two series) and the 0-100 axis range.
|
||||
const meta = {
|
||||
@@ -16,20 +16,20 @@ export const Radar: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Radar Chart" />
|
||||
{/* Same grid as the other Chart pages: two per row from 1024px, one below. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Radar">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Basic">
|
||||
<RadarPlainExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radar + Dot">
|
||||
<StorySection title="Dot">
|
||||
<RadarDotExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radar + Lines Only">
|
||||
<StorySection title="Lines Only">
|
||||
<RadarLinesOnlyExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radar + Custom Label">
|
||||
<StorySection title="Custom Label">
|
||||
<RadarCustomLabelExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
RadialStackedExample,
|
||||
RadialTextExample,
|
||||
} from "@hm-design/chart/examples"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Sample data only. One example per official shadcn/ui radial chart (radial-simple, -label, -grid, -text, -shape, -stacked).
|
||||
const meta = {
|
||||
@@ -23,26 +23,26 @@ export const Radial: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Radial Chart" />
|
||||
{/* Same grid as the other Chart pages: two per row from 1024px, one below; source order is the mobile order. */}
|
||||
<div className="grid grid-cols-1 gap-x-6 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Radial Chart">
|
||||
<StorySectionGrid className="grid-cols-1 lg:auto-rows-fr lg:grid-cols-2 *:flex *:min-w-0 *:flex-col [&_.sb-page-section-body]:flex-1">
|
||||
<StorySection title="Basic">
|
||||
<RadialSimpleExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radial Chart + Label">
|
||||
<StorySection title="Label">
|
||||
<RadialLabelExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radial Chart - Grid">
|
||||
<StorySection title="Grid">
|
||||
<RadialGridExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radial Chart + Text">
|
||||
<StorySection title="Text">
|
||||
<RadialTextExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radial Chart - Shape">
|
||||
<StorySection title="Shape">
|
||||
<RadialShapeExample />
|
||||
</StorySection>
|
||||
<StorySection title="Radial Chart + Stacked">
|
||||
<StorySection title="Stacked">
|
||||
<RadialStackedExample />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -231,18 +231,7 @@ function DestructiveBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
// Compact PC layout of the representative page (Storybook presentation only; Button itself is unchanged).
|
||||
// Page and section spacing are tightened for this Story only through `compactPage`; the shared story-page.css is not edited.
|
||||
// The important modifier is required because the shared rules use the same specificity.
|
||||
const compactPage = [
|
||||
"py-4!",
|
||||
"[&_.sb-page-header]:pb-3!",
|
||||
"[&_.sb-page-title]:mt-1!",
|
||||
"[&_.sb-page-title]:mb-2!",
|
||||
"[&_.sb-page-section]:mt-5!",
|
||||
"[&_.sb-page-section-header]:pb-2!",
|
||||
"[&_.sb-page-section-header]:mb-3!",
|
||||
].join(" ")
|
||||
// Page and section spacing come from the common compact Story page (story-page.css); this Story keeps no override.
|
||||
// 8px gap so the three Brand sizes stay on one line in the narrowest (Brand) column, same as the Variants rows.
|
||||
const compactRow = "flex flex-wrap items-center gap-2"
|
||||
// Equal two columns over the whole content width. StateBoard's own auto-fill grid left an empty third column
|
||||
@@ -285,7 +274,7 @@ export const All: Story = {
|
||||
name: "Button",
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage className={compactPage}>
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Button" />
|
||||
<StorySection title="Sizes">
|
||||
{/* Same columns as Variants below (Brand / Standard Solid / Standard Outline): Brand and Standard Solid sizes line up with
|
||||
|
||||
@@ -10,12 +10,14 @@ export type BoardState = { id: string; label: string; note?: string; noteAfter?:
|
||||
// `fixed` uses columns of exactly `min` px instead of stretching them (1fr). Specimens whose popup is anchored to a Radix
|
||||
// trigger need whole-pixel columns: a fractional column (e.g. 358.4px) puts the trigger edge between device pixels while the
|
||||
// popper rounds the popup to whole pixels, so the two outlines end up 1px apart.
|
||||
// The labels are H3 (14/20/500, secondary) under the section's divider: they carry no divider of their own, and rows sit
|
||||
// 24px apart (the section gap of story-page.css).
|
||||
export function StateBoard({ states, min = 320, fixed = false }: { states: BoardState[]; min?: number; fixed?: boolean }) {
|
||||
// Inside a StorySection the labels sit one heading level below the section title.
|
||||
const Label = useContext(StorySectionContext) ? "h3" : "h2"
|
||||
return (
|
||||
<div
|
||||
className={`grid items-start gap-x-6 gap-y-10 ${fixed ? "[grid-template-columns:repeat(auto-fill,min(100%,var(--sb-state-min,320px)))]" : "[grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sb-state-min,320px)),1fr))]"}`}
|
||||
className={`grid items-start gap-x-6 gap-y-6 ${fixed ? "[grid-template-columns:repeat(auto-fill,min(100%,var(--sb-state-min,320px)))]" : "[grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sb-state-min,320px)),1fr))]"}`}
|
||||
style={{ "--sb-state-min": `${min}px` } as CSSProperties}
|
||||
>
|
||||
{states.map(({ id, label, note, noteAfter, wide, children }) => (
|
||||
@@ -25,7 +27,7 @@ export function StateBoard({ states, min = 320, fixed = false }: { states: Board
|
||||
data-overview-state={id}
|
||||
aria-label={label}
|
||||
>
|
||||
<header className="grid gap-2 border-b border-black/10 pb-2">
|
||||
<header className="grid gap-1">
|
||||
<Label className="text-sm font-medium text-muted-foreground">{label}</Label>
|
||||
{note && !noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
|
||||
</header>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
/* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page.
|
||||
Inner (fluid; 16px each side up to 480px, growing to 60px at 768px; vertical gutter 16–40) and title (kicker 12 / title 28·36·700 / description, no divider below) reuse Foundation / Color.
|
||||
Section reuses the Center Graphic / 교량 split: heading + description, a divider, then the specimen. */
|
||||
2026-11-11 compact document rhythm, shared by Web / Chart / OrgChart (SW and CENTER pages use the same frame only):
|
||||
H1 page title 28/36/700 (Page Header title) → description 14/20/400 secondary, 8px below (Page Header description)
|
||||
→ 24px → H2 section title 16/24/700 (body-md) over a thin divider → 12px → specimen; sections 24px apart (Dashboard gap);
|
||||
H3 specimen labels 14/20/500 secondary, no divider of their own (story-board.tsx). Top / bottom gutter 16–24px.
|
||||
Side margins are unchanged (16px up to 480px, growing to 60px at 768px): the SW breakpoints 2040 / 1125 / 787 depend on them.
|
||||
No design token is added; --sb-page-* only names existing component token values for this Storybook frame. */
|
||||
.sb-page {
|
||||
--sb-page-gutter: clamp(16px, 3vw, 40px);
|
||||
--sb-page-gutter: clamp(16px, 2vw, 24px);
|
||||
--sb-page-section-gap: 24px;
|
||||
--sb-page-section-title-font-size: var(--hm-dialog-body-font-size);
|
||||
--sb-page-section-title-line-height: var(--hm-dialog-body-line-height);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
@@ -20,38 +27,46 @@
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* No bottom padding: the header's margin collapses with the next block's own top margin (a section's 24px), so the first
|
||||
content sits 24px below the title whether it is a section, a section grid or a plain preview. */
|
||||
.sb-page .sb-page-header {
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: var(--sb-page-section-gap);
|
||||
font-size: var(--hm-label-font-size);
|
||||
line-height: var(--hm-label-line-height);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sb-page .sb-page-header .sb-page-kicker {
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 4px;
|
||||
color: var(--hm-sem-fg-secondary);
|
||||
font-size: var(--hm-button-font-size-sm);
|
||||
font-weight: var(--hm-font-weight-bold);
|
||||
}
|
||||
.sb-page .sb-page-header .sb-page-title {
|
||||
margin: 8px 0 12px;
|
||||
margin: 0;
|
||||
font-size: var(--hm-page-header-title-font-size);
|
||||
line-height: var(--hm-page-header-title-line-height);
|
||||
font-weight: var(--hm-page-header-title-font-weight);
|
||||
}
|
||||
.sb-page .sb-page-header .sb-page-description { margin: 0 0 12px; font-weight: var(--hm-input-font-weight); }
|
||||
.sb-page .sb-page-header .sb-page-description {
|
||||
margin: var(--hm-page-header-description-gap) 0 0;
|
||||
color: var(--hm-page-header-description-color);
|
||||
font-size: var(--hm-page-header-description-font-size);
|
||||
line-height: var(--hm-page-header-description-line-height);
|
||||
font-weight: var(--hm-input-font-weight);
|
||||
}
|
||||
|
||||
.sb-page .sb-page-section { margin-top: 32px; scroll-margin-top: 16px; }
|
||||
.sb-page .sb-page-section { margin-top: var(--sb-page-section-gap); scroll-margin-top: 16px; }
|
||||
.sb-page .sb-page-section-header {
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--hm-sem-stroke-input-default);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sb-page .sb-page-section-header .sb-page-section-title {
|
||||
margin: 0;
|
||||
font-size: var(--hm-card-title-font-size);
|
||||
line-height: var(--hm-card-title-line-height);
|
||||
font-weight: var(--hm-card-title-font-weight);
|
||||
font-size: var(--sb-page-section-title-font-size);
|
||||
line-height: var(--sb-page-section-title-line-height);
|
||||
font-weight: var(--hm-font-weight-bold);
|
||||
}
|
||||
.sb-page .sb-page-section-header .sb-page-section-description {
|
||||
margin: 4px 0 0;
|
||||
@@ -61,9 +76,13 @@
|
||||
font-weight: var(--hm-input-font-weight);
|
||||
}
|
||||
.sb-page .sb-page-section-body { min-width: 0; }
|
||||
/* Sections side by side (StorySectionGrid): grid items keep their margins, so the grid owns the row gap instead.
|
||||
Columns and alignment stay with the Story (className): this unlayered rule would override Tailwind's layered grid-cols-*. */
|
||||
.sb-page .sb-page-section-grid { display: grid; gap: var(--sb-page-section-gap) 24px; margin-top: var(--sb-page-section-gap); }
|
||||
.sb-page .sb-page-section-grid > .sb-page-section { margin-top: 0; }
|
||||
|
||||
/* Notice above a page title (StoryNotice). Reuses .sb-page / .sb-page-inner (width and side margin are not redefined).
|
||||
The frame drops its bottom gutter and pulls the next page up so the notice sits 24px above the title block's own top gutter. */
|
||||
The frame drops its bottom gutter and pulls the next page up so the notice sits 24px above the page title. */
|
||||
.sb-page.sb-page-notice {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: calc(24px - var(--sb-page-gutter));
|
||||
|
||||
@@ -70,6 +70,12 @@ export function StorySection({
|
||||
)
|
||||
}
|
||||
|
||||
// Sections side by side (e.g. two charts per row). The grid owns the row gap (the section gap), so rows line up the same
|
||||
// way as stacked sections; columns and alignment come from `className`.
|
||||
export function StorySectionGrid({ className, children }: { className?: string; children: ReactNode }) {
|
||||
return <div className={className ? `sb-page-section-grid ${className}` : "sb-page-section-grid"}>{children}</div>
|
||||
}
|
||||
|
||||
export const STORY_KICKER = {
|
||||
foundation: "HM DESIGN SYSTEM / FOUNDATION",
|
||||
components: "HM DESIGN SYSTEM / COMPONENTS",
|
||||
|
||||
@@ -159,9 +159,8 @@ export const All: Story = {
|
||||
name: "Tabs",
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
// `py-4!` trims this page's top and bottom padding (30-40px by viewport width) so the page fits a 900px-high PC screen without
|
||||
// shrinking any component; the important modifier is needed because the shared rule has the same specificity. Section gaps stay 32px.
|
||||
<StoryPage className="py-4!">
|
||||
// The common compact Story page (16–24px gutter, 24px section gap) fits this page on a 900px-high PC screen without an override.
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Tabs" />
|
||||
<StorySection title="Variants">
|
||||
{/* The former States section (Overflow) is the third Variants item. */}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { BoldIcon, ItalicIcon, StarIcon } from "lucide-react"
|
||||
import { Toggle, ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
|
||||
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Toggle is a persistent pressed-state control. The representative Story shows each state with real, clickable toggles; the same
|
||||
// specimens also exist as Stories tagged `verification` (hidden from the sidebar). Icon-only toggles carry an `aria-label`.
|
||||
@@ -97,14 +97,14 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Toggle" />
|
||||
<div className="grid gap-x-6 min-[1000px]:grid-cols-3">
|
||||
<StorySectionGrid className="grid-cols-1 min-[1000px]:grid-cols-3">
|
||||
<StorySection title="Variants">
|
||||
<StateBoard min={10000} states={[specimen("offon")]} />
|
||||
</StorySection>
|
||||
<StorySection title="States">
|
||||
<StateBoard min={10000} states={[specimen("disabled")]} />
|
||||
</StorySection>
|
||||
</div>
|
||||
</StorySectionGrid>
|
||||
<StorySection title="단독 · 연결 그룹">
|
||||
<SpecimenGrid
|
||||
states={[
|
||||
|
||||
Vendored
+6
-7
@@ -49,11 +49,11 @@
|
||||
.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
|
||||
(two columns on wide screens, stacked rows on narrow screens). */
|
||||
/* Typography: compact specification page. Font Family one line, Font Weight as three plain columns (no card box; the
|
||||
section divider already frames them), Specimens as comparison tables (two columns on wide screens, stacked rows on narrow screens). */
|
||||
.hm-foundation .hm-typo-family { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; margin: 0; }
|
||||
.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px; }
|
||||
.hm-foundation .hm-typo-weight { padding: 12px 16px; }
|
||||
.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px 24px; }
|
||||
.hm-foundation .hm-typo-weight { min-width: 0; }
|
||||
.hm-foundation .hm-typo-weight p { margin: 0; }
|
||||
.hm-foundation .hm-typo-weight-preview { margin: 4px 0 !important; font-size: var(--hm-card-title-font-size); line-height: var(--hm-card-title-line-height); }
|
||||
.hm-foundation .hm-typo-tables { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px 32px; align-items: start; }
|
||||
@@ -73,10 +73,9 @@
|
||||
}
|
||||
|
||||
/* Layout: compact specification page (PC: three bands; narrow screens stack and scroll naturally).
|
||||
Tightening applies only inside .hm-layout-ref; the common Story page and other Foundation pages are unchanged. No fixed heights or clipping. */
|
||||
.hm-foundation .hm-layout-ref { display: grid; gap: 32px; word-break: keep-all; }
|
||||
Section header spacing and the section gap come from the common Story page; the bands only own their grid. No fixed heights or clipping. */
|
||||
.hm-foundation .hm-layout-ref { display: grid; gap: var(--sb-page-section-gap); word-break: keep-all; }
|
||||
.hm-foundation .hm-layout-ref .sb-page-section { margin-top: 0; }
|
||||
.hm-foundation .hm-layout-ref .sb-page-section-header { padding-bottom: 8px; margin-bottom: 12px; }
|
||||
.hm-foundation .hm-layout-ref p, .hm-foundation .hm-layout-ref h3 { margin: 0; }
|
||||
.hm-foundation .hm-layout-ref .hm-foundation-card, .hm-foundation .hm-layout-ref .hm-foundation-panel { padding: 8px 16px; }
|
||||
.hm-foundation .hm-layout-ref code { font-size: var(--hm-button-font-size-sm); color: var(--hm-sem-fg-secondary); }
|
||||
|
||||
@@ -48,7 +48,7 @@ function TypographyReferencePage() {
|
||||
<p className="hm-typo-family"><span>Noto Sans KR → 맑은 고딕 → 시스템 sans-serif</span><code className="hm-foundation-note">--hm-font-family-base: {values["--hm-font-family-base"]}</code></p>
|
||||
</ReferenceSection>
|
||||
<ReferenceSection title="Font Weight">
|
||||
<div className="hm-typo-weights">{weights.map(({ property, label }) => <article className="hm-foundation-card hm-typo-weight" key={property}>
|
||||
<div className="hm-typo-weights">{weights.map(({ property, label }) => <article className="hm-typo-weight" key={property}>
|
||||
<p className="hm-typo-weight-meta"><strong>{label}</strong> <span className="hm-foundation-note">{values[property]}</span></p>
|
||||
<p className="hm-typo-weight-preview" style={{ fontWeight: `var(${property})` }}>한맥 Hanmac 0123</p>
|
||||
<code className="hm-foundation-note">{property}</code>
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
.sw-footer-story-row { display: flex; align-items: flex-start; gap: 16px; }
|
||||
.sw-footer-story-row--tight { gap: 0; }
|
||||
.sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; }
|
||||
.sw-footer-story-width + .sw-footer-story-width { margin-top: 16px; }
|
||||
/* Story page frame only (2026-11-11): the width rows sit in a container (.sw-story-responsive), where margins do not collapse,
|
||||
so the rows own the common section gap (story-page.css) and their sections drop their own top margin. */
|
||||
.sb-page .sw-footer-story-width > .sb-page-section { margin-top: 0; }
|
||||
.sw-footer-story-width + .sw-footer-story-width { margin-top: var(--sb-page-section-gap, 24px); }
|
||||
/* The hidden 1920px row (Responsive More, narrower than 1920px) takes its gap with it. */
|
||||
@container sw-story-page (max-width: 2039.98px) { .sw-story-responsive-hidden + .sw-footer-story-width { margin-top: 0; } }
|
||||
.sw-footer-story-aux-compare { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 16px; }
|
||||
|
||||
@@ -944,5 +944,5 @@ function PropertyCompare() {
|
||||
}
|
||||
/** Rendered by SW/Popup › Property Panel · 4개 비교 (sw-property-panel.stories.tsx, ID sw-header--property-panel-compare). */
|
||||
export function PropertyPanelComparePage() {
|
||||
return <StoryPage className="sw-story-compare-page"><StoryPageHeader title="속성창 4개 상태 비교" /><PropertyCompare /></StoryPage>
|
||||
return <StoryPage className="sw-story-compare-page"><StoryPageHeader title="SW Popup — 속성창" /><PropertyCompare /></StoryPage>
|
||||
}
|
||||
@@ -28,6 +28,10 @@
|
||||
.sw-story-fixed-dimension .hm-sw-header__list-menu--dimension-style[data-shifted="true"]::before { top: calc(-1 * var(--hm-sw-header-field-height) - 1px); }
|
||||
.sw-story-log { margin: 0 0 8px; font-size: 13px; color: #3d4a45; }
|
||||
.sw-story-stack { display: flex; flex-direction: column; gap: 8px; }
|
||||
/* Story page frame only (2026-11-11): a stack of sections (e.g. the 화면 너비 rows) takes the common section gap of
|
||||
story-page.css, so the 8px stack gap and each section's own top margin do not add up. Headers inside are unchanged. */
|
||||
.sb-page .sw-story-stack:has(> .sb-page-section, > div > .sb-page-section) { gap: var(--sb-page-section-gap); margin-top: var(--sb-page-section-gap); }
|
||||
.sb-page .sw-story-stack > .sb-page-section, .sb-page .sw-story-stack > div > .sb-page-section { margin-top: 0; }
|
||||
/* Window Move Motion: window controls alone on the window-area surface, magnified for comparison. */
|
||||
.sw-story-window { display: flex; width: 152px; height: 48px; background: var(--hm-sw-header-document-background); }
|
||||
.sw-story-zoom { zoom: 3; width: max-content; }
|
||||
|
||||
@@ -275,7 +275,7 @@ export const ContentVariants: Story = {
|
||||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] })
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW Viewer — 설정창 (SWViewerPanel)" />
|
||||
<StoryPageHeader title="SW Viewer — 설정창" />
|
||||
<Log lines={demo.log} />
|
||||
{/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */}
|
||||
<ViewerFrame height={620} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
|
||||
Reference in new issue
Block a user