Files
HMDesign/docs/sw/sw-dotum-font-qa/consumer-viewer/hmdesign-snapshot.patch
T
shkim4 6504668e85 docs(sw): Dotum default font, Footer design check and real viewer consumer QA
Report docs/sw/sw-dotum-font-2026-10-20.md and evidence docs/sw/sw-dotum-font-qa/: Noto -> Dotum layout diff of 40 SW
Stories x 4 widths, Header hysteresis trace, Footer design items (4 PASS), SW regression runs with baseline failure
classification, and the isolated HMDesignSwTest-viewer run (Chrome 34/34, Firefox 15/15; original project untouched).
Open items: synthesized-bold overflow in fixed Header cells and Footer POLYGON / WIPEOUT labels (design review),
viewer-regression not run, macOS / Linux fallback not verified.
2026-10-20 16:59:55 +09:00

397 lines
39 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
diff --git a/README.md b/README.md
index 06a943a..648c5fb 100644
--- a/README.md
+++ b/README.md
@@ -124,7 +124,7 @@ import { OrgChart } from "@hm-design/org-chart-react" // Core: @hm-design/org-ch
별도 React 앱은 [Tailwind v4 검색 범위 가이드](docs/guidelines/runtime-portability.md#21-tailwind-v4-소비자-검색-범위)를 확인합니다. `source("./")`는 CSS 위치에 따라 범위가 달라지며 HM `tailwind.css`의 명시 source를 유지해야 합니다. 공개 package Theme는 동봉된 `dist/fonts/`를 상대 URL로 참조합니다. 개발 저장소·포터블 ZIP·React 소비자·단일 HTML의 차이는 [폰트·Asset 경로](docs/guidelines/runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)를 따릅니다.
-SW는 Tailwind 없이 앱 진입점에서 `import "@hm-design/sw/styles.css"`를 한 번 추가합니다. 한 화면에서는 SW 일반 모드와 뷰어 모드 중 하나를 고르고, 글꼴 등 토큰은 가장 바깥 `<SWTheme style>`에서 바꿉니다([SW 사용 안내](packages/sw/ai/AI_USAGE.md)). SW 기본 글꼴은 Noto Sans KR이며 Figma의 Dotum을 기본값으로 쓸지는 디자인 승인 대기입니다. OrgChart는 Core CSS 두 개(`@hm-design/org-chart/engine.css`, `/style.css`)의 URL을 `stylesheetUrls`로 전달합니다(Shadow DOM, [OrgChart React README](packages/org-chart-react/README.md)).
+SW는 Tailwind 없이 앱 진입점에서 `import "@hm-design/sw/styles.css"`를 한 번 추가합니다. 한 화면에서는 SW 일반 모드와 뷰어 모드 중 하나를 고르고, 글꼴 등 토큰은 가장 바깥 `<SWTheme style>`에서 바꿉니다([SW 사용 안내](packages/sw/ai/AI_USAGE.md)). SW 기본 글꼴은 돋움(Dotum, 설치된 시스템 글꼴)이고 Web UI 기본 글꼴은 Noto Sans KR입니다. 서로의 글꼴 설정을 복사하지 않습니다. OrgChart는 Core CSS 두 개(`@hm-design/org-chart/engine.css`, `/style.css`)의 URL을 `stylesheetUrls`로 전달합니다(Shadow DOM, [OrgChart React README](packages/org-chart-react/README.md)).
화면 생성 Reference: [Navigation 선택 근거](docs/guidelines/navigation-single-mode.md#선택-근거), [GNB Desktop·Mobile 구성](docs/patterns/gnb.md#8-desktop--mobile)과 [내부 Sidebar](docs/patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08), [FilterBar 조회·초기화 소비 예제](docs/patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제). 일반 업무 목록의 모드를 임의 확정하거나 FilterBar에 조회 API가 있다고 가정하지 않습니다.
diff --git a/docs/ai/AI_GUIDE.md b/docs/ai/AI_GUIDE.md
index 8ac07f4..5fd7e42 100644
--- a/docs/ai/AI_GUIDE.md
+++ b/docs/ai/AI_GUIDE.md
@@ -138,7 +138,7 @@
혼합 규칙:
-1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다.
+1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다. 글꼴도 같습니다: SW 기본 글꼴은 돋움(Dotum, `--hm-sw-header-font-family`), Web 기본 글꼴은 Noto Sans KR(`--hm-font-family-base`), OrgChart는 자기 설정을 유지합니다. 한 패키지의 글꼴 값을 다른 영역에 복사하지 않습니다.
2. 다른 패키지 컴포넌트는 **문서에 허용된 지점에서만** 씁니다.
- SW 일반 모드 `SWPopup`의 `children` 안 Web 컨트롤(SW 카탈로그가 허용, 예: Popup 하단 `Button`)
- SW 뷰어 모드 `SWViewerPanel`의 `children` 안 Web 컨트롤 — `SWViewer*` 컨트롤로 안 될 때만(SW `AI_USAGE.md` §0.4)
diff --git a/packages/org-chart-react/ai/AI_GUIDE.md b/packages/org-chart-react/ai/AI_GUIDE.md
index 8ac07f4..5fd7e42 100644
--- a/packages/org-chart-react/ai/AI_GUIDE.md
+++ b/packages/org-chart-react/ai/AI_GUIDE.md
@@ -138,7 +138,7 @@
혼합 규칙:
-1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다.
+1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다. 글꼴도 같습니다: SW 기본 글꼴은 돋움(Dotum, `--hm-sw-header-font-family`), Web 기본 글꼴은 Noto Sans KR(`--hm-font-family-base`), OrgChart는 자기 설정을 유지합니다. 한 패키지의 글꼴 값을 다른 영역에 복사하지 않습니다.
2. 다른 패키지 컴포넌트는 **문서에 허용된 지점에서만** 씁니다.
- SW 일반 모드 `SWPopup`의 `children` 안 Web 컨트롤(SW 카탈로그가 허용, 예: Popup 하단 `Button`)
- SW 뷰어 모드 `SWViewerPanel`의 `children` 안 Web 컨트롤 — `SWViewer*` 컨트롤로 안 될 때만(SW `AI_USAGE.md` §0.4)
diff --git a/packages/org-chart/ai/AI_GUIDE.md b/packages/org-chart/ai/AI_GUIDE.md
index 8ac07f4..5fd7e42 100644
--- a/packages/org-chart/ai/AI_GUIDE.md
+++ b/packages/org-chart/ai/AI_GUIDE.md
@@ -138,7 +138,7 @@
혼합 규칙:
-1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다.
+1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다. 글꼴도 같습니다: SW 기본 글꼴은 돋움(Dotum, `--hm-sw-header-font-family`), Web 기본 글꼴은 Noto Sans KR(`--hm-font-family-base`), OrgChart는 자기 설정을 유지합니다. 한 패키지의 글꼴 값을 다른 영역에 복사하지 않습니다.
2. 다른 패키지 컴포넌트는 **문서에 허용된 지점에서만** 씁니다.
- SW 일반 모드 `SWPopup`의 `children` 안 Web 컨트롤(SW 카탈로그가 허용, 예: Popup 하단 `Button`)
- SW 뷰어 모드 `SWViewerPanel`의 `children` 안 Web 컨트롤 — `SWViewer*` 컨트롤로 안 될 때만(SW `AI_USAGE.md` §0.4)
diff --git a/packages/sw/ai/AI_GUIDE.md b/packages/sw/ai/AI_GUIDE.md
index 8ac07f4..5fd7e42 100644
--- a/packages/sw/ai/AI_GUIDE.md
+++ b/packages/sw/ai/AI_GUIDE.md
@@ -138,7 +138,7 @@
혼합 규칙:
-1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다.
+1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다. 글꼴도 같습니다: SW 기본 글꼴은 돋움(Dotum, `--hm-sw-header-font-family`), Web 기본 글꼴은 Noto Sans KR(`--hm-font-family-base`), OrgChart는 자기 설정을 유지합니다. 한 패키지의 글꼴 값을 다른 영역에 복사하지 않습니다.
2. 다른 패키지 컴포넌트는 **문서에 허용된 지점에서만** 씁니다.
- SW 일반 모드 `SWPopup`의 `children` 안 Web 컨트롤(SW 카탈로그가 허용, 예: Popup 하단 `Button`)
- SW 뷰어 모드 `SWViewerPanel`의 `children` 안 Web 컨트롤 — `SWViewer*` 컨트롤로 안 될 때만(SW `AI_USAGE.md` §0.4)
diff --git a/packages/sw/ai/AI_USAGE.md b/packages/sw/ai/AI_USAGE.md
index cb50552..b09a057 100644
--- a/packages/sw/ai/AI_USAGE.md
+++ b/packages/sw/ai/AI_USAGE.md
@@ -102,8 +102,9 @@ import "@hm-design/sw/styles.css" // 앱 진입점에서 한 번
- **금지:** `@hm-design/sw/src/...`, `@hm-design/sw/dist/components/...` 같은 내부 경로 Import. `dist`에 내부 모듈의 `.d.ts`가 보여도 공개 API가 아니다.
- Peer: `react`, `react-dom` `^19.0.0`. 런타임 dependency 없음. `@hm-design/ui`·조직도 패키지와 무관하다.
- SVG·PNG 자산은 JS에 data URI로 포함되어 별도 설정이 없다.
-- 글꼴 파일은 포함하지 않는다. 앱에서 Noto Sans KR(400/500/700)을 로드한다(없으면 Malgun Gothic → Apple SD Gothic Neo).
-- SW 기본 글꼴은 Noto Sans KR이다(Figma는 Dotum). **Dotum을 기본으로 쓸지는 디자인 승인 대기**이므로 AI가 임의로 Dotum을 적용하지 않는다. 프로그램(사용자)이 글꼴을 정한 경우에만 §3.1 방법으로 `--hm-sw-header-font-family`를 바꾸고, 결과 보고에 "글꼴 변경 — 디자인 승인 대기 항목"으로 적는다.
+- SW 기본 글꼴은 돋움(Dotum)이다(`--hm-sw-header-font-family: Dotum, "돋움", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif`, Figma와 같음). 설치된 시스템 글꼴(Windows)을 쓰며 패키지는 글꼴 파일을 포함하지 않는다. 돋움이 없는 환경은 Malgun Gothic → Apple SD Gothic Neo로 대체된다. 앱이 별도 글꼴 파일을 로드할 필요는 없다.
+- SW 화면은 `SWTheme`(또는 SW 컴포넌트 루트)와 SW 토큰의 글꼴을 그대로 쓴다. Web UI의 기본 글꼴(Noto Sans KR, `--hm-font-family-base`)이나 Web `theme.css`의 글꼴 설정을 SW 화면에 복사하지 않고, SW 컴포넌트에 `font-family`를 개별 지정하지 않는다. 프로그램(사용자)이 다른 글꼴을 정한 경우에만 §3.1 방법으로 `--hm-sw-header-font-family`를 바꾸고 결과 보고에 "글꼴 변경"으로 적는다.
+- 돋움에는 Bold 글꼴이 없어 700은 브라우저가 합성 Bold로 그리며 글자당 약 1px 넓어진다. Figma 고정 폭 칸(예: 가중치·스케일 38px, 레이어 값, 문자/치수스타일)에 긴 값이 들어가면 말줄임이 된다.
- CSS는 전역(`html`, `body`, `*`)을 건드리지 않는다. `body` margin 등 페이지 기본값은 소비 앱이 정한다.
- 스크롤바: SW 전용 스크롤바 디자인은 없다. 기존 SW 컴포넌트는 브라우저 기본 스크롤바를 쓰며(속성Bar만 `scrollbar-width: thin`), Popup 본문 스크롤은 `SWPopup`이 처리한다. 새 스크롤 영역에 임의 스크롤바 색·두께를 만들지 않는다.
- TypeScript에서 CSS side-effect import 오류가 나면 Vite의 `vite/client` 타입을 포함한다.
@@ -122,7 +123,7 @@ SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-viewer-*`, `--hm-sw
1. 화면 전체(Header·Panel·Menu가 같은 값)를 바꾸려면 화면을 `<SWTheme style={theme}>` 하나로 감싸고 거기서만 토큰을 정합니다. 프로그램 class(`<SWTheme className="app-sw-theme">` + `.app-sw-theme { --hm-sw-header-font-family: … }`)도 됩니다.
```tsx
- const appSWTheme = { "--hm-sw-header-font-family": '"Noto Sans KR", sans-serif' } as CSSProperties
+ const appSWTheme = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' } as CSSProperties // 프로그램이 정한 값만
<SWTheme style={appSWTheme}>
<SWViewerHeader … /> {/* Dropdown·More 포함 */}
<SWViewerPanel …>…</SWViewerPanel>
diff --git a/packages/sw/ai/COMPOSITION_PATTERNS.md b/packages/sw/ai/COMPOSITION_PATTERNS.md
index 891e8f9..ad08596 100644
--- a/packages/sw/ai/COMPOSITION_PATTERNS.md
+++ b/packages/sw/ai/COMPOSITION_PATTERNS.md
@@ -235,7 +235,7 @@ const sections: SWHomeMenuSectionData[] = [
일반·뷰어 모드 공통입니다. 토큰 기본값은 가장 바깥 SW 범위에서만 선언되고 안쪽 SW 요소는 상속합니다([AI_USAGE.md](./AI_USAGE.md) §3.1).
```tsx
-const appSWTheme = { "--hm-sw-header-font-family": '"Noto Sans KR", sans-serif' } as CSSProperties // 프로그램이 정한 값만
+const appSWTheme = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' } as CSSProperties // 프로그램이 정한 값만
<SWTheme style={appSWTheme}> {/* 가장 바깥 범위: 여기서만 지정 */}
<SWViewerHeader … /> {/* Dropdown·More까지 상속 */}
@@ -246,7 +246,7 @@ const appSWTheme = { "--hm-sw-header-font-family": '"Noto Sans KR", sans-serif'
```
- 일반 모드도 같습니다: `<SWTheme style={appSWTheme}>` 안의 `SWHeader`(Dropdown·창 이동 Tooltip)·`SWFooter`(`SWFooterCommandLine`·명령 등록 안내)가 같은 값을 씁니다.
-- 기본 글꼴은 Noto Sans KR입니다. Dotum 기본 적용은 디자인 승인 대기이므로 AI가 임의로 Dotum을 넣지 않습니다.
+- 기본 글꼴은 돋움(Dotum)입니다. 기본값을 쓸 때는 글꼴 토큰을 지정하지 않습니다. Web UI 글꼴(Noto Sans KR)을 SW 화면에 넣지 않습니다.
- `hm-sw-*` class 선택자로 토큰을 덮거나 `!important`, 컴포넌트마다 같은 값 반복 지정을 하지 않습니다.
## 9. 하지 말 것
diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md
index 85ac0be..437eda1 100644
--- a/packages/sw/ai/DESIGN_RULES.md
+++ b/packages/sw/ai/DESIGN_RULES.md
@@ -44,7 +44,7 @@
| 항목 | 값 |
|---|---|
-| 글꼴 | Noto Sans KR → Malgun Gothic → Apple SD Gothic Neo (`--hm-sw-header-font-family`, Figma Dotum 대체 — 현재 패키지 기본값). 폰트 파일은 앱이 로드. **Dotum 기본 적용 여부는 디자인 승인 대기**: 기본값을 바꾸지 않으며, 프로그램이 정한 경우에만 [AI_USAGE.md](./AI_USAGE.md) §3.1 방법으로 변경 |
+| 글꼴 | 돋움(Dotum) → Malgun Gothic → Apple SD Gothic Neo (`--hm-sw-header-font-family: Dotum, "돋움", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif`, Figma와 같음, 2026-10-20). 설치된 시스템 글꼴을 쓰며 글꼴 파일은 포함하지 않음. Web UI 기본 글꼴(Noto Sans KR)과 별개. 다른 글꼴은 프로그램이 정한 경우에만 [AI_USAGE.md](./AI_USAGE.md) §3.1 방법으로 변경. 돋움 700은 합성 Bold(글자당 약 +1px) |
| 기본 / 라벨 | 14px / 12px (`--hm-sw-header-font-size` / `--hm-sw-header-label-size`) |
| Weight | 400, 500, 700만 사용(600 없음). 값·제목·토글 700 |
| 자간 | 값 0.04em, 라벨 0.02em, 파일명 0.06em |
@@ -180,7 +180,7 @@ Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타
## 13. Footer (`SWFooter*`, Figma 26:10097) — SW 일반 모드 전용
-원본 `src/theme/sw-footer.css`. 토큰은 `--hm-sw-footer-*`(`.hm-sw-header`, `.hm-sw-theme` 범위). 값은 Figma Plugin API로 읽은 값입니다(2026-10-20). 글꼴은 Header와 같이 Dotum 대신 Noto Sans KR, 글자 외곽선은 Header 토큰(`--hm-sw-header-text-outline` 1.3px / `--hm-sw-header-label-outline` 1px)을 공유하고, Figma의 자기 색 0.3 / 0.4px stroke는 Header 2026-10-16 수정과 같이 그리지 않습니다.
+원본 `src/theme/sw-footer.css`. 토큰은 `--hm-sw-footer-*`(`.hm-sw-header`, `.hm-sw-theme` 범위). 값은 Figma Plugin API로 읽은 값입니다(2026-10-20). 글꼴은 Header와 같은 SW 기본 글꼴(돋움), 글자 외곽선은 Header 토큰(`--hm-sw-header-text-outline` 1.3px / `--hm-sw-header-label-outline` 1px)을 공유하고, Figma의 자기 색 0.3 / 0.4px stroke는 Header 2026-10-16 수정과 같이 그리지 않습니다.
| 항목 | 값 | 토큰 |
|---|---|---|
diff --git a/packages/sw/ai/VIEWER.md b/packages/sw/ai/VIEWER.md
index 6c76a21..4fdae42 100644
--- a/packages/sw/ai/VIEWER.md
+++ b/packages/sw/ai/VIEWER.md
@@ -101,7 +101,7 @@ const groups: SWViewerHeaderGroup[] = [
| 뷰어 Header를 `div` + `button`으로 다시 만들기, `hm-sw-viewer-*` class를 소비 코드에 붙이기 | 데이터(`groups`)와 `custom` / `trailing`으로 구성 |
| Header 버튼 글자에 `-webkit-text-stroke`·`border`로 외곽선 만들기 | 외곽선은 패키지 토큰이 이미 적용합니다 |
| 메뉴 색·간격을 `#123456`, `margin` 등 임의 값으로 덮기 | 토큰 `--hm-sw-viewer-*` / `--hm-sw-header-*`만 가장 바깥 SW 범위(`<SWTheme>` 또는 단독 컴포넌트 루트)의 `style`로 조정(기본값 유지 권장, [AI_USAGE.md](./AI_USAGE.md) §3.1) |
-| Header·Dropdown·More·Panel의 글꼴을 각각 따로 지정, `.hm-sw-viewer-menu` 등 class로 덮기 | 화면을 `<SWTheme style={theme}>` 하나로 감싸면 Header·Dropdown·More·Panel이 같은 값을 상속합니다. 글꼴 기본값은 Noto Sans KR이며 Dotum 기본 적용은 디자인 승인 대기 |
+| Header·Dropdown·More·Panel의 글꼴을 각각 따로 지정, `.hm-sw-viewer-menu` 등 class로 덮기 | 화면을 `<SWTheme style={theme}>` 하나로 감싸면 Header·Dropdown·More·Panel이 같은 값을 상속합니다. 글꼴 기본값은 돋움(Dotum)이며 Web UI 글꼴(Noto Sans KR)을 복사하지 않습니다 |
| 존재하지 않는 Props 추측(`items`, `content`, `renderBody` …) | 위 표와 `dist/index.d.ts`가 전부 |
| 일반 모드 화면에서 `SWViewerHeader` 사용, 뷰어에서 `SWHeader` 재스타일 | 모드별 Header를 바꿔 끼웁니다 |
| 기능 없는 메뉴를 가짜 동작으로 연결 | 항목 제외 또는 `disabled` + 미연결 보고 |
diff --git a/packages/sw/ai/examples/ExampleJ-Theme.tsx b/packages/sw/ai/examples/ExampleJ-Theme.tsx
index 3b1d240..91bc5e9 100644
--- a/packages/sw/ai/examples/ExampleJ-Theme.tsx
+++ b/packages/sw/ai/examples/ExampleJ-Theme.tsx
@@ -1,8 +1,8 @@
// Example J — SW theme tokens (both modes). The only official way to change SW tokens: `style` (or the program's own
// class) on the OUTERMOST SW scope. Here one <SWTheme> wraps the viewer screen, so the header, its dropdowns, More,
// and every panel read the same value. Portals leave the DOM tree: wrap their content in <SWTheme> with the same style.
-// The font below is a placeholder for a program decision: the SW default stays Noto Sans KR (a Dotum default is
-// pending design approval), so only set it when the program has decided to.
+// The font below is a placeholder for a program decision: the SW default is Dotum (2026-10-20) and needs no token,
+// so only set it when the program has decided on another font. Never copy the Web UI font (Noto Sans KR) here.
import { useState, type CSSProperties } from "react"
import { createPortal } from "react-dom"
import {
@@ -12,7 +12,7 @@ import {
/** One theme object for the screen and for every portal. Token names only (`--hm-sw-*`), never `hm-sw-*` classes. */
export const appSWTheme = {
- "--hm-sw-header-font-family": '"Noto Sans KR", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif',
+ "--hm-sw-header-font-family": '"Malgun Gothic", "Apple SD Gothic Neo", sans-serif',
} as CSSProperties
export function ExampleJTheme({ onEvent }: { onEvent?: (name: string) => void }) {
diff --git a/packages/sw/src/theme/sw-footer.css b/packages/sw/src/theme/sw-footer.css
index 41f13b1..9e63565 100644
--- a/packages/sw/src/theme/sw-footer.css
+++ b/packages/sw/src/theme/sw-footer.css
@@ -1,6 +1,6 @@
/* @hm-design/sw — SW Footer (Figma "레이아웃 분리" 26:10097 bottom bar, components 26:9459 … 26:10097, read
through the Figma Plugin API 2026-10-20). Footer-only tokens. Text keeps the SW typography of sw.css
- (Noto Sans KR instead of Dotum, black OUTSIDE outline as 16 text-shadows, own-color 0.3 / 0.4px strokes removed
+ (Dotum like Figma since 2026-10-20, black OUTSIDE outline as 16 text-shadows, own-color 0.3 / 0.4px strokes removed
like the 2026-10-16 header fix). Never touches global selectors, SWHeader or SWPopup classes.
Declared on the outermost SW scope only, like sw.css: the command line inside the bar inherits the tokens. */
diff --git a/packages/sw/src/theme/sw.css b/packages/sw/src/theme/sw.css
index ad511f5..27441bb 100644
--- a/packages/sw/src/theme/sw.css
+++ b/packages/sw/src/theme/sw.css
@@ -17,8 +17,10 @@
--hm-sw-header-logo-width: 32px;
--hm-sw-header-logo-height: 17px;
- /* Typography (Figma Dotum is replaced by Noto Sans KR on purpose) */
- --hm-sw-header-font-family: "Noto Sans KR", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
+ /* Typography: SW default is Dotum like Figma (2026-10-20). Installed system font only (Windows gulim.ttc); the
+ package ships no font file. Malgun Gothic / Apple SD Gothic Neo cover systems without Dotum. Web UI keeps
+ Noto Sans KR (its own token); this token never leaves the SW scope. */
+ --hm-sw-header-font-family: Dotum, "돋움", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
--hm-sw-header-font-size: 14px;
--hm-sw-header-label-size: 12px;
@@ -539,7 +541,9 @@
flex: 0 1 90px;
min-width: 0;
margin-left: -1px;
- padding: 0 8px;
+ /* 4px (was 8px): Dotum has no bold face, so 700 is synthesized ~1px wider per glyph ("그리기모드" 77.8px);
+ the centered label fits the Figma 90px cell again. */
+ padding: 0 4px;
overflow: hidden;
text-align: center;
white-space: nowrap;
diff --git a/packages/sw/tests/ai-template-consumer.browser.mjs b/packages/sw/tests/ai-template-consumer.browser.mjs
index b4b1abf..ad917cc 100644
--- a/packages/sw/tests/ai-template-consumer.browser.mjs
+++ b/packages/sw/tests/ai-template-consumer.browser.mjs
@@ -41,7 +41,7 @@ await page.evaluate(() => document.fonts.ready)
const assets = await page.evaluate(() => [...document.images].map(image => ({ src: image.currentSrc.slice(0, 30), ok: image.complete && image.naturalWidth > 0 })))
check("1 all SVG/PNG assets decode (data URI, no external path)", assets.length > 0 && assets.every(item => item.ok && item.src.startsWith("data:")), { count: assets.length, broken: assets.filter(item => !item.ok) })
const tokens = await page.evaluate(() => { const s = getComputedStyle(document.querySelector('[data-example="b"] .hm-sw-theme')); return { height: s.getPropertyValue("--hm-sw-header-height").trim(), font: s.fontFamily } })
-check("1 styles.css loaded; SWTheme provides tokens", tokens.height === "49px" && tokens.font.includes("Noto Sans KR"), tokens)
+check("1 styles.css loaded; SWTheme provides tokens", tokens.height === "49px" && tokens.font.startsWith("Dotum"), tokens)
// 2. Example A: default header unchanged (Figma 1920 coordinates)
const a = section("a")
diff --git a/packages/sw/tests/theme-inheritance.browser.mjs b/packages/sw/tests/theme-inheritance.browser.mjs
index 007855e..1e6ebab 100644
--- a/packages/sw/tests/theme-inheritance.browser.mjs
+++ b/packages/sw/tests/theme-inheritance.browser.mjs
@@ -1,7 +1,7 @@
// SW theme token inheritance (2026-10-20, A-1).
// 1) SW/Theme › Theme Inheritance: a token set through `style` on the outermost SW scope reaches the header, dropdowns,
// More, panels, footer command line and tooltips; portals get it only when wrapped in a themed <SWTheme>; areas do
-// not mix; unthemed components keep Noto Sans KR. Dotum is a verification value only (not the SW default).
+// not mix; unthemed components keep the SW default Dotum (2026-10-20). Noto Sans KR is a verification value only.
// 2) Regression: every existing SW Story renders identically with the new cascade and with the legacy cascade
// (token blocks re-declared on every `.hm-sw-header, .hm-sw-theme`, i.e. the code before this change): computed
// font / color / geometry of every SW element and an element screenshot comparison. No Story sets an override,
@@ -22,9 +22,9 @@ const [storybook = 'http://127.0.0.1:6024', outArg] = process.argv.slice(2)
const out = path.resolve(outArg ?? 'docs/sw/theme-inheritance-qa')
await mkdir(out, { recursive: true })
-const VERIFY = 'Dotum, 돋움, sans-serif'
+const VERIFY = '"Noto Sans KR", sans-serif'
const NESTED = '"Malgun Gothic", sans-serif'
-const DEFAULT = '"Noto Sans KR", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif'
+const DEFAULT = 'Dotum, 돋움, "Malgun Gothic", "Apple SD Gothic Neo", sans-serif'
const results = []
const check = (name, pass, detail = '') => { results.push({ name, result: pass ? 'PASS' : 'FAIL', detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`) }
const errors = []
@@ -67,52 +67,52 @@ const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${fa
// Default (no theme): header, then its sample dropdown and More.
const def = scope('default')
- check('기본: 단독 SWViewerHeader = Noto Sans KR', only(await fonts(page, `${def} .hm-sw-viewer-header`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-header`)))
+ check('기본: 단독 SWViewerHeader = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-header`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-header`)))
await page.locator(`${def} .hm-sw-viewer-header [aria-haspopup="menu"]`).first().click()
await page.waitForSelector(`${def} .hm-sw-viewer-menu`)
- check('기본: Dropdown = Noto Sans KR', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
+ check('기본: Dropdown = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
await page.keyboard.press('Escape')
await page.locator(`${def} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
await page.waitForSelector(`${def} .hm-sw-viewer-menu[data-surface="beige"]`)
- check('기본: More = Noto Sans KR', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
+ check('기본: More = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
await page.keyboard.press('Escape')
// Viewer, themed through <SWTheme style>.
const viewer = scope('viewer-themed')
- check('뷰어 테마: SWViewerHeader 전체 = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-header`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-header`)))
+ check('뷰어 테마: SWViewerHeader 전체 = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-header`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-header`)))
await page.locator(`${viewer} .hm-sw-viewer-header [aria-haspopup="menu"]`).first().click()
await page.waitForSelector(`${viewer} .hm-sw-viewer-menu`)
- check('뷰어 테마: Dropdown = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
+ check('뷰어 테마: Dropdown = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
await page.keyboard.press('Escape')
await page.locator(`${viewer} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
await page.waitForSelector(`${viewer} .hm-sw-viewer-menu[data-surface="beige"]`)
- check('뷰어 테마: More = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
+ check('뷰어 테마: More = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
// A submenu of More, if any, is a nested menu inside a nested menu.
const sub = page.locator(`${viewer} .hm-sw-viewer-menu[data-surface="beige"] [aria-haspopup="menu"]`).first()
if (await sub.count()) {
await sub.hover()
await page.waitForTimeout(150)
- check('뷰어 테마: More 서브메뉴 = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
+ check('뷰어 테마: More 서브메뉴 = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
}
await page.keyboard.press('Escape')
const panels = await page.locator(`${viewer} .hm-sw-viewer-panel`).count()
- check('뷰어 테마: SWViewerPanel(제목·입력·버튼·스위치·힌트) = Dotum', panels === 2 && only(await fonts(page, `${viewer} .hm-sw-viewer-panel`), VERIFY), `${panels} panels · ${fmt(await fonts(page, `${viewer} .hm-sw-viewer-panel`))}`)
+ check('뷰어 테마: SWViewerPanel(제목·입력·버튼·스위치·힌트) = 검증 글꼴', panels === 2 && only(await fonts(page, `${viewer} .hm-sw-viewer-panel`), VERIFY), `${panels} panels · ${fmt(await fonts(page, `${viewer} .hm-sw-viewer-panel`))}`)
await page.screenshot({ path: path.join(out, 'viewer-themed-more.png'), clip: await page.locator(viewer).boundingBox() })
// General mode: SWHeader (+ open layer dropdown, window-move tooltip), SWFooter (+ command line, guide tooltip).
const general = scope('general-themed')
const headerFonts = await fonts(page, `${general} .hm-sw-header`, '.hm-sw-header__motion-tooltip')
- check('일반 테마: SWHeader + 열린 레이어 Dropdown = Dotum (창 이동 Tooltip 별도)', only(headerFonts, VERIFY), fmt(headerFonts))
+ check('일반 테마: SWHeader + 열린 레이어 Dropdown = 검증 글꼴 (창 이동 Tooltip 별도)', only(headerFonts, VERIFY), fmt(headerFonts))
const tooltip = await page.locator(`${general} .hm-sw-header__motion-tooltip`).evaluate(element => getComputedStyle(element).fontFamily)
check('일반 테마: 창 이동 Tooltip = Inter → 테마 글꼴', tooltip === `Inter, ${VERIFY}`, tooltip)
const layerMenu = await page.locator(`${general} .hm-sw-header`).getByText('A-WALL').count()
check('일반 테마: 레이어 Dropdown이 열린 상태(목록 행 A-WALL 표시)', layerMenu > 0, `${layerMenu} row`)
- check('일반 테마: SWFooter = Dotum', only(await fonts(page, `${general} .hm-sw-footer`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer`)))
- check('일반 테마: 명령창(SWFooterCommandLine, 중첩 hm-sw-theme) = Dotum', only(await fonts(page, `${general} .hm-sw-footer-command`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer-command`)))
+ check('일반 테마: SWFooter = 검증 글꼴', only(await fonts(page, `${general} .hm-sw-footer`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer`)))
+ check('일반 테마: 명령창(SWFooterCommandLine, 중첩 hm-sw-theme) = 검증 글꼴', only(await fonts(page, `${general} .hm-sw-footer-command`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer-command`)))
await page.locator(`${general} .hm-sw-footer-add`).first().focus()
await page.waitForTimeout(150)
const guide = page.locator(`${general} .hm-sw-footer [role="tooltip"]`)
- check('일반 테마: 명령 등록 안내(Tooltip) = Dotum', (await guide.count()) > 0 && only(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`)))
+ check('일반 테마: 명령 등록 안내(Tooltip) = 검증 글꼴', (await guide.count()) > 0 && only(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`)))
// Portals.
const [plain, themed, insideSource] = await page.evaluate(() => {
@@ -122,15 +122,15 @@ const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${fa
!!document.querySelector('[data-theme-case="portal-source"] [data-portal]')]
})
check('Portal: DOM이 테마 영역 밖에 렌더링됨', !insideSource)
- check('Portal: SWTheme만 감싸면 기본 글꼴(테마 미전달)', plain.startsWith('"Noto Sans KR"'), plain)
+ check('Portal: SWTheme만 감싸면 기본 글꼴(테마 미전달)', plain === DEFAULT, plain)
check('Portal: 같은 theme style의 SWTheme으로 감싸면 테마 적용', themed === VERIFY, themed)
// Areas.
const area = name => fonts(page, `${scope(name)} .hm-sw-viewer-hint`)
- check('영역 A(Dotum)', only(await area('area-a'), VERIFY), fmt(await area('area-a')))
+ check('영역 A(검증 글꼴)', only(await area('area-a'), VERIFY), fmt(await area('area-a')))
check('영역 B(기본) — 옆 영역 값이 섞이지 않음', only(await area('area-b'), DEFAULT), fmt(await area('area-b')))
const outer = await page.locator(`${scope('area-c')} > .hm-sw-viewer-hint`).evaluate(element => getComputedStyle(element).fontFamily)
- check('영역 C 바깥(Dotum)', outer === VERIFY, outer)
+ check('영역 C 바깥(검증 글꼴)', outer === VERIFY, outer)
check('영역 C 안쪽 SWTheme style(Malgun Gothic)만 안쪽에 적용', only(await area('area-c-nested'), NESTED), fmt(await area('area-c-nested')))
await page.screenshot({ path: path.join(out, 'theme-inheritance-full.png'), fullPage: true })
await page.close()
@@ -213,7 +213,7 @@ for (const width of [1920, 1440, 1180, 900, 560]) for (const id of ids) {
await page.waitForSelector(`${viewer} .hm-sw-viewer-menu[data-surface="beige"]`)
const more = await fonts(page, `${viewer} .hm-sw-viewer-menu`)
const command = await fonts(page, '[data-theme-case="general-themed"] .hm-sw-footer-command')
- check('대조: 기존 구조 주입 시 More·명령창이 Noto로 되돌아감(결함 재현)', only(more, DEFAULT) && only(command, DEFAULT), `More ${fmt(more)} · 명령창 ${fmt(command)}`)
+ check('대조: 기존 구조 주입 시 More·명령창이 기본 글꼴로 되돌아감(결함 재현)', only(more, DEFAULT) && only(command, DEFAULT), `More ${fmt(more)} · 명령창 ${fmt(command)}`)
await page.close()
}
const failedRegression = regression.filter(item => item.result !== 'PASS')
diff --git a/packages/ui/ai/AI_GUIDE.md b/packages/ui/ai/AI_GUIDE.md
index 8ac07f4..5fd7e42 100644
--- a/packages/ui/ai/AI_GUIDE.md
+++ b/packages/ui/ai/AI_GUIDE.md
@@ -138,7 +138,7 @@
혼합 규칙:
-1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다.
+1. 영역마다 하나의 패키지를 정하고 그 영역의 신규 UI는 그 패키지의 토큰만 읽습니다. Web 토큰(`--hm-sem-*`)을 SW 영역에, SW 토큰(`--hm-sw-*`)을 Web 영역에 쓰지 않습니다. OrgChart 내부는 `--hm-orgchart-*`만 씁니다. 글꼴도 같습니다: SW 기본 글꼴은 돋움(Dotum, `--hm-sw-header-font-family`), Web 기본 글꼴은 Noto Sans KR(`--hm-font-family-base`), OrgChart는 자기 설정을 유지합니다. 한 패키지의 글꼴 값을 다른 영역에 복사하지 않습니다.
2. 다른 패키지 컴포넌트는 **문서에 허용된 지점에서만** 씁니다.
- SW 일반 모드 `SWPopup`의 `children` 안 Web 컨트롤(SW 카탈로그가 허용, 예: Popup 하단 `Button`)
- SW 뷰어 모드 `SWViewerPanel`의 `children` 안 Web 컨트롤 — `SWViewer*` 컨트롤로 안 될 때만(SW `AI_USAGE.md` §0.4)
diff --git a/src/sw/sw-theme.stories.tsx b/src/sw/sw-theme.stories.tsx
index a389af1..bc4c2d8 100644
--- a/src/sw/sw-theme.stories.tsx
+++ b/src/sw/sw-theme.stories.tsx
@@ -14,9 +14,9 @@ import "./sw-viewer-stories.css"
// SW theme token inheritance (2026-10-20). The official way to change SW tokens: set them through `style` (or the
// consumer's own class) on the OUTERMOST SW scope — <SWTheme> around the screen, or the component root when it is
// used alone. Nested SW roots (viewer menus, More, panels, footer command line) inherit the value.
-// Dotum is used here ONLY to make the inheritance visible: it is NOT the SW default font (design approval pending).
+// The SW default font is Dotum (2026-10-20). Noto Sans KR is used here ONLY to make the inheritance visible.
const KICKER = "HM DESIGN SYSTEM / SW"
-const VERIFY_FONT = 'Dotum, "돋움", sans-serif'
+const VERIFY_FONT = '"Noto Sans KR", sans-serif'
const VERIFY_THEME = { "--hm-sw-header-font-family": VERIFY_FONT } as CSSProperties
const NESTED_THEME = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' } as CSSProperties
@@ -77,8 +77,8 @@ export const ThemeInheritance: Story = {
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW — 테마 토큰 상속"
- description={<>SW 토큰은 가장 바깥 SW 범위(<code>&lt;SWTheme&gt;</code> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 <code>style</code>로만 바꿉니다. 아래 Dotum은 <strong>상속 확인용 검증 값</strong>이며 SW 기본 글꼴(Noto Sans KR)은 바뀌지 않습니다(Dotum 기본 적용은 디자인 승인 대기).</>} />
- <StorySection title="1. 기본 (테마 지정 없음)" description="단독 SWViewerHeader. 기본 글꼴 Noto Sans KR. 1000px 폭이라 일부 항목이 More(⋯)로 이동합니다.">
+ description={<>SW 토큰은 가장 바깥 SW 범위(<code>&lt;SWTheme&gt;</code> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 <code>style</code>로만 바꿉니다. SW 기본 글꼴은 Dotum이며, 아래 Noto Sans KR은 <strong>상속 확인용 검증 값</strong>입니다.</>} />
+ <StorySection title="1. 기본 (테마 지정 없음)" description="단독 SWViewerHeader. 기본 글꼴 Dotum. 1000px 폭이라 일부 항목이 More(⋯)로 이동합니다.">
<ViewerArea width={1000} data-theme-case="default" />
</StorySection>
<StorySection title="2. 뷰어 모드 — SWTheme style로 글꼴 지정" description="<SWTheme style={{ '--hm-sw-header-font-family': … }}> 안의 Header·Dropdown·More·패널(내부 컨트롤 포함)이 같은 글꼴을 씁니다.">
@@ -105,10 +105,10 @@ export const ThemeInheritance: Story = {
</StorySection>
<StorySection title="5. 여러 테마 영역" description="옆 영역끼리 값이 섞이지 않고, 안쪽 SWTheme의 style은 그 안에서만 적용됩니다.">
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 12 }}>
- <SWTheme style={VERIFY_THEME} data-theme-case="area-a"><SWViewerHint>영역 A — 검증 글꼴(Dotum)</SWViewerHint></SWTheme>
- <SWTheme data-theme-case="area-b"><SWViewerHint>영역 B — 기본(Noto Sans KR)</SWViewerHint></SWTheme>
+ <SWTheme style={VERIFY_THEME} data-theme-case="area-a"><SWViewerHint>영역 A — 검증 글꼴(Noto Sans KR)</SWViewerHint></SWTheme>
+ <SWTheme data-theme-case="area-b"><SWViewerHint>영역 B — 기본(Dotum)</SWViewerHint></SWTheme>
<SWTheme style={VERIFY_THEME} data-theme-case="area-c">
- <SWViewerHint>영역 C 바깥 — Dotum</SWViewerHint>
+ <SWViewerHint>영역 C 바깥 — Noto Sans KR</SWViewerHint>
<SWTheme style={NESTED_THEME} data-theme-case="area-c-nested"><SWViewerHint>영역 C 안쪽 — Malgun Gothic</SWViewerHint></SWTheme>
</SWTheme>
</div>