# 機能設計書 41-テーマ設定

## 概要

本ドキュメントは、Actualの「テーマ設定」機能の設計内容を記載する。ユーザーインターフェースの配色を変更するテーマ機能について、処理内容、データ構造、UIコンポーネントの仕様を定義する。

### 本機能の処理概要

テーマ設定機能は、Actualアプリケーションの視覚的な外観を制御する機能である。組み込みテーマ（ライト、ダーク、ミッドナイト、システムデフォルト）の選択と、カスタムテーマのインストールをサポートする。

**業務上の目的・背景**：ユーザーの視覚的好みや作業環境（明るい場所/暗い場所）に応じてUIの配色を調整することで、長時間の使用でも目の疲れを軽減し、ユーザーエクスペリエンスを向上させる。また、カスタムテーマ機能により、企業やチームのブランドカラーに合わせたカスタマイズも可能となる。

**機能の利用シーン**：設定画面からテーマを変更する場合、夜間作業時にダークテーマへ切り替える場合、システムの外観設定に自動追従させたい場合、コミュニティ提供のカスタムテーマを導入したい場合に利用される。

**主要な処理内容**：
1. 組み込みテーマ（Light/Dark/Midnight/Auto）の選択と適用
2. システム設定に追従する「Auto」モード（ダークテーマ選択可能）
3. カスタムテーマのカタログからの選択・インストール
4. CSS直接入力によるカスタムテーマの適用
5. テーマCSSのバリデーション（セキュリティ対策）
6. グローバル設定への保存とリアルタイム反映

**関連システム・外部連携**：GitHubからのテーマカタログ取得、カスタムテーマCSSのリモート取得

**権限による制御**：特になし（全ユーザーが利用可能）。ただしカスタムテーマ機能は実験的機能フラグ「customThemes」による制御がある。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 40 | 設定画面 | 主機能 | ライト/ダークテーマの切り替え設定 |

## 機能種別

設定・表示切替処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| theme | Theme | Yes | 選択するテーマ種別 | 'light' / 'dark' / 'midnight' / 'auto' / 'development' のいずれか |
| preferredDarkTheme | DarkTheme | No | Auto選択時のダークテーマ種別 | 'dark' / 'midnight' のいずれか |
| installedCustomTheme | string (JSON) | No | インストール済みカスタムテーマ | InstalledTheme形式のJSON文字列 |
| pastedCss | string | No | 直接入力されたCSS | validateThemeCss関数によるバリデーション |

### 入力データソース

- 画面入力（テーマ選択ドロップダウン）
- GitHubカタログ（カスタムテーマ一覧）
- テキストエリア入力（CSS直接入力）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| theme | Theme | 選択されたテーマ |
| preferredDarkTheme | DarkTheme | Autoモード時のダークテーマ |
| installedCustomTheme | string | インストール済みカスタムテーマのJSON |

### 出力先

- グローバル設定（global-store.json）
- DOMへのスタイル注入（`<style>`タグ）

## 処理フロー

### 処理シーケンス

```
1. テーマ選択ドロップダウンの変更検知
   └─ handleThemeChange関数が呼び出される

2. 選択値の判定
   ├─ 組み込みテーマの場合
   │    └─ switchTheme関数でグローバル設定を更新
   ├─ "Custom theme"の場合
   │    └─ ThemeInstallerモーダルを表示
   └─ カスタムテーマIDの場合
        └─ 既にインストール済みのため処理なし

3. カスタムテーマインストール（ThemeInstaller）
   ├─ カタログからの選択
   │    ├─ GitHubからactual.cssを取得
   │    ├─ CSSバリデーション実行
   │    └─ グローバル設定に保存
   └─ CSS直接入力
        ├─ CSSバリデーション実行
        └─ グローバル設定に保存

4. スタイル適用（ThemeStyle/CustomThemeStyle）
   └─ CSS変数を:rootに注入
```

