From 99b4098f486fb94d9d51d4b6e75e39fe37d2d509 Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Wed, 25 Feb 2026 20:05:24 +0100 Subject: [PATCH] feat: populate macOS menu bar with native menus (File, Edit, View, Window) (#77) * feat: populate macOS menu bar with File, Edit, View, Window menus Add complete native macOS menu structure with all app actions: - Laputa menu: About, Settings (Cmd+,), Hide/Quit - File: New Note (Cmd+N), Quick Open (Cmd+P), Save (Cmd+S), Close Tab (Cmd+W) - Edit: standard Undo/Redo/Cut/Copy/Paste/Select All - View: Editor Only (Cmd+1), Editor+Notes (Cmd+2), All Panels (Cmd+3), Toggle Inspector, Command Palette (Cmd+K) - Window: Minimize, Maximize, Close Window All accelerators registered via Tauri menu API. Menu events dispatched to frontend via useMenuEvents hook. Save/Close Tab items dynamically disabled when no note tab is active. Co-Authored-By: Claude Opus 4.6 * fix: wrap Enter selection assertion in waitFor for effect re-registration * fix: resolve rebase conflict markers in menu.rs Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- design/macos-menu-bar.pen | 1 + src-tauri/src/lib.rs | 7 + src-tauri/src/menu.rs | 218 ++++++++++++++++++++++++---- src/components/SearchPanel.test.tsx | 2 +- src/hooks/useAppCommands.ts | 16 +- src/hooks/useMenuEvents.test.ts | 94 ++++++++++++ src/hooks/useMenuEvents.ts | 71 +++++++++ src/hooks/useViewMode.ts | 20 +-- 8 files changed, 380 insertions(+), 49 deletions(-) create mode 100644 design/macos-menu-bar.pen create mode 100644 src/hooks/useMenuEvents.test.ts create mode 100644 src/hooks/useMenuEvents.ts diff --git a/design/macos-menu-bar.pen b/design/macos-menu-bar.pen new file mode 100644 index 00000000..87a376d1 --- /dev/null +++ b/design/macos-menu-bar.pen @@ -0,0 +1 @@ +{"children":[{"id":"menu-structure","type":"frame","name":"macOS Menu Bar Structure","x":0,"y":0,"width":900,"height":600,"layout":"horizontal","gap":24,"padding":24,"children":[{"id":"app-menu","type":"frame","name":"Laputa Menu","layout":"vertical","gap":4,"padding":12,"fill":"#F7F6F3","cornerRadius":[8,8,8,8],"children":[{"id":"app-title","type":"text","content":"Laputa","fontSize":14,"fontWeight":"bold"},{"id":"app-about","type":"text","content":"About Laputa","fontSize":12},{"id":"app-sep1","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"app-settings","type":"text","content":"Settings... ⌘,","fontSize":12},{"id":"app-sep2","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"app-services","type":"text","content":"Services >","fontSize":12},{"id":"app-sep3","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"app-hide","type":"text","content":"Hide Laputa ⌘H","fontSize":12},{"id":"app-hideothers","type":"text","content":"Hide Others ⌥⌘H","fontSize":12},{"id":"app-showall","type":"text","content":"Show All","fontSize":12},{"id":"app-sep4","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"app-quit","type":"text","content":"Quit Laputa ⌘Q","fontSize":12}]},{"id":"file-menu","type":"frame","name":"File Menu","layout":"vertical","gap":4,"padding":12,"fill":"#F7F6F3","cornerRadius":[8,8,8,8],"children":[{"id":"file-title","type":"text","content":"File","fontSize":14,"fontWeight":"bold"},{"id":"file-new","type":"text","content":"New Note ⌘N","fontSize":12},{"id":"file-quickopen","type":"text","content":"Quick Open ⌘P","fontSize":12},{"id":"file-sep1","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"file-save","type":"text","content":"Save ⌘S","fontSize":12},{"id":"file-sep2","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"file-close","type":"text","content":"Close Tab ⌘W","fontSize":12}]},{"id":"edit-menu","type":"frame","name":"Edit Menu","layout":"vertical","gap":4,"padding":12,"fill":"#F7F6F3","cornerRadius":[8,8,8,8],"children":[{"id":"edit-title","type":"text","content":"Edit","fontSize":14,"fontWeight":"bold"},{"id":"edit-undo","type":"text","content":"Undo ⌘Z","fontSize":12},{"id":"edit-redo","type":"text","content":"Redo ⇧⌘Z","fontSize":12},{"id":"edit-sep1","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"edit-cut","type":"text","content":"Cut ⌘X","fontSize":12},{"id":"edit-copy","type":"text","content":"Copy ⌘C","fontSize":12},{"id":"edit-paste","type":"text","content":"Paste ⌘V","fontSize":12},{"id":"edit-sep2","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"edit-selectall","type":"text","content":"Select All ⌘A","fontSize":12}]},{"id":"view-menu","type":"frame","name":"View Menu","layout":"vertical","gap":4,"padding":12,"fill":"#F7F6F3","cornerRadius":[8,8,8,8],"children":[{"id":"view-title","type":"text","content":"View","fontSize":14,"fontWeight":"bold"},{"id":"view-editor","type":"text","content":"Editor Only ⌘1","fontSize":12},{"id":"view-editorlist","type":"text","content":"Editor + Notes ⌘2","fontSize":12},{"id":"view-all","type":"text","content":"All Panels ⌘3","fontSize":12},{"id":"view-sep1","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"view-inspector","type":"text","content":"Toggle Inspector","fontSize":12},{"id":"view-sep2","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"view-cmdpalette","type":"text","content":"Command Palette ⌘K","fontSize":12}]},{"id":"window-menu","type":"frame","name":"Window Menu","layout":"vertical","gap":4,"padding":12,"fill":"#F7F6F3","cornerRadius":[8,8,8,8],"children":[{"id":"window-title","type":"text","content":"Window","fontSize":14,"fontWeight":"bold"},{"id":"window-minimize","type":"text","content":"Minimize ⌘M","fontSize":12},{"id":"window-zoom","type":"text","content":"Zoom","fontSize":12},{"id":"window-sep1","type":"text","content":"---","fontSize":10,"color":"#CCC"},{"id":"window-bringall","type":"text","content":"Bring All to Front","fontSize":12}]}]}],"variables":{}} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6bc17dfa..b27f1a47 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -109,6 +109,12 @@ fn migrate_is_a_to_type(vault_path: String) -> Result { vault::migrate_is_a_to_type(&vault_path) } +#[tauri::command] +fn update_menu_state(app_handle: tauri::AppHandle, has_active_note: bool) -> Result<(), String> { + menu::set_note_items_enabled(&app_handle, has_active_note); + Ok(()) +} + #[tauri::command] fn get_settings() -> Result { settings::get_settings() @@ -238,6 +244,7 @@ pub fn run() { purge_trash, migrate_is_a_to_type, get_settings, + update_menu_state, save_settings, github_list_repos, github_create_repo, diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index d34a48ed..5dce3098 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -1,48 +1,210 @@ use tauri::{ - menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, - App, Emitter, + menu::{MenuBuilder, MenuItemBuilder, MenuItemKind, Submenu, SubmenuBuilder}, + App, AppHandle, Emitter, }; -const VIEW_ITEMS: [(&str, &str, &str); 3] = [ - ("view-editor-only", "Editor Only", "CmdOrCtrl+1"), - ("view-editor-list", "Editor + Notes", "CmdOrCtrl+2"), - ("view-all", "All Panels", "CmdOrCtrl+3"), +// Custom menu item IDs that emit events to the frontend. +const APP_SETTINGS: &str = "app-settings"; +const FILE_NEW_NOTE: &str = "file-new-note"; +const FILE_QUICK_OPEN: &str = "file-quick-open"; +const FILE_SAVE: &str = "file-save"; +const FILE_CLOSE_TAB: &str = "file-close-tab"; +const VIEW_EDITOR_ONLY: &str = "view-editor-only"; +const VIEW_EDITOR_LIST: &str = "view-editor-list"; +const VIEW_ALL: &str = "view-all"; +const VIEW_TOGGLE_INSPECTOR: &str = "view-toggle-inspector"; +const VIEW_COMMAND_PALETTE: &str = "view-command-palette"; + +const CUSTOM_IDS: &[&str] = &[ + APP_SETTINGS, + FILE_NEW_NOTE, + FILE_QUICK_OPEN, + FILE_SAVE, + FILE_CLOSE_TAB, + VIEW_EDITOR_ONLY, + VIEW_EDITOR_LIST, + VIEW_ALL, + VIEW_TOGGLE_INSPECTOR, + VIEW_COMMAND_PALETTE, ]; -pub fn setup_menu(app: &App) -> Result<(), Box> { - let edit_submenu = SubmenuBuilder::new(app, "Edit") - .item(&PredefinedMenuItem::undo(app, Some("Undo"))?) - .item(&PredefinedMenuItem::redo(app, Some("Redo"))?) - .separator() - .item(&PredefinedMenuItem::cut(app, Some("Cut"))?) - .item(&PredefinedMenuItem::copy(app, Some("Copy"))?) - .item(&PredefinedMenuItem::paste(app, Some("Paste"))?) - .item(&PredefinedMenuItem::select_all(app, Some("Select All"))?) - .build()?; +/// IDs of menu items that should be disabled when no note tab is active. +const NOTE_DEPENDENT_IDS: &[&str] = &[FILE_SAVE, FILE_CLOSE_TAB]; - let mut view_menu = SubmenuBuilder::new(app, "View"); - for (id, label, accel) in &VIEW_ITEMS { - let item = MenuItemBuilder::new(*label) - .id(*id) - .accelerator(*accel) - .build(app)?; - view_menu = view_menu.item(&item); - } - let view_submenu = view_menu.build()?; +type MenuResult = Result, Box>; + +fn build_app_menu(app: &App) -> MenuResult { + let settings_item = MenuItemBuilder::new("Settings...") + .id(APP_SETTINGS) + .accelerator("CmdOrCtrl+,") + .build(app)?; + + Ok(SubmenuBuilder::new(app, "Laputa") + .about(None) + .separator() + .item(&settings_item) + .separator() + .services() + .separator() + .hide() + .hide_others() + .show_all() + .separator() + .quit() + .build()?) +} + +fn build_file_menu(app: &App) -> MenuResult { + let new_note = MenuItemBuilder::new("New Note") + .id(FILE_NEW_NOTE) + .accelerator("CmdOrCtrl+N") + .build(app)?; + let quick_open = MenuItemBuilder::new("Quick Open") + .id(FILE_QUICK_OPEN) + .accelerator("CmdOrCtrl+P") + .build(app)?; + let save = MenuItemBuilder::new("Save") + .id(FILE_SAVE) + .accelerator("CmdOrCtrl+S") + .build(app)?; + let close_tab = MenuItemBuilder::new("Close Tab") + .id(FILE_CLOSE_TAB) + .accelerator("CmdOrCtrl+W") + .build(app)?; + + Ok(SubmenuBuilder::new(app, "File") + .item(&new_note) + .item(&quick_open) + .separator() + .item(&save) + .separator() + .item(&close_tab) + .build()?) +} + +fn build_edit_menu(app: &App) -> MenuResult { + Ok(SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .separator() + .select_all() + .build()?) +} + +fn build_view_menu(app: &App) -> MenuResult { + let editor_only = MenuItemBuilder::new("Editor Only") + .id(VIEW_EDITOR_ONLY) + .accelerator("CmdOrCtrl+1") + .build(app)?; + let editor_list = MenuItemBuilder::new("Editor + Notes") + .id(VIEW_EDITOR_LIST) + .accelerator("CmdOrCtrl+2") + .build(app)?; + let all_panels = MenuItemBuilder::new("All Panels") + .id(VIEW_ALL) + .accelerator("CmdOrCtrl+3") + .build(app)?; + let toggle_inspector = MenuItemBuilder::new("Toggle Inspector") + .id(VIEW_TOGGLE_INSPECTOR) + .build(app)?; + let command_palette = MenuItemBuilder::new("Command Palette") + .id(VIEW_COMMAND_PALETTE) + .accelerator("CmdOrCtrl+K") + .build(app)?; + + Ok(SubmenuBuilder::new(app, "View") + .item(&editor_only) + .item(&editor_list) + .item(&all_panels) + .separator() + .item(&toggle_inspector) + .separator() + .item(&command_palette) + .build()?) +} + +fn build_window_menu(app: &App) -> MenuResult { + Ok(SubmenuBuilder::new(app, "Window") + .minimize() + .maximize() + .separator() + .close_window() + .build()?) +} + +pub fn setup_menu(app: &App) -> Result<(), Box> { + let app_menu = build_app_menu(app)?; + let file_menu = build_file_menu(app)?; + let edit_menu = build_edit_menu(app)?; + let view_menu = build_view_menu(app)?; + let window_menu = build_window_menu(app)?; let menu = MenuBuilder::new(app) - .item(&edit_submenu) - .item(&view_submenu) + .item(&app_menu) + .item(&file_menu) + .item(&edit_menu) + .item(&view_menu) + .item(&window_menu) .build()?; app.set_menu(menu)?; app.on_menu_event(|app_handle, event| { let id = event.id().0.as_str(); - if id.starts_with("view-") { + if CUSTOM_IDS.contains(&id) { let _ = app_handle.emit("menu-event", id); } }); Ok(()) } + +/// Enable or disable menu items that depend on having an active note tab. +pub fn set_note_items_enabled(app_handle: &AppHandle, enabled: bool) { + let Some(menu) = app_handle.menu() else { + return; + }; + for id in NOTE_DEPENDENT_IDS { + if let Some(MenuItemKind::MenuItem(mi)) = menu.get(*id) { + let _ = mi.set_enabled(enabled); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn custom_ids_include_all_expected_items() { + let expected = [ + "app-settings", + "file-new-note", + "file-quick-open", + "file-save", + "file-close-tab", + "view-editor-only", + "view-editor-list", + "view-all", + "view-toggle-inspector", + "view-command-palette", + ]; + for id in &expected { + assert!(CUSTOM_IDS.contains(id), "missing custom ID: {id}"); + } + } + + #[test] + fn note_dependent_ids_are_subset_of_custom_ids() { + for id in NOTE_DEPENDENT_IDS { + assert!( + CUSTOM_IDS.contains(id), + "note-dependent ID {id} not in CUSTOM_IDS" + ); + } + } +} diff --git a/src/components/SearchPanel.test.tsx b/src/components/SearchPanel.test.tsx index 7e07bacb..1f9f2138 100644 --- a/src/components/SearchPanel.test.tsx +++ b/src/components/SearchPanel.test.tsx @@ -201,7 +201,7 @@ describe('SearchPanel', () => { expect(screen.getByText('How to Design AI-first APIs')).toBeInTheDocument() }) - // Wait for the results state to propagate to the keydown handler (useEffect re-run) + // Wait for the effect to re-register with new results before firing Enter fireEvent.keyDown(window, { key: 'Enter' }) await waitFor(() => { diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts index 83d285b1..0c5286ae 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -2,6 +2,7 @@ import { useAppKeyboard } from './useAppKeyboard' import { useCommandRegistry } from './useCommandRegistry' import type { CommandAction } from './useCommandRegistry' import { useKeyboardNavigation } from './useKeyboardNavigation' +import { useMenuEvents } from './useMenuEvents' import type { SidebarSelection, VaultEntry } from '../types' import type { ViewMode } from './useViewMode' @@ -39,7 +40,7 @@ interface AppCommandsConfig { canGoForward?: boolean } -/** Sets up keyboard shortcuts, command registry, and keyboard navigation. */ +/** Sets up keyboard shortcuts, command registry, menu events, and keyboard navigation. */ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { useAppKeyboard({ onQuickOpen: config.onQuickOpen, @@ -57,6 +58,19 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { handleCloseTabRef: config.handleCloseTabRef, }) + useMenuEvents({ + onSetViewMode: config.onSetViewMode, + onCreateNote: config.onCreateNote, + onQuickOpen: config.onQuickOpen, + onSave: config.onSave, + onOpenSettings: config.onOpenSettings, + onToggleInspector: config.onToggleInspector, + onCommandPalette: config.onCommandPalette, + activeTabPathRef: config.activeTabPathRef, + handleCloseTabRef: config.handleCloseTabRef, + activeTabPath: config.activeTabPath, + }) + const commands = useCommandRegistry({ activeTabPath: config.activeTabPath, entries: config.entries, diff --git a/src/hooks/useMenuEvents.test.ts b/src/hooks/useMenuEvents.test.ts new file mode 100644 index 00000000..a520d4a7 --- /dev/null +++ b/src/hooks/useMenuEvents.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, vi } from 'vitest' +import { dispatchMenuEvent, type MenuEventHandlers } from './useMenuEvents' + +function makeHandlers(): MenuEventHandlers { + return { + onSetViewMode: vi.fn(), + onCreateNote: vi.fn(), + onQuickOpen: vi.fn(), + onSave: vi.fn(), + onOpenSettings: vi.fn(), + onToggleInspector: vi.fn(), + onCommandPalette: vi.fn(), + activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject, + handleCloseTabRef: { current: vi.fn() } as React.MutableRefObject<(path: string) => void>, + activeTabPath: '/vault/test.md', + } +} + +describe('dispatchMenuEvent', () => { + it('view-editor-only sets editor-only mode', () => { + const h = makeHandlers() + dispatchMenuEvent('view-editor-only', h) + expect(h.onSetViewMode).toHaveBeenCalledWith('editor-only') + }) + + it('view-editor-list sets editor-list mode', () => { + const h = makeHandlers() + dispatchMenuEvent('view-editor-list', h) + expect(h.onSetViewMode).toHaveBeenCalledWith('editor-list') + }) + + it('view-all sets all mode', () => { + const h = makeHandlers() + dispatchMenuEvent('view-all', h) + expect(h.onSetViewMode).toHaveBeenCalledWith('all') + }) + + it('file-new-note triggers create note', () => { + const h = makeHandlers() + dispatchMenuEvent('file-new-note', h) + expect(h.onCreateNote).toHaveBeenCalled() + }) + + it('file-quick-open triggers quick open', () => { + const h = makeHandlers() + dispatchMenuEvent('file-quick-open', h) + expect(h.onQuickOpen).toHaveBeenCalled() + }) + + it('file-save triggers save', () => { + const h = makeHandlers() + dispatchMenuEvent('file-save', h) + expect(h.onSave).toHaveBeenCalled() + }) + + it('file-close-tab closes the active tab', () => { + const h = makeHandlers() + dispatchMenuEvent('file-close-tab', h) + expect(h.handleCloseTabRef.current).toHaveBeenCalledWith('/vault/test.md') + }) + + it('file-close-tab does nothing when no active tab', () => { + const h = makeHandlers() + h.activeTabPathRef = { current: null } + dispatchMenuEvent('file-close-tab', h) + expect(h.handleCloseTabRef.current).not.toHaveBeenCalled() + }) + + it('app-settings triggers open settings', () => { + const h = makeHandlers() + dispatchMenuEvent('app-settings', h) + expect(h.onOpenSettings).toHaveBeenCalled() + }) + + it('view-toggle-inspector triggers toggle inspector', () => { + const h = makeHandlers() + dispatchMenuEvent('view-toggle-inspector', h) + expect(h.onToggleInspector).toHaveBeenCalled() + }) + + it('view-command-palette triggers command palette', () => { + const h = makeHandlers() + dispatchMenuEvent('view-command-palette', h) + expect(h.onCommandPalette).toHaveBeenCalled() + }) + + it('unknown event ID does nothing', () => { + const h = makeHandlers() + dispatchMenuEvent('unknown-event', h) + expect(h.onSetViewMode).not.toHaveBeenCalled() + expect(h.onCreateNote).not.toHaveBeenCalled() + expect(h.onSave).not.toHaveBeenCalled() + }) +}) diff --git a/src/hooks/useMenuEvents.ts b/src/hooks/useMenuEvents.ts new file mode 100644 index 00000000..af973aa2 --- /dev/null +++ b/src/hooks/useMenuEvents.ts @@ -0,0 +1,71 @@ +import { useEffect, useRef } from 'react' +import { isTauri } from '../mock-tauri' +import type { ViewMode } from './useViewMode' + +export interface MenuEventHandlers { + onSetViewMode: (mode: ViewMode) => void + onCreateNote: () => void + onQuickOpen: () => void + onSave: () => void + onOpenSettings: () => void + onToggleInspector: () => void + onCommandPalette: () => void + activeTabPathRef: React.MutableRefObject + handleCloseTabRef: React.MutableRefObject<(path: string) => void> + activeTabPath: string | null +} + +const VIEW_MODE_MAP: Record = { + 'view-editor-only': 'editor-only', + 'view-editor-list': 'editor-list', + 'view-all': 'all', +} + +/** Dispatch a Tauri menu event ID to the matching handler. Exported for testing. */ +export function dispatchMenuEvent(id: string, h: MenuEventHandlers): void { + const viewMode = VIEW_MODE_MAP[id] + if (viewMode) { h.onSetViewMode(viewMode); return } + + switch (id) { + case 'file-new-note': h.onCreateNote(); break + case 'file-quick-open': h.onQuickOpen(); break + case 'file-save': h.onSave(); break + case 'file-close-tab': { + const path = h.activeTabPathRef.current + if (path) h.handleCloseTabRef.current(path) + break + } + case 'app-settings': h.onOpenSettings(); break + case 'view-toggle-inspector': h.onToggleInspector(); break + case 'view-command-palette': h.onCommandPalette(); break + } +} + +/** Listen for native macOS menu events and dispatch them to the appropriate handlers. */ +export function useMenuEvents(handlers: MenuEventHandlers) { + const ref = useRef(handlers) + ref.current = handlers + + // Subscribe once to Tauri menu events + useEffect(() => { + if (!isTauri()) return + + let cleanup: (() => void) | undefined + import('@tauri-apps/api/event').then(({ listen }) => { + const unlisten = listen('menu-event', (event) => { + dispatchMenuEvent(event.payload, ref.current) + }) + cleanup = () => { unlisten.then(fn => fn()) } + }).catch(() => { /* not in Tauri */ }) + + return () => cleanup?.() + }, []) + + // Sync menu item enabled state when active tab changes + useEffect(() => { + if (!isTauri()) return + import('@tauri-apps/api/core').then(({ invoke }) => { + invoke('update_menu_state', { hasActiveNote: handlers.activeTabPath !== null }) + }).catch(() => {}) + }, [handlers.activeTabPath]) +} diff --git a/src/hooks/useViewMode.ts b/src/hooks/useViewMode.ts index b3f27d19..cb68fec0 100644 --- a/src/hooks/useViewMode.ts +++ b/src/hooks/useViewMode.ts @@ -1,5 +1,4 @@ -import { useState, useCallback, useEffect } from 'react' -import { isTauri } from '../mock-tauri' +import { useState, useCallback } from 'react' export type ViewMode = 'editor-only' | 'editor-list' | 'all' @@ -24,22 +23,5 @@ export function useViewMode() { const sidebarVisible = viewMode === 'all' const noteListVisible = viewMode === 'all' || viewMode === 'editor-list' - // Listen for Tauri menu events - useEffect(() => { - if (!isTauri()) return - - let cleanup: (() => void) | undefined - import('@tauri-apps/api/event').then(({ listen }) => { - const unlisten = listen('menu-event', (event) => { - if (event.payload === 'view-editor-only') setViewMode('editor-only') - else if (event.payload === 'view-editor-list') setViewMode('editor-list') - else if (event.payload === 'view-all') setViewMode('all') - }) - cleanup = () => { unlisten.then((fn) => fn()) } - }).catch(() => { /* not in Tauri */ }) - - return () => cleanup?.() - }, [setViewMode]) - return { viewMode, setViewMode, sidebarVisible, noteListVisible } }