# 画面設計書 3-VersionSelect（バージョン選択）

## 概要

本ドキュメントは、SFC Playground のバージョン選択コンポーネント（VersionSelect.vue）の画面設計書である。Vue および TypeScript のバージョンをドロップダウンで選択する機能を提供し、CDN（jsdelivr）からバージョン一覧を動的に取得する。Header コンポーネント内で2回使用される。

### 本画面の処理概要

**業務上の目的・背景**：SFC Playground で使用する Vue および TypeScript のバージョンを柔軟に切り替えられるようにする。開発者が特定のバージョンでのコンパイル結果を確認したり、バージョン間の動作差異を検証したりする際に使用する。バージョン一覧は jsdelivr の API から動的に取得されるため、新しいバージョンがリリースされた際にも自動的に反映される。

**画面へのアクセス方法**：Header コンポーネント内に配置されており、SFC Playground にアクセスすると自動的にナビゲーションバー内に表示される。ドロップダウンは現在のバージョン表示部分をクリックすることで展開される。

**主要な操作・処理内容**：
1. 現在選択されているバージョンの表示
2. ドロップダウンの展開/折りたたみ
3. jsdelivr API からバージョン一覧の非同期取得
4. バージョンの選択と親コンポーネントへの反映（defineModel 経由）
5. バージョン番号のクリップボードコピー
6. ドロップダウン外クリックによる自動閉じ

**画面遷移**：本コンポーネントは画面遷移を伴わない。バージョン選択は親コンポーネントへのデータ反映のみを行う。

**権限による表示制御**：権限による表示制御は存在しない。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 1 | ref() | 主機能 | expanded（ドロップダウン展開状態）、versions（バージョン配列）のリアクティブ管理 |
| 14 | defineProps() / defineEmits() | 主機能 | pkg（パッケージ名）、label（表示ラベル）の Props 定義 |
| 16 | defineSlots() / defineModel() | 主機能 | defineModel() による version 値の v-model バインディング定義 |
| 18 | onBeforeMount() / onMounted() | 補助機能 | ウィンドウクリック・blur イベントリスナー登録（ドロップダウン外クリック時の閉じ処理） |
| 34 | v-on修飾子 | 補助機能 | @click.stop 修飾子によるイベント伝播制御（ドロップダウン内クリック時） |

## 画面種別

ドロップダウン選択コンポーネント

## URL/ルーティング

本コンポーネントは個別の URL/ルーティングを持たない。Header.vue 内に組み込まれて使用される。

## 入出力項目

| 項目名 | 入力/出力 | データ型 | 説明 | 初期値 |
|--------|----------|---------|------|--------|
| pkg | Props（入力） | string | npm パッケージ名（'vue' または 'typescript'） | 必須 |
| label | Props（入力） | string | 表示ラベル（'Vue Version' または 'TypeScript Version'） | 必須 |
| version (defineModel) | 入出力 | string \| undefined | 選択されたバージョン文字列 | 親コンポーネントから注入 |
| expanded | 内部 ref | boolean | ドロップダウン展開状態 | false |
| versions | 内部 ref | string[] \| undefined | 取得済みバージョン一覧 | undefined |
| default スロット | スロット（入力） | VNode | ドロップダウン末尾に追加される要素 | 省略可能 |

## 表示項目

| 項目名 | 表示領域 | データソース | 説明 |
|--------|---------|-------------|------|
| ラベル | ドロップダウントリガー | label Props | "Vue Version" / "TypeScript Version" 等 |
| 現在のバージョン | ドロップダウントリガー右 | version (defineModel) | 緑色で表示される現在のバージョン番号 |
| 展開インジケーター | ラベル右 | CSS疑似要素 | 下向き三角形（▼）の CSS ボーダートリック |
| ローディング | ドロップダウン内 | versions が undefined | "loading versions..." テキスト |
| バージョン一覧 | ドロップダウン内 | versions 配列 | "v{バージョン番号}" 形式のリスト。アクティブバージョンは緑色表示 |
| コピーボタン | 各バージョン行右 | - | ホバー時に表示される Copy アイコン |
| スロットコンテンツ | ドロップダウン末尾 | default スロット | Vue版では "This Commit" と "Commits History" リンク |

## イベント仕様

### 1-ドロップダウン展開/折りたたみ

