본문으로 건너뛰기

디자인 시스템

Elftia의 비주얼 디자인은 따뜻함, 친근감, 세련됨을 추구하며 차갑고 순수 기술적인 미감을 지양합니다.


디자인 철학

색상 원칙

  • 따뜻한 중성 색상: 모든 서피스 색상은 약간의 따뜻한 색조(hue 24-38)를 띠며 순수 그레이스케일을 사용하지 않습니다
  • 다크 모드: 순수 검정 대신 따뜻한 차콜(warm charcoal) 사용
  • 라이트 모드: 순수 흰색 대신 따뜻한 크림(warm cream) 사용
  • 차가운 그레이 금지: 배경이나 테두리에 순수 그레이(hsl(0, 0%, ...)) 사용 금지

모서리 반경 원칙

요소반경Tailwind 클래스
카드/컨테이너12pxrounded-xl
입력 필드12pxrounded-xl
버튼/배지6pxrounded-md
기본값8pxrounded-lg

4px 미만의 모서리 반경 사용 금지 (선 장식 제외).

타이포그래피 원칙

목적폰트Tailwind 클래스
디스플레이/대형 헤드라인Noto Serif / Georgiafont-display
본문/인터페이스Interfont-sans
코드JetBrains Monofont-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-backgroundvar(--background)
L0 배경bg-surface-0var(--surface-0)
L1 배경 (카드/사이드바)bg-surface-1var(--surface-1)
L2 배경 (입력/보조 컨테이너)bg-surface-2var(--surface-2)
L3 배경 (팝오버)bg-surface-3var(--surface-3)
주 텍스트text-foregroundvar(--foreground)
보조 텍스트text-muted-foregroundvar(--muted-foreground)
부가 텍스트text-text-subtlevar(--text-subtle)
테두리border-bordervar(--border)
테마 색상bg-primary / text-primaryvar(--primary)
성공text-successvar(--success)
오류text-destructivevar(--destructive)
경고text-warningvar(--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-07%페이지 주 배경#121212
L1--surface-112%사이드바, 카드#1E1E1E
L2--surface-217%보조 컨테이너, 입력#2B2928
L3--surface-322%팝오버, 드롭다운#383635

레벨 간 밝기 차이는 시각적 구분을 보장하기 위해 >= 4-5%여야 합니다.

테두리 규범

다크 모드에서는 어두운 영역에 대한 인간의 지각 민감도가 낮아지므로 테두리의 가시성을 높여야 합니다:

상황라이트 모드다크 모드
카드 테두리border-border/30 ~ /40border-border/50 ~ /70
구분선border-border/20 ~ /30border-border/40 ~ /50
입력 필드border-border/40border-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 컴포넌트-

드롭다운 컴포넌트

모든 드롭다운 컴포넌트는 다음을 지원해야 합니다:

  • 뷰포트 인식 위치 지정 (useDropdownPosition Hook 사용)
  • 외부 클릭 시 닫기
  • Escape 키로 닫기
  • 항목 테마 색상 및 스타일

배경화면 투명도 시스템

사용자가 배경화면을 설정하면 bodydata-wallpaper-active="true" 속성이 추가되어 CSS 투명도 규칙이 적용됩니다.

CSS 규칙 계층

우선순위선택자효과목적
1.bg-background, .bg-surface-0완전 투명페이지 주 배경
2.wallpaper-blur35% 불투명 + blur주 컨테이너 (WorkspaceShell)
3.wallpaper-blur .wallpaper-blur투명 + blur*0.67중첩 컨테이너 (겹침 방지)
4.wallpaper-blurbg-surface-0/XX투명레이아웃 패널
5.wallpaper-blurbg-surface-1/XX12% + blur콘텐츠 카드 (alpha 변형)
6bg-surface-1, bg-popover15% + blur버튼/카드 (입력 제외)
7bg-surface-220% + blur보조 컨테이너 (입력 제외)
8.wallpaper-panel85% + 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 / DropdownMenuSubContent
  • ContextMenuContent / 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 필드 추가 시

  1. ThemePreferences(settings-types.ts)와 ThemePreferencesSchema(configSchema.ts) 양쪽에 필드 추가
  2. ThemeService.setWallpaperPreferences에 setter 분기 추가 + readPreferences/importProfile/resetTheme/mergePreferences에 기본값 추가
  3. ThemeRouter의 Zod 스키마 두 곳(themeProfileSchematheme:setWallpaperPreferences의 인라인 스키마) 모두에 필드 추가
  4. ThemeContext에 context value + setWallpaperPreferences 파라미터 + commitState 폴백 추가
  5. themeUtils.applyWallpaperToDocument에 파라미터 + _prev* 캐시 + body 속성/CSS 변수 쓰기 추가
  6. WallpaperPanel에 UI(토글/컬러 피커/슬라이더) 추가 + i18n 3개 언어
  7. index.css에 선택자 추가 (계층 순서와 !important 우선순위 주의)
  8. 투과 체인: AppearanceTabSettings.tsx / ThemeStudioPage.tsx (DraftState + effective + Apply 제출 포함)
  9. Agent 스텁: desktop-api.ts (preload 계약), shared/agent/types/settings.ts (공유 인터페이스), shared/agent/web/theme.ts (HTTP 구현)
  10. 이 표 업데이트 + architecture-index SKILL.md 필드 빠른 참조 + ipc-channels.mdtheme: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-subtlesurface-0 배경에서만 사용
  • 인터랙티브 요소에 시맨틱 태그 사용 또는 role + tabIndex 추가
  • 포커스 스타일에 focus-visible:ring-2 사용
  • 화면 밝기 30%에서도 텍스트와 테두리 식별 가능
  • 창을 200%로 확대해도 레이아웃이 깨지지 않음