Merge pull request 'Feat/storybook ui refinement' (#21) from feat/storybook-ui-refinement into main
Deploy Storybook / deploy (push) Failing after 43s
Deploy Storybook / deploy (push) Failing after 43s
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
commit
8007eb5b11
39 files changed
+259
-113
No files matched your search
+60
-1
@@ -1,5 +1,57 @@
|
|||||||
import type { StorybookConfig } from "@storybook/react-vite"
|
import type { StorybookConfig } from "@storybook/react-vite"
|
||||||
import { readFileSync } from "node:fs"
|
import { readFileSync, realpathSync } from "node:fs"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
// Checkouts that share another checkout's node_modules (a linked git worktree whose node_modules is a junction) also
|
||||||
|
// inherit that checkout's node_modules/@hm-design/* links. Those links point at the other checkout's packages/ui and its
|
||||||
|
// compiled dist, so Storybook would show the other checkout's tokens (a stale palette) instead of this checkout's.
|
||||||
|
// Only in that case, resolve the five @hm-design/ui entry points to this checkout's own source. Nothing is built, the
|
||||||
|
// other checkout's node_modules and dist are only read, and a checkout whose links already point at its own
|
||||||
|
// packages/ui keeps the unchanged dist-based resolution.
|
||||||
|
const checkoutRoot = fileURLToPath(new URL("../", import.meta.url))
|
||||||
|
const localUiRoot = path.join(checkoutRoot, "packages/ui")
|
||||||
|
function uiLinkedElsewhere() {
|
||||||
|
try {
|
||||||
|
return realpathSync(path.join(checkoutRoot, "node_modules/@hm-design/ui")) !== realpathSync(localUiRoot)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const localUiAliases = [
|
||||||
|
[/^@hm-design\/ui$/, "src/index.ts"],
|
||||||
|
[/^@hm-design\/ui\/theme$/, "src/theme/index.ts"],
|
||||||
|
[/^@hm-design\/ui\/theme\.css$/, "src/theme.css"],
|
||||||
|
[/^@hm-design\/ui\/tailwind\.css$/, "src/tailwind.css"],
|
||||||
|
[/^@hm-design\/ui\/dashboard-status\.css$/, "src/patterns/dashboard-status/dashboard-status.css"],
|
||||||
|
].map(([find, file]) => ({ find: find as RegExp, replacement: path.join(localUiRoot, file as string) }))
|
||||||
|
|
||||||
|
// @hm-design/ui's dist/theme.css declares the bundled font with a relative URL. When node_modules/@hm-design/ui is
|
||||||
|
// symlinked to a checkout outside this Vite root, Vite rewrites that URL to /@fs/<outside path> and the dev preview
|
||||||
|
// gets a 403 ("outside of Vite serving allow list"). Point only that font URL at the /fonts static directory the
|
||||||
|
// Manager already uses, instead of widening server.fs.allow. theme.css is inlined through @import, so the rewrite
|
||||||
|
// runs on the already-resolved CSS and is limited to the exact ".../ui/dist/fonts/NotoSansKR-variable.ttf" URL.
|
||||||
|
// Dev server only: the static build emits the font as a bundled asset.
|
||||||
|
const UI_DIST_FONT_URL = /[^"'()\\\s]*\/ui\/dist\/fonts\/NotoSansKR-variable\.ttf/g
|
||||||
|
const FONT_STATIC_URL = "/fonts/NotoSansKR-variable.ttf"
|
||||||
|
|
||||||
|
const hmUiThemeFontPath = {
|
||||||
|
name: "hm-ui-theme-font-path",
|
||||||
|
apply: "serve" as const,
|
||||||
|
enforce: "post" as const,
|
||||||
|
transform(code: string, id: string) {
|
||||||
|
const file = id.split("?")[0]
|
||||||
|
if (!file.endsWith(".css") || !code.includes("NotoSansKR-variable.ttf")) return null
|
||||||
|
const next = code.replace(UI_DIST_FONT_URL, FONT_STATIC_URL)
|
||||||
|
if (next === code) {
|
||||||
|
if (!code.includes(FONT_STATIC_URL)) {
|
||||||
|
this.warn(`[hm-ui-theme-font-path] ${file}: found NotoSansKR-variable.ttf but not at ".../ui/dist/fonts/"; the URL was left unchanged and Preview may fail to load Noto Sans KR.`)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return { code: next, map: null }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
||||||
@@ -21,6 +73,13 @@ const config: StorybookConfig = {
|
|||||||
viteFinal: (config) => {
|
viteFinal: (config) => {
|
||||||
config.server ??= {}
|
config.server ??= {}
|
||||||
config.server.watch = { ...config.server.watch, ignored: ["**/_local/**", "**/storybook-static/**"] }
|
config.server.watch = { ...config.server.watch, ignored: ["**/_local/**", "**/storybook-static/**"] }
|
||||||
|
config.plugins = [...(config.plugins ?? []), hmUiThemeFontPath]
|
||||||
|
if (uiLinkedElsewhere()) {
|
||||||
|
config.resolve ??= {}
|
||||||
|
const existing = config.resolve.alias
|
||||||
|
const current = Array.isArray(existing) ? existing : Object.entries(existing ?? {}).map(([find, replacement]) => ({ find, replacement }))
|
||||||
|
config.resolve.alias = [...localUiAliases, ...current]
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
framework: "@storybook/react-vite",
|
framework: "@storybook/react-vite",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const preview: Preview = {
|
|||||||
includeNames: true,
|
includeNames: true,
|
||||||
order: [
|
order: [
|
||||||
"Foundation",
|
"Foundation",
|
||||||
["한맥 컬러", "가족사 컬러", "Typography"],
|
["한맥 컬러", "가족사 컬러", "폰트"],
|
||||||
"Components",
|
"Components",
|
||||||
[
|
[
|
||||||
"Form",
|
"Form",
|
||||||
@@ -46,7 +46,7 @@ const preview: Preview = {
|
|||||||
"Patterns",
|
"Patterns",
|
||||||
["Data", ["Data Table", "Filter Bar"], "Layout", ["GNB", "LNB", "Dashboard Layout", "Loading Screen"]],
|
["Data", ["Data Table", "Filter Bar"], "Layout", ["GNB", "LNB", "Dashboard Layout", "Loading Screen"]],
|
||||||
"SW",
|
"SW",
|
||||||
["Header", ["SW Header", "*"], "Footer", ["SW Footer", "*"], "Popup", "Viewer", "Theme"],
|
["Header", ["SW Header", "*"], "하단 명령어바", ["SW 하단 명령어바", "*"], "Popup", "Viewer", "Theme"],
|
||||||
"조직도",
|
"조직도",
|
||||||
["개요", "부서별 색상", "전체 구성", "확장 조직도"],
|
["개요", "부서별 색상", "전체 구성", "확장 조직도"],
|
||||||
"노선도",
|
"노선도",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
| variant | 면 | 글자 | 테두리 |
|
| variant | 면 | 글자 | 테두리 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `default` | `--hm-sem-action-primary` (Primary Green `#123328`) | `--hm-sem-on-action-primary` (White) | 면과 동일 |
|
| `default` | `--hm-sem-action-primary` (Primary Green `#164434`) | `--hm-sem-on-action-primary` (White) | 면과 동일 |
|
||||||
| `secondary` | `--hm-sem-bg-subtle` | `--hm-sem-fg-secondary` | 면과 동일 |
|
| `secondary` | `--hm-sem-bg-subtle` | `--hm-sem-fg-secondary` | 면과 동일 |
|
||||||
| `destructive` | `--hm-sem-action-destructive` | White | 면과 동일 |
|
| `destructive` | `--hm-sem-action-destructive` | White | 면과 동일 |
|
||||||
| `outline` | `--hm-sem-bg-surface` | `--hm-sem-fg-primary` | `--hm-sem-stroke-default` |
|
| `outline` | `--hm-sem-bg-surface` | `--hm-sem-fg-primary` | `--hm-sem-stroke-default` |
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
| Page link | 34 × 34px(min-width 34), radius 4px(`--hm-radius-control-md`), 14 / 20 / 500, `--hm-sem-fg-primary` |
|
| Page link | 34 × 34px(min-width 34), radius 4px(`--hm-radius-control-md`), 14 / 20 / 500, `--hm-sem-fg-primary` |
|
||||||
| Previous / Next | 높이 34px, padding `0 12px`, 아이콘 16px, 라벨 `hidden sm:block`("Previous"/"Next", 모바일은 아이콘만) |
|
| Previous / Next | 높이 34px, padding `0 12px`, 아이콘 16px, 라벨 `hidden sm:block`("Previous"/"Next", 모바일은 아이콘만) |
|
||||||
| 간격 | 목록 gap 4px |
|
| 간격 | 목록 gap 4px |
|
||||||
| Current Page | 배경 `--hm-sem-action-primary`(Primary Green `#123328`), 글자 `--hm-sem-on-action-primary`(흰색), weight 700 |
|
| Current Page | 배경 `--hm-sem-action-primary`(Primary Green `#164434`), 글자 `--hm-sem-on-action-primary`(흰색), weight 700 |
|
||||||
| Ellipsis | 34 × 34 영역, 아이콘 16px, `--hm-sem-fg-secondary` |
|
| Ellipsis | 34 × 34 영역, 아이콘 16px, `--hm-sem-fg-secondary` |
|
||||||
|
|
||||||
## 4. 상태 규격
|
## 4. 상태 규격
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@hm-design/ui"
|
|||||||
| 상태 | 면 | 테두리 | 전경 |
|
| 상태 | 면 | 테두리 | 전경 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Default (`data-state="inactive"`) | `rgba(189,223,212,.45)` | top/right `rgba(38,102,81,.35)`, bottom `#DEDBD2` | `#266651` |
|
| Default (`data-state="inactive"`) | `rgba(189,223,212,.45)` | top/right `rgba(38,102,81,.35)`, bottom `#DEDBD2` | `#266651` |
|
||||||
| Selected (`data-state="active"`) | `#FFFFFF` (`--hm-sem-bg-surface`) | top/right/left `#DEDBD2`, bottom 1px 표면색(기준선을 덮어 콘텐츠와 연결), `z-index: 2` | `#123328` |
|
| Selected (`data-state="active"`) | `#FFFFFF` (`--hm-sem-bg-surface`) | top/right/left `#DEDBD2`, bottom 1px 표면색(기준선을 덮어 콘텐츠와 연결), `z-index: 2` | `#164434` |
|
||||||
| Hover (비선택만, `:hover`) | 기본 면 + Black 20% overlay (`linear-gradient`) | 변화 없음 | `#123328` |
|
| Hover (비선택만, `:hover`) | 기본 면 + Black 20% overlay (`linear-gradient`) | 변화 없음 | `#164434` |
|
||||||
| Hover (선택) | 적용 안 함 | — | — |
|
| Hover (선택) | 적용 안 함 | — | — |
|
||||||
| Focus (`:focus-visible`) | shadcn ring 유지 | — | — |
|
| Focus (`:focus-visible`) | shadcn ring 유지 | — | — |
|
||||||
| Disabled | shadcn 기본 `opacity-50`, 클릭 불가 | — | — |
|
| Disabled | shadcn 기본 `opacity-50`, 클릭 불가 | — | — |
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
| Light 3 | #249473 | #C1A770 | #EC8F53 |
|
| Light 3 | #249473 | #C1A770 | #EC8F53 |
|
||||||
| Light 2 | #266651 | #897445 | #F87725 |
|
| Light 2 | #266651 | #897445 | #F87725 |
|
||||||
| Light 1 | #205241 | #604F32 | #EB5F00 |
|
| Light 1 | #205241 | #604F32 | #EB5F00 |
|
||||||
| Default | #123328 | #3E3523 | #CC5200 |
|
| Default | #164434 | #4E422D | #CC5200 |
|
||||||
| Dark 1 | #0A221D | #352D1D | #BC4C00 |
|
| Dark 1 | #11372D | #403726 | #BC4C00 |
|
||||||
| Dark 2 | #071E18 | #2F281C | #883700 |
|
| Dark 2 | #0B2B23 | #2F281C | #883700 |
|
||||||
| Dark 3 | #020A07 | #1D1810 | #4F2000 |
|
| Dark 3 | #030E0A | #0D0B07 | #4F2000 |
|
||||||
|
|
||||||
모든 칸의 Key는 `color.palette.{green|brown|orange}-{light-4|light-3|light-2|light-1|default|dark-1|dark-2|dark-3}`로 선언되어 있다. CSS는 동일 suffix의 `--hm-sem-palette-*` → `--hm-*` Primitive를 사용한다. Orange Dark 1·2 확정 당시에는 83개(색상 81개 + radius 2개)였으며 이후 Generic Status Error 1개와 Brown/Orange Background Stop 10개가 추가됐다. 현재 공개 입력은 94개(색상 92개 + radius 2개)다. Dark 3와 Light 4~Default 값은 유지한다.
|
모든 칸의 Key는 `color.palette.{green|brown|orange}-{light-4|light-3|light-2|light-1|default|dark-1|dark-2|dark-3}`로 선언되어 있다. CSS는 동일 suffix의 `--hm-sem-palette-*` → `--hm-*` Primitive를 사용한다. Orange Dark 1·2 확정 당시에는 83개(색상 81개 + radius 2개)였으며 이후 Generic Status Error 1개와 Brown/Orange Background Stop 10개가 추가됐다. 현재 공개 입력은 94개(색상 92개 + radius 2개)다. Dark 3와 Light 4~Default 값은 유지한다.
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,24 @@
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `color.bg.content` | `--hm-sem-bg-content` | `#f5f0e5` | 콘텐츠 면 | Yes | gnb, lnb |
|
| `color.bg.content` | `--hm-sem-bg-content` | `#f5f0e5` | 콘텐츠 면 | Yes | gnb, lnb |
|
||||||
| `color.bg.surface` | `--hm-sem-bg-surface` | `#ffffff` | 기본/카드/입력 면 | Yes | checkbox, radio, switch, slider, badge, table, input, select, button, gnb |
|
| `color.bg.surface` | `--hm-sem-bg-surface` | `#ffffff` | 기본/카드/입력 면 | Yes | checkbox, radio, switch, slider, badge, table, input, select, button, gnb |
|
||||||
| `color.fg.primary` | `--hm-sem-fg-primary` | `#0a221d` | 주요 텍스트 | Yes | calendar, pagination, badge, table, input, select, gnb |
|
| `color.fg.primary` | `--hm-sem-fg-primary` | `#11372d` | 주요 텍스트 | Yes | calendar, pagination, badge, table, input, select, gnb |
|
||||||
| `color.fg.secondary` | `--hm-sem-fg-secondary` | `#6e6759` | 보조 텍스트 | Yes | calendar, pagination, badge, table, gnb |
|
| `color.fg.secondary` | `--hm-sem-fg-secondary` | `#6e6759` | 보조 텍스트 | Yes | calendar, pagination, badge, table, gnb |
|
||||||
| `color.fg.placeholder` | `--hm-sem-fg-placeholder` | `#999999` | Placeholder | Yes | input, select |
|
| `color.fg.placeholder` | `--hm-sem-fg-placeholder` | `#999999` | Placeholder | Yes | input, select |
|
||||||
| `color.action.primary` | `--hm-sem-action-primary` | `#123328` | Strong / Primary ink | Yes | checkbox/radio hover, slider Hover/Drag halo, pagination current, badge default, button outline, navigation current ink |
|
| `color.action.primary` | `--hm-sem-action-primary` | `#164434` | Strong / Primary ink | Yes | checkbox/radio hover, slider Hover/Drag halo, pagination current, badge default, button outline, navigation current ink |
|
||||||
| `color.action.cancel` | `--hm-sem-action-cancel` | `#6e6759` | 공개 취소 역할 | Yes | Button `variant="cancel"` + `appearance="outline"` |
|
| `color.action.cancel` | `--hm-sem-action-cancel` | `#6e6759` | 공개 취소 역할 | Yes | Button `variant="cancel"` + `appearance="outline"` |
|
||||||
| `color.action.destructive` | `--hm-sem-action-destructive` | `#d32f2f` | 오류/위험 역할 | Yes | checkbox, radio, badge, input, button |
|
| `color.action.destructive` | `--hm-sem-action-destructive` | `#d32f2f` | 오류/위험 역할 | Yes | checkbox, radio, badge, input, button |
|
||||||
| `color.status.error` | `--hm-sem-status-error` | `#b3261e` | Generic Status Error · 일반 상태 오류 · Provisional | Yes | Foundation Color / 선택적 Theme 입력; 기존 Component 자동 연결 없음 |
|
| `color.status.error` | `--hm-sem-status-error` | `#b3261e` | Generic Status Error · 일반 상태 오류 · Provisional | Yes | Foundation Color / 선택적 Theme 입력; 기존 Component 자동 연결 없음 |
|
||||||
| `color.stroke.default` | `--hm-sem-stroke-default` | `#9a9284` | 기본 border | Yes | checkbox, radio, switch, badge |
|
| `color.stroke.default` | `--hm-sem-stroke-default` | `#9a9284` | 기본 border | Yes | checkbox, radio, switch, badge |
|
||||||
| `color.stroke.input.default` | `--hm-sem-stroke-input-default` | `#dedbd2` | 입력/패널 divider border | Yes | table, input, select, GNB 콘텐츠 LNB divider |
|
| `color.stroke.input.default` | `--hm-sem-stroke-input-default` | `#dedbd2` | 입력/패널 divider border | Yes | table, input, select, GNB 콘텐츠 LNB divider |
|
||||||
| `color.stroke.input.focus` | `--hm-sem-stroke-input-focus` | `#266651` | 공통 Focus border/ring | Yes | input, select, button, checkbox, calendar, slider Keyboard Focus halo 등; GNB/LNB는 contextual 예외 |
|
| `color.stroke.input.focus` | `--hm-sem-stroke-input-focus` | `#266651` | 공통 Focus border/ring | Yes | input, select, button, checkbox, calendar, slider Keyboard Focus halo 등; GNB/LNB는 contextual 예외 |
|
||||||
| `color.bg.brand.stop.0` | `--hm-sem-brand-bg-stop-0` | `#0a221d` | 기존 gradient stop: color.bg.brand.stop.0 | Yes | Theme palette / adapter |
|
| `color.bg.brand.stop.0` | `--hm-sem-brand-bg-stop-0` | `#11372d` | 기존 gradient stop: color.bg.brand.stop.0 | Yes | Theme palette / adapter |
|
||||||
| `color.bg.brand.stop.12` | `--hm-sem-brand-bg-stop-12` | `#071e18` | 기존 gradient stop: color.bg.brand.stop.12 | Yes | Theme palette / adapter |
|
| `color.bg.brand.stop.12` | `--hm-sem-brand-bg-stop-12` | `#0b2b23` | 기존 gradient stop: color.bg.brand.stop.12 | Yes | Theme palette / adapter |
|
||||||
| `color.bg.brand.stop.42` | `--hm-sem-brand-bg-stop-42` | `#123328` | 기존 gradient stop: color.bg.brand.stop.42 | Yes | Theme palette / adapter |
|
| `color.bg.brand.stop.42` | `--hm-sem-brand-bg-stop-42` | `#164434` | 기존 gradient stop: color.bg.brand.stop.42 | Yes | Theme palette / adapter |
|
||||||
| `color.bg.brand.stop.78` | `--hm-sem-brand-bg-stop-78` | `#266651` | 기존 gradient stop: color.bg.brand.stop.78 | Yes | Theme palette / adapter |
|
| `color.bg.brand.stop.78` | `--hm-sem-brand-bg-stop-78` | `#266651` | 기존 gradient stop: color.bg.brand.stop.78 | Yes | Theme palette / adapter |
|
||||||
| `color.bg.brand.stop.100` | `--hm-sem-brand-bg-stop-100` | `#205241` | 기존 gradient stop: color.bg.brand.stop.100 | Yes | Theme palette / adapter |
|
| `color.bg.brand.stop.100` | `--hm-sem-brand-bg-stop-100` | `#205241` | 기존 gradient stop: color.bg.brand.stop.100 | Yes | Theme palette / adapter |
|
||||||
| `color.bg.brown.stop.0` | `--hm-sem-brown-bg-stop-0` | `#352d1d` | Brown Background · Dark 1 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
| `color.bg.brown.stop.0` | `--hm-sem-brown-bg-stop-0` | `#403726` | Brown Background · Dark 1 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
||||||
| `color.bg.brown.stop.12` | `--hm-sem-brown-bg-stop-12` | `#2f281c` | Brown Background · Dark 2 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
| `color.bg.brown.stop.12` | `--hm-sem-brown-bg-stop-12` | `#2f281c` | Brown Background · Dark 2 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
||||||
| `color.bg.brown.stop.42` | `--hm-sem-brown-bg-stop-42` | `#3e3523` | Brown Background · Default | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
| `color.bg.brown.stop.42` | `--hm-sem-brown-bg-stop-42` | `#4e422d` | Brown Background · Default | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
||||||
| `color.bg.brown.stop.78` | `--hm-sem-brown-bg-stop-78` | `#897445` | Brown Background · Light 2 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
| `color.bg.brown.stop.78` | `--hm-sem-brown-bg-stop-78` | `#897445` | Brown Background · Light 2 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
||||||
| `color.bg.brown.stop.100` | `--hm-sem-brown-bg-stop-100` | `#604f32` | Brown Background · Light 1 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
| `color.bg.brown.stop.100` | `--hm-sem-brown-bg-stop-100` | `#604f32` | Brown Background · Light 1 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
||||||
| `color.bg.orange.stop.0` | `--hm-sem-orange-bg-stop-0` | `#bc4c00` | Orange Background · Dark 1 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
| `color.bg.orange.stop.0` | `--hm-sem-orange-bg-stop-0` | `#bc4c00` | Orange Background · Dark 1 | Yes · 5개 세트 | 공통 Background / Foundation Color |
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
| `surface.action.primary.stop.30` | `--hm-sem-action-surface-stop-30` | `#165c47` | 기존 gradient stop: surface.action.primary.stop.30 | Yes | button, gnb |
|
| `surface.action.primary.stop.30` | `--hm-sem-action-surface-stop-30` | `#165c47` | 기존 gradient stop: surface.action.primary.stop.30 | Yes | button, gnb |
|
||||||
| `surface.action.primary.stop.65` | `--hm-sem-action-surface-stop-65` | `#114838` | 기존 gradient stop: surface.action.primary.stop.65 | Yes | button, gnb |
|
| `surface.action.primary.stop.65` | `--hm-sem-action-surface-stop-65` | `#114838` | 기존 gradient stop: surface.action.primary.stop.65 | Yes | button, gnb |
|
||||||
| `surface.action.primary.stop.100` | `--hm-sem-action-surface-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.stop.100 | Yes | button, gnb |
|
| `surface.action.primary.stop.100` | `--hm-sem-action-surface-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.stop.100 | Yes | button, gnb |
|
||||||
| `surface.action.primary.border.stop.0` | `--hm-sem-action-border-stop-0` | `#0a221d` | 기존 gradient stop: surface.action.primary.border.stop.0 | Yes | button |
|
| `surface.action.primary.border.stop.0` | `--hm-sem-action-border-stop-0` | `#11372d` | 기존 gradient stop: surface.action.primary.border.stop.0 | Yes | button |
|
||||||
| `surface.action.primary.border.stop.50` | `--hm-sem-action-border-stop-50` | `#34daaa` | 기존 gradient stop: surface.action.primary.border.stop.50 | Yes | button |
|
| `surface.action.primary.border.stop.50` | `--hm-sem-action-border-stop-50` | `#34daaa` | 기존 gradient stop: surface.action.primary.border.stop.50 | Yes | button |
|
||||||
| `surface.action.primary.border.stop.100` | `--hm-sem-action-border-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.border.stop.100 | Yes | button |
|
| `surface.action.primary.border.stop.100` | `--hm-sem-action-border-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.border.stop.100 | Yes | button |
|
||||||
| `radius.input.default` | `--hm-radius-input` | `4px` | Input/Select radius | Yes | input, select |
|
| `radius.input.default` | `--hm-radius-input` | `4px` | Input/Select radius | Yes | input, select |
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
| `color.palette.green-light-3` | `--hm-sem-palette-green-light-3` | `#249473` | 선택적 Theme 팔레트: green-light-3 | Yes | data-table Sort Direction Accent, tabs 장식, lnb gradient |
|
| `color.palette.green-light-3` | `--hm-sem-palette-green-light-3` | `#249473` | 선택적 Theme 팔레트: green-light-3 | Yes | data-table Sort Direction Accent, tabs 장식, lnb gradient |
|
||||||
| `color.palette.green-light-2` | `--hm-sem-palette-green-light-2` | `#266651` | 선택적 Theme 팔레트: green-light-2 | Yes | checkbox, radio, switch, slider, progress, calendar, table, input, select, gnb, lnb |
|
| `color.palette.green-light-2` | `--hm-sem-palette-green-light-2` | `#266651` | 선택적 Theme 팔레트: green-light-2 | Yes | checkbox, radio, switch, slider, progress, calendar, table, input, select, gnb, lnb |
|
||||||
| `color.palette.green-light-1` | `--hm-sem-palette-green-light-1` | `#205241` | 선택적 Theme 팔레트: green-light-1 | Yes | table, button, lnb |
|
| `color.palette.green-light-1` | `--hm-sem-palette-green-light-1` | `#205241` | 선택적 Theme 팔레트: green-light-1 | Yes | table, button, lnb |
|
||||||
| `color.palette.green-default` | `--hm-sem-palette-green-default` | `#123328` | 선택적 Theme 팔레트: green-default | Yes | checkbox, radio, slider, calendar, pagination, badge, select, button, gnb, lnb |
|
| `color.palette.green-default` | `--hm-sem-palette-green-default` | `#164434` | 선택적 Theme 팔레트: green-default | Yes | checkbox, radio, slider, calendar, pagination, badge, select, button, gnb, lnb |
|
||||||
| `color.palette.green-dark-1` | `--hm-sem-palette-green-dark-1` | `#0a221d` | 선택적 Theme 팔레트: green-dark-1 | Yes | calendar, pagination, badge, table, input, select, button, gnb |
|
| `color.palette.green-dark-1` | `--hm-sem-palette-green-dark-1` | `#11372d` | 선택적 Theme 팔레트: green-dark-1 | Yes | calendar, pagination, badge, table, input, select, button, gnb |
|
||||||
| `color.palette.green-dark-2` | `--hm-sem-palette-green-dark-2` | `#071e18` | 선택적 Theme 팔레트: green-dark-2 | Yes | Theme palette / adapter |
|
| `color.palette.green-dark-2` | `--hm-sem-palette-green-dark-2` | `#0b2b23` | 선택적 Theme 팔레트: green-dark-2 | Yes | Theme palette / adapter |
|
||||||
| `color.palette.brown-light-2` | `--hm-sem-palette-brown-light-2` | `#897445` | 선택적 Theme 팔레트: brown-light-2 | Yes | button |
|
| `color.palette.brown-light-2` | `--hm-sem-palette-brown-light-2` | `#897445` | 선택적 Theme 팔레트: brown-light-2 | Yes | button |
|
||||||
| `color.palette.brown-light-1` | `--hm-sem-palette-brown-light-1` | `#604f32` | 선택적 Theme 팔레트: brown-light-1 | Yes | button |
|
| `color.palette.brown-light-1` | `--hm-sem-palette-brown-light-1` | `#604f32` | 선택적 Theme 팔레트: brown-light-1 | Yes | button |
|
||||||
| `color.palette.brown-default` | `--hm-sem-palette-brown-default` | `#3e3523` | 선택적 Theme 팔레트: brown-default | Yes | button |
|
| `color.palette.brown-default` | `--hm-sem-palette-brown-default` | `#4e422d` | 선택적 Theme 팔레트: brown-default | Yes | button |
|
||||||
| `color.palette.brown-dark-1` | `--hm-sem-palette-brown-dark-1` | `#352d1d` | 선택적 Theme 팔레트: brown-dark-1 | Yes | button |
|
| `color.palette.brown-dark-1` | `--hm-sem-palette-brown-dark-1` | `#403726` | 선택적 Theme 팔레트: brown-dark-1 | Yes | button |
|
||||||
| `color.palette.brown-light-4` | `--hm-sem-palette-brown-light-4` | `#ecd4a2` | 선택적 Theme 팔레트: brown-light-4 | Yes | Theme palette / adapter |
|
| `color.palette.brown-light-4` | `--hm-sem-palette-brown-light-4` | `#ecd4a2` | 선택적 Theme 팔레트: brown-light-4 | Yes | Theme palette / adapter |
|
||||||
| `color.palette.orange-light-4` | `--hm-sem-palette-orange-light-4` | `#feba88` | 선택적 Theme 팔레트: orange-light-4 | Yes | button |
|
| `color.palette.orange-light-4` | `--hm-sem-palette-orange-light-4` | `#feba88` | 선택적 Theme 팔레트: orange-light-4 | Yes | button |
|
||||||
| `color.palette.orange-default` | `--hm-sem-palette-orange-default` | `#cc5200` | 선택적 Theme 팔레트: orange-default | Yes | button |
|
| `color.palette.orange-default` | `--hm-sem-palette-orange-default` | `#cc5200` | 선택적 Theme 팔레트: orange-default | Yes | button |
|
||||||
@@ -89,10 +89,10 @@
|
|||||||
| `color.palette.gray-dark` | `--hm-sem-palette-gray-dark` | `#5a5a5a` | 선택적 Theme 팔레트: gray-dark | Yes | button |
|
| `color.palette.gray-dark` | `--hm-sem-palette-gray-dark` | `#5a5a5a` | 선택적 Theme 팔레트: gray-dark | Yes | button |
|
||||||
| `color.palette.indicator` | `--hm-sem-palette-indicator` | `#2d2d2d` | 선택적 Theme 팔레트: indicator | Yes | select |
|
| `color.palette.indicator` | `--hm-sem-palette-indicator` | `#2d2d2d` | 선택적 Theme 팔레트: indicator | Yes | select |
|
||||||
|
|
||||||
| `color.palette.green-dark-3` | `--hm-sem-palette-green-dark-3` | `#020a07` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.green-dark-3` | `--hm-sem-palette-green-dark-3` | `#030e0a` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
| `color.palette.brown-light-3` | `--hm-sem-palette-brown-light-3` | `#c1a770` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.brown-light-3` | `--hm-sem-palette-brown-light-3` | `#c1a770` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
| `color.palette.brown-dark-2` | `--hm-sem-palette-brown-dark-2` | `#2f281c` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.brown-dark-2` | `--hm-sem-palette-brown-dark-2` | `#2f281c` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
| `color.palette.brown-dark-3` | `--hm-sem-palette-brown-dark-3` | `#1d1810` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.brown-dark-3` | `--hm-sem-palette-brown-dark-3` | `#0d0b07` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
| `color.palette.orange-light-3` | `--hm-sem-palette-orange-light-3` | `#ec8f53` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.orange-light-3` | `--hm-sem-palette-orange-light-3` | `#ec8f53` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
| `color.palette.orange-light-2` | `--hm-sem-palette-orange-light-2` | `#f87725` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.orange-light-2` | `--hm-sem-palette-orange-light-2` | `#f87725` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
| `color.palette.orange-light-1` | `--hm-sem-palette-orange-light-1` | `#eb5f00` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
| `color.palette.orange-light-1` | `--hm-sem-palette-orange-light-1` | `#eb5f00` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ HMDesignSystem의 핵심 원칙은 다음과 같다.
|
|||||||
- Progress는 현재 Active와 색 정책이 같아 같은 Semantic을 쓴다. 의미가 달라지면 별도 Semantic으로 분리한다.
|
- Progress는 현재 Active와 색 정책이 같아 같은 Semantic을 쓴다. 의미가 달라지면 별도 Semantic으로 분리한다.
|
||||||
- 대상 아님: Toggle / Toggle Group(on = `--accent` 면, 별도 정책), GNB·LNB·Navigation Menu current(기존 Strong Brand 언어), Error/Destructive, Button.
|
- 대상 아님: Toggle / Toggle Group(on = `--accent` 면, 별도 정책), GNB·LNB·Navigation Menu current(기존 Strong Brand 언어), Error/Destructive, Button.
|
||||||
- **Persistent Selected Surface**: Toggle / Toggle Group의 Selected 및 Selected + Hover = `--accent` 100% / `--accent-foreground`(Git baseline `d101530` 매핑, 2026-10-06 원복). 아이콘은 전경을 따르고 border는 기존 default 투명 / outline input stroke를 유지한다. Active override에 영향받지 않고 Accent override를 따른다. Focus override는 별도 ring만 바꾼다.
|
- **Persistent Selected Surface**: Toggle / Toggle Group의 Selected 및 Selected + Hover = `--accent` 100% / `--accent-foreground`(Git baseline `d101530` 매핑, 2026-10-06 원복). 아이콘은 전경을 따르고 border는 기존 default 투명 / outline input stroke를 유지한다. Active override에 영향받지 않고 Accent override를 따른다. Focus override는 별도 ring만 바꾼다.
|
||||||
- Color 역할 정리: Hover / Soft surface = `--accent` 45% (Select·Command·Combobox·Menu Item highlighted, Calendar hover·range middle, Pagination hover, Table row hover·selected) / Persistent Selected Surface = `--accent` 100%(Toggle·Toggle Group) / Menubar Top Trigger Highlighted·Open 및 Navigation Menu Trigger·Open = `--accent` 100% + On Accent / Active Control fill = `--hm-sem-state-active`(위 적용 대상) / Focus = Focus chain / Strong Brand·Navigation·Badge default·Pagination current = Primary Green(`--hm-sem-action-primary`, `#123328`) / Error = Destructive.
|
- Color 역할 정리: Hover / Soft surface = `--accent` 45% (Select·Command·Combobox·Menu Item highlighted, Calendar hover·range middle, Pagination hover, Table row hover·selected) / Persistent Selected Surface = `--accent` 100%(Toggle·Toggle Group) / Menubar Top Trigger Highlighted·Open 및 Navigation Menu Trigger·Open = `--accent` 100% + On Accent / Active Control fill = `--hm-sem-state-active`(위 적용 대상) / Focus = Focus chain / Strong Brand·Navigation·Badge default·Pagination current = Primary Green(`--hm-sem-action-primary`, `#164434`) / Error = Destructive.
|
||||||
- Runtime 공개 계약(`packages/ui/src/theme/contract.ts`)에 Active/on Active를 포함했다. `src/theme/preview.ts`는 패키지 Runtime API의 호환 어댑터다.
|
- Runtime 공개 계약(`packages/ui/src/theme/contract.ts`)에 Active/on Active를 포함했다. `src/theme/preview.ts`는 패키지 Runtime API의 호환 어댑터다.
|
||||||
- **Floating Surface Semantic**(`default.css`): `--hm-sem-floating-background|border-width|border-color|radius|shadow`. Dropdown Menu·Context Menu·Popover·Hover Card·Menubar content·Navigation Menu panel의 `--hm-<component>-*`가 이를 참조한다. Toast(Sonner)는 notification surface라 이를 쓰지 않고 `--hm-toast-*`를 따로 둔다(Component Token은 유지하며 컴포넌트별 override 가능). Date Picker·Combobox는 Popover를 조합하므로 별도 mapping이 없다. Tooltip(어두운 면)·Dialog/Alert Dialog/Sheet/Drawer(modal)·Select·Card·Command는 대상이 아니다.
|
- **Floating Surface Semantic**(`default.css`): `--hm-sem-floating-background|border-width|border-color|radius|shadow`. Dropdown Menu·Context Menu·Popover·Hover Card·Menubar content·Navigation Menu panel의 `--hm-<component>-*`가 이를 참조한다. Toast(Sonner)는 notification surface라 이를 쓰지 않고 `--hm-toast-*`를 따로 둔다(Component Token은 유지하며 컴포넌트별 override 가능). Date Picker·Combobox는 Popover를 조합하므로 별도 mapping이 없다. Tooltip(어두운 면)·Dialog/Alert Dialog/Sheet/Drawer(modal)·Select·Card·Command는 대상이 아니다.
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Progress는 Indeterminate 표현을 여전히 갖지 않는다(`progress.md` §1
|
|||||||
| 텍스트 | 14/500/-0.02em, white 60%, 끝에 정적 "..." | 동일, 폰트는 `--hm-font-family-base` | MATCHED |
|
| 텍스트 | 14/500/-0.02em, white 60%, 끝에 정적 "..." | 동일, 폰트는 `--hm-font-family-base` | MATCHED |
|
||||||
| Inline | padding 36/24, radius 6, inset shadow 24px .45, gap 12, 가운데 정렬 | 동일 (실측 높이 149, ui_viz smoke 148은 폰트 line box 차이) | MATCHED |
|
| Inline | padding 36/24, radius 6, inset shadow 24px .45, gap 12, 가운데 정렬 | 동일 (실측 높이 149, ui_viz smoke 148은 폰트 line box 차이) | MATCHED |
|
||||||
| Fullscreen | fixed inset 0, assembly는 block·gap 0, 스피너는 묶음 leading edge(텍스트가 44px보다 넓으면 두 중심축이 어긋남), z-index 9999 | fixed inset 0, inline과 같은 column·gap 12로 묶음 전체를 viewport 중앙(스피너·텍스트 중심축 일치) | INTENTIONAL_CHANGE (2026-10-06 사용자 지시: 스피너가 왼쪽으로 치우쳐 보임) |
|
| Fullscreen | fixed inset 0, assembly는 block·gap 0, 스피너는 묶음 leading edge(텍스트가 44px보다 넓으면 두 중심축이 어긋남), z-index 9999 | fixed inset 0, inline과 같은 column·gap 12로 묶음 전체를 viewport 중앙(스피너·텍스트 중심축 일치) | INTENTIONAL_CHANGE (2026-10-06 사용자 지시: 스피너가 왼쪽으로 치우쳐 보임) |
|
||||||
| 면 색 | `#0a3322` (Dashboard-local) | `--hm-sem-action-primary` (Default `#123328`) | INTENTIONAL_CHANGE (HM Primitive 재사용, 새 색 만들지 않음) |
|
| 면 색 | `#0a3322` (Dashboard-local) | `--hm-sem-action-primary` (Default `#164434`) | INTENTIONAL_CHANGE (HM Primitive 재사용, 새 색 만들지 않음) |
|
||||||
| 폰트 | Inter → Noto Sans KR | HM 기본 폰트 | INTENTIONAL_CHANGE |
|
| 폰트 | Inter → Noto Sans KR | HM 기본 폰트 | INTENTIONAL_CHANGE |
|
||||||
| 막대 끝 | radius 1.5 → 렌더에서 pill | `9999px` | MATCHED |
|
| 막대 끝 | radius 1.5 → 렌더에서 pill | `9999px` | MATCHED |
|
||||||
| 숨김/표시 fade 0.3s | 있음 | 없음 | FOLLOW_UP (소비자 lifecycle) |
|
| 숨김/표시 fade 0.3s | 있음 | 없음 | FOLLOW_UP (소비자 lifecycle) |
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
| 역할 | 값 | 변수 |
|
| 역할 | 값 | 변수 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Green(주색) | `#123328` | `--hm-sem-palette-green-default` |
|
| Green(주색) | `#164434` | `--hm-sem-palette-green-default` |
|
||||||
| Green 진한 글자 | `#0A221D` | `--hm-sem-palette-green-dark-1` |
|
| Green 진한 글자 | `#11372D` | `--hm-sem-palette-green-dark-1` |
|
||||||
| Green Focus·Active | `#266651` | `--hm-sem-palette-green-light-2` |
|
| Green Focus·Active | `#266651` | `--hm-sem-palette-green-light-2` |
|
||||||
| Green Tint(강조 면) | `#BDDFD4` | `--hm-sem-palette-green-tint` |
|
| Green Tint(강조 면) | `#BDDFD4` | `--hm-sem-palette-green-tint` |
|
||||||
| Beige(콘텐츠 면) | `#F5F0E5` | `--hm-sem-palette-beige-light` |
|
| Beige(콘텐츠 면) | `#F5F0E5` | `--hm-sem-palette-beige-light` |
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
export const runtimeThemeDefaults = {
|
export const runtimeThemeDefaults = {
|
||||||
"--hm-sem-bg-content": "#f5f0e5",
|
"--hm-sem-bg-content": "#f5f0e5",
|
||||||
"--hm-sem-bg-surface": "#ffffff",
|
"--hm-sem-bg-surface": "#ffffff",
|
||||||
"--hm-sem-fg-primary": "#0a221d",
|
"--hm-sem-fg-primary": "#11372d",
|
||||||
"--hm-sem-fg-secondary": "#6e6759",
|
"--hm-sem-fg-secondary": "#6e6759",
|
||||||
"--hm-sem-fg-placeholder": "#999999",
|
"--hm-sem-fg-placeholder": "#999999",
|
||||||
"--hm-sem-action-primary": "#123328",
|
"--hm-sem-action-primary": "#164434",
|
||||||
"--hm-sem-action-cancel": "#6e6759",
|
"--hm-sem-action-cancel": "#6e6759",
|
||||||
"--hm-sem-action-destructive": "#d32f2f",
|
"--hm-sem-action-destructive": "#d32f2f",
|
||||||
"--hm-sem-status-error": "#b3261e",
|
"--hm-sem-status-error": "#b3261e",
|
||||||
"--hm-sem-stroke-default": "#9a9284",
|
"--hm-sem-stroke-default": "#9a9284",
|
||||||
"--hm-sem-stroke-input-default": "#dedbd2",
|
"--hm-sem-stroke-input-default": "#dedbd2",
|
||||||
"--hm-sem-stroke-input-focus": "#266651",
|
"--hm-sem-stroke-input-focus": "#266651",
|
||||||
"--hm-sem-brand-bg-stop-0": "#0a221d",
|
"--hm-sem-brand-bg-stop-0": "#11372d",
|
||||||
"--hm-sem-brand-bg-stop-12": "#071e18",
|
"--hm-sem-brand-bg-stop-12": "#0b2b23",
|
||||||
"--hm-sem-brand-bg-stop-42": "#123328",
|
"--hm-sem-brand-bg-stop-42": "#164434",
|
||||||
"--hm-sem-brand-bg-stop-78": "#266651",
|
"--hm-sem-brand-bg-stop-78": "#266651",
|
||||||
"--hm-sem-brand-bg-stop-100": "#205241",
|
"--hm-sem-brand-bg-stop-100": "#205241",
|
||||||
"--hm-sem-brown-bg-stop-0": "#352d1d",
|
"--hm-sem-brown-bg-stop-0": "#403726",
|
||||||
"--hm-sem-brown-bg-stop-12": "#2f281c",
|
"--hm-sem-brown-bg-stop-12": "#2f281c",
|
||||||
"--hm-sem-brown-bg-stop-42": "#3e3523",
|
"--hm-sem-brown-bg-stop-42": "#4e422d",
|
||||||
"--hm-sem-brown-bg-stop-78": "#897445",
|
"--hm-sem-brown-bg-stop-78": "#897445",
|
||||||
"--hm-sem-brown-bg-stop-100": "#604f32",
|
"--hm-sem-brown-bg-stop-100": "#604f32",
|
||||||
"--hm-sem-orange-bg-stop-0": "#bc4c00",
|
"--hm-sem-orange-bg-stop-0": "#bc4c00",
|
||||||
@@ -32,7 +32,7 @@ export const runtimeThemeDefaults = {
|
|||||||
"--hm-sem-action-surface-stop-30": "#165c47",
|
"--hm-sem-action-surface-stop-30": "#165c47",
|
||||||
"--hm-sem-action-surface-stop-65": "#114838",
|
"--hm-sem-action-surface-stop-65": "#114838",
|
||||||
"--hm-sem-action-surface-stop-100": "#08241c",
|
"--hm-sem-action-surface-stop-100": "#08241c",
|
||||||
"--hm-sem-action-border-stop-0": "#0a221d",
|
"--hm-sem-action-border-stop-0": "#11372d",
|
||||||
"--hm-sem-action-border-stop-50": "#34daaa",
|
"--hm-sem-action-border-stop-50": "#34daaa",
|
||||||
"--hm-sem-action-border-stop-100": "#08241c",
|
"--hm-sem-action-border-stop-100": "#08241c",
|
||||||
"--hm-radius-input": "4px",
|
"--hm-radius-input": "4px",
|
||||||
@@ -47,13 +47,13 @@ export const runtimeThemeDefaults = {
|
|||||||
"--hm-sem-palette-green-light-3": "#249473",
|
"--hm-sem-palette-green-light-3": "#249473",
|
||||||
"--hm-sem-palette-green-light-2": "#266651",
|
"--hm-sem-palette-green-light-2": "#266651",
|
||||||
"--hm-sem-palette-green-light-1": "#205241",
|
"--hm-sem-palette-green-light-1": "#205241",
|
||||||
"--hm-sem-palette-green-default": "#123328",
|
"--hm-sem-palette-green-default": "#164434",
|
||||||
"--hm-sem-palette-green-dark-1": "#0a221d",
|
"--hm-sem-palette-green-dark-1": "#11372d",
|
||||||
"--hm-sem-palette-green-dark-2": "#071e18",
|
"--hm-sem-palette-green-dark-2": "#0b2b23",
|
||||||
"--hm-sem-palette-brown-light-2": "#897445",
|
"--hm-sem-palette-brown-light-2": "#897445",
|
||||||
"--hm-sem-palette-brown-light-1": "#604f32",
|
"--hm-sem-palette-brown-light-1": "#604f32",
|
||||||
"--hm-sem-palette-brown-default": "#3e3523",
|
"--hm-sem-palette-brown-default": "#4e422d",
|
||||||
"--hm-sem-palette-brown-dark-1": "#352d1d",
|
"--hm-sem-palette-brown-dark-1": "#403726",
|
||||||
"--hm-sem-palette-brown-light-4": "#ecd4a2",
|
"--hm-sem-palette-brown-light-4": "#ecd4a2",
|
||||||
"--hm-sem-palette-orange-light-4": "#feba88",
|
"--hm-sem-palette-orange-light-4": "#feba88",
|
||||||
"--hm-sem-palette-orange-default": "#cc5200",
|
"--hm-sem-palette-orange-default": "#cc5200",
|
||||||
@@ -85,10 +85,10 @@ export const runtimeThemeDefaults = {
|
|||||||
"--hm-sem-palette-gray": "#767676",
|
"--hm-sem-palette-gray": "#767676",
|
||||||
"--hm-sem-palette-gray-dark": "#5a5a5a",
|
"--hm-sem-palette-gray-dark": "#5a5a5a",
|
||||||
"--hm-sem-palette-indicator": "#2d2d2d",
|
"--hm-sem-palette-indicator": "#2d2d2d",
|
||||||
"--hm-sem-palette-green-dark-3": "#020a07",
|
"--hm-sem-palette-green-dark-3": "#030e0a",
|
||||||
"--hm-sem-palette-brown-light-3": "#c1a770",
|
"--hm-sem-palette-brown-light-3": "#c1a770",
|
||||||
"--hm-sem-palette-brown-dark-2": "#2f281c",
|
"--hm-sem-palette-brown-dark-2": "#2f281c",
|
||||||
"--hm-sem-palette-brown-dark-3": "#1d1810",
|
"--hm-sem-palette-brown-dark-3": "#0d0b07",
|
||||||
"--hm-sem-palette-orange-light-3": "#ec8f53",
|
"--hm-sem-palette-orange-light-3": "#ec8f53",
|
||||||
"--hm-sem-palette-orange-light-2": "#f87725",
|
"--hm-sem-palette-orange-light-2": "#f87725",
|
||||||
"--hm-sem-palette-orange-light-1": "#eb5f00",
|
"--hm-sem-palette-orange-light-1": "#eb5f00",
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
--hm-green-light-3: #249473;
|
--hm-green-light-3: #249473;
|
||||||
--hm-green-light-2: #266651;
|
--hm-green-light-2: #266651;
|
||||||
--hm-green-light-1: #205241;
|
--hm-green-light-1: #205241;
|
||||||
--hm-green-default: #123328;
|
--hm-green-default: #164434;
|
||||||
--hm-green-dark-1: #0a221d;
|
--hm-green-dark-1: #11372d;
|
||||||
--hm-green-dark-2: #071e18;
|
--hm-green-dark-2: #0b2b23;
|
||||||
|
|
||||||
--hm-brown-light-2: #897445;
|
--hm-brown-light-2: #897445;
|
||||||
--hm-brown-light-1: #604f32;
|
--hm-brown-light-1: #604f32;
|
||||||
--hm-brown-default: #3e3523;
|
--hm-brown-default: #4e422d;
|
||||||
--hm-brown-dark-1: #352d1d;
|
--hm-brown-dark-1: #403726;
|
||||||
|
|
||||||
--hm-brown-light-4: #ecd4a2;
|
--hm-brown-light-4: #ecd4a2;
|
||||||
|
|
||||||
@@ -47,10 +47,10 @@
|
|||||||
|
|
||||||
/* Confirmed brand palette additions (ui_viz Color 4.1; Orange Dark 1/2 confirmed by user 2026-10-11). */
|
/* Confirmed brand palette additions (ui_viz Color 4.1; Orange Dark 1/2 confirmed by user 2026-10-11). */
|
||||||
:root {
|
:root {
|
||||||
--hm-green-dark-3: #020a07;
|
--hm-green-dark-3: #030e0a;
|
||||||
--hm-brown-light-3: #c1a770;
|
--hm-brown-light-3: #c1a770;
|
||||||
--hm-brown-dark-2: #2f281c;
|
--hm-brown-dark-2: #2f281c;
|
||||||
--hm-brown-dark-3: #1d1810;
|
--hm-brown-dark-3: #0d0b07;
|
||||||
--hm-orange-light-3: #ec8f53;
|
--hm-orange-light-3: #ec8f53;
|
||||||
--hm-orange-light-2: #f87725;
|
--hm-orange-light-2: #f87725;
|
||||||
--hm-orange-light-1: #eb5f00;
|
--hm-orange-light-1: #eb5f00;
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ try {
|
|||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.goto(`${base}/storybook/iframe.html?id=theme-designer-playground--editor&viewMode=story`, { waitUntil: 'networkidle' });
|
await page.goto(`${base}/storybook/iframe.html?id=theme-designer-playground--editor&viewMode=story`, { waitUntil: 'networkidle' });
|
||||||
await page.getByRole('button', { name: 'Load Service Example JSON', exact: true }).click();
|
await page.getByRole('button', { name: 'Load Service Example JSON', exact: true }).click();
|
||||||
await page.waitForFunction(() => document.querySelector('[aria-label="Primary / Brand HEX"]')?.value !== '#123328');
|
await page.waitForFunction(() => document.querySelector('[aria-label="Primary / Brand HEX"]')?.value !== '#164434');
|
||||||
const changed = await page.getByRole('textbox', { name: 'Primary / Brand HEX', exact: true }).inputValue();
|
const changed = await page.getByRole('textbox', { name: 'Primary / Brand HEX', exact: true }).inputValue();
|
||||||
await page.getByRole('button', { name: 'Load Default JSON', exact: true }).click();
|
await page.getByRole('button', { name: 'Load Default JSON', exact: true }).click();
|
||||||
await page.waitForFunction(() => document.querySelector('[aria-label="Primary / Brand HEX"]')?.value === '#123328');
|
await page.waitForFunction(() => document.querySelector('[aria-label="Primary / Brand HEX"]')?.value === '#164434');
|
||||||
await page.close(); return { servicePrimary: changed, defaultPrimary: '#123328' };
|
await page.close(); return { servicePrimary: changed, defaultPrimary: '#164434' };
|
||||||
});
|
});
|
||||||
await task('RUNTIME_THEME_KEYBOARD', async () => {
|
await task('RUNTIME_THEME_KEYBOARD', async () => {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|||||||
@@ -61,3 +61,29 @@
|
|||||||
font-weight: var(--hm-input-font-weight);
|
font-weight: var(--hm-input-font-weight);
|
||||||
}
|
}
|
||||||
.sb-page .sb-page-section-body { min-width: 0; }
|
.sb-page .sb-page-section-body { min-width: 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. */
|
||||||
|
.sb-page.sb-page-notice {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: calc(24px - var(--sb-page-gutter));
|
||||||
|
}
|
||||||
|
.sb-page .sb-notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--hm-label-font-size);
|
||||||
|
line-height: var(--hm-label-line-height);
|
||||||
|
font-weight: var(--hm-input-font-weight);
|
||||||
|
color: var(--hm-sem-fg-primary);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.sb-page .sb-notice-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
/* centers the icon on the first text line even when the line height differs from 20px */
|
||||||
|
margin-block: calc((var(--hm-label-line-height) - 20px) / 2);
|
||||||
|
}
|
||||||
|
.sb-page .sb-notice-text { min-width: 0; }
|
||||||
@@ -25,6 +25,26 @@ export function StoryPageHeader({ kicker, title, description }: { kicker?: strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notice row (icon + text) shown above a Story page title. Reusable by any Story group through a decorator
|
||||||
|
// (e.g. src/sw/sw-story-notice.tsx); nothing renders it unless a group opts in. It owns a page frame whose Inner is the
|
||||||
|
// same as StoryPage's, and cancels its own bottom gutter so the page title below stays where it was.
|
||||||
|
// Icon: Group 1171290812.svg (circle + "!"), colors via currentColor.
|
||||||
|
export function StoryNotice({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="sb-page sb-page-notice" data-story-notice="">
|
||||||
|
<div className="sb-page-inner">
|
||||||
|
<p className="sb-notice">
|
||||||
|
<svg className="sb-notice-icon" width="20" height="20" viewBox="0 0 29 29" fill="none" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M15.7415 6L15.5383 17.3984H13.3743L13.1711 6H15.7415ZM14.4602 22.1484C14.0279 22.1484 13.6581 21.9974 13.3508 21.6953C13.0435 21.388 12.8925 21.0182 12.8977 20.5859C12.8925 20.1589 13.0435 19.7943 13.3508 19.4922C13.6581 19.1849 14.0279 19.0312 14.4602 19.0312C14.8821 19.0312 15.2467 19.1849 15.554 19.4922C15.8612 19.7943 16.0175 20.1589 16.0227 20.5859C16.0175 20.8724 15.942 21.1354 15.7961 21.375C15.6555 21.6094 15.468 21.7969 15.2336 21.9375C14.9993 22.0781 14.7415 22.1484 14.4602 22.1484Z" fill="currentColor" />
|
||||||
|
<circle cx="14.5" cy="14.5" r="13.5" stroke="currentColor" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
<span className="sb-notice-text">{children}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Headings and specimens share StoryPage's single Inner. Wide specimens own their local scroll frame.
|
// Headings and specimens share StoryPage's single Inner. Wide specimens own their local scroll frame.
|
||||||
export function StorySection({
|
export function StorySection({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const palettes = [
|
|||||||
] as const
|
] as const
|
||||||
type ColorStage = typeof stages[number]
|
type ColorStage = typeof stages[number]
|
||||||
type PalettePrefix = typeof palettes[number]["prefix"]
|
type PalettePrefix = typeof palettes[number]["prefix"]
|
||||||
const stageLabel = (stage: ColorStage) => stage === "default" ? "Default · 기본색" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase())
|
const stageLabel = (stage: ColorStage) => stage === "default" ? "Default · 기본색" : stage === "light-4" ? "Light 4 · 보더 하이라이트용" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase())
|
||||||
const paletteToken = (prefix: PalettePrefix, stage: ColorStage) => `color.palette.${prefix}-${stage}` as RuntimeThemeToken
|
const paletteToken = (prefix: PalettePrefix, stage: ColorStage) => `color.palette.${prefix}-${stage}` as RuntimeThemeToken
|
||||||
const colorProperties = palettes.flatMap(palette => stages.map(stage => runtimeThemeContract[paletteToken(palette.prefix, stage)].property))
|
const colorProperties = palettes.flatMap(palette => stages.map(stage => runtimeThemeContract[paletteToken(palette.prefix, stage)].property))
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
@@ -27,6 +27,9 @@
|
|||||||
.hm-brand-color-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; }
|
.hm-brand-color-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; }
|
||||||
.hm-brand-color-chip { flex: 1 0 110px; min-width: 0; padding: 8px; border: 1px solid var(--hm-sem-stroke-default); }
|
.hm-brand-color-chip { flex: 1 0 110px; min-width: 0; padding: 8px; border: 1px solid var(--hm-sem-stroke-default); }
|
||||||
.hm-brand-color-chip[data-base] { border: 2px solid var(--hm-sem-fg-primary); font-weight: var(--hm-font-weight-bold); }
|
.hm-brand-color-chip[data-base] { border: 2px solid var(--hm-sem-fg-primary); font-weight: var(--hm-font-weight-bold); }
|
||||||
|
/* Hanmac Color only (data-color-stage): wider gap (8px + 16px extra = 24px) between Light 4 and Light 3 with a divider centered in it. */
|
||||||
|
.hm-brand-color-chip[data-color-stage="light-3"] { --hm-color-divider-extra: 16px; position: relative; margin-left: var(--hm-color-divider-extra); }
|
||||||
|
.hm-brand-color-chip[data-color-stage="light-3"]::before { content: ""; position: absolute; top: 0; bottom: 0; left: calc(-1px - 0.5px - (8px + var(--hm-color-divider-extra)) / 2); width: 1px; background: var(--hm-sem-stroke-default); pointer-events: none; }
|
||||||
.hm-brand-color-chip h3, .hm-brand-color-chip p { margin: 8px 0 0; font-size: 13px; }
|
.hm-brand-color-chip h3, .hm-brand-color-chip p { margin: 8px 0 0; font-size: 13px; }
|
||||||
|
|
||||||
/* Color reference additions only; preserve the existing palette row and Default emphasis. */
|
/* Color reference additions only; preserve the existing palette row and Default emphasis. */
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) {
|
|||||||
|
|
||||||
function TypographyReferencePage() {
|
function TypographyReferencePage() {
|
||||||
const values = useResolvedProperties(typographyProperties)
|
const values = useResolvedProperties(typographyProperties)
|
||||||
return <ReferencePage title="Typography">
|
return <ReferencePage title="폰트">
|
||||||
<ReferenceSection title="Font Family">
|
<ReferenceSection title="Font Family">
|
||||||
<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>
|
<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>
|
||||||
@@ -59,6 +59,7 @@ function TypographyReferencePage() {
|
|||||||
</ReferenceSection>
|
</ReferenceSection>
|
||||||
</ReferencePage>
|
</ReferencePage>
|
||||||
}
|
}
|
||||||
const meta = { title: "Foundation/Typography", parameters: { layout: "fullscreen" } } satisfies Meta
|
// The sidebar name is "폰트"; `id` keeps the existing Story ID (foundation-typography--all) stable.
|
||||||
|
const meta = { title: "Foundation/폰트", id: "foundation-typography", parameters: { layout: "fullscreen" } } satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
export const All: StoryObj<typeof meta> = { name: "Typography", render: () => <TypographyReferencePage /> }
|
export const All: StoryObj<typeof meta> = { name: "폰트", render: () => <TypographyReferencePage /> }
|
||||||
@@ -641,15 +641,6 @@ export const All: Story = {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
<StorySection title="Variants">
|
|
||||||
<StateBoard
|
|
||||||
min={560}
|
|
||||||
states={[
|
|
||||||
{ id: "with-logo", label: "With Logo", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} logo={testLogo} dividers={false} /> },
|
|
||||||
{ id: "long", label: "Long Labels", children: <ResponsiveSpecimen nodes={simpleNav(true)} initial="tasks/list" initialOpen={{ tasks: true }} longLabels dividers={false} /> },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</StorySection>
|
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-pag
|
|||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "Patterns/Layout/Page Header",
|
title: "Patterns/Layout/Page Header",
|
||||||
|
tags: ["verification"], // 임시 숨김: 사이드바에서만 제외. 다시 표시하려면 이 줄 삭제
|
||||||
component: PageHeader,
|
component: PageHeader,
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
} satisfies Meta<typeof PageHeader>
|
} satisfies Meta<typeof PageHeader>
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
.sw-footer-story-screen { position: relative; display: flex; flex-direction: column; background: #060606; }
|
.sw-footer-story-screen { position: relative; display: flex; flex-direction: column; background: #060606; }
|
||||||
.sw-footer-story-canvas { position: relative; background: var(--sw-story-canvas); }
|
.sw-footer-story-canvas { position: relative; background: var(--sw-story-canvas); }
|
||||||
/* 2026-10-22: the workspace fits the viewport under the page title (720px at most), so the page itself does not
|
/* 2026-10-22: the workspace fits the viewport under the page title (720px at most), so the page itself does not
|
||||||
scroll where there is room; a short window keeps 480px and the page scrolls instead of clipping. */
|
scroll where there is room; a short window keeps 480px and the page scrolls instead of clipping.
|
||||||
.sw-footer-story-workspace { display: flex; flex-direction: column; height: clamp(480px, calc(100dvh - 210px), 720px); background: #060606; }
|
2026-11-09: the 49px SWHeader above the drawing area was removed, so every bound is 49px lower (drawing area and footer unchanged). */
|
||||||
|
.sw-footer-story-workspace { display: flex; flex-direction: column; height: clamp(431px, calc(100dvh - 259px), 671px); background: #060606; }
|
||||||
.sw-footer-story-canvas--fill { flex: 1 1 auto; min-height: 0; }
|
.sw-footer-story-canvas--fill { flex: 1 1 auto; min-height: 0; }
|
||||||
.sw-footer-story-nodes { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; }
|
.sw-footer-story-nodes { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; }
|
||||||
.sw-footer-story-node { margin: 0; padding: 12px; border: 1px solid #dedbd2; border-radius: 6px; background: #fff; }
|
.sw-footer-story-node { margin: 0; padding: 12px; border: 1px solid #dedbd2; border-radius: 6px; background: #fff; }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, type ReactNode } from "react"
|
import { useState, type ReactNode } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWFooter, SWFooterAddSlot, SWFooterAuxButton, SWFooterAuxGroup, SWFooterCommandLine, SWFooterGuide, SWFooterLayoutTabs, SWFooterMenu,
|
SWFooter, SWFooterAddSlot, SWFooterAuxButton, SWFooterAuxGroup, SWFooterCommandLine, SWFooterGuide, SWFooterLayoutTabs, SWFooterMenu,
|
||||||
SWFooterToolButton, SWHeader, SWTheme, SW_FOOTER_MODEL, swFooterToolIcons,
|
SWFooterToolButton, SWTheme, SW_FOOTER_MODEL, swFooterToolIcons,
|
||||||
type SWFooterAuxItem, type SWFooterCategory, type SWFooterLayout, type SWFooterTool,
|
type SWFooterAuxItem, type SWFooterCategory, type SWFooterLayout, type SWFooterTool,
|
||||||
} from "@hm-design/sw"
|
} from "@hm-design/sw"
|
||||||
import "@hm-design/sw/styles.css"
|
import "@hm-design/sw/styles.css"
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
} from "./sw-footer-fixture"
|
} from "./sw-footer-fixture"
|
||||||
import "./sw-stories.css"
|
import "./sw-stories.css"
|
||||||
import "./sw-footer-stories.css"
|
import "./sw-footer-stories.css"
|
||||||
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
|
|
||||||
|
|
||||||
// SW/Footer (2026-10-20): Figma 이지빔-SW 26:10097 bottom bar and its 34 component sets as @hm-design/sw components.
|
// SW/Footer (2026-10-20): Figma 이지빔-SW 26:10097 bottom bar and its 34 component sets as @hm-design/sw components.
|
||||||
// Stories use the public API only, like a consuming program. No CAD engine: commands are logged.
|
// Stories use the public API only, like a consuming program. No CAD engine: commands are logged.
|
||||||
@@ -22,8 +22,11 @@ const KICKER = "HM DESIGN SYSTEM / SW"
|
|||||||
const ref = (id: string) => new URL(`./assets/figma-reference/footer/${id}.png`, import.meta.url).href
|
const ref = (id: string) => new URL(`./assets/figma-reference/footer/${id}.png`, import.meta.url).href
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "SW/Footer",
|
title: "SW/하단 명령어바",
|
||||||
|
// Fixed so the Story IDs (sw-footer--*) and direct URLs stay unchanged when the sidebar title changes.
|
||||||
|
id: "sw-footer",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
@@ -51,7 +54,7 @@ function FigmaFooter() {
|
|||||||
|
|
||||||
const LAYOUTS_2: SWFooterLayout[] = [{ id: "layout2", label: "Layout 02" }, { id: "layout1", label: "Layout 01" }]
|
const LAYOUTS_2: SWFooterLayout[] = [{ id: "layout2", label: "Layout 02" }, { id: "layout1", label: "Layout 01" }]
|
||||||
|
|
||||||
/** The reference screen (2026-10-20): SWHeader, drawing area, footer with the command line inside the bar. */
|
/** The reference screen (2026-10-20): drawing area, footer with the command line inside the bar. 2026-11-09: no SWHeader above the drawing area. */
|
||||||
function Workspace({ width = "100%" }: { width?: number | string }) {
|
function Workspace({ width = "100%" }: { width?: number | string }) {
|
||||||
const demo = useFooterDemo({ aux: REFERENCE_AUX, layouts: LAYOUTS_2, history: REFERENCE_HISTORY })
|
const demo = useFooterDemo({ aux: REFERENCE_AUX, layouts: LAYOUTS_2, history: REFERENCE_HISTORY })
|
||||||
const [commandOpen, setCommandOpen] = useState(false)
|
const [commandOpen, setCommandOpen] = useState(false)
|
||||||
@@ -60,7 +63,6 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
|
|||||||
<Log lines={demo.log} />
|
<Log lines={demo.log} />
|
||||||
{/* 2026-10-22: no inner scroller; `width` is a maximum and the header / footer fit the available width. */}
|
{/* 2026-10-22: no inner scroller; `width` is a maximum and the header / footer fit the available width. */}
|
||||||
<div className="sw-footer-story-workspace" style={{ width: "100%", maxWidth: width }}>
|
<div className="sw-footer-story-workspace" style={{ width: "100%", maxWidth: width }}>
|
||||||
<SWHeader documentTitle="Drawing1.grm" logoSrc={placeholderLogo} logoAlt="예제 SW" />
|
|
||||||
<div className="sw-footer-story-canvas sw-footer-story-canvas--fill" aria-label="도면 영역 (예시)" />
|
<div className="sw-footer-story-canvas sw-footer-story-canvas--fill" aria-label="도면 영역 (예시)" />
|
||||||
<SWFooter categories={REFERENCE_CATEGORIES} categoryId={demo.categoryId} onCategoryChange={demo.onCategoryChange}
|
<SWFooter categories={REFERENCE_CATEGORIES} categoryId={demo.categoryId} onCategoryChange={demo.onCategoryChange}
|
||||||
status="7.4177, -0.2807, 0.0000"
|
status="7.4177, -0.2807, 0.0000"
|
||||||
@@ -76,10 +78,10 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FullFooter: Story = {
|
export const FullFooter: Story = {
|
||||||
name: "SW Footer",
|
name: "SW 하단 명령어바",
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="SW Footer"description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
<StoryPageHeader title="SW 하단 명령어바"description="사용자 제공 실제 화면(2026-10-20) 구성: 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
||||||
{/* 2026-10-22 (user request): no section heading; the live composition follows the page title. Behaviour: command
|
{/* 2026-10-22 (user request): no section heading; the live composition follows the page title. Behaviour: command
|
||||||
click = select + history, category tabs, empty slot guide, aux toggles, ▲ = command window, Layout tabs, Enter runs. */}
|
click = select + history, category tabs, empty slot guide, aux toggles, ▲ = command window, Layout tabs, Enter runs. */}
|
||||||
<section className="sb-page-section" aria-label="실제 Footer 조합 (동작)">
|
<section className="sb-page-section" aria-label="실제 Footer 조합 (동작)">
|
||||||
@@ -379,10 +381,10 @@ function ReferenceFooter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Responsive: Story = {
|
export const Responsive: Story = {
|
||||||
name: "Responsive More",
|
name: "화면 너비",
|
||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Responsive More" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
<StoryPageHeader title="SW 하단 명령어바 — 화면 너비"description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||||
{/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */}
|
{/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */}
|
||||||
<div className="sw-story-responsive">
|
<div className="sw-story-responsive">
|
||||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import { ComposedDemo } from "./sw-header.stories"
|
import { ComposedDemo } from "./sw-header.stories"
|
||||||
import "./sw-composed-controls.css"
|
import "./sw-composed-controls.css"
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ const meta = {
|
|||||||
// Keep existing bookmarks; Dropdown is a Story directly under Header.
|
// Keep existing bookmarks; Dropdown is a Story directly under Header.
|
||||||
id: "sw-header",
|
id: "sw-header",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLayoutEffect, useRef, useState, type ReactNode } from "react"
|
import { useLayoutEffect, useRef, useState, type ReactNode } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
// Public API only, like a consuming project.
|
// Public API only, like a consuming project.
|
||||||
import {
|
import {
|
||||||
SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection,
|
SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection,
|
||||||
@@ -25,6 +26,7 @@ const KICKER = "HM DESIGN SYSTEM / SW"
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: "SW/Header",
|
title: "SW/Header",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, type ReactNode } from "react"
|
import { useState, type ReactNode } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWHeader, SWHeaderDocumentTitle, SWHeaderHome, SWHeaderLayerMenu, SWTheme, SWToolButton,
|
SWHeader, SWHeaderDocumentTitle, SWHeaderHome, SWHeaderLayerMenu, SWTheme, SWToolButton,
|
||||||
type SWHeaderDocument, type SWHeaderLayer, type SWHeaderProps,
|
type SWHeaderDocument, type SWHeaderLayer, type SWHeaderProps,
|
||||||
@@ -54,6 +55,7 @@ const meta = {
|
|||||||
title: "SW/Header",
|
title: "SW/Header",
|
||||||
id: "sw-header",
|
id: "sw-header",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|||||||
@@ -69,8 +69,10 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
|
|||||||
if (!slot) return
|
if (!slot) return
|
||||||
const measure = () => {
|
const measure = () => {
|
||||||
const bounds = slot.getBoundingClientRect()
|
const bounds = slot.getBoundingClientRect()
|
||||||
const width = Math.max(0, Math.min(bounds.width, bounds.height * 16 / 9))
|
// 2026-11-09: the Stage height is still the 16:9 height of the fitted area (unchanged); the width now fills the slot.
|
||||||
setSize(previous => previous.width === width ? previous : { width, height: width * 9 / 16 })
|
const height = Math.max(0, Math.min(bounds.width, bounds.height * 16 / 9)) * 9 / 16
|
||||||
|
const width = Math.max(0, bounds.width)
|
||||||
|
setSize(previous => previous.width === width && previous.height === height ? previous : { width, height })
|
||||||
}
|
}
|
||||||
measure()
|
measure()
|
||||||
const observer = new ResizeObserver(measure)
|
const observer = new ResizeObserver(measure)
|
||||||
@@ -150,7 +152,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
|
|||||||
</div></details>
|
</div></details>
|
||||||
</div>}
|
</div>}
|
||||||
<div ref={slotRef} className="sw-overview-slot" data-preview-slot="">
|
<div ref={slotRef} className="sw-overview-slot" data-preview-slot="">
|
||||||
<div className="sw-overview-stage" data-preview-stage="" role="region" tabIndex={0} aria-label="SW Header 16:9 미리보기, 작은 화면에서는 내부 스크롤"
|
<div className="sw-overview-stage" data-preview-stage="" role="region" tabIndex={0} aria-label="SW Header 미리보기, 작은 화면에서는 내부 스크롤"
|
||||||
style={{ ...size, "--sw-preview-stage-height": `${size.height}px`, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}>
|
style={{ ...size, "--sw-preview-stage-height": `${size.height}px`, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}>
|
||||||
<div className="sw-overview-world">
|
<div className="sw-overview-world">
|
||||||
<SWHeader ref={headerRef} {...initial}
|
<SWHeader ref={headerRef} {...initial}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useId, useRef, useState, type ReactNode } from "react"
|
import { useEffect, useId, useRef, useState, type ReactNode } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
|
SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
|
||||||
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
|
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
|
||||||
@@ -68,6 +69,7 @@ const meta = {
|
|||||||
excludeStories: ["ComposedDemo", "PropertyPanelComparePage"],
|
excludeStories: ["ComposedDemo", "PropertyPanelComparePage"],
|
||||||
component: SWHeader,
|
component: SWHeader,
|
||||||
parameters: { layout: "fullscreen" },
|
parameters: { layout: "fullscreen" },
|
||||||
|
decorators: [withSWNotice],
|
||||||
args: { ...figmaArgs, ...callbacks },
|
args: { ...figmaArgs, ...callbacks },
|
||||||
argTypes: {
|
argTypes: {
|
||||||
activeMenu: { control: "select", options: [null, "layer", "color", "lineStyle", "textStyle", "dimensionStyle"] },
|
activeMenu: { control: "select", options: [null, "layer", "color", "lineStyle", "textStyle", "dimensionStyle"] },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useId, useState } from "react"
|
import { useId, useState } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWHeaderInput, SWHeaderPropertyPanel, SWPopup, SWPopupPropertyRow, SWPopupRow, SWPopupSection, SWSelect, SWSlider, SWTheme,
|
SWHeaderInput, SWHeaderPropertyPanel, SWPopup, SWPopupPropertyRow, SWPopupRow, SWPopupSection, SWSelect, SWSlider, SWTheme,
|
||||||
type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty,
|
type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty,
|
||||||
@@ -30,6 +31,7 @@ const layerProperty = (value: string): SWHeaderProperty => ({ id: "layer", name:
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: "SW/Popup",
|
title: "SW/Popup",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, type ReactNode } from "react"
|
import { useState, type ReactNode } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWProcessBar, SWProcessHeader, SWProcessSplitButton, SWTheme, SWToolButton, type SWProcessGroup, type SWProcessHeaderProps, type SWProcessSplitType,
|
SWProcessBar, SWProcessHeader, SWProcessSplitButton, SWTheme, SWToolButton, type SWProcessGroup, type SWProcessHeaderProps, type SWProcessSplitType,
|
||||||
} from "@hm-design/sw"
|
} from "@hm-design/sw"
|
||||||
@@ -31,6 +32,7 @@ const LOGO = { logoSrc: placeholderLogo, logoAlt: "임시 로고" } satisfies Pa
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: "SW/Step Header",
|
title: "SW/Step Header",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import { PropertyPanelComparePage } from "./sw-header.stories"
|
import { PropertyPanelComparePage } from "./sw-header.stories"
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
@@ -8,6 +9,7 @@ const meta = {
|
|||||||
// Custom Content and Multiple Popups.
|
// Custom Content and Multiple Popups.
|
||||||
id: "sw-header",
|
id: "sw-header",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
|
|
||||||
|
|||||||
@@ -68,20 +68,20 @@
|
|||||||
.sw-story-detail-row > div { display: flex; align-items: center; gap: 8px; }
|
.sw-story-detail-row > div { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|
||||||
/* Overview host layout only. Native controls below are Storybook data controls, outside the product. */
|
/* Overview host layout only. Native controls below are Storybook data controls, outside the product. */
|
||||||
.sw-story-overview.sb-page { height: 100dvh; min-height: 0; padding-block: 16px; }
|
/* 2026-11-09: the page frame (gutter, title block, Inner) is the common SW Story page; only the Stage keeps a viewport-based height.
|
||||||
.sw-story-overview > .sb-page-inner { height: 100%; display: flex; flex-direction: column; gap: 12px; }
|
The Stage fits the slot (16:9 inside it), so the slot gets a definite height instead of the page being locked to 100dvh. */
|
||||||
.sw-story-overview .sb-page-header { flex: 0 0 auto; padding-bottom: 8px; }
|
.sw-story-overview > .sb-page-inner { display: flex; flex-direction: column; }
|
||||||
.sw-story-overview .sb-page-header .sb-page-kicker { margin-bottom: 4px; }
|
.sw-overview-toolbar { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; }
|
||||||
.sw-story-overview .sb-page-header .sb-page-title { margin-block: 0 4px; }
|
|
||||||
.sw-overview-toolbar { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 13px; }
|
|
||||||
.sw-overview-toolbar select, .sw-overview-data button { font: inherit; padding: 3px 8px; color: inherit; background: var(--hm-sem-bg-surface); border: 1px solid var(--hm-sem-stroke-input-default); border-radius: 4px; }
|
.sw-overview-toolbar select, .sw-overview-data button { font: inherit; padding: 3px 8px; color: inherit; background: var(--hm-sem-bg-surface); border: 1px solid var(--hm-sem-stroke-input-default); border-radius: 4px; }
|
||||||
.sw-overview-toolbar details { flex-basis: 100%; }
|
.sw-overview-toolbar details { flex-basis: 100%; }
|
||||||
.sw-overview-toolbar summary { width: max-content; cursor: pointer; }
|
.sw-overview-toolbar summary { width: max-content; cursor: pointer; }
|
||||||
.sw-overview-data { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding-block: 8px; }
|
.sw-overview-data { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding-block: 8px; }
|
||||||
.sw-overview-data label { display: inline-flex; align-items: center; gap: 4px; }
|
.sw-overview-data label { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
.sw-overview-slot { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; align-items: center; justify-content: center; }
|
.sw-overview-slot { flex: none; height: max(360px, calc(100dvh - 260px)); min-width: 0; min-height: 0; display: flex; align-items: center; justify-content: center; }
|
||||||
.sw-overview-stage { flex: 0 0 auto; overflow: auto; overscroll-behavior: contain; background: var(--sw-story-canvas); outline: 1px solid var(--hm-sem-stroke-input-default); }
|
.sw-overview-stage { flex: 0 0 auto; overflow: auto; overscroll-behavior: contain; background: var(--sw-story-canvas); outline: 1px solid var(--hm-sem-stroke-input-default); }
|
||||||
.sw-overview-world { min-width: 390px; min-height: 100%; position: relative; }
|
/* 2026-11-09: floor 390 -> 358, the measured minimum width of SWHeader (content width 358px; narrower overflows). 390 forced a 32px
|
||||||
|
Stage scroll at a 390px viewport (Inner 358px). Below 358px the Stage still scrolls instead of clipping. */
|
||||||
|
.sw-overview-world { min-width: 358px; min-height: 100%; position: relative; }
|
||||||
/* Placement-only Storybook exception: the panel sits in the Stage. 2026-10-26 (user request): no Stage scrollbar when
|
/* Placement-only Storybook exception: the panel sits in the Stage. 2026-10-26 (user request): no Stage scrollbar when
|
||||||
it opens — the panel is clamped to the Stage height (as the product clamps it to the viewport) and its body scrolls.
|
it opens — the panel is clamped to the Stage height (as the product clamps it to the viewport) and its body scrolls.
|
||||||
The Stage height comes from the Story (--sw-preview-stage-height): the panel's containing block is the Header.
|
The Stage height comes from the Story (--sw-preview-stage-height): the panel's containing block is the Header.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Decorator } from "@storybook/react-vite"
|
||||||
|
import { StoryNotice } from "@/components/story-page"
|
||||||
|
|
||||||
|
// Storybook presentation only. Common notice shown above the page title of every SW Story (SW/* groups only):
|
||||||
|
// each SW stories file opts in with `decorators: [withSWNotice]`, so other menus never render it.
|
||||||
|
// Hidden verification / internal Stories are skipped; `parameters.swNotice: false` opts a Story out
|
||||||
|
// (use it where the page already carries the same notice, so it is never shown twice).
|
||||||
|
export const SW_NOTICE_TEXT =
|
||||||
|
"SW UI는 공통 컴포넌트의 기능과 동작을 유지하면서, 제품별 특성에 따라 시각적 스타일을 다르게 적용할 수 있습니다."
|
||||||
|
|
||||||
|
const HIDDEN_TAGS = ["verification", "internal"]
|
||||||
|
|
||||||
|
export const withSWNotice: Decorator = (Story, context) => {
|
||||||
|
const hidden = (context.tags ?? []).some(tag => HIDDEN_TAGS.includes(tag))
|
||||||
|
if (hidden || context.parameters.swNotice === false) return <Story />
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StoryNotice>{SW_NOTICE_TEXT}</StoryNotice>
|
||||||
|
<Story />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, type CSSProperties, type ReactNode } from "react"
|
import { useState, type CSSProperties, type ReactNode } from "react"
|
||||||
import { createPortal } from "react-dom"
|
import { createPortal } from "react-dom"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWFooter, SWFooterCommandLine, SWHeader, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerSection,
|
SWFooter, SWFooterCommandLine, SWHeader, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerSection,
|
||||||
} from "@hm-design/sw"
|
} from "@hm-design/sw"
|
||||||
@@ -23,6 +24,7 @@ const NESTED_THEME = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-seri
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: "SW/Theme",
|
title: "SW/Theme",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRef, useState, type ReactNode } from "react"
|
import { useRef, useState, type ReactNode } from "react"
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||||
|
import { withSWNotice } from "./sw-story-notice"
|
||||||
import {
|
import {
|
||||||
SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
|
SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
|
||||||
type SWViewerHeaderGroup, type SWViewerMenuEntry,
|
type SWViewerHeaderGroup, type SWViewerMenuEntry,
|
||||||
@@ -20,6 +21,7 @@ const KICKER = "HM DESIGN SYSTEM / SW"
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: "SW/Viewer",
|
title: "SW/Viewer",
|
||||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||||
|
decorators: [withSWNotice],
|
||||||
} satisfies Meta
|
} satisfies Meta
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export const LiveVerification: Story = {
|
|||||||
}
|
}
|
||||||
fillJson('{"schemaVersion":1,"tokens":{"color.state.active":"#345678"}}'); click("Import JSON")
|
fillJson('{"schemaVersion":1,"tokens":{"color.state.active":"#345678"}}'); click("Import JSON")
|
||||||
await waitForThemeState(() => variable("--hm-sem-state-active") === "#345678", "Designer partial import")
|
await waitForThemeState(() => variable("--hm-sem-state-active") === "#345678", "Designer partial import")
|
||||||
assert(variable("--hm-sem-state-active") === "#345678" && variable("--hm-sem-action-primary") === "#123328" && variable("--hm-sem-stroke-input-focus") === "#266651", "Partial import replaces previous keys; unspecified roles use Default")
|
assert(variable("--hm-sem-state-active") === "#345678" && variable("--hm-sem-action-primary") === "#164434" && variable("--hm-sem-stroke-input-focus") === "#266651", "Partial import replaces previous keys; unspecified roles use Default")
|
||||||
for (const [index, [token, label]] of roles.entries()) {
|
for (const [index, [token, label]] of roles.entries()) {
|
||||||
const value = `#3456${(120 + index).toString(16)}`
|
const value = `#3456${(120 + index).toString(16)}`
|
||||||
setInput(`${label} HEX`, value)
|
setInput(`${label} HEX`, value)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async function verifyLoader(container: HTMLElement) {
|
|||||||
assert(checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && getComputedStyle(checked).backgroundColor === "rgb(0, 127, 115)", "HTTP JSON reaches rendered Checkbox")
|
assert(checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && getComputedStyle(checked).backgroundColor === "rgb(0, 127, 115)", "HTTP JSON reaches rendered Checkbox")
|
||||||
assert(JSON.stringify(geometry()) === initial, "External JSON preserves component geometry")
|
assert(JSON.stringify(geometry()) === initial, "External JSON preserves component geometry")
|
||||||
const resolved = resolveRuntimeTheme({ schemaVersion: 1, tokens: { "color.state.active": "#345678" } }, defaults)
|
const resolved = resolveRuntimeTheme({ schemaVersion: 1, tokens: { "color.state.active": "#345678" } }, defaults)
|
||||||
assert(resolved.tokens["color.action.primary"] === "#123328" && resolved.tokens["color.state.active"] === "#345678", "Default JSON + partial service override merge")
|
assert(resolved.tokens["color.action.primary"] === "#164434" && resolved.tokens["color.state.active"] === "#345678", "Default JSON + partial service override merge")
|
||||||
assert(await loadRuntimeTheme(root, "/themes/service-example.json", { defaults }), "Loader accepts separately fetched Default JSON")
|
assert(await loadRuntimeTheme(root, "/themes/service-example.json", { defaults }), "Loader accepts separately fetched Default JSON")
|
||||||
assert(variable("--hm-sem-action-primary") === "#6837a8", "Service override wins supplied Default")
|
assert(variable("--hm-sem-action-primary") === "#6837a8", "Service override wins supplied Default")
|
||||||
resetRuntimeTheme(root)
|
resetRuntimeTheme(root)
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ async function verifyRuntime(canvasElement: HTMLElement, keyboardFocus = false)
|
|||||||
applyRuntimeTheme(after, {schemaVersion:1,tokens:{"color.state.active":"#345678"}})
|
applyRuntimeTheme(after, {schemaVersion:1,tokens:{"color.state.active":"#345678"}})
|
||||||
assert(variable("--hm-checkbox-checked-background") === "#345678", "Scoped B overrides global A")
|
assert(variable("--hm-checkbox-checked-background") === "#345678", "Scoped B overrides global A")
|
||||||
assert(getComputedStyle(globalRoot).getPropertyValue("--hm-checkbox-checked-background").trim() === "#007f73", "Global A retains its own mapping")
|
assert(getComputedStyle(globalRoot).getPropertyValue("--hm-checkbox-checked-background").trim() === "#007f73", "Global A retains its own mapping")
|
||||||
assert(variable("--hm-badge-default-background") === "#123328", "Explicit isolated scope falls back to HM preset")
|
assert(variable("--hm-badge-default-background") === "#164434", "Explicit isolated scope falls back to HM preset")
|
||||||
} finally { resetRuntimeTheme(globalRoot); resetRuntimeTheme(after) }
|
} finally { resetRuntimeTheme(globalRoot); resetRuntimeTheme(after) }
|
||||||
after.style.setProperty("--hm-sem-state-active", "#112233", "important")
|
after.style.setProperty("--hm-sem-state-active", "#112233", "important")
|
||||||
applyRuntimeTheme(after, external); resetRuntimeTheme(after)
|
applyRuntimeTheme(after, external); resetRuntimeTheme(after)
|
||||||
|
|||||||
@@ -779,13 +779,13 @@
|
|||||||
"--hm-green-light-3": "#249473",
|
"--hm-green-light-3": "#249473",
|
||||||
"--hm-green-light-2": "#266651",
|
"--hm-green-light-2": "#266651",
|
||||||
"--hm-green-light-1": "#205241",
|
"--hm-green-light-1": "#205241",
|
||||||
"--hm-green-default": "#123328",
|
"--hm-green-default": "#164434",
|
||||||
"--hm-green-dark-1": "#0a221d",
|
"--hm-green-dark-1": "#11372d",
|
||||||
"--hm-green-dark-2": "#071e18",
|
"--hm-green-dark-2": "#0b2b23",
|
||||||
"--hm-brown-light-2": "#897445",
|
"--hm-brown-light-2": "#897445",
|
||||||
"--hm-brown-light-1": "#604f32",
|
"--hm-brown-light-1": "#604f32",
|
||||||
"--hm-brown-default": "#3e3523",
|
"--hm-brown-default": "#4e422d",
|
||||||
"--hm-brown-dark-1": "#352d1d",
|
"--hm-brown-dark-1": "#403726",
|
||||||
"--hm-brown-light-4": "#ecd4a2",
|
"--hm-brown-light-4": "#ecd4a2",
|
||||||
"--hm-orange-light-4": "#feba88",
|
"--hm-orange-light-4": "#feba88",
|
||||||
"--hm-orange-default": "#cc5200",
|
"--hm-orange-default": "#cc5200",
|
||||||
|
|||||||
+15
-15
@@ -3,24 +3,24 @@
|
|||||||
"tokens": {
|
"tokens": {
|
||||||
"color.bg.content": "#f5f0e5",
|
"color.bg.content": "#f5f0e5",
|
||||||
"color.bg.surface": "#ffffff",
|
"color.bg.surface": "#ffffff",
|
||||||
"color.fg.primary": "#0a221d",
|
"color.fg.primary": "#11372d",
|
||||||
"color.fg.secondary": "#6e6759",
|
"color.fg.secondary": "#6e6759",
|
||||||
"color.fg.placeholder": "#999999",
|
"color.fg.placeholder": "#999999",
|
||||||
"color.action.primary": "#123328",
|
"color.action.primary": "#164434",
|
||||||
"color.action.cancel": "#6e6759",
|
"color.action.cancel": "#6e6759",
|
||||||
"color.action.destructive": "#d32f2f",
|
"color.action.destructive": "#d32f2f",
|
||||||
"color.status.error": "#b3261e",
|
"color.status.error": "#b3261e",
|
||||||
"color.stroke.default": "#9a9284",
|
"color.stroke.default": "#9a9284",
|
||||||
"color.stroke.input.default": "#dedbd2",
|
"color.stroke.input.default": "#dedbd2",
|
||||||
"color.stroke.input.focus": "#266651",
|
"color.stroke.input.focus": "#266651",
|
||||||
"color.bg.brand.stop.0": "#0a221d",
|
"color.bg.brand.stop.0": "#11372d",
|
||||||
"color.bg.brand.stop.12": "#071e18",
|
"color.bg.brand.stop.12": "#0b2b23",
|
||||||
"color.bg.brand.stop.42": "#123328",
|
"color.bg.brand.stop.42": "#164434",
|
||||||
"color.bg.brand.stop.78": "#266651",
|
"color.bg.brand.stop.78": "#266651",
|
||||||
"color.bg.brand.stop.100": "#205241",
|
"color.bg.brand.stop.100": "#205241",
|
||||||
"color.bg.brown.stop.0": "#352d1d",
|
"color.bg.brown.stop.0": "#403726",
|
||||||
"color.bg.brown.stop.12": "#2f281c",
|
"color.bg.brown.stop.12": "#2f281c",
|
||||||
"color.bg.brown.stop.42": "#3e3523",
|
"color.bg.brown.stop.42": "#4e422d",
|
||||||
"color.bg.brown.stop.78": "#897445",
|
"color.bg.brown.stop.78": "#897445",
|
||||||
"color.bg.brown.stop.100": "#604f32",
|
"color.bg.brown.stop.100": "#604f32",
|
||||||
"color.bg.orange.stop.0": "#bc4c00",
|
"color.bg.orange.stop.0": "#bc4c00",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"surface.action.primary.stop.30": "#165c47",
|
"surface.action.primary.stop.30": "#165c47",
|
||||||
"surface.action.primary.stop.65": "#114838",
|
"surface.action.primary.stop.65": "#114838",
|
||||||
"surface.action.primary.stop.100": "#08241c",
|
"surface.action.primary.stop.100": "#08241c",
|
||||||
"surface.action.primary.border.stop.0": "#0a221d",
|
"surface.action.primary.border.stop.0": "#11372d",
|
||||||
"surface.action.primary.border.stop.50": "#34daaa",
|
"surface.action.primary.border.stop.50": "#34daaa",
|
||||||
"surface.action.primary.border.stop.100": "#08241c",
|
"surface.action.primary.border.stop.100": "#08241c",
|
||||||
"color.state.active": "#266651",
|
"color.state.active": "#266651",
|
||||||
@@ -45,13 +45,13 @@
|
|||||||
"color.palette.green-light-3": "#249473",
|
"color.palette.green-light-3": "#249473",
|
||||||
"color.palette.green-light-2": "#266651",
|
"color.palette.green-light-2": "#266651",
|
||||||
"color.palette.green-light-1": "#205241",
|
"color.palette.green-light-1": "#205241",
|
||||||
"color.palette.green-default": "#123328",
|
"color.palette.green-default": "#164434",
|
||||||
"color.palette.green-dark-1": "#0a221d",
|
"color.palette.green-dark-1": "#11372d",
|
||||||
"color.palette.green-dark-2": "#071e18",
|
"color.palette.green-dark-2": "#0b2b23",
|
||||||
"color.palette.brown-light-2": "#897445",
|
"color.palette.brown-light-2": "#897445",
|
||||||
"color.palette.brown-light-1": "#604f32",
|
"color.palette.brown-light-1": "#604f32",
|
||||||
"color.palette.brown-default": "#3e3523",
|
"color.palette.brown-default": "#4e422d",
|
||||||
"color.palette.brown-dark-1": "#352d1d",
|
"color.palette.brown-dark-1": "#403726",
|
||||||
"color.palette.brown-light-4": "#ecd4a2",
|
"color.palette.brown-light-4": "#ecd4a2",
|
||||||
"color.palette.orange-light-4": "#feba88",
|
"color.palette.orange-light-4": "#feba88",
|
||||||
"color.palette.orange-default": "#cc5200",
|
"color.palette.orange-default": "#cc5200",
|
||||||
@@ -83,10 +83,10 @@
|
|||||||
"color.palette.gray": "#767676",
|
"color.palette.gray": "#767676",
|
||||||
"color.palette.gray-dark": "#5a5a5a",
|
"color.palette.gray-dark": "#5a5a5a",
|
||||||
"color.palette.indicator": "#2d2d2d",
|
"color.palette.indicator": "#2d2d2d",
|
||||||
"color.palette.green-dark-3": "#020a07",
|
"color.palette.green-dark-3": "#030e0a",
|
||||||
"color.palette.brown-light-3": "#c1a770",
|
"color.palette.brown-light-3": "#c1a770",
|
||||||
"color.palette.brown-dark-2": "#2f281c",
|
"color.palette.brown-dark-2": "#2f281c",
|
||||||
"color.palette.brown-dark-3": "#1d1810",
|
"color.palette.brown-dark-3": "#0d0b07",
|
||||||
"color.palette.orange-light-3": "#ec8f53",
|
"color.palette.orange-light-3": "#ec8f53",
|
||||||
"color.palette.orange-light-2": "#f87725",
|
"color.palette.orange-light-2": "#f87725",
|
||||||
"color.palette.orange-light-1": "#eb5f00",
|
"color.palette.orange-light-1": "#eb5f00",
|
||||||
|
|||||||
Reference in new issue
Block a user