diff --git a/apps/web/src/components/NotebookPane.tsx b/apps/web/src/components/NotebookPane.tsx index 2a543ea7..9c7fa805 100644 --- a/apps/web/src/components/NotebookPane.tsx +++ b/apps/web/src/components/NotebookPane.tsx @@ -248,14 +248,17 @@ const SidebarCollapseButton = ({ onToggle, className, tooltipSide = "bottom", + shortcutLabel, }: { collapsed: boolean; onToggle: () => void; className?: string; tooltipSide?: "top" | "right" | "bottom" | "left"; + shortcutLabel?: string; }) => { const { t } = useTranslation(); const label = t(collapsed ? "notebookPane.expandSidebar" : "notebookPane.collapseSidebar"); + const tooltipLabel = shortcutLabel ? `${label} (${shortcutLabel})` : label; return ( @@ -275,7 +278,7 @@ const SidebarCollapseButton = ({ {collapsed ? ); @@ -512,6 +515,7 @@ export const NotebookPane = ({ isResettingDemo = false, collapsed = false, onToggleCollapsed, + collapseShortcutLabel, }: { repository: EdgeEverRepository; user: AuthUser | null; @@ -550,6 +554,7 @@ export const NotebookPane = ({ isResettingDemo?: boolean; collapsed?: boolean; onToggleCollapsed?: () => void; + collapseShortcutLabel?: string; }) => { const { t } = useTranslation(); const showDescendantNotes = useShowDescendantNotesPreference(); @@ -946,7 +951,7 @@ export const NotebookPane = ({ notice={deployedUpdateUnseen} onClick={onOpenSettings} /> - + @@ -1176,7 +1181,7 @@ export const NotebookPane = ({ {onToggleCollapsed ? ( - + ) : null} diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx index 8aca7455..8da6de73 100644 --- a/apps/web/src/components/WorkspaceApp.tsx +++ b/apps/web/src/components/WorkspaceApp.tsx @@ -71,6 +71,8 @@ import { isTextEntryTarget, getSearchShortcutScope, getShortcutActionForEvent, + formatShortcutBinding, + canToggleNotebookSidebar, getNotebookDropSortOrder, getNotebookMoveOptions, getMemoIdsNeedingMove, @@ -2810,6 +2812,19 @@ export const WorkspaceApp = ({ return; } + if (action === "toggleSidebar") { + event.preventDefault(); + if ( + event.repeat + || event.isComposing + || !canToggleNotebookSidebar({ isDesktop, focusModeActive: desktopFocusModeActive }) + ) { + return; + } + setNotebookSidebarCollapsed(!notebookSidebarCollapsed); + return; + } + if (action === "focusSearch") { event.preventDefault(); if (getSearchShortcutScope(selectedMemoId) === "memo-list") { @@ -2868,6 +2883,10 @@ export const WorkspaceApp = ({ handleGlobalSearch, handleMobileSearch, shortcutSettings, + isDesktop, + desktopFocusModeActive, + notebookSidebarCollapsed, + setNotebookSidebarCollapsed, emptyTrashConfirmationOpen, memoDeleteConfirmation, memoView, @@ -3083,6 +3102,7 @@ export const WorkspaceApp = ({ isResettingDemo={resetDemoMutation.isPending} collapsed={desktopNotebookSidebarCollapsed} onToggleCollapsed={() => setNotebookSidebarCollapsed(!notebookSidebarCollapsed)} + collapseShortcutLabel={formatShortcutBinding(shortcutSettings.toggleSidebar)} /> )} diff --git a/apps/web/src/lib/app-helpers.test.mjs b/apps/web/src/lib/app-helpers.test.mjs index 3bc0b3a6..9506a990 100644 --- a/apps/web/src/lib/app-helpers.test.mjs +++ b/apps/web/src/lib/app-helpers.test.mjs @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; import { DEFAULT_SHORTCUT_SETTINGS, DESKTOP_FOCUS_MODE_STORAGE_KEY, @@ -14,6 +15,7 @@ import { SHORTCUT_SETTINGS_STORAGE_KEY, getSearchShortcutScope, getShortcutActionForEvent, + canToggleNotebookSidebar, getNotebookSortComparator, readEditorContentAlignmentPreference, readNotebookSortPreference, @@ -445,6 +447,12 @@ describe("workspace shortcut preferences", () => { shift: true, alt: false, }); + expect(DEFAULT_SHORTCUT_SETTINGS.toggleSidebar).toEqual({ + key: "\\", + ctrlOrMeta: true, + shift: false, + alt: false, + }); }); test("migrates the unreleased reading protection shortcut without replacing custom bindings", () => { @@ -532,6 +540,21 @@ describe("workspace shortcut preferences", () => { keyboardEvent("!", { code: "Digit1", ctrlKey: true, shiftKey: true }), DEFAULT_SHORTCUT_SETTINGS, )).toBe("toggleOutline"); + expect(getShortcutActionForEvent( + keyboardEvent("\\", { metaKey: true }), + DEFAULT_SHORTCUT_SETTINGS, + )).toBe("toggleSidebar"); + }); + + test("toggles the sidebar only when it is visible", () => { + expect(canToggleNotebookSidebar({ isDesktop: true, focusModeActive: false })).toBe(true); + // Focus Mode hides the sidebar, so the shortcut must not change its saved state. + expect(canToggleNotebookSidebar({ isDesktop: true, focusModeActive: true })).toBe(false); + expect(canToggleNotebookSidebar({ isDesktop: false, focusModeActive: false })).toBe(false); + expect(canToggleNotebookSidebar({ isDesktop: false, focusModeActive: true })).toBe(false); + // The workspace checks the same flag that hides the sidebar in its layout. + const workspaceSource = readFileSync(new URL("../components/WorkspaceApp.tsx", import.meta.url), "utf8"); + expect(workspaceSource).toContain("canToggleNotebookSidebar({ isDesktop, focusModeActive: desktopFocusModeActive })"); }); }); diff --git a/apps/web/src/lib/app-helpers.ts b/apps/web/src/lib/app-helpers.ts index 153bfab4..3f6a3d9b 100644 --- a/apps/web/src/lib/app-helpers.ts +++ b/apps/web/src/lib/app-helpers.ts @@ -26,7 +26,8 @@ export type ShortcutAction = | "saveAndSync" | "toggleReadingProtection" | "toggleEditorMode" - | "toggleOutline"; + | "toggleOutline" + | "toggleSidebar"; export type ShortcutBinding = { key: string; ctrlOrMeta: boolean; @@ -190,6 +191,7 @@ export const getShortcutActionOptions = ( { value: "toggleReadingProtection", label: t("shortcuts.actions.toggleReadingProtection.label") }, { value: "toggleEditorMode", label: t("shortcuts.actions.toggleEditorMode.label") }, { value: "toggleOutline", label: t("shortcuts.actions.toggleOutline.label") }, + { value: "toggleSidebar", label: t("shortcuts.actions.toggleSidebar.label") }, ]; export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = { @@ -206,6 +208,7 @@ export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = { toggleReadingProtection: { key: "e", ctrlOrMeta: true, shift: false, alt: false }, toggleEditorMode: { key: "/", ctrlOrMeta: true, shift: false, alt: false }, toggleOutline: { key: "1", ctrlOrMeta: true, shift: true, alt: false }, + toggleSidebar: { key: "\\", ctrlOrMeta: true, shift: false, alt: false }, }; const LEGACY_READING_PROTECTION_SHORTCUT: ShortcutBinding = { @@ -233,6 +236,7 @@ const SHORTCUT_ACTION_VALUES: ShortcutAction[] = [ "toggleReadingProtection", "toggleEditorMode", "toggleOutline", + "toggleSidebar", ]; export const isDefaultMemoTitle = (title: string | null | undefined) => title?.trim() === DEFAULT_MEMO_TITLE; @@ -583,6 +587,13 @@ export const shortcutBindingsEqual = (first: ShortcutBinding, second: ShortcutBi first.shift === second.shift && first.alt === second.alt; +// The sidebar shortcut only acts when the sidebar can be seen: Focus Mode hides +// it, so toggling there would change a persisted state without any feedback. +export const canToggleNotebookSidebar = ({ isDesktop, focusModeActive }: { + isDesktop: boolean; + focusModeActive: boolean; +}) => isDesktop && !focusModeActive; + export const getShortcutActionForEvent = (event: KeyboardEvent, settings: ShortcutSettings): ShortcutAction | null => { const eventBinding = shortcutBindingFromKeyboardEvent(event); diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 1b70e904..3bdf1c36 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -1681,6 +1681,7 @@ export const enUS = { toggleReadingProtection: { label: "Toggle reading protection" }, toggleEditorMode: { label: "Switch editor mode" }, toggleOutline: { label: "Show/hide document outline" }, + toggleSidebar: { label: "Show/hide sidebar" }, }, }, quickSwitcher: { diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index e4a35334..83c7d9fe 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -1681,6 +1681,7 @@ export const ja = { toggleReadingProtection: { label: "閲覧保護を切り替え" }, toggleEditorMode: { label: "エディタモードを切り替え" }, toggleOutline: { label: "ドキュメントアウトラインの表示/非表示" }, + toggleSidebar: { label: "サイドバーの表示/非表示" }, }, }, quickSwitcher: { diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 890435fb..b95021a5 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -1681,6 +1681,7 @@ export const zhCN = { toggleReadingProtection: { label: "切换阅读保护" }, toggleEditorMode: { label: "切换编辑模式" }, toggleOutline: { label: "显示/隐藏文档大纲" }, + toggleSidebar: { label: "显示/隐藏侧边栏" }, }, }, quickSwitcher: {