# Runtime / AI Independent Portability Policy 이 문서는 HM Design System 결과물을 React 외 실행 형식으로 옮길 때 지켜야 하는 공통 계약이다. 특정 AI 모델, IDE, 유료 기능, 프레임워크 또는 빌드 도구를 전제로 하지 않는다. ## 1. Source of Truth와 Target Runtime 디자인 시스템의 기준은 다음 순서다. 1. Design Token과 `themes/*.json` 2. CSS Custom Properties와 Runtime Theme 계약 3. Component visual state 및 interaction contract 4. Component implementation 5. Storybook/Preview 작업 시작 전에 Target Runtime을 기록한다. Source of Truth 우선순위는 `themes/default.json`/공개 Theme contract → Runtime-neutral Component/Pattern contract → CSS Custom Properties → Runtime adapter → Storybook/Preview 순서다. - `REACT_VITE`: 현재 React/Vite workspace와 `@hm-design/ui` 공개 API를 사용한다. - `STATIC_HTML`: HTML을 `file://`로 직접 열 수 있어야 한다. JSX/TSX/TypeScript, Vite 전용 기능, bare package import를 사용하지 않는다. 저장소 내 샘플은 승인된 상대 Theme artifact와 상대 font/compatibility CSS를 사용하며, `file://`에서 `/src/...` 절대 경로를 사용하지 않는다. - `VANILLA_WEB`: 일반 웹 서버 또는 정적 호스팅에서 HTML/CSS/JavaScript로 실행한다. - `OTHER_RUNTIME`: Vue, Svelte, Angular, Web Component 등으로 변환하더라도 HM Token·Theme·Component Contract를 유지한다. Runtime이 달라져도 디자인을 재해석하거나 React 구현을 삭제하지 않는다. 기존 구현을 Reference로 삼아 같은 Design Contract와 Theme Contract를 소비하는 adapter/standalone implementation을 추가한다. ## 2. 실제 HM 계약과 파일 경로 다음 파일을 추측으로 복제하거나 별도 값으로 재작성하지 않는다. - `themes/default.json`: HM Default 컬러의 공식 원본 - `packages/ui/src/theme/contract.ts`: 공개 Runtime Token, CSS property, 타입·검증 계약 - `packages/ui/src/theme/runtime.ts`: Global/Scoped 적용·해제와 atomic validation - `packages/ui/src/theme/loader.ts`: URL fetch/resolve/load 경로 - `packages/ui/src/theme.css`: package Theme CSS 진입점 - `packages/ui/src/theme/default.css`, `primitives.css`, `.css`: fallback, Semantic, Component Mapping - `packages/ui/package.json`: 공개 export인 `@hm-design/ui/theme`, `@hm-design/ui/theme.css`, `@hm-design/ui/tailwind.css` - `docs/guidelines/runtime-theme-contract.md`: Token별 CSS variable, 기본값, 영향 범위 React 소비 앱은 다음 순서로 HM CSS를 로드한다. ```css @import "tailwindcss"; @import "@hm-design/ui/tailwind.css"; @import "@hm-design/ui/theme.css"; ``` Standalone 결과물은 승인된 Theme CSS artifact를 상대 경로로 연결하거나, 프로젝트가 생성한 standalone artifact를 포함한다. AI가 `#...` 색상, 임의 spacing/radius, Theme variable을 추측해 복제하지 않는다. 실제 변수명과 허용 입력은 Runtime 계약표를 기준으로 한다. ### 2.1 Tailwind v4 소비자 검색 범위 현재 저장소의 `package.json`과 lockfile은 `tailwindcss / @tailwindcss/vite` **4.3.3**을 사용하며, [Vite 설정](../../vite.config.ts)은 `react()`와 `tailwindcss()` plugin을 연결한다. 앱은 [src/index.css](../../src/index.css)의 Tailwind/HM Tailwind와 [src/hm-theme.css](../../src/hm-theme.css)의 HM Theme를 [main.tsx](../../src/main.tsx)에서 로드한다. 외부 React 소비 앱도 CSS entry를 실제 bootstrap에서 import해야 한다. Tailwind는 기본적으로 실행 cwd를 기준으로 자동 검색하고 `node_modules`, `.gitignore` 대상 등을 제외한다. 외부 프로젝트의 junction 또는 넓은 실행 루트에서 검색 지연이 발생하면 실제 CSS 위치·실행 cwd·연결 폴더를 확인한 뒤 앱 소스 범위만 지정한다. `source("./")`는 CSS 파일의 위치를 기준으로 자동 검색 기준 경로를 정하는 지원 구문이며, 모든 junction 지연을 해결한다고 보증하지 않는다. 아래는 **소비 앱 CSS가 `src/index.css`이고 앱 코드가 `src/`에 있는 경우**의 예다. ```css @import "tailwindcss" source("./"); @import "@hm-design/ui/tailwind.css"; @import "@hm-design/ui/theme.css"; /* Dashboard 상태를 표시하는 화면만 Theme 다음에 추가 */ @import "@hm-design/ui/dashboard-status.css"; @source "../index.html"; ``` CSS entry가 루트에 있으면 같은 `source("./")`는 루트 전체를 뜻하므로 제한 효과가 다르다. `src/` 밖 공유 UI가 있다면 그 실제 경로를 `@source`로 추가한다. 명시 목록으로 제한할 때는 `source(none)` + 필요한 `@source`도 지원하지만, 앱·공유 UI·HM source를 빠짐없이 지정해야 한다. [공식 Tailwind 소스 검색 문서](https://tailwindcss.com/docs/detecting-classes-in-source-files). **HM Component 클래스 보존**: 공개 `@hm-design/ui/tailwind.css`를 생략하지 않는다. [패키지 소스](../../packages/ui/src/tailwind.css)는 `./components / ./patterns`를 검색하고, [기존 package build](../../scripts/build-package.mjs)는 배포 CSS에서 이를 `@source "./index.js"`로 바꾼다. 따라서 앱 자동 검색을 `src/`로 좁혀도 HM compiled 클래스는 해당 CSS의 명시 source를 통해 검색한다. 소비자가 소스 alias로 직접 연결한다면 배포 CSS와 경로가 같다고 가정하지 말고 실제 source 파일 존재를 확인한다. HM package 전체나 외부 `node_modules` 전체를 추가 검색 대상으로 삼지 않는다. 완료 시 앱 CSS import 순서, HM `@source` 대상 존재, 필요한 utility 생성 여부(예: GNB 모바일 trigger의 `lg:hidden`, Badge의 flex·크기 class)를 실제 소비 빌드에서 확인한다. 이 문서 작업에서는 버전·설정·구문 지원·source 연결만 대조했다. junction 지연 재현과 외부 앱 utility 생성은 미검증이며, 새 설정 파일·의존성·빌드는 추가하지 않았다. ## 3. 변환 시 보존해야 하는 계약 layout, spacing, typography, border, radius, icon 크기·정렬, responsive behavior와 hover/focus/active/selected/disabled/checked/error/loading 상태를 유지한다. 접근성 semantic element, keyboard behavior, ARIA 속성도 Target Runtime에 맞는 방식으로 보존한다. 프레임워크 기능은 Target Runtime의 최소 구현으로 변환한다. - React state/props → 명시적 DOM state와 Vanilla JavaScript state - event handler → `addEventListener` - conditional className → class/attribute toggle - component composition → semantic HTML structure - package icon → 배포 가능한 SVG 또는 허용된 icon artifact 단순 layout·typography·color·responsive 표현은 JavaScript에 의존하지 않는다. JavaScript는 accordion, dropdown, modal, tabs 등 실제 interaction에만 추가한다. ### 3.1 Dashboard 상태 계약 Dashboard 업무 상태의 Runtime-neutral 기준은 `packages/ui/src/patterns/dashboard-status/contract.ts`다. 별도 Runtime은 다음을 동일하게 유지한다. - 상태 ID: `active`, `hold`, `done`; 필터 ID: `all`, `active`, `hold`, `done`, `issue` - `issue`는 독립 Boolean flag이며 상태 값과 동시에 존재할 수 있음 - `done` label은 `완료` - `all`은 모든 record, `issue`는 `issue === true`, 나머지는 `record.status === filter`로 매칭 - 상태 표시에는 `data-status`, 이슈에는 `data-issue="true"`, 필터 컨트롤에는 `data-filter`, 선택에는 `aria-pressed="true|false"` 사용 - CSS 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며, 패키지 소비자는 `@hm-design/ui/theme.css` 다음에 `@hm-design/ui/dashboard-status.css`를 로드한다. 기존 `src/patterns/dashboard-status-contract.css`는 같은 원본을 import하는 호환 경로다. 상태 CSS는 Custom Property만 정의하므로 Badge와 필터 숫자 등의 색상 mapping은 Runtime adapter에서 연결하며 색상 literal을 복제하지 않는다. active/hold/issue 값은 기존 Story 시각 보존용 compatibility mapping이며 HM Theme JSON을 변경하지 않는다. React/HTML의 `done`은 모두 `--hm-dashboard-status-done`을 사용하고, `:root` 및 `[data-hm-theme]`에서 해당 scope의 HM white token으로 재계산한다. [적용 순서와 Badge mapping](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약). React는 이 계약을 `Badge`와 `Button`/기존 `FilterBar` 조합으로 표현한다. Static HTML은 같은 attribute를 가진 semantic button/span과 `addEventListener` 기반 필터링으로 표현하며, `aria-controls`와 결과 count의 갱신도 유지한다. `selected`, `hover`, `focus-visible`, `disabled`는 CSS와 native button semantics로 보존한다. ### 3.2 Static HTML 실행 체크리스트 1. `docs/patterns/dashboard-status-static.html`을 브라우저에서 직접 `file:///...`로 연다. 2. `../../packages/ui/dist/theme.css`, `../../src/patterns/dashboard-status-contract.css`, `../../public/fonts/NotoSansKR-variable.ttf`가 모두 로드되는지 확인한다. 3. 전체/진행/보류/완료/이슈 버튼의 `aria-pressed`와 결과 count를 확인한다. 4. 이슈 record가 상태 Badge와 독립 이슈 flag를 동시에 표시하는지 확인한다. 5. 결과가 0건인 필터에서 Empty State와 `aria-live` 갱신을 확인한다. 6. DevTools Console에 오류가 없는지 확인한다. 자동화 환경이 `file://`를 차단하면 PASS로 기록하지 않고 미검증으로 남긴다. ### 3.3 AI 독립 재현 체크리스트 이전 대화 없이도 다음 파일만으로 재현 가능해야 한다: `AGENTS.md`, `packages/ui/src/patterns/dashboard-status/contract.ts`, `src/patterns/dashboard-status-contract.css`, `src/patterns/dashboard-layout.stories.tsx`, `docs/patterns/dashboard-status-static.html`, `packages/ui/dist/theme.css`, `docs/guidelines/runtime-theme-contract.md`. 구현 후 상태 ID/label, matching/count, DOM attributes, Theme artifact 상대 경로, React/Static HTML 차이, `file://` 검증 결과를 각각 보고한다. ### 3.2 Button 사용 계약 Button의 공통 기준은 [Button 문서](../components/button.md)의 역할·조합 규칙을 따른다. `brand`는 실제 화면당 최대 1개이고 팝업에는 사용하지 않으며, 일반 확인/작성은 같은 `size`의 Solid 세트, 삭제 확인과 반복·보조 작업은 같은 `size`의 Outline 세트를 사용한다. Static HTML과 다른 프레임워크는 React API를 복제하지 않아도 되지만, semantic button, `data-slot`/`data-variant`/`data-appearance`/`data-size` 계약, 기존 Theme CSS variable, native disabled 및 keyboard/modal 동작을 보존해야 한다. ## 4. Asset과 실행 위치 `STATIC_HTML`은 Vite asset resolution을 사용하지 않는다. HTML과 함께 이동 가능한 상대 경로를 사용한다. ```text ./assets/ ./styles/ ./fonts/ ./icons/ ``` CSS, JavaScript, SVG/image/icon, font, theme stylesheet를 모두 확인한다. package의 compiled CSS가 사용하는 font 경로와 source 개발 서버의 경로가 다를 수 있으므로, package 산출물 사용 시 package `dist`와 export 계약을 우선한다. 현재 source `packages/ui/src/theme.css`의 개발용 font 경로는 `/fonts/NotoSansKR-variable.ttf`이며 App/Storybook은 `public/fonts/`를 사용한다. ### 4.1 Noto Sans KR 경로: 실행 환경별 확인 - **HMDesignSystem 개발 저장소**: 원본은 `public/fonts/NotoSansKR-variable.ttf`, 라이선스는 같은 폴더의 `NotoSansKR-OFL.txt`다. Theme source의 `@font-face`는 `/fonts/NotoSansKR-variable.ttf`를 사용한다. Vite는 `public/`을 루트에서 제공하고 Storybook은 [.storybook/main.ts](../../.storybook/main.ts)의 `staticDirs`로 `/fonts`에 연결한다. 이 경로는 해당 서버 환경의 계약이다. - **포터블 테스트 ZIP(현재 기준 r3, 아래는 r2 시점 확인 기록)**: r2 ZIP 안 `HMDesignTest/public/fonts/`, `HMDesignTest/packages/ui/dist/fonts/`, `HMDesignTest/storybook-static/fonts/`에 기존 폰트와 라이선스가 모두 있다. 포터블 실행기는 사전 빌드된 Storybook을 제공하므로 실행 화면은 그 안의 font/CSS asset을 사용한다. 개발 폴더의 `/fonts` 경로를 다른 실행 위치에 그대로 적용한다고 가정하지 않는다. r2는 문서 개선 이전 과거 스냅샷으로 보존하며 현재 기준이 아니다. r3의 폰트 포함 여부는 r3 Inventory로 별도 확인한다. - **별도 React 소비자**: `@hm-design/ui/theme.css`는 공개 export인 `dist/theme.css`다. 기존 build가 원본 font-face URL을 `./fonts/NotoSansKR-variable.ttf`로 바꾸고 폰트·OFL 파일을 `dist/fonts/`로 복사한다. 정상 package를 CSS import로 소비할 때 별도 앱 `/fonts` 복사에 의존하지 않는다. Vite가 CSS `url()` 자산을 처리하므로 최종 배포 `base`와 출력 URL을 확인한다. Theme 소스 파일만 복사하거나 CSS만 분리하면 이 계약이 사라지므로 실제 폰트와 상대 경로도 함께 배치한다. [Vite 공식 Asset 문서](https://vite.dev/guide/assets.html). - **단일 `preview.html`**: `file://`에서는 `/fonts/...`가 HTML 옆 폴더를 뜻하지 않는다. 파일과 폴더를 함께 전달한다면 상대 CSS/font URL을 사용하고, CSS 안 URL은 HTML이 아닌 해당 CSS 파일 위치 기준으로 계산한다. [기존 Static HTML](../patterns/dashboard-status-static.html)은 상대 package Theme와 별도 상대 font-face를 사용하는 **다중 파일** Reference다. HTML 하나만 전달하려면 승인된 Theme·소비 CSS의 import를 모두 펼쳐 inline하고 폰트·아이콘·이미지 등 외부 자산도 내장한다. Tailwind 입력 CSS의 `@source / @apply`나 bare package import를 브라우저에 그대로 전달하지 않는다. 단일 HTML의 font-face는 기존 TTF를 data URL로 내장할 수 있다. 아래는 저장소 루트에서 기존 폰트로 **삽입할 CSS 문자열만 만드는** PowerShell 예이며, Theme/다른 Asset의 내장을 대신하지 않는다. ```powershell $fontPath = Join-Path (Get-Location) 'public/fonts/NotoSansKR-variable.ttf' $fontData = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($fontPath)) $fontRule = '@font-face { font-family: "Noto Sans KR"; src: url("data:font/ttf;base64,' + $fontData + '") format("truetype"); font-style: normal; font-weight: 100 900; font-display: swap; }' # 기존 font-face를 $fontRule로 교체해 preview.html의