Skip to content

Commit 868cce0

Browse files
committed
merge: integrate GUI design system with cross-platform hardening
2 parents 05b0ec8 + a0e910e commit 868cce0

19 files changed

Lines changed: 741 additions & 265 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ADR 0004: GUI toggle contrast and sidebar item spacing
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
In dark mode, enabled switches used the monochrome accent for the track. The bright track could
10+
visually merge with the switch knob, making the enabled state and knob position difficult to read.
11+
Sidebar navigation items also had no vertical separation, so adjacent hover and active backgrounds
12+
appeared as one continuous block.
13+
14+
## Decision
15+
16+
Keep the existing shared switch components and CSS tokens. Use the existing dark-theme success
17+
green for enabled switch tracks while retaining a dark knob, and make `.switch` consume the same
18+
toggle tokens as the label-based `.toggle` control.
19+
20+
Lay out the sidebar `nav` as a vertical flex container with a 4px gap. This preserves each item's
21+
full click target while separating adjacent hover and active surfaces.
22+
23+
## Consequences
24+
25+
- Enabled switches have distinct track and knob silhouettes in dark mode.
26+
- Both switch implementations share one enabled-state color rule.
27+
- Sidebar hover and active states remain visually separate without adding divider noise.
28+
- No component API or dependency changes are required.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ADR 0005: GUI uses role-based CSS design tokens
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
The GUI accumulated thirteen font sizes, several near-duplicate weights, page-local radii, and
10+
inline typography values. Those values made equivalent labels, controls, helper text, and machine
11+
data render differently across pages and made dark/light maintenance harder.
12+
13+
The GUI must also remain usable when served locally or offline, including Korean locales. A remote
14+
font or runtime styling dependency would add network and packaging failure modes to a management
15+
surface that should remain available while the proxy is being repaired.
16+
17+
## Decision
18+
19+
Use native CSS custom properties in `gui/src/styles.css` as the runtime source of truth. Define:
20+
21+
- semantic light/dark color tokens;
22+
- a Korean-safe UI font stack and a separate code/data font stack;
23+
- eight role-based type sizes, four weights, and four line heights;
24+
- a 4px-based spacing scale;
25+
- shared radius, control height, icon size, and motion tokens;
26+
- small typography utility classes for TSX contexts that previously used inline numeric values.
27+
28+
Keep component styling in the existing CSS and React primitives instead of adding CSS-in-JS,
29+
Tailwind, a component framework, or a remote font. Document the contract under
30+
`docs/design-system/` and require new visual values to use tokens.
31+
32+
For local integrated visual QA, use Vite's opt-in `OPENCODEX_PROXY_TARGET` proxy so the development
33+
GUI can call the running management API through the same origin without changing production output.
34+
35+
## Alternatives considered
36+
37+
- **CSS-in-JS:** strong component co-location, but adds runtime and migration cost for no functional gain.
38+
- **Utility framework:** broad ecosystem, but would duplicate the existing CSS and enlarge the change surface.
39+
- **Remote or bundled web font:** stronger cross-platform identity, but remote loading harms offline reliability
40+
and bundling adds package size/licensing work. System Korean fallbacks are sufficient for this console.
41+
- **Page-local cleanup only:** smaller initial diff, but leaves the inconsistency mechanism intact.
42+
43+
## Consequences
44+
45+
- Equivalent UI roles render consistently across all pages and locales.
46+
- Dark/light changes can be made at the token layer.
47+
- New contributors have a documented component and token contract.
48+
- Existing layout-specific inline values remain allowed when they are algorithmic rather than visual roles.
49+
- A future branded font can be introduced by changing `--font-ui` after packaging and licensing are decided.

