Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4b9be99
Add: First-run onboarding flow for new installs
ujiro99 Aug 24, 2026
b62b6c6
Update: Refine onboarding flow with return hint, pattern captions, an…
ujiro99 Aug 24, 2026
6438e16
Update: Redesign onboarding chrome for clarity and mental-model reinf…
ujiro99 Aug 25, 2026
5cd3492
Update: Adjust styling.
ujiro99 Aug 25, 2026
76bdfb2
Refactor: Extract popupContext into hooks/usePopupContext
ujiro99 Aug 26, 2026
596e365
Update: Refine onboarding flow copy, callout motion, and styling
ujiro99 Aug 31, 2026
ee3de98
Update: Keep the onboarding rail's beat animation continuous through …
ujiro99 Sep 1, 2026
ca814c4
refactoring.
ujiro99 Sep 1, 2026
a45a8b9
Update: Localize onboarding copy tweaks to all supported languages
ujiro99 Sep 1, 2026
01635b4
Update: Refine onboarding step transitions, menu placement, and copy
ujiro99 Sep 2, 2026
1199c91
Update: Add return-phase handling to Steps 2-3 and refresh onboarding…
ujiro99 Sep 3, 2026
17ec602
Update: Storybooの導入ができなかったときの調査記録
ujiro99 Sep 3, 2026
e9b7351
Update: Refine Step4 screenshots, Step5 animation sequencing, and con…
ujiro99 Sep 3, 2026
d8c3e7c
Update: Add onboarding value sub-messages and localize step4/step3 copy
ujiro99 Sep 4, 2026
276bd7e
Fix: Prevent to send duplicated events.
ujiro99 Sep 4, 2026
00e29f2
Refactor: Manage onboarding step animation delays per StepPhase
ujiro99 Sep 4, 2026
f4f33e6
Fix: Rename onboarding analytics events and dedupe value/complete tra…
ujiro99 Sep 4, 2026
ec4902f
Update: Revise onboarding step3/complete copy and localize to all lan…
ujiro99 Sep 6, 2026
69582bb
Fix: Skip link command analytics event on the onboarding page
ujiro99 Sep 6, 2026
5d73436
Fix: Collect event timing of ONBOARDING_TEXT_SELECTION at AiPrompt.
ujiro99 Sep 6, 2026
c10d898
Fix: Correct onboarding analytics bugs found in code review
ujiro99 Sep 6, 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
99 changes: 99 additions & 0 deletions docs/design/storybook-investigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Storybook導入 調査記録

## 目的

`packages/extension` へ Storybook を導入し、`OnboardingPage.tsx` の各ステップ
(`OnboardingStep` × `StepPhase` の組み合わせ)を個別に確認できるようにする
試みを行った。作業時間の制約により完了に至らなかったため、原因と得られた
知見を記録する。

## 採用した方針(概要)

- `OnboardingPage` は自身の状態を `useOnboardingState` フックで管理しており、
Props を受け取らない。そのため、各ステップ用の Story を個別に自作する
のではなく、`useOnboardingState.ts` に既に存在する e2e 用の URL 上書き
機構(`?step=SEARCH&phase=value_shown`、`e2e/onboarding-shots.spec.ts` が
スクリーンショット撮影に使用しているもの)を Storybook からも流用する
方針を取った。
- `chrome.*` API は Storybook のプレビュー用に独自モック(`chrome.storage`
のメモリ実装、`chrome.i18n.getMessage` の実メッセージ引き当て等)を用意
し、`Storage.setCommands(getDefaultCommands(locale))` を Story 読み込み時
に実行して、本番の初回インストール時と同じ状態を再現する設計とした。

## 遭遇した問題と原因

### 1. Storybook の Vite ビルダーが、拡張機能用の `vite.config.ts` を自動で読み込んでしまう

- `@storybook/react-vite` は既定で `.storybook` の親ディレクトリにある
`vite.config.ts` を自動検出し、Storybook 用の設定にマージする仕様になっ
ている(`@storybook/builder-vite` 内部の `getBuilderOptions()` /
`loadConfigFromFile()`)。
- 本プロジェクトの `vite.config.ts` は Chrome 拡張機能のビルド専用で、
`@crxjs/vite-plugin`(manifest.json の解釈やコンテンツスクリプトの特別
なバンドリング)や、Shadow DOM 環境向けにカスタムした
`vite-plugin-css-injected-by-js` の `injectCodeFunction`
(`document.getElementById("selection-command")?.shadowRoot ?? document.head`
への注入)を含んでいる。
- これが Storybook のプレビュー(通常の DOM ページ)に対しても適用されて
しまい、
- `@crxjs/vite-plugin` の HMR ハンドラがエラーを送出してオーバーレイ
表示になる
- CSS が期待通り `<style>` タグとして注入されない(Tailwind の生成物
自体は正しいのに、実際のDOMには反映されない)
という2つの不具合を引き起こしていた。
- **対策**: `.storybook/main.ts` の `core.builder.options.viteConfigPath`
に、拡張機能とは無関係の空の `vite.config.ts` を指定し、本来の
`vite.config.ts` が読み込まれないようにすることで、HMR エラーと
Shadow DOM 向け CSS 注入コードの混入は解消した。

### 2. `chrome` グローバルのモック順序・カバレッジ不足によるハング

- `commandStorage.ts` など一部のモジュールは、**import された時点**(トッ
プレベル)で `chrome.storage.onChanged.addListener(...)` を呼び出す。
そのため、Storybook のプレビュー側で `chrome` のモックを設定するコード
は、アプリ本体のどのモジュールよりも先に評価される必要があった
(ESM の import 文はファイル内の他の文より先に評価されるため、モック
用モジュールは import 文の先頭に置く、かつ本体コードを import しない
独立したファイルにする、という工夫が必要だった)。
- `services/storage/index.ts` の `debouncedSyncSet()` は、
`chrome.storage.sync.set(items, callback)` という**コールバック形式**
で呼び出している。Storybook 用のモックを Promise ベースのみで実装して
いたためコールバックが一切呼ばれず、`Storage.setCommands()` の
Promise が永久に解決しない(= Story の初期化処理がハングする)という、
原因の特定が難しい不具合が発生した。
- 症状としては「Storybook のプレビューがローディングスピナーのまま
止まる」だけで、コンソールにもエラーが出ないため、`chrome.storage`
まわりの実装差異に気づくまでに時間を要した。
- **対策**: モックの `set`/`remove`/`clear` を、Promise を返しつつ
コールバック引数が渡された場合はそれも呼び出す形に修正し、解消した。

### 3. 自動化ブラウザに実機の拡張機能がインストールされていたことによるノイズ

- 動作確認に使用したブラウザ(claude-in-chrome)に、この拡張機能自体の
開発ビルドと思われる拡張機能が既にインストールされており、あらゆる
ページ(Storybook のプレビュー含む)に content script や Vite の
HMR クライアントを注入していた。
- これにより、コンソールログやネットワークログに無関係な
`chrome-extension://...` 由来のログが大量に混入し、問題の切り分け
(どこまでが自分たちの Storybook 由来で、どこからが無関係な拡張機能
由来か)が難しくなった。

## 未解決だった点

- 上記 1, 2 を解消した直後の時点で、Tailwind のユーティリティクラス自体
は正しく生成されるようになっていたが、実際にブラウザへ注入された
`<style>` タグには反映されておらず、`@/components/App.css` の Vite
モジュールを直接 `import()` しても `<style data-vite-dev-id="...">`
が生成されない事象が残っていた。原因(Vite のモジュールキャッシュ、
もしくは項番3のノイズの影響)を切り分けている途中で作業を打ち切った。

## 次回への申し送り

- `.storybook/main.ts` で `viteConfigPath` を明示的に空の設定に差し替え
る対策は有効だったので、再挑戦する場合はこの対策を先に適用した状態
から始めるとよい。
- `chrome.storage.*` のモックは、コールバック形式・Promise形式の両方を
サポートする実装にしておく(`src/test/setup.ts` の Vitest 用モックも
同様の考慮がされているため、参考にできる)。
- 動作確認は、他の拡張機能がインストールされていないクリーンなブラウザ
プロファイルで行うことを推奨する。
109 changes: 109 additions & 0 deletions packages/extension/e2e/onboarding-shots.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { test, chromium } from "@playwright/test"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import { OnboardingStep, StepPhase } from "@/types/onboarding"

