Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
da08ffd
refactor(hub): stop using Tailwind CSS and shadcn/ui
ujiro99 Oct 18, 2026
3f62e19
docs: add Tailwind CSS v4 migration plan
ujiro99 Oct 18, 2026
7263389
feat(extension): add helpers to isolate content script CSS from the page
ujiro99 Oct 19, 2026
940b6db
feat(extension): migrate to Tailwind CSS v4
ujiro99 Oct 19, 2026
912e8d1
test(extension): cover cn() with Tailwind CSS v4 class names
ujiro99 Oct 19, 2026
546a1a8
docs: document Tailwind CSS v4 rules for content script styles
ujiro99 Oct 19, 2026
5424137
chore: require Node.js 20+ (needed by Tailwind CSS v4)
ujiro99 Oct 19, 2026
b50fd4e
fix(extension): resolve theme colors on the element and limit class s…
ujiro99 Oct 19, 2026
75c6bcf
test(e2e): pass hub assets missing locally through to the deployed hub
ujiro99 Oct 19, 2026
0c78de9
docs: add findings from e2e verification to the migration plan
ujiro99 Oct 19, 2026
7f2f079
fix(extension): address code review findings on content script CSS is…
ujiro99 Oct 19, 2026
92bc543
fix(extension): address review findings on menu hover and shadow DOM …
ujiro99 Oct 20, 2026
3323d00
fix(extension): center dialog enter animation on Tailwind CSS v4
ujiro99 Oct 20, 2026
6f9a833
fix(extension): render menu hover area outside animated popover content
ujiro99 Oct 20, 2026
3d26109
fix(extension): unmount hover area while popover is closing
ujiro99 Oct 20, 2026
c904f3c
fix(extension): remove background transition from menu items
ujiro99 Oct 21, 2026
32e26f8
chore(extension): remove unused cn package
ujiro99 Oct 21, 2026
9d53741
style(extension): format menu files with Prettier
ujiro99 Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 303 additions & 0 deletions docs/design/tailwindcss-v4-migration.md

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,15 @@
"@types/react-dom": "^18.3.5",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"jsdom": "^26.1.0",
"postcss": "^8.4.49",
"prettier": "^3.6.2",
"pretty-quick": "^4.2.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vitest": "^3.2.4"
},
"engines": {
"node": ">=18.0.0",
"node": ">=20.0.0",
"yarn": ">=1.22.0"
},
"dependencies": {},
Expand Down
20 changes: 19 additions & 1 deletion packages/extension/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ This file provides guidance to AI Agent when working with code in this repositor
- **ビルドシステム**: Vite with `@crxjs/vite-plugin` for Chrome extension development
- **UIコンポーネント**: Shadcn
- **フォームとバリデーション**: react-hook-form and zod
- **スタイリング**: CSS Modules + Tailwind CSS(ver.3)
- **スタイリング**: CSS Modules + Tailwind CSS(ver.4、`@tailwindcss/vite`。テーマは `src/components/global.css` の `@theme` で定義)
- **状態管理**: React hooks with Chrome extension storage APIs
- **テスト**: Vitest with jsdom for unit/integration testing
- **e2eテスト**: Playwright for browser automation testing
Expand Down Expand Up @@ -119,6 +119,24 @@ CSS は `content_script.tsx` の Shadow DOM 初期化時に `insertCss()` で手
> content_script に CSS import を追加する際は、manifest 経由での自動注入が抑制されていることを確認すること。
> 新たな content_script エントリーを追加した場合も同様に Shadow DOM への手動注入が必要。

**content script のスタイルはユーザーページと相互に干渉させない**

content script はユーザーが閲覧中の任意のページ上で動作するため、ユーザーページへ影響を与えず、
ユーザーページから影響も受けないようにする(詳細は `docs/design/tailwindcss-v4-migration.md`)。

- **`rem` を使わない**: `rem` は Shadow DOM 内でもユーザーページの root font-size に従う。
Tailwind のテーマは `--spacing: 4px` をはじめ、rem で定義されるトークンをすべて px で再定義している。
任意値(`w-[2rem]` 等)や CSS Modules でも `rem` は使わず `px` を使うこと。
content script の CSS に `rem` が含まれるとビルドが失敗する(`src/lib/findRemUsages.ts`)。
素の `rounded` は v4 の組み込みで `0.25rem` 固定のため、代わりに `rounded-sm`(4px)を使う。
ブレークポイント(`--breakpoint-*`)も v3 と同じ px で再定義している。
- **`@property` は Shadow DOM 内で無視される**: Tailwind v4 が `@property` で登録する `--tw-*` の初期値は、
`src/lib/postcss-shadow-dom-properties.ts`(PostCSS プラグイン)が Shadow root に注入される CSS 内で宣言している。
`@property` をドキュメント(`document.head`)へ注入するなど、Shadow root の外へスタイルを出す方法は取らないこと。
- **CSS Modules のルールはレイヤー外のため Tailwind のユーティリティ(`@layer utilities`)より常に優先される**:
同じ要素に付けるユーティリティ(`hover:bg-accent` 等)と同じプロパティを CSS Modules 側で宣言しないこと(`background: none` 等のリセットも含む)。
- **CSS Modules で `@apply` を使う場合**は、ファイル先頭に `@reference "<相対パス>/global.css";` を書くこと。

**パスエイリアス**:

- `@/` - `src/`ディレクトリへのエイリアス
Expand Down
4 changes: 4 additions & 0 deletions packages/extension/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import path from "path"
import { fileURLToPath } from "url"
import { attachSWConsole } from "./utils/logConsole"
import { fetchCfAuthToken } from "./utils/cfAuth"
import { routeHubToLocal } from "./utils/hubRoute"
import {
STORAGE_KEY,
LOCAL_STORAGE_KEY,
Expand Down Expand Up @@ -64,6 +65,9 @@ export const test = base.extend<Fixtures>({
// and its early logs are lost before attachSWConsole is called.
context.on("serviceworker", (worker) => attachSWConsole(worker))

// Optionally serve the hub test page from a local build (E2E_HUB_BASE_URL).
await routeHubToLocal(context)

// Wait for the service worker to be ready before proceeding, so tests can interact with it immediately.
let [sw] = context.serviceWorkers()
if (!sw) {
Expand Down
59 changes: 59 additions & 0 deletions packages/extension/e2e/utils/hubRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { BrowserContext } from "@playwright/test"

const DEPLOYED_HUB_ORIGIN = "https://ujiro99.github.io"
const DEPLOYED_HUB_BASE_PATH = "/selection-command"

/**
* Redirects requests for the deployed hub (GitHub Pages) to a locally served
* hub build, so that hub changes can be verified with e2e before deploying.
*
* Enabled only when E2E_HUB_BASE_URL is set (e.g. "http://localhost:3100"),
* pointing at a static server for `packages/hub/out`.
* The local build has no basePath, so both "/selection-command/*" pages and
* the root-relative "/_next/*" assets they reference are mapped to the
* server root. Extensionless paths are mapped to their exported ".html" file.
*
* Limitation: navigations of windows opened by the extension itself (e.g. the
* PageAction recorder) can't be intercepted, so those windows still load the
* deployed HTML. Its assets have different hashed file names than the local
* build, so assets the local server can't serve (404) are passed through to
* the deployed hub to keep such pages working. Pages (HTML) are never passed
* through, so that a broken local build isn't hidden by the deployed hub.
*/
export async function routeHubToLocal(context: BrowserContext): Promise<void> {
const localBaseUrl = process.env.E2E_HUB_BASE_URL
if (!localBaseUrl) return

await context.route(`${DEPLOYED_HUB_ORIGIN}/**`, async (route) => {
const url = new URL(route.request().url())
let pathname = url.pathname
if (
pathname === DEPLOYED_HUB_BASE_PATH ||
pathname.startsWith(`${DEPLOYED_HUB_BASE_PATH}/`)
) {
pathname = pathname.slice(DEPLOYED_HUB_BASE_PATH.length) || "/"
}
const hasExtension = /\.[a-z0-9]+$/i.test(pathname)
const isPage = !hasExtension || pathname.endsWith(".html")
if (!hasExtension) {
pathname = `${pathname.replace(/\/$/, "/index")}.html`
}
try {
const response = await route.fetch({
url: `${localBaseUrl}${pathname}${url.search}`,
})
if (response.status() === 404 && !isPage) {
await route.continue()
return
}
// A missing page is returned as the local 404, so that the tests fail
// instead of passing against the deployed (old) hub.
await route.fulfill({ response })
} catch {
// Requests still in flight when a page or the context is closed at the
// end of a test fail with "Request context disposed". They don't matter
// to the test, so swallow the error instead of failing the test.
await route.abort().catch(() => {})
}
})
}
13 changes: 8 additions & 5 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
"@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^4.0.0",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dialog": "^1.3.1",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-menubar": "^1.2.1",
"@radix-ui/react-popover": "^1.3.1",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
Expand Down Expand Up @@ -57,14 +57,14 @@
"react-textarea-autosize": "^8.5.3",
"react-transition-group": "^4.4.5",
"sonner": "git+https://github.com/ujiro99/sonner.git",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"tailwind-merge": "^3.7.0",
"tldts": "^7.4.13",
"uuid": "^14.0.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.30",
"@tailwindcss/vite": "^4.3.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/canvas-confetti": "^1.9.0",
Expand All @@ -81,7 +81,10 @@
"glob": "^10.3.10",
"globals": "^15.14.0",
"npm-build-zip": "^1.0.4",
"postcss": "^8.4.49",
"tailwindcss": "^4.3.3",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5",
"vite-plugin-css-injected-by-js": "^3.5.2",
Expand Down
6 changes: 0 additions & 6 deletions packages/extension/postcss.config.cjs

This file was deleted.

4 changes: 2 additions & 2 deletions packages/extension/src/components/App.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import url("global.css");
@import url("Animation.css");
@import "./global.css";
@import "./Animation.css";

:host,
:root {
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/components/OpenInTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function OpenInTab(): JSX.Element {
<div className="OpenInTab" style={{ visibility: "hidden" }}>
<button
type="button"
className="OpenInTab__button rounded flex items-center px-2 py-1 text-xs text-foreground/60 hover:text-foreground cursor-pointer gap-1.5"
className="OpenInTab__button rounded-sm flex items-center px-2 py-1 text-xs text-foreground/60 hover:text-foreground cursor-pointer gap-1.5"
onClick={onClickOpenTab}
>
<ExternalLink className="OpenInTab__icon size-4" />
Expand Down
11 changes: 7 additions & 4 deletions packages/extension/src/components/Popup.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@reference "./global.css";

.popup {
border: solid 1px var(--sc-bd-color);
font-family: var(--font-family);
Expand All @@ -21,7 +23,8 @@
--background: var(--sc-bg-color-h) var(--sc-bg-color-s) var(--sc-bg-color-l);
--foreground: 222.2 47.4% 11.2%;
--border: var(--sc-bd-color-h) var(--sc-bd-color-s) var(--sc-bd-color-l);
--accent: var(--sc-bg-color-h) var(--sc-bg-color-s) calc(var(--sc-bg-color-l) - 6%);
--accent: var(--sc-bg-color-h) var(--sc-bg-color-s)
calc(var(--sc-bg-color-l) - 6%);
--accent-foreground: 224 71% 4%;
}

Expand All @@ -36,20 +39,20 @@
.previewContainer {
position: relative;

&>div {
& > div {
transform: translate(0, 0) !important;
}
}

.previewLabel {
@apply font-mono text-gray-800;
font-size: 1.2rem;
font-size: 19.2px;
}

.previewDescription {
position: absolute;
width: 200px;
font-size: 0.8rem;
font-size: 12.8px;
margin: 10px 0 0;
}

Expand Down
Loading