docs/design-system/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# OpenCodex GUI Design System
2+
3+
OpenCodex 관리 GUI의 시각 언어와 구현 규칙을 정의한다. 목표는 화면마다 새 스타일을
4+
만드는 것이 아니라, 같은 역할의 요소가 어떤 페이지에서도 같은 글꼴, 크기, 간격,
5+
표면, 상태 표현을 사용하게 하는 것이다.
6+
7+
## 원칙
8+
9+
1. **역할이 값보다 먼저다.** `13px` 대신 `control`, `12px` 대신 `label`처럼 UI 역할을 선택한다.
10+
2. **기계 데이터만 monospace를 쓴다.** 모델 ID, URL, 버전, 토큰 수, 코드에 한정한다.
11+
3. **4px 그리드를 따른다.** 반복 간격은 spacing token을 우선 사용한다.
12+
4. **상태에는 의미 색상을 쓴다.** 성공/활성은 green, 경고는 amber, 위험은 red다.
13+
5. **라이트와 다크를 함께 설계한다.** 색상은 `light-dark()` 기반 semantic token으로 정의한다.
14+
6. **새 의존성보다 네이티브 CSS를 우선한다.** 디자인 시스템은 런타임 라이브러리 없이 동작한다.
15+
16+
## 소스 구조
17+
18+
```text
19+
gui/src/styles.css
20+
├── semantic color tokens
21+
├── spacing / radius / motion tokens
22+
├── typography tokens and utilities
23+
├── app shell and responsive rules
24+
└── shared component styles
25+
26+
gui/src/ui.tsx
27+
├── Switch
28+
├── Field
29+
├── Select
30+
└── EmptyState
31+
32+
docs/design-system/
33+
├── README.md
34+
├── foundations.md
35+
├── components.md
36+
└── contributing.md
37+
```
38+
39+
실행 시점의 유일한 스타일 소스는 `gui/src/styles.css`다. 이 폴더의 문서는 해당 코드의
40+
사용 계약을 설명하며, 토큰 값이 바뀌면 코드와 문서를 같은 변경에서 갱신해야 한다.
41+
42+
## 빠른 사용법
43+
44+
```tsx
45+
<h2 className="text-title font-semibold">Models</h2>
46+
<p className="text-body muted">라우팅 가능한 모델을 관리합니다.</p>
47+
<code className="mono text-label">openrouter/gpt-5</code>
48+
<span className="badge badge-green">active</span>
49+
```
50+
51+
```css
52+
.feature-row {
53+
display: flex;
54+
gap: var(--space-2);
55+
padding: var(--space-3) var(--space-4);
56+
border: 1px solid var(--border);
57+
border-radius: var(--radius-sm);
58+
transition: background var(--motion-fast);
59+
}
60+
```
61+
62+
## 문서 안내
63+
64+
- [Foundations](./foundations.md): 색상, 폰트, 크기, 간격, 반경, 모션
65+
- [Components](./components.md): 버튼, 입력, 패널, 표, 배지, 토글, 내비게이션
66+
- [Contributing](./contributing.md): 새 화면/컴포넌트 추가 규칙과 QA 체크리스트
67+
68+
## 관련 결정 기록
69+
70+
- [ADR 0004](../adr/0004-gui-toggle-contrast-and-nav-spacing.md)
71+
- [ADR 0005](../adr/0005-gui-design-token-system.md)

