From ba973061522c4f643fa2ec99d8aa06f6d9e43b38 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 09:44:08 +0000 Subject: [PATCH 1/7] test(e2e): fix broken test (#3356) --- e2e/navbar.spec.ts | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/e2e/navbar.spec.ts b/e2e/navbar.spec.ts index fc0c36de3..baf51426b 100644 --- a/e2e/navbar.spec.ts +++ b/e2e/navbar.spec.ts @@ -39,29 +39,19 @@ test.describe('Navbar - Regression from Svelte 5 UI lib to Flowbite Svelte v1.31 } }); - test.fixme( - 'navbar is visible and functional on mobile (375px)', - { - annotation: { - type: 'issue', - description: - 'Mobile navbar regression in flowbite-svelte v1.31 (from Svelte 5 UI lib migration). See: https://github.com/themesberg/flowbite-svelte/issues/1710. Expected to be fixed in flowbite-svelte v2.x', - }, - }, - async ({ page }) => { - await page.setViewportSize({ width: 375, height: 667 }); - await goToHome(page); + test('navbar is visible and functional on mobile (375px)', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }); + await goToHome(page); - const navbar = page.locator('nav'); - await expect(navbar).toBeVisible(); + const navbar = page.locator('nav'); + await expect(navbar).toBeVisible(); - // Verify hamburger menu exists on mobile - const hamburger = page.locator('nav button:not([aria-label])'); - await expect(hamburger).toBeVisible(); + // Verify hamburger menu exists on mobile + const hamburger = page.locator('nav button[aria-label="Open main menu"]'); + await expect(hamburger).toBeVisible(); - // Verify menu is hidden (hidden class) - const menuContainer = page.locator('div[role="none"] ul'); - await expect(menuContainer).not.toBeVisible(); - }, - ); + // Verify menu is hidden initially + const menuContainer = page.locator('nav div ul'); + await expect(menuContainer).not.toBeVisible(); + }); }); From cd67238f6848012382f043c1ee8c2912cf6d2d9b Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 09:44:47 +0000 Subject: [PATCH 2/7] docs: Remove old comment and update plan (#3356) --- .../README-plan.md | 56 ++++++++++++------- src/lib/components/Header.svelte | 2 - 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md index 0bbe5af3a..05c7394e2 100644 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md +++ b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md @@ -2,9 +2,9 @@ **作成日**: 2026-01-02 -**最終更新**: 2026-01-17 +**最終更新**: 2026-04-04 -**ステータス**: フェーズ1-4 完了 +**ステータス**: フェーズ0-3 完了(visual regression テストは手動確認待ち) --- @@ -259,15 +259,13 @@ ## 📋 今後対応が必要なタスク -### ⚠️ フェーズ2: テスト実行・回帰検出(進行中) +### ✅ フェーズ2: テスト実行・回帰検出(完了) -**状態**: 一部テスト PASS、一部失敗・スキップ +**状態**: 完了(2026-04-04) -**テスト結果(2026-01-17)**: +**テスト結果(2026-04-04 更新)**: -- ✅ 17 passed -- ❌ 1 failed(logout テスト - タイムアウト) -- ⏭️ 1 skipped(mobile navbar テスト - `test.fixme()`) +- ✅ 全テスト PASS(navbar mobile + logout 修正完了) #### チェックリスト @@ -277,15 +275,11 @@ - [x] `dark-mode.spec.ts` → dark mode toggle 動作確認 ✅ PASS (4/4) - [x] `navbar.spec.ts` → navbar レスポンシブ確認 - [x] lg (1024px) での動作 ✅ PASS - - [ ] mobile (375px) での動作 ⏭️ SKIP(`test.fixme()`でマーク) - - 原因: Flowbite-Svelte v1.31.0 の navbar `breakpoint` prop が Svelte 5 Context API 実装の不具合で機能しない - - 対応: v2.0 リリース待機推奨 - - [GitHub Issue #1710](https://github.com/themesberg/flowbite-svelte/issues/1710) - -- [ ] logout テスト失敗の調査・修正 - - [ ] 原因: navbar header dropdown の実装・状態確認 - - [ ] アクション: Header.svelte の dropdown 実装を確認し、ユーザー名リンクの可視性をテスト - - [ ] 期待時期: フェーズ1-4 完了後 + - [x] mobile (375px) での動作 ✅ PASS(2026-04-04 修正完了) + - 原因: v1.31.0 の breakpoint context が $effect 内で set されていたため子コンポーネントに伝播せずハンバーガーが非表示 + テストセレクタミス(`nav button:not([aria-label])`) + - 修正: flowbite-svelte 1.33.0 へアップグレード(fix #1909 + #1924)+ セレクタを `nav button[aria-label="Open main menu"]` に修正 + - 関連 PR: [#1928](https://github.com/themesberg/flowbite-svelte/pull/1928) + - [x] `signin.spec.ts` → login/logout 動作確認 ✅ PASS(2026-04-04 確認済み) - [ ] visual regression テスト(手動確認) - [ ] `space-y-_` / `space-x-_` セレクタ変更による layout shift 確認 @@ -309,18 +303,32 @@ - package.json から両ライブラリを削除されたことを確認 +### ✅ Flowbite-Svelte 1.33.0 アップグレード(完了) + +**状態**: 完了(2026-04-04) + +**実施内容**: + +- ✅ flowbite-svelte 1.31.0 → 1.33.0 へアップグレード +- ✅ navbar breakpoint バグ修正(fix #1909 + #1924)により mobile での動作が正常化 +- ✅ E2E テスト(navbar.spec.ts)のセレクタ修正とテスト復活 +- ✅ 全 E2E テスト PASS 確認 + +**修正された問題**: + +- v1.31.0 では `$effect` 内で `setContext` を呼んでいたため、breakpoint context が子コンポーネントに伝播しない問題があった +- v1.33.0 で reactive object (`$state`) + `$derived` パターンに変更され、context が正しく伝播するようになった + ### 🔮 将来タスク: Flowbite-Svelte v2.0 アップグレード検討 **状態**: 将来(v2.0 リリース後) -**背景**: Flowbite-Svelte v1.31.0 は Svelte 5 + Tailwind CSS v4 への対応が不完全な可能性がある +**背景**: Flowbite-Svelte v1.33.0 で主要な Svelte 5 対応は完了したが、v2.0 でさらなる改善が予定されている **確認事項**: - [ ] Flowbite-Svelte v2.0 リリース(ETA: TBD) -- [ ] v2.0 で navbar `breakpoint` バグが解決されているか検証 - [ ] v2.0 への upgrade 実行(必要に応じて) -- [ ] mobile navbar テスト復活(`test.fixme()` → `test()`) **参考**: [Flowbite-Svelte v2.0 進捗(GitHub Issues #1614)](https://github.com/themesberg/flowbite-svelte/issues/1614) @@ -407,7 +415,13 @@ - boundingBox 取得前に `toBeVisible()` で存在確認 - 複数 SVG がある場合は `.locator('svg').count()` で存在確認 -5. **カスタムフィクスチャの活用** +5. **テストセレクタの検証**(2026-04-04 追加) + - セレクタが実際の DOM と一致するか、ライブラリのソースコードを追って検証する + - `nav button:not([aria-label])` → `nav button[aria-label="Open main menu"]` の修正例 + - 「テストが通らない」原因が「ライブラリのバグ」だけでなく「セレクタミス」の可能性も常に考慮 + - ToolbarButton.svelte → NavHamburger.svelte のコードを追跡し、`name = "Open main menu"` が aria-label に変換されることを確認 + +6. **カスタムフィクスチャの活用** - `test.extend()` で device 固有設定を抽象化(`iPhonePage`, `desktopPage`) - Playwright の `await use(page)` パターンで自動 context クローズを実現し、ボイラープレート削減 - 型定義 `<{ iPhonePage: Page; desktopPage: Page }>` で TypeScript サポート確保 diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index b229d28b6..f06cd41d1 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -33,8 +33,6 @@ let user = $derived(page.data.user); - - {PRODUCT_NAME} Logo From e88cb4e2c84b5de85c3387899cb3c5b8b9b548e4 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 12:07:45 +0000 Subject: [PATCH 3/7] test(e2e): fix broken test (#3356) --- e2e/navbar.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/e2e/navbar.spec.ts b/e2e/navbar.spec.ts index baf51426b..56cd0808a 100644 --- a/e2e/navbar.spec.ts +++ b/e2e/navbar.spec.ts @@ -53,5 +53,15 @@ test.describe('Navbar - Regression from Svelte 5 UI lib to Flowbite Svelte v1.31 // Verify menu is hidden initially const menuContainer = page.locator('nav div ul'); await expect(menuContainer).not.toBeVisible(); + + // Click hamburger to open menu + await hamburger.click(); + await expect(menuContainer).toBeVisible(); + + // Click hamburger again to close menu + await hamburger.click(); + // Svelte {#if}/{:else} renders two
    during slide-out outro; verify single element after transition + await expect(menuContainer).toHaveCount(1); + await expect(menuContainer).not.toBeVisible(); }); }); From 412bacaca3c841cad4704015305a7af424665998 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 12:19:53 +0000 Subject: [PATCH 4/7] refactor: summarize plan and remove old docs (#3356) --- .../README-plan.md | 499 +---------- .../component-mapping.md | 832 ------------------ .../investigation.md | 310 ------- .../testing-strategy.md | 508 ----------- 4 files changed, 33 insertions(+), 2116 deletions(-) delete mode 100644 docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/component-mapping.md delete mode 100644 docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/investigation.md delete mode 100644 docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/testing-strategy.md diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md index 05c7394e2..a404240f5 100644 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md +++ b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md @@ -1,499 +1,66 @@ -# UIライブラリ移行計画:svelte-5-ui-lib → Flowbite Svelte +# UIライブラリ移行:svelte-5-ui-lib → Flowbite Svelte -**作成日**: 2026-01-02 +**作成日**: 2026-01-02 | **最終更新**: 2026-04-04 | **ステータス**: 移行完了 -**最終更新**: 2026-04-04 - -**ステータス**: フェーズ0-3 完了(visual regression テストは手動確認待ち) - ---- - -## 概要と読む順序 - -本ドキュメントは svelte-5-ui-lib から Flowbite Svelte v1.31.0 への大規模移行プロジェクトの計画と実行結果を記載します。 - -### 推奨される読む順序 - -1. **このファイル(README-plan.md)** ← ここから開始 - - 全体像 + プロジェクトスコープ - - フェーズ別チェックリスト - - 事前に予想できなかったトラブルと教訓 - -2. [testing-strategy.md](./testing-strategy.md) → 詳細なテスト実装方法 - - 前提条件確認 - - E2E テスト設計 - - テストコード実装例 - -3. [investigation.md](./investigation.md) → Breaking Changes の詳細 - - Tailwind CSS v3→v4 の破壊的変更 - - Flowbite/Flowbite Svelte のバージョン対応 - - 本プロジェクトへの影響度分析 - -4. [component-migration.md](./component-migration.md) → 実装の参考資料 - - コンポーネント対応マトリクス - - 難易度別対応例 - - 参考コード - ---- - -## プロジェクトスコープ - -### 必要な変更 - -- UI ライブラリの置換(svelte-5-ui-lib → Flowbite Svelte) -- TailwindCSS v3 → v4 への移行(Flowbite Svelte の利用で必須) - -### スコープ外 - -- デザイン刷新(既存コンポーネントと等価な機能を保証) -- 新機能追加 - -### 優先保護対象 - -- navbar(ユーザーナビゲーションの最優先) -- dropdown(複数の navbar menu item で使用) -- modal(フォーム送信時の確認ダイアログで使用) - ---- - -## フェーズ別チェックリスト - -### ✅ フェーズ-1: テスト環境確認とテスト実装 - -**期間**: 2026-01-02~2026-01-04 - -**目的**: 前回 v3→v4 失敗ポイント(colors, responsive, dark mode)を検出するテストを先に実装 - -#### チェックリスト - -- [x] Playwright e2e テストフレームワーク選択・基本構成 -- [x] `tests/custom-colors.spec.ts` 実装(ビルド出力 CSS 検証) -- [x] `tests/dark-mode.spec.ts` 実装(ダークモード toggle) -- [x] `tests/navbar.spec.ts` 実装(navbar レスポンシブ) -- [x] セレクタ確定(HTML 構造確認済み) -- [x] すべてのテストが v3 環境で PASS - -**実装結果**: ✅ 18/19 テスト PASS(残り 1 つは環境依存) - -**重要な教訓**: - -- **Playwright API の変化**: `page.setViewport()` は削除済み → `browser.newContext()` で device 固有設定を実装 -- **CSS ファイル構造**: SvelteKit は複数 CSS ファイルに分割 → regex での探索に注意 -- **セレクタ設計**: `aria-label`, `role="none"` など属性ベースの選定で CSS 実装詳細に依存しない - ---- - -### ✅ フェーズ0: TailwindCSS v4 移行 - -**期間**: 2026-01-03~2026-01-04 - -**目的**: TailwindCSS v4 環境を整備し、カスタムカラー・ブレークポイントが機能する状態にする - -#### チェックリスト - -- [x] `src/app.css` を v4 CSS-first 形式に更新 - - [x] `@import 'tailwindcss'` に統一 - - [x] `@plugin` / `@custom-variant dark` を CSS に移動 - - [x] `@source` ディレクティブで content スキャン範囲を明示 - -- [x] `tailwind.config.ts` から `content` 配列を削除 - - [x] CSS の `@source` で制御(v4 推奨) - -- [x] `postcss.config.mjs` を `@tailwindcss/postcss` に更新 - -- [x] ビルド確認 - - [x] `pnpm build` 成功(エラー 0) - - [x] CSS ファイル生成確認 - - [x] カスタムカラーが CSS に含まれることを確認 - -- [x] Playwright smoke test PASS - - [x] `custom-colors.spec.ts` → primary/atcoder colors 生成確認 - - [x] `dark-mode.spec.ts` → dark mode toggle 動作確認 - -**実装結果**: ✅ 全チェック完了 - -**重要な教訓**: - -- **@source の必須性**: Flowbite Svelte コンポーネント scan には `@source '../node_modules/flowbite-svelte/dist'` が必須 -- **v3→v4 の段階的アプローチ**: 必須変更だけを先行実装し、任意推奨事項(CSS Variables 化等)は後回し -- **テスト駆動の価値**: 「colors が CSS に含まれるか」を単純に grep で検証するのではなく、Playwright での E2E テストが有効(実際のレンダリング結果を検証) - ---- - -### ✅ フェーズ1-1: カテゴリ1 コンポーネント置き換え(import のみ) - -**期間**: 2026-01-04 - -**難易度**: ⭐ 低 - -**対象**: Heading, P, Label, Input, Button, Card, Alert 等 20+ コンポーネント - -#### チェックリスト - -- [x] コンポーネント一覧確認([component-migration.md](./component-migration.md)) -- [x] import パスを変更: `svelte-5-ui-lib` → `flowbite-svelte` -- [x] ビルド成功確認 - -**実装結果**: ✅ 各ファイルの import パス更新完了 - -**重要な教訓**: - -- **一貫した import 形式**: svelte-5-ui-lib の混在パターン(direct file path, named import 混在)を Flowbite Svelte の `import { Component } from 'flowbite-svelte'` に統一 → IDE サポート向上 - ---- - -### ✅ フェーズ1-2: カテゴリ2 コンポーネント置き換え(属性調整) - -**期間**: 2026-01-04~2026-01-16 - -**難易度**: ⭐⭐ 中 - -**対象**: Navbar, NavBrand, NavUl, NavLi, NavHamburger, Tabs, Tooltip, Checkbox, Radio, Toggle - -#### チェックリスト - -- [x] **Navbar コンポーネント群** - - [x] NavBrand, NavUl, NavLi import 変更 - - [x] `NavHamburger` コンポーネント新規追加(モバイル対応) - - [x] `navStatus`, `toggleNav`, `closeNav` props 削除(NavHamburger で内部管理) - - [x] `aClass` → `activeClass` にリネーム - - [x] Header.svelte ビルド確認 ✅ - -- [x] **その他 UI コンポーネント** - - [x] Tabs + TabItem(slot 名確認) - - [x] Tooltip(`triggeredBy` prop で target selector 指定) - - [x] Checkbox/Radio/Toggle(`bind:checked` / `bind:group` を Svelte v5 runes で実装) - -**実装結果**: ✅ Navbar 実装完了、その他準備中 - -**Navbar の fluid prop 動作確認**:(2026-01-17 検証済み) - -- ✅ `fluid={true}` は正常に動作 -- ✅ `fluid={false}` も正常に動作 -- ✅ app.css でのグローバルスタイル設定により、width 制限の問題は解決済み - -**重要な教訓**: - -- **State 管理の簡潔化**: svelte-5-ui-lib の独自 state(`navStatus`, `toggleNav`, `closeNav`)を Flowbite Svelte 内部管理に委譲 → コード削減 -- **Props 名の確認重要**: `aClass`(svelte-5-ui-lib 固有)→ `activeClass`(Flowbite Svelte 標準)への変更は、GitHub の NavLi 定義を確認して初めて気づく -- **$app/stores の必須性**: Flowbite docs は client-only デモで `$app/state` を使うが、SSR 環境では `$app/stores` が必須 -- **Tailwind CSS の動的クラス生成の制限**: テンプレートリテラル内での変数補間(例:`max-w-[${width}px]`)は Tailwind のビルド時スキャンで認識されず、CSS が生成されない。動的値にはArbitrary propertyで CSS変数を使う(例:`[--tooltip-width:${width}px]` + `max-w-[var(--tooltip-width)]`)か、CSSカスタムプロパティ+インラインスタイルを併用すること - ---- - -### ✅ フェーズ1-3: カテゴリ3 コンポーネント置き換え(外部ライブラリから復帰) - -**期間**: 2026-01-04 - -**難易度**: ⭐⭐ 中 - -**対象**: Carousel(embla-carousel-svelte → Flowbite Svelte Carousel) - -#### チェックリスト - -- [x] embla-carousel-svelte を削除 -- [x] Flowbite Svelte Carousel + Controls + CarouselIndicators に置き換え - -**実装結果**: ✅ 完了(コンポーネント動作確認済み) - -**重要な教訓**: - -- **ライブラリ統一の価値**: 外部依存ライブラリを減らすことで、保守性が向上 - ---- - -### ✅ フェーズ1-4: カテゴリ4 コンポーネント置き換え(抜本的な書き直し) - -**期間**: 2026-01-04〜2026-01-14 - -**難易度**: ⭐⭐⭐ 高 - -**対象**: Dropdown, Modal, Spinner, ButtonGroup, Footer - -#### チェックリスト - -- [x] **Dropdown(最優先)** - - [x] DropdownUl / DropdownLi の props 調整 - - [x] `triggeredBy` prop で target selector 指定(ID ベース) - - [x] Floating UI によるポジショニング確認(複雑な CSS positioning 削減) - - [x] 複数 dropdown の navbar 実装例(#nav-dashboard など) - -- [x] **Modal** - - [x] `bind:open` で UI state 管理 - - [x] SvelteKit Form Actions との共存(`form` prop は不要) - - [x] `use:enhance` で server action 処理 - -- [x] **Spinner** - - [x] import パス変更のみ - -- [x] **ButtonGroup** - - [x] 既に正しい状態(修正不要) - -- [x] **Footer** - - [x] `footerType` prop 削除 - - [x] `FooterCopyright` コンポーネント新規追加 - -**実装結果**: ✅ 全コンポーネント置き換え完了 - -**重要な教訓**: - -- **ライブラリの API 仕様を context に合わせる** - - Flowbite Modal の `form` prop + `onaction` callback は「クライアント側の form validation UI」に特化 - - SvelteKit server action 環境では不要 → `bind:open` + `use:enhance` で十分 - -- **相対パスの重要性(SvelteKit Form Actions)** - - URL ネストが深い場合、絶対パス `/logout` では routing が失敗の可能性 - - 相対パス `../../logout?/logout` で確実に target route に到達 - -- **Flowbite のシンプルな設計** - - Props が少なく、CSS クラスでカスタマイズする方針 - - 例:Footer では `class="shadow-none w-screen m-6"` で見た目をコントロール - -- **最小限の変更が最良** - - uiHelpers 削除 → `$state(open)` のみ - - Modal component の props 調整 → `bind:open` + `outsideclose` のみ - - form structure は完全保持 → `handleSubmit()` + `?/update` action - - 余計な「modernization」は避けるべき - ---- - -## 📋 今後対応が必要なタスク - -### ✅ フェーズ2: テスト実行・回帰検出(完了) - -**状態**: 完了(2026-04-04) - -**テスト結果(2026-04-04 更新)**: - -- ✅ 全テスト PASS(navbar mobile + logout 修正完了) - -#### チェックリスト - -- [x] `pnpm test:integration` で全テスト実行 -- [x] Playwright smoke test 実行 - - [x] `custom-colors.spec.ts` → primary/atcoder colors 生成確認 ✅ PASS - - [x] `dark-mode.spec.ts` → dark mode toggle 動作確認 ✅ PASS (4/4) - - [x] `navbar.spec.ts` → navbar レスポンシブ確認 - - [x] lg (1024px) での動作 ✅ PASS - - [x] mobile (375px) での動作 ✅ PASS(2026-04-04 修正完了) - - 原因: v1.31.0 の breakpoint context が $effect 内で set されていたため子コンポーネントに伝播せずハンバーガーが非表示 + テストセレクタミス(`nav button:not([aria-label])`) - - 修正: flowbite-svelte 1.33.0 へアップグレード(fix #1909 + #1924)+ セレクタを `nav button[aria-label="Open main menu"]` に修正 - - 関連 PR: [#1928](https://github.com/themesberg/flowbite-svelte/pull/1928) - - [x] `signin.spec.ts` → login/logout 動作確認 ✅ PASS(2026-04-04 確認済み) - -- [ ] visual regression テスト(手動確認) - - [ ] `space-y-_` / `space-x-_` セレクタ変更による layout shift 確認 - - [ ] divide-y / divide-x による table border 変更確認 - - [ ] デフォルト値変更(border color, ring size)の visual 確認 +svelte-5-ui-lib から Flowbite Svelte v1.33.0 への移行と、TailwindCSS v3→v4 移行を完了。 +フェーズ-1〜3(テスト環境構築、Tailwind v4 移行、コンポーネント置換、旧ライブラリ削除)はすべて完了済み。 --- -### ✅ フェーズ3: 依存ライブラリ削除(完了) - -**状態**: 完了(2026-01-17 確認済み) - -**実施内容**: - -- ✅ `pnpm remove svelte-5-ui-lib` 実行済み -- ✅ `pnpm remove embla-carousel-svelte` 実行済み(Carousel を Flowbite Svelte に置き換え完了) -- ✅ `pnpm-lock.yaml` 更新済み -- ✅ ビルド + テスト実行で問題なし(17 passed) - -**確認**: - -- package.json から両ライブラリを削除されたことを確認 - -### ✅ Flowbite-Svelte 1.33.0 アップグレード(完了) - -**状態**: 完了(2026-04-04) +## 未着手タスク -**実施内容**: +### Visual regression テスト(手動確認) -- ✅ flowbite-svelte 1.31.0 → 1.33.0 へアップグレード -- ✅ navbar breakpoint バグ修正(fix #1909 + #1924)により mobile での動作が正常化 -- ✅ E2E テスト(navbar.spec.ts)のセレクタ修正とテスト復活 -- ✅ 全 E2E テスト PASS 確認 +- [ ] `space-y-*` / `space-x-*` セレクタ変更による layout shift 確認(8箇所) +- [ ] `divide-y` / `divide-x` による table border 変更確認(13箇所) +- [ ] デフォルト値変更(border color → `currentColor`、ring size 1px)の visual 確認 -**修正された問題**: +### Flowbite-Svelte v2.0 アップグレード検討 -- v1.31.0 では `$effect` 内で `setContext` を呼んでいたため、breakpoint context が子コンポーネントに伝播しない問題があった -- v1.33.0 で reactive object (`$state`) + `$derived` パターンに変更され、context が正しく伝播するようになった +- [ ] v2.0 リリース後に検討([GitHub Issues #1614](https://github.com/themesberg/flowbite-svelte/issues/1614)) -### 🔮 将来タスク: Flowbite-Svelte v2.0 アップグレード検討 +### FormWrapper refactor -**状態**: 将来(v2.0 リリース後) - -**背景**: Flowbite-Svelte v1.33.0 で主要な Svelte 5 対応は完了したが、v2.0 でさらなる改善が予定されている - -**確認事項**: - -- [ ] Flowbite-Svelte v2.0 リリース(ETA: TBD) -- [ ] v2.0 への upgrade 実行(必要に応じて) - -**参考**: [Flowbite-Svelte v2.0 進捗(GitHub Issues #1614)](https://github.com/themesberg/flowbite-svelte/issues/1614) +- [ ] FormWrapper の purpose(外側のスタイリング用 or 内側のフォーム処理用)を明確化 +- [ ] nested form 設計をドキュメント化 --- -### 📝 設計課題: FormWrapper refactor(後回し) - -**優先度**: 低 - -**課題**: - -- [ ] FormWrapper の purpose(外側のスタイリング用か、内側のフォーム処理用か)を明確化 -- [ ] nested form 設計を ドキュメント化 - -**期待時期**: フェーズ3 完了後 - ---- +## 教訓 -## 学習点・教訓(全フェーズ共通) +### 1. Tailwind v4 + Flowbite Svelte の `@source` 設定 -### Tailwind CSS v4 移行時 - -1. **@source ディレクティブの必須性** - - v4 では `@source` で指定したディレクトリのファイルのみスキャン - - Flowbite Svelte コンポーネント利用時は `@source '../node_modules/flowbite-svelte/dist'` が必須 - - テストで CSS ファイルの存在だけを確認するのではなく、実際のコンポーネント使用状況を見る方が信頼性が高い - -2. **段階的アプローチの効果** - - 必須変更(CSS ディレクティブ、@source、plugin 指定)だけを先行実装 - - 任意推奨事項(CSS Variables 化、autoprefixer 削除など)はフェーズ1 以降で検討 - - この段階的アプローチにより、安定した状態での次フェーズ開始が可能 - -3. **テスト駆動開発の価値** - - v4 では「colors が CSS に含まれるか」を単純に grep で検証することは困難 - - 実際の HTML 内で使用されているクラス名のみ生成されるため、Playwright での E2E テストが有効(実際のレンダリング結果を確認) - -### コンポーネント置き換え時 - -1. **「新しい = 正しい」という仮定は危険** - - Flowbite Modal の `form` prop + `onaction` callback は「クライアント側の form validation UI」に特化 - - SvelteKit の server action context では使うべきではない - - ドキュメントをしっかり読み込まないと、誤った方向に進む - -2. **ライブラリの API 仕様を context に合わせるべき** - - 新しいライブラリのすべての機能を使う必要はない - - SvelteKit Form Actions との共存を優先 → `form` prop は不要 - - `bind:open` (UI 管理) + `use:enhance` (server action) の分離が正解 - -3. **最小限の変更が最良** - - uiHelpers 削除 → `$state(open)` のみ - - Modal component の props 調整 → `bind:open` + `outsideclose` のみ - - form structure は完全保持 → `handleSubmit()` + `?/update` action - - 余計な最適化や「modernization」は避けるべき - -4. **Props 名の確認重要** - - `aClass`(svelte-5-ui-lib 固有)→ `activeClass`(Flowbite Svelte 標準)への変更は、GitHub の NavLi 定義確認が必須 - -5. **$app/stores の必須性(SSR 環境)** - - Flowbite docs は client-only デモで `$app/state` を使うが、SSR 環境では `$app/stores` が必須 - -6. **相対パスの重要性(SvelteKit Form Actions)** - - URL ネストが深い場合、絶対パス `/logout` では routing が失敗の可能性 - - 相対パス `../../logout?/logout` で確実に target route に到達 - -### テスト実装時 - -1. **Playwright API の変化** - - `page.setViewport()` は削除済み → `browser.newContext()` で device 固有設定を実装 - - BDD スタイルテストでは `let page` の手動管理ではなく、`async ({ page })` で injection - - mobile/desktop テストは `devices` config を使用した `browser.newContext()` が推奨 - -2. **CSS ファイル構造の理解** - - SvelteKit ビルドは複数の CSS ファイルに分割される - - `.svelte-kit/output/client/_app/immutable/assets/0.*.css` がメイン CSS - - CSS は圧縮されるため regex での検索はセレクタサイズの違いに注意 - -3. **セレクタ設計の工夫** - - `aria-label` がない button を `nav button:not([aria-label])` で識別 - - `div[role="none"] ul` で menu container を特定(CSS class 依存を避ける) - - Playwright の `isVisible()` は実際のレンダリング結果を確認(CSS 実装詳細に依存しない) - -4. **テストの安定性向上** - - viewport 変更後のページ再レンダリング問題 → `browser.newContext()` で新規コンテキスト作成 - - boundingBox 取得前に `toBeVisible()` で存在確認 - - 複数 SVG がある場合は `.locator('svg').count()` で存在確認 - -5. **テストセレクタの検証**(2026-04-04 追加) - - セレクタが実際の DOM と一致するか、ライブラリのソースコードを追って検証する - - `nav button:not([aria-label])` → `nav button[aria-label="Open main menu"]` の修正例 - - 「テストが通らない」原因が「ライブラリのバグ」だけでなく「セレクタミス」の可能性も常に考慮 - - ToolbarButton.svelte → NavHamburger.svelte のコードを追跡し、`name = "Open main menu"` が aria-label に変換されることを確認 - -6. **カスタムフィクスチャの活用** - - `test.extend()` で device 固有設定を抽象化(`iPhonePage`, `desktopPage`) - - Playwright の `await use(page)` パターンで自動 context クローズを実現し、ボイラープレート削減 - - 型定義 `<{ iPhonePage: Page; desktopPage: Page }>` で TypeScript サポート確保 - ---- - -## 参考資料 - -- [testing-strategy.md](./testing-strategy.md) - テスト詳細・実装例 -- [investigation.md](./investigation.md) - Breaking Changes 詳細 -- [component-migration.md](./component-migration.md) - コンポーネント対応表 - ---- - -## トラブルシューティング - -### Q: TailwindCSS v4 への移行後、カスタムカラーが反映されない - -**A**: `src/app.css` の `@source` ディレクティブを確認する。 +Flowbite Svelte コンポーネントのクラスを Tailwind v4 に認識させるには `@source` が**必須**。設定漏れで「色が出ない」問題が発生する。 ```css -@source "../src/**/*.{html,js,svelte,ts}"; @source "../node_modules/flowbite-svelte/dist/**/*.{html,js,svelte,ts}"; ``` -Flowbite Svelte コンポーネント scan には両方の `@source` が必須です。 - ---- - -### Q: Flowbite Svelte での Tabs コンポーネントがモバイルで改行される - -**A**: Tabs の layout 設計が v3 と v4 で異なるため、レスポンシブ対応が必須。 +### 2. SvelteKit Form Actions と Flowbite Modal の共存 -- v3 では単純な horizontal layout だが、v4 では `flex-wrap` + `gap` + item alignment の組み合わせが必要 -- ハマった点:`items-center` のままだと改行後に中央寄せされて見づらくなる → `items-start` or `items-baseline` に変更 -- 教訓:Flowbite 内部の layout 実装を仮定せず、ブラウザ DevTools で実際の flex layout を確認する +Flowbite Modal の `form` prop は client-side validation UI に特化しており、SvelteKit server action には不適。正しいパターンは `bind:open`(UI state)+ `use:enhance`(server action)の分離。 ---- - -### Q: ButtonGroup の角が丸くなってしまう - -**A**: Tailwind v4 の `rounded` スケール変更により、Flowbite の ButtonGroup 内部の `rounded-sm` 適用が意図せず変更された。 +### 3. 動的カラーの CSS 変数パターン -- ハマった点:`rounded-none md:rounded-md` という条件付きスタイリングが必要(base で角を消し、md breakpoint で復活) -- 教訓:Flowbite components は内部的に rounded ユーティリティを使うため、カスタマイズ時は `rounded-none` で明示的に unset する - ---- +Tailwind v4 では動的クラス名(`"bg-" + variable`)が生成されない。インラインスタイル + CSS 変数で解決する: -### Q: Dropdown のスタイリングが複雑に絡み合っている +```typescript +// getTaskGradeColor() returns "var(--color-atcoder-Q4)" etc. +style={`background-color: ${getTaskGradeColor(grade)};`} +``` -**A**: Flowbite Dropdown は以下の 4 つの要素の組み合わせで成り立っており、各層でのスタイリング衝突を理解する必要がある。 +### 4. テストセレクタはライブラリソースを追って検証する -- **bullet point 欠落**:`DropdownDivider` がリスト項目として認識されず、bullet がない → CSS `list-style: none` で統一 -- **divide-y のデフォルト色**:v4 では `divide-gray-200` から `divide-currentColor` に変更 → Dropdown の仕切り線が親要素の text color を継承してしまう → 明示的に色を指定(`divide-gray-200`) -- **border の可視性**:Dropdown の border color も同様にデフォルト値変更 → `border-gray-200` を明示指定 -- **ポジショニング(Floating UI)**:ネストされた dropdown や navbar での z-index 競合 → Floating UI の middleware 設定を確認 +テストが通らない原因が「ライブラリのバグ」だけでなく「セレクタミス」の可能性を常に考慮。ToolbarButton.svelte → NavHamburger.svelte のコードを追跡し、`name = "Open main menu"` が `aria-label` に変換されることを確認した例。 -ハマった点:「なぜ Dropdown がこんなに複雑か」を理解するまで試行錯誤が続いた +### 5. Svelte `{#if}/{:else}` とトランジションの DOM 共存 -教訓:UI component ライブラリを導入する際は「内部実装(何を親から継承するか)」を早期に document に記載すべき +NavUl の `{#if !hidden}` ブロックに `transition:slide` がある場合、閉じる際に outgoing 要素と incoming 要素が一時的に DOM に共存する。Playwright テストでは `toHaveCount(1)` でトランジション完了を確認してから `not.toBeVisible()` を assert する。 --- -### Q: Tailwind v4 への移行後、動的カラー(CSS 変数)が機能しない - -**A**: Tailwind v4 の CSS-first 設計では、ユーティリティクラスは「必要なもの」のみ生成される(v3 の auto-generation は廃止)。 - -- ハマった点:`class="bg-" + getTaskGradeName(grade)` という動的クラス名の組み立てが動作しない → クラスそのものが CSS に存在しない -- 採用した解決策:インラインスタイル + CSS 変数の組み合わせ - - `style={`background-color: ${getTaskGradeColor(grade)};`}` で CSS 変数名を展開 - - `getTaskGradeColor()` は `"var(--color-atcoder-Q4)"` のような形式を返す - - ブラウザが `var()` 関数を解釈して実際の色を適用 -- 教訓:v4 では「ユーティリティクラス = すべて事前定義」という assumption は危険。動的値にはインラインスタイル + CSS 変数を活用する +## 参考資料 ---- +- [Tailwind CSS v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) +- [Flowbite Svelte GitHub](https://github.com/themesberg/flowbite-svelte) +- [flowbite-svelte breakpoint fix PR #1928](https://github.com/themesberg/flowbite-svelte/pull/1928) diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/component-mapping.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/component-mapping.md deleted file mode 100644 index 4291f5ca8..000000000 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/component-mapping.md +++ /dev/null @@ -1,832 +0,0 @@ -# コンポーネント対応表・実装参考資料 - -**作成日**: 2026-01-02 - -**最終更新:** 2026-01-17 - -**用途**: svelte-5-ui-lib → Flowbite Svelte 移行時の実装参考資料 - ---- - -## 概要 - -本ドキュメントは、svelte-5-ui-lib から Flowbite Svelte への移行時に、各コンポーネントの対応方法を示す実装参考資料です。 - -コンポーネントを以下の4カテゴリに分類し、対応手数と注意点を記載しています: - -1. **カテゴリ1**: ライブラリ名置き換えのみ(難易度低) -2. **カテゴリ2**: 置き換え + 属性調整(難易度中) -3. **カテゴリ3**: 外部ライブラリからの復帰(難易度中) -4. **カテゴリ4**: 抜本的な書き直し(難易度高) - -実装時は、[README-plan.md](./README-plan.md) のチェックリストと照らし合わせて実施してください。 - ---- - -## 関連ドキュメント - -- [README-plan.md](./README-plan.md) - 実行計画・フェーズチェックリスト・教訓 -- [testing-strategy.md](./testing-strategy.md) - テスト戦略・実装例 -- [investigation.md](./investigation.md) - Breaking Changes 詳細分析 - ---- - -## カテゴリ1:ライブラリ名置き換えのみ(⭐ 難易度低) - -| コンポーネント | svelte-5-ui-lib | Flowbite Svelte | 対応内容 | 注意点 | -| ---------------- | --------------- | --------------- | --------------------------- | ----------------------------- | -| `Heading` | ✅ | ✅ | import のみ変更 | `tag` prop 互換 | -| `P` | ✅ | ✅ | import のみ変更 | - | -| `Label` | ✅ | ✅ | import のみ変更 | - | -| `Input` | ✅ | ✅ | import のみ変更 | - | -| `Hr` | ✅ | ✅ | import のみ変更 | - | -| `Img` | ✅ | ✅ | import のみ変更 | - | -| `List` | ✅ | ✅ | import のみ変更 | slot 互換 | -| `Li` | ✅ | ✅ | import のみ変更 | - | -| `Helper` | ✅ | ✅ | import のみ変更 | - | -| `Badge` | ✅ | ✅ | import のみ変更 | `color` prop 互換 | -| `Avatar` | ✅ | ✅ | import のみ変更 | `src`, `size` prop 互換 | -| `Breadcrumb` | ✅ | ✅ | import のみ変更 | component 階層互換 | -| `BreadcrumbItem` | ✅ | ✅ | import のみ変更 | slot 互換 | -| `Table` | ✅ | ✅ | import のみ変更 | - | -| `TableHeadCell` | ✅ | ✅ | import のみ変更 | - | -| `TableBodyCell` | ✅ | ✅ | import のみ変更 | - | -| `TableBodyRow` | ✅ | ✅ | import のみ変更 | - | -| `Button` | ✅ | ✅ | import のみ変更 + props確認 | size, color, variant 系を確認 | -| `Card` | ✅ | ✅ | import のみ変更 | slot 互換 | -| `Alert` | ✅ | ✅ | import のみ変更 | `color` prop 互換 | - -**対応方法:** - -```typescript -// Before -import { Heading, Button, Label } from 'svelte-5-ui-lib'; - -// After -import { Heading, Button, Label } from 'flowbite-svelte'; -``` - -**テスト:** Vitest snapshot または Playwright (コンポーネント render 確認) - -**参考:** [Flowbite Svelte Components](https://flowbite-svelte.com/docs/components/accordion) - ---- - -## カテゴリ2:置き換え + 属性調整(⭐⭐ 難易度中) - -| コンポーネント | 変更内容 | 詳細 | 参考 | -| ------------------ | -------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `Tabs` + `TabItem` | import + slot 名確認 | API は同一だが、slot 名が異なる可能性 | [Flowbite Tabs](https://flowbite-svelte.com/docs/components/tabs) | -| `Tooltip` | import + `triggeredBy` prop | v3: `content` prop / v5: `triggeredBy` で target selector 指定 | [Flowbite Tooltip](https://flowbite-svelte.com/docs/components/tooltip) | -| `Checkbox` | import + `bind:checked` | Svelte v5 runes: `bind:checked` で直接管理 | [Flowbite Checkbox](https://flowbite-svelte.com/docs/components/checkbox) | -| `Radio` | import + `bind:group` | Svelte v5 runes: `bind:group` で group 管理 | [Flowbite Radio](https://flowbite-svelte.com/docs/components/radio) | -| `Toggle` | import + `bind:checked` | Svelte v5 runes: `bind:checked` で state 管理 | [Flowbite Toggle](https://flowbite-svelte.com/docs/components/toggle) | -| `Navbar` | import + props 削減 | 内部 state 管理に変更(`toggleNav`, `closeNav` props 廃止) | [Flowbite Navbar](https://flowbite-svelte.com/docs/components/navbar) | -| `NavBrand` | import のみ変更 | slot 互換、`href` prop 互換 | [Flowbite NavBrand](https://flowbite-svelte.com/docs/components/navbar) | -| `NavUl` | import + prop 確認 | `activeUrl` で active state 管理、`classes` prop で styling | [Flowbite NavUl](https://flowbite-svelte.com/docs/components/navbar) | -| `NavLi` | `aClass` → `activeClass` | `activeClass`, `nonActiveClass` で非アクティブ時のスタイル指定 | [Flowbite NavLi](https://flowbite-svelte.com/docs/components/navbar) | -| `NavHamburger` | 新規追加(svelte-5-ui-lib に同等なし) | モバイル時の menu toggle ボタン。内部 state 自動管理 | [Flowbite NavHamburger](https://flowbite-svelte.com/docs/components/navbar) | - -**対応例:Navbar(Header.svelte の実装例)** - -```svelte - - - - - - About - - - - - - - - - - About - - - - -``` - -**主な変更点:** - -- `navStatus`, `toggleNav`, `closeNav` → 廃止(`NavHamburger` が内部管理) -- `aClass` → `activeClass` に rename -- `NavHamburger` 新規追加(モバイル対応のハンバーガーメニュー) -- Navbar の内部 state 管理が簡潔に - -**テスト:** Playwright navbar responsive + dropdown trigger test - ---- - -**対応例:Tabs** - -```svelte - - - - - - Content 1 - - - - - - - - - Content 1 - - -``` - -**対応例:Checkbox** - -```svelte - - - - - - - -``` - -**テスト:** Vitest props + event テスト - ---- - -## カテゴリ3:外部ライブラリからの復帰(⭐⭐ 難易度中) - -| コンポーネント | 変更内容 | 詳細 | -| -------------- | ------------------------------------------------ | --------------------------------------------------- | -| **Carousel** | embla-carousel-svelte → Flowbite Svelte Carousel | Plugin-based → Prop-based API、自動スケーリング無し | - -### Carousel プロパティ対応表 - -| 項目 | embla-carousel-svelte | Flowbite Carousel | 説明 | -| ---------------------- | ------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------- | -| **基本API** | `use:emblaCarouselSvelte={{ options, plugins }}` | `` | embla: action directive / Flowbite: component-based | -| **自動スライド** | `Autoplay()` plugin | `duration` prop | embla: plugin系 / Flowbite: prop単位で制御 | -| **ループ動作** | `options = { loop: true }` | デフォルト有効 | Flowbite は常にループ(設定不可) | -| **画像配列形式** | `[{ src: '...', alt: '...' }]` | `[{ src: '...', alt: '...', title: '...' }]` | **互換性あり**(同一形式) | -| **画像スケーリング** | `imgClass="object-contain h-full w-fit"` | `slideFit="contain"` | embla: CSS class管理 / Flowbite: prop制御 | -| **レスポンシブ高さ** | 外側div に手動で class 設定 | `class="min-h-[300px] xs:min-h-[400px]..."` | どちらも外側divで制御必須 | -| **Overflow 処理** | 外側 div に `overflow-hidden` | 内部処理あり + 明示的推奨 | Flowbite内部処理だが、CSS overrides対応のため明示的指定が安全 | -| **Alt 属性** | 手動設定(`imgClass` 別管理) | `images` 配列内に `alt` 含める | **自動適用**(Slide.svelte で自動反映) | -| **インジケータ表示** | 手動実装が必要 | `` | Flowbite が提供(コンポーネント化) | -| **ナビゲーション矢印** | 手動実装が必要 | `` (任意) | Flowbite が提供(optional) | - -### 移行実装例 - -**Before (embla-carousel-svelte v8.6.0)** - -```svelte - - -
    -
    - {#each problemImages as image} -
    - {image.alt} -
    - {/each} -
    -
    -``` - -**After (Flowbite Carousel v1.31.0)** ✅ - -```svelte - - -
    - - - -
    -``` - -### 移行時の注意点 - -1. **Plugin-based → Prop-based への設計変更** - - `Autoplay()` plugin → `duration` prop(ミリ秒単位) - - 簡潔だが、細かい制御が必要な場合は Flowbite API では対応不可 - -2. **自動スケーリング不可** - - embla: `imgClass` で自動管理 - - Flowbite: `slideFit` prop で明示的に指定が必要 - -3. **レスポンシブクラスは手動指定** - - 外側 div の `class` prop に`min-h-[300px] xs:min-h-[400px]` など記載必須 - - embla同様、Flowbite も内部では自動生成されない - -4. **Alt 属性は自動適用** ✅ - - `images` 配列の各オブジェクトに `alt` を含める - - Slide.svelte 内で `{...image}` で展開されるため自動で反映 - -5. **Overflow 処理は明示的に指定** ✅ - - Flowbite 内部で処理される可能性だが、CSS overrides に対応するため外側 div に `overflow-hidden` を追加推奨 - -### 教訓 - -- **API 設計の違いを理解することの重要性**: Plugin-based と Prop-based では柔軟性が異なる -- **ドキュメント不足時はソースコード確認が必須**: alt属性の自動適用はドキュメント未記載だったが GitHub で確認可能 -- **Canonical CSS classes の使用**: Tailwind v4 では `min-h-[300px]` 形式が推奨される(VSCode拡張で警告あり) - ---- - -## カテゴリ4:抜本的な書き直し必要(⭐⭐⭐ 難易度高) - -### 4-1. Dropdown(最優先対応) - -**差分の大きさ:** 🔴 高 - -**svelte-5-ui-lib:** - -```svelte - - - - - - Item 1 - - -``` - -**Flowbite Svelte:** - -```svelte - - - - - - Item 1 - (isOpen = false)}>Item 2 - -``` - -**主な変更点:** - -- `DropdownUl` / `DropdownLi` → `DropdownUl`/`DropdownLi` (Flowbite でも互換) -- `uiHelpers()` → `triggeredBy` prop で target selector 指定 -- Floating UI が自動ポジショニングを処理(複雑な CSS クラス不要) -- `bind:isOpen` で内部状態管理 - -#### Header.svelte における `triggeredBy` パターン - -**Before (svelte-5-ui-lib + 複雑な CSS positioning):** - -```svelte - - - (dropdownForDashboard.open = !dropdownForDashboard.open)} -> - 管理画面 - - - - - - Submissions - Users - - -``` - -**After (Flowbite Svelte + `triggeredBy` + Floating UI):** - -```svelte - - - - 管理画面 - - - - - - Submissions - Users - - - - - - ユーザーページ - - - - - - Profile - - -``` - -**主要改善点:** - -1. **ポジショニングの簡略化**: `left-32 mt-0 lg:-left-10 lg:mt-10` → `w-48 z-20` に削減 -2. **状態管理の削除**: `uiHelpers()` で 3つのオブジェクト管理 → Floating UI に委譲 -3. **ID ベース選択**: `triggeredBy="#nav-dashboard"` で CSS selector に統一 -4. **保守性向上**: trigger 要素と Dropdown の関連付けが明示的 - -**参考:** [Flowbite Dropdown](https://flowbite-svelte.com/docs/components/dropdown) / [Floating UI Placement](https://floating-ui.com/docs/placement) - ---- - -#### Dropdown ベースのカスタムコンポーネント(⭐⭐ 難易度中) - -| コンポーネント | 変更内容 | 詳細 | -| --------------------- | ------------------------------------- | ----------------------------------------------------------- | -| **UpdatingDropdown** | `uiHelpers()` 削除 + trigger 内部移動 | `triggeredBy` CSS selector で自動制御、位置管理ロジック削除 | -| **TaskTableBodyCell** | `bind:this` 削除、trigger ボタン削除 | UpdatingDropdown に trigger 統合、呼び出しを簡潔化 | - -### UpdatingDropdown + TaskTableBodyCell 移行実装例 - -**変更内容:** - -1. **位置管理ロジックの削除** - - `calculateDropdownPosition()`, `updateDropdownPosition()` 等をコメントアウト - - `uiHelpers()` と `handleDropdownBehavior` action を削除 - - CSS 変数 `--dropdown-x`, `--dropdown-y` の設定を削除 - - → Floating UI が自動的にビューポート外での調整を担当 - -2. **trigger ボタンの内部移動** - - 親側: ` - - -``` - -```svelte - - - -
    - - - {#each submissionStatusOptions as submissionStatus} - handleClick(submissionStatus)}> - {submissionStatus.labelName} - - {/each} - - -
    -``` - -**After (Flowbite Svelte v1.31.0)** ✅ - -```svelte - - - - -``` - -```svelte - - - -
    - -
    - -
    - - - - {#if isLoggedIn} - {#each submissionStatusOptions as submissionStatus} - handleClick(submissionStatus)}> -
    - {submissionStatus.labelName} - {#if taskResult.status_name === submissionStatus.innerName} - - {/if} -
    -
    - {/each} - {:else} - アカウント作成 - - ログイン - {/if} -
    -
    - -{#if showForm && selectedSubmissionStatus} - {@render submissionStatusForm(taskResult, selectedSubmissionStatus)} -{/if} -``` - -### 移行時の注意点 - -1. **trigger ID の一意性確保** ✅ - - `Math.random().toString(36).substring(2)` で unique ID 生成 - - 複数の UpdatingDropdown インスタンスでも selector が重複しない - -2. **アクセシビリティ対応** ✅ - - trigger `
    ` に `role="button"`, `tabindex="0"`, `aria-label` を付与 - - ブラウザのデフォルトボタンスタイルを避けつつ、キーボード操作対応 - -3. **スタイル統一** ✅ - - trigger: `hover:bg-gray-200 dark:hover:bg-gray-700` でホバー状態表示 - - Dropdown: `class="w-32 z-50"` で幅と重なり順序を指定 - - 既存の見た目を保持 - -4. **位置管理ロジックはコメント保持** ✅ - - Floating UI の自動ポジショニングに任せる - - 将来的にカスタム配置が必要になった場合の参考用にコメント保持 - -### 教訓 - -- **trigger の責任分離**: 親から trigger ボタン管理を削除し、コンポーネント内で完結 → 呼び出し側が単純化 -- **CSS selector ベースの制御**: `bind:this` や export function より、`triggeredBy` selector による自動制御が保守性↑ -- **Floating UI の活用**: ビューポート外での自動調整により、複雑な位置計算が不要 - -### 4-2. Modal - -**差分の大きさ:** 🟡 中(native `` ベース) - -**主な変更点:** - -- native HTML `` element ベース -- `$state(open)` runes で状態管理(uiHelpers 削除) -- `bind:open` で Modal の可視性制御 -- SvelteKit Form Actions (`use:enhance`) との共存 -- フォーカストラップ、outside click は native で自動処理 - -### Modal 状態管理パターン比較表 - -| 項目 | svelte-5-ui-lib | Flowbite Svelte | 説明 | -| ---------------------- | --------------------------------------------- | ------------------------------------------- | ---------------------------------- | -| **状態管理** | `uiHelpers()` で `open/close` 関数実装 | `$state(open)` runes で boolean 管理 | Flowbite は単純な boolean binding | -| **バインド方式** | Custom `modalStatus` prop + 関数呼び出し | `bind:open` で双方向バインド | UI state の管理のみ | -| **フォーム統合** | 手動 `
    ` タグで別管理 | 手動 `` タグで管理 | SvelteKit Form Actions と共存 | -| **フォーム送信** | `onsubmit` handler + `event.preventDefault()` | `use:enhance` で server action 自動処理 | server-side form submission に対応 | -| **モーダルクローズ** | `closeModal()` 関数を明示呼び出し | form success 後に `modalOpen = false` 設定 | `use:enhance` result で制御 | -| **フッターレンダリ** | Slot で custom レンダリング | Button を form 内に直接配置 | form submit button として機能 | -| **フォーカストラップ** | `uiHelpers()` で実装 | native `` が自動処理 | HTML5仕様で自動 | -| **Outside Click** | `uiHelpers()` で実装 | `outsideclose` prop で制御(default: true) | clickoutside での dismiss 可能 | - -### Modal 実装例 - -**Before (svelte-5-ui-lib)** - -```svelte - - - - - - - - - - - -``` - -**After (Flowbite Svelte v1.31.0)** ✅ - -```svelte - - - - - -
    - - -
    -
    -``` - -### Modal 実装時の注意点 - -1. **`form` prop は不要** - - Flowbite の `form` prop は「クライアント側の form validation UI」に特化 - - SvelteKit server action には向かない - - 手動で `
    ` を用意すること - -2. **`bind:open` は UI state のみ** - - Modal の可視性を制御するだけ - - form submission とは独立 - -3. **`use:enhance` で server action を自動処理** - - ```typescript - async function handleSubmit(event: Event) { - event.preventDefault(); - const response = await fetch('?/update', { ... }); - if (response.ok) { - modalOpen = false; // form success 時のみ close - } - } - ``` - -4. **`outsideclose` で dismiss 制御** - - `outsideclose={true}` : outside click で modal close 可能(デフォルト) - - `outsideclose={false}` : outside click を無視 - -5. **フォーカストラップと keyboard 処理** - - native `` の focustrap は自動 - - Esc キーでも close 可能(HTML5 standard) - -6. **エラーハンドリングは手動** - - `use:enhance` では server error を自動処理しない - - try-catch で HTTP error をキャッチして処理 - -### 参考資料 - -- [Flowbite Modal](https://flowbite-svelte.com/docs/components/modal) -- [HTML5 `` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) -- [SvelteKit Form Actions use:enhance](https://kit.svelte.dev/docs/form-actions) - ---- - -### 4-3. Toast - -**差分の大きさ:** 🟡 中 - -**主な変更点:** - -- `ToastContainer` で位置管理(top-right, bottom-left 等) -- auto-dismiss は手動実装(setTimeout) -- `transition` props で Svelte transitions 対応 - -**参考:** [Flowbite Toast](https://flowbite-svelte.com/docs/components/toast) - ---- - -### 4-4. Spinner - -**差分の大きさ:** ⭐ 低(置き換えのみ) - -**主な変更点:** - -- `type`: "default", "dots", "bars", "pulse", "orbit" -- `color`: "primary", "green", "red", "yellow" 等 -- `size`: "4", "6", "8" - -**参考:** [Flowbite Spinner](https://flowbite-svelte.com/docs/components/spinner) - ---- - -### 4-5. ButtonGroup - -**差分の大きさ:** ⭐ 低(ラッパー) - -**Flowbite Svelte:** - -```svelte - - - - - - - -``` - -**参考:** [Flowbite ButtonGroup](https://flowbite-svelte.com/docs/components/button-group) - ---- - -### 4-6. Footer / FooterCopyright - -**差分の大きさ:** 🟡 中(variants) - -**Flowbite Svelte:** - -```svelte - - -
    - - - About - Privacy Policy - -
    -``` - -**参考:** [Flowbite Footer](https://flowbite-svelte.com/docs/components/footer) - ---- - -## uiHelpers 廃止への対応 - -`svelte-5-ui-lib` の `uiHelpers()` は以下で使用: - -| 使用箇所 | svelte-5-ui-lib | Flowbite Svelte での代替 | -| -------------- | -------------------------------- | -------------------------------------- | -| Modal state | `uiHelpers()` で open/close 管理 | `$state(open)` runes + `bind:open` | -| Dropdown state | `uiHelpers()` で open/close 管理 | `$state(isOpen)` runes + `bind:isOpen` | -| Focus trap | `uiHelpers()` で実装 | native `` が自動処理 | -| Outside click | `uiHelpers()` で実装 | Floating UI で自動処理 | -| Scroll lock | `uiHelpers()` で実装 | native `` が自動処理 | - -**置き換え方針:** - -- Modal / Dropdown の state は Svelte v5 `$state` runes で管理 -- フォーカストラップ、outside click は Flowbite Svelte が自動処理 - ---- - -## Svelte v4 → v5 runes への書き換え - -移行時に v5 runes 記法への統一が必須。 - -| v4 | v5 | 用途 | -| ---------------------------------------- | ----------------------------------------------------------- | -------------------- | -| `let count = 0; $: doubled = count * 2;` | `let count = $state(0); let doubled = $derived(count * 2);` | 反応性、派生状態 | -| `let open = false;` (with event) | `let open = $state(false);` (with event) | state 管理 | -| `onMount(...)` | `$effect(() => { ... })` | ライフサイクル | -| `if (browser) { ... }` | `$effect()` の中でブラウザチェック | クライアント専用処理 | - -**参考:** - -- [Svelte 5 Runes Documentation](https://svelte.dev/docs/svelte-5-migration-guide) -- [PR #1731 (v4→v5 書き換え例)](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/1731) - ---- - -## 参考資料 - -### Flowbite Svelte 公式ドキュメント - -- [Components Overview](https://flowbite-svelte.com/docs/components/accordion) -- [TypeScript API Reference](https://flowbite-svelte.com/docs/pages/typescript) -- [GitHub Repository](https://github.com/themesberg/flowbite-svelte) - -### Svelte 関連 - -- [svelte 5 runes guide](https://svelte.dev/docs/svelte/v5-migration-guide) -- [Svelte 5 API Reference](https://svelte.dev/docs) - -### 移行ガイド - -- [メイン計画ドキュメント](./plan.md) -- [テスト戦略](./testing-strategy.md) diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/investigation.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/investigation.md deleted file mode 100644 index 36c3fbf80..000000000 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/investigation.md +++ /dev/null @@ -1,310 +0,0 @@ -# 調査報告書:Breaking Changes と本プロジェクトへの影響度分析 - -**作成日**: 2026-01-07 - -**調査対象**: Tailwind CSS v3→v4 / Flowbite v2→v3 / Flowbite Svelte v0.45→v1.31 の破壊的変更 - -**ステータス**: 調査完了・実装フェーズ進行中 - ---- - -## 目的 - -Tailwind CSS v3.4.19 → v4.1.18 および Flowbite ライブラリ群のアップグレード時に、本プロジェクトに影響するすべての breaking changes を徹底的に把握し、実装リスク評価を行う。 - ---- - -## 1. Tailwind CSS v3→v4 Breaking Changes(概観) - -### 1.1 必須対応項目 - -| 項目 | v3 | v4 | 本プロジェクトへの影響 | -| ---------------- | ------------------------------------- | ----------------------- | ---------------------- | -| CSS 記法 | `@tailwind base/components/utilities` | `@import "tailwindcss"` | ✅ 対応済み | -| PostCSS plugin | `tailwindcss` | `@tailwindcss/postcss` | ✅ 対応済み | -| config 読込 | 自動検出 | `@config` で明示 | ✅ 対応済み | -| content スキャン | `tailwind.config.ts` 内で指定 | CSS の `@source` で指定 | ✅ 対応済み | - -**影響**: `content` 配列が読まれなくなるため、`@source` ディレクティブを CSS 側に明示的に追加しないと、コンポーネント内のクラスが走査されない。 - -### 1.2 削除されたユーティリティ(v3→v4) - -| v3 ユーティリティ | v4 での代替 | 修正内容 | 本プロジェクトへの影響 | -| ----------------------- | -------------------------------------- | ------------------------------- | ------------------------------- | -| `break-words` | `wrap-break-word` | クラス名変更 | 未使用の可能性(要検査) | -| `flex-shrink-0` | `shrink-0` | クラス名変更 | ✅ **2 箇所で使用中・対応済み** | -| `overflow-ellipsis` | `text-ellipsis` | クラス名変更 | 未使用の可能性(要検査) | -| `decoration-slice` | `box-decoration-slice` | クラス名変更 | 未使用 | -| `decoration-clone` | `box-decoration-clone` | クラス名変更 | 未使用 | -| `bg-opacity-*` | `bg-black/50` 等 opacity modifier | 削除(opacity modifier で代替) | Flowbite 内部実装に依存 | -| `text-opacity-*` | `text-black/50` 等 opacity modifier | 削除(opacity modifier で代替) | Flowbite 内部実装に依存 | -| `border-opacity-*` | `border-black/50` 等 opacity modifier | 削除(opacity modifier で代替) | Flowbite 内部実装に依存 | -| `divide-opacity-*` | `divide-black/50` 等 opacity modifier | 削除(opacity modifier で代替) | Flowbite 内部実装に依存 | -| `ring-opacity-*` | `ring-blue-500/50` 等 opacity modifier | 削除(opacity modifier で代替) | Flowbite 内部実装に依存 | -| `placeholder-opacity-*` | `placeholder-gray-400` 等 | 削除(opacity modifier で代替) | 未使用 | - -**影響**: Flowbite Svelte が v4 対応済みのため、直接の修正は不要。プロジェクトコード内での使用有無を検査。 - -### 1.3 ユーティリティのスケール変更(v3→v4) - -#### shadow / blur / rounded スケール大規模シフト - -| v3 | v4 | 影響 | -| ---------------------- | ------------------ | ------------------------- | -| `shadow-sm` | `shadow-xs` | **サフィックス 1 段階下** | -| `shadow` (base) | `shadow-sm` | **サフィックス 1 段階下** | -| `drop-shadow-sm` | `drop-shadow-xs` | **サフィックス 1 段階下** | -| `drop-shadow` (base) | `drop-shadow-sm` | **サフィックス 1 段階下** | -| `blur-sm` | `blur-xs` | **サフィックス 1 段階下** | -| `blur` (base) | `blur-sm` | **サフィックス 1 段階下** | -| `backdrop-blur-sm` | `backdrop-blur-xs` | **サフィックス 1 段階下** | -| `backdrop-blur` (base) | `backdrop-blur-sm` | **サフィックス 1 段階下** | -| `rounded-sm` | `rounded-xs` | **サフィックス 1 段階下** | -| `rounded` (base) | `rounded-sm` | **サフィックス 1 段階下** | - -**本プロジェクトへの影響**: - -- ✅ `shadow-sm` を使用 → `shadow-xs` への変更必須(✅ 対応済み) -- ✅ `shadow-lg` を使用 → そのまま(v4 で `shadow-lg` は存在) -- ✅ 複数の `rounded`/`rounded-*` を使用 → 全一括確認 + 対応(✅ 対応済み) - -**リスク**: スケール名が全体的に下がるため、視覚的にぼやけたり小さくなる可能性。 - -#### outline ユーティリティのリネーム - -| v3 | v4 | 理由 | -| -------------- | ---------------- | ----------------------------------------- | -| `outline-none` | `outline-hidden` | `outline: none` の明確な表現 | -| (新)- | `outline-none` | 新たに `outline-style: none` を厳密に設定 | - -**本プロジェクトへの影響**: `focus:outline-none` 等の使用有無を検査。 - -#### ring ユーティリティのデフォルト変更 - -| 設定項目 | v3 | v4 | 影響 | -| ------------ | ---------- | -------------- | --------------------------------------- | -| デフォルト幅 | 3px | 1px | `ring` のみ使用時は `ring-3` に明示必須 | -| デフォルト色 | `blue-500` | `currentColor` | `ring` のみ使用時は色を明示必須 | - -**本プロジェクトへの影響**: `ring` 単独使用が少ないため低リスク。Flowbite components 内部での使用を確認。 - ---- - -### 1.4 セレクタの大規模変更(UI 崩れの最大リスク) - -#### space-x-_ / space-y-_ セレクタ変更 - -**v3**: - -```css -.space-y-4 > :not([hidden]) ~ :not([hidden]) { - margin-top: 1rem; -} -``` - -**v4**: - -```css -.space-y-4 > :not(:last-child) { - margin-bottom: 1rem; -} -``` - -**影響**: - -- 最後の兄弟要素にマージンが追加されない -- インライン要素(`` など)の扱いが異なる可能性 -- 複数行レイアウトで微妙にズレる可能性 - -**本プロジェクト内の使用箇所(8 箇所)**: - -- `/account_transfer/+page.svelte`: `space-y-4`, `space-x-2` -- `/workbooks/[slug]/+page.svelte`: `space-y-4` -- `/lib/components/WorkBook/WorkBookForm.svelte`: `space-y-4` -- `/lib/components/TaskForm.svelte`: `space-y-4` -- `/lib/components/LabelWithTooltips.svelte`: `space-x-2` -- `/lib/components/TaskTables/TaskTableBodyCell.svelte`: `space-x-1 lg:space-x-2` -- `/lib/components/TabItemWrapper.svelte`: `space-x-2` -- `/lib/components/WorkBooks/TitleTableBodyCell.svelte`: `space-x-2` - -**推奨対応**: ブラウザで visual regression 確認。必要なら `gap` (flex/grid) への移行。 - -#### divide-x-_ / divide-y-_ セレクタ変更 - -**v3**: - -```css -.divide-y-4 > :not([hidden]) ~ :not([hidden]) { - border-top-width: 4px; -} -``` - -**v4**: - -```css -.divide-y-4 > :not(:last-child) { - border-bottom-width: 4px; -} -``` - -**影響**: - -- 最後の row/col に border が引かれなくなる -- テーブルの下部ボーダーが消える可能性 - -**本プロジェクト内の使用箇所(13 箇所)**: - -- `/account_transfer/+page.svelte`: `divide-y` -- `/workbooks/[slug]/+page.svelte`: `divide-y` -- `/lib/components/TaskTables/TaskTable.svelte`: `divide-y divide-gray-200 dark:divide-gray-700` ✅ 色指定済み -- その他複数の TableBody で `divide-y` 使用 - -**推奨対応**: ブラウザで table 下部の border 有無を確認。必要なら border を明示的に追加。 - ---- - -### 1.5 デフォルト値の変更(Preflight 変更) - -| 項目 | v3 | v4 | 影響 | -| -------------------------------- | -------------------------- | -------------------------------------- | ---------------------------------------------- | -| `border` / `divide` デフォルト色 | `gray-200` | `currentColor` | 色が text 色に変わる(色を明示していれば OK) | -| `ring` デフォルト幅 | 3px | 1px | ring が細くなる | -| `ring` デフォルト色 | `blue-500` | `currentColor` | ring 色が text 色になる | -| Placeholder 色 | `gray-400` | `currentColor` 50% opacity | placeholder が淡くなる可能性 | -| Button cursor | `pointer` | `default` | button hover で cursor 変わらない | -| Dialog margins | リセットなし | `margin: auto` リセット | dialog の配置が変わる可能性 | -| `hidden` 属性優先度 | display class で上書き可能 | display class よりも優先(上書き不可) | `hidden` 属性があると `block` クラスが効かない | - -**本プロジェクトへの影響**: - -- `border` / `divide` は色を明示している箇所が多いため低リスク -- button/dialog については Flowbite component に依存 -- `hidden` 属性と display class の組み合わせに要注意 - ---- - -### 1.6 その他の重要な変更 - -| 項目 | v3 | v4 | 対応 | -| --------------------- | -------------------------------- | --------------------------------------------- | ------------------------------------ | -| important 修飾子 | `!flex` (先頭) | `flex!` (末尾) | 記法変更、検索後に修正 | -| arbitrary 値の括弧 | `bg-[--var]` | `bg-(--var)` | CSS variable 使用時に修正 | -| arbitrary 値の grid | `grid-cols-[max-content,auto]` | `grid-cols-[max-content_auto]` | コンマをアンダースコアに変更 | -| variant stacking 順序 | 右から左 | 左から右 | 複雑なバリアント組み合わせで修正必須 | -| hover variant | 常に適用 | hover 可能デバイス限定 | タッチデバイスでの動作が異なる | -| 色の uniform gradient | 不可 | 可能(3 stop では `via-none` で 2 stop へ) | グラデーション指定方法が変わる | -| transform リセット | `transform-none` | `scale-none` / `rotate-none` 等個別 | 個別プロパティでリセット | -| transition transform | `transition-[opacity,transform]` | `transition-[opacity,scale,rotate,translate]` | transform 系は個別プロパティを指定 | - ---- - -## 2. Flowbite Breaking Changes - -### 2.1 Flowbite v2.5.0 → v3.1.2 の破壊的変更 - -#### v3.0.0 (2025-01-24) - TailwindCSS v4 統合による大型変更 - -**主要な変更:** - -- TailwindCSS v4 への完全移行 -- CSS architecture の再設計 -- CSS variables の生成方式が変更 -- Plugin システムが新規実装 - -**本プロジェクトへの影響:** - -- ✅ **直接影響なし** - Flowbite は CSS ライブラリであり、コンポーネント構造に変更なし -- ✅ **flowbite-svelte v1.31.0 が対応済み** - Svelte レイヤーで抽象化 - -#### v3.0.0 ~ v3.1.2 の間 - -- CSS variables のバグ修正(theme file 新規作成) -- 新たな破壊的変更なし - v3.0.0 が最大の転換点 - ---- - -### 2.2 Flowbite Svelte v0.45.0 以降 → v1.31.0 の破壊的変更 - -#### v0.45.0 (2024-04-16) - Node 要件変更 - -**破壊的変更:** - -- Node 要件を >= 20.0.0 に引き上げ -- v0.44 までは Node >= 18.0.0 で動作 - -**本プロジェクトへの影響:** - -- ✅ Node 要件確認済み(本プロジェクトは Node >= 20.0.0) - -#### v0.45.0 ~ v1.31.0 の間 - -- 機能追加と軽微なバグ修正のみ -- v1.0.0 では Button cursor デフォルト修正(TailwindCSS v4.0.0 対応) -- 新たな破壊的変更なし - ---- - -## 3. 本プロジェクトへの影響度分析 - -### 3.1 優先度1(必ず修正が必要) - -#### ✅ 対応済み項目 - -- `postcss.config.mjs`: `@tailwindcss/postcss` へ更新 -- `src/app.css`: `@import "tailwindcss"` + `@config` の記述 -- `src/app.css`: `@source` ディレクティブでコンテンツスキャン範囲を明示 -- `tailwind.config.ts`: `content` 配列を削除(v4 では CSS 側の `@source` で指定) -- `flex-shrink-0` → `shrink-0`(2 箇所)✅ 対応済み -- `shadow-sm` → `shadow-xs`(1 箇所)✅ 対応済み -- `rounded` → `rounded-sm`(1 箇所)✅ 対応済み - ---- - -### 3.2 優先度2(テスト必須で挙動が変わる) - -1. **space-y-_ / space-x-_ セレクタ変更(8 箇所)** - - 影響: 最後の要素にマージンが追加されない - - テスト方法: ブラウザで visual regression 確認 - - 推奨: 必要なら `gap` (flex/grid) への移行 - -2. **divide-y / divide-x セレクタ変更(13 箇所)** - - 影響: 最後の row/col に border が引かれない - - テスト方法: テーブル表示を確認 - - 推奨: テーブル下部に border が必要なら明示的に追加 - -3. **divide デフォルト色確認** - - `/lib/components/TaskTables/TaskTable.svelte`: `divide-gray-200 dark:divide-gray-700` (明示済み、安全) - - その他 `divide-y` で色未指定の箇所: v4 で `currentColor` になるため、ブラウザで確認 - ---- - -### 3.3 優先度3(後回し可だが確認推奨) - -1. **outline-none → outline-hidden(使用有無を検査)** -2. **ring 無サフィックス → ring-3 + 色明示(使用有無を検査)** -3. **important 修飾子を末尾に移動(使用有無を検査)** -4. **arbitrary 値の括弧記法確認(CSS variable 使用有無を検査)** -5. **button cursor 変更(UX への影響確認)** - ---- - -## 4. 実装レベルでの追加注意点 - -### 4.1 Flowbite Svelte 関連 - -- **State 管理の簡潔化**: svelte-5-ui-lib の独自 state(`navStatus`, `toggleNav`, `closeNav`)を削除可能 -- **Props 名の確認**: `aClass`(svelte-5-ui-lib)→ `activeClass`(Flowbite Svelte) -- **$app/stores の必須性**: SSR 環境では `$app/stores` が必須(client-only デモは `$app/state` を使用) - -### 4.2 Form Actions との共存 - -- **Modal の `form` prop は不要**: SvelteKit server action で十分 -- **`bind:open` + `use:enhance` の分離**: `form` prop での validation handling より簡潔 - ---- - -## 参考資料 - -- [Tailwind CSS v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) -- [Flowbite v3 Release Notes](https://github.com/themesberg/flowbite/releases/tag/v3.0.0) -- [Flowbite Svelte GitHub](https://github.com/themesberg/flowbite-svelte) diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/testing-strategy.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/testing-strategy.md deleted file mode 100644 index a0a002e1f..000000000 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/testing-strategy.md +++ /dev/null @@ -1,508 +0,0 @@ -# テスト戦略・E2E スモークテスト実装ガイド - -**作成日**: 2026-01-02 - -**対象**: svelte-5-ui-lib → flowbite-svelte 移行時の回帰検出テスト - ---- - -## 概要 - -本ドキュメントは、TailwindCSS v3→v4 移行と Flowbite Svelte への置き換え後に、前回失敗ポイント(カスタムカラー未反映、navbar responsive 破壊、ダークモード消失)を検出するテスト戦略を記載します。 - ---- - -## テストフレームワークの選択 - -| テスト種別 | Vitest | Playwright E2E | -| ---------------------- | ------------------------ | ---------------------------- | -| **対象** | コンポーネント単体、関数 | ユーザーフロー(実ブラウザ) | -| **実行速度** | 高速(秒単位) | 遅い(分単位) | -| **デバッグ難度** | 低い | 高い | -| **スモークテスト向き** | △ 条件付き | ⭐ 推奨 | - -**本プロジェクトでは Playwright を重視する理由:** - -- CSS ビルド出力検証(ファイル読み込み + regex 検証) -- 実ブラウザでの UI 動作確認(セレクタ特定、レイアウト確認) -- 前回失敗ポイント(レスポンシブ、ダークモード toggle)は UI 操作テストが有効 - ---- - -## テスト設計方針 - -### 目的の明確化 - -```text -前回の失敗: - - ビルドは成功したが UI が崩壊 - - カスタム colors(primary, atcoder)が反映されない - - navbar のレスポンシブが動作しない - - ダークモード toggle が消失 - -対策: - - 各フェーズ完了直後に「最小限の smoke test」を実行 - - 大きな問題を素早く検出 → 詳細テストは後回し - - 時間単位での確認を重視(分単位の遅さは許容) -``` - ---- - -## フェーズごとのテスト実行タイミング - -| フェーズ | テスト対象 | テストファイル | 実行タイミング | -| ----------------------- | --------------------------------- | ----------------------------- | ----------------------- | -| **フェーズ-1** | Playwright 環境確認、セレクタ特定 | smoke-tests.md 前提条件確認 | フェーズ-1 中に手動確認 | -| **フェーズ0** | ビルド出力確認(カスタムカラー) | `tests/custom-colors.spec.ts` | フェーズ0 完了直後 | -| **フェーズ1-1/1-2/1-4** | UI 動作確認(dark mode, navbar) | `tests/dark-mode.spec.ts` | フェーズ1-4 完了直後 | -| **フェーズ1-4** | Navbar レスポンシブ確認 | `tests/navbar.spec.ts` | フェーズ1-4 完了直後 | -| **フェーズ2** | 全テスト実行による回帰検出 | `pnpm test:integration` | 毎回の CI 実行 | - ---- - -## 前提条件確認(フェーズ-1) - -フェーズ実装前に、以下を手動確認してセレクタを特定します。 - -### 前提条件確認の流れ - -1. `pnpm dev` で開発サーバ起動 -2. ブラウザで [http://localhost:5174/](http://localhost:5174/) にアクセス -3. Developer Tools で以下を確認 - -### 確認項目 - -#### 1-1. ダークモード切り替えボタンのセレクタ確認 - -```html - - -``` - -**確定セレクタ**: `button[aria-label="Dark mode"]` ✅ - -#### 1-2. navbar 構造確認 - -```html - - -``` - -**確定セレクタ:** - -- navbar: `nav` ✅ -- menu items: `nav ul li a` ✅ -- hamburger button: `nav button:not([aria-label])` ✅ -- menu container: `div[role="none"] ul` ✅ - -#### 1-3. ブレークポイント確認 - -| デバイス | 幅 | navbar menu 表示状態 | -| --------------- | ------ | -------------------------------------- | -| Mobile (iPhone) | 375px | `hidden` クラス + `lg:hidden` で非表示 | -| Tablet | 768px | 同上 | -| Desktop (lg) | 1024px | `lg:block` で表示 | - -**確認方法**: Playwright `isVisible()` で実際のレンダリング確認 - ---- - -## テストコード実装 - -### テスト A: TailwindCSS v4 ビルド出力確認 - -**ファイル**: `tests/custom-colors.spec.ts` - -**フェーズ**: フェーズ0(TailwindCSS v4 移行直後) - -**目的**: ビルド出力に カスタムカラーが含まれているか確認(前回 v3→v4 失敗の再現防止) - -```typescript -import { test, expect } from '@playwright/test'; -import { readFileSync, readdirSync } from 'fs'; -import { resolve } from 'path'; - -test.describe('TailwindCSS v4 configuration', () => { - /** - * ビルド出力(.svelte-kit/output/client/_app/immutable/assets/0.*.css)に - * カスタムカラーが生成されているか確認 - * - * 前提条件:pnpm build 実行済み - */ - - test('primary color is generated in CSS', () => { - const cssDir = resolve('.svelte-kit/output/client/_app/immutable/assets'); - let cssFiles: string[] = []; - - try { - const allCssFiles = readdirSync(cssDir).filter((f: string) => f.endsWith('.css')); - cssFiles = allCssFiles.filter((f: string) => f.startsWith('0.')); - - // Fallback: use any CSS file if no 0.*.css files found - if (cssFiles.length === 0) { - cssFiles = allCssFiles; - } - } catch (e) { - // True error: directory not found or inaccessible - throw new Error(`CSS directory not found: ${cssDir}`); - } - - expect(cssFiles.length).toBeGreaterThan(0); - - const cssPath = resolve(cssDir, cssFiles[0]); - const css = readFileSync(cssPath, 'utf-8'); - - // primary-* クラスが生成されているか - expect(css).toMatch(/\.text-primary-[0-9]/); - expect(css).toMatch(/\.bg-primary-[0-9]/); - }); - - test('atcoder color is generated', () => { - const cssDir = resolve('.svelte-kit/output/client/_app/immutable/assets'); - const cssFiles = require('fs') - .readdirSync(cssDir) - .filter((f: string) => f.startsWith('0.') && f.endsWith('.css')); - const cssPath = resolve(cssDir, cssFiles[0]); - const css = readFileSync(cssPath, 'utf-8'); - - // atcoder-* クラスが生成されているか - expect(css).toMatch(/\.bg-atcoder-/); - }); - - test('xs breakpoint media queries are generated in CSS', () => { - const cssDir = resolve('.svelte-kit/output/client/_app/immutable/assets'); - const cssFiles = readdirSync(cssDir) - .filter((f: string) => f.endsWith('.css')) - .sort() - .reverse(); - - expect(cssFiles.length).toBeGreaterThan(0); - - // xs ブレークポイントを確認するため、全ての CSS ファイルをまとめる - const allCss = cssFiles - .map((f: string) => readFileSync(resolve(cssDir, f), 'utf-8')) - .join('\n'); - - // xs: プレフィックス付きクラスが CSS に含まれているか確認 - // TailwindCSS v4 では @media クエリで xs breakpoint が定義される - expect(allCss).toMatch(/@media\(min-width:26\.25rem\)/); - }); -}); -``` - -**実行方法**: - -```bash -# フェーズ0: TailwindCSS v4 @import/@config/@source 対応完了直後 -pnpm build -pnpm playwright test tests/custom-colors.spec.ts -``` - -**成功条件**: - -- ✅ primary color classes が CSS に含まれている -- ✅ atcoder color classes が CSS に含まれている -- ✅ xs: プレフィックス付きクラスが CSS に含まれている(breakpoint 生成確認) - ---- - -### テスト B: ダークモード検出 - -**ファイル**: `tests/dark-mode.spec.ts` - -**フェーズ**: フェーズ1-4(navbar/auth コンポーネント置換完了後) - -**目的**: ダークモード切り替えボタンが消失していないか確認(前回 v3→v4 失敗の再現防止) - -```typescript -import { test, expect, Page, Browser } from '@playwright/test'; - -test.describe('Dark mode - Regression from v3->v4 migration', () => { - /** - * 前提条件: - * - 前提条件確認セクション で確認したセレクタを使用 - * - pnpm dev で開発サーバ起動済み - */ - - let page: Page; - - test.beforeEach(async ({ browser }) => { - page = await browser.newPage(); - }); - - test.afterEach(async () => { - await page.close(); - }); - - test('dark toggle button is visible on homepage', async () => { - await page.goto('http://localhost:5174/'); - - const darkToggle = page.locator('button[aria-label="Dark mode"]'); - await expect(darkToggle).toBeVisible(); - }); - - test('dark mode icon shows correctly on mobile', async () => { - // iPhone viewport - await page.setViewportSize({ width: 375, height: 667 }); - await page.goto('http://localhost:5174/'); - - const darkIcon = page.locator('button[aria-label="Dark mode"] svg'); - await expect(darkIcon).toBeVisible(); - - // 位置がずれていないか確認(viewport 外ではない) - const bbox = await darkIcon.boundingBox(); - expect(bbox?.x).toBeGreaterThan(0); - expect(bbox?.width).toBeGreaterThan(0); - }); - - test('dark mode icon shows correctly on lg (desktop)', async () => { - // Desktop viewport - await page.setViewportSize({ width: 1024, height: 768 }); - await page.goto('http://localhost:5174/'); - - const darkIcon = page.locator('button[aria-label="Dark mode"] svg'); - await expect(darkIcon).toBeVisible(); - }); - - test('dark mode toggle switches theme', async () => { - await page.goto('http://localhost:5174/'); - - const html = page.locator('html'); - const initialClass = await html.getAttribute('class'); - - // toggle button クリック - const darkToggle = page.locator('button[aria-label="Dark mode"]'); - await darkToggle.click(); - - // クラスが変更されたか確認(dark class が toggle される) - const afterClass = await html.getAttribute('class'); - expect(initialClass).not.toBe(afterClass); - }); -}); -``` - -**実行方法**: - -```bash -# フェーズ1-4: navbar/auth コンポーネント置換完了直後 -pnpm dev # 別ターミナルで起動 -pnpm playwright test tests/dark-mode.spec.ts -``` - -**成功条件**: - -- ✅ ダークモード button が visible -- ✅ mobile / lg で icon が正しく表示 -- ✅ toggle で `` が変更される - ---- - -### テスト C: Navbar レスポンシブ確認 - -**ファイル**: `tests/navbar.spec.ts` - -**フェーズ**: フェーズ1-4(navbar 置換完了後) - -**目的**: navbar のレスポンシブ動作を確認(モバイルでメニューが隠れる、デスクトップで表示されるか) - -```typescript -import { test, expect, Page } from '@playwright/test'; - -test.describe('Navbar responsive behavior', () => { - /** - * 前提条件: 前提条件確認セクション で確認したセレクタを使用 - */ - - test('navbar menu is hidden on mobile', async ({ browser }) => { - const context = await browser.newContext({ - viewport: { width: 375, height: 667 }, - }); - const page = await context.newPage(); - - await page.goto('http://localhost:5174/'); - - // menu container が非表示であることを確認 - const menuContainer = page.locator('div[role="none"] ul'); - const isVisible = await menuContainer.isVisible(); - - // mobile では hidden class が効いて非表示のはず - expect(isVisible).toBe(false); - - await context.close(); - }); - - test('navbar menu is visible on lg (desktop)', async ({ browser }) => { - const context = await browser.newContext({ - viewport: { width: 1024, height: 768 }, - }); - const page = await context.newPage(); - - await page.goto('http://localhost:5174/'); - - // menu container が表示されていることを確認 - const menuContainer = page.locator('div[role="none"] ul'); - const isVisible = await menuContainer.isVisible(); - - // desktop では lg:block が効いて表示のはず - expect(isVisible).toBe(true); - - await context.close(); - }); - - test('hamburger button is visible on mobile', async ({ browser }) => { - const context = await browser.newContext({ - viewport: { width: 375, height: 667 }, - }); - const page = await context.newPage(); - - await page.goto('http://localhost:5174/'); - - // hamburger button が表示されていることを確認 - const hamburger = page.locator('nav button:not([aria-label])'); - const isVisible = await hamburger.isVisible(); - - expect(isVisible).toBe(true); - - await context.close(); - }); - - test('hamburger button is hidden on lg (desktop)', async ({ browser }) => { - const context = await browser.newContext({ - viewport: { width: 1024, height: 768 }, - }); - const page = await context.newPage(); - - await page.goto('http://localhost:5174/'); - - // hamburger button が非表示であることを確認(lg:hidden) - const hamburger = page.locator('nav button:not([aria-label])'); - const isVisible = await hamburger.isVisible(); - - expect(isVisible).toBe(false); - - await context.close(); - }); - - test('menu items are visible on lg', async ({ browser }) => { - const context = await browser.newContext({ - viewport: { width: 1024, height: 768 }, - }); - const page = await context.newPage(); - - await page.goto('http://localhost:5174/'); - - // menu items が表示されていることを確認 - const menuItems = page.locator('nav ul li a'); - const count = await menuItems.count(); - - expect(count).toBeGreaterThan(0); - - await context.close(); - }); -}); -``` - -**実行方法**: - -```bash -# フェーズ1-4: navbar 置換完了直後 -pnpm dev # 別ターミナルで起動 -pnpm playwright test tests/navbar.spec.ts -``` - -**成功条件**: - -- ✅ mobile で menu hidden, hamburger visible -- ✅ lg (desktop) で menu visible, hamburger hidden -- ✅ menu items が lg で表示される - ---- - -## テスト実行手順 - -### 全 Playwright テストの実行 - -```bash -# 全テスト実行 -pnpm playwright test tests/ - -# または特定のテストのみ -pnpm playwright test tests/custom-colors.spec.ts -pnpm playwright test tests/dark-mode.spec.ts -pnpm playwright test tests/navbar.spec.ts -``` - -### テスト結果の確認 - -```bash -# テスト実行後に HTML report が生成される -pnpm playwright show-report -``` - ---- - -## Visual Regression テスト(手動確認チェックリスト) - -E2E テストの後、以下の visual regression をブラウザで手動確認します(investigation.md の breaking changes 参照)。 - -### フェーズ0 → フェーズ1 移行時 - -- [ ] カスタムカラー(primary, atcoder)が正しい色で表示されているか -- [ ] navbar メニューのレイアウトが崩れていないか -- [ ] dark mode toggle が正しく動作するか - -### フェーズ1 → フェーズ2 移行時(任意) - -- [ ] space-y-_ / space-x-_ による margin 変更を visual 確認 -- [ ] divide-y / divide-x による table border 変更を visual 確認 -- [ ] 各コンポーネントの色、サイズが期待通りか - ---- - -## トラブルシューティング - -### Q: テストが localhost に接続できない - -**A**: `pnpm dev` で開発サーバが起動しているか確認してください。 - -```bash -# 別ターミナルで起動 -pnpm dev - -# その後、別のターミナルでテスト実行 -pnpm playwright test -``` - -### Q: CSS ファイルが見つからない(custom-colors.spec.ts) - -**A**: `pnpm build` を先に実行してください。 - -```bash -pnpm build -pnpm playwright test tests/custom-colors.spec.ts -``` - ---- - -## 参考資料 - -- [README-plan.md](./README-plan.md) - 実行計画とチェックリスト -- [investigation.md](./investigation.md) - Breaking Changes 詳細 -- [Playwright 公式ドキュメント](https://playwright.dev/docs/intro) - ---- - -**最終更新**: 2026-01-04 From a4d2a3141894692795887a7d2b846c3e72c75f3d Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 13:02:15 +0000 Subject: [PATCH 5/7] docs: Update plan and rules (#3356) --- .claude/rules/testing-e2e.md | 8 ++++++++ .../README-plan.md | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.claude/rules/testing-e2e.md b/.claude/rules/testing-e2e.md index 9c394361a..babeef13b 100644 --- a/.claude/rules/testing-e2e.md +++ b/.claude/rules/testing-e2e.md @@ -88,6 +88,14 @@ await expect(toggleInput).toBeChecked({ checked: true }); The same pattern applies to any Flowbite component that visually overlays its native input (e.g. `Checkbox`, `Radio`). +## Waiting for Svelte Transitions + +Svelte's `{#if}/{:else}` blocks with transitions may temporarily render multiple elements with the same selector during outro/intro overlap. Playwright locators may match either element non-deterministically, causing flaky tests. + +Use `waitForFunction` with `Array.from(elements).every(...)` to explicitly wait until **all** matching elements reach the desired state before asserting — checking every element guards against the transient window where multiple elements coexist. **Do not use assertions as implicit sleeps** — separating wait logic from assertions makes test intent clear and avoids timing-dependent failures. + +This pattern applies to any Svelte component with transitions: modals, drawers, dropdowns, etc. + ## Strict Mode: Scope Locators to the Content Area When the navbar and page body both contain a link or button with the same text (e.g., a breadcrumb and a nav link share the same label), `getByRole` in strict mode will find multiple matches and throw. Scope the locator to the page's content container: diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md index a404240f5..6d037239e 100644 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md +++ b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md @@ -1,13 +1,13 @@ # UIライブラリ移行:svelte-5-ui-lib → Flowbite Svelte -**作成日**: 2026-01-02 | **最終更新**: 2026-04-04 | **ステータス**: 移行完了 +**作成日**: 2026-01-02 | **最終更新**: 2026-04-04 | **ステータス**: 移行完了(フォローアップタスクあり) svelte-5-ui-lib から Flowbite Svelte v1.33.0 への移行と、TailwindCSS v3→v4 移行を完了。 フェーズ-1〜3(テスト環境構築、Tailwind v4 移行、コンポーネント置換、旧ライブラリ削除)はすべて完了済み。 --- -## 未着手タスク +## フォローアップタスク ### Visual regression テスト(手動確認) From f40eeb50647650ebd971ddd705bdaa81115c07da Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 13:02:35 +0000 Subject: [PATCH 6/7] test(e2e): fix flaky test (#3356) --- e2e/navbar.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/e2e/navbar.spec.ts b/e2e/navbar.spec.ts index 56cd0808a..ba39aba97 100644 --- a/e2e/navbar.spec.ts +++ b/e2e/navbar.spec.ts @@ -60,8 +60,16 @@ test.describe('Navbar - Regression from Svelte 5 UI lib to Flowbite Svelte v1.31 // Click hamburger again to close menu await hamburger.click(); - // Svelte {#if}/{:else} renders two
      during slide-out outro; verify single element after transition - await expect(menuContainer).toHaveCount(1); + + // Wait for Svelte outro transition to complete + // (during transition, {#if}/{:else} may render two
        elements) + await page.waitForFunction(() => { + const menus = document.querySelectorAll('nav div ul'); + return Array.from(menus).every((menu) => !menu.checkVisibility()); + }); + + // Verify final state after transition await expect(menuContainer).not.toBeVisible(); + await expect(menuContainer).toHaveCount(1); }); }); From d48730502087a032e1c46a20556c9eebdc3c5368 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 4 Apr 2026 13:11:19 +0000 Subject: [PATCH 7/7] chore: Remove old lesson (#3356) --- .../README-plan.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md index 6d037239e..fdd39000e 100644 --- a/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md +++ b/docs/dev-notes/2026-01-02/migrate-from-svelte-5-ui-lib-to-flowbite-svelte/README-plan.md @@ -53,10 +53,6 @@ style={`background-color: ${getTaskGradeColor(grade)};`} テストが通らない原因が「ライブラリのバグ」だけでなく「セレクタミス」の可能性を常に考慮。ToolbarButton.svelte → NavHamburger.svelte のコードを追跡し、`name = "Open main menu"` が `aria-label` に変換されることを確認した例。 -### 5. Svelte `{#if}/{:else}` とトランジションの DOM 共存 - -NavUl の `{#if !hidden}` ブロックに `transition:slide` がある場合、閉じる際に outgoing 要素と incoming 要素が一時的に DOM に共存する。Playwright テストでは `toHaveCount(1)` でトランジション完了を確認してから `not.toBeVisible()` を assert する。 - --- ## 参考資料