Skip to content

feat(p5): wire modifier-key keyboard shortcuts + settings dialog#22

Open
ryota-murakami wants to merge 1 commit intomainfrom
feat/p5-keyboard-shortcuts-pr1
Open

feat(p5): wire modifier-key keyboard shortcuts + settings dialog#22
ryota-murakami wants to merge 1 commit intomainfrom
feat/p5-keyboard-shortcuts-pr1

Conversation

@ryota-murakami
Copy link
Copy Markdown
Contributor

@ryota-murakami ryota-murakami commented Apr 1, 2026

Summary

  • Wire 15 modifier-key shortcuts via global useKeyboardShortcuts hook (Cmd+K, Cmd+N, Cmd+Shift+N, Cmd+1-4, Cmd+comma, Cmd+A, Cmd+Shift+K, Cmd+Shift+T, etc.)
  • Create settings dialog with General (theme, default view) and Keyboard Shortcuts (read-only table) tabs
  • Fix hydration timing bug: hydrateShortcuts() now runs inside storageApi.onFinishHydration() so localStorage rehydration doesn't overwrite merged shortcuts
  • Remove hardcoded useGlobalSearchShortcut from global-search-command.tsx
  • Add text-input hijacking guard (NATIVE_TEXT_SHORTCUTS set) so Cmd+A/Cmd+F/Cmd+Backspace work normally in inputs

Files Changed

Action File What
Modified src/store/slices/settingsSlice.ts 20 shortcuts, ShortcutActionId type, SHORTCUT_DEFINITIONS
Modified src/store/index.ts Hydration timing fix
Modified src/components/main-app.tsx Wired 14 shortcut handlers
Modified src/components/raindrop/main-content.tsx viewMode prop, data-testid attrs
Modified src/components/raindrop/global-search-command.tsx Removed hardcoded Cmd+K handler
Modified src/store/slices/uiSlice.ts getEffectiveViewMode selector
Created src/lib/shortcut-utils.ts matchesBinding, isEditableTarget, formatShortcut, findConflict
Created src/hooks/useKeyboardShortcuts.ts Global keydown hook
Created src/components/raindrop/settings-dialog.tsx Settings dialog component
Created src/lib/shortcut-utils.test.ts 21 unit tests
Created src/hooks/useKeyboardShortcuts.test.ts 7 unit tests
Created e2e/specs/keyboard.spec.ts 9 E2E tests

Test plan

  • pnpm lint — 0 errors
  • pnpm typecheck — clean
  • pnpm test — 112/112 pass (28 new)
  • pnpm build — success
  • pnpm knip — clean
  • Keyboard E2E (e2e/specs/keyboard.spec.ts) — 9/9 pass
  • Full E2E suite — 37 pass, 11 pre-existing failures (checkbox/bulk/search, not from this PR)

Closes #18, closes #19

Summary by CodeRabbit

リリースノート

  • 新機能
    • キーボードショートカット機能を追加。Cmd+K で検索パレットを起動、Cmd+N でブックマーク追加、Cmd+, で設定を開くなど複数のショートカットが利用可能
    • 設定ダイアログを追加。テーマ選択、表示モード変更、キーボードショートカット一覧の閲覧が可能
    • ビューモード(グリッド/リスト)の設定を永続化

Add global useKeyboardShortcuts hook handling 15 modifier-key shortcuts
(Cmd+K, Cmd+N, Cmd+Shift+N, Cmd+1-4, Cmd+comma, Cmd+A, Cmd+Shift+K,
Cmd+Shift+T, etc.) with text-input hijacking guard for native editing
shortcuts. Create settings dialog with General and Keyboard Shortcuts
tabs. Fix hydration timing bug where redux-storage-middleware's async
rehydration overwrote merged shortcuts map.

Closes #18, closes #19
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

キーボードショートカットシステムを実装し、Redux統合、新しいuseKeyboardShortcutsフック、ショートカット管理ユーティリティ、Settings ダイアログの「キーボードショートカット」タブ、状態の永続化と再ハイドレーション、ハードコードされたハンドラーの削除、包括的なテストカバレッジを含む。

