fix: align v2 publisher review with HMDesign stories and tokens

This commit is contained in:
DESKTOP-KPCP6AL\ㅅㅎ committed 2026-10-11 11:36:32 +09:00
1 parent 1c4e0bb9a1
commit 7b22fe88a1
27 files changed
+413 -49

No files matched your search

+8
View File
@@ -18,3 +18,11 @@
11. 390/768/1280/1440px에서 overflow·줄바꿈·Navigation을 확인한다(Dashboard는 960px 내부 스크롤). 숨김 absolute 알림의 위치 기준과 문서/내부 scrollHeight·clientHeight, ID의 한 줄 식별 가능성도 확인한다. Label/접근성 이름, 키보드·focus 복귀, disabled/readOnly, 핵심 상호작용을 확인한다. 결과는 실제 사용 요소·Pattern 선택/미사용 이유·Navigation·검증·미검증으로 보고하며 확인하지 않은 항목은 `NOT VERIFIED`다.
필요할 때만 [보존된 상세 지침](docs/guidelines/ai-implementation-details.md)을 읽는다. 신규 화면에는 위 필수 규칙이 우선한다.
## 공식 작업 경로와 Git 정책 — 2026-10-11 사용자 확정
- HMDesign 실제 수정은 `D:\디자인시스템\HMDesign-Gitea-Transfer`에서 진행한다. 시작 전 절대경로·`main` 브랜치·공식 origin `https://gitea.hmac.kr/b25027/HMDesign.git`·최신 원격 HEAD·Working Tree·기존 미커밋 변경을 확인한다.
- 퍼블리셔 요청에 필요한 부분만 수정하고 기존 Component·Pattern·Theme·Storybook을 보존한다. 변경 영향과 관련 MD/Storybook 정합성을 검증하고 유효한 기존 정상 검증은 불필요하게 반복하지 않는다.
- 필수 검증을 통과하면 관련 파일만 Stage하고 Staged Diff를 검토한 뒤 Commit·공식 Gitea `main` 일반 Push를 수행한다. 작업별 사용자의 명시적 Commit/Push 보류 지시가 있으면 그 지시를 우선한다.
- Commit/Push 직전에 원격 `main`을 다시 확인한다. 시작 기준 이후 원격이 변경됐거나 중요한 충돌이 있으면 자동으로 덮어쓰지 않고 중단한다. 완료 후 Local/Remote SHA와 Working Tree를 확인한다.
- GitHub 이전 원본 폴더·remote에는 수정/Push하지 않는다. Force Push·Reset·Clean·기존 이력 삭제·백업 브랜치 변경·무관한 파일 Stage를 금지한다.
+4
View File
@@ -16,6 +16,8 @@ Storybook은 기존 HM Component·Pattern·Theme·Variant·State·Interaction을
## Git Clone 설치·실행 (현재 테스트 공유 기준)
v2 추가 5개 요청의 구현·실제 Storybook 검증과 Orange Dark 1/2 보류는 [2026-10-11 선택 검증 보고](docs/reports/v2-publisher-review-2026-10-11.md)를 확인합니다.
공식 소스 저장소는 [Gitea HMDesign](https://gitea.hmac.kr/b25027/HMDesign)입니다. 이전 원본은 [GitHub HMDesignSystemTest](https://github.com/shkim25027/HMDesignSystemTest)의 `90583ab6046ab5dc1ac35d94d38f3301c9a4dfe1`이며, GitHub 이력을 병합하지 않고 공식 Gitea의 기존 이력 위에 소스만 반영합니다. 먼저 [AGENTS.md](AGENTS.md)를 읽고 기존 Component → Variant/Prop → Composition 순서로 재사용합니다.
현재 검증 범위와 남은 제한은 [2026-10-10 잔여 이슈 회귀 보고](docs/guidelines/publisher-validation-2026-10-10.md)를 확인합니다.
@@ -42,6 +44,8 @@ Windows에서는 기존 `start-storybook.bat`도 사용할 수 있습니다. 의
정적 Storybook 빌드는 `npm run build-storybook`, 앱 빌드는 `npm run build`, 타입 확인은 `npm run typecheck`입니다. 명령은 루트 package.json의 현재 scripts를 기준으로 합니다.
문서 내부 링크 검사는 프로젝트 루트에서 `node scripts/check-doc-links.mjs`로 실행합니다. `.git`이 있으면 기존 Git 추적·비추적(ignored 제외) Markdown 수집을 유지하고, `.git` 없는 압축 해제본에서는 파일 시스템과 루트·하위 `.gitignore`의 현재 경로·와일드카드·제외/예외 규칙으로 수집합니다. `.git` 메타데이터와 ignored 폴더는 순회하지 않습니다. Node만 있으면 독립 실행할 수 있으며 결과에 `collectionMode`, 실제 MD/링크 수와 문제 목록을 출력합니다. 종료 코드 0은 통과, 1은 링크 오류, 2는 수집 실행 오류입니다. 회귀 검사는 `node --test scripts/check-doc-links.test.mjs`입니다(두 수집 방식 비교에는 Git 필요).
## 기존 Component 기반 새 React 화면
```powershell
+2
View File
@@ -54,6 +54,8 @@ shadcn의 `bg-border`, `h-px`/`w-px` class를 제거하고 theme CSS가 두께·
## 10. 사용자 확정 결정
2026-10-10: 같은 제목·설명 묶음에서는 제목 아래와 설명 아래에 Separator를 각각 연속으로 사용하지 않고, 필요한 위치 한 곳에만 적용한다. 대표 Story는 제목·설명 묶음 뒤 한 곳에 적용한다. 모든 제목이나 설명에 구분선을 필수로 추가하는 규칙은 아니다. Component 스타일과 API는 유지한다.
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
## 11. 미확정 / 보류
+34
View File
@@ -0,0 +1,34 @@
# 브랜드 컬러 8단계 확인
2026-10-11 v2 요청의 표시 이름은 한맥그린·한맥브라운·한맥오렌지, 단계 순서는 Light 4 → Light 3 → Light 2 → Light 1 → Default → Dark 1 → Dark 2 → Dark 3이다. 읽기 전용 `D:/디자인시스템/ui_viz/docs/brand/hanmac/foundation/color.md` §4.1, `implementations/web/src/tokens/primitives.css`, `docs/foundation/token-inventory.md`의 HEX·단계·승인 상태를 대조했다. 기존 [공개 계약](../../packages/ui/src/theme/contract.ts)과 [Default JSON](../../themes/default.json)의 동일 단계명·HEX는 원본과 일치한다.
| 단계 | 한맥그린 | 한맥브라운 | 한맥오렌지 |
|---|---|---|---|
| Light 4 | #34DAAA | #ECD4A2 | #FEBA88 |
| Light 3 | #249473 | #C1A770 | #EC8F53 |
| Light 2 | #266651 | #897445 | #F87725 |
| Light 1 | #205241 | #604F32 | #EB5F00 |
| Default | #123328 | #3E3523 | #CC5200 |
| Dark 1 | #0A221D | #352D1D | Needs Review — 공개 Key 없음 |
| Dark 2 | #071E18 | #2F281C | Needs Review — 공개 Key 없음 |
| Dark 3 | #020A07 | #1D1810 | #4F2000 |
확정 칸의 Key는 `color.palette.{green|brown|orange}-{light-4|light-3|light-2|light-1|default|dark-1|dark-2|dark-3}` 중 실제 선언된 Key다. CSS는 동일 suffix의 `--hm-sem-palette-*` → `--hm-*` Primitive를 사용한다. 기존 Key·값을 보존하고 확정 누락분 8개만 추가했다. 전체 공개 입력은 73 → 81개(색상 71 → 79개, 기존 radius 2개)다. 미확정 칸에는 공개 Key·CSS 변수·HEX를 새로 만들지 않는다.
## Legacy Key 보존과 미확정 항목
| 기존 Key | 확인된 HEX | 확인 필요 사항 |
|---|---|---|
| color.palette.brown-deep | #221D13 | 기존 Button gradient 소비 유지. 새 Dark 2 #2F281C·Dark 3 #1D1810과 다르므로 alias/대체하지 않음 |
| color.palette.orange-light | #D35A00 | 기존 Button 배경 0% stop 유지. 브랜드 Light 1 #EB5F00과 별도 |
| color.palette.orange-mid | #CE5600 | 기존 Button 배경 30% stop 유지. 브랜드 단계 대응 없음; Default #CC5200과도 별도 |
| color.palette.orange-dark | #B44B00 | 기존 Button 배경 65% stop 유지. 번호 있는 Dark 단계로 승격하지 않음 |
| color.palette.orange-deep | #783402 | 기존 Button 배경 100% stop 유지. 브랜드 Dark 3 #4F2000과 별도 |
Button 원본 근거는 `ui_viz/docs/brand/hanmac/components/button.md`의 D-087 및 `implementations/web/src/components/Button/Button.css`다. 단순 단계 이름 변경으로 기존 Button의 색상을 바꾸지 않는다. Brown Highlight와 Orange Border 계열도 기존 Key·값·용도를 보존한다.
Orange Dark 1의 원본 후보 `hm-orange-dark-1: #BC4C00`, Dark 2 후보 `hm-orange-dark-2: #883700`은 위 Color §4.1·Primitive CSS·inventory 모두 **Needs Review**다. 원본 Figma 대조/디자인 승인 확인 후 반영해야 한다. 기존 `color.palette.orange-border: #BC4C00`은 Button border 범위 확정값으로 유지하지만 같은 HEX를 근거로 브랜드 Dark 1을 확정하지 않는다. 이미지 추출·보간·밝기 순서 추정은 하지 않는다.
## 호환성과 Storybook
[Color 대표 Story](../../src/foundation/color.stories.tsx)는 브랜드별 8개 순서 슬롯을 제공한다. Green 8/8·Brown 8/8 확정, Orange 6/8 확정·Dark 1/2 미확정이다. Legacy Button 색상은 별도로 표시하고 동일 Key를 계속 사용한다. 추가 8개 팔레트는 Color Reference에서 소비하며 기존 Component mapping을 새 값에 연결하지 않는다. 기존 public Key·Property·type·Default·API·schemaVersion 1과 서비스 partial JSON 호환성을 유지한다. [Runtime Theme](runtime-theme.md)의 geometry/상태 권한과 Designer의 기존 17개 상위 역할 입력 UI는 확대하지 않는다. 3색 전체 HEX·단계 확정은 PARTIAL이다.
+1 -1
View File
@@ -37,7 +37,7 @@ resetRuntimeTheme(document.documentElement)
- 일반 새 탭은 독립 세션입니다. 탭 복제 또는 opener로 연 탭은 브라우저가 초기 sessionStorage를 복사할 수 있으며 이후 편집은 탭별로 독립적입니다.
- 서버/DB/Publish 기능은 없습니다. 같은 브라우저 탭의 Storybook preview origin에 `sessionStorage`로 보관하며, 세션 저장을 사용할 수 없는 환경에서도 현재 화면의 편집/Export는 동작합니다. 장기 보관은 JSON Export를 사용하세요.
- Global Theme는 `document.documentElement`에 적용하며 body Portal도 같은 Theme를 사용합니다. wrapper 밖으로 Portal된 popup은 Scoped Theme를 자동 전달받지 않습니다. 이 제한은 기존 FOLLOW_UP입니다.
- 전체 Runtime Contract는 73개 입력을 유지합니다. 배포하는 Default JSON은 71개 컬러이며 radius/geometry는 package에서 관리합니다. 편집 화면은 상위 컬러 역할 17개만 노출하며 Component Token, radius, palette/gradient 개별 입력 UI를 만들지 않습니다. 기존 컬러 계약의 palette/gradient는 유효 JSON으로 Import할 수 있으며 gradient는 세트 전체를 제공해야 합니다.
- 전체 Runtime Contract는 81개 입력입니다(2026-10-11 확정 브랜드 팔레트 8개 추가, 기존 73개 보존). 배포하는 Default JSON은 79개 컬러이며 radius/geometry는 package에서 관리합니다. 편집 화면은 상위 컬러 역할 17개만 노출하며 Component Token, radius, palette/gradient 개별 입력 UI를 만들지 않습니다. 기존 컬러 계약의 palette/gradient는 유효 JSON으로 Import할 수 있으며 gradient는 세트 전체를 제공해야 합니다.
- 명시적 `[data-hm-theme]` scope는 독립 HM preset이므로 Global Theme를 이어받지 않습니다. Production에서 같은 root는 한 owner가 Apply/Reset을 관리합니다.
- Storybook 편집기 설치는 이번 코드 변경을 적용하는 최초 빌드가 필요합니다. 설치 후 컬러 값을 바꾸는 운영에는 재빌드가 필요 없습니다.
+15 -6
View File
@@ -13,7 +13,7 @@
| `color.action.cancel` | `--hm-sem-action-cancel` | `#6e6759` | 공개 취소 역할 | Yes | Button `variant="cancel"` + `appearance="outline"` |
| `color.action.destructive` | `--hm-sem-action-destructive` | `#d32f2f` | 오류/위험 역할 | Yes | checkbox, radio, badge, input, button |
| `color.stroke.default` | `--hm-sem-stroke-default` | `#9a9284` | 기본 border | Yes | checkbox, radio, switch, badge |
| `color.stroke.input.default` | `--hm-sem-stroke-input-default` | `#dedbd2` | 입력/패널 divider border | Yes | table, input, select |
| `color.stroke.input.default` | `--hm-sem-stroke-input-default` | `#dedbd2` | 입력/패널 divider border | Yes | table, input, select, GNB 콘텐츠 LNB divider |
| `color.stroke.input.focus` | `--hm-sem-stroke-input-focus` | `#266651` | 공통 Focus border/ring | Yes | input, select, button, checkbox, calendar, slider Keyboard Focus halo 등; GNB/LNB는 contextual 예외 |
| `color.bg.brand.stop.0` | `--hm-sem-brand-bg-stop-0` | `#0a221d` | 기존 gradient stop: color.bg.brand.stop.0 | Yes | Theme palette / adapter |
| `color.bg.brand.stop.12` | `--hm-sem-brand-bg-stop-12` | `#071e18` | 기존 gradient stop: color.bg.brand.stop.12 | Yes | Theme palette / adapter |
@@ -78,14 +78,23 @@
| `color.palette.gray-dark` | `--hm-sem-palette-gray-dark` | `#5a5a5a` | 선택적 Theme 팔레트: gray-dark | Yes | button |
| `color.palette.indicator` | `--hm-sem-palette-indicator` | `#2d2d2d` | 선택적 Theme 팔레트: indicator | Yes | select |
총 73개. 기존 25개 + 핵심 역할 6개 + 선택적 팔레트 42개. 팔레트는 컴포넌트별 계약이 아니며 Primary / Active / Focus를 개별 지정하면 팔레트 alias보다 우선한다. Focus와 Active, Primary와 On Accent는 각각 sibling이며 서로 alias하지 않는다.
| `color.palette.green-dark-3` | `--hm-sem-palette-green-dark-3` | `#020a07` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.brown-light-3` | `--hm-sem-palette-brown-light-3` | `#c1a770` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.brown-dark-2` | `--hm-sem-palette-brown-dark-2` | `#2f281c` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.brown-dark-3` | `--hm-sem-palette-brown-dark-3` | `#1d1810` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.orange-light-3` | `--hm-sem-palette-orange-light-3` | `#ec8f53` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.orange-light-2` | `--hm-sem-palette-orange-light-2` | `#f87725` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.orange-light-1` | `--hm-sem-palette-orange-light-1` | `#eb5f00` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
| `color.palette.orange-dark-3` | `--hm-sem-palette-orange-dark-3` | `#4f2000` | ui_viz 확정 브랜드 단계 | Yes | Color Reference / 선택적 Theme palette |
총 81개. 기존 25개 + 핵심 역할 6개 + 선택적 팔레트 50개(2026-10-11 ui_viz 확정 단계 8개 추가). 팔레트는 컴포넌트별 계약이 아니며 Primary / Active / Focus를 개별 지정하면 팔레트 alias보다 우선한다. Focus와 Active, Primary와 On Accent는 각각 sibling이며 서로 alias하지 않는다.
`color.action.cancel`의 공개 key·기본값은 보존한다. 2026-10-10 Gray 통일 요청에 따라 기본 Outline 취소 ink는 기존 Gray palette `#767676`을 사용한다. 기본값과 다른 명시적 취소 컬러 override는 내부 Button mapping에 전달하고 Reset 시 복원한다. 전체 Default preset을 주입해도 기본 Gray와 동일하다. Solid 취소는 기존 Gray mapping을 유지한다. [Runtime Theme](runtime-theme.md).
# 디자인팀 Runtime 운영 추가 결정 — 2026-10-06
# 디자인팀 Runtime 운영 결정 — 2026-10-06, 팔레트 보강 2026-10-11
기존 v1 입력 73개와 CSS property/type 계약을 유지한다. 공개 API `isRuntimeTheme(unknown): value is RuntimeTheme`를 제공한다. 이 사전 검증은 DOM을 변경하지 않는다. 기존 `applyRuntimeTheme`의 invalid 입력 시 reset 정책은 유지한다. 편집기는 사전 검증으로 invalid Import를 차단해 마지막 유효 Theme를 보존한다.
기존 v1 입력 73개의 Key·CSS property·type·Default를 유지하고 확정 팔레트 8개만 추가한다. Orange Dark 1·2는 Needs Review라 공개 입력에 추가하지 않는다. 공개 API `isRuntimeTheme(unknown): value is RuntimeTheme`를 제공한다. 이 사전 검증은 DOM을 변경하지 않는다. 기존 `applyRuntimeTheme`의 invalid 입력 시 reset 정책은 유지한다. 편집기는 사전 검증으로 invalid Import를 차단해 마지막 유효 Theme를 보존한다.
Default 컬러의 운영 원본은 `themes/default.json`(71개 컬러)이다. `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`(79개 컬러)이다. `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 계약은 유지한다.
@@ -95,4 +104,4 @@ Storybook `Theme/Designer Playground/Editor`는 17개 상위 컬러 역할과 JS
## Dashboard 상태 색상 범위
Dashboard의 `active / hold / done / issue` 업무 상태 계약은 `packages/ui/src/patterns/dashboard-status/contract.ts`에서 ID·label·tone을 제공한다. 상태 변수 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며 `@hm-design/ui/dashboard-status.css`로 배포한다. `src/patterns/dashboard-status-contract.css`는 기존 Story/HTML 경로의 호환 import다. Theme CSS 다음에 상태 CSS를 로드하고 `--hm-dashboard-status-active|hold|done|issue`를 화면 요소의 색상에 별도로 연결한다. 상태 CSS 자체는 Badge나 `data-status`를 자동으로 꾸미지 않는다. active/hold/issue는 기존 Story 색상을 보존하는 compatibility mapping이고 done은 `:root` 및 `[data-hm-theme]`에서 해당 scope의 기존 HM white token을 참조한다. 기존 HM Theme 및 Public Runtime Token/JSON 73개는 변경하지 않으며, 실제 Production 소비처가 반복되어 공통 색상 의미가 확정될 때만 Semantic Token 승격을 검토한다.
Dashboard의 `active / hold / done / issue` 업무 상태 계약은 `packages/ui/src/patterns/dashboard-status/contract.ts`에서 ID·label·tone을 제공한다. 상태 변수 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며 `@hm-design/ui/dashboard-status.css`로 배포한다. `src/patterns/dashboard-status-contract.css`는 기존 Story/HTML 경로의 호환 import다. Theme CSS 다음에 상태 CSS를 로드하고 `--hm-dashboard-status-active|hold|done|issue`를 화면 요소의 색상에 별도로 연결한다. 상태 CSS 자체는 Badge나 `data-status`를 자동으로 꾸미지 않는다. active/hold/issue는 기존 Story 색상을 보존하는 compatibility mapping이고 done은 `:root` 및 `[data-hm-theme]`에서 해당 scope의 기존 HM white token을 참조한다. 기존 HM Theme 및 Public Runtime Token/JSON에 상태 입력은 추가하지 않으며, 실제 Production 소비처가 반복되어 공통 색상 의미가 확정될 때만 Semantic Token 승격을 검토한다.
+4 -2
View File
@@ -1,5 +1,7 @@
# Runtime Theme Contract v1
브랜드 컬러 8단계의 확정값·Legacy 대응 보류 및 공개 Key 보존은 [브랜드 컬러 단계](brand-color-steps.md)를 따른다. 미확정 표시는 새 Runtime Token이나 색상 승인이 아니다.
2026-10-06 · 분류: HM_THEME / Theme infrastructure. Component props, DOM, Radix 상태, Portal, 키보드, ARIA, breakpoint는 변경하지 않았다.
## 현재 파일과 적용 흐름
@@ -12,7 +14,7 @@
React 외 실행 형식으로 변환할 때도 이 문서의 Token·CSS Custom Properties·Runtime 적용 계약을 유지한다. Target Runtime, standalone asset, `file://` 검증 및 AI/도구 독립성 기준은 [Runtime Portability Policy](runtime-portability.md)를 따른다.
소비 CSS는 `tailwindcss` → `@hm-design/ui/tailwind.css` → `@hm-design/ui/theme.css` 순서로 앱 entry에서 로드한다. Dashboard 상태 표시가 있으면 그 뒤에 `@hm-design/ui/dashboard-status.css`와 소비 Badge mapping을 연결한다([상태 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)). Status CSS는 변수만 정의하며 73개 Runtime Theme 계약에 새 상태 Token을 추가하지 않는다. [Tailwind 검색 범위](runtime-portability.md#21-tailwind-v4-소비자-검색-범위)와 [Noto Sans KR·Asset 경로](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)는 실행 환경별 소비 가이드를 따른다.
소비 CSS는 `tailwindcss` → `@hm-design/ui/tailwind.css` → `@hm-design/ui/theme.css` 순서로 앱 entry에서 로드한다. Dashboard 상태 표시가 있으면 그 뒤에 `@hm-design/ui/dashboard-status.css`와 소비 Badge mapping을 연결한다([상태 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)). Status CSS는 변수만 정의하며 81개 Runtime Theme 계약에 새 상태 Token을 추가하지 않는다. [Tailwind 검색 범위](runtime-portability.md#21-tailwind-v4-소비자-검색-범위)와 [Noto Sans KR·Asset 경로](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)는 실행 환경별 소비 가이드를 따른다.
Default는 특별한 컴포넌트 내부 상수가 아니라 이 계약을 만족하는 기본 제공 Theme Set이다. 주입이 없으면 패키지 CSS의 Default preset을 사용한다. `defaultRuntimeTheme`는 동일한 값을 가진 JSON 형태 preset이다. Story의 CSS fallback / Default preset 주입 비교와 계약 값 검사로 두 표현의 일치를 검증한다.
@@ -22,7 +24,7 @@ Default는 특별한 컴포넌트 내부 상수가 아니라 이 계약을 만
- **내부 Derived**: `--hm-sem-surface-highlight`(Accent 45%), gradient 조합, shadow/overlay 조합, shadcn adapter(`--primary`, `--ring` 등). 외부 JSON은 이 변수를 직접 쓰지 않는다.
- **내부 Component Mapping**: `--hm-checkbox-*`, `--hm-calendar-*`, `--hm-pagination-*` 등. 기존 이름을 유지한다. Runtime JSON으로 받지 않는다.
- Typography, height, spacing, gradient geometry, shadow geometry, responsive layout은 코드 계약이다. 이번 컬러 구조 변경으로 Runtime 범위를 확대하지 않았다.
- Typography 현황(2026-10-08): 기존 크기와 ui_viz 승인 변경값을 유지한다. `packages/ui/src/theme/*.css`의 font-size는 px이고 shadcn 유래 Tailwind class(`text-sm` 등)는 rem 기반이라 단위가 혼재한다. 소비자가 Tailwind class나 `className`으로 크기를 덮어쓰면 Theme 값보다 우선할 수 있다. Typography Role(heading/body/label 등)은 아직 공통 Token으로 도입하지 않았고 Runtime JSON 73개 계약에도 포함하지 않는다.
- Typography 현황(2026-10-08): 기존 크기와 ui_viz 승인 변경값을 유지한다. `packages/ui/src/theme/*.css`의 font-size는 px이고 shadcn 유래 Tailwind class(`text-sm` 등)는 rem 기반이라 단위가 혼재한다. 소비자가 Tailwind class나 `className`으로 크기를 덮어쓰면 Theme 값보다 우선할 수 있다. Typography Role(heading/body/label 등)은 아직 공통 Token으로 도입하지 않았고 Runtime JSON 81개 계약에도 포함하지 않는다.
기존 25개 allowlist의 JSON 이름과 validation을 유지하고 Active / on Active / Inverse Text / Subtle Surface / Accent / on Accent를 추가했다. 같은 CSS property로 향하는 중복 공개 key는 없다. Component 수백 개의 Token을 요구하는 대신 Theme 팔레트는 선택적 확장이다. 핵심 역할만 지정해도 사용할 수 있다. 팔레트 이름은 색상 계열을 유지하며, 역할 Token은 각각 독립적으로 override할 수 있다.
+2 -2
View File
@@ -12,7 +12,7 @@ HMDesignSystem/
│ ├─ ui/ # @hm-design/ui: logic, contract, mapping, runtime
│ └─ center-graphic/ # 독립 Graphic package
├─ themes/
│ ├─ default.json # 71개 public color 값의 공식 Default 원본
│ ├─ default.json # 79개 public color 값의 공식 Default 원본
│ ├─ service-example.json # 5개 역할만 바꾸는 명시적 예제
│ └─ drafts/ # 과거 초안; Theme 산출 대상 아님
├─ scripts/
@@ -23,7 +23,7 @@ HMDesignSystem/
└─ docs/guidelines/
```
`themes/default.json`은 기존 v1 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. Component Token은 없다. 73개 Runtime 입력 중 71개 컬러를 포함한다. 나머지 두 radius와 layout은 패키지 template에서 관리한다. 서비스 Theme는 같은 계약의 부분 컬러 override다.
`themes/default.json`은 기존 v1 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. Component Token은 없다. 81개 Runtime 입력 중 79개 컬러를 포함한다. 나머지 두 radius와 layout은 패키지 template에서 관리한다. 서비스 Theme는 같은 계약의 부분 컬러 override다.
## 단일 Default 원본과 package fallback
+1 -1
View File
@@ -32,7 +32,7 @@ AI 신규·수정 UI 작업은 사용자의 별도 요청 없이 기존 Componen
| `.storybook/`, `src/components/*.stories.tsx` | 현재 시제품 → 계속 사용 | 실제 컴포넌트 검수. 패키지 산출물에는 Story를 넣지 않음 |
| `packages/ui/src/patterns/`, `packages/ui/src/theme.css` | 현재 구현 | 업무/Layout composition과 기본 Theme CSS. Library 산출물은 `npm run build:packages`로 생성 |
| `packages/center-graphic/` | 현재 구현 | `@hm-design/center-graphic` 독립 package 배포 경계. UI package와 필수 의존 없음 |
| `packages/ui/src/theme/contract.ts`, `runtime.ts` | 현재 구현 | 공개 73개 입력, 형식/범위 검증, atomic fallback·cleanup, Global/Scoped 적용. `@hm-design/ui/theme` export |
| `packages/ui/src/theme/contract.ts`, `runtime.ts` | 현재 구현 | 공개 81개 입력, 형식/범위 검증, atomic fallback·cleanup, Global/Scoped 적용. `@hm-design/ui/theme` export |
| `themes/drafts/` | Runtime MVP 제안 | 디자인팀의 Theme 값 초안·검수 이력. Git 커밋만으로 운영 Theme이 바뀌지 않으며 패키지에도 넣지 않음 |
| `themes/*.json` | 현재 구현 | `themes/default.json`은 공식 Default Color Source of Truth. 검증된 top-level JSON은 Theme CDN 배포 단위이며 Git push와 CDN publish는 별도 단계 |
| 서비스 앱의 bootstrap | 개발팀 앱 | 패키지 버전 설치, themeId/mode 결정, API/CDN fetch·캐시·실패 처리, 검증된 값 적용 |
+1 -1
View File
@@ -43,7 +43,7 @@ Primitive (설계·빌드 시 관리) → Semantic (컴포넌트 참조) → Com
컴포넌트는 `blue-600` 같은 Primitive나 `#2563eb`, `8px` 같은 실제 값을 직접 참조하지 않는다. Tailwind를 쓰는 경우에도 `bg-primary`의 `--color-primary` → `--primary` → `--hm-sem-action-primary` adapter 연결을 확인한다. **Runtime override는 공개 allowlist의 Semantic/Theme palette 값만** 허용한다. Primitive, 컴포넌트 전용 내부 변수, 임의 CSS selector/rule/JavaScript는 Publish 대상이 아니다.
현재 allowlist는 `packages/ui/src/theme/contract.ts`의 73개 입력이다(색상 Semantic/Theme palette와 input/container radius). 정확한 이름·타입·기본값·허용 범위는 이 계약과 `runtime-theme-contract.md`를 따른다. Shadow geometry는 현재 공개 입력이 아니다. Typography와 spacing은 실제 사용처와 레이아웃 영향 검토 후 개별 등록한다. 문서에 예시가 있다는 이유로 곧바로 Runtime 허용 Token이 되지 않는다.
현재 allowlist는 `packages/ui/src/theme/contract.ts`의 81개 입력이다(색상 Semantic/Theme palette와 input/container radius). 정확한 이름·타입·기본값·허용 범위는 이 계약과 `runtime-theme-contract.md`를 따른다. Shadow geometry는 현재 공개 입력이 아니다. Typography와 spacing은 실제 사용처와 레이아웃 영향 검토 후 개별 등록한다. 문서에 예시가 있다는 이유로 곧바로 Runtime 허용 Token이 되지 않는다.
기본 Theme은 `@company/ui`가 배포하는 CSS에 포함하고 앱이 초기 렌더 전에 로드한다. Runtime 값이 없거나 가져오기·검증에 실패해도 기본값으로 UI가 렌더링되어야 한다. 현재 Light/Dark 구분은 없으며 단일 HM Theme 값을 기준으로 한다([테마 토큰 가이드](테마-토큰-가이드.md) 참조).
+3 -1
View File
@@ -54,7 +54,7 @@ TanStack Table v9의 state model 그대로. v9는 기능을 명시 등록하는
- 상태는 TanStack 내부가 소유한다(필요하면 `useDataTable`에 `state` / `atoms`로 제어). 검색은 `table.setGlobalFilter`, 열 필터는 `table.getColumn(id)?.setFilterValue`, 초기화는 `resetGlobalFilter(true)` / `resetColumnFilters(true)`로 한다. 필터 변경 시 TanStack이 page index를 0으로 되돌린다.
- error는 Data Table이 소유하지 않는다(소비자가 Alert / Toast로 처리).
조회 버튼을 사용하는 목록은 [FilterBar의 draft / applied 소비 예제](filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 따른다. 입력 중에는 draft만 바꾸고 조회 시 table 필터 또는 `useMemo`로 만든 `data`를 갱신한다. 초기화는 입력값과 적용값, 사용 중인 table 필터와 페이지를 소비자가 함께 처리한다. 대표 Data Table Story는 표 기능의 운영 Reference이며, `WithFilterBarStory`(`patterns-data-data-table--with-filter-bar-story`)는 **입력 즉시 적용**을 확인하는 숨김 verification이다. 조회 버튼으로 조건을 확정하는 공개 API나 서버 호출 기능은 제공하지 않는다.
조회 버튼을 사용하는 목록은 [FilterBar의 draft / applied 소비 예제](filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 따른다. 입력 중에는 draft만 바꾸고 조회 시 table 필터 또는 `useMemo`로 만든 `data`를 갱신한다. 초기화는 입력값과 적용값, 사용 중인 table 필터와 페이지를 소비자가 함께 처리한다. 대표 Data Table Story는 표 기능의 운영 Reference이며, `WithFilterBarStory`(`patterns-data-data-table--with-filter-bar-story`)도 **조회 버튼으로 적용**하는 같은 소비 조합을 확인하는 숨김 verification이다. Pattern 자체는 조회 버튼으로 조건을 확정하는 별도 공개 API나 서버 호출 기능을 제공하지 않는다.
## 7. 조합 컴포넌트
@@ -79,6 +79,8 @@ semantic `<table>` 유지. 정렬 열 `<th aria-sort>`(ascending / descending),
## 11. Responsive
2026-10-11 v2 추가 요청: 대표 Story의 목록 검색과 ‘열’ 버튼은 공간이 충분할 때 같은 행에 둔다. 검색 form의 내용 폭을 기준으로 공간이 부족할 때만 목록 도구가 다음 행으로 줄바꿈된다. 기본 검색은 form 안에서 가운데, 추가 조건은 왼쪽, ‘열’ 버튼은 우측 정렬한다. 간격은 기존 `--hm-filter-bar-gap`(8px)을 재사용하고 모바일 검색·버튼 행 정렬을 유지한다. `src/list-screen.css`의 `hm-data-table-*` 조합에만 적용하며 공통 FilterBar·DataTable Component API/CSS는 변경하지 않는다.
Table primitive의 container가 가로 스크롤한다. Pattern은 `[data-slot="data-table"] { min-width: 0 }`로 grid / flex 부모 안에서 페이지를 밀지 않게 한다. 390px에서 페이지 가로 overflow 없음(표만 520px 안에서 스크롤), footer는 요약 / 페이지 크기 / 페이지 이동이 줄바꿈된다. 카드형 변환은 없다.
### 11.1 식별자 줄바꿈과 열 폭
+2
View File
@@ -1,5 +1,7 @@
# GNB (Pattern)
> 2026-10-11 v2 추가 요청: 콘텐츠 LNB와 본문 사이 세로 구분선은 공통 `gnb.css`에서 1px solid `--hm-sem-stroke-input-default`(Default `#DEDBD2`)를 사용한다. 기존 `--hm-sem-stroke-default`(`#9A9284`) 연결만 교체하며 외곽 Border·배경·Green LNB 2Depth 및 Storybook 목차의 Alpha 8%는 유지한다.
> 2026-10-10 추가 수정 요청 기준: 기존 Reference 콘텐츠 LNB는 본문과 동일 배경·세로 divider 1개, 아이콘 기본 생략(선택적 허용), 해당 컬러 계열 선택·Hover. data-hm-color="brown" 범위는 Brown 색상. 신규 AI GNB_ONLY Sidebar 금지 정책 유지. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
> **SINGLE NAVIGATION MODE.** 독립 GNB / LNB / Dashboard Mode는 함께 사용하지 않는다. 2026-10-08 확정: GNB ONLY는 기존 LNB 부품을 재사용하는 내부 Sidebar를 포함할 수 있다(§15). [공통 정책](../guidelines/navigation-single-mode.md)이 이전 조합 기록보다 우선한다.
@@ -0,0 +1,75 @@
# HMDesign v2 추가 5개 요청 — 선택 검증 보고
RESULT: **PARTIAL**. 브랜드 명칭·8단계 표시 구조와 확정 팔레트, LNB 구분선, Data Table 목록 도구, Separator 문서, Git 없는 문서 검사를 반영했다. Green·Brown은 8/8, Orange는 6/8 확정이다. Orange Dark 1·2는 읽기 전용 ui_viz 원본의 Needs Review 상태를 보존한다. [컬러 근거와 보류](../guidelines/brand-color-steps.md), [공개 계약](../guidelines/runtime-theme-contract.md).
## 작업 환경과 기준
- PROJECT_PATH: `D:/디자인시스템/HMDesign-Gitea-Transfer`
- BRANCH: `main`
- ORIGIN: `https://gitea.hmac.kr/b25027/HMDesign.git`
- BASE_SHA / 시작 Local·Remote HEAD: `1c4e0bb9a1ef4e5db391b93d7c874f78a0e6bcc5`
- 시작 Working Tree: CLEAN. `backup/main-before-transfer-20261011` 및 기존 작업 브랜치 확인, 변경 없음.
- 범위: 기존 Component/Theme와 Storybook Reference 수정(React). 신규 업무 화면 없음. Commit·일반 Push는 후속 명시 지시로 승인됐으며 종료 SHA·시간·Git 상태는 최종 응답 및 로컬 완료 보고에 기록한다.
- 실행 환경: Windows, Node 20.20.1, npm 10.8.2. 최종 정적 Storybook은 6025, 개발 렌더 검사는 6024.
## 5개 수정
| 항목 | 실제 변경과 결과 | 영향 범위 |
|---|---|---|
| 01_COLOR | 한맥그린·한맥브라운·한맥오렌지와 8단계 슬롯. ui_viz 문서·Primitive CSS·inventory에서 Confirmed인 누락 8개만 추가. Green 8/8, Brown 8/8, Orange 6/8. | Color Story·JSON·공개 계약·생성 CSS. 기존 Key/HEX/Mapping 보존. PARTIAL |
| 02_LNB_BORDER | 공통 `gnb.css`의 콘텐츠 LNB 세로선 참조를 stroke-input-default로 변경. 실제 Green/Brown 모두 1px solid rgb(222,219,210). | 기존 콘텐츠 LNB Reference. 외곽 Border·배경·선택/Hover/Focus 유지. PASS |
| 03_DATA_TABLE_LAYOUT | 검색 form과 열 표시를 한 도구 행으로 묶고, 실제 내용 폭이 부족할 때만 줄바꿈. 검색 내부 gap/가운데·왼쪽 정렬 유지. | `hm-data-table-*` opt-in Story 소비 조합. 공통 FilterBar API/CSS 미변경. PASS |
| 04_SEPARATOR_DOC | 같은 제목·설명 묶음에서 필요한 한 곳에만 구분선 적용. 모든 제목에 필수로 추가하는 규칙 없음. | MD만 변경, 기존 Story/Component 스타일·API 유지. PASS |
| 05_GITLESS_DOC_CHECK | Git 메타데이터 유무와 손상/권한 오류 구분. Git 수집 유지, 비Git은 파일 시스템 및 현재 .gitignore 규칙 사용. 0/1/2 종료 코드. | 검사 스크립트·회귀 테스트·README. Node만으로 비Git 실행. PASS |
8개 추가 Key: green-dark-3, brown-light-3, brown-dark-2, brown-dark-3, orange-light-3, orange-light-2, orange-light-1, orange-dark-3 (`color.palette.*`). 기존 73개 입력(71색+radius 2개)을 보존한 81개 입력(79색+radius 2개)이다. Brown Deep #221D13과 새 Dark 2 #2F281C / Dark 3 #1D1810을 alias하지 않는다. Orange Mid #CE5600은 Button 배경 30% stop이며 브랜드 단계로 추정하지 않는다. 기존 Button 계열 Key·값·소비처를 유지한다.
## 실제 Storybook 검증
| 검사 | 결과 |
|---|---|
| COLOR | 390/768/1280/1440px 모두 8개 슬롯×3. 확정 HEX와 computed background 일치. Orange Dark 1/2는 변수·색 미지정. 가로 칩 목록 내부 스크롤만 있으며 문서 가로 넘침 없음. 최종 static 렌더도 확인. |
| GNB_SIDEBAR | Green/Brown의 Border 1px solid rgb(222,219,210), token #dedbd2, LNB·Main 배경 rgb(245,240,229). 1280/1440px Sidebar 272px, 390/768px 숨김 유지. 문서 가로 넘침 없음. 최종 static도 동일. |
| DATA_TABLE_DESKTOP | 1280/1440px 기본·추가 조건 form과 열 버튼 y 좌표 동일. 1440px 기본 1241/1241, 추가 2221/2221. 열 버튼 right=도구 행 right. 검색 gap 8px, 가운데/왼쪽 정렬, Solid Green 조회/Outline Gray 초기화 유지. |
| DATA_TABLE_MOBILE | 390px 기존 조건/검색/버튼 줄바꿈·버튼 행 우측 정렬. 768px 기본 검색은 한 행을 유지하고 열 버튼만 다음 행, 추가 조건은 공간 부족으로 줄바꿈. 4개 폭 모두 문서 scrollWidth=clientWidth. |
| DATA_TABLE_BEHAVIOR | 김민수 draft 입력 시 47개 유지, 조회 시 4개, 초기화 시 47개/빈 입력 복원. 열 메뉴 열림, Escape 후 trigger focus 복귀. |
| SEPARATOR | 최종 static Story 안내·MD 일치. Horizontal 제목/설명 묶음 뒤 Separator 정확히 1개. 스타일 소스 미변경. |
| RUNTIME_CONTRACT | 추가 계약을 포함해 146 checks / 58 rendered parts PASS, KEYBOARD_FOCUS PASS. Default parity, 모든 공개 입력, 기존 Component·독립 역할·partial/invalid/cleanup/scoped/geometry 확인. |
| JSON_LOADER / DESIGNER | 최종 static Loader 127 checks PASS, Designer 96 checks PASS. 기존 17개 역할 UI·Default/partial·Import/Export·reset·geometry 보존. |
개발 서버에서 dist 재생성 중 일시적인 CSS HMR 오류가 있었으나, 최종 빌드 성공 후 정적 Color/GNB/Data Table/Separator/Loader/Designer 화면의 console error는 0건이다. 실패/시도 기록을 최종 PASS와 혼동하지 않는다.
## 기술 검증과 재사용
- TYPECHECK / UI_BUILD: 최종 root·UI·CENTER typecheck 및 workspace build PASS. 색상 계약 추가 이후 실행했고 동일 조건으로 다시 실행하지 않았다.
- STORYBOOK_BUILD: 최종 PASS. 기존 bundler 성능/큰 chunk 경고 유지. 앞선 build 이후 CSS·계약이 변경되어 최종 산출물에 필요한 한 번만 추가 build했다.
- TOKEN_COMPATIBILITY: 시작 기준의 모든 기존 JSON 값·Key, contract Property/type, 기존 Primitive/default CSS 선언 동일. 이전 전체 JSON·서비스 partial JSON·empty theme 유효. Needs Review인 orange-dark-1/2는 공개 입력 거부.
- PUBLIC_EXPORT: UI 255 / Theme 8, 공개 export 소스·package exports 미변경. Runtime Token union은 승인된 팔레트 8개만 가산.
- CSS_IMPORTS: 추적 CSS의 상대경로 import 57개, 누락 0. 외부/패키지 import까지 합한 과거 수량과 혼용하지 않는다.
- STORYBOOK_ID: 최종 index 292개. Story title/export 이름 추가·삭제 없음.
- 문서 검사 회귀: 4/4 PASS. Git/비Git 동등 범위, 한글·공백 경로/중복 heading/explicit id, 코드블록 제외, 실제 누락/heading/_local 오류, nested ignore/negation, 손상 Git metadata exit 2 확인.
- GITLESS_DOC_CHECK: 현재 추적·신규 소스 598개만 복사한 별도 `_local/v2-gitless`에서 `.git` 없이 실행, Git 모드와 MD·링크 수 및 오류 목록 동일. 최종 104 MD / 463 internal links / 오류 0. 이 보고서 추가 전의 103 MD / 460 links와 구분한다.
- RELATED_GUARD: 수정 소비 영역 PARTIAL / STORYBOOK 검사. 원본 status/staticStatus NOT VERIFIED, FAIL 0. styleEvidence STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED. 결합 styleVerification은 생성하지 않았다. 실제 브라우저 PASS는 별도 근거이며 정적 원본을 덮어쓰지 않는다.
- REUSED PASS: 이전 Guard100·Component15 중 코드/fixtures·영향이 변하지 않은 정책/기능 검사만 재사용. 계약 소비자는 위 Runtime/Loader/Designer로 새로 검증. 과거 전체 Theme/Story/AI 생성 근거를 최신 전체 PASS로 승격하지 않는다.
- NOT VERIFIED: 모든 Story×너비 전수, 실기기 터치·SR 낭독, 신규 Theme CDN 배포. 이번 요청의 변경 영역 외 전체 생산 앱/AI 생성 회귀는 반복 생략.
## 기존 ZIP 별도 확인
읽기 전용 `D:/디자인시스템/HMDesign-main.zip`에는 검사기 자체가 없다. `_local/v2-existing-zip`에 압축을 해제하고 새 검사기만 복사하여 Node로 실행했다. Git 실행 실패 없이 84 MD / 307 internal links를 검사했고, 실제 과거 링크 문제 3건을 정상 검출했다: README와 shadcn 가이드의 삭제된 AGENTS heading 2건, runtime-theme-report의 제외된 _local 증거 1건. 이 과거 ZIP을 수정하거나 새 ZIP을 생성하지 않았다. 현재 공식 소스 사본의 오류 0과 구분한다.
## 보존과 남은 확인
D-01/D-02, Tabs 상단 White 80%·Overflow, Green LNB/목차 8%, Button/Gray/Dialog, Table 20%, Progress/Accordion, Sheet/Drawer, 모바일 Navigation, 기존 Component API, Guard 정책은 시작 기준 소스와 비교해 보존했다. 추가 색상 외 기존 Theme 값·Typography·Spacing·geometry는 동일하다. ui_viz 참조 파일은 조사 전후 SHA-256 동일, 쓰기 없음. GitHub 원본 폴더/remote에는 쓰기·Push 없음. 백업 브랜치 및 Gitea 기존 이력 유지, Force Push·Reset·Clean 없음.
Orange Dark 1 #BC4C00 / Dark 2 #883700은 ui_viz Color §4.1·inventory·Primitive가 Needs Review다. 승인 확인 전 공개 palette에 넣지 않는다. 기존 orange-border #BC4C00의 Button 범위 승인과 별개다. 현재 안전하게 완료한 나머지 수정은 일반 Commit·Push 대상이다.
## 수정 파일 범위
- Color: `src/foundation/color.stories.tsx`, `themes/default.json`, `packages/ui/src/theme/contract.ts`, `scripts/theme-templates/{primitives,default}.css` 및 생성 `packages/ui/src/theme/{primitives.css,default.css,default-values.generated.ts}`.
- LNB: `packages/ui/src/theme/gnb.css`, `docs/patterns/gnb.md`.
- Data Table: `src/patterns/data-table.stories.tsx`, `src/list-screen.css`, `docs/patterns/data-table.md`(실제 조회 적용 안내도 정정).
- Separator: `docs/components/separator.md`.
- 문서 검사: `scripts/check-doc-links.mjs`, `scripts/check-doc-links.test.mjs`, `README.md`.
- 계약 정합성: `src/theme/runtime-loader.stories.tsx`의 전체 컬러 수 assertion을 실제 contract에서 계산; `docs/guidelines/{brand-color-steps,runtime-theme,runtime-theme-contract,designer-theme-guide,theme-delivery,개발-적용-가이드,프로젝트-지침}.md`의 현행 수량/설명. 과거 날짜별 검증 보고와 baseline snapshot은 보존.
- 본 보고서는 검증 근거 및 보류 범위를 기록한다. 로컬 `_local` 검사 사본·로그·이미지는 Stage/Commit하지 않는다.
- `AGENTS.md`에는 사용자의 별도 후속 지시인 공식 작업 경로·시작/종료 Git 확인·검증 후 일반 Commit/Push·원격 변경 시 중단 정책을 기록했다. 기존 화면 구현 필수 규칙은 보존했다.
+9
View File
@@ -75,6 +75,15 @@ export const runtimeThemeContract = {
"color.palette.gray": { property: "--hm-sem-palette-gray", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray"], type: "color" },
"color.palette.gray-dark": { property: "--hm-sem-palette-gray-dark", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-dark"], type: "color" },
"color.palette.indicator": { property: "--hm-sem-palette-indicator", defaultValue: runtimeThemeDefaults["--hm-sem-palette-indicator"], type: "color" },
// Confirmed ui_viz brand stages; legacy keys and component mappings remain unchanged.
"color.palette.green-dark-3": { property: "--hm-sem-palette-green-dark-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-dark-3"], type: "color" },
"color.palette.brown-light-3": { property: "--hm-sem-palette-brown-light-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-light-3"], type: "color" },
"color.palette.brown-dark-2": { property: "--hm-sem-palette-brown-dark-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-dark-2"], type: "color" },
"color.palette.brown-dark-3": { property: "--hm-sem-palette-brown-dark-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-dark-3"], type: "color" },
"color.palette.orange-light-3": { property: "--hm-sem-palette-orange-light-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-light-3"], type: "color" },
"color.palette.orange-light-2": { property: "--hm-sem-palette-orange-light-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-light-2"], type: "color" },
"color.palette.orange-light-1": { property: "--hm-sem-palette-orange-light-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-light-1"], type: "color" },
"color.palette.orange-dark-3": { property: "--hm-sem-palette-orange-dark-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-dark-3"], type: "color" },
} as const
export type RuntimeThemeToken = keyof typeof runtimeThemeContract
@@ -73,5 +73,13 @@ export const runtimeThemeDefaults = {
"--hm-sem-palette-orange-border-deep": "#5e2902",
"--hm-sem-palette-gray": "#767676",
"--hm-sem-palette-gray-dark": "#5a5a5a",
"--hm-sem-palette-indicator": "#2d2d2d"
"--hm-sem-palette-indicator": "#2d2d2d",
"--hm-sem-palette-green-dark-3": "#020a07",
"--hm-sem-palette-brown-light-3": "#c1a770",
"--hm-sem-palette-brown-dark-2": "#2f281c",
"--hm-sem-palette-brown-dark-3": "#1d1810",
"--hm-sem-palette-orange-light-3": "#ec8f53",
"--hm-sem-palette-orange-light-2": "#f87725",
"--hm-sem-palette-orange-light-1": "#eb5f00",
"--hm-sem-palette-orange-dark-3": "#4f2000"
} as const
+12
View File
@@ -201,3 +201,15 @@
--ring: var(--hm-sem-stroke-input-focus);
--radius: var(--hm-radius-container);
}
/* Additive brand palette; does not change existing component aliases. */
:root, [data-hm-theme] {
--hm-sem-palette-green-dark-3: var(--hm-green-dark-3);
--hm-sem-palette-brown-light-3: var(--hm-brown-light-3);
--hm-sem-palette-brown-dark-2: var(--hm-brown-dark-2);
--hm-sem-palette-brown-dark-3: var(--hm-brown-dark-3);
--hm-sem-palette-orange-light-3: var(--hm-orange-light-3);
--hm-sem-palette-orange-light-2: var(--hm-orange-light-2);
--hm-sem-palette-orange-light-1: var(--hm-orange-light-1);
--hm-sem-palette-orange-dark-3: var(--hm-orange-dark-3);
}
+1 -1
View File
@@ -566,7 +566,7 @@
--hm-lnb-group-open-background: transparent;
--hm-lnb-group-open-line: 0;
--hm-lnb-focus-outline: 2px solid var(--hm-gnb-content-menu-color);
border-right: 1px solid var(--hm-sem-stroke-default);
border-right: 1px solid var(--hm-sem-stroke-input-default);
}
[data-hm-gnb-layout="sidebar"][data-hm-color="brown"] > [data-hm-gnb-body] > [data-hm-gnb-sidebar] {
--hm-gnb-content-menu-color: var(--hm-sem-palette-brown-default);
+12
View File
@@ -44,3 +44,15 @@
:root {
--hm-yellow-accent: #ffc600;
}
/* Confirmed brand palette additions (ui_viz Color 4.1). */
:root {
--hm-green-dark-3: #020a07;
--hm-brown-light-3: #c1a770;
--hm-brown-dark-2: #2f281c;
--hm-brown-dark-3: #1d1810;
--hm-orange-light-3: #ec8f53;
--hm-orange-light-2: #f87725;
--hm-orange-light-1: #eb5f00;
--hm-orange-dark-3: #4f2000;
}
+65 -2
View File
@@ -3,7 +3,70 @@ import path from 'node:path'
import { execFileSync } from 'node:child_process'
const root = process.cwd()
const files = [...new Set(execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { encoding: 'utf8' }).split('\0'))].filter(file => file.endsWith('.md'))
// A portable checkout must not accidentally use a parent directory's Git repository.
let collectionMode
try {
fs.lstatSync(path.join(root, '.git'))
collectionMode = 'git'
} catch (error) {
if (error.code === 'ENOENT') collectionMode = 'filesystem'
else {
console.error(JSON.stringify({ error: 'Git metadata detection failed', detail: error.message }))
process.exit(2)
}
}
function ignoreRules(directory) {
const ignoreFile = path.join(directory, '.gitignore')
let source
try { source = fs.readFileSync(ignoreFile, 'utf8') }
catch (error) { if (error.code === 'ENOENT') return []; throw error }
return source.split(/\r?\n/).flatMap(line => {
if (!line || line.startsWith('#')) return []
const negate = line.startsWith('!')
let pattern = (negate ? line.slice(1) : line).trimEnd()
const directoryOnly = pattern.endsWith('/')
if (directoryOnly) pattern = pattern.slice(0, -1)
const anchored = pattern.startsWith('/') || pattern.includes('/')
pattern = pattern.replace(/^\//, '')
// Repository ignore rules use literal paths, *, **, ? and negation.
let expression = ''
for (let i = 0; i < pattern.length; i++) {
const char = pattern[i]
if (char === '*' && pattern[i + 1] === '*') {
i++
if (pattern[i + 1] === '/') { expression += '(?:.*/)?'; i++ }
else expression += '.*'
} else if (char === '*') expression += '[^/]*'
else if (char === '?') expression += '[^/]'
else expression += char.replace(/[\\^$+?.()|{}\[\]]/g, '\\$&')
}
return [{ directory, negate, directoryOnly, regex: new RegExp(`${anchored ? '^' : '(?:^|/)'}${expression}$`) }]
})
}
function filesystemMarkdown(directory = root, inherited = []) {
const rules = [...inherited, ...ignoreRules(directory)]
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
if (entry.name === '.git') return []
const absolute = path.join(directory, entry.name)
let ignored = false
for (const rule of rules) {
const relative = path.relative(rule.directory, absolute).replaceAll('\\', '/')
if ((!rule.directoryOnly || entry.isDirectory()) && rule.regex.test(relative)) ignored = !rule.negate
}
if (ignored) return []
if (entry.isDirectory()) return filesystemMarkdown(absolute, rules)
return entry.isFile() && entry.name.endsWith('.md') ? [path.relative(root, absolute).replaceAll('\\', '/')] : []
})
}
let files
try {
files = collectionMode === 'git'
? [...new Set(execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).split('\0'))].filter(file => file.endsWith('.md'))
: filesystemMarkdown().sort()
} catch (error) {
console.error(JSON.stringify({ collectionMode, error: 'Markdown collection failed', detail: error.message }))
process.exit(2)
}
const stripCode = text => text.replace(/^```[^\n]*\n[\s\S]*?^```[^\n]*$/gm, '')
const slug = text => text.toLowerCase().replace(/`/g, '').replace(/[^\p{Letter}\p{Number}\p{Mark}_\- ]/gu, '').replace(/ /g, '-')
const anchors = file => {
@@ -30,5 +93,5 @@ for (const file of files) for (const [, raw] of stripCode(fs.readFileSync(file,
if (reason) problems.push({ file, href, reason })
} catch (error) { problems.push({ file, href, reason: error.message }) }
}
console.log(JSON.stringify({ files: files.length, internalLinks, problems }, null, 2))
console.log(JSON.stringify({ collectionMode, files: files.length, internalLinks, problems }, null, 2))
if (problems.length) process.exitCode = 1
+76
View File
@@ -0,0 +1,76 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import os from 'node:os'
import { fileURLToPath } from 'node:url'
import { spawnSync, execFileSync } from 'node:child_process'
const script = fileURLToPath(new URL('./check-doc-links.mjs', import.meta.url))
function fixture(t, contents) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hm-doc-links-'))
t.after(() => {
const relative = path.relative(path.resolve(os.tmpdir()), path.resolve(root))
assert.ok(relative.startsWith('hm-doc-links-') && !relative.includes(path.sep), 'cleanup must stay in its own temporary fixture')
fs.rmSync(root, { recursive: true, force: true })
})
for (const [name, source] of Object.entries(contents)) {
const target = path.join(root, name)
fs.mkdirSync(path.dirname(target), { recursive: true })
fs.writeFileSync(target, source)
}
return root
}
function run(root) {
const result = spawnSync(process.execPath, [script], { cwd: root, encoding: 'utf8' })
return { status: result.status, report: JSON.parse(result.stdout || result.stderr) }
}
test('Git and filesystem modes preserve current ignore scope and link counts', t => {
const root = fixture(t, {
'.gitignore': 'node_modules/\n_local/\n**/dist-dashboard/\n*.log\n.env.*\n!.env.example\n',
'README.md': '[한글](docs/a%20b.md#한글-제목) [asset](.env.example)\n```md\n[x](missing.md)\n```\n',
'docs/a b.md': '# 한글 제목\n# 한글 제목\n[duplicate](#한글-제목-1) [id](#custom)\n<a id="custom"></a>\n',
'.env.example': '',
'node_modules/package/bad.md': '[x](missing.md)',
'_local/bad.md': '[x](missing.md)',
'app/dist-dashboard/bad.md': '[x](missing.md)',
})
const portable = run(root)
assert.equal(portable.status, 0)
assert.equal(portable.report.collectionMode, 'filesystem')
assert.equal(portable.report.files, 2)
assert.equal(portable.report.internalLinks, 4)
execFileSync('git', ['init', '--quiet', root])
execFileSync('git', ['add', '.'], { cwd: root })
const repository = run(root)
assert.equal(repository.status, 0)
assert.equal(repository.report.collectionMode, 'git')
assert.deepEqual({ ...repository.report, collectionMode: 'filesystem' }, portable.report)
})
test('missing paths, headings and excluded local evidence return exit 1', t => {
const root = fixture(t, {
'README.md': '[path](missing.md) [heading](doc.md#missing) [local](_local/proof.json)',
'doc.md': '# Present', '_local/proof.json': '{}',
})
const result = run(root)
assert.equal(result.status, 1)
assert.deepEqual(result.report.problems.map(problem => problem.reason), ['missing path', 'missing heading', 'local evidence is excluded from Git Clone'])
})
test('nested ignore rules and negation retain the intended Markdown files', t => {
const root = fixture(t, {
'.gitignore': '*.md\n!README.md\n', 'README.md': '# Readme', 'ignored.md': '[x](missing)',
'docs/.gitignore': '!keep.md\n', 'docs/keep.md': '# Kept', 'docs/drop.md': '[x](missing)',
})
assert.equal(run(root).report.files, 2)
})
test('broken Git metadata is an operational error, never a silent fallback', t => {
const root = fixture(t, { '.git': 'gitdir: missing-metadata', 'README.md': '# Readme' })
const result = run(root)
assert.equal(result.status, 2)
assert.equal(result.report.collectionMode, 'git')
assert.equal(result.report.error, 'Markdown collection failed')
})
+12
View File
@@ -200,3 +200,15 @@
--ring: var(--hm-sem-stroke-input-focus);
--radius: var(--hm-radius-container);
}
/* Additive brand palette; does not change existing component aliases. */
:root, [data-hm-theme] {
--hm-sem-palette-green-dark-3: {{color.palette.green-dark-3|var(--hm-green-dark-3)}};
--hm-sem-palette-brown-light-3: {{color.palette.brown-light-3|var(--hm-brown-light-3)}};
--hm-sem-palette-brown-dark-2: {{color.palette.brown-dark-2|var(--hm-brown-dark-2)}};
--hm-sem-palette-brown-dark-3: {{color.palette.brown-dark-3|var(--hm-brown-dark-3)}};
--hm-sem-palette-orange-light-3: {{color.palette.orange-light-3|var(--hm-orange-light-3)}};
--hm-sem-palette-orange-light-2: {{color.palette.orange-light-2|var(--hm-orange-light-2)}};
--hm-sem-palette-orange-light-1: {{color.palette.orange-light-1|var(--hm-orange-light-1)}};
--hm-sem-palette-orange-dark-3: {{color.palette.orange-dark-3|var(--hm-orange-dark-3)}};
}
+12
View File
@@ -43,3 +43,15 @@
:root {
--hm-yellow-accent: {{color.palette.yellow-accent}};
}
/* Confirmed brand palette additions (ui_viz Color 4.1). */
:root {
--hm-green-dark-3: {{color.palette.green-dark-3}};
--hm-brown-light-3: {{color.palette.brown-light-3}};
--hm-brown-dark-2: {{color.palette.brown-dark-2}};
--hm-brown-dark-3: {{color.palette.brown-dark-3}};
--hm-orange-light-3: {{color.palette.orange-light-3}};
--hm-orange-light-2: {{color.palette.orange-light-2}};
--hm-orange-light-1: {{color.palette.orange-light-1}};
--hm-orange-dark-3: {{color.palette.orange-dark-3}};
}
+21 -14
View File
@@ -2,23 +2,30 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { runtimeThemeContract, type RuntimeThemeToken } from "@hm-design/ui/theme"
import { ReferencePage, ReferenceSection } from "./reference"
// Existing palette tokens only; values come from the public default contract.
// ui_viz Color 4.1 confirmed stages only. Orange Dark 1/2 remain Needs Review.
const stages = ["light-4", "light-3", "light-2", "light-1", "default", "dark-1", "dark-2", "dark-3"] as const
const palettes = [
{ name: "Green", prefix: "green", stages: ["light-4", "light-3", "light-2", "light-1", "default", "dark-1", "dark-2"] },
{ name: "Brown", prefix: "brown", stages: ["light-4", "light-2", "light-1", "default", "dark-1", "deep"] },
{ name: "Orange", prefix: "orange", stages: ["light-4", "light", "mid", "default", "dark", "deep"] },
{ name: "한맥그린", prefix: "green", confirmed: [...stages], legacy: [] },
{ name: "한맥브라운", prefix: "brown", confirmed: [...stages], legacy: ["deep"] },
{ name: "한맥오렌지", prefix: "orange", confirmed: ["light-4", "light-3", "light-2", "light-1", "default", "dark-3"], legacy: ["light", "mid", "dark", "deep"] },
]
const stageLabel = (stage: string) => stage === "default" ? "Default · 기본색" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase())
function ColorChip({ prefix, stage, confirmed = true }: { prefix: string; stage: string; confirmed?: boolean }) {
const token = `color.palette.${prefix}-${stage}` as RuntimeThemeToken
const definition = confirmed ? runtimeThemeContract[token] : undefined
return <article className="hm-brand-color-chip" data-color-stage={stage} data-color-token={definition ? token : undefined} data-base={stage === "default" ? "" : undefined}>
<div className="hm-foundation-swatch" style={definition ? { background: `var(${definition.property})` } : undefined} />
<h3>{stageLabel(stage)}</h3><p>{definition ? definition.defaultValue.toUpperCase() : "미확정"}</p>
</article>
}
function ColorReferencePage() {
return <ReferencePage title="Color" intro="현재 HM 브랜드 컬러를 밝은색 → 기본색 → 어두운색 순서로 비교합니다. 기본색을 강조하며 컬러의 상세 의미와 적용 규칙은 추후 보완합니다.">
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="기존 컬러값을 유지합니다. 가로 컬러칩에서 단계명과 HEX를 확인합니다.">
<div className="hm-brand-color-row">{palette.stages.map(stage => {
const token = `color.palette.${palette.prefix}-${stage}` as RuntimeThemeToken
const definition = runtimeThemeContract[token]
return <article key={stage} className="hm-brand-color-chip" data-base={stage === "default" ? "" : undefined}>
<div className="hm-foundation-swatch" style={{ background: `var(${definition.property})` }} />
<h3>{stage === "default" ? "Default · 기본색" : stage.replaceAll("-", " ")}</h3><p>{definition.defaultValue.toUpperCase()}</p>
</article>
})}</div>
return <ReferencePage title="Color" intro="한맥 브랜드 컬러를 8단계로 비교합니다. 확정된 기존 색상만 표시하며, 미확정 단계는 색상을 지정하지 않습니다. 기본색을 강조합니다.">
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="Light 4 → Light 3 → Light 2 → Light 1 → Default → Dark 1 → Dark 2 → Dark 3. 미확정 단계는 추가 확인이 필요합니다.">
<div className="hm-brand-color-row">{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} confirmed={palette.confirmed.includes(stage)} />)}</div>
{palette.legacy.length > 0 && <>
<p>기존 버튼 그라데이션 색상 · 브랜드 단계와 별도 유지</p>
<div className="hm-brand-color-row">{palette.legacy.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} />)}</div>
</>}
</ReferenceSection>)}
</ReferencePage>
}
+5
View File
@@ -1,4 +1,9 @@
/* Opt-in list composition. FilterBar's general layout contract stays unchanged. */
/* Data Table Story composition only: intrinsic search width decides when tools wrap. */
.hm-data-table-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--hm-filter-bar-gap); min-width: 0; }
.hm-data-table-search { flex: 1 0 max-content; min-width: 0; max-width: 100%; }
.hm-data-table-search [data-slot="filter-bar-search"] { width: var(--hm-filter-bar-search-width); max-width: 100%; }
.hm-data-table-view-options { flex: 0 0 auto; margin-left: auto; }
.hm-list-search-basic, .hm-list-search-extended { gap: 8px; }
.hm-list-search-basic { justify-content: center; }
.hm-list-search-extended { justify-content: flex-start; }
+17 -15
View File
@@ -122,19 +122,21 @@ function WithFilterBar({ extended = true }: { extended?: boolean }) {
<PageHeader><PageHeaderContent><PageHeaderTitle>사용자 목록</PageHeaderTitle><PageHeaderDescription>조건을 입력한 뒤 조회합니다.</PageHeaderDescription></PageHeaderContent></PageHeader>
<Card className="min-w-0"><CardContent>
<div className="grid min-w-0 gap-4">
<form onSubmit={event => { event.preventDefault(); apply() }}>
<FilterBar className={extended ? "hm-list-search-extended" : "hm-list-search-basic"}>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger size="medium" aria-label="상태"><SelectValue placeholder="상태" /></SelectTrigger>
<SelectContent><SelectItem value="all">전체 상태</SelectItem>{statusOptions.map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectContent>
</Select>
{extended && <><Combobox controlled size="medium" aria-label="부서" options={departmentOptions} value={department} placeholder="부서" onValueChange={setDepartment} />
<DatePicker controlled size="medium" aria-label="등록일" placeholder="등록일" value={date} onValueChange={setDate} /></>}
<FilterBarSearch value={search} onValueChange={setSearch} placeholder="이름·부서 검색" />
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button></FilterBarActions>
</FilterBar>
</form>
<div className="flex justify-end"><DataTableViewOptions table={table} labels={columnLabels} /></div>
<div className="hm-data-table-tools">
<form className="hm-data-table-search" onSubmit={event => { event.preventDefault(); apply() }}>
<FilterBar className={extended ? "hm-list-search-extended" : "hm-list-search-basic"}>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger size="medium" aria-label="상태"><SelectValue placeholder="상태" /></SelectTrigger>
<SelectContent><SelectItem value="all">전체 상태</SelectItem>{statusOptions.map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectContent>
</Select>
{extended && <><Combobox controlled size="medium" aria-label="부서" options={departmentOptions} value={department} placeholder="부서" onValueChange={setDepartment} />
<DatePicker controlled size="medium" aria-label="등록일" placeholder="등록일" value={date} onValueChange={setDate} /></>}
<FilterBarSearch value={search} onValueChange={setSearch} placeholder="이름·부서 검색" />
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={reset}>초기화</Button></FilterBarActions>
</FilterBar>
</form>
<div className="hm-data-table-view-options"><DataTableViewOptions table={table} labels={columnLabels} /></div>
</div>
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
</div>
</CardContent></Card>
@@ -159,8 +161,8 @@ export const All: Story = {
<StorySection title="Overview" description="행 선택(헤더 혼합 상태), 정렬, 열 표시, 행 작업, 페이지 크기와 페이지 이동을 실제로 조작합니다.">
<Basic />
</StorySection>
<StorySection title="기본 목록 검색" description="조건·검색어·조회·초기화를 8px 간격으로 가운데 정렬합니다. 제목은 Card 밖에 배치합니다."><WithFilterBar extended={false} /></StorySection>
<StorySection title="추가 조건 목록 검색" description="부서·등록일을 제공하는 구성은 입력값과 관계없이 왼쪽 정렬합니다. 모바일 버튼 행은 우측 정렬합니다."><WithFilterBar /></StorySection>
<StorySection title="기본 목록 검색" description="조건·검색어·조회·초기화를 8px 간격으로 가운데 정렬합니다. 공간이 충분하면 열 버튼은 같은 행 우측, 부족할 때만 다음 행에 배치합니다. 제목은 Card 밖에 배치합니다."><WithFilterBar extended={false} /></StorySection>
<StorySection title="추가 조건 목록 검색" description="부서·등록일을 제공하는 구성은 입력값과 관계없이 왼쪽 정렬합니다. 열 버튼은 공간이 충분하면 같은 행 우측에 배치하며 모바일 버튼 행은 우측 정렬합니다."><WithFilterBar /></StorySection>
</StoryPage>
),
}
+1 -1
View File
@@ -39,7 +39,7 @@ async function verifyLoader(container: HTMLElement) {
const initial = JSON.stringify(geometry())
const origin = performance.timeOrigin
const defaults = await fetchRuntimeTheme("/themes/default.json", { cache: "no-store" })
assert(Object.keys(defaults.tokens).length === 71, "Default JSON exposes 71 color contract entries")
assert(Object.keys(defaults.tokens).length === Object.values(runtimeThemeContract).filter(token => token.type === "color").length, "Default JSON exposes every public color contract entry")
for (const [key, value] of Object.entries(defaults.tokens)) assert(defaultRuntimeTheme.tokens[key as keyof typeof runtimeThemeContract] === value, `${key}: JSON equals generated package fallback`)
assert(await loadRuntimeTheme(root, "/themes/default.json", { request: { cache: "no-store" } }), "Default JSON uses official URL Loader")
assert(JSON.stringify(geometry()) === initial, "Default JSON preserves component geometry")
+9 -1
View File
@@ -71,6 +71,14 @@
"color.palette.orange-border-deep": "#5e2902",
"color.palette.gray": "#767676",
"color.palette.gray-dark": "#5a5a5a",
"color.palette.indicator": "#2d2d2d"
"color.palette.indicator": "#2d2d2d",
"color.palette.green-dark-3": "#020a07",
"color.palette.brown-light-3": "#c1a770",
"color.palette.brown-dark-2": "#2f281c",
"color.palette.brown-dark-3": "#1d1810",
"color.palette.orange-light-3": "#ec8f53",
"color.palette.orange-light-2": "#f87725",
"color.palette.orange-light-1": "#eb5f00",
"color.palette.orange-dark-3": "#4f2000"
}
}