Merge pull request 'Style/admin text gray hierarchy' (#42) from style/admin-text-gray-hierarchy into main
Deploy Storybook / deploy (push) Successful in 1m19s
Deploy Storybook / deploy (push) Successful in 1m19s
Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
commit
15c64b0003
39 files changed
+277
-157
No files matched your search
@@ -1,5 +1,13 @@
|
||||
# Working Context
|
||||
|
||||
## 2026-11-11 — Admin 텍스트 컬러 위계·Foundation Gray (브랜치 `style/admin-text-gray-hierarchy`, 미커밋)
|
||||
|
||||
- Theme: `themes/default.json`에 `color.palette.gray-900|800|700|600|500`(#212121·#424242·#616161·#757575·#9E9E9E) 추가, `color.fg.primary|secondary|placeholder` 기본값을 #212121 / #616161 / #757575로 변경. 템플릿 텍스트 alias만 Gray를 가리키고 브랜드 Primitive 값은 불변. `contract.ts` 공개 키 5개 추가(기존 키 불변). 생성물은 `generate:theme-defaults`.
|
||||
- Web: Combobox·Date Picker Placeholder를 공통 `--hm-sem-fg-placeholder`로 통일, 투명 면 readonly Input Placeholder는 `fg-secondary`. 테이블 헤더 Green·Disabled opacity 유지.
|
||||
- Chart: Cartesian 축 눈금 selector가 Recharts 3.8 DOM에 맞지 않아 #666이던 기존 결함 수정, Radar 기본 축 이름(#808080)도 `--hm-chart-axis-text`에 연결. 포인트 값 Label(시리즈색) 대비 미달은 README에 후속 항목으로만 기록.
|
||||
- OrgChart: `--hm-orgchart-resolved-fg-*` 기본값 #212121 / #616161, 카드 직위 #797979 → fg-secondary, 이름·툴팁·Workdesk 요약 하드코딩 → `resolved-fg-primary`. 브랜드 블루·Role tag·회사색·상태 회색 유지. 승인 기록 `docs/org-chart/design-approval.md` §8.
|
||||
- 보류: 비readOnly 상태 그룹 제목(15px Bold, 회색 Gradient)은 글자색만으로 4.5:1 불가 — 배경 또는 크기 결정 필요.
|
||||
|
||||
## 2026-11-11 — SW 크기 규격 문서 정합성·기존 토큰 연결 (브랜치 `style/sw-size-spec-docs-2026-11-11`, 미커밋)
|
||||
|
||||
- `packages/sw/ai/DESIGN_RULES.md`: §1 필드 안쪽 여백을 필드별·폭별로 정정(레이어·색상 왼쪽 12, 760~1799 `0 8px`, 760~1079 `0 6px`)하고 속성보기 행 추가. §11 뷰어 Header 굵기 정정(기본 500, 파일명·버튼 라벨·탭 700). §13 좌표 글자 토큰 표기. §16 SW 크기 체계 요약(글자·아이콘·높이·여백·반응형 색인, 새 규격 아님) 추가.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Combobox
|
||||
|
||||
> 현재 접근성 보완(2026-10-10): 흰 Trigger의 빈 값 안내는 기존 `--hm-sem-fg-secondary`를 사용한다. 기본 Theme에서 #6e6759/흰색이며 공통 placeholder Token과 Disabled opacity는 보존한다. 아래 과거 검증의 #999 실측은 당시 기록이다.
|
||||
> 현재 기준(2026-11-11 Admin 텍스트 위계): 흰 Trigger의 빈 값 안내는 공통 `--hm-sem-fg-placeholder`(#757575, 흰색 대비 4.61:1)를 사용한다. 2026-10-10 접근성 보완의 `--hm-sem-fg-secondary`(#6e6759) 적용은 당시 Placeholder #999 대비 부족 때문이었고, Placeholder 기본값이 4.5:1을 넘으면서 공통 Token으로 되돌렸다. Disabled opacity는 보존한다. 아래 과거 검증의 #999 실측은 당시 기록이다.
|
||||
|
||||
> 2026-10-10 추가 수정 요청 기준: 현재 구현의 연결형 Popup을 기준으로 문서를 통일. 팝업 dialog 이름은 aria-label → aria-labelledby → 고유 trigger ID 순으로 연결. Command 검색·listbox 이름과 인스턴스 ID는 독립. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|---|---|
|
||||
| Size | `large`(기본, 44px, 16 / 24), `medium`(34px, 14 / 20) — Select와 같은 두 크기 |
|
||||
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px `--hm-sem-stroke-input-default`, radius 4px, 흰 면, 오른쪽 chevron 18px |
|
||||
| 값 / placeholder | 선택된 option의 label, 없으면 placeholder("선택", `--hm-sem-fg-secondary`), 길면 말줄임 |
|
||||
| 값 / placeholder | 선택된 option의 label, 없으면 placeholder("선택", `--hm-sem-fg-placeholder`), 길면 말줄임 |
|
||||
| Popup | padding 0, 폭 = trigger 폭(`--radix-popover-trigger-width`), sideOffset=-1로 1px 연결 경계 공유. Focus border, 안쪽 두 모서리 radius는 기존 Input token, shadow `0 2px 24px black 12%` |
|
||||
| 검색 | Command Input(placeholder 기본 "검색...") |
|
||||
| 목록 | Command List(최대 280px), Item 높이 36px·padding-x 12px·radius 0·weight 400. Large 16/24, Medium 14/20. 선택 check 16px, 긴 label은 말줄임 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Date Picker
|
||||
|
||||
> 현재 접근성 보완(2026-10-10): 흰 Trigger의 빈 값 안내는 기존 `--hm-sem-fg-secondary`를 사용한다. 기본 Theme에서 #6e6759/흰색이며 공통 placeholder Token과 Disabled opacity는 보존한다. 아래 과거 검증의 #999 실측은 당시 기록이다.
|
||||
> 현재 기준(2026-11-11 Admin 텍스트 위계): 흰 Trigger의 빈 값 안내는 공통 `--hm-sem-fg-placeholder`(#757575, 흰색 대비 4.61:1)를 사용한다. 2026-10-10 접근성 보완의 `--hm-sem-fg-secondary`(#6e6759) 적용은 당시 Placeholder #999 대비 부족 때문이었고, Placeholder 기본값이 4.5:1을 넘으면서 공통 Token으로 되돌렸다. Disabled opacity는 보존한다. 아래 과거 검증의 #999 실측은 당시 기록이다.
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Date Picker 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|---|---|
|
||||
| Size | `large`(기본, 안쪽 42px + 1px border = 44px, 16 / 24), `medium`(안쪽 32px = 34px, 14 / 20) — Select와 같은 두 크기 |
|
||||
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px `--hm-sem-stroke-input-default`, radius 4px, 흰 면, 오른쪽 달력 아이콘 18px `--hm-sem-fg-secondary` |
|
||||
| 값 / placeholder | 선택값은 `--hm-sem-fg-primary`, placeholder("날짜 선택")는 `--hm-sem-fg-secondary`, 길면 말줄임 |
|
||||
| 값 / placeholder | 선택값은 `--hm-sem-fg-primary`, placeholder("날짜 선택")는 `--hm-sem-fg-placeholder`, 길면 말줄임 |
|
||||
| 날짜 표시 | 로컬 `Date`의 연·월·일 → `YYYY.MM.DD` (예: `2026.01.09`, `2026.10.06`). `format` prop으로 교체 |
|
||||
| Popup | Popover 면(흰 면, 1px pale border, 4px radius, 그림자)에 Popover padding 0, 폭 `auto`로 Calendar에 맞춤 |
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
- 높이는 `min-height`, 한 줄 텍스트는 가운데.
|
||||
- Border 1px(`--hm-sem-stroke-input-default`), radius `--hm-radius-input`, face `--hm-sem-bg-surface`, 글자색 `--hm-sem-fg-primary`, weight 400.
|
||||
- Placeholder: `--hm-sem-fg-placeholder`, `opacity: 1`.
|
||||
- Placeholder: `--hm-sem-fg-placeholder`(#757575), `opacity: 1`. readonly는 면이 투명해 베이지 콘텐츠 면에서 4.5:1 미만이 되므로 `--hm-sem-fg-secondary`(#616161)를 쓴다(2026-11-11).
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
@@ -93,9 +93,9 @@ native `<input>` 동작 전체(`type`, `file:` 스타일 class, `placeholder`),
|
||||
|
||||
**2026-10-11 최종 사용자 결정 — Disabled Input Label**
|
||||
|
||||
- Input Story 소비 조합(`src/components/input.stories.tsx`의 `Specimen`)의 Disabled Label은 기존 `text-muted-foreground` → `--muted-foreground` → `--hm-sem-fg-secondary` 색(`#6E6759`)을 유지하고, `input.disabled`일 때만 `opacity-60`을 적용한다. HEX·Semantic Token·공개 Input API는 변경하지 않는다.
|
||||
- Input Story 소비 조합(`src/components/input.stories.tsx`의 `Specimen`)의 Disabled Label은 기존 `text-muted-foreground` → `--muted-foreground` → `--hm-sem-fg-secondary` 색(2026-11-11부터 `#616161`, 당시 `#6E6759`)을 유지하고, `input.disabled`일 때만 `opacity-60`을 적용한다. HEX·Semantic Token·공개 Input API는 변경하지 않는다.
|
||||
- Disabled Input 컨트롤 `0.6`, 소비 Label `0.6`, 부모 Field `1`이다. 형제 요소에 각각 적용하여 중첩 opacity를 만들지 않는다. 기본·readOnly·Error Label은 기존 `1`을 유지한다(Disabled가 아닌 경우).
|
||||
- 공통 [Label Component](label.md#4-상태-규격)의 Disabled `0.5`는 유지한다. Helper Text(`#6E6759`)와 Error Message(`#D32F2F`)도 기존 구현을 유지하며 이번 변경 대상이 아니다.
|
||||
- 공통 [Label Component](label.md#4-상태-규격)의 Disabled `0.5`는 유지한다. Helper Text(`--hm-sem-fg-secondary`, 2026-11-11부터 `#616161`)와 Error Message(`#D32F2F`)도 기존 구현을 유지하며 이번 변경 대상이 아니다.
|
||||
- 이전 검토의 “Label 100% 유지”는 당시 선택이며 현재 최종 결정은 Disabled Input Story Label `60%`다. 아래 2026-10-02 승인과 2026-10-06 검증은 당시 기록으로 보존하며, 이번 Label 정책의 현재 검증을 대신하지 않는다.
|
||||
- 실제 소비 화면은 자동으로 확대 수정하지 않는다. 신규 화면의 공개 Label 재사용·control 연결 계약은 유지하며, 공통 Label의 Disabled 정책을 이 Story 결정으로 대체하지 않는다.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 14 / 20 / 500 |
|
||||
| 색 | `--hm-sem-fg-secondary` (`#6E6759`) |
|
||||
| 색 | `--hm-sem-fg-secondary` (`#616161`, 2026-11-11 Admin 텍스트 위계 전 `#6E6759`) |
|
||||
| 배치 | `flex w-fit items-center gap-2` (shadcn 기본, 부모 grid에서 시작 정렬) |
|
||||
|
||||
Label ↔ Control 간격 8px(`input.field.gap`)은 Field 조합의 값이며 Label이 소유하지 않는다.
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|---|---|---|---|---|---|
|
||||
| `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.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.placeholder` | `--hm-sem-fg-placeholder` | `#999999` | Placeholder | Yes | input, select |
|
||||
| `color.fg.primary` | `--hm-sem-fg-primary` | `#212121` | 주요 텍스트 | Yes | calendar, pagination, badge, table, input, select, gnb |
|
||||
| `color.fg.secondary` | `--hm-sem-fg-secondary` | `#616161` | 보조 텍스트 | Yes | calendar, pagination, badge, table, gnb |
|
||||
| `color.fg.placeholder` | `--hm-sem-fg-placeholder` | `#757575` | Placeholder | Yes | input, textarea, select, combobox, date-picker, command |
|
||||
| `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.destructive` | `--hm-sem-action-destructive` | `#d32f2f` | 오류/위험 역할 | Yes | checkbox, radio, badge, input, button |
|
||||
@@ -51,7 +51,7 @@
|
||||
| `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-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` | `#11372d` | 선택적 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 | brand gradient stop, button border, tabs arrow (2026-11-11부터 텍스트 기본값 아님) |
|
||||
| `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-1` | `--hm-sem-palette-brown-light-1` | `#604f32` | 선택적 Theme 팔레트: brown-light-1 | Yes | button |
|
||||
@@ -62,12 +62,12 @@
|
||||
| `color.palette.orange-default` | `--hm-sem-palette-orange-default` | `#cc5200` | 선택적 Theme 팔레트: orange-default | Yes | button |
|
||||
| `color.palette.warm-neutral-light` | `--hm-sem-palette-warm-neutral-light` | `#e0dace` | 선택적 Theme 팔레트: warm-neutral-light | Yes | slider, progress |
|
||||
| `color.palette.warm-neutral-mid` | `--hm-sem-palette-warm-neutral-mid` | `#9a9284` | 선택적 Theme 팔레트: warm-neutral-mid | Yes | checkbox, radio, switch, badge |
|
||||
| `color.palette.warm-neutral-dark` | `--hm-sem-palette-warm-neutral-dark` | `#6e6759` | 선택적 Theme 팔레트: warm-neutral-dark | Yes | calendar, pagination, badge, table, gnb |
|
||||
| `color.palette.warm-neutral-dark` | `--hm-sem-palette-warm-neutral-dark` | `#6e6759` | 선택적 Theme 팔레트: warm-neutral-dark | Yes | action cancel, chart series side (2026-11-11부터 텍스트 기본값 아님) |
|
||||
| `color.palette.beige-light` | `--hm-sem-palette-beige-light` | `#f5f0e5` | 선택적 Theme 팔레트: beige-light | Yes | gnb, lnb |
|
||||
| `color.palette.red-danger` | `--hm-sem-palette-red-danger` | `#d32f2f` | 선택적 Theme 팔레트: red-danger | Yes | checkbox, radio, badge, input, button |
|
||||
| `color.palette.white` | `--hm-sem-palette-white` | `#ffffff` | 선택적 Theme 팔레트: white | Yes | checkbox, radio, switch, slider, calendar, pagination, badge, table, input, select, button, gnb, lnb |
|
||||
| `color.palette.black` | `--hm-sem-palette-black` | `#000000` | 선택적 Theme 팔레트: black | Yes | checkbox, switch, select, button, gnb, lnb |
|
||||
| `color.palette.neutral-gray-500` | `--hm-sem-palette-neutral-gray-500` | `#999999` | 선택적 Theme 팔레트: neutral-gray-500 | Yes | input, select |
|
||||
| `color.palette.neutral-gray-500` | `--hm-sem-palette-neutral-gray-500` | `#999999` | 선택적 Theme 팔레트: neutral-gray-500 | Yes | 호환 유지(2026-11-11부터 Placeholder 기본 연결 없음) |
|
||||
| `color.palette.green-tone-1` | `--hm-sem-palette-green-tone-1` | `#1f8265` | 선택적 Theme 팔레트: green-tone-1 | Yes | button, gnb |
|
||||
| `color.palette.green-tone-2` | `--hm-sem-palette-green-tone-2` | `#165c47` | 선택적 Theme 팔레트: green-tone-2 | Yes | button, gnb |
|
||||
| `color.palette.green-tone-3` | `--hm-sem-palette-green-tone-3` | `#114838` | 선택적 Theme 팔레트: green-tone-3 | Yes | button, gnb |
|
||||
@@ -99,8 +99,13 @@
|
||||
| `color.palette.orange-dark-1` | `--hm-sem-palette-orange-dark-1` | `#bc4c00` | 2026-10-11 사용자 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||
| `color.palette.orange-dark-2` | `--hm-sem-palette-orange-dark-2` | `#883700` | 2026-10-11 사용자 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||
| `color.palette.orange-dark-3` | `--hm-sem-palette-orange-dark-3` | `#4f2000` | 2026-10-11 사용자 재확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
|
||||
| `color.palette.gray-900` | `--hm-sem-palette-gray-900` | `#212121` | Foundation Gray 텍스트 단계 (2026-11-11) | Yes | fg-primary 기본 |
|
||||
| `color.palette.gray-800` | `--hm-sem-palette-gray-800` | `#424242` | Foundation Gray 텍스트 단계 (2026-11-11) | Yes | 기본 연결 없음(Strong Secondary 예약) |
|
||||
| `color.palette.gray-700` | `--hm-sem-palette-gray-700` | `#616161` | Foundation Gray 텍스트 단계 (2026-11-11) | Yes | fg-secondary 기본 |
|
||||
| `color.palette.gray-600` | `--hm-sem-palette-gray-600` | `#757575` | Foundation Gray 텍스트 단계 (2026-11-11) | Yes | fg-placeholder 기본 |
|
||||
| `color.palette.gray-500` | `--hm-sem-palette-gray-500` | `#9e9e9e` | Foundation Gray 텍스트 단계 (2026-11-11) | Yes | 기본 연결 없음(Disabled는 opacity 정책 유지) |
|
||||
|
||||
총 94개. 기존 25개 + 핵심 역할 6개 + 선택적 팔레트 52개(2026-10-11 ui_viz 확정 단계 8개 및 사용자 확정 Orange Dark 1·2 추가) + Provisional Generic Status Error 1개 + Brown·Orange Background Stop 10개. 팔레트는 컴포넌트별 계약이 아니며 Primary / Active / Focus를 개별 지정하면 팔레트 alias보다 우선한다. Focus와 Active, Primary와 On Accent는 각각 sibling이며 서로 alias하지 않는다.
|
||||
총 99개(2026-11-11 Foundation Gray 텍스트 단계 5개 추가 전 94개). 기존 25개 + 핵심 역할 6개 + 선택적 팔레트 52개(2026-10-11 ui_viz 확정 단계 8개 및 사용자 확정 Orange Dark 1·2 추가) + Provisional Generic Status Error 1개 + Brown·Orange Background Stop 10개. 팔레트는 컴포넌트별 계약이 아니며 Primary / Active / Focus를 개별 지정하면 팔레트 alias보다 우선한다. Focus와 Active, Primary와 On Accent는 각각 sibling이며 서로 alias하지 않는다.
|
||||
|
||||
## Generic Status Error — Provisional · 2026-10-11
|
||||
|
||||
@@ -113,7 +118,7 @@ Input Error와 Generic Status Error는 별도 역할이다. Input / Alert destru
|
||||
|
||||
기존 v1 입력 73개와 이전 팔레트 보강 이후 81개의 Key·CSS property·type·Default를 유지한다. Orange Dark 1·2는 최신 사용자 확정값으로 공개 입력에 추가하며 Dark 3는 기존 값을 유지한다. 과거 Needs Review 기록은 이력으로 보존하고 현재 기준은 [브랜드 컬러 단계](brand-color-steps.md)를 따른다. 공개 API `isRuntimeTheme(unknown): value is RuntimeTheme`를 제공한다. 이 사전 검증은 DOM을 변경하지 않는다. 기존 `applyRuntimeTheme`의 invalid 입력 시 reset 정책은 유지한다. 편집기는 사전 검증으로 invalid Import를 차단해 마지막 유효 Theme를 보존한다.
|
||||
|
||||
Default 컬러의 운영 원본은 `themes/default.json`(92개 컬러)이다. `scripts/theme-defaults.mjs`가 JSON과 컬러 literal 없는 alias/geometry template에서 package `primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. `contract.ts`의 defaultValue와 `defaultRuntimeTheme`는 생성 데이터를 읽는다. 2개 radius/geometry는 template에서 유지한다. CSS fallback/현재 색을 유지하고 세 가지 최종 명령의 pre hook에서 stale 생성 데이터를 검사한다. Theme-only 산출은 `npm run build:themes`로 분리한다.
|
||||
Default 컬러의 운영 원본은 `themes/default.json`(97개 컬러, 2026-11-11 Gray 5개 추가 전 92개)이다. `scripts/theme-defaults.mjs`가 JSON과 컬러 literal 없는 alias/geometry template에서 package `primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. `contract.ts`의 defaultValue와 `defaultRuntimeTheme`는 생성 데이터를 읽는다. 2개 radius/geometry는 template에서 유지한다. CSS fallback/현재 색을 유지하고 세 가지 최종 명령의 pre hook에서 stale 생성 데이터를 검사한다. Theme-only 산출은 `npm run build:themes`로 분리한다.
|
||||
|
||||
Storybook `Theme/Designer Playground/Editor`는 17개 상위 컬러 역할과 JSON URL/파일 Preview를 제공하는 소비자다. Global decorator에서 `resolveRuntimeTheme(theme, defaultJson)` 후 Production `applyRuntimeTheme(document.documentElement, theme)` → CSS Custom Properties → 기존 Mapping으로 적용한다. JSON Export/Import는 동일 v1 계약이다. 컬러 artifact/편집기만 radius를 거부한다. Production API의 기존 radius/gradient 계약은 유지한다.
|
||||
|
||||
@@ -132,3 +137,18 @@ Dashboard의 `active / hold / done / issue` 업무 상태 계약은 `packages/ui
|
||||
조합 변수 `--hm-sem-bg-brown` / `--hm-sem-bg-orange`는 내부 Derived CSS이며 전체 Gradient 문자열·방향·위치를 Runtime JSON 입력으로 받지 않는다. Runtime에서 한 stop을 바꾸려면 해당 브랜드의 5개 stop을 모두 전달한다. Brown·Orange 두 그룹은 서로 독립적이며 미지정 그룹은 기존 fallback을 따른다. 부분 그룹은 atomic validation으로 거부한다. Global / Scoped 적용과 Reset은 [Runtime Theme](runtime-theme.md)의 기존 계약을 사용한다.
|
||||
|
||||
공통 Background는 기존 Green·Brown/Orange Button·Dashboard 배경에 자동 연결하지 않는다. 2026-10-11 최종 표시 요청으로 [Foundation Color](../../src/foundation/color.stories.tsx)의 기존 `foundation-color--all`은 [브랜드 3개 그룹·24개 컬러칩](brand-color-steps.md#호환성과-storybook)만 표시하며 모든 Gradient 미리보기와 개발용 설명을 제거한다. 실제 공통 Background·Button·Dashboard Gradient의 CSS·Theme·Token과 Runtime 계약은 유지한다. 과거 미정의 및 Gradient 추가 검증 보고서는 당시 이력으로 유지한다.
|
||||
|
||||
## Admin 텍스트 위계 — Foundation Gray · 2026-11-11
|
||||
|
||||
Web·Chart 공통 텍스트 기본값을 Foundation Gray로 바꿨다. 공개 Key·CSS property·type은 그대로이고 기본값만 바뀌었으므로, `color.fg.*`를 지정한 외부 Theme JSON은 이전과 같이 우선한다.
|
||||
|
||||
| 역할 | 공개 Key | 기본값 | 이전 기본값 |
|
||||
|---|---|---|---|
|
||||
| Primary — 제목·본문 | `color.fg.primary` | `#212121` (gray-900) | `#11372d` (green-dark-1) |
|
||||
| Secondary — 설명·보조·캡션 | `color.fg.secondary` | `#616161` (gray-700) | `#6e6759` (warm-neutral-dark) |
|
||||
| Placeholder | `color.fg.placeholder` | `#757575` (gray-600) | `#999999` (neutral-gray-500) |
|
||||
|
||||
- 브랜드 Primitive(`green-dark-1`, `warm-neutral-dark`, `neutral-gray-500`)의 값은 바꾸지 않았다. 텍스트 alias만 Gray를 가리킨다.
|
||||
- Strong Secondary(`gray-800`)·Tertiary·Disabled(`gray-500`) Semantic은 만들지 않았다. Disabled는 기존 opacity 정책을 유지한다.
|
||||
- 대비(흰 면 / 베이지 `#f5f0e5`): #212121 16.10 / 14.17, #616161 6.19 / 5.45, #757575 4.61 / **4.05**. Placeholder는 흰 입력 면에서만 쓰며, 투명 면인 readonly Input은 Secondary를 쓴다(`input.css`).
|
||||
- SW(`--hm-sw-*`)·CENTER·OrgChart Core는 이 기본값을 참조하지 않는다. OrgChart는 자체 `--hm-orgchart-fg-*` 기본값을 같은 값으로 맞췄다([OrgChart 승인 기록 §8](../org-chart/design-approval.md#8-2026-11-11-admin-텍스트-위계-적용)).
|
||||
@@ -321,3 +321,19 @@ HMDesign의 Font Weight는 400(Regular), 500(Medium), 700(Bold)의 3단계만
|
||||
- 신규 컴포넌트·Storybook·AI 생성 코드에 같은 정책을 적용한다. Tailwind `font-normal` / `font-medium` / `font-bold`만 사용하며 `font-semibold`나 750·800 등 추가 단계는 사용하지 않는다.
|
||||
- 폰트 패밀리·크기·행간·자간은 변경하지 않는다. `@font-face`의 100–900은 가변 폰트의 지원 범위이며 UI 사용 단계가 아니다. 폰트 파일과 라이선스는 보존한다.
|
||||
- Typography는 Runtime Theme JSON 공개 allowlist 입력이 아니다. 공개 API·외부 CSS 커스터마이징 계약은 유지하며 과거 보고서의 600 실측값은 당시 검증 기록으로 보존한다.
|
||||
|
||||
## Admin Text Hierarchy — 2026-11-11
|
||||
|
||||
Web / Chart / OrgChart가 공유하는 Admin 텍스트 위계다. SW는 적용 대상이 아니다. 원본 값은 `themes/default.json`, Primitive는 `--hm-gray-900|800|700|600|500`(텍스트용 5단계만), 공개 Runtime Key는 `color.palette.gray-*`다. 상세 계약은 [Runtime Theme Contract](runtime-theme-contract.md#admin-텍스트-위계--foundation-gray--2026-11-11).
|
||||
|
||||
| 역할 | 값 | 연결 | 사용 |
|
||||
|---|---|---|---|
|
||||
| Primary | `#212121` | `--hm-sem-fg-primary` (`--foreground`) | 페이지·카드·Dialog 제목, 본문, 입력값, 테이블 셀, 툴팁 값 |
|
||||
| Strong Secondary | `#424242` | Primitive만 (`--hm-gray-800`) | 현재 Semantic 없음 |
|
||||
| Secondary | `#616161` | `--hm-sem-fg-secondary` (`--muted-foreground`) | 설명, Label, 캡션·요약·빈 상태, 축·범례 |
|
||||
| Tertiary | `#757575` | `--hm-sem-fg-placeholder` | Placeholder 전용. 흰 면에서만 4.5:1 이상(베이지 4.05:1) |
|
||||
| Disabled | `#9e9e9e` | Primitive만 (`--hm-gray-500`) | 색으로 쓰지 않음. Disabled는 opacity 정책 유지 |
|
||||
|
||||
- 유지: 브랜드·액션·상태·반전 텍스트(Button, Link `text-primary`, Tabs, GNB/LNB, Tooltip, Badge default, Destructive), 테이블 헤더 Green(`--hm-table-header-text`).
|
||||
- Placeholder는 Input·Textarea·Select·Command·Combobox·Date Picker 모두 `--hm-sem-fg-placeholder`를 쓴다. 투명 면 readonly Input만 Secondary를 쓴다.
|
||||
- 새 텍스트 역할은 위 Semantic을 쓰고 Gray Primitive를 컴포넌트에서 직접 참조하지 않는다.
|
||||
@@ -157,7 +157,7 @@ HM 지정값이 있는 색상, radius, control height, typography 등은 Theme T
|
||||
- 컴포넌트가 실제 지원하는 default, variant, size, disabled 및 주요 상태를 확인한다. invalid, open 등 해당 컴포넌트에 실제로 있는 상태만 다루고, 없는 상태를 문서화를 위해 만들지 않는다.
|
||||
- Storybook은 구현 Reference Catalog이자 컴포넌트 단위의 검수와 사용법 문서화 도구다. 신규 UI 구현 전 유사 대표/PRIMARY Story를 먼저 확인한다. `Demo/App` 화면은 여러 컴포넌트를 조합한 적용 결과 확인용이며, Demo의 페이지·타일 UI를 Story의 기준으로 쓰지 않는다.
|
||||
- 컴포넌트 Story는 현재 HM 대표 Story의 공통 구조를 따른다. 컴포넌트 이름의 대표 Story 하나에 종류·크기·상태를 한 화면으로 모으고, 같은 구역을 `verification` 태그 Story(예: Roles, Sizes, Brand States)로도 둔다. `verification` Story는 메뉴와 검색에서 기본으로 숨기고(`.storybook/main.ts`의 `tags`) 직접 URL과 사이드바 태그 필터로 연다. HM 전용 Story(Playground, Compatibility)도 같은 태그로 숨긴다. 기본 사이드바에는 컴포넌트가 하위 메뉴 없는 1뎁스 항목 하나로만 보이고(대표 Story 하나만 노출되면 Storybook이 컴포넌트 항목에 합친다), 클릭하면 한 페이지에서 전체 검수 내용이 열린다. Story 이름과 순서, 행·열 구성은 현재 HM 유사 대표 Story의 공통 구성을 먼저 따른다. 외부 ui_viz Story는 제공된 경우에만 충돌하지 않는 참고로 사용한다.
|
||||
- Storybook 페이지 프레임(2026-11-11 확정)은 `src/components/story-page.tsx`·`story-page.css`의 `StoryPage`·`StoryPageHeader`·`StorySection`·`StorySectionGrid`가 공통으로 소유한다. Web·Chart·OrgChart(Admin)의 문서 위계는 H1 페이지 제목 28/36/700 → 설명 14/20/400 보조색(제목 아래 8px) → 24px → H2 섹션 제목 16/24/700과 얇은 구분선 → 12px → 예제이며, 섹션 사이는 24px, `StateBoard`의 H3 라벨은 14/20/500 보조색이고 자체 구분선을 두지 않는다. 위·아래 여백은 16–24px, 좌우 여백은 기존 값(480px까지 16px, 768px부터 60px)을 유지한다. 값은 기존 Component Token을 `--sb-page-*`로 참조하며 새 Design Token이 아니다. 섹션을 나란히 놓을 때는 `StorySectionGrid`를 쓰고, Story별 `!important`·`[&_.sb-page-*]` 덮어쓰기로 간격을 바꾸지 않는다. SW와 노선도(CENTER)는 이 페이지 프레임과 섹션 간격만 공유하고 컴포넌트 크기·글꼴·캔버스·카드 규격은 각자 유지한다. 페이지 제목은 사이드바 이름을 따르며, 한 그룹에 Story가 여럿이면 `그룹 — Story 이름`(예: `SW Header — 화면 너비`)으로 쓴다. 섹션 제목은 페이지 제목과 예제 카드 제목을 반복하지 않는 짧은 변형명(예: Chart `Basic`, `Label`, `Solid`)으로 쓴다.
|
||||
- Storybook 페이지 프레임(2026-11-11 확정)은 `src/components/story-page.tsx`·`story-page.css`의 `StoryPage`·`StoryPageHeader`·`StorySection`·`StorySectionGrid`가 공통으로 소유한다. 2026-11-11 사용자 요청으로 `StoryPageHeader`는 H1 페이지 제목·kicker·설명·SW 공통 안내 문구를 표시하지 않으며(호출과 props는 유지), 페이지는 위 여백(16–24px) 바로 아래 첫 섹션부터 시작한다. 단, Preview 문서 제목이 Story ID라 스크린 리더가 페이지를 식별할 수 있도록 `title`만 시각적으로 숨긴 접근성 전용 H1(`.sb-page-a11y-title`, 1px clip·문서 흐름 밖)로 렌더링한다. 검증 스크립트는 이 H1의 시각 스타일에 의존하지 않고 첫 콘텐츠 블록(`.sb-page-inner > :not(h1)`)으로 렌더링을 기다리며, 페이지 식별은 H1 텍스트로 한다. 제품 `PageHeader` 컴포넌트와는 무관하다. Web·Chart·OrgChart(Admin)의 문서 위계는 H2 섹션 제목 16/24/700과 얇은 구분선 → 12px → 예제이며, 섹션 사이는 24px, `StateBoard`의 H3 라벨은 14/20/500 보조색이고 자체 구분선을 두지 않는다. 위·아래 여백은 16–24px, 좌우 여백은 기존 값(480px까지 16px, 768px부터 60px)을 유지한다. 값은 기존 Component Token을 `--sb-page-*`로 참조하며 새 Design Token이 아니다. 섹션을 나란히 놓을 때는 `StorySectionGrid`를 쓰고, Story별 `!important`·`[&_.sb-page-*]` 덮어쓰기로 간격을 바꾸지 않는다. SW와 노선도(CENTER)는 이 페이지 프레임과 섹션 간격만 공유하고 컴포넌트 크기·글꼴·캔버스·카드 규격은 각자 유지한다. 페이지 제목은 사이드바 이름을 따르며, 한 그룹에 Story가 여럿이면 `그룹 — Story 이름`(예: `SW Header — 화면 너비`)으로 쓴다. 섹션 제목은 페이지 제목과 예제 카드 제목을 반복하지 않는 짧은 변형명(예: Chart `Basic`, `Label`, `Solid`)으로 쓴다.
|
||||
- 대표 Story의 Focus 구역은 pseudo-state 복제 대신 `play` 함수로 실제 요소에 focus를 준다(ui_viz도 Focus는 정적 specimen을 만들지 않는다). hover 같은 상태가 ui_viz에 없으면 `.storybook/pseudo-states.ts`를 쓰지 않는다. 구역 배치 helper는 `src/components/story-board.tsx`의 `StateBoard`를 쓴다.
|
||||
- hover, pressed 같은 pseudo state는 Storybook이 `:hover`/`:active` 규칙을 `data-sb-hover`/`data-sb-active` 규칙으로 복제해 고정한다(`.storybook/pseudo-states.ts`). 복제만 하고 값을 새로 정의하지 않으므로 실제 디자인을 보정하는 용도로 쓰지 않는다. Story에서는 `data-sb-*` 속성만 붙인다.
|
||||
- ui_viz에서 값이 `TBD`인 상태(예: Button Focus)는 별도 specimen을 만들지 않고 키보드로 확인하는 안내만 둔다. ui_viz에 없는 상태(예: Button `aria-invalid`)와 `REFERENCE_ONLY` 항목은 검수 Story에 넣지 않는다. shadcn 호환 API는 `Compatibility` Story로 분리한다.
|
||||
|
||||
@@ -149,3 +149,26 @@ Cell Border Only 디자인 실험은 사용자 결정으로 정식 적용에서
|
||||
Guard는 [P11.1 기록](consistency-review-p11-1.md)과 기존 로컬 `test-results/org-chart-p11-1/guard-actual.json`을 확인한 결과이며 이번 문서 작업에서 재실행하지 않는다. P10 당시 실제 FAIL 5건 등 과거 원본은 보존한다. 정적 PASS·디자인 승인·별도 브라우저 증거로 원본 Runtime NOT VERIFIED를 덮어쓰지 않는다.
|
||||
|
||||
남은 승인과 운영 확인은 [정식 통합 승인 준비 문서](release-readiness-p10-7.md)를 따른다. 이번 P11.2는 문서화 단계이며 제품 코드·디자인 변경, 제품 전체 회귀, Commit·Push·main Merge·배포 실행을 포함하지 않는다.
|
||||
|
||||
## 8. 2026-11-11 Admin 텍스트 위계 적용
|
||||
|
||||
사용자가 Web / Chart / OrgChart 공통 Admin 텍스트 위계(Foundation Gray) 적용을 승인했다(2026-11-11, 결정 D8). §6-4의 사전 확인에 해당하는 승인이며, 아래 범위의 기본값만 바뀐다. OrgChart의 독립 구조는 유지한다. Web `--hm-sem-*` 토큰을 조직도에 연결하지 않고 `--hm-orgchart-*` 기본값만 같은 값으로 맞췄다. 공개 CSS 입력 이름은 바뀌지 않으며, 외부 테마가 지정한 값이 계속 우선한다.
|
||||
|
||||
| 대상 | 이전 | 현재 | 이유 |
|
||||
|---|---|---|---|
|
||||
| `fg-primary` 기본값(제목·본문·카드 이름·테이블·툴바) | `#10243B` | `#212121` | 공통 Primary |
|
||||
| `fg-secondary` 기본값(설명·직위·캡션·빈 상태) | `#596575` | `#616161` | 공통 Secondary |
|
||||
| SVG·compact 카드 직위 | `#797979` (카드 면 `#E0E6F4` 대비 3.48:1) | `fg-secondary` `#616161` (4.95:1) | 대비 4.5:1 미달 보정 |
|
||||
| SVG 카드 이름 fallback, 이름 툴팁 | `#071D3A` / `#323232` | `fg-primary` | 중복 하드코딩 정리(`card-color` 입력 우선은 유지) |
|
||||
| readOnly·Overview 인원 패널·Workdesk 표·소속 막대 | `#18253A` / `#223149` / `#1C2534` / `#071D3A` | `fg-primary` | 중복 하드코딩 정리 |
|
||||
| Core 팝업 닫기 아이콘 마지막 fallback | `#0A221D` | `#212121` | HM 기본값 변경에 맞춤(`--foreground` 체인 유지) |
|
||||
|
||||
유지한 항목: 브랜드 블루(대표자 직위·테이블 헤더·on-accent `#224387`, action `#45639F`), 6개 부서 Gradient·Role tag 색, 7개 회사 컬러와 흰색 그룹 제목, 파견·휴직 전용 회색(`#555F6A` / `#3E4650`, 대비 5.04 / 7.41), 인원 패널 제목의 흰 글자, 노드 배경, 연결선, Hub 라벨 artwork, Department Colors의 미승인 후보 `#10243B` 표본(진단용, 기본값 아님). React `HMEmployeeDialog`는 HM Dialog 토큰을 쓰므로 Core 팝업과 같은 `#212121` / `#616161`이 되었다.
|
||||
|
||||
| 접근성 항목 | 상태 |
|
||||
|---|---|
|
||||
| 카드 직위 `#797979` 대비 미달 | 수정함 |
|
||||
| 비readOnly 상태 그룹 제목(15px Bold, `#87909C → #68727E` 그라데이션) | **보류.** 텍스트 `#212121`은 4.98 → 3.29:1, 흰색은 3.23 → 4.89:1이라 글자색만으로 전 구간 4.5:1을 확보할 수 없다. 배경(상태색)이나 글자 크기를 바꿔야 하므로 별도 결정이다. readOnly 제목은 20px Bold 대형 텍스트라 흰색 3.23:1 이상(대형 기준 3:1)이다. |
|
||||
| 삼안·장헌산업 흰색 그룹 제목(§4) | 미해결 유지 |
|
||||
|
||||
대비는 선언색 기준 계산값이다. 2026-11-11 Storybook(전체 구성·개요·확장 조직도·Cards·Status Summary)에서 계산된 글자색이 위 값과 일치함을 확인했다. SVG 카드 artwork 위 픽셀 대비는 측정하지 않았다.
|
||||
@@ -46,8 +46,11 @@ page.setDefaultTimeout(20000);
|
||||
async function settle(target = page) {
|
||||
await target.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); });
|
||||
}
|
||||
// The Storybook page title is a visually hidden H1 (2026-11-11), so readiness waits for the first visible content block
|
||||
// of the StoryPage frame, and page identity reads the H1 text (the Story's accessible page heading).
|
||||
const pageContent = '.sb-page-inner > :not(h1)', pageHeading = '.sb-page-inner > h1';
|
||||
async function ready(expected) {
|
||||
await page.locator('.sb-page-title').waitFor();
|
||||
await page.locator(pageContent).first().waitFor();
|
||||
await page.waitForFunction(expected => {
|
||||
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
|
||||
if (!hosts.length) return false;
|
||||
@@ -76,10 +79,8 @@ async function commonStyle() {
|
||||
const properties = {
|
||||
'.sb-page': ['fontFamily', 'color', 'paddingTop', 'paddingBottom', 'boxSizing', 'width'],
|
||||
'.sb-page-inner': ['width', 'boxSizing'],
|
||||
'.sb-page-header': ['paddingBottom', 'borderBottomWidth', 'borderBottomColor', 'fontSize', 'lineHeight'],
|
||||
'.sb-page-kicker': ['fontSize', 'fontWeight', 'color', 'marginBottom'],
|
||||
'.sb-page-title': ['fontSize', 'fontWeight', 'lineHeight', 'marginTop', 'marginBottom'],
|
||||
'.sb-page-description': ['fontSize', 'fontWeight', 'lineHeight', 'marginBottom'],
|
||||
// The visible header (kicker, title, description) is no longer rendered; the page H1 stays visually hidden.
|
||||
'.sb-page-inner > h1': ['position', 'width', 'height', 'overflow', 'clipPath'],
|
||||
'.sb-page-section': ['marginTop', 'scrollMarginTop'],
|
||||
'.sb-page-section-header': ['paddingBottom', 'marginBottom', 'borderBottomWidth', 'borderBottomColor'],
|
||||
'.sb-page-section-title': ['fontSize', 'lineHeight', 'fontWeight', 'marginTop', 'marginBottom'],
|
||||
@@ -105,7 +106,7 @@ async function pageGeometry() {
|
||||
return page.evaluate(() => {
|
||||
const rect = element => { if (!element) return null; const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; };
|
||||
const documentGeometry = { scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, innerWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight };
|
||||
const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-title')?.textContent };
|
||||
const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-inner > h1')?.textContent };
|
||||
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||||
const wrapper = shadow?.querySelector('.hm-orgchart-composition');
|
||||
const stage = shadow?.querySelector('.hm-orgchart-stage');
|
||||
@@ -159,10 +160,10 @@ try {
|
||||
await manager.waitForFunction(() => {
|
||||
const preview = document.querySelector('#storybook-preview-iframe')?.contentDocument;
|
||||
const hosts = [...preview?.querySelectorAll('.hm-orgchart-component-host') ?? []];
|
||||
return preview?.querySelector('.sb-page-title')?.textContent === 'Overview' && hosts.length > 0
|
||||
return preview?.querySelector('.sb-page-inner > h1')?.textContent === 'Overview' && hosts.length > 0
|
||||
&& hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet));
|
||||
});
|
||||
await manager.frameLocator('#storybook-preview-iframe').locator('.sb-page-title').evaluate(async element => {
|
||||
await manager.frameLocator('#storybook-preview-iframe').locator(pageHeading).evaluate(async element => {
|
||||
const document = element.ownerDocument, view = document.defaultView;
|
||||
await document.fonts.ready;
|
||||
for (let i = 0; i < 5; i++) await new Promise(resolve => view.requestAnimationFrame(resolve));
|
||||
@@ -198,7 +199,7 @@ try {
|
||||
await gotoStory(story);
|
||||
const rendered = await page.evaluate(({ selectors }) => {
|
||||
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
|
||||
return { title: document.querySelector('.sb-page-title')?.textContent, hosts: hosts.length,
|
||||
return { title: document.querySelector('.sb-page-inner > h1')?.textContent, hosts: hosts.length,
|
||||
specimens: Object.fromEntries(selectors.map(selector => [selector, hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll(selector).length, 0)])),
|
||||
employees: hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll('.hm-orgchart-employee').length, 0),
|
||||
autoLayout: hosts.find(host => host.classList.contains('hm-orgchart-host'))?.shadowRoot.querySelector('.hm-orgchart-expanded')?.dataset.autoLayout };
|
||||
@@ -214,7 +215,7 @@ try {
|
||||
for (const id of patterns) {
|
||||
await page.setViewportSize({ width: 1440, height: 1100 });
|
||||
await page.goto(`${origin}/iframe.html?id=${id}&viewMode=story`);
|
||||
await page.locator('.sb-page-title').waitFor(); await settle();
|
||||
await page.locator(pageContent).first().waitFor(); await settle();
|
||||
references.push({ id, style: await commonStyle() });
|
||||
}
|
||||
const reference = references[0].style;
|
||||
@@ -268,7 +269,7 @@ try {
|
||||
await section('full-repair', async () => {
|
||||
await page.setViewportSize({ width: 1440, height: 1100 });
|
||||
await page.goto(`${origin}/iframe.html?id=${patterns[0]}&viewMode=story`);
|
||||
await page.locator('.sb-page-title').waitFor(); await settle();
|
||||
await page.locator(pageContent).first().waitFor(); await settle();
|
||||
const reference = await commonStyle();
|
||||
await gotoStory(stories[2]);
|
||||
const style = await commonStyle();
|
||||
|
||||
@@ -39,7 +39,8 @@ async function settle() {
|
||||
async function goto(name, size) {
|
||||
await page.setViewportSize(size);
|
||||
await page.goto(`${origin}/iframe.html?id=${stories[name]}&viewMode=story`);
|
||||
await page.locator('.sb-page-title').waitFor();
|
||||
// The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead.
|
||||
await page.locator('.sb-page-inner > :not(h1)').first().waitFor();
|
||||
await page.waitForFunction(() => {
|
||||
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
|
||||
return hosts.length === 1 && hosts.every(host => host.shadowRoot && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet));
|
||||
@@ -86,7 +87,7 @@ async function snapshot() {
|
||||
const css = getComputedStyle(element);
|
||||
return { scrollWidth: element.scrollWidth, clientWidth: element.clientWidth, scrollHeight: element.scrollHeight, clientHeight: element.clientHeight, left: element.scrollLeft, top: element.scrollTop, overflowX: css.overflowX, overflowY: css.overflowY };
|
||||
};
|
||||
const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-title', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))]));
|
||||
const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-inner > h1', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))]));
|
||||
// Page/section height is content-dependent; typography, borders and gutters are invariant.
|
||||
for (const value of Object.values(common)) if (value) for (const key of ['height', 'minHeight']) delete value[key];
|
||||
const documentScroll = { viewportWidth: innerWidth, viewportHeight: innerHeight, root: scroll(document.documentElement), body: scroll(document.body), scrollingElement: scroll(document.scrollingElement) };
|
||||
@@ -145,7 +146,7 @@ async function snapshot() {
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: document.querySelector('.sb-page-title')?.textContent, common, documentScroll, departments, unwanted,
|
||||
title: document.querySelector('.sb-page-inner > h1')?.textContent, common, documentScroll, departments, unwanted,
|
||||
optionsPresent: [...document.querySelectorAll('button')].filter(visible).some(button => /긴 이름 검증/.test(button.textContent)),
|
||||
sectionTitles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), fixed,
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ async function settle() {
|
||||
async function open(size) {
|
||||
await page.setViewportSize(size);
|
||||
await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`);
|
||||
await page.locator('.sb-page-title').waitFor();
|
||||
// The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead.
|
||||
await page.locator('.sb-page-inner > :not(h1)').first().waitFor();
|
||||
await page.waitForFunction(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||||
return shadow?.querySelector('.hm-orgchart-stage[data-workdesk-artwork]') && !shadow.querySelector('[aria-busy="true"]') && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet);
|
||||
|
||||
@@ -39,7 +39,8 @@ page.setDefaultTimeout(20000);
|
||||
let expectedHosts = 1;
|
||||
async function settle() { await page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); }); }
|
||||
async function ready(requirePage = true) {
|
||||
if (requirePage) await page.locator('.sb-page-title').waitFor();
|
||||
// The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead.
|
||||
if (requirePage) await page.locator('.sb-page-inner > :not(h1)').first().waitFor();
|
||||
await page.waitForFunction(expected => {
|
||||
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
|
||||
return hosts.length === expected && hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet) && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('img')].every(image => image.complete));
|
||||
@@ -111,8 +112,8 @@ async function snapshot() {
|
||||
cards: [...expanded.querySelectorAll('.hm-orgchart-employee')].map(card => { const css = getComputedStyle(card), direction = card.dataset.connectionDirection; return { company: card.dataset.companyId, direction, width: card.offsetWidth, height: card.offsetHeight, border: hex(css[direction === 'left' ? 'borderLeftColor' : direction === 'right' ? 'borderRightColor' : 'borderTopColor']), image: Boolean(card.querySelector('img')) }; }),
|
||||
connectors: expanded.querySelectorAll('svg,.edge,.lines').length,
|
||||
} : null;
|
||||
const common = Object.fromEntries(['.sb-page-title', '.sb-page-description', '.sb-page-section-title', '.sb-page-section-header'].map(selector => [selector, style(document.querySelector(selector))]));
|
||||
return { title: document.querySelector('.sb-page-title')?.textContent, titles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), document: { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, common, summary, affiliation, logos, departments, fixed, auto };
|
||||
const common = Object.fromEntries(['.sb-page-inner > h1', '.sb-page-section-title', '.sb-page-section-header'].map(selector => [selector, style(document.querySelector(selector))]));
|
||||
return { title: document.querySelector('.sb-page-inner > h1')?.textContent, titles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), document: { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, common, summary, affiliation, logos, departments, fixed, auto };
|
||||
});
|
||||
}
|
||||
function noHorizontalOverflow(state) { return state.document.scrollWidth <= state.document.width + 1; }
|
||||
|
||||
@@ -125,5 +125,7 @@ npm run storybook # Chart 그룹: Bar / Line / Donut / P
|
||||
## 제한
|
||||
|
||||
- 차트 전용 HM 디자인 규격은 아직 없습니다. 색은 기존 HM 팔레트 Semantic 토큰의 별칭이며(`src/chart.css`), 차트 Token은 이 패키지 안에서만 정의합니다.
|
||||
- 글자색은 Admin 텍스트 위계를 따릅니다: 툴팁 값·중앙값·막대 이름/값은 `--hm-sem-fg-primary`(#212121), 축 눈금(Cartesian·Radar 기본 축 이름)·범례·툴팁 이름·중앙 캡션은 `--hm-chart-axis-text` → `--hm-sem-fg-secondary`(#616161)입니다.
|
||||
- 접근성 후속 항목(2026-11-11 기록, 미수정): Line·Area 포인트 값 Label은 시리즈 색을 그대로 씁니다. 기본 시리즈 4 `#ECD4A2`(흰색 대비 1.45:1)·5 `#9A9284`(3.08:1)는 4.5:1 미만입니다. 시리즈 색은 유지하며 Label 색 정책은 별도 결정입니다.
|
||||
- `@hm-design/chart/examples`의 `ExampleCard`는 공개 Export가 아닙니다(예제 카드를 통해서만 사용).
|
||||
- Dark 테마 없음. Runtime Theme(`@hm-design/ui/theme`)의 팔레트 override는 차트 별칭을 통해 반영됩니다.
|
||||
@@ -42,7 +42,11 @@
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
[data-slot="chart"] .recharts-cartesian-axis-tick text {
|
||||
[data-slot="chart"] .recharts-cartesian-axis-tick text,
|
||||
/* Recharts 3.8 renders tick text as .recharts-cartesian-axis-tick-value outside the tick group, so the rule above no longer matched. */
|
||||
[data-slot="chart"] .recharts-cartesian-axis-tick-value,
|
||||
/* Radar default axis names: Recharts would otherwise fill them with its own #808080. */
|
||||
[data-slot="chart"] .recharts-polar-angle-axis-tick-value {
|
||||
fill: var(--hm-chart-axis-text);
|
||||
}
|
||||
[data-slot="chart"] .recharts-cartesian-grid line {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
| Fixed readOnly / Full Composition 배경 | `#F5F8FB`(`bg-content` 기본) |
|
||||
| 회사 메인 컬러(Auto 그룹 제목·카드 강조) | `hanmac` 한맥기술 `#CB2E28`, `saman` 삼안 `#F08300`, `jangheon-industry` 장헌산업 `#4580A6`, `jangheon` (주)장헌 `#1D418D`, `ptc` 피티씨 `#84338E`, `halla` 한라산업개발 `#0068B7`, `baron` 바론컨설턴트 `#0C367F` |
|
||||
| 그룹 제목 글자 | 흰색 |
|
||||
| 기본 글자 / 보조 글자 | `#212121` / `#616161`(`fg-primary` / `fg-secondary` 기본, 2026-11-11 Admin 텍스트 위계) |
|
||||
| 카드 직위 / 대표자 직위 | `#616161`(`fg-secondary`) / `#224387`(브랜드 블루) |
|
||||
| Auto 연결선 | `#738399`(`expanded-line-color` 기본) |
|
||||
| Fixed 부서 Gradient | 6종(`business-management`, `business-planning`, `talent-growth`, `integrated-system`, `design-planning`, `solution-sales`). 회사색과 별개 — 서로 대체하지 않음 |
|
||||
| 글꼴 | Noto Sans KR(`--hm-orgchart-font-family`로 override 가능). 호스트가 font-face 등록 |
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
--hm-orgchart-resolved-bg-content: var(--hm-orgchart-bg-content, #f4f7fb);
|
||||
--hm-orgchart-resolved-bg-subtle: var(--hm-orgchart-bg-subtle, #e9edf3);
|
||||
--hm-orgchart-resolved-bg-brand: var(--hm-orgchart-bg-brand, linear-gradient(180deg, #45639f 0%, #809dd7 100%));
|
||||
--hm-orgchart-resolved-fg-primary: var(--hm-orgchart-fg-primary, #10243b);
|
||||
--hm-orgchart-resolved-fg-secondary: var(--hm-orgchart-fg-secondary, #596575);
|
||||
/* Text defaults follow the Admin Foundation Gray hierarchy (2026-11-11): Gray 900 / Gray 700. Was #10243b / #596575. */
|
||||
--hm-orgchart-resolved-fg-primary: var(--hm-orgchart-fg-primary, #212121);
|
||||
--hm-orgchart-resolved-fg-secondary: var(--hm-orgchart-fg-secondary, #616161);
|
||||
--hm-orgchart-resolved-stroke-default: var(--hm-orgchart-stroke-default, #bac7d7);
|
||||
--hm-orgchart-resolved-stroke-input-default: var(--hm-orgchart-stroke-input-default, #d3dce8);
|
||||
--hm-orgchart-resolved-stroke-input-focus: var(--hm-orgchart-stroke-input-focus, #45639f);
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"])[data-card-asset-size="leader"] .hm-orgchart-avatar { flex-basis: 32px; width: 32px; height: 32px; }
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"])[data-card-asset-size="representative"] .hm-orgchart-avatar { flex-basis: 36px; width: 36px; height: 36px; }
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) {
|
||||
--hm-orgchart-person-position-color: #797979;
|
||||
/* Secondary text on the light card artwork; #797979 was 3.48:1 on #E0E6F4. */
|
||||
--hm-orgchart-person-position-color: var(--hm-orgchart-resolved-fg-secondary);
|
||||
--hm-orgchart-person-position-size: 15px;
|
||||
--hm-orgchart-person-position-line-height: 18px;
|
||||
--hm-orgchart-person-position-letter-spacing: -0.6px;
|
||||
@@ -246,7 +247,7 @@
|
||||
letter-spacing: var(--hm-orgchart-person-position-letter-spacing);
|
||||
}
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name {
|
||||
color: var(--hm-orgchart-card-color, #071D3A);
|
||||
color: var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary));
|
||||
font-size: var(--hm-orgchart-person-name-size);
|
||||
font-weight: 700;
|
||||
line-height: var(--hm-orgchart-person-name-line-height);
|
||||
@@ -255,7 +256,7 @@
|
||||
text-overflow: clip;
|
||||
}
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name[data-name-overflow="true"] { text-overflow: ellipsis; }
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]):is([data-card-asset-size="employee"], [data-card-layout="compact"]) .hm-orgchart-person-name { color: var(--hm-orgchart-card-color, #323232); }
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]):is([data-card-asset-size="employee"], [data-card-layout="compact"]) .hm-orgchart-person-name { color: var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)); }
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name:focus-visible { outline: 2px solid var(--hm-orgchart-resolved-stroke-input-focus); outline-offset: 1px; }
|
||||
.hm-orgchart-employee[data-card-asset-size="employee"] .hm-orgchart-affiliation-mark { top: 2px; right: 1px; width: 20px; height: 20px; min-height: 20px; }
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]):is([data-card-asset-size="employee"], [data-card-layout="compact"]) .hm-orgchart-affiliation-fallback { font-size: 6px; white-space: nowrap; }
|
||||
@@ -331,5 +332,5 @@
|
||||
/* Status is retained for assistive reading; the specified two-line card
|
||||
composition has no third status row. Supplementary status cards are intact. */
|
||||
.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-status-label { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
.hm-orgchart-person-name-tooltip { position: fixed; z-index: 10; box-sizing: border-box; max-width: min(320px, calc(100vw - 16px)); padding: 6px 8px; border: 1px solid #224387; border-radius: 4px; background: #ffffff; color: #071D3A; font-family: var(--hm-orgchart-resolved-font-family); font-size: 14px; line-height: 20px; overflow-wrap: anywhere; pointer-events: auto; }
|
||||
.hm-orgchart-person-name-tooltip { position: fixed; z-index: 10; box-sizing: border-box; max-width: min(320px, calc(100vw - 16px)); padding: 6px 8px; border: 1px solid #224387; border-radius: 4px; background: #ffffff; color: var(--hm-orgchart-resolved-fg-primary); font-family: var(--hm-orgchart-resolved-font-family); font-size: 14px; line-height: 20px; overflow-wrap: anywhere; pointer-events: auto; }
|
||||
.hm-orgchart-person-name-tooltip[hidden] { display: none; }
|
||||
@@ -35,7 +35,7 @@
|
||||
.hm-orgchart-employee-dialog-close {
|
||||
/* Match HM Dialog's ghost / icon-sm Button without a React or theme dependency.
|
||||
Fallbacks are the declared HM defaults (theme/default.css and primitives.css). */
|
||||
--hm-orgchart-dialog-close-ink: var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #0a221d)));
|
||||
--hm-orgchart-dialog-close-ink: var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #212121)));
|
||||
--hm-orgchart-dialog-close-hover: var(--hm-orgchart-dialog-close-hover-background, var(--muted, var(--hm-sem-bg-subtle, #f4f1ea)));
|
||||
--hm-orgchart-dialog-close-ring: var(--hm-orgchart-dialog-focus-color, var(--ring, var(--hm-sem-stroke-input-focus, #266651)));
|
||||
position: absolute;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
color: #18253a;
|
||||
color: var(--hm-orgchart-resolved-fg-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
:is(.hm-orgchart-composition[data-display-mode="readOnly"], .hm-orgchart-summary-preview) .hm-orgchart-organization-totals-title {
|
||||
@@ -38,7 +38,7 @@
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
color: #18253a;
|
||||
color: var(--hm-orgchart-resolved-fg-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
.hm-orgchart-workdesk-position-table th:first-child { padding: 0 7px; }
|
||||
.hm-orgchart-workdesk-position-table :is(th, td) + :is(th, td) { border-left: 0.8px solid #d7e1ea; }
|
||||
.hm-orgchart-workdesk-position-table thead { color: #223149; }
|
||||
.hm-orgchart-workdesk-position-table thead { color: var(--hm-orgchart-resolved-fg-primary); }
|
||||
.hm-orgchart-workdesk-position-table thead th,
|
||||
.hm-orgchart-workdesk-position-table tfoot :is(th, td) { background: #f2f6fa; }
|
||||
.hm-orgchart-workdesk-position-table tfoot :is(th, td) { height: 20px; }
|
||||
@@ -79,7 +79,7 @@
|
||||
border-radius: 10.7px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 5.4px 14.3px rgba(44, 68, 96, 0.08);
|
||||
color: #1c2534;
|
||||
color: var(--hm-orgchart-resolved-fg-primary);
|
||||
}
|
||||
:is(.hm-orgchart-composition[data-display-mode="readOnly"], .hm-orgchart-summary-preview) .hm-orgchart-affiliation-bar-list {
|
||||
flex-wrap: nowrap;
|
||||
@@ -114,7 +114,7 @@
|
||||
}
|
||||
:is(.hm-orgchart-composition[data-display-mode="readOnly"], .hm-orgchart-summary-preview) .hm-orgchart-affiliation-bar .hm-orgchart-affiliation-mark img { object-fit: cover; }
|
||||
:is(.hm-orgchart-composition[data-display-mode="readOnly"], .hm-orgchart-summary-preview) .hm-orgchart-affiliation-bar-total {
|
||||
color: #071d3a;
|
||||
color: var(--hm-orgchart-resolved-fg-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ try {
|
||||
await open('color','색상');await open('layer','레이어')
|
||||
check('opening another dropdown closes the previous one',await header.locator('.hm-sw-header__color-menu').count()===0&&await header.locator('.hm-sw-header__layer-panel').count()===1)
|
||||
await page.keyboard.press('Escape');check('Escape closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0)
|
||||
await open('layer','레이어');await page.locator('.sb-page-title').click();check('outside click closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0)
|
||||
await open('layer','레이어');await page.locator('.sb-page').click({position:{x:4,y:4}});check('outside click closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0)
|
||||
|
||||
const barState=await page.locator('.hm-sw-property-bar').evaluate(bar=>({object:bar.querySelector('.hm-sw-property-bar__object').textContent,order:[...bar.querySelectorAll('[data-property-id]')].map(e=>e.dataset.propertyId),inputs:bar.querySelectorAll('input').length,focusable:[...bar.querySelectorAll('output')].filter(e=>e.tabIndex>=0).length,colors:[...new Set([...bar.querySelectorAll('output')].map(e=>getComputedStyle(e).color))]}))
|
||||
check('Bar: object text 원, order 반지름 / 지름 / 원주 / 면적, read-only red text (edited in the panel) without input or focus stop',barState.object==='원'&&barState.order.join()==='radius,diameter,circumference,area'&&barState.inputs===0&&barState.focusable===0&&barState.colors.join()==='rgb(230, 4, 3)',barState)
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
| 역할 | 값 | 변수 |
|
||||
|---|---|---|
|
||||
| Green(주색) | `#164434` | `--hm-sem-palette-green-default` |
|
||||
| Green 진한 글자 | `#11372D` | `--hm-sem-palette-green-dark-1` |
|
||||
| Green Dark 1 | `#11372D` | `--hm-sem-palette-green-dark-1` (브랜드 gradient·테두리. 글자 기본값 아님) |
|
||||
| Green Focus·Active | `#266651` | `--hm-sem-palette-green-light-2` |
|
||||
| Green Tint(강조 면) | `#BDDFD4` | `--hm-sem-palette-green-tint` |
|
||||
| Beige(콘텐츠 면) | `#F5F0E5` | `--hm-sem-palette-beige-light` |
|
||||
| Warm neutral | dark `#6E6759`, mid `#9A9284`, pale `#DEDBD2`, muted `#F4F1EA` | `--hm-sem-palette-warm-neutral-dark` / `-mid` / `-pale` / `-muted` |
|
||||
| Red | `#D32F2F` | `--hm-sem-palette-red-danger` |
|
||||
| Gray placeholder | `#999999` | `--hm-sem-palette-neutral-gray-500` |
|
||||
| Foundation Gray(글자) | 900 `#212121`, 800 `#424242`, 700 `#616161`, 600 `#757575`, 500 `#9E9E9E` | `--hm-sem-palette-gray-900` / `-800` / `-700` / `-600` / `-500` |
|
||||
| Neutral gray(호환) | `#999999` | `--hm-sem-palette-neutral-gray-500` — 기본 연결 없음 |
|
||||
| White / Black | `#FFFFFF` / `#000000` | `--hm-sem-palette-white` / `--hm-sem-palette-black` |
|
||||
|
||||
Brown·Orange 계열 팔레트(`--hm-sem-palette-brown-*`, `--hm-sem-palette-orange-*`)도 선언되어 있으며 Button `secondary`(Brown)·`modify`(Orange) 역할에 쓰입니다. 새 UI는 팔레트보다 §2의 Semantic 변수를 우선합니다.
|
||||
@@ -31,9 +32,9 @@ Brown·Orange 계열 팔레트(`--hm-sem-palette-brown-*`, `--hm-sem-palette-ora
|
||||
| 카드·팝업 표면 | `--hm-sem-bg-surface` | White |
|
||||
| 보조 면 | `--hm-sem-bg-subtle` | warm-neutral-muted |
|
||||
| 브랜드 배경(gradient) | `--hm-sem-bg-brand`, `--hm-sem-bg-brown`, `--hm-sem-bg-orange` | 공통 세로 gradient 배경(소비 화면 배경용). 패키지 컴포넌트(GNB·Button·Dashboard)에는 자동 연결되지 않음 — GNB 면은 `--hm-gnb-background`(저장소 `docs/guidelines/runtime-theme-contract.md` "Brown·Orange 공통 Background") |
|
||||
| 기본 글자 | `--hm-sem-fg-primary` | green-dark-1 |
|
||||
| 보조 글자 | `--hm-sem-fg-secondary` | warm-neutral-dark |
|
||||
| placeholder | `--hm-sem-fg-placeholder` | gray-500 |
|
||||
| 기본 글자(제목·본문) | `--hm-sem-fg-primary` | gray-900 `#212121` |
|
||||
| 보조 글자(설명·Label·캡션) | `--hm-sem-fg-secondary` | gray-700 `#616161` |
|
||||
| placeholder | `--hm-sem-fg-placeholder` | gray-600 `#757575`(흰 면 전용, 베이지 면에서는 보조 글자) |
|
||||
| 일반 테두리 | `--hm-sem-stroke-default` | warm-neutral-mid |
|
||||
| 입력 테두리 | `--hm-sem-stroke-input-default` | warm-neutral-pale |
|
||||
| Focus 테두리 | `--hm-sem-stroke-input-focus` | green-light-2 |
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
--hm-combobox-background: var(--hm-sem-bg-surface);
|
||||
--hm-combobox-text: var(--hm-sem-fg-primary);
|
||||
/* Readable empty-value guidance on the white trigger; preserve shared tokens. */
|
||||
--hm-combobox-placeholder: var(--hm-sem-fg-secondary);
|
||||
/* Shared placeholder (Gray 600, 4.61:1 on the white trigger) since 2026-11-11; was fg-secondary for contrast. */
|
||||
--hm-combobox-placeholder: var(--hm-sem-fg-placeholder);
|
||||
--hm-combobox-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-combobox-focus-border-color: var(--hm-sem-stroke-input-focus);
|
||||
--hm-combobox-seam-color: var(--hm-sem-stroke-input-default);
|
||||
|
||||
@@ -98,6 +98,12 @@ export const runtimeThemeContract = {
|
||||
"color.palette.orange-dark-1": { property: "--hm-sem-palette-orange-dark-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-dark-1"], type: "color" },
|
||||
"color.palette.orange-dark-2": { property: "--hm-sem-palette-orange-dark-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-dark-2"], type: "color" },
|
||||
"color.palette.orange-dark-3": { property: "--hm-sem-palette-orange-dark-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-dark-3"], type: "color" },
|
||||
// Foundation Gray text steps (Admin Web / Chart text hierarchy, 2026-11-11). Additive; existing keys unchanged.
|
||||
"color.palette.gray-900": { property: "--hm-sem-palette-gray-900", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-900"], type: "color" },
|
||||
"color.palette.gray-800": { property: "--hm-sem-palette-gray-800", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-800"], type: "color" },
|
||||
"color.palette.gray-700": { property: "--hm-sem-palette-gray-700", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-700"], type: "color" },
|
||||
"color.palette.gray-600": { property: "--hm-sem-palette-gray-600", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-600"], type: "color" },
|
||||
"color.palette.gray-500": { property: "--hm-sem-palette-gray-500", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-500"], type: "color" },
|
||||
} as const
|
||||
|
||||
export type RuntimeThemeToken = keyof typeof runtimeThemeContract
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
--hm-date-picker-background: var(--hm-sem-bg-surface);
|
||||
--hm-date-picker-text: var(--hm-sem-fg-primary);
|
||||
/* Readable empty-value guidance on the white trigger; preserve shared tokens. */
|
||||
--hm-date-picker-placeholder: var(--hm-sem-fg-secondary);
|
||||
/* Shared placeholder (Gray 600, 4.61:1 on the white trigger) since 2026-11-11; was fg-secondary for contrast. */
|
||||
--hm-date-picker-placeholder: var(--hm-sem-fg-placeholder);
|
||||
--hm-date-picker-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-date-picker-focus-border-color: var(--hm-sem-stroke-input-focus);
|
||||
--hm-date-picker-icon-size: 18px;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
export const runtimeThemeDefaults = {
|
||||
"--hm-sem-bg-content": "#f5f0e5",
|
||||
"--hm-sem-bg-surface": "#ffffff",
|
||||
"--hm-sem-fg-primary": "#11372d",
|
||||
"--hm-sem-fg-secondary": "#6e6759",
|
||||
"--hm-sem-fg-placeholder": "#999999",
|
||||
"--hm-sem-fg-primary": "#212121",
|
||||
"--hm-sem-fg-secondary": "#616161",
|
||||
"--hm-sem-fg-placeholder": "#757575",
|
||||
"--hm-sem-action-primary": "#164434",
|
||||
"--hm-sem-action-cancel": "#6e6759",
|
||||
"--hm-sem-action-destructive": "#d32f2f",
|
||||
@@ -94,5 +94,10 @@ export const runtimeThemeDefaults = {
|
||||
"--hm-sem-palette-orange-light-1": "#eb5f00",
|
||||
"--hm-sem-palette-orange-dark-1": "#bc4c00",
|
||||
"--hm-sem-palette-orange-dark-2": "#883700",
|
||||
"--hm-sem-palette-orange-dark-3": "#4f2000"
|
||||
"--hm-sem-palette-orange-dark-3": "#4f2000",
|
||||
"--hm-sem-palette-gray-900": "#212121",
|
||||
"--hm-sem-palette-gray-800": "#424242",
|
||||
"--hm-sem-palette-gray-700": "#616161",
|
||||
"--hm-sem-palette-gray-600": "#757575",
|
||||
"--hm-sem-palette-gray-500": "#9e9e9e"
|
||||
} as const
|
||||
@@ -9,10 +9,11 @@
|
||||
--hm-color-surface-default: var(--hm-sem-palette-white);
|
||||
--hm-color-surface-content: var(--hm-sem-palette-beige-light);
|
||||
--hm-color-surface-subtle: var(--hm-sem-palette-warm-neutral-muted);
|
||||
--hm-color-text-primary: var(--hm-sem-palette-green-dark-1);
|
||||
--hm-color-text-secondary: var(--hm-sem-palette-warm-neutral-dark);
|
||||
/* Admin text hierarchy (2026-11-11): Primary Gray 900, Secondary Gray 700, Placeholder Gray 600. Brand primitives keep their values. */
|
||||
--hm-color-text-primary: var(--hm-sem-palette-gray-900);
|
||||
--hm-color-text-secondary: var(--hm-sem-palette-gray-700);
|
||||
--hm-color-text-inverse: var(--hm-sem-palette-white);
|
||||
--hm-color-text-placeholder: var(--hm-sem-palette-neutral-gray-500);
|
||||
--hm-color-text-placeholder: var(--hm-sem-palette-gray-600);
|
||||
--hm-color-border-default: var(--hm-sem-palette-warm-neutral-mid);
|
||||
--hm-color-border-control: var(--hm-sem-palette-warm-neutral-pale);
|
||||
/* Common Focus color. Trial value: Light 2 (#266651), pending user confirmation. Was --hm-color-primary. */
|
||||
@@ -237,3 +238,12 @@
|
||||
--hm-sem-palette-orange-dark-2: var(--hm-orange-dark-2);
|
||||
--hm-sem-palette-orange-dark-3: var(--hm-orange-dark-3);
|
||||
}
|
||||
|
||||
/* Foundation Gray text steps; only the text aliases above reference them. */
|
||||
:root, [data-hm-theme] {
|
||||
--hm-sem-palette-gray-900: var(--hm-gray-900);
|
||||
--hm-sem-palette-gray-800: var(--hm-gray-800);
|
||||
--hm-sem-palette-gray-700: var(--hm-gray-700);
|
||||
--hm-sem-palette-gray-600: var(--hm-gray-600);
|
||||
--hm-sem-palette-gray-500: var(--hm-gray-500);
|
||||
}
|
||||
@@ -99,6 +99,8 @@
|
||||
border-width: 0;
|
||||
padding-inline: 0;
|
||||
background: transparent;
|
||||
/* Transparent face can sit on the beige content surface, where Gray 600 is below 4.5:1. */
|
||||
--hm-input-placeholder: var(--hm-sem-fg-secondary);
|
||||
}
|
||||
[data-slot="input"][readonly]:focus-visible {
|
||||
border-width: 0;
|
||||
|
||||
@@ -58,3 +58,12 @@
|
||||
--hm-orange-dark-2: #883700;
|
||||
--hm-orange-dark-3: #4f2000;
|
||||
}
|
||||
|
||||
/* Foundation Gray text steps (2026-11-11). Admin text hierarchy only; SW keeps its own tokens. */
|
||||
:root {
|
||||
--hm-gray-900: #212121;
|
||||
--hm-gray-800: #424242;
|
||||
--hm-gray-700: #616161;
|
||||
--hm-gray-600: #757575;
|
||||
--hm-gray-500: #9e9e9e;
|
||||
}
|
||||
@@ -8,10 +8,11 @@
|
||||
--hm-color-surface-default: var(--hm-sem-palette-white);
|
||||
--hm-color-surface-content: var(--hm-sem-palette-beige-light);
|
||||
--hm-color-surface-subtle: var(--hm-sem-palette-warm-neutral-muted);
|
||||
--hm-color-text-primary: var(--hm-sem-palette-green-dark-1);
|
||||
--hm-color-text-secondary: var(--hm-sem-palette-warm-neutral-dark);
|
||||
/* Admin text hierarchy (2026-11-11): Primary Gray 900, Secondary Gray 700, Placeholder Gray 600. Brand primitives keep their values. */
|
||||
--hm-color-text-primary: var(--hm-sem-palette-gray-900);
|
||||
--hm-color-text-secondary: var(--hm-sem-palette-gray-700);
|
||||
--hm-color-text-inverse: var(--hm-sem-palette-white);
|
||||
--hm-color-text-placeholder: var(--hm-sem-palette-neutral-gray-500);
|
||||
--hm-color-text-placeholder: var(--hm-sem-palette-gray-600);
|
||||
--hm-color-border-default: var(--hm-sem-palette-warm-neutral-mid);
|
||||
--hm-color-border-control: var(--hm-sem-palette-warm-neutral-pale);
|
||||
/* Common Focus color. Trial value: Light 2 (#266651), pending user confirmation. Was --hm-color-primary. */
|
||||
@@ -236,3 +237,12 @@
|
||||
--hm-sem-palette-orange-dark-2: {{color.palette.orange-dark-2|var(--hm-orange-dark-2)}};
|
||||
--hm-sem-palette-orange-dark-3: {{color.palette.orange-dark-3|var(--hm-orange-dark-3)}};
|
||||
}
|
||||
|
||||
/* Foundation Gray text steps; only the text aliases above reference them. */
|
||||
:root, [data-hm-theme] {
|
||||
--hm-sem-palette-gray-900: {{color.palette.gray-900|var(--hm-gray-900)}};
|
||||
--hm-sem-palette-gray-800: {{color.palette.gray-800|var(--hm-gray-800)}};
|
||||
--hm-sem-palette-gray-700: {{color.palette.gray-700|var(--hm-gray-700)}};
|
||||
--hm-sem-palette-gray-600: {{color.palette.gray-600|var(--hm-gray-600)}};
|
||||
--hm-sem-palette-gray-500: {{color.palette.gray-500|var(--hm-gray-500)}};
|
||||
}
|
||||
@@ -57,3 +57,12 @@
|
||||
--hm-orange-dark-2: {{color.palette.orange-dark-2}};
|
||||
--hm-orange-dark-3: {{color.palette.orange-dark-3}};
|
||||
}
|
||||
|
||||
/* Foundation Gray text steps (2026-11-11). Admin text hierarchy only; SW keeps its own tokens. */
|
||||
:root {
|
||||
--hm-gray-900: {{color.palette.gray-900}};
|
||||
--hm-gray-800: {{color.palette.gray-800}};
|
||||
--hm-gray-700: {{color.palette.gray-700}};
|
||||
--hm-gray-600: {{color.palette.gray-600}};
|
||||
--hm-gray-500: {{color.palette.gray-500}};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page.
|
||||
2026-11-11 compact document rhythm, shared by Web / Chart / OrgChart (SW and CENTER pages use the same frame only):
|
||||
H1 page title 28/36/700 (Page Header title) → description 14/20/400 secondary, 8px below (Page Header description)
|
||||
→ 24px → H2 section title 16/24/700 (body-md) over a thin divider → 12px → specimen; sections 24px apart (Dashboard gap);
|
||||
2026-11-11: the H1 page title / description header is no longer shown (the H1 stays for screen readers only); pages start with the
|
||||
H2 section title 16/24/700 (body-md) over a thin divider → 12px → specimen; sections 24px apart (Dashboard gap);
|
||||
H3 specimen labels 14/20/500 secondary, no divider of their own (story-board.tsx). Top / bottom gutter 16–24px.
|
||||
Side margins are unchanged (16px up to 480px, growing to 60px at 768px): the SW breakpoints 2040 / 1125 / 787 depend on them.
|
||||
No design token is added; --sb-page-* only names existing component token values for this Storybook frame. */
|
||||
@@ -27,32 +27,21 @@
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* No bottom padding: the header's margin collapses with the next block's own top margin (a section's 24px), so the first
|
||||
content sits 24px below the title whether it is a section, a section grid or a plain preview. */
|
||||
.sb-page .sb-page-header {
|
||||
margin-bottom: var(--sb-page-section-gap);
|
||||
font-size: var(--hm-label-font-size);
|
||||
line-height: var(--hm-label-line-height);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sb-page .sb-page-header .sb-page-kicker {
|
||||
margin: 0 0 4px;
|
||||
color: var(--hm-sem-fg-secondary);
|
||||
font-size: var(--hm-button-font-size-sm);
|
||||
font-weight: var(--hm-font-weight-bold);
|
||||
}
|
||||
.sb-page .sb-page-header .sb-page-title {
|
||||
margin: 0;
|
||||
font-size: var(--hm-page-header-title-font-size);
|
||||
line-height: var(--hm-page-header-title-line-height);
|
||||
font-weight: var(--hm-page-header-title-font-weight);
|
||||
}
|
||||
.sb-page .sb-page-header .sb-page-description {
|
||||
margin: var(--hm-page-header-description-gap) 0 0;
|
||||
color: var(--hm-page-header-description-color);
|
||||
font-size: var(--hm-page-header-description-font-size);
|
||||
line-height: var(--hm-page-header-description-line-height);
|
||||
font-weight: var(--hm-input-font-weight);
|
||||
/* 2026-11-11: no visible page header, so the first block starts right at the top gutter. StoryPageHeader only renders the
|
||||
visually hidden page H1 (screen readers only, out of flow), so the first visible block after it gets the same reset. */
|
||||
.sb-page > .sb-page-inner > :first-child,
|
||||
.sb-page > .sb-page-inner > .sb-page-a11y-title:first-child + * { margin-top: 0; }
|
||||
.sb-page .sb-page-a11y-title {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sb-page .sb-page-section { margin-top: var(--sb-page-section-gap); scroll-margin-top: 16px; }
|
||||
@@ -80,25 +69,3 @@
|
||||
Columns and alignment stay with the Story (className): this unlayered rule would override Tailwind's layered grid-cols-*. */
|
||||
.sb-page .sb-page-section-grid { display: grid; gap: var(--sb-page-section-gap) 24px; margin-top: var(--sb-page-section-gap); }
|
||||
.sb-page .sb-page-section-grid > .sb-page-section { margin-top: 0; }
|
||||
|
||||
/* Group notice under the page title (StoryNotice inside StoryPageHeader): 8px below the H1, same type and color as the
|
||||
Page Header description (one level below the title), no box. The header's own bottom margin keeps the first section 24px below. */
|
||||
.sb-page .sb-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin: var(--hm-page-header-description-gap) 0 0;
|
||||
font-size: var(--hm-page-header-description-font-size);
|
||||
line-height: var(--hm-page-header-description-line-height);
|
||||
font-weight: var(--hm-input-font-weight);
|
||||
color: var(--hm-page-header-description-color);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sb-page .sb-notice-icon {
|
||||
flex: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
/* centers the 14px icon on the first text line (20px line height → 3px above and below) */
|
||||
margin-block: calc((var(--hm-page-header-description-line-height) - 14px) / 2);
|
||||
}
|
||||
.sb-page .sb-notice-text { min-width: 0; }
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createContext, useContext, type ReactNode } from "react"
|
||||
import { createContext, type ReactNode } from "react"
|
||||
import "./story-page.css"
|
||||
|
||||
// Storybook presentation only (not part of @hm-design/ui): the common frame of a representative Story page.
|
||||
// Page → Header (title, description) → Sections (title, description, specimen). It never styles the component being reviewed.
|
||||
// Inner and title follow Foundation / Color; the section header → content split follows Center Graphic / 교량.
|
||||
// Page → Sections (title, description, specimen). It never styles the component being reviewed.
|
||||
// Inner follows Foundation / Color; the section header → content split follows Center Graphic / 교량.
|
||||
// A Story using it sets `parameters.layout: "fullscreen"` so the page owns its inner padding.
|
||||
|
||||
// Lets StateBoard render its specimen labels one level below the section heading.
|
||||
@@ -13,36 +13,18 @@ export function StoryPage({ className, children }: { className?: string; childre
|
||||
return <div className={className ? `sb-page ${className}` : "sb-page"}><div className="sb-page-inner">{children}</div></div>
|
||||
}
|
||||
|
||||
// Group notice shown directly under the page title (H1 → notice → description). A Story group opts in through a decorator
|
||||
// that provides this context (e.g. src/sw/sw-story-notice.tsx); without a provider nothing is rendered.
|
||||
// Group notice context. Still provided by src/sw/sw-story-notice.tsx, but nothing renders it since the page header was
|
||||
// removed from the Storybook frame (2026-11-11).
|
||||
export const StoryNoticeContext = createContext<ReactNode>(null)
|
||||
|
||||
// Representative (sidebar) Stories omit `kicker`: the sidebar path already names the category.
|
||||
// Hidden verification Stories keep passing it so their page structure stays unchanged.
|
||||
export function StoryPageHeader({ kicker, title, description }: { kicker?: string; title: string; description?: ReactNode }) {
|
||||
const notice = useContext(StoryNoticeContext)
|
||||
return (
|
||||
<header className="sb-page-header">
|
||||
{kicker ? <p className="sb-page-kicker">{kicker}</p> : null}
|
||||
<h1 className="sb-page-title">{title}</h1>
|
||||
{notice ? <StoryNotice>{notice}</StoryNotice> : null}
|
||||
{description ? <p className="sb-page-description">{description}</p> : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
// Notice row (icon + text) under the page title, one level below it (Page Header description type and color).
|
||||
// No box, background or border. Icon: Group 1171290812.svg (circle + "!"), colors via currentColor.
|
||||
export function StoryNotice({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<p className="sb-notice" data-story-notice="">
|
||||
<svg className="sb-notice-icon" width="14" height="14" 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>
|
||||
)
|
||||
// 2026-11-11 (user request): the Storybook page header (kicker, description and group notice) is no longer shown, so every
|
||||
// page visually starts with its first section. Only the title stays, as a visually hidden H1 (no box, no space), so the
|
||||
// preview document keeps one page heading above its H2 sections: the preview's own document title is the Story ID and the
|
||||
// iframe title is generic, so this is the only place a screen reader can identify the page.
|
||||
// Stories keep their calls and props, so the visible header can be restored here.
|
||||
// The product Page Header component (@hm-design/ui PageHeader) is unrelated and unchanged.
|
||||
export function StoryPageHeader({ title }: { kicker?: string; title: string; description?: ReactNode }) {
|
||||
return <h1 className="sb-page-a11y-title">{title}</h1>
|
||||
}
|
||||
|
||||
// Headings and specimens share StoryPage's single Inner. Wide specimens own their local scroll frame.
|
||||
|
||||
Vendored
+3
@@ -65,6 +65,9 @@
|
||||
.hm-foundation .hm-typo-metrics span, .hm-foundation .hm-typo-metrics small { display: block; }
|
||||
.hm-foundation .hm-typo-metrics small { font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); }
|
||||
@media (min-width: 1100px) { .hm-foundation .hm-typo-tables { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
/* Typography Text Colors reuse the Hanmac color chips (.hm-brand-color-chip). Four equal tracks keep "name · 설명" on one line on wide screens;
|
||||
narrow screens fall back to 150px tracks (2 columns at 390px) and wrap the label inside the chip instead of scrolling. */
|
||||
.hm-foundation .hm-text-color-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(max(150px, (100% - 24px) / 4), 1fr)); overflow: visible; padding-bottom: 0; }
|
||||
@media (max-width: 640px) {
|
||||
.hm-foundation .hm-typo-table thead { display: none; }
|
||||
.hm-foundation .hm-typo-table, .hm-foundation .hm-typo-table tbody, .hm-foundation .hm-typo-table tr, .hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { display: block; width: auto !important; }
|
||||
|
||||
@@ -15,6 +15,31 @@ const textLabels = ["Page Header · title", "Card · title", "Page Header · des
|
||||
const textRows = typographyReferences.filter(item => textLabels.includes(item.label))
|
||||
const controlRows = typographyReferences.filter(item => !textLabels.includes(item.label))
|
||||
|
||||
// Admin text colors (Web / Chart / OrgChart; not SW). The chip color and HEX read the current token; no color is hardcoded here.
|
||||
const textColorRoles = [
|
||||
{ label: "Primary", name: "기본 텍스트", token: "--hm-sem-fg-primary" },
|
||||
{ label: "Secondary", name: "보조 텍스트", token: "--hm-sem-fg-secondary" },
|
||||
{ label: "Placeholder", name: "입력 안내", token: "--hm-sem-fg-placeholder" },
|
||||
// Transparent readonly Input on the beige surface uses Secondary (Placeholder is below 4.5:1 there).
|
||||
{ label: "Readonly Placeholder", name: "읽기 전용 입력 안내", token: "--hm-sem-fg-secondary" },
|
||||
]
|
||||
const textColorProperties = [...new Set(textColorRoles.map(item => item.token))]
|
||||
|
||||
function toHex(value: string | undefined) {
|
||||
if (!value) return ""
|
||||
if (/^#[\da-f]{6}$/i.test(value)) return value.toUpperCase()
|
||||
const rgb = value.match(/^rgb\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)\s*\)$/)
|
||||
return rgb ? `#${rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, "0")).join("").toUpperCase()}` : value
|
||||
}
|
||||
|
||||
// Same chip markup and classes as Foundation/한맥 컬러 and 가족사 컬러 (foundation.css): swatch, name, HEX.
|
||||
function TextColorChips({ values }: { values: Record<string, string> }) {
|
||||
return <div className="hm-brand-color-row hm-text-color-row">{textColorRoles.map(item => <article className="hm-brand-color-chip" key={item.label} data-text-color={item.label} data-color-token={item.token}>
|
||||
<div className="hm-foundation-swatch" style={{ background: `var(${item.token})` }} />
|
||||
<h3>{item.label} · {item.name}</h3><p data-color-value>{toHex(values[item.token])}</p>
|
||||
</article>)}</div>
|
||||
}
|
||||
|
||||
function SpecimenRow({ label, size, line, weight }: TypographyReference & { weight?: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [metrics, setMetrics] = useState({ main: "", spacing: "" })
|
||||
@@ -43,7 +68,11 @@ function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) {
|
||||
|
||||
function TypographyReferencePage() {
|
||||
const values = useResolvedProperties(typographyProperties)
|
||||
const colorValues = useResolvedProperties(textColorProperties)
|
||||
return <ReferencePage title="폰트">
|
||||
<ReferenceSection title="Text Colors" note="">
|
||||
<TextColorChips values={colorValues} />
|
||||
</ReferenceSection>
|
||||
<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>
|
||||
</ReferenceSection>
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
/* 2026-10-21 Storybook simplification (SW pages): no event log or long descriptions on screen; the log stays in the DOM
|
||||
for the browser tests. Same rule as sw-stories.css (this page does not load it). */
|
||||
.sb-page:has([class*="hm-sw-"]) :is(.sw-home-menu-story__log, [data-callback-log], .sb-page-description, .sb-page-section-description) { display: none; }
|
||||
.sb-page:has([class*="hm-sw-"]) :is(.sw-home-menu-story__log, [data-callback-log], .sb-page-section-description) { display: none; }
|
||||
@@ -101,4 +101,4 @@
|
||||
/* 2026-10-21 Storybook simplification (SW pages): the components only. Callback logs, state dumps and long page /
|
||||
section descriptions are not shown; the log and state elements stay in the DOM because the browser tests read
|
||||
their text. Scoped to pages that render SW components, so other Story pages are unaffected. */
|
||||
.sb-page:has([class*="hm-sw-"]) :is([data-callback-log], [data-property-log], [data-style-state], .sb-page-description, .sb-page-section-description) { display: none; }
|
||||
.sb-page:has([class*="hm-sw-"]) :is([data-callback-log], [data-property-log], [data-style-state], .sb-page-section-description) { display: none; }
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Decorator } from "@storybook/react-vite"
|
||||
import { StoryNoticeContext } from "@/components/story-page"
|
||||
|
||||
// Storybook presentation only. Common notice of every SW Story page (SW/* groups only), shown directly under the page
|
||||
// title by StoryPageHeader: each SW stories file opts in with `decorators: [withSWNotice]`, so other menus never render it.
|
||||
// Storybook presentation only. Common notice of every SW Story page (SW/* groups only). Since 2026-11-11 the page header
|
||||
// that showed it is not rendered, so the notice is hidden; the decorator and context stay so it can be restored in one place.
|
||||
// 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 =
|
||||
|
||||
+9
-4
@@ -3,9 +3,9 @@
|
||||
"tokens": {
|
||||
"color.bg.content": "#f5f0e5",
|
||||
"color.bg.surface": "#ffffff",
|
||||
"color.fg.primary": "#11372d",
|
||||
"color.fg.secondary": "#6e6759",
|
||||
"color.fg.placeholder": "#999999",
|
||||
"color.fg.primary": "#212121",
|
||||
"color.fg.secondary": "#616161",
|
||||
"color.fg.placeholder": "#757575",
|
||||
"color.action.primary": "#164434",
|
||||
"color.action.cancel": "#6e6759",
|
||||
"color.action.destructive": "#d32f2f",
|
||||
@@ -92,6 +92,11 @@
|
||||
"color.palette.orange-light-1": "#eb5f00",
|
||||
"color.palette.orange-dark-1": "#bc4c00",
|
||||
"color.palette.orange-dark-2": "#883700",
|
||||
"color.palette.orange-dark-3": "#4f2000"
|
||||
"color.palette.orange-dark-3": "#4f2000",
|
||||
"color.palette.gray-900": "#212121",
|
||||
"color.palette.gray-800": "#424242",
|
||||
"color.palette.gray-700": "#616161",
|
||||
"color.palette.gray-600": "#757575",
|
||||
"color.palette.gray-500": "#9e9e9e"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user