2026-02-23 08:53:43 +01:00
|
|
|
import { useEffect } from 'react'
|
2026-02-22 18:42:33 +01:00
|
|
|
import type { ViewMode } from './useViewMode'
|
feat: implement PostHog event tracking plan
Add trackEvent() calls for all user actions in the tracking plan:
- Vault: vault_opened (with has_git, note_count), vault_switched
- Notes: note_created (with has_type, creation_path), note_deleted,
note_trashed, note_archived, note_favorited, note_unfavorited
- Git: commit_made, sync_triggered
- Search: search_used
- Editor: raw_mode_toggled, wikilink_inserted
- Types & Views: type_created, view_created
- Telemetry: telemetry_opted_in, telemetry_opted_out
All events gated by PostHog initialization (only fires when opted in).
No PII in any event properties — counts, booleans, and enums only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:42:43 +02:00
|
|
|
import { trackEvent } from '../lib/telemetry'
|
2026-02-20 19:59:05 +01:00
|
|
|
|
|
|
|
|
interface KeyboardActions {
|
|
|
|
|
onQuickOpen: () => void
|
2026-02-24 23:41:54 +01:00
|
|
|
onCommandPalette: () => void
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
onSearch: () => void
|
2026-02-20 19:59:05 +01:00
|
|
|
onCreateNote: () => void
|
2026-03-02 03:08:15 +01:00
|
|
|
onOpenDailyNote: () => void
|
2026-02-20 19:59:05 +01:00
|
|
|
onSave: () => void
|
2026-02-22 13:38:18 +01:00
|
|
|
onOpenSettings: () => void
|
2026-04-06 12:21:56 +02:00
|
|
|
onDeleteNote: (path: string) => void
|
2026-02-21 18:45:25 +01:00
|
|
|
onArchiveNote: (path: string) => void
|
2026-02-22 18:42:33 +01:00
|
|
|
onSetViewMode: (mode: ViewMode) => void
|
2026-02-28 12:41:57 +01:00
|
|
|
onZoomIn: () => void
|
|
|
|
|
onZoomOut: () => void
|
|
|
|
|
onZoomReset: () => void
|
2026-02-25 19:39:12 +01:00
|
|
|
onGoBack?: () => void
|
|
|
|
|
onGoForward?: () => void
|
2026-03-02 05:00:29 +01:00
|
|
|
onToggleAIChat?: () => void
|
2026-03-02 19:12:11 +01:00
|
|
|
onToggleRawEditor?: () => void
|
2026-03-30 18:45:45 +02:00
|
|
|
onToggleInspector?: () => void
|
2026-04-03 17:20:33 +02:00
|
|
|
onToggleFavorite?: (path: string) => void
|
2026-03-19 08:47:25 +01:00
|
|
|
onOpenInNewWindow?: () => void
|
2026-02-20 19:59:05 +01:00
|
|
|
activeTabPathRef: React.MutableRefObject<string | null>
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 18:45:25 +01:00
|
|
|
type ShortcutHandler = () => void
|
|
|
|
|
|
2026-02-25 18:30:20 +01:00
|
|
|
const TEXT_EDITING_KEYS = new Set(['Backspace', 'Delete'])
|
|
|
|
|
|
|
|
|
|
function isTextInputFocused(): boolean {
|
|
|
|
|
const tag = document.activeElement?.tagName
|
|
|
|
|
return tag === 'INPUT' || tag === 'TEXTAREA'
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 18:42:33 +01:00
|
|
|
const VIEW_MODE_KEYS: Record<string, ViewMode> = {
|
|
|
|
|
'1': 'editor-only',
|
|
|
|
|
'2': 'editor-list',
|
|
|
|
|
'3': 'all',
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 08:13:32 +01:00
|
|
|
function isCmdOnly(e: KeyboardEvent): boolean {
|
|
|
|
|
return (e.metaKey || e.ctrlKey) && !e.altKey
|
2026-02-22 18:42:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleViewModeKey(e: KeyboardEvent, onSetViewMode: (m: ViewMode) => void): boolean {
|
2026-02-23 08:13:32 +01:00
|
|
|
if (!isCmdOnly(e)) return false
|
2026-02-22 18:42:33 +01:00
|
|
|
const mode = VIEW_MODE_KEYS[e.key]
|
|
|
|
|
if (!mode) return false
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
onSetViewMode(mode)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCmdKey(e: KeyboardEvent, keyMap: Record<string, ShortcutHandler>): boolean {
|
|
|
|
|
const mod = e.metaKey || e.ctrlKey
|
|
|
|
|
if (!mod) return false
|
|
|
|
|
const handler = keyMap[e.key]
|
|
|
|
|
if (!handler) return false
|
2026-02-25 18:30:20 +01:00
|
|
|
if (TEXT_EDITING_KEYS.has(e.key) && isTextInputFocused()) return false
|
2026-02-22 18:42:33 +01:00
|
|
|
e.preventDefault()
|
|
|
|
|
handler()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 19:59:05 +01:00
|
|
|
export function useAppKeyboard({
|
2026-04-06 12:21:56 +02:00
|
|
|
onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onDeleteNote, onArchiveNote,
|
2026-04-03 17:20:33 +02:00
|
|
|
onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onToggleInspector, onToggleFavorite, onOpenInNewWindow, activeTabPathRef,
|
2026-02-20 19:59:05 +01:00
|
|
|
}: KeyboardActions) {
|
2026-02-23 08:53:43 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
|
|
|
|
|
const path = activeTabPathRef.current
|
|
|
|
|
if (path) fn(path)
|
|
|
|
|
}
|
2026-02-21 18:45:25 +01:00
|
|
|
|
2026-02-23 08:53:43 +01:00
|
|
|
const cmdKeyMap: Record<string, ShortcutHandler> = {
|
2026-02-24 23:41:54 +01:00
|
|
|
k: onCommandPalette,
|
2026-02-23 08:53:43 +01:00
|
|
|
p: onQuickOpen,
|
|
|
|
|
n: onCreateNote,
|
2026-03-02 03:08:15 +01:00
|
|
|
j: onOpenDailyNote,
|
2026-02-23 08:53:43 +01:00
|
|
|
s: onSave,
|
|
|
|
|
',': onOpenSettings,
|
2026-04-03 17:20:33 +02:00
|
|
|
d: withActiveTab((path) => onToggleFavorite?.(path)),
|
2026-02-23 08:53:43 +01:00
|
|
|
e: withActiveTab(onArchiveNote),
|
2026-04-06 12:21:56 +02:00
|
|
|
Backspace: withActiveTab(onDeleteNote),
|
|
|
|
|
Delete: withActiveTab(onDeleteNote),
|
2026-02-25 19:39:12 +01:00
|
|
|
'[': () => onGoBack?.(),
|
|
|
|
|
']': () => onGoForward?.(),
|
2026-02-28 12:41:57 +01:00
|
|
|
'=': onZoomIn,
|
|
|
|
|
'+': onZoomIn,
|
|
|
|
|
'-': onZoomOut,
|
|
|
|
|
'0': onZoomReset,
|
2026-03-02 19:12:11 +01:00
|
|
|
'\\': () => onToggleRawEditor?.(),
|
2026-02-23 08:53:43 +01:00
|
|
|
}
|
2026-02-21 18:45:25 +01:00
|
|
|
|
2026-02-20 19:59:05 +01:00
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
2026-04-06 12:52:25 +02:00
|
|
|
// Cmd+Shift+L: toggle AI panel
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && !e.altKey && (e.key === 'l' || e.key === 'L')) {
|
2026-04-02 11:28:04 +02:00
|
|
|
e.preventDefault()
|
|
|
|
|
onToggleAIChat?.()
|
|
|
|
|
return
|
|
|
|
|
}
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
// Cmd+Shift+F: full-text search (distinct from Cmd+F browser find)
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'f') {
|
|
|
|
|
e.preventDefault()
|
feat: implement PostHog event tracking plan
Add trackEvent() calls for all user actions in the tracking plan:
- Vault: vault_opened (with has_git, note_count), vault_switched
- Notes: note_created (with has_type, creation_path), note_deleted,
note_trashed, note_archived, note_favorited, note_unfavorited
- Git: commit_made, sync_triggered
- Search: search_used
- Editor: raw_mode_toggled, wikilink_inserted
- Types & Views: type_created, view_created
- Telemetry: telemetry_opted_in, telemetry_opted_out
All events gated by PostHog initialization (only fires when opted in).
No PII in any event properties — counts, booleans, and enums only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:42:43 +02:00
|
|
|
trackEvent('search_used')
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
onSearch()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-30 18:45:45 +02:00
|
|
|
// Cmd+Shift+I: toggle properties/inspector panel
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 'i' || e.key === 'I')) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
onToggleInspector?.()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-19 08:47:25 +01:00
|
|
|
// Cmd+Shift+O: open active note in new window
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 'o' || e.key === 'O')) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
onOpenInNewWindow?.()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-23 08:53:43 +01:00
|
|
|
if (!handleViewModeKey(e, onSetViewMode)) {
|
|
|
|
|
handleCmdKey(e, cmdKeyMap)
|
|
|
|
|
}
|
2026-02-20 19:59:05 +01:00
|
|
|
}
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
2026-04-06 12:21:56 +02:00
|
|
|
}, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onDeleteNote, onArchiveNote, activeTabPathRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onToggleInspector, onOpenInNewWindow])
|
2026-02-20 19:59:05 +01:00
|
|
|
}
|