デザインシステム
Elftia のビジュアルデザインは、冷たく純粋に技術的な美学を避けながら、温かみ、親しみやすさ、洗練さを追求します。
デザイン哲学
色の原則
- 暖色系ニュートラルカラー: すべてのサーフェスカラーは、純粋なグレースケールではなく、わずかに暖かい色調(hue 24-38)を持ちます
- ダークモード: 純粋な黒ではなく、暖かみのあるチャコールを使用します
- ライトモード: 純粋な白ではなく、暖かみのあるクリームを使用します
- 寒色系グレー禁止: 背景やボーダーに純粋なグレー(
hsl(0, 0%, ...))を使用しないでください
角丸の原則
| 要素 | 半径 | Tailwind Class |
|---|---|---|
| カード/コンテナ | 12px | rounded-xl |
| 入力フィールド | 12px | rounded-xl |
| ボタン/バッジ | 6px | rounded-md |
| デフォルト | 8px | rounded-lg |
4px 未満の角丸は避けてください(線の装飾を除く)。
タイポグラフィの原則
| 目的 | フォント | Tailwind Class |
|---|---|---|
| ディスプレイ/大きな見出し | Noto Serif / Georgia | font-display |
| 本文/インターフェース | Inter | font-sans |
| コード | JetBrains Mono | font-mono |
見出しには font-bold ではなく font-semibold を使用し、tracking-tight と組み合わせてください。
カラーシステム
セマンティックトークン
すべての色は CSS 変数で定義し、Tailwind 設定でユーティリティクラスにマッピングします。ハードコードされた色値は絶対に使用しないでください。
// Don't do this
<div className="bg-white text-black border-gray-200">
<div style={{ backgroundColor: '#ffffff' }}>
// Correct
<div className="bg-surface-0 text-foreground border-border">
<div style={{ backgroundColor: 'var(--surface-0)' }}>
よく使うトークン参照
| 目的 | Tailwind Class | CSS Variable |
|---|---|---|
| ページ背景 | 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 で一元管理します。
// Correct: get theme info via useTheme
import { useTheme } from '@/shared/state/themeStore';
function MyComponent() {
const { mode, resolvedMode, userTheme } = useTheme();
}
// Don't: manual detection
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% | ポップオーバー、Dropdown | #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 bold) | 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 のみ | タイムスタンプ、メタデータ |
// Good: description text uses text-muted
<p className="text-muted-foreground">5 models total</p>
// Bad: using text-subtle on surface-1 card (insufficient contrast)
<div className="bg-surface-1">
<span className="text-text-subtle">Hard to read</span>
</div>
色による情報伝達
ステータスを伝えるために色だけに依存してはいけません。必ずテキストラベルまたはアイコンと組み合わせてください。
// Bad: color only
<div className={status === 'error' ? 'border-red-500' : 'border-border'} />
// Good: color + icon + text
<div className={status === 'error' ? 'border-destructive' : 'border-border'}>
{status === 'error' && <AlertCircle className="text-destructive" />}
<span>{errorMessage}</span>
</div>
フォーカス状態
キーボードナビゲーションのユーザーにフォーカスボーダーを提供するため、focus-visible を使用してください。
// Good
<button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none">
Button
</button>
// Don't: remove focus styles
<button className="outline-none focus:outline-none">Button</button>
モーション設定
システムの prefers-reduced-motion 設定を尊重してください。
<div className="motion-safe:animate-fadeIn motion-reduce:animate-none">
Content
</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 コンポーネント | - |
Dropdown コンポーネント
すべての Dropdown コンポーネントは、以下をサポートする必要があります。
- ビューポートを考慮した位置決め(
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 | ボタン/カード(input を除外) |
| 7 | bg-surface-2 | 20% + blur | セカンダリコンテナ(input を除外) |
| 8 | .wallpaper-panel | 85% + blur*1.33 | フローティング Dropdown/menu |
| 9 | .wallpaper-solid | 不透明な surface-0 | Dialog/Modal |
階層スタッキングモデル
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 クラスを追加 |
| フローティング Dropdown/menu | wallpaper-panel クラスを追加 |
| Dialog/Modal | wallpaper-solid クラスを追加 |
| 入力フィールド | <input> / <textarea> + bg-surface-1(自動除外、ソリッドを維持) |
壁紙サポートが組み込まれたコンポーネント
半透明のフロストガラス(wallpaper-panel):
SelectDropdown パネルDropdownMenuContent/DropdownMenuSubContentContextMenuContent/ContextMenuSubContent
完全に不透明(wallpaper-solid):
DialogContent
禁止事項
- カード背景として素の
bg-surface-0を使用しないでください(壁紙モードでは完全に透明になって消えます) bg-white、bg-black、bg-gray-*、bg-neutral-*のようなハードコード色を使用しないでください- 不透明な効果が必要な場合は、同時に
wallpaper-solidクラスを追加してください
ユーザーがカスタマイズ可能なカラーオーバーレイレイヤー(0.1.11+)
上記の「基本的な壁紙透過システム」に加えて、壁紙パネルはユーザーが調整可能な 3 種類のカラーレイヤーを公開します。それぞれ body data 属性 + CSS 変数 によって駆動され、CSS セレクタがレイヤーごとにデフォルトのサーフェス動作を上書きします。すべての書き込みは themeUtils.applyWallpaperToDocument で一元管理し、コンポーネントが直接 body.style.setProperty してはいけません。
1. 調光レイヤー(body::before 疑似要素)
| Data Attribute | トリガー | CSS Variable |
|---|---|---|
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% になります(元の白/黒の動作に相当)。
2. 要素サーフェスレイヤー(サイドバー / カード / タブ / コンテキストメニュー)
| Data Attribute | トリガー | CSS Variable |
|---|---|---|
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. メッセージバブルレイヤー(user / assistant を分離)
| Data Attribute | トリガー | CSS Variable |
|---|---|---|
data-wp-bubble-override="true" | wallpaperBubbleOverride === true | --wp-bubble-alpha (0–1) |
data-wp-bubble-tint-user="true" | User バブルの hex が有効 | --wp-bubble-user-{h,s,l} |
data-wp-bubble-tint-assistant="true" | Assistant バブルの hex が有効 | --wp-bubble-asst-{h,s,l} |
data-wp-bubble-gradient-{user,assistant}="true" | 対応する gradient が設定済み | --wp-bubble-{user,asst}-gradient |
CSS セレクタは 2 つのレイヤーでカスケードします。
/* Layer 1: when override off, bubbles follow element tint (inherit) */
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;
}
/* Layer 2: when override on, per-side tint + custom alpha applies */
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;
}
Layer 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(toggle/color picker/slider)と 3 言語の i18n を追加するindex.cssにセレクタを追加する(階層順序と!importantの優先度に注意)- パススルーチェーン:
AppearanceTab→Settings.tsx/ThemeStudioPage.tsx(DraftState + effective + Apply submit を含む) - Agent スタブ:
desktop-api.ts(preload contract)、shared/agent/types/settings.ts(shared interface)、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)' }}>Normal text</div>
<code style={{ fontFamily: 'var(--font-code)' }}>Code</code>
// Or use Tailwind classes
<div className="font-ui">Normal text</div>
<code className="font-code">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% に拡大してもレイアウトが崩れない