docs/design-system/components.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Components
2+
3+
## App shell and navigation
4+
5+
- 데스크톱은 232px sidebar와 main content의 2열 구조다.
6+
- 760px 이하에서는 sidebar가 off-canvas drawer로 전환된다.
7+
- `.nav-item`은 아이콘 17px, control text, 4px 세로 간격을 사용한다.
8+
- hover와 active는 같은 surface family를 쓰되 active는 semibold로 구분한다.
9+
- 메뉴마다 margin을 직접 추가하지 않고 `.sidebar nav``gap`을 사용한다.
10+
11+
## Buttons
12+
13+
| 변형 | 클래스 | 용도 |
14+
|---|---|---|
15+
| Primary | `.btn.btn-primary` | 저장, 로그인, 확정 |
16+
| Ghost | `.btn.btn-ghost` | 보조 액션, 취소 |
17+
| Danger | `.btn.btn-danger` | 삭제, 중단 |
18+
| Small | `.btn.btn-sm` | 필터, 행 내부 액션 |
19+
| Icon | `.btn-icon` | 닫기, 도움말, 제거 |
20+
21+
버튼은 기본적으로 control text와 medium weight를 사용한다. 액션 중요도는 크기가 아니라
22+
색상 변형으로 표현한다. 아이콘 전용 버튼에는 반드시 접근 가능한 label/title을 제공한다.
23+
24+
## Inputs and selects
25+
26+
- `.input`, `.select-trigger`, `.select-option`, `.field-label`을 재사용한다.
27+
- 라벨은 label text, 입력값은 control text를 사용한다.
28+
- placeholder는 `--faint`, 값은 `--text`, 오류는 `--red`를 사용한다.
29+
- focus는 border와 `--accent-soft` ring을 함께 사용한다.
30+
- 네이티브 select를 새로 만들기보다 `ui.tsx``Select`를 우선한다.
31+
32+
## Cards and panels
33+
34+
- `.card`: 경계와 배경만 제공한다. 내부 padding은 문맥이 소유한다.
35+
- `.panel`: 기본 18px padding이 포함된 독립 구획이다.
36+
- `.panel-accent`: 선택되거나 강조된 구획이다.
37+
- 카드 중첩은 최대 한 단계로 제한한다. 정보를 나누기 위해 무조건 카드부터 추가하지 않는다.
38+
39+
## Tables and rows
40+
41+
- `.tbl`/`.tbl-wrap`은 데이터 표의 기본 조합이다.
42+
- 본문은 control text, 헤더는 label text + medium weight다.
43+
- 숫자 열은 `.num` 또는 `.mono`로 tabular 숫자를 사용한다.
44+
- 작은 화면에서는 열을 억지로 접지 않고 `.tbl-wrap`에서 가로 스크롤한다.
45+
46+
## Badges and status
47+
48+
- `.badge-green`: 성공, 연결됨, 활성
49+
- `.badge-amber`: 경고, 다음 적용, 주의 필요
50+
- `.badge-muted`: 중립 메타데이터
51+
- `.notice-ok/.notice-err/.notice-warn`: 한 줄 이상의 상태 메시지
52+
53+
배지는 핵심 본문을 대신하지 않는다. 매우 작은 `.text-micro`는 짧은 상태 문자열에만 허용한다.
54+
55+
## Toggle and switch
56+
57+
두 마크업 형태가 있지만 동일한 토큰을 사용한다.
58+
59+
- 버튼형: `ui.tsx``Switch`, `.switch`, `.knob`
60+
- label/input형: `.toggle`, `.slider`
61+
62+
켜짐은 `--toggle-on-bg`, 꺼짐은 `--toggle-off-bg`, 핸들은 `--toggle-dot-color`를 사용한다.
63+
다크 모드 활성 상태는 녹색 트랙과 짙은 핸들로 분리한다. 화면별 토글 색상 override는 금지한다.
64+
65+
## Icons
66+
67+
- 기본 아이콘은 `icons.tsx`의 동일한 outline family를 사용한다.
68+
- 기본 크기는 `--icon-md(16px)`, 조밀한 메타 UI는 `--icon-sm(14px)`, 상단 액션은 `--icon-lg(20px)`다.
69+
- 아이콘만으로 의미가 불명확하면 텍스트를 함께 배치한다.
70+
- active 아이콘은 `--text`, inactive 아이콘은 `--faint`를 사용한다.
71+
72+
## Responsive behavior
73+
74+
- 760px 이하: drawer navigation, 세로형 form row, 44px 터치 대상
75+
- 640px 이하: dashboard stat 2열
76+
- 데이터 표와 heatmap은 정보 손실보다 가로 스크롤을 우선한다.
77+
- 모바일에서 제목/본문 크기를 임의 축소하지 않는다. 같은 역할은 같은 type token을 유지한다.

