<!-- canonical: https://docs.axelabs.ai/services/ui -->
<!-- source: content/services/ui.mdx -->

---
title: UI (디자인 시스템)
description: axelabs 플랫폼이 공유하는 토큰·컴포넌트·layout chrome SSOT. design.axelabs.ai 에서 라이브 데모.
---

# UI

**한 줄 소개**: axelabs 플랫폼 (메인 사이트 · docs · cortex · frame · hive · matrix · blueprint) 이 공통으로 쓰는 **디자인 시스템 SSOT** — 토큰 (color/spacing/typography) + 40+ React 컴포넌트 + `.axe-app-shell` 통합 layout chrome ([D-ui-1](/ops/decisions)). 외부 데모 = [design.axelabs.ai](https://design.axelabs.ai) (구 `ui.axelabs.ai` → [D-ui-3](/ops/decisions) 에서 rename·폐기).

## 도메인

| URL | 용도 |
|---|---|
| https://design.axelabs.ai | 디자인 시스템 라이브 사이트 (variant 토글 + 본문 + 데모 모두 한 페이지에 통합) |
| `localhost:3902/design` | 운영자 로컬 dev preview (port 3902 — :3900 은 docker production 점유) |

`design.axelabs.ai` 의 sub-path 는 root 로 308 redirect (단일 페이지 통합 — 운영자 결정 2026-05-29).
메인 `axelabs.ai/design/*` 는 404 차단 (디자인 시스템 surface 는 `design.axelabs.ai` 단일 host).
구 `ui.axelabs.ai` 는 [D-ui-3](/ops/decisions) 에서 완전 폐기 — tunnel ingress 제거로 catch-all 404.

## 기술 스택

| 항목 | 값 |
|---|---|
| 패키지 이름 | `@axe/ui` |
| 위치 (SSOT) | `/Users/axe/axelabs/src/lib/` |
| 프레임워크 | Next.js 16 (App Router) + React 19 |
| 호스팅 | axelabs.ai 와 동일 Next.js 컨테이너 (multi-domain rewrite, [D-ui-1](/ops/decisions)) |
| 토큰 | CSS 변수 — `tokens/{colors,typography,spacing,fonts}.css` |
| 컴포넌트 | 40+ React (.tsx) + 14 CSS 그룹 (`styles/*.css`) |
| Layout chrome | `.axe-app-shell` + variant (`data-shell="docs|dashboard|landing"`) — Phase 15 |
| 다크모드 | `data-theme="dark"` + `.dark/.light` 동시 set (next-themes 호환) |
| 폰트 | Pretendard (본문) + Sarasa Fixed K (mono) + Clash Display (옵션 display) |

## 배포 모델 (D-ui-1 · 도메인 D-ui-3)

design.axelabs.ai 는 **별도 컨테이너가 아닙니다**. axelabs.ai (회사 홈) 와 같은 Next.js standalone 컨테이너가 host header 를 보고 분기:

- `Host: axelabs.ai` 또는 `www.axelabs.ai` → 메인 사이트
- `Host: design.axelabs.ai` → root 를 `/design` 으로 rewrite (`proxy.ts`, host 분기 — [D-ui-3](/ops/decisions))

분기 메커니즘은 `next.config.mjs` 의 `rewrites()` 가 아니라 **`proxy.ts`** (Next.js 16 convention, 구 middleware.ts) 다 — 정적 prerender 된 `/` 에는 `next.config` rewrites 가 안 걸리는 함정 때문 (proxy 는 항상 runtime).

`axelabs.ai/design` 으로 직접 접근도 살아있음 — dev/preview + canary 용도.

이유: 디자인 시스템 코드가 본 레포에 살고, build 산출물 공유가 자연스러움. 단일 docker compose 운영 단순.

> **tunnel ingress 변경은 로컬 `config.yml` 이 아님** — `axelabs-tunnel` 은 Cloudflare remote-managed (source=cloudflare, version-tracked). ingress 추가/변경/삭제는 `PUT /accounts/<acct>/cfd_tunnel/<uuid>/configurations` (token = vault `Cloudflare API - axelabs`). 상세 = [known-gaps Cortex 7함정 #7](/ops/known-gaps).

## Chrome variants

| Variant | 형태 | 사용처 |
|---|---|---|
| `docs` | TopNav + Sidebar + Content (80ch) + TOC + Footer | docs.axelabs.ai, 가이드 페이지 |
| `dashboard` | TopNav + Sidebar + Content (1280px) + Footer | cortex / frame admin / blueprint workspace |
| `landing` | TopNav + Content (full width) + Footer | axelabs.ai 메인, 마케팅 페이지 |

CSS 변수 (variant 별 width):

| 변수 | 기본값 |
|---|---|
| `--app-shell-sidebar-w` | 240px |
| `--app-shell-toc-w` | 220px |
| `--app-shell-gutter` | `var(--space-6)` (32px) |
| `--app-shell-topbar-h` | 56px |

## Layout primitives + ThemeToggle (Phase 16)

Chrome 가 페이지 macro 골격이라면, **primitives 는 그 안의 micro 레이아웃** — `display:flex`/`grid` + gap 을 매번 inline style 로 재발명하던 것을 SSOT 클래스로 흡수 ([D-ui-2](/ops/decisions)). Chrome 과 동일하게 styling 은 100% CSS 클래스 → React 아닌 소비자 (cortex maud, jinja) 도 같은 markup 재사용.

| 컴포넌트 | CSS 클래스 | 역할 | 주요 prop |
|---|---|---|---|
| `Stack` | `.axe-stack` | 세로 flex + gap | `gap` 0–10 (기본 4) · `align` · `as` |
| `Cluster` | `.axe-cluster` | 가로 flex + wrap + gap (toolbar/액션 줄) | `gap` 0–10 (기본 3) · `align`(기본 center) · `justify` · `nowrap` · `as` |
| `Grid` | `.axe-grid` | 반응형 grid | `cols` 1–6 (기본 3) · `gap` 0–6 (기본 4) · `auto`+`min` · `responsive` |
| `Container` | `.axe-container` | max-width 중앙 + gutter | `size` sm/md/lg/xl/full (기본 lg) · `as` |
| `ThemeToggle` | `.axe-theme-toggle` | light/system/dark 전환 | `mode` segment\|cycle (기본 segment) · `size` |

- **gap 은 spacing scale 와 1:1** — `gap={4}` → `var(--space-4)`. 매직 px 금지.
- **`Grid auto`** — `min` (기본 15rem) 이하로 안 줄게 `auto-fill` wrap. cortex 의 `.cortex-metric-grid` 재발명 대체.
- **`ThemeToggle`** 은 `useTheme` 위 thin UI — DOM/storage 반영을 `ThemeProvider` 에 위임 (이전 design 페이지가 Provider 우회해 `document.documentElement` 를 직접 만지던 버그 제거). 아이콘은 inline SVG (외부 dep 0).

CSS-only 소비자는 동일 클래스를 직접 쓴다:

```html
<div class="axe-stack axe-stack--gap-4">
  <div class="axe-cluster axe-cluster--gap-2 axe-cluster--justify-between">…</div>
  <div class="axe-grid axe-grid--auto axe-grid--gap-3" style="--axe-grid-min: 220px">…</div>
</div>
```

채택 (2026-05-29 브라우저 검증): `app/page.tsx` (axelabs.ai 홈 — Hero/Section/Footer 컴포지트 + Stack/Cluster/Grid) · `app/matrix` (MatrixNav ThemeToggle + page 전체 레이아웃) · `app/design` (design.axelabs.ai 본 페이지). cortex/docs/blueprint 는 순차 적용 대기.

## 문서 템플릿 (Phase 17)

Chrome·primitives 가 **화면** 레이아웃이라면, 문서 템플릿은 **인쇄물** — AXE 가 외부로 발신하는 문서 (IC Memo · LP 서한 등) 를 `.axe-doc` CSS-class HTML 로 통일 ([D-ui-3](/ops/decisions)). 화면 컴포넌트와 동일하게 styling 100% CSS 클래스 → React 쇼케이스와 jinja2 렌더가 **같은 markup** 을 공유.

| 템플릿 | 위치 | 용도 |
|---|---|---|
| IC Memo | `src/lib/templates/documents/ic-memo.html.jinja` | 투자심의 메모 — 권고 callout + deal/returns KPI + 번호 섹션 + 리스크/표결 표 + 서명 |
| LP 서한 | `src/lib/templates/documents/lp-letter.html.jinja` | 출자자 서한 — 분기(quarterly)/수시(adhoc) 분기 + NAV KPI + 서명 |
| 스타일 SSOT | `src/lib/styles/document.css` | `.axe-doc*` 전 클래스 (`components.css` 가 @import → globals 경유 전역 로드) |

**가로·세로 모두 지원** (문서 성격별 고정 X — 운영자 결정): `.axe-doc--portrait` (A4 210×297mm) / `.axe-doc--landscape` (297×210mm). landscape 본문엔 `.axe-doc__cols` (2단) 적용 가능.

| 측면 | 방식 |
|---|---|
| 단위 | `pt` (화면·인쇄 일관) |
| 페이지 | named `@page axe-doc-portrait` / `axe-doc-landscape` + `.axe-doc` 의 `page:` 속성으로 선택 — **전역 bare `@page` 금지** (사이트 전 페이지에 A4 누수) |
| 테마 | `.axe-doc` 가 자체 `--doc-*` 팔레트 정의 → light/dark 무관 동일 (흰 종이·따뜻한 잉크) |
| 인쇄 | `@media print` 가 stage·toolbar 숨김 + box-shadow 제거 + `print-color-adjust:exact` |

### 렌더 파이프라인 (기존 md-to-pdf 재사용)

병렬 렌더러 신설 X — 기존 마크다운→PDF 파이프라인에 끼움:

1. **본문 (markdown → HTML)** — pandoc 가 narrative 를 HTML 로 (md-to-pdf skill). 결과를 jinja 컨텍스트의 `memo.body_html` / `letter.body_html` 슬롯에 `safe` 필터로 주입.
2. **wrapper (jinja → HTML)** — `.axe-doc` chrome (masthead·meta·KPI·callout·표·서명) 으로 본문을 감쌈. CSS 는 `axe_css` (inline, production 권장) 또는 `css_href` (dev `<link>`).
3. **HTML → PDF** — `chromium --headless --print-to-pdf` (md-to-pdf skill `render.sh` 와 **동일 엔진**). orientation 클래스가 named `@page` 를 선택.
4. **품질 게이트** — ic skill `check_pdf_quality.py` 로 검증.

### 쇼케이스

[design.axelabs.ai](https://design.axelabs.ai) `§ 문서 템플릿` 에 라이브 데모 — IC Memo ↔ LP 서한 · 세로 ↔ 가로 · zoom 토글 + "인쇄·PDF" 버튼 (현재 문서 노드만 새 창에 복제해 `window.print()` → AppShell chrome 없는 깨끗한 PDF). 컨텍스트 스키마는 각 `.jinja` 파일 상단 주석 참조.

## Multi-stack

React 가 아닌 stack (cortex 의 Rust+maud 등) 도 동일 markup 으로 동일 chrome:

```html
<div class="axe-app-shell" data-shell="dashboard">
  <div class="axe-app-shell__topbar">
    <nav class="axe-topnav">…</nav>
  </div>
  <div class="axe-app-shell__main">
    <aside class="axe-app-shell__sidebar"><nav class="axe-sidebar">…</nav></aside>
    <main  class="axe-app-shell__content">…</main>
  </div>
  <div class="axe-app-shell__footer">
    <footer class="axe-footer">…</footer>
  </div>
</div>
```

cortex 의 `scripts/sync-axe-ui.sh` 가 `layout.css` 를 bundle 에 포함시키도록 갱신하면 즉시 채택 가능. 가이드 = `/Users/axe/cortex/CLAUDE.md` (D-cortex-design-axe-ui).

## 소비자 (현재 + 예정)

| 서비스 | 현재 chrome | 목표 variant | 상태 |
|---|---|---|---|
| axelabs.ai (메인) | TopNav + Hero/Section/Footer + Stack/Cluster/Grid | `landing` | 채택 (2026-05-29 dogfood) |
| docs.axelabs.ai | Nextra theme | `docs` (slot override) | 대기 (C2 — Nextra navbar/sidebar 슬롯에 axe 컴포넌트 끼움) |
| cortex | TopNav-only (maud) | `dashboard` | 다음 (`scripts/sync-axe-ui.sh` + `page()` 갱신) |
| frame · hive · matrix | — | `dashboard` | 대기 (신규 admin UI 시작 시) |
| Blueprint | 자체 | `dashboard` | 대기 (기존 chrome 분리 후) |

## 환경 변수

본 서비스는 별도 환경 변수 없음. axelabs.ai 와 동일 컨테이너 + 정적 자원.

## 함정

- **`:3900` dev 띄우기** — docker production container 가 점유 중. 로컬 dev 는 `npx next dev --port 3902` ([reference](/onboard/troubleshooting)) 또는 `.claude/launch.json` 의 `axe-ui-design-preview` 항목 사용.
- **host header rewrite 검증** — `axelabs.ai/design` (404 차단) 와 `design.axelabs.ai` (200, /design 서빙) 가 같게 동작하면 host 분기 실패. cloudflared 의 `httpHostHeader` override 없이 default (preserve) 사용 가정. 컨테이너 직접 검증: `curl -H 'Host: design.axelabs.ai' http://127.0.0.1:3900/` → 200, `curl -H 'Host: axelabs.ai' http://127.0.0.1:3900/design` → 404.
- **신규 1-level 호스트는 IPv6-only 로 잠깐 보일 수 있음** — 새 `design.axelabs.ai` 직후 로컬 resolver 가 AAAA(Cloudflare IPv6 edge)만 반환 → IPv6 무라우팅 머신에서 `curl` 이 `No route to host` (HTTP 000). production 문제 아님. `curl -4` 또는 `--resolve design.axelabs.ai:443:104.21.66.67` 로 IPv4 edge 검증.