`.active-version` 要素をクリックすると `toggle()` 関数が実行される（14-19行目）。
1. `expanded.value` を反転（15行目）
2. `versions.value` が未取得（undefined）の場合、`fetchVersions()` を非同期呼び出し（16-18行目）
3. ドロップダウンのルート要素に `@click.stop` を設定し、内部クリックのイベント伝播を抑制（76行目）

### 2-バージョン一覧の取得（fetchVersions）

初回展開時に `fetchVersions()` が呼ばれる（21-50行目）。
1. `https://data.jsdelivr.com/v1/package/npm/{pkg}` に fetch リクエスト送信（22-24行目）
2. レスポンスから versions 配列を取得（25行目）
3. パッケージ種別による フィルタリング:
   - **Vue の場合**（27-45行目）: 最新バージョンがプレリリースの場合はプレリリースを含め、そうでなければプレリリースを除外。最大30件、または v3.0.10 まで
   - **TypeScript の場合**（46-48行目）: 'dev' と 'insiders' を含むバージョンを除外
   - **その他**（49行目）: フィルタリングなしで全バージョンを返す

### 3-バージョン選択

バージョン一覧の各項目をクリックすると `setVersion(v)` が実行される（52-55行目）。
1. `version.value = v` で defineModel の値を更新（53行目）
2. `expanded.value = false` でドロップダウンを閉じる（54行目）

### 4-バージョンコピー

各バージョン行のコピーボタンをクリックすると `copyVersion(v)` が実行される（57-61行目）。
1. `window.navigator.clipboard.writeText(v)` でバージョン文字列をクリップボードにコピー（58行目）
2. コピー成功時に `alert('Vue version has been copied to clipboard.')` を表示（59行目）

### 5-ドロップダウン外クリックによる閉じ

`onMounted` で以下のイベントリスナーを登録（63-72行目）:
1. `window.addEventListener('click', ...)`: ウィンドウ上の任意の場所をクリックすると `expanded.value = false`（64-66行目）
2. `window.addEventListener('blur', ...)`: iframe にフォーカスが移った場合に `expanded.value = false`（67-71行目）

ドロップダウン内部のクリックは `@click.stop`（76行目）により window のクリックイベントに伝播しないため、ドロップダウンは閉じない。

### 6-スロットコンテンツクリック

スロットを囲む div に `@click="expanded = false"` を設定（100行目）。スロット内の要素をクリックするとドロップダウンが閉じる。

## データベース更新仕様

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

本コンポーネントはクライアントサイドのみで動作するため、データベースへのアクセスは発生しない。

| 操作（イベント） | 対象 | 操作種別 | 概要 |
|----------------|------|---------|------|
| バージョン一覧取得 | jsdelivr API | GET | `https://data.jsdelivr.com/v1/package/npm/{pkg}` から読み取り |
| バージョンコピー | クリップボード | WRITE | バージョン文字列をクリップボードに書き込み |

## メッセージ仕様

| メッセージ種別 | メッセージ内容 | 発生条件 | 表示方法 |
|--------------|--------------|---------|---------|
| 情報 | "Vue version has been copied to clipboard." | バージョンコピーボタンクリック成功時 | alert() |
| ローディング | "loading versions..." | バージョン一覧取得中 | ドロップダウン内テキスト表示 |

## 例外処理

| 例外条件 | 処理内容 | 発生箇所 |
|---------|---------|---------|
| jsdelivr API 通信失敗 | ハンドリングなし（fetchVersions に try-catch なし） | VersionSelect.vue 21-50行目 |
| クリップボード API 非対応 | Promise が reject される（ハンドリングなし） | VersionSelect.vue 58行目 |

## 備考

- defineModel() は Vue 3.4+ で安定版となった機能であり、v-model の双方向バインディングを簡潔に定義できる
- Vue 用の VersionSelect では Header.vue 側で `:model-value` と `@update:model-value` を分離して使用しており、直接 v-model ではなくカスタムハンドラを介している
- TypeScript 用の VersionSelect では `v-model="store.typescriptVersion"` で直接バインドしている
- ドロップダウンの最大高さは `calc(100vh - 70px)` で制限され、スクロール可能である
- バージョン一覧は初回展開時にのみ取得され、その後はキャッシュされる（versions ref に保持）

---

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

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

### 推奨読解順序

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

defineModel と defineProps によるコンポーネントインターフェースを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 5-6行目: ref による内部状態定義（expanded, versions） |
| 1-2 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 8行目: defineModel() による双方向バインディング定義 |
| 1-3 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 9-12行目: defineProps による Props 型定義（pkg, label） |

**読解のコツ**: `defineModel()` は Vue 3.4 で導入されたマクロで、暗黙的に `modelValue` props と `update:modelValue` イベントを定義する。戻り値は ref オブジェクトとして振る舞う。

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

Header.vue からの呼び出しパターンを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | Header.vue | `packages-private/sfc-playground/src/Header.vue` | 76-80行目: TypeScript 用 VersionSelect（v-model 直接バインド） |
| 2-2 | Header.vue | `packages-private/sfc-playground/src/Header.vue` | 81-97行目: Vue 用 VersionSelect（カスタムイベント + スロット） |

**主要処理フロー**:
1. **76-80行目**: TypeScript用 - `v-model="store.typescriptVersion"` で store プロパティに直接バインド
2. **81-97行目**: Vue用 - `:model-value="vueVersion"` と `@update:model-value="setVueVersion"` で間接バインド。スロットに "This Commit" リンクと "Commits History" リンクを挿入

#### Step 3: バージョン取得ロジックを理解する

fetchVersions 関数のフィルタリングロジックを詳細に把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 21-50行目: fetchVersions 非同期関数 |
| 3-2 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 27-45行目: Vue パッケージ用フィルタリング（プレリリース制御、30件制限） |
| 3-3 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 46-48行目: TypeScript パッケージ用フィルタリング（dev/insiders除外） |

**主要処理フロー**:
- **22-25行目**: jsdelivr API へ fetch リクエスト、レスポンスから versions 配列を抽出
- **30行目**: 最新バージョンがプレリリースかどうかを判定（'-' を含むか）
- **31-44行目**: プレリリース版の包含/除外ロジックとバージョン数制限
- **47行目**: TypeScript は 'dev' と 'insiders' サフィックスのバージョンを除外

#### Step 4: テンプレートとイベントハンドリングを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 75-105行目: テンプレート全体構造 |
| 4-2 | VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | 63-72行目: onMounted でのイベントリスナー登録 |

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

```
Header.vue
    |
    +-- VersionSelect.vue (TypeScript用, v-model直接)
    |       |
    |       +-- toggle() --> expanded 切替
    |       +-- fetchVersions() --> jsdelivr API
    |       |       +-- fetch('https://data.jsdelivr.com/v1/package/npm/typescript')
    |       |       +-- TypeScriptフィルタ (dev/insiders除外)
    |       +-- setVersion(v) --> version.value = v
    |       +-- copyVersion(v) --> clipboard API
    |       +-- Copy.vue (アイコン)
    |
    +-- VersionSelect.vue (Vue用, カスタムイベント)
            |
            +-- toggle() --> expanded 切替
            +-- fetchVersions() --> jsdelivr API
            |       +-- fetch('https://data.jsdelivr.com/v1/package/npm/vue')
            |       +-- Vueフィルタ (プレリリース制御, 30件制限)
            +-- setVersion(v) --> version.value = v
            +-- copyVersion(v) --> clipboard API
            +-- <slot> (This Commit / Commits History)
            +-- Copy.vue (アイコン)
```

### データフロー図

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

Header.vue (props) ────────> VersionSelect.vue ────────────> UI表示
  pkg ('vue'/'typescript')    |
  label (表示ラベル)           +-- toggle() ───────────────> expanded ref 更新
  version (defineModel)       |                               --> ドロップダウン表示切替
                              |
                              +-- fetchVersions() ────────> jsdelivr API GET
                              |       |                       <── versions 配列
                              |       +-- フィルタリング ──> versions ref 更新
                              |
                              +-- setVersion(v) ──────────> version.value 更新
                              |                               --> emit('update:modelValue')
                              |                               --> expanded = false
                              |
window click/blur ───────────> イベントリスナー ──────────> expanded = false
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| VersionSelect.vue | `packages-private/sfc-playground/src/VersionSelect.vue` | ソース | バージョン選択ドロップダウンコンポーネント本体 |
| Header.vue | `packages-private/sfc-playground/src/Header.vue` | ソース | 親コンポーネント。VersionSelect を2回使用 |
| Copy.vue | `packages-private/sfc-playground/src/icons/Copy.vue` | ソース | バージョンコピー用アイコンコンポーネント |
