fix: align v2 publisher review with HMDesign stories and tokens
This commit is contained in:
1 parent
1c4e0bb9a1
commit
7b22fe88a1
27 files changed
+413
-49
No files matched your search
@@ -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를 금지한다.
|
||||
@@ -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
|
||||
|
||||
@@ -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. 미확정 / 보류
|
||||
|
||||
@@ -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이다.
|
||||
@@ -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 편집기 설치는 이번 코드 변경을 적용하는 최초 빌드가 필요합니다. 설치 후 컬러 값을 바꾸는 운영에는 재빌드가 필요 없습니다.
|
||||
|
||||
|
||||
@@ -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 승격을 검토한다.
|
||||
@@ -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할 수 있다.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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·캐시·실패 처리, 검증된 값 적용 |
|
||||
|
||||
@@ -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) 참조).
|
||||
|
||||
|
||||
@@ -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 식별자 줄바꿈과 열 폭
|
||||
|
||||
@@ -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·원격 변경 시 중단 정책을 기록했다. 기존 화면 구현 필수 규칙은 보존했다.
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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)}};
|
||||
}
|
||||
@@ -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}};
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user