### フローチャート

```mermaid
flowchart TD
    A[テーマ設定開始] --> B{選択値の種類}
    B -->|組み込みテーマ| C[switchTheme呼び出し]
    B -->|Custom theme| D[ThemeInstaller表示]
    B -->|custom:ID| E[処理なし]

    C --> F[グローバル設定更新]

    D --> G{インストール方法}
    G -->|カタログ選択| H[GitHubからCSS取得]
    G -->|CSS直接入力| I[入力CSSを使用]

    H --> J[validateThemeCss]
    I --> J

    J -->|成功| K[InstalledTheme作成]
    J -->|失敗| L[エラー表示]

    K --> M[グローバル設定に保存]

    F --> N[ThemeStyle/CustomThemeStyle]
    M --> N

    N --> O[CSS変数を:rootに注入]
    O --> P[UI反映完了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-41-01 | デフォルトテーマ | 初期状態では'auto'テーマが選択される | テーマ未設定時 |
| BR-41-02 | システム追従 | 'auto'選択時はシステムの外観設定に自動追従 | theme === 'auto' |
| BR-41-03 | ダークテーマ選択 | Auto選択時のダークテーマはユーザーが選択可能（dark/midnight） | theme === 'auto' かつ カスタムテーマ未インストール |
| BR-41-04 | カスタムテーマ制限 | カスタムテーマは実験的機能フラグ有効時のみ利用可能 | flags.customThemes === true |
| BR-41-05 | CSS検証必須 | カスタムテーマCSSは必ずバリデーションを通過する必要がある | カスタムテーマ適用時 |

### 計算ロジック

カスタムテーマIDの生成:
```typescript
function generateThemeId(urlOrRepo: string): string {
  let hash = 0;
  for (let i = 0; i < urlOrRepo.length; i++) {
    const char = urlOrRepo.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return `theme-${Math.abs(hash).toString(36)}`;
}
```

## データベース操作仕様

### 操作別データベース影響一覧

本機能はデータベースを直接操作しない。グローバル設定ファイル（global-store.json）に保存される。

| 操作 | 対象 | 操作種別 | 概要 |
|-----|------|---------|------|
| テーマ変更 | global-store.json | UPDATE | theme, preferredDarkTheme, installedCustomThemeを更新 |

### 設定ファイル詳細

#### global-store.json

| キー | 型 | 説明 |
|-----|-----|------|
| theme | Theme | 選択中のテーマ |
| preferredDarkTheme | DarkTheme | Autoモード時のダークテーマ |
| installedCustomTheme | string | InstalledTheme形式のJSON文字列 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| THEME_FETCH_FAILED | 取得エラー | GitHubからのCSS取得失敗 | エラーメッセージ表示、CSS直接入力を案内 |
| THEME_VALIDATION_FAILED | 検証エラー | CSSが検証ルールに違反 | 具体的な違反内容をエラーメッセージで表示 |
| CATALOG_LOAD_FAILED | 取得エラー | テーマカタログの取得失敗 | CSS直接入力のみ利用可能と案内 |

### CSSバリデーションエラー詳細

validateThemeCss関数は以下のエラーをスロー:
- `:root { ... }`以外のセレクタ検出
- CSS変数以外のプロパティ検出
- @import, @media等のat-rule検出
- ネストされたブロック検出
- var()等の関数使用検出
- URL()の使用検出

### リトライ仕様

GitHubからのCSS取得失敗時: ユーザーによる再試行のみ（自動リトライなし）

## トランザクション仕様

本機能はデータベーストランザクションを使用しない。グローバル設定の保存は非同期で行われ、保存失敗時は次回起動時に前回の設定が復元される。

## パフォーマンス要件

- テーマ切替: 即座に反映（100ms以内）
- カスタムテーマCSS取得: 最大20秒のタイムアウト
- CSSバリデーション: 通常1ms以内

## セキュリティ考慮事項

1. **CSSインジェクション対策**: validateThemeCss関数によりホワイトリスト方式で許可されたCSSのみ適用
   - :root セレクタのみ許可
   - CSS変数（--で始まるプロパティ）のみ許可
   - var()、url()等の関数は禁止
   - @import、@media等のat-ruleは禁止

2. **外部リソース制限**: カスタムテーマはGitHubの公開リポジトリからのみ取得可能

3. **ローカルストレージ**: テーマ設定はローカルのglobal-store.jsonに保存され、サーバーには送信されない

## 備考

- 開発環境では'development'テーマが追加で利用可能
- カスタムテーマ機能は実験的機能として提供されており、将来的に仕様が変更される可能性がある
- テーマカタログはGitHub上のactualbudget/actual-themesリポジトリから取得される

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

テーマ設定で使用されるデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | customThemes.ts | `packages/desktop-client/src/style/customThemes.ts` | CatalogTheme型とInstalledTheme型の定義（1-16行目） |
| 1-2 | prefs.d.ts | `packages/loot-core/src/types/prefs.d.ts` | Theme型、DarkTheme型、GlobalPrefsの定義 |

**読解のコツ**: InstalledThemeはid, name, repo, cssContentを持ち、インストール済みカスタムテーマを表現する。cssContentは必須フィールドで、テーマのCSS全体を保持する。

#### Step 2: エントリーポイントを理解する

テーマ設定UIのエントリーポイントを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | Themes.tsx | `packages/desktop-client/src/components/settings/Themes.tsx` | ThemeSettingsコンポーネント全体の構造（34-188行目） |

**主要処理フロー**:
1. **34-45行目**: 状態管理フックの初期化（useTheme, usePreferredDarkTheme, useGlobalPref）
2. **51-72行目**: buildOptions関数でドロップダウンオプションを構築
3. **83-100行目**: handleThemeChange関数でテーマ選択を処理
4. **115-187行目**: UIのレンダリング

#### Step 3: テーマ切替ロジックを理解する

テーマの保存と適用の仕組みを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | theme.tsx | `packages/desktop-client/src/style/theme.tsx` | useTheme, ThemeStyle, CustomThemeStyleの実装（36-136行目） |
| 3-2 | useGlobalPref.ts | `packages/desktop-client/src/hooks/useGlobalPref.ts` | グローバル設定の読み書きフック（12-35行目） |

**主要処理フロー**:
- **47-101行目（theme.tsx）**: ThemeStyleコンポーネントがCSS変数を:rootに注入
- **58-93行目（theme.tsx）**: Autoモード時はMediaQueryListで外観変更を監視
- **107-136行目（theme.tsx）**: CustomThemeStyleがカスタムテーマCSSを注入

#### Step 4: カスタムテーマインストーラーを理解する

カスタムテーマのインストール処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | ThemeInstaller.tsx | `packages/desktop-client/src/components/settings/ThemeInstaller.tsx` | インストーラーUI全体の構造（41-443行目） |
| 4-2 | customThemes.ts | `packages/desktop-client/src/style/customThemes.ts` | CSSのフェッチとバリデーション（57-311行目） |

**主要処理フロー**:
- **96-137行目（ThemeInstaller.tsx）**: installTheme関数でCSS取得・検証・保存
- **139-155行目（ThemeInstaller.tsx）**: handleCatalogThemeClickでカタログからのインストール
- **164-174行目（ThemeInstaller.tsx）**: handleInstallPastedCssでCSS直接入力のインストール
- **135-259行目（customThemes.ts）**: validateThemeCss関数でセキュリティ検証

### プログラム呼び出し階層図

```
ThemeSettings (Themes.tsx)
    │
    ├─ useTheme() → グローバル設定からthemeを取得
    │      └─ useGlobalPref('theme')
    │
    ├─ usePreferredDarkTheme() → preferredDarkThemeを取得
    │      └─ useGlobalPref('preferredDarkTheme')
    │
    ├─ useGlobalPref('installedCustomTheme') → カスタムテーマJSON取得
    │
    ├─ handleThemeChange()
    │      ├─ switchTheme() → テーマ変更
    │      └─ setShowInstaller(true) → インストーラー表示
    │
    └─ ThemeInstaller
           ├─ useThemeCatalog() → カタログ取得
           ├─ installTheme()
           │      ├─ fetchThemeCss() → GitHub から CSS 取得
           │      ├─ validateThemeCss() → CSS 検証
           │      └─ onInstall() → setInstalledThemeJson()
           └─ handleInstallPastedCss()
                  ├─ validateThemeCss()
                  └─ onInstall()

ThemeStyle (theme.tsx)
    └─ CSS変数を<style>タグで:rootに注入

CustomThemeStyle (theme.tsx)
    ├─ parseInstalledTheme() → JSON パース
    ├─ validateThemeCss() → CSS 検証
    └─ <style>タグでカスタムCSSを注入
```

### データフロー図

```
[入力]                    [処理]                         [出力]

テーマ選択               handleThemeChange()
ドロップダウン ───────▶  switchTheme() ──────────────▶ グローバル設定
                                                        (theme)

ダークテーマ             switchDarkTheme() ──────────▶ グローバル設定
選択 ─────────────────▶                                 (preferredDarkTheme)

カタログ                 handleCatalogThemeClick()
テーマ選択 ────────────▶ fetchThemeCss() ─────────────▶
                         validateThemeCss() ──────────▶ グローバル設定
                         installTheme() ─────────────▶ (installedCustomTheme)

CSS直接入力 ────────────▶ handleInstallPastedCss()
                         validateThemeCss() ──────────▶ グローバル設定
                         installTheme() ─────────────▶ (installedCustomTheme)

グローバル設定           ThemeStyle
(theme) ────────────────▶ useEffect() ─────────────────▶ <style>タグ
                                                         (CSS変数)

グローバル設定           CustomThemeStyle
(installedCustomTheme) ─▶ parseInstalledTheme() ──────▶ <style>タグ
                         validateThemeCss() ──────────▶ (カスタムCSS)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| Themes.tsx | `packages/desktop-client/src/components/settings/Themes.tsx` | ソース | テーマ設定UIコンポーネント |
| ThemeInstaller.tsx | `packages/desktop-client/src/components/settings/ThemeInstaller.tsx` | ソース | カスタムテーマインストーラーUI |
| theme.tsx | `packages/desktop-client/src/style/theme.tsx` | ソース | テーマ管理ロジックとスタイル注入 |
| customThemes.ts | `packages/desktop-client/src/style/customThemes.ts` | ソース | カスタムテーマ取得・検証ユーティリティ |
| useGlobalPref.ts | `packages/desktop-client/src/hooks/useGlobalPref.ts` | ソース | グローバル設定読み書きフック |
| useFeatureFlag.ts | `packages/desktop-client/src/hooks/useFeatureFlag.ts` | ソース | 機能フラグ確認フック |
| useThemeCatalog.ts | `packages/desktop-client/src/hooks/useThemeCatalog.ts` | ソース | テーマカタログ取得フック |
| ColorPalette.tsx | `packages/desktop-client/src/components/settings/ColorPalette.tsx` | ソース | カラーパレット表示コンポーネント |
| UI.tsx | `packages/desktop-client/src/components/settings/UI.tsx` | ソース | 設定画面共通UIコンポーネント |
| light.ts | `packages/desktop-client/src/style/themes/light.ts` | ソース | ライトテーマ色定義 |
| dark.ts | `packages/desktop-client/src/style/themes/dark.ts` | ソース | ダークテーマ色定義 |
| midnight.ts | `packages/desktop-client/src/style/themes/midnight.ts` | ソース | ミッドナイトテーマ色定義 |