// Not part of the regular e2e suite - a design-review tool. Skipped unless
// explicitly requested (`CAPTURE=1 yarn test:e2e onboarding-shots`) so it
// never slows down or flakes the normal CI run.
//
// Uses `?step=..&phase=..` (see useOnboardingState.ts's readE2eOverride(),
// e2e-build only) to land directly on each screen instead of driving the
// real selection/command flow, and launches its own persistent context per
// locale (rather than the shared `fixtures.ts` one) so this file alone
// controls `--lang`.

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pathToExtension = path.join(__dirname, "../dist")

type Shot = {
name: string
step: keyof typeof OnboardingStep
phase?: StepPhase
}

// One shot per visually distinct screen - not one per StepPhase transition;
// e.g. Step2's WAIT_EXECUTE looks like Step1's, so only Step1 gets it.
const SHOTS: Shot[] = [
{ name: "00-intro", step: "INTRO" },
{ name: "01-search-explain", step: "SEARCH", phase: StepPhase.EXPLAIN },
{
name: "02-search-wait-execute",
step: "SEARCH",
phase: StepPhase.WAIT_EXECUTE,
},
{
name: "03-search-wait-return",
step: "SEARCH",
phase: StepPhase.WAIT_RETURN,
},
{ name: "04-search-value", step: "SEARCH", phase: StepPhase.VALUE_SHOWN },
{ name: "05-ai-explain", step: "AI_PROMPT", phase: StepPhase.EXPLAIN },
{ name: "06-ai-value", step: "AI_PROMPT", phase: StepPhase.VALUE_SHOWN },
{
name: "07-preview-explain",
step: "LINK_PREVIEW",
phase: StepPhase.EXPLAIN,
},
{
name: "08-preview-value",
step: "LINK_PREVIEW",
phase: StepPhase.VALUE_SHOWN,
},
{ name: "09-customize", step: "CUSTOMIZE" },
{ name: "10-complete", step: "COMPLETE" },
]

const LOCALES = ["ja", "en-US", "de"]

test.describe("onboarding screenshots (design review)", () => {
test.skip(
!process.env.CAPTURE,
"set CAPTURE=1 to run onboarding screenshot capture",
)

for (const locale of LOCALES) {
test(`capture (${locale})`, async () => {
test.setTimeout(120_000)

const context = await chromium.launchPersistentContext("", {
headless: false,
args: [
"--headless=new",
`--lang=${locale}`,
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
})

try {
let [sw] = context.serviceWorkers()
if (!sw) sw = await context.waitForEvent("serviceworker")
const extensionId = sw.url().split("/")[2]

const page = await context.newPage()
const outDir = path.join(
__dirname,
"../test-results/onboarding",
locale,
)
fs.mkdirSync(outDir, { recursive: true })

for (const shot of SHOTS) {
const params = new URLSearchParams({ step: shot.step })
if (shot.phase) params.set("phase", shot.phase)
await page.goto(
`chrome-extension://${extensionId}/src/onboarding_page.html?${params}`,
)
// Let entrance animations and the selection-demo cursor settle
// into a representative frame before capturing.
await page.waitForTimeout(700)
await page.screenshot({ path: path.join(outDir, `${shot.name}.png`) })
}
} finally {
await context.close()
}
})
}
})
2 changes: 2 additions & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@supabase/supabase-js": "^2.105.4",
"@testing-library/user-event": "^14.6.0",
"bowser": "^2.14.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"colorthief": "^2.6.0",
Expand All @@ -65,6 +66,7 @@
"@crxjs/vite-plugin": "^2.0.0-beta.30",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/canvas-confetti": "^1.9.0",
"@types/chrome": "^0.1.36",
"@types/jest": "^29.5.11",
"@types/platform": "^1.3.6",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 117 additions & 0 deletions packages/extension/public/_locales/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1301,5 +1301,122 @@
},
"hub_login": {
"message": "Beim Hub anmelden"
},
"onboarding_step0Title": {
"message": "Danke fürs Installieren!"
},
"onboarding_step0Body": {
"message": "Lass uns das Ganze in etwa einer Minute direkt ausprobieren."
},
"onboarding_startButton": {
"message": "Los geht's"
},
"onboarding_skipButton": {
"message": "Überspringen"
},
"onboarding_step1Explain": {
"message": "Markiere den folgenden Text, um sofort eine Suche auszuprobieren."
},
"onboarding_step1TargetText": {
"message": "Eine Erweiterung, mit der du markierten Text mit einem Klick durchsuchen kannst."
},
"onboarding_selectHint": {
"message": "Text unten durch Ziehen markieren"
},
"onboarding_railSelect": {
"message": "Auswählen"
},
"onboarding_railCommand": {
"message": "Befehl"
},
"onboarding_railResultSearch": {
"message": "Suchen"
},
"onboarding_step1Callout": {
"message": "Klicke auf diesen Befehl, um mit Google zu suchen."
},
"onboarding_step1ReturnHint": {
"message": "Wechsle zurück zu diesem Tab, dann schließt sich das Fenster automatisch."
},
"onboarding_step1Callout_2": {
"message": "Klicke irgendwo auf den Bildschirm"
},
"onboarding_step1ValueMessage": {
"message": "Fertig – jetzt kannst du alles, was du markierst, sofort durchsuchen"
},
"onboarding_step1ValueSubmessage": {
"message": "Füge deine bevorzugte Suchmaschine hinzu, um alltägliche Suchen noch schneller zu machen."
},
"onboarding_step2Explain": {
"message": "Als Nächstes fragen wir die KI zum folgenden Text."
},
"onboarding_step2TargetText": {
"message": "Dies ist eine Chrome-Erweiterung, mit der du Text aus dem Web in einen Prompt einfügen und ausführen kannst - überlege dir ein paar Ideen, wofür du sie nutzen könntest."
},
"onboarding_step2Callout": {
"message": "Führe diesen Befehl aus, um deine Auswahl + einen Prompt an die KI zu senden."
},
"onboarding_railResultAi": {
"message": "KI"
},
"onboarding_step2WaitReturn": {
"message": "Sende deine Auswahl + einen Prompt an die KI …"
},
"onboarding_step2ValueMessage": {
"message": "Jetzt kannst du die KI direkt auf der Seite, die du liest, zu allem fragen, was du markierst"
},
"onboarding_step2ValueSubmessage": {
"message": "Frag die KI sofort Dinge wie „Fasse das zusammen\" oder „Analysiere dieses Unternehmen\".\nDen Prompt kannst du frei anpassen."
},
"onboarding_step3Explain": {
"message": "Schauen wir uns einen Link an, ohne einen neuen Tab zu öffnen."
},
"onboarding_step3LinkLabel": {
"message": "Beispiel-Link"
},
"onboarding_railResultPreview": {
"message": "Vorschau"
},
"onboarding_step3Callout": {
"message": "Klicke mit gedrückter Umschalttaste (Shift) auf diesen Link, um ihn in der Vorschau zu öffnen."
},
"onboarding_clickKeycap": {
"message": "Klick"
},
"onboarding_step3ReturnHint": {
"message": "Wechsle zurück zu diesem Tab, dann schließt sich das Fenster automatisch."
},
"onboarding_step3ValueMessage": {
"message": "Fertig – jetzt kannst du Links in der Vorschau ansehen, ohne die Seite zu verlassen"
},
"onboarding_step3ValueSubmessage": {
"message": "Du hast die Information erhalten und bist sofort zurückgekehrt!\nDas reduziert den Aufwand für Tab-Wechsel, damit du dich auf das Wesentliche konzentrieren kannst."
},
"onboarding_nextButton": {
"message": "Weiter"
},
"onboarding_step4_title": {
"message": "Frei anpassen"
},
"onboarding_step4Explain": {
"message": "Befehle lassen sich jederzeit hinzufügen oder bearbeiten"
},
"onboarding_step4SettingsHeading": {
"message": "Erstellen & Bearbeiten in den Einstellungen"
},
"onboarding_step4HubHeading": {
"message": "Entdecken im Selection Command Hub"
},
"onboarding_completeTitle": {
"message": "Alles bereit!"
},
"onboarding_completeBody": {
"message": "Selection Command ist einsatzbereit.\nWähle einfach Text auf einer beliebigen Seite aus, um loszulegen."
},
"onboarding_openSettingsButton": {
"message": "Einstellungen öffnen"
},
"onboarding_closeButton": {
"message": "Schließen und ausprobieren"
}
}
Loading
Loading