docs/design-system/contributing.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Design System Contribution Guide
2+
3+
## 새 UI를 만들기 전
4+
5+
1. `gui/src/ui.tsx``gui/src/styles.css`에 같은 역할의 컴포넌트가 있는지 검색한다.
6+
2. 기존 컴포넌트를 변형으로 확장할지, 새 컴포넌트가 필요한지 판단한다.
7+
3. 새 값이 아니라 기존 semantic token으로 표현 가능한지 확인한다.
8+
4. 새 토큰이 필요하면 최소 두 곳에서 재사용될 역할인지 설명하고 ADR 또는 Decision Log를 남긴다.
9+
10+
## 금지 규칙
11+
12+
- TSX에 숫자형 `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing` 추가
13+
- hex/rgb 색상을 페이지 컴포넌트에 직접 추가
14+
- `borderRadius: 999`처럼 토큰을 우회하는 값 추가
15+
- 한 화면만을 위한 새로운 폰트 family 추가
16+
- 기존 `Switch`, `Select`, `.btn`, `.card`, `.panel`, `.badge`와 중복되는 컴포넌트 생성
17+
- hover만 있고 keyboard focus가 없는 인터랙션 추가
18+
19+
## 허용되는 예외
20+
21+
차트 셀 크기, 가상화 row height, viewport 계산처럼 데이터/알고리즘에 종속된 값은 인라인으로
22+
남길 수 있다. 다만 시각적 역할을 나타내는 값은 반드시 token을 사용한다.
23+
24+
## 권장 구현 예시
25+
26+
```tsx
27+
<div className="panel">
28+
<div className="text-body font-semibold">설정 이름</div>
29+
<div className="muted text-control leading-body">설정 설명</div>
30+
<Switch on={enabled} onClick={toggle} label="설정 이름" />
31+
</div>
32+
```
33+
34+
## 검증 체크리스트
35+
36+
- [ ] 라이트/다크에서 텍스트와 상태 색상이 읽힌다.
37+
- [ ] 페이지 제목, 본문, 라벨, 코드가 역할별 type token을 사용한다.
38+
- [ ] 데스크톱과 760px 이하 viewport에서 overflow/clipping이 없다.
39+
- [ ] hover, active, focus-visible, disabled 상태가 존재한다.
40+
- [ ] 토글과 입력이 실제 상태를 변경한다.
41+
- [ ] `bun run lint`가 오류 없이 끝난다.
42+
- [ ] `bun run build`가 성공한다.
43+
- [ ] Playwright 또는 Browser로 console error와 framework overlay가 없음을 확인한다.
44+
- [ ] 시각 변경 시 전/후 또는 기준/구현 스크린샷을 `view_image`로 직접 비교한다.
45+
46+
## 리뷰 명령
47+
48+
```bash
49+
rg -n 'font-size:\s*[0-9]|font-weight:\s*[0-9]' gui/src/styles.css
50+
rg -n 'fontSize:\s*[0-9]|fontWeight:\s*[0-9]|lineHeight:\s*[0-9]' gui/src --glob '*.tsx'
51+
rg -n 'border-radius:\s*[0-9]|borderRadius:\s*[0-9]' gui/src --glob '*.{css,tsx}'
52+
cd gui && bun run lint && bun run build
53+
```
54+
55+
첫 세 명령은 결과가 없어야 한다.
56+
57+
실제 로컬 API와 함께 화면을 확인할 때는 Vite의 opt-in proxy를 사용한다.
58+
59+
```bash
60+
cd gui
61+
OPENCODEX_PROXY_TARGET=http://127.0.0.1:10101 bun run dev --host 127.0.0.1
62+
```

docs/design-system/foundations.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Foundations
2+
3+
## Color
4+
5+
색상 토큰은 역할을 나타낸다. 컴포넌트에서 hex 값을 직접 사용하지 않는다.
6+
7+
| 역할 | 토큰 | 라이트 | 다크 |
8+
|---|---|---:|---:|
9+
| 앱 배경 | `--bg` | `#ffffff` | `#212121` |
10+
| 사이드바 | `--rail` | `#f9f9f9` | `#171717` |
11+
| 기본 표면 | `--surface` | `#ffffff` | `#262626` |
12+
| 올라온 표면 | `--raised` | `#f4f4f4` | `#303030` |
13+
| 기본 텍스트 | `--text` | `#0d0d0d` | `#ececec` |
14+
| 보조 텍스트 | `--muted` | `#6e6e6e` | `#a6a6a6` |
15+
| 약한 텍스트 | `--faint` | `#707070` | `#9a9a9a` |
16+
| 성공/활성 | `--green` | `#0a7d5c` | `#4ecb9d` |
17+
| 경고 | `--amber` | `#9a4a08` | `#fbbf24` |
18+
| 위험 | `--red` | `#b91c1c` | `#f87171` |
19+
20+
`--accent`는 기본 액션과 포커스에 사용한다. 활성 토글은 다크 모드에서 트랙과 핸들이
21+
겹쳐 보이지 않도록 `--toggle-on-bg``--toggle-dot-color`를 사용한다.
22+
23+
## Typography
24+
25+
### Font families
26+
27+
- `--font-ui`: 일반 UI, 제목, 본문, 버튼, 입력. Pretendard/Noto Sans KR/Apple SD Gothic Neo/Malgun Gothic을 포함해 한글 fallback을 보장한다.
28+
- `--font-code`: 모델 ID, URL, 버전, 토큰 수, 로그, 코드. 숫자는 tabular 형태로 정렬한다.
29+
30+
외부 CDN 폰트를 사용하지 않는다. 프록시 관리 화면은 오프라인에서도 열려야 하고, 폰트
31+
다운로드 실패가 레이아웃 이동이나 한글 누락으로 이어지면 안 되기 때문이다.
32+
33+
### Type scale
34+
35+
| 역할 | 클래스 | 토큰 | 크기 | 대표 용도 |
36+
|---|---|---|---:|---|
37+
| Micro | `.text-micro` | `--text-micro` | 10px | 매우 작은 상태 배지 |
38+
| Caption | `.text-caption` | `--text-caption` | 11px | 메타데이터, 보조 수치 |
39+
| Label | `.text-label` | `--text-label` | 12px | 필드 라벨, 표 헤더, 코드 |
40+
| Control | `.text-control` | `--text-control` | 13px | 버튼, 메뉴, 필터, 알림 |
41+
| Body | `.text-body` | `--text-body` | 14px | 본문, 카드 제목 |
42+
| Subtitle | `.text-subtitle` | `--text-subtitle` | 16px | 모달 제목, 브랜드명 |
43+
| Title | `.text-title` | `--text-title` | 20px | 페이지 제목, 주요 수치 |
44+
| Display | `.text-display` | `--text-display` | 24px | 제한적인 대형 수치/빈 상태 |
45+
46+
굵기는 `regular(400)`, `medium(500)`, `semibold(600)`, `bold(700)` 네 단계만 사용한다.
47+
행간은 `tight(1.2)`, `ui(1.35)`, `body(1.5)`, `relaxed(1.6)` 네 단계만 사용한다.
48+
49+
## Spacing
50+
51+
기본 단위는 4px이다.
52+
53+
| 토큰 || 토큰 ||
54+
|---|---:|---|---:|
55+
| `--space-0-5` | 2px | `--space-1` | 4px |
56+
| `--space-1-5` | 6px | `--space-2` | 8px |
57+
| `--space-3` | 12px | `--space-4` | 16px |
58+
| `--space-5` | 20px | `--space-6` | 24px |
59+
| `--space-8` | 32px | `--space-10` | 40px |
60+
| `--space-12` | 48px | `--space-16` | 64px |
61+
62+
컴팩트 컨트롤 내부는 4–8px, 카드 내부는 12–20px, 페이지 구획은 24–64px 범위를 사용한다.
63+
64+
## Radius and controls
65+
66+
| 토큰 || 용도 |
67+
|---|---:|---|
68+
| `--radius-2xs` | 4px | 작은 차트 셀, 포커스 보정 |
69+
| `--radius-xs` | 6px | 칩, 작은 아이콘 표면 |
70+
| `--radius-sm` | 8px | 입력, 메뉴 항목, 작은 카드 |
71+
| `--radius` | 12px | 기본 카드/패널 |
72+
| `--radius-lg` | 16px | 모달 |
73+
| `--radius-round` | 50% | 점, 원형 아이콘 |
74+
| `--radius-pill` | 999px | 버튼, 토글, segmented control |
75+
76+
컨트롤 높이는 `28/34/40/44px` 단계다. 44px는 모바일 터치 영역에 사용한다.
77+
78+
## Motion
79+
80+
- `--motion-fast: 120ms`: hover, border, color 변화
81+
- `--motion-normal: 180ms`: drawer, toggle, 위치 변화
82+
- `prefers-reduced-motion: reduce`에서는 transition과 animation을 제거한다.
83+
84+
모션은 상태 이해를 돕는 범위에서만 사용하며 장식 목적의 반복 애니메이션은 추가하지 않는다.

0 commit comments

Comments
 (0)