diff --git a/docs/design/tailwindcss-v4-migration.md b/docs/design/tailwindcss-v4-migration.md new file mode 100644 index 000000000..da5e10ea5 --- /dev/null +++ b/docs/design/tailwindcss-v4-migration.md @@ -0,0 +1,303 @@ +# Plan: Tailwind CSS v4 への移行(extension)と Hub からの Tailwind 撤去 + +- Issue: [#497 tailwindcss v4系への移行](https://github.com/ujiro99/selection-command/issues/497) +- 参照: [Tailwind CSS Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) + +## Context + +現在は Tailwind CSS v3(root `^3.4.17` / hub `^3.4.1`)を使用している。v4 は CSS-first な設定(`@theme`)、新エンジン、ネイティブ Cascade Layers / `@property` / `color-mix()` を前提とした大型アップデートであり、設定方法・ユーティリティ名・デフォルトスタイルに破壊的変更がある。 + +本対応は 1 つの PR で以下の 2 つを行う。 + +1. **extension**: Tailwind CSS v4 へ移行する。 +2. **hub**: Tailwind CSS の使用をやめ、素の CSS(CSS Modules + グローバル CSS)に置き換える。hub の役割はテストページと設定値(`ai-services.json` / `experiments.json`)のホスティングのみであり、Tailwind を導入しておくメリットがないため。 + +### 現状の構成 + +| 項目 | 内容 | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 共通設定 | `/tailwind.config.js`(shadcn 系のカラートークン、`borderRadius`、`fontFamily`、`tailwindcss-animate`、`darkMode: ["class"]`、`container`)。extension と hub が spread して利用 | +| extension | `packages/extension/tailwind.config.js` が共通設定を spread して拡張(独自 keyframes/animation 多数、`fontSize` / `padding` / `translate` / `minWidth` を **px で上書き**、`listStyleType`) | +| hub | `packages/hub/tailwind.config.ts` が共通設定を spread して拡張(`chart` カラー、`scalePop` 等の keyframes、`data` variant) | +| PostCSS | extension: `postcss.config.cjs`(`tailwindcss` + `autoprefixer`) / hub: `postcss.config.mjs`(`tailwindcss`) | +| エントリ CSS | extension: `src/components/global.css`(`@import "tailwindcss/base"` 等)→ `App.css` から `@import` / hub: `src/app/globals.css`(`@tailwind base` 等) | +| `@apply` を使う CSS Modules | extension 8 ファイル(`Popup` / `TableOfContents` / `HubBanner` / `ShortcutList` / `Option` / `CommandEditDialog` / `PageActionRecorder` / `help`)、hub 2 ファイル(`page` / `Header`) | +| 関連ライブラリ | `tailwind-merge ^2.5`(shared の `cn()`、extension、hub)、`tailwindcss-animate ^1.0.7`、`class-variance-authority`、shadcn/ui(extension の `components/ui/*`、hub の `components/ui/*` と `components.json`) | +| ビルド | extension: Vite 6 + crxjs。content script の CSS は manifest から除去し、Shadow DOM(`#selection-command`)へ手動注入(`content_script.tsx` / `vite-plugin-manifest.ts`)。dev 時は `vite-plugin-css-injected-by-js` で Shadow root へ注入 | + +### extension で v4 の影響を受けるクラスの使用状況(概算、`src` 配下の grep 件数) + +| v3 クラス | v4 での扱い | 件数 | +| --------------------------------------------------------------- | ----------------------------------- | ----------- | +| `shadow-sm` / `shadow` | `shadow-xs` / `shadow-sm` | 9 / ~60 | +| `rounded-sm` / `rounded` | `rounded-xs` / `rounded-sm` | 17 / ~144 | +| `blur` / `backdrop-blur` | `blur-sm` / `backdrop-blur-sm` | ~13 / 3 | +| `outline-none` | `outline-hidden` | 16 | +| `ring` | `ring-3`(v4 の `ring` は 1px) | ~45 | +| `flex-shrink-*` | `shrink-*` | 1 | +| `space-x/y-*` | セレクタ変更(`:not(:last-child)`) | 36 | +| `bg-gradient-to-*` | `bg-linear-to-*` | 1+ | +| `!` important 修飾子 / `bg-[--var]` / `theme()` / `*-opacity-*` | — | 0 | +| `container` ユーティリティ | — | 0(未使用) | + +※ `shadow` / `rounded` / `ring` などはコメントや変数名の誤検出を含むため概数。正確な置換はアップグレードツールに任せ、差分をレビューする。 + +### hub で Tailwind を使用している箇所 + +| ファイル | 内容 | +| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `src/app/globals.css` | `@tailwind` 指令、shadcn 系 CSS 変数(`:root` / `.dark`)、`@apply` による base、`clickable-button` | +| `src/app/page.module.css` | `@apply` によるレイアウト(`sm:` / `md:` のレスポンシブ指定を含む) | +| `src/components/layout/Header.module.css` | `@apply sm:top-8 sm:right-12` | +| `src/app/layout.tsx` / `src/app/[lang]/layout.tsx` | body の `bg-stone-50 text-stone-700` 等 | +| `src/app/page.tsx` / `src/app/not-found.tsx` | 移転案内ページ(各 9 箇所の `className`) | +| `src/app/[lang]/test/page.tsx` | **e2e テストページ**(41 箇所の `className`) | +| `src/components/layout/Header.tsx` / `Footer.tsx` / `LocaleSelector.tsx` / `Image.tsx` | レイアウト部品 | +| `src/components/ui/button.tsx` / `select.tsx` / `separator.tsx` | shadcn/ui(`cva` + `cn` + Tailwind クラス)。`select` は `@radix-ui/react-select`、`separator` は `@radix-ui/react-separator` に依存 | +| `src/lib/utils.ts` / `src/lib/__tests__/utils.test.ts` | shared の `cn()`(`tailwind-merge`)の再エクスポートとそのテスト | + +## 設計方針 + +### 1. 対応ブラウザ要件 + +v4 は Chrome 111+ / Safari 16.4+ / Firefox 128+ を要求する。extension は Chrome 拡張(Manifest V3)のため **要件上の問題はない**。manifest の `minimum_chrome_version` を設定している場合は 111 以上であることを確認する。hub は Tailwind を撤去するため本要件の対象外。 + +### 2. ビルド統合 + +- **extension**: `@tailwindcss/vite` を採用(公式推奨・高速)。`postcss.config.cjs` と `autoprefixer` を削除する。 + - crxjs / `vite-plugin-css-injected-by-js` / `removeCssFromContentScript` との併用で CSS 出力ファイル構成が変わらないかを確認する。問題があれば `@tailwindcss/postcss` にフォールバックする。 +- **hub**: Tailwind / PostCSS 設定を削除する(7 章)。 +- `tailwindcss` 本体の依存は root と hub から外し、extension の `devDependencies` にのみ明示する(現在 extension は root の hoist に依存している)。 + +### 3. extension の設定は CSS-first(`@theme`)へ移行する + +`@config` で JS 設定を読み込み続ける方式も可能だが、v4 では非推奨寄りで、共通設定を spread する現在の構成は `@config` と相性が悪い。また hub が Tailwind を使わなくなるため、パッケージ間で Tailwind テーマを共有する必要もなくなる。そこで root の `tailwind.config.js` と extension の `tailwind.config.js` の内容を、extension のエントリ CSS(`global.css`、必要に応じて `theme.css` に分割)へ統合する。 + +- shadcn 系カラー: 既存の `hsl(var(--xxx))` 形式を維持し、`@theme inline { --color-border: hsl(var(--border)); ... }` で接続する(oklch への変換は本移行のスコープ外)。 +- `--radius-lg/md/sm`、`--font-sans` を移植。 +- `darkMode: ["class"]` → `@custom-variant dark (&:where(.dark, .dark *));` +- keyframes / animation(`--animate-*` + `@keyframes`)を移植。 +- `listStyleType` は `@utility list-square` / `list-circle` 等で再定義。 +- `fontSize` / `padding` / `translate` / `minWidth` の px 上書きは 4.2 の方針で置き換える。 +- `container` 設定は `container` ユーティリティ未使用のため削除する。 +- `tailwindcss-animate` → **`tw-animate-css`**(shadcn 公式の v4 向け後継)へ置き換え、CSS から `@import "tw-animate-css";` する。 +- root / extension の `tailwind.config.js` は削除する。 + +### 4. ユーザーページとの相互不干渉(設計原則) + +content script はユーザーが閲覧中の任意のページ(以下「ユーザーページ」)上で動作する。そのため、本移行では以下の 2 点を必須要件とする。 + +- **ユーザーページへ影響を与えない**: 拡張機能の CSS・CSS 変数・`@property` 登録などを Shadow root の外(`document` / `document.head`)へ一切出さない。 +- **ユーザーページから影響を受けない**: ユーザーページの root font-size(`rem` の基準)や `@property` 登録などに、content script UI の見た目が左右されないようにする。 + +以下の 4.1 / 4.2 はこの原則に基づく設計である。 + +#### 4.1 Shadow DOM 内での `@property` 非対応への対策(最重要リスク) + +content script の UI は Shadow DOM 内でスタイルを適用している。v4 は `shadow-*` / `ring-*` / `translate-*` / `scale-*` / `rotate-*` / グラデーション / `blur` などで `@property` による CSS 変数の初期値登録に依存しているが、**Chrome は Shadow root 内の `@property` を無視する**ため、これらのユーティリティが効かなくなる(既知の問題)。 + +**対策: 自前のビルドプラグインで初期値を Shadow root 内に閉じて付与する。** + +- ビルド時に生成 CSS の `@property --tw-*` 定義から `initial-value` を抽出し、`:host, *, ::before, ::after, ::backdrop { --tw-xxx: ; }` を `@layer base`(`@layer properties`)に付与する小さな PostCSS / Vite プラグインを作成する。 +- `initial-value` を持たない `@property` も `initial` として宣言する。Shadow DOM 内では未登録のカスタムプロパティとして扱われ常に継承されるため、宣言しないと祖先要素(ユーザーページ側を含む)の値が流入する(例: 親の `duration-300` が子の `transition-all` に伝わる)。 +- 付与したルールは Shadow root に注入される CSS の中にのみ存在するため、ユーザーページへは影響しない。 +- `@property` を `document.head` に注入する方式は、ユーザーページにグローバルな登録を残して影響を与えるため**採用しない**。逆に、ユーザーページ側が同名の `--tw-*` を `@property` 登録している場合(ユーザーページが Tailwind v4 製の場合など)でも、各要素に明示的に値を宣言するため初期値の差異の影響を受けない。 + - ただし `@property` の `syntax`(型)はユーザーページ側の登録が Shadow DOM 内にも適用される。Tailwind v4 同士であれば同一定義のため問題ないが、型の異なる同名登録がある場合の挙動を PoC で確認する。 +- options page / onboarding page は拡張機能自身のページであり、`@property` が通常どおり機能するため本プラグインの付与は無害(同値の宣言が増えるのみ)。content script 用 CSS のみに適用するかは PoC で判断する。 + +また v4 の Preflight は `html, :host` を対象にするため、v3 より Shadow DOM との相性は改善する見込み。 + +#### 4.2 `rem` 依存の排除(px 化) + +`rem` はドキュメントの root(``)の font-size を基準とするため、Shadow DOM 内でも**ユーザーページの root font-size の影響を受ける**。現状(v3)でも extension は `fontSize.xs/sm` と `padding` / `translate` / `minWidth` の一部を px で上書きしてこれを回避しているが、上書きしていないトークン(margin / gap / width、`text-base` 以上、`max-w-*` 等)は rem のままで、**既にユーザーページの影響を受けている**。 + +v4 ではスペーシングが `--spacing` を基準にした単一スケールになり、`padding` だけを上書きする手段がない。そこで本移行で、extension のテーマから rem 依存を排除する。 + +- `@theme { --spacing: 4px; }` とし、スペーシング系ユーティリティ(padding / margin / gap / width / height / inset / translate 等)をすべて px 基準にする。現在の px 上書き(`p-1 = 4px` 等)と同じ値になる。 +- rem で定義されている v4 のデフォルトトークンを px で再定義する(16px = 1rem 換算で、標準環境での見た目は不変)。 + - `--text-*`(`xs: 12px` / `sm: 14px` / `base: 16px` / `lg` / `xl` / `2xl`〜`4xl` 等、使用中のもの以上)と対応する `--text-*--line-height` + - `--container-*`(`max-w-lg` / `max-w-xl` / `max-w-2xl`〜`4xl` 等) + - `--radius-*`(root の `--radius` は extension では既に `8px`) + - `--tracking-*` は em 単位のため対象外。`--leading-*` は単位なしのため対象外。 + - `--breakpoint-*` もメディアクエリ内の rem がブラウザの既定フォントサイズ設定に連動して v3 と挙動が変わるため、px で再定義する(ユーザーページの影響は受けないため rem 検査の対象外)。 +- 再定義漏れを防ぐため、ビルド後の content script 用 CSS に `rem` が残っていないことを確認するチェック(ビルドスクリプト or ユニットテスト)を追加する。arbitrary value(`w-[2rem]` 等)での rem 記述も禁止し、同チェックで検出する。 +- v3 では `fontSize.xs/sm` が文字列指定のため line-height が付与されていなかったが、v4 では `--text-*--line-height` が付与される。差分が出る場合は `--text-xs--line-height` / `--text-sm--line-height` を調整する。 +- options page / onboarding page も同じテーマを共有するが、root font-size は既定の 16px のため見た目は変わらない。 + +### 5. extension のデフォルトスタイル変更への対応 + +| 変更点 | 対応 | +| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| border の既定色が `currentColor` に | 既存の `* { @apply border-border; }` があるため影響なし(維持する) | +| `ring` の既定幅 3px→1px、既定色 | アップグレードツールで `ring` → `ring-3` に置換。既定色は shadcn の `ring-ring` 指定箇所が多いため個別確認 | +| placeholder 色 | 現行の見た目を確認し、必要なら `@layer base` で `gray-400` 相当を指定 | +| `button` の `cursor: default` | v3 と同様にするため `button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; }` を base に追加 | +| `hover:` が `@media (hover: hover)` 内に | 拡張機能はデスクトップ前提のため原則そのまま。e2e で hover 依存のテストが落ちる場合のみ `@custom-variant hover (&:hover);` | +| `hidden` 属性の優先度 | `hidden` 属性と `flex` 等を併用している箇所がないか確認 | +| `space-x/y-*` のセレクタ変更 | 36 箇所を目視確認。インライン要素や条件付きレンダリングの子でズレる場合は `flex` + `gap` に置換 | +| `transform` 系の個別プロパティ化 | `transition-[transform]` / `transform-none` の使用箇所を確認。`onboarding-float` は既に `translate` プロパティを使用しているため互換性は良い | + +### 6. extension の CSS Modules / `@layer` の書き換え + +- `@apply` を含む CSS Modules(8 ファイル)の先頭に `@reference "";` を追加する。 +- `global.css` の `@layer components` / `@layer utilities` 内の独自クラス(`marquee-var` / `animate-pause` / `transition-size`)を `@utility` に移行する。`@layer base` はそのまま利用可能。 +- `@import "tailwindcss/base"` 等を `@import "tailwindcss";` に置換する。 + +### 7. hub からの Tailwind 撤去 + +hub は「テストページ」と「設定値(`ai-services.json` / `experiments.json`)のホスティング」のみを担うため、Tailwind・shadcn/ui をやめて素の CSS にする。見た目の厳密な再現は不要だが、**e2e テストページの DOM 構造は維持する**。 + +#### 7.1 スタイリング方針 + +- Next.js 組み込みの CSS サポート(グローバル CSS + CSS Modules)のみを使う。PostCSS プラグインは不要なため `postcss.config.mjs` を削除する(Next.js のデフォルト設定に戻る)。CSS のネスト(`common.module.css` の `&:hover`)はブラウザのネイティブ CSS Nesting で動作する。 +- `globals.css`: `@tailwind` 指令と `@apply` を削除し、必要最小限のリセット(`box-sizing`、`margin: 0` 等)と body の配色・フォントを素の CSS で記述する。shadcn 系 CSS 変数のうち使用するもの(`--border` 等)のみ残し、`.dark` / `--chart-*` / `clickable-button` は削除する。 +- `page.module.css` / `Header.module.css`: `@apply` を素の CSS に展開する。`sm:` / `md:` は `@media (min-width: 640px)` / `(min-width: 768px)` に置き換える。 +- 各コンポーネントの `className` に書かれた Tailwind クラスは、コンポーネントごとの CSS Modules に移す。対象: `layout.tsx`、`[lang]/layout.tsx`、`page.tsx`、`not-found.tsx`、`[lang]/test/page.tsx`、`Header.tsx`、`Footer.tsx`、`LocaleSelector.tsx`、`Image.tsx`。 + - `page.tsx` と `not-found.tsx` は同じ移転案内のマークアップのため、CSS Modules を共有する。 + - `Image.tsx` の `dark:invert` は `.dark` クラスを付与する仕組みが hub にないため削除する。 + +#### 7.2 shadcn/ui コンポーネントの置き換え + +| コンポーネント | 置き換え | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `ui/button.tsx` | テストページでのみ使用。素の `