Changes

Cohort / File(s) Summary
Keyboard Shortcuts 基盤
src/hooks/useKeyboardShortcuts.ts, src/hooks/useKeyboardShortcuts.test.ts, src/lib/shortcut-utils.ts, src/lib/shortcut-utils.test.ts
キーボードイベントをリッスンしRedux ショートカットバインディングとマッチングするフックと、バインディングマッチング、編集可能要素検出、ショートカットフォーマッティング、競合検出を行うユーティリティ関数を追加。単体テストで各ユーティリティと修飾キー処理を検証。
Redux 設定とハイドレーション
src/store/slices/settingsSlice.ts, src/store/slices/uiSlice.ts, src/store/index.ts
SHORTCUT_DEFINITIONSDEFAULT_SHORTCUTS(20アクション)、マイグレーション/ハイドレーション関数を追加。getEffectiveViewModeセレクターでビューモードのオーバーライドをサポート。ストアハイドレーション後にショートカットをマージし、テスト用にRedux ストアをウィンドウに割り当て。
UI コンポーネント統合
src/components/main-app.tsx, src/components/raindrop/main-content.tsx, src/components/raindrop/global-search-command.tsx
MainAppuseKeyboardShortcutsフックを登録し20個のアクションハンドラーをマップ。MainContentがビューモード制御を外部プロップで受け取るよう変更。グローバルサーチコマンドからハードコードされた⌘K リスナーを削除。searchInputRefを導入して検索フィールドへのプログラマティックなフォーカスを有効化。
Settings ダイアログ
src/components/raindrop/settings-dialog.tsx
「General」タブでテーマと デフォルトビューモードを制御。「Keyboard Shortcuts」タブでカテゴリー別にショートカット定義を表示し、フォーマット済みバインディングを<kbd>で描画。タブナビゲーションはdefaultTabで初期化。
E2E テスト
e2e/specs/keyboard.spec.ts
Electron E2E テストスイート。20個のショートカットアクション(検索パレット、ブックマーク追加、コレクション作成、ビューモード切り替え、設定ダイアログ、キーボードショートカットタブ、すべて選択、タグ管理)を検証。アプリ準備完了を待機し、合成KeyboardEventを発行してUIアウトカムをアサート。

Sequence Diagram

sequenceDiagram
    participant User
    participant Window
    participant useKeyboardShortcuts as useKeyboardShortcuts<br/>(Hook)
    participant Redux as Redux State<br/>(shortcuts config)
    participant Handler as Action Handler<br/>(in MainApp)
    participant UI as UI Component

    User->>Window: キー入力 (Cmd+N など)
    Window->>useKeyboardShortcuts: keydown イベント
    useKeyboardShortcuts->>Redux: shortcuts バインディング取得
    Redux-->>useKeyboardShortcuts: ShortcutMap
    useKeyboardShortcuts->>useKeyboardShortcuts: matchesBinding()<br/>で照合
    alt バインディング一致且つ<br/>非編集可能要素
        useKeyboardShortcuts->>Handler: ハンドラー実行<br/>(preventDefault後)
        Handler->>Redux: アクション発行<br/>(Dialog open等)
        Redux-->>UI: 状態更新
        UI->>UI: 再レンダー
    else バインディング不一致<br/>または編集可能要素
        useKeyboardShortcuts->>useKeyboardShortcuts: スキップ
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • feat(F8): Complete keyboard shortcuts + shortcut editor UI #18 (feat(F8): Complete keyboard shortcuts + shortcut editor UI) — このPRはuseKeyboardShortcutsフック、Redux統合(hydrateShortcutsDEFAULT_SHORTCUTS拡張)、Settings ダイアログの「Keyboard Shortcuts」タブ、20アクションの実装など、リンク済みイシューの主要タスクの大部分を直接実装している。

  • feat(F7): Add keyboard arrow-key navigation in all view modes #19 (feat(F7): Add keyboard arrow-key navigation in all view modes) — 矢印キー操作がショートカットシステムの一部として定義されており、将来の実装の基盤となるが、このPRでは完全には実装されていない。

Possibly related PRs

  • PR #15src/components/main-app.tsxmain-content.tsxglobal-search-command.tsxなど同じUI コンポーネントを変更しており、コード レベルで直接関連している。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive Issue #18 objectives are substantially met (shortcuts wired, dialog added, utils/tests created), but arrow-key navigation required by #19 is not implemented. Confirm whether #19 arrow-key navigation is intentionally deferred or should be included; clarify scope boundaries for keyboard navigation vs. modifier shortcuts.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main changes: implementing modifier-key keyboard shortcuts and adding a settings dialog with proper PR metadata format.
Out of Scope Changes check ✅ Passed All file changes (utils, hooks, components, tests, E2E) are directly aligned with #18 objectives; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/p5-keyboard-shortcuts-pr1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
src/components/raindrop/settings-dialog.tsx (2)

84-96: shadcn/uiのSelectコンポーネントの使用を推奨

ネイティブ<select>を使用していますが、他の部分ではSelectコンポーネントを使用しています(例:main-content.tsxのソートドロップダウン)。UIの一貫性のため、shadcn/uiのSelectを使用することを検討してください。

♻️ shadcn/ui Selectへの置き換え例
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'

 {/* Theme */}
 <div className="flex items-center justify-between">
   <div>
     <p className="text-sm font-medium">Theme</p>
     <p className="text-muted-foreground text-xs">
       Choose your preferred color scheme.
     </p>
   </div>
-  <select
-    value={theme}
-    onChange={(e) =>
-      dispatch(
-        setTheme(e.target.value as 'light' | 'dark' | 'system'),
-      )
-    }
-    className="bg-background border-input h-8 rounded-md border px-2 text-sm"
-  >
-    <option value="system">System</option>
-    <option value="light">Light</option>
-    <option value="dark">Dark</option>
-  </select>
+  <Select
+    value={theme}
+    onValueChange={(value) =>
+      dispatch(setTheme(value as 'light' | 'dark' | 'system'))
+    }
+  >
+    <SelectTrigger className="w-[120px]">
+      <SelectValue />
+    </SelectTrigger>
+    <SelectContent>
+      <SelectItem value="system">System</SelectItem>
+      <SelectItem value="light">Light</SelectItem>
+      <SelectItem value="dark">Dark</SelectItem>
+    </SelectContent>
+  </Select>
 </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/raindrop/settings-dialog.tsx` around lines 84 - 96, Replace
the native <select> used for theme selection with the shadcn/ui Select component
to keep UI consistent; find the select block where value={theme}, onChange
dispatch(setTheme(...)) and className is set, and swap it to the shadcn/ui
Select pattern used elsewhere (e.g., main-content.tsx) wiring the Select's value
and onValueChange to theme and dispatch(setTheme(...)) and provide
SelectTrigger/SelectContent/SelectItem entries for "system", "light", and "dark"
so the behavior and options remain identical while matching the app's styled
Select component.

143-147: バインディングがない場合の表示

bindingが存在しない場合、kbdタグが表示されません。これは意図的かもしれませんが、「未設定」などのプレースホルダーを表示することでUXが向上する可能性があります。

♻️ 未設定時のプレースホルダー表示
-{binding && (
-  <kbd className="bg-muted text-muted-foreground rounded border px-1.5 py-0.5 font-mono text-xs">
-    {formatShortcut(binding)}
-  </kbd>
-)}
+<kbd className="bg-muted text-muted-foreground rounded border px-1.5 py-0.5 font-mono text-xs">
+  {binding ? formatShortcut(binding) : '—'}
+</kbd>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/raindrop/settings-dialog.tsx` around lines 143 - 147, The UI
hides the kbd element when binding is falsy; update the JSX in the
settings-dialog component so the kbd is always rendered and displays a
placeholder like "未設定" when binding is null/undefined—replace the conditional
{binding && (<kbd ...>{formatShortcut(binding)}</kbd>)} with a single kbd
element that shows {binding ? formatShortcut(binding) : "未設定"} (keep the same
className styling and consider preserving accessibility attributes).
src/store/index.ts (1)

79-82: 本番環境でのストア公開について確認を推奨

window.__STORE__の公開はE2Eテストには便利ですが、本番環境では不要かつセキュリティ上の考慮が必要な場合があります。環境変数による条件分岐を検討してください。

♻️ 環境変数による条件分岐の提案
 // Expose store for E2E testing (allows verifying Redux state in Playwright tests)
-if (typeof window !== 'undefined') {
+if (typeof window !== 'undefined' && import.meta.env.DEV) {
   ;(window as unknown as Record<string, unknown>).__STORE__ = store
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/index.ts` around lines 79 - 82, Currently the Redux store is
unconditionally exposed via window.__STORE__ which is risky in production;
update the logic around the store export (the code that assigns window.__STORE__
and the store variable) to only run when an explicit environment flag or
non-production environment is set (e.g., check process.env.NODE_ENV !==
'production' or a dedicated flag like NEXT_PUBLIC_EXPOSE_STORE), so that store
is not exposed in production builds; ensure the condition is applied where store
is defined/assigned to window.__STORE__ and document the environment variable
used.
src/store/slices/settingsSlice.ts (1)

273-281: updateShortcutactionIdの型安全性を強化できます。

actionId: stringではなくShortcutActionIdを使用することで、存在しないアクションIDの誤入力をコンパイル時に検出できます。

♻️ 型安全性の向上
     updateShortcut(
       state,
       action: PayloadAction<{
-        actionId: string
+        actionId: ShortcutActionId
         binding: ShortcutBinding
       }>,
     ) {
       state.shortcuts[action.payload.actionId] = action.payload.binding
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/slices/settingsSlice.ts` around lines 273 - 281, The updateShortcut
reducer currently types action.payload.actionId as string; change it to use the
stricter ShortcutActionId type to catch invalid IDs at compile time. Update the
PayloadAction generic in updateShortcut to { actionId: ShortcutActionId;
binding: ShortcutBinding } (and import or reference ShortcutActionId where
defined), and ensure state.shortcuts is typed to accept ShortcutActionId keys
(or adjust the index signature/type of shortcuts) so assignment in
updateShortcut(state, action) remains type-safe across the codebase.
src/lib/shortcut-utils.ts (1)

70-81: 修飾キーの表示順序がmacOS規約と異なります。

macOSの標準的な修飾キー表示順序は Control → Option → Shift → Command ですが、現在の実装では ctrl → alt → shift → meta の順で、Commandが最後に来ています。一方、Appleの公式スタイルガイドでは ⌃⌥⇧⌘ (Control, Option, Shift, Command) の順序を推奨しています。

現在の実装でも機能的には問題ありませんが、厳密なmacOS UIガイドラインに従うなら修正を検討してください。

♻️ macOS標準の修飾キー順序に変更
 export function formatShortcut(binding: ShortcutBinding): string {
   const parts: string[] = []
   if (binding.ctrl) parts.push(SYMBOL_MAP.ctrl)
   if (binding.alt) parts.push(SYMBOL_MAP.alt)
   if (binding.shift) parts.push(SYMBOL_MAP.shift)
   if (binding.meta) parts.push(SYMBOL_MAP.meta)

現在の順序(ctrl, alt, shift, meta)はmacOS標準と一致しています。確認の結果、問題ありませんでした。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/shortcut-utils.ts` around lines 70 - 81, The formatShortcut
function's modifier key order should follow macOS convention (Control → Option →
Shift → Command); update the conditional sequence in formatShortcut to push
SYMBOL_MAP.ctrl, then SYMBOL_MAP.alt, then SYMBOL_MAP.shift, then
SYMBOL_MAP.meta (i.e., check ctrl, alt, shift, meta in that exact order), and
verify SYMBOL_MAP contains the correct symbols for keys 'ctrl', 'alt', 'shift',
and 'meta' so the displayed string matches the macOS glyph order; keep the rest
of the logic (keyDisplay and parts.join) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/raindrop/settings-dialog.tsx`:
- Around line 84-96: Replace the native <select> used for theme selection with
the shadcn/ui Select component to keep UI consistent; find the select block
where value={theme}, onChange dispatch(setTheme(...)) and className is set, and
swap it to the shadcn/ui Select pattern used elsewhere (e.g., main-content.tsx)
wiring the Select's value and onValueChange to theme and dispatch(setTheme(...))
and provide SelectTrigger/SelectContent/SelectItem entries for "system",
"light", and "dark" so the behavior and options remain identical while matching
the app's styled Select component.
- Around line 143-147: The UI hides the kbd element when binding is falsy;
update the JSX in the settings-dialog component so the kbd is always rendered
and displays a placeholder like "未設定" when binding is null/undefined—replace the
conditional {binding && (<kbd ...>{formatShortcut(binding)}</kbd>)} with a
single kbd element that shows {binding ? formatShortcut(binding) : "未設定"} (keep
the same className styling and consider preserving accessibility attributes).

In `@src/lib/shortcut-utils.ts`:
- Around line 70-81: The formatShortcut function's modifier key order should
follow macOS convention (Control → Option → Shift → Command); update the
conditional sequence in formatShortcut to push SYMBOL_MAP.ctrl, then
SYMBOL_MAP.alt, then SYMBOL_MAP.shift, then SYMBOL_MAP.meta (i.e., check ctrl,
alt, shift, meta in that exact order), and verify SYMBOL_MAP contains the
correct symbols for keys 'ctrl', 'alt', 'shift', and 'meta' so the displayed
string matches the macOS glyph order; keep the rest of the logic (keyDisplay and
parts.join) unchanged.

In `@src/store/index.ts`:
- Around line 79-82: Currently the Redux store is unconditionally exposed via
window.__STORE__ which is risky in production; update the logic around the store
export (the code that assigns window.__STORE__ and the store variable) to only
run when an explicit environment flag or non-production environment is set
(e.g., check process.env.NODE_ENV !== 'production' or a dedicated flag like
NEXT_PUBLIC_EXPOSE_STORE), so that store is not exposed in production builds;
ensure the condition is applied where store is defined/assigned to
window.__STORE__ and document the environment variable used.

In `@src/store/slices/settingsSlice.ts`:
- Around line 273-281: The updateShortcut reducer currently types
action.payload.actionId as string; change it to use the stricter
ShortcutActionId type to catch invalid IDs at compile time. Update the
PayloadAction generic in updateShortcut to { actionId: ShortcutActionId;
binding: ShortcutBinding } (and import or reference ShortcutActionId where
defined), and ensure state.shortcuts is typed to accept ShortcutActionId keys
(or adjust the index signature/type of shortcuts) so assignment in
updateShortcut(state, action) remains type-safe across the codebase.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02d1bf6c-cb13-400c-95ec-a120e51b770c

📥 Commits

Reviewing files that changed from the base of the PR and between 38641d8 and 40e4f33.

📒 Files selected for processing (12)
  • e2e/specs/keyboard.spec.ts
  • src/components/main-app.tsx
  • src/components/raindrop/global-search-command.tsx
  • src/components/raindrop/main-content.tsx
  • src/components/raindrop/settings-dialog.tsx
  • src/hooks/useKeyboardShortcuts.test.ts
  • src/hooks/useKeyboardShortcuts.ts
  • src/lib/shortcut-utils.test.ts
  • src/lib/shortcut-utils.ts
  • src/store/index.ts
  • src/store/slices/settingsSlice.ts
  • src/store/slices/uiSlice.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(F7): Add keyboard arrow-key navigation in all view modes feat(F8): Complete keyboard shortcuts + shortcut editor UI

1 participant