디자인 시스템
Elftia의 비주얼 디자인은 따뜻함, 친근감, 세련됨을 추구하며 차갑고 순수 기술적인 미감을 지양합니다.
디자인 철학
색상 원칙
- 따뜻한 중성 색상: 모든 서피스 색상은 약간의 따뜻한 색조(hue 24-38)를 띠며 순수 그레이스케일을 사용하지 않습니다
- 다크 모드: 순수 검정 대신 따뜻한 차콜(warm charcoal) 사용
- 라이트 모드: 순수 흰색 대신 따뜻한 크림(warm cream) 사용
- 차가운 그레이 금지: 배경이나 테두리에 순수 그레이(
hsl(0, 0%, ...)) 사용 금지
모서리 반경 원칙
| 요소 | 반경 | Tailwind 클래스 |
|---|---|---|
| 카드/컨테이너 | 12px | rounded-xl |
| 입력 필드 | 12px | rounded-xl |
| 버튼/배지 | 6px | rounded-md |
| 기본값 | 8px | rounded-lg |
4px 미만의 모서리 반경 사용 금지 (선 장식 제외).
타이포그래피 원칙
| 목적 | 폰트 | Tailwind 클래스 |
|---|---|---|
| 디스플레이/대형 헤드라인 | Noto Serif / Georgia | font-display |
| 본문/인터페이스 | Inter | font-sans |
| 코드 | JetBrains Mono | font-mono |
헤드라인에는 font-bold 대신 font-semibold를 사용하고 tracking-tight와 함께 씁니다.
색상 시스템
시맨틱 토큰
모든 색상은 CSS 변수로 정의하고 Tailwind 설정에서 유틸리티 클래스로 매핑합니다. 하드코딩된 색상 값은 절대 사용하지 마세요.
// 하면 안 됨
<div className="bg-white text-black border-gray-200">
<div style={{ backgroundColor: '#ffffff' }}>
// 올바른 방법
<div className="bg-surface-0 text-foreground border-border">
<div style={{ backgroundColor: 'var(--surface-0)' }}>
자주 쓰는 토큰 대조표
| 목적 | Tailwind 클래스 | CSS 변수 |
|---|---|---|
| 페이지 배경 | bg-background | var(--background) |
| L0 배경 | bg-surface-0 | var(--surface-0) |
| L1 배경 (카드/사이드바) | bg-surface-1 | var(--surface-1) |
| L2 배경 (입력/보조 컨테이너) | bg-surface-2 | var(--surface-2) |
| L3 배경 (팝오버) | bg-surface-3 | var(--surface-3) |
| 주 텍스트 | text-foreground | var(--foreground) |
| 보조 텍스트 | text-muted-foreground | var(--muted-foreground) |
| 부가 텍스트 | text-text-subtle | var(--text-subtle) |
| 테두리 | border-border | var(--border) |
| 테마 색상 | bg-primary / text-primary | var(--primary) |
| 성공 | text-success | var(--success) |
| 오류 | text-destructive | var(--destructive) |
| 경고 | text-warning | var(--warning) |
다크/라이트 모드
전환 메커니즘
Tailwind의 class 전략(darkMode: 'class')을 사용하며 ThemeContext를 통해 중앙에서 관리합니다.
// 올바른 방법: useTheme으로 테마 정보 가져오기
import { useTheme } from '@/shared/state/themeStore';
function MyComponent() {
const { mode, resolvedMode, userTheme } = useTheme();
}
// 하면 안 됨: 수동 감지
const isDark = localStorage.getItem('theme') === 'dark';
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
계층 구조 시스템
다크 모드는 밝기로 계층을 구분합니다. 사용자에게 더 가까운 UI 요소일수록 배경이 더 밝습니다.
| 레벨 | 토큰 | 밝기 | 목적 | 참고 색상 |
|---|---|---|---|---|
| L0 | --surface-0 | 7% | 페이지 주 배경 | #121212 |
| L1 | --surface-1 | 12% | 사이드바, 카드 | #1E1E1E |
| L2 | --surface-2 | 17% | 보조 컨테이너, 입력 | #2B2928 |
| L3 | --surface-3 | 22% | 팝오버, 드롭다운 | #383635 |
레벨 간 밝기 차이는 시각적 구분을 보장하기 위해 >= 4-5%여야 합니다.
테두리 규범
다크 모드에서는 어두운 영역에 대한 인간의 지각 민감도가 낮아지므로 테두리의 가시성을 높여야 합니다:
| 상황 | 라이트 모드 | 다크 모드 |
|---|---|---|
| 카드 테두리 | border-border/30 ~ /40 | border-border/50 ~ /70 |
| 구분선 | border-border/20 ~ /30 | border-border/40 ~ /50 |
| 입력 필드 | border-border/40 | border-border/60 ~ border-border |
WCAG 접근성
WCAG 2.1 표준 기반입니다.
대비율 요구사항
| 요소 유형 | 최소 대비율 | 설명 |
|---|---|---|
| 일반 텍스트 (< 18pt) | 4.5:1 | 본문, 설명, 레이블 |
| 큰 텍스트 (>= 18pt 또는 14pt 굵게) | 3:1 | 헤드라인 |
| UI 컨트롤 (아이콘, 테두리) | 3:1 | 입력 테두리, 아이콘, 배지 |
| 비활성 상태 | 면제, 2.5:1 권장 | 완전히 보이지 않는 경우 방지 |
텍스트 토큰 사용 규칙
| 토큰 | 밝기 | 허용되는 배경 | 일반 사용처 |
|---|---|---|---|
text-foreground (93%) | 최고 | 모든 서피스 | 주 헤드라인, 본문 텍스트 |
text-muted-foreground (65%) | 중간 | surface-0, surface-1 | 보조 텍스트, 설명 |
text-text-subtle (50%) | 낮음 | surface-0에만 | 타임스탬프, 메타데이터 |
// 좋음: 설명 텍스트에 text-muted 사용
<p className="text-muted-foreground">모델 5개</p>
// 나쁨: surface-1 카드 내에서 text-subtle 사용 (대비율 부족)
<div className="bg-surface-1">
<span className="text-text-subtle">읽기 어려움</span>
</div>
색상으로 정보 전달
색상만으로 상태를 전달해서는 안 되며, 반드시 텍스트 레이블이나 아이콘을 함께 사용해야 합니다:
// 나쁨: 색상만 사용
<div className={status === 'error' ? 'border-red-500' : 'border-border'} />
// 좋음: 색상 + 아이콘 + 텍스트
<div className={status === 'error' ? 'border-destructive' : 'border-border'}>
{status === 'error' && <AlertCircle className="text-destructive" />}
<span>{errorMessage}</span>
</div>
포커스 상태
키보드 내비게이션 사용자를 위해 focus-visible로 포커스 테두리를 제공하세요:
// 좋음
<button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none">
버튼
</button>
// 하면 안 됨: 포커스 스타일 제거
<button className="outline-none focus:outline-none">버튼</button>
모션 환경 설정
시스템의 prefers-reduced-motion 설정을 존중하세요:
<div className="motion-safe:animate-fadeIn motion-reduce:animate-none">
콘텐츠
</div>
UI 컴포넌트 규범
네이티브 컨트롤 사용 금지
| 네이티브 컨트롤 | 프로젝트 컴포넌트 | 경로 |
|---|---|---|
<select> | Select | @/components/ui/select |
<input type="text"> | Input | @/components/ui/input |
<input type="checkbox"> | Switch / Checkbox | @/components/ui/switch |
<button> | Button | @/components/ui/button |
window.confirm() | ConfirmDialog | @/components/ui/confirm-dialog |
window.alert() | Toast 컴포넌트 | - |
드롭다운 컴포넌트
모든 드롭다운 컴포넌트는 다음을 지원해야 합니다:
- 뷰포트 인식 위치 지정 (
useDropdownPositionHook 사용) - 외부 클릭 시 닫기
- Escape 키로 닫기
- 항목 테마 색상 및 스타일
배경화면 투명도 시스템
사용자가 배경화면을 설정하면 body에 data-wallpaper-active="true" 속성이 추가되어 CSS 투명도 규칙이 적용됩니다.
CSS 규칙 계층
| 우선순위 | 선택자 | 효과 | 목적 |
|---|---|---|---|
| 1 | .bg-background, .bg-surface-0 | 완전 투명 | 페이지 주 배경 |
| 2 | .wallpaper-blur | 35% 불투명 + blur | 주 컨테이너 (WorkspaceShell) |
| 3 | .wallpaper-blur .wallpaper-blur | 투명 + blur*0.67 | 중첩 컨테이너 (겹침 방지) |
| 4 | .wallpaper-blur 내 bg-surface-0/XX | 투명 | 레이아웃 패널 |
| 5 | .wallpaper-blur 내 bg-surface-1/XX | 12% + blur | 콘텐츠 카드 (alpha 변형) |
| 6 | bg-surface-1, bg-popover | 15% + blur | 버튼/카드 (입력 제외) |
| 7 | bg-surface-2 | 20% + blur | 보조 컨테이너 (입력 제외) |
| 8 | .wallpaper-panel | 85% + blur*1.33 | 플로팅 드롭다운/메뉴 |
| 9 | .wallpaper-solid | 불투명 surface-0 | 다이얼로그/모달 |
계층 스택 모델
WorkspaceShell (wallpaper-blur, 35%)
+-- Sidebar (bg-surface-0/75 -> transparent) = 35%
+-- Main content (bg-background -> transparent) = 35%
| +-- Content cards (bg-surface-1/80 -> 12%) ~ 43%
| +-- Buttons (bg-surface-1 -> 15%) ~ 45%
| +-- Input (input -> solid) = Opaque
| +-- Dropdown panel (wallpaper-panel -> 85%) = 85%
+-- Bottombar (wallpaper-blur -> transparent) = 35%
배경화면 CSS 클래스 사용 가이드
| 상황 | 권장 방법 |
|---|---|
| 페이지 주 컨테이너 | bg-background 또는 bg-surface-0 (자동 완전 투명) |
| 버튼/카드 | bg-surface-1 또는 bg-surface-1/80 (자동 반투명) |
| 보조 컨테이너 | bg-surface-2 (자동 20% 반투명) |
| 메인 레이아웃 컨테이너 | wallpaper-blur 클래스 추가 |
| 플로팅 드롭다운/메뉴 | wallpaper-panel 클래스 추가 |
| 다이얼로그/모달 | wallpaper-solid 클래스 추가 |
| 입력 필드 | <input> / <textarea> + bg-surface-1 (자동 제외, 불투명 유지) |
배경화면 지원이 내장된 컴포넌트
반투명 프로스티드 글래스 (wallpaper-panel):
Select드롭다운 패널DropdownMenuContent/DropdownMenuSubContentContextMenuContent/ContextMenuSubContent
완전 불투명 (wallpaper-solid):
DialogContent
금지 사항
- 카드 배경으로 bare
bg-surface-0사용 금지 (배경화면 모드에서 완전 투명해져 사라짐) bg-white,bg-black,bg-gray-*,bg-neutral-*같은 하드코딩 색상 사용 금지- 불투명 효과가 필요한 경우
wallpaper-solid클래스를 함께 추가
사용자 커스터마이징 가능한 색상 오버레이 레이어 (0.1.11+)
위의 "기본 배경화면 투명도 시스템" 위에 배경화면 패널은 세 가지 사용자 조정 가능한 색상 레이어를 노출합니다. 각 레이어는 body 데이터 속성 + CSS 변수로 구동되며, CSS 선택자가 계층적으로 기본 서피스 동작을 재정의합니다. 모든 쓰기는 themeUtils.applyWallpaperToDocument가 중앙에서 관리하며, 컴포넌트는 직접 body.style.setProperty를 호출해서는 안 됩니다.
1. 디밍 레이어 (body::before 가상 요소)
| 데이터 속성 | 트리거 조건 | CSS 변수 |
|---|---|---|
data-wallpaper-active="true" | 배경화면 소스 준비 완료 | --wp-dimming (0–1), --wp-dim-{h,s,l} |
data-wp-dim-gradient="true" | wallpaperDimmingGradient 설정됨 | --wp-dim-gradient (HSL 재정의) |
밝기 폴백: --wp-dim-l이 설정되지 않은 경우 라이트 모드 기본값은 100%, 다크 모드 기본값은 0% (기존 white/black 동작에 해당).
2. 요소 서피스 레이어 (사이드바 / 카드 / 탭 / 컨텍스트 메뉴)
| 데이터 속성 | 트리거 조건 | CSS 변수 |
|---|---|---|
data-wp-element-tint="true" | wallpaperElementTint가 유효한 hex | --wp-elem-{h,s,l} |
data-wp-element-gradient="true" | wallpaperElementGradient 설정됨 | --wp-elem-gradient-{15,20,35} (서피스 티어 alpha 버전별) |
디자인 핵심: 각 서피스 티어는 독립적인 alpha를 유지합니다 (surface-1 = 15%, surface-2 = 20%, .wallpaper-card = 35%). 따라서 같은 색조로 변경해도 시각적 계층이 구분됩니다. Input/Textarea와 wallpaper-solid / wallpaper-panel 선택자는 :not(...)으로 제외됩니다 — 색상 일관성보다 가독성 우선.
3. 메시지 버블 레이어 (사용자/어시스턴트 별도 적용)
| 데이터 속성 | 트리거 조건 | CSS 변수 |
|---|---|---|
data-wp-bubble-override="true" | wallpaperBubbleOverride === true | --wp-bubble-alpha (0–1) |
data-wp-bubble-tint-user="true" | 사용자 버블 hex가 유효 | --wp-bubble-user-{h,s,l} |
data-wp-bubble-tint-assistant="true" | 어시스턴트 버블 hex가 유효 | --wp-bubble-asst-{h,s,l} |
data-wp-bubble-gradient-{user,assistant}="true" | 해당 그라데이션 설정됨 | --wp-bubble-{user,asst}-gradient |
CSS 선택자는 두 레이어로 캐스케이드됩니다:
/* 레이어 1: override 꺼진 경우, 버블은 요소 tint 따름 (상속) */
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-user,
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-assistant {
background: hsl(var(--wp-elem-h) var(--wp-elem-s) var(--wp-elem-l) / var(--wp-alpha-35, 0.35)) !important;
}
/* 레이어 2: override 켜진 경우, per-side tint + 커스텀 alpha 적용 */
body[data-wp-bubble-override="true"][data-wp-bubble-tint-user="true"] .chat-bubble-user {
background: hsl(var(--wp-bubble-user-h) var(--wp-bubble-user-s) var(--wp-bubble-user-l) / var(--wp-bubble-alpha, var(--wp-alpha-35, 0.35))) !important;
}
레이어 2 선택자는 더 높은 특이성 + 나중에 등장하므로, !important 동점 시 우선합니다.
새 user-tint 필드 추가 시
ThemePreferences(settings-types.ts)와ThemePreferencesSchema(configSchema.ts) 양쪽에 필드 추가ThemeService.setWallpaperPreferences에 setter 분기 추가 +readPreferences/importProfile/resetTheme/mergePreferences에 기본값 추가ThemeRouter의 Zod 스키마 두 곳(themeProfileSchema와theme:setWallpaperPreferences의 인라인 스키마) 모두에 필드 추가ThemeContext에 context value + setWallpaperPreferences 파라미터 + commitState 폴백 추가themeUtils.applyWallpaperToDocument에 파라미터 +_prev*캐시 + body 속성/CSS 변수 쓰기 추가WallpaperPanel에 UI(토글/컬러 피커/슬라이더) 추가 + i18n 3개 언어index.css에 선택자 추가 (계층 순서와!important우선순위 주의)- 투과 체인:
AppearanceTab→Settings.tsx/ThemeStudioPage.tsx(DraftState + effective + Apply 제출 포함) - Agent 스텁:
desktop-api.ts(preload 계약),shared/agent/types/settings.ts(공유 인터페이스),shared/agent/web/theme.ts(HTTP 구현) - 이 표 업데이트 +
architecture-indexSKILL.md 필드 빠른 참조 +ipc-channels.md의theme:setWallpaperPreferences필드 표 +appearance.md사용자 문서
테마 호환 개발 규칙
시맨틱 토큰만 사용
#fff/rgb() 또는 Tailwind 기본 색상을 직접 쓰지 마세요.
ThemeContext 통일 사용
컴포넌트에서 테마 정보가 필요할 때는 useTheme()으로 읽으세요.
설정 가능한 폰트 존중
텍스트/코드 영역에는 CSS 변수를 사용하세요:
<div style={{ fontFamily: 'var(--font-ui)' }}>일반 텍스트</div>
<code style={{ fontFamily: 'var(--font-code)' }}>코드</code>
// 또는 Tailwind 클래스 사용
<div className="font-ui">일반 텍스트</div>
<code className="font-code">코드</code>
customCss 재정의 허용
!important와 대형 인라인 스타일을 피하고 className + CSS 변수를 우선 사용하세요.
셀프 테스트 체크리스트
새 UI 컴포넌트 생성 시:
- 시맨틱 토큰만 사용 (하드코딩 색상 없음)
-
useTheme()을 통해 테마 정보 가져오기 - 다크/라이트 모드 전환 테스트
- 배경화면 투명도 효과 테스트
- 일반 텍스트 대비율 >= 4.5:1
- 다크 모드 테두리에
dark:border-border/50이상 사용 -
text-subtle은surface-0배경에서만 사용 - 인터랙티브 요소에 시맨틱 태그 사용 또는
role+tabIndex추가 - 포커스 스타일에
focus-visible:ring-2사용 - 화면 밝기 30%에서도 텍스트와 테두리 식별 가능
- 창을 200%로 확대해도 레이아웃이 깨지지 않음