diff --git a/src/App.tsx b/src/App.tsx
index 08a3e294..5e73b355 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1237,6 +1237,7 @@ function App() {
// Raw-toggle ref: Editor registers its handleToggleRaw here so the command palette can call it
const rawToggleRef = useRef<() => void>(() => {})
+ const tableOfContentsToggleRef = useRef<() => void>(() => {})
// Diff-toggle ref: Editor registers its handleToggleDiff here so the command palette can call it
const diffToggleRef = useRef<() => void>(() => {})
const findInNoteRef = useRef<((options?: { replace?: boolean }) => void) | null>(null)
@@ -1441,6 +1442,9 @@ function App() {
() => canToggleRichEditor ? () => rawToggleRef.current() : undefined,
[canToggleRichEditor],
)
+ const toggleTableOfContentsCommand = useCallback(() => {
+ if (notes.activeTabPath) tableOfContentsToggleRef.current()
+ }, [notes.activeTabPath])
const findInNoteCommand = useCallback(() => {
findInNoteRef.current?.({ replace: false })
}, [])
@@ -1558,6 +1562,7 @@ function App() {
onToggleInspector: handleToggleInspector,
onToggleDiff: toggleDiffCommand,
onToggleRawEditor: toggleRawEditorCommand,
+ onToggleTableOfContents: toggleTableOfContentsCommand,
noteWidth: activeNoteWidth,
defaultNoteWidth,
onSetNoteWidth: handleSetActiveNoteWidth,
@@ -1785,6 +1790,7 @@ function App() {
noteWidth={activeNoteWidth}
onToggleNoteWidth={handleToggleNoteWidth}
rawToggleRef={rawToggleRef}
+ tableOfContentsToggleRef={tableOfContentsToggleRef}
findInNoteRef={findInNoteRef}
diffToggleRef={diffToggleRef}
canGoBack={canGoBack}
diff --git a/src/components/BreadcrumbBar.test.tsx b/src/components/BreadcrumbBar.test.tsx
index 539b96f0..c72ff8ac 100644
--- a/src/components/BreadcrumbBar.test.tsx
+++ b/src/components/BreadcrumbBar.test.tsx
@@ -562,6 +562,15 @@ describe('BreadcrumbBar — table of contents toggle', () => {
expect(screen.getByRole('button', { name: 'Close table of contents' })).toBeInTheDocument()
})
+ it('shows the table of contents shortcut in the button tooltip', async () => {
+ render()
+ await expectTooltip(
+ screen.getByRole('button', { name: 'Open table of contents' }),
+ 'Open table of contents',
+ formatShortcutDisplay({ display: '⌘⇧T' }),
+ )
+ })
+
it('offers the table of contents action from the overflow menu', async () => {
const onToggleTableOfContents = vi.fn()
const restoreMeasurement = mockCollapsedBreadcrumbOverflow()
diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx
index 51b50998..6495f760 100644
--- a/src/components/BreadcrumbBar.tsx
+++ b/src/components/BreadcrumbBar.tsx
@@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
import type { NoteWidthMode, VaultEntry } from '../types'
import { cn } from '@/lib/utils'
import { translate, type AppLocale } from '../lib/i18n'
-import { formatShortcutDisplay } from '../hooks/appCommandCatalog'
+import { APP_COMMAND_IDS, formatShortcutDisplay, getAppCommandShortcutDisplay } from '../hooks/appCommandCatalog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ActionTooltip, type ActionTooltipCopy } from '@/components/ui/action-tooltip'
@@ -353,7 +353,10 @@ function TableOfContentsAction({
return (
diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx
index d4688991..761ec4bd 100644
--- a/src/components/Editor.tsx
+++ b/src/components/Editor.tsx
@@ -103,6 +103,8 @@ interface EditorProps {
findInNoteRef?: React.MutableRefObject<((options?: { replace?: boolean }) => void) | null>
/** Mutable ref that Editor registers its diff-mode toggle into, for command palette access. */
diffToggleRef?: React.MutableRefObject<() => void>
+ /** Mutable ref that Editor registers its table-of-contents toggle into, for app shortcuts and menus. */
+ tableOfContentsToggleRef?: React.MutableRefObject<() => void>
onFileCreated?: (relativePath: string) => void
onFileModified?: (relativePath: string) => void
onVaultChanged?: () => void
@@ -601,6 +603,12 @@ export const Editor = memo(function Editor(props: EditorProps) {
flushPendingRawContentRef: props.flushPendingRawContentRef,
})
const rightPanel = useRightPanelExclusion(props)
+ const { tableOfContentsToggleRef } = props
+ useEffect(() => {
+ if (tableOfContentsToggleRef) {
+ tableOfContentsToggleRef.current = rightPanel.handleToggleTableOfContents
+ }
+ }, [tableOfContentsToggleRef, rightPanel.handleToggleTableOfContents])
return (
{
fireEvent.click(screen.getByRole('button', { name: /What causes teams to degrade/ }))
expect(setTextCursorPosition).toHaveBeenCalledWith('h2', 'start')
})
+
+ it('shows note info at the bottom of the table of contents', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText('Info')).toBeInTheDocument()
+ expect(screen.getByText('Words')).toBeInTheDocument()
+ expect(screen.getByText('3')).toBeInTheDocument()
+ expect(screen.getByText('2.0 KB')).toBeInTheDocument()
+ })
})
diff --git a/src/components/TableOfContentsPanel.tsx b/src/components/TableOfContentsPanel.tsx
index e0f38530..7e3be687 100644
--- a/src/components/TableOfContentsPanel.tsx
+++ b/src/components/TableOfContentsPanel.tsx
@@ -16,6 +16,7 @@ import {
getFolderConnectorLeft,
getFolderDepthIndent,
} from './folder-tree/folderTreeLayout'
+import { NoteInfoPanel } from './inspector/NoteInfoPanel'
interface TableOfContentsEditor {
document?: unknown[]
@@ -283,6 +284,11 @@ export const TableOfContentsPanel = memo(function TableOfContentsPanel({
onNavigate={navigateToItem}
/>
+ {entry && (
+
+
+
+ )}
)
})
diff --git a/src/hooks/appCommandCatalog.ts b/src/hooks/appCommandCatalog.ts
index 11cdb7b5..476b8c28 100644
--- a/src/hooks/appCommandCatalog.ts
+++ b/src/hooks/appCommandCatalog.ts
@@ -45,6 +45,7 @@ type SimpleHandlerKey =
| 'onToggleDiff'
| 'onToggleInspector'
| 'onToggleAIChat'
+ | 'onToggleTableOfContents'
| 'onCommandPalette'
| 'onZoomIn'
| 'onZoomOut'
diff --git a/src/hooks/appCommandDispatcher.test.ts b/src/hooks/appCommandDispatcher.test.ts
index 12b61d03..9903803e 100644
--- a/src/hooks/appCommandDispatcher.test.ts
+++ b/src/hooks/appCommandDispatcher.test.ts
@@ -48,6 +48,7 @@ function makeHandlers(): AppCommandHandlers {
onToggleRawEditor: vi.fn(),
onToggleDiff: vi.fn(),
onToggleAIChat: vi.fn(),
+ onToggleTableOfContents: vi.fn(),
onPastePlainText: vi.fn(),
onGoBack: vi.fn(),
onGoForward: vi.fn(),
@@ -134,6 +135,7 @@ describe('appCommandDispatcher', () => {
expect(findShortcutCommandId('command-or-ctrl', 'o', 'KeyO')).toBe(APP_COMMAND_IDS.fileQuickOpen)
expect(findShortcutCommandId('command-or-ctrl', '\\')).toBe(APP_COMMAND_IDS.editToggleRawEditor)
expect(findShortcutCommandId('command-or-ctrl-shift', '¬', 'KeyL')).toBe(APP_COMMAND_IDS.viewToggleAiChat)
+ expect(findShortcutCommandId('command-or-ctrl-shift', 'T', 'KeyT')).toBe(APP_COMMAND_IDS.viewToggleTableOfContents)
expect(findShortcutCommandId('command-or-ctrl-shift', 'v', 'KeyV')).toBe(APP_COMMAND_IDS.editPastePlainText)
})
@@ -196,6 +198,7 @@ describe('appCommandDispatcher', () => {
expectShortcutEventCommand({ key: 'ArrowLeft', code: 'ArrowLeft', metaKey: true }, APP_COMMAND_IDS.viewGoBack)
expectShortcutEventCommand({ key: 'ArrowRight', code: 'ArrowRight', metaKey: true }, APP_COMMAND_IDS.viewGoForward)
expectShortcutEventCommand({ key: 'l', code: 'KeyL', ctrlKey: true, shiftKey: true }, APP_COMMAND_IDS.viewToggleAiChat)
+ expectShortcutEventCommand({ key: 'T', code: 'KeyT', metaKey: true, shiftKey: true }, APP_COMMAND_IDS.viewToggleTableOfContents)
expectShortcutEventCommand({ key: 'V', code: 'KeyV', metaKey: true, shiftKey: true }, APP_COMMAND_IDS.editPastePlainText)
})
@@ -227,6 +230,12 @@ describe('appCommandDispatcher', () => {
expect(handlers.onToggleAIChat).toHaveBeenCalled()
})
+ it('dispatches table of contents toggle through the shared command path', () => {
+ const handlers = makeHandlers()
+ expect(dispatchAppCommand(APP_COMMAND_IDS.viewToggleTableOfContents, handlers)).toBe(true)
+ expect(handlers.onToggleTableOfContents).toHaveBeenCalled()
+ })
+
it('dispatches plain-text paste through the shared command path', () => {
const handlers = makeHandlers()
expect(dispatchAppCommand(APP_COMMAND_IDS.editPastePlainText, handlers)).toBe(true)
diff --git a/src/hooks/appCommandDispatcher.ts b/src/hooks/appCommandDispatcher.ts
index a4077504..44ad9e38 100644
--- a/src/hooks/appCommandDispatcher.ts
+++ b/src/hooks/appCommandDispatcher.ts
@@ -50,6 +50,7 @@ export interface AppCommandHandlers {
onToggleRawEditor?: () => void
onToggleDiff?: () => void
onToggleAIChat?: () => void
+ onToggleTableOfContents?: () => void
onGoBack?: () => void
onGoForward?: () => void
onCheckForUpdates?: () => void
@@ -87,6 +88,7 @@ type SimpleHandlerKey = keyof Pick<
| 'onToggleDiff'
| 'onToggleInspector'
| 'onToggleAIChat'
+ | 'onToggleTableOfContents'
| 'onCommandPalette'
| 'onZoomIn'
| 'onZoomOut'
@@ -131,6 +133,7 @@ const SIMPLE_HANDLER_EXECUTORS: readonly [SimpleHandlerKey, SimpleHandlerExecuto
['onToggleDiff', (handlers) => handlers.onToggleDiff?.()],
['onToggleInspector', (handlers) => handlers.onToggleInspector()],
['onToggleAIChat', (handlers) => handlers.onToggleAIChat?.()],
+ ['onToggleTableOfContents', (handlers) => handlers.onToggleTableOfContents?.()],
['onCommandPalette', (handlers) => handlers.onCommandPalette()],
['onZoomIn', (handlers) => handlers.onZoomIn()],
['onZoomOut', (handlers) => handlers.onZoomOut()],
diff --git a/src/hooks/appKeyboardShortcuts.ts b/src/hooks/appKeyboardShortcuts.ts
index 4290fbfa..aa7f4cf5 100644
--- a/src/hooks/appKeyboardShortcuts.ts
+++ b/src/hooks/appKeyboardShortcuts.ts
@@ -28,6 +28,7 @@ export type KeyboardActions = Pick<
| 'onGoBack'
| 'onGoForward'
| 'onToggleAIChat'
+ | 'onToggleTableOfContents'
| 'onToggleRawEditor'
| 'onToggleInspector'
| 'onToggleFavorite'
diff --git a/src/hooks/commands/viewCommands.ts b/src/hooks/commands/viewCommands.ts
index 971234ab..11e120a4 100644
--- a/src/hooks/commands/viewCommands.ts
+++ b/src/hooks/commands/viewCommands.ts
@@ -29,6 +29,7 @@ interface ViewCommandsConfig {
onSetNoteWidth?: (mode: NoteWidthMode) => void
onSetDefaultNoteWidth?: (mode: NoteWidthMode) => void
onToggleAIChat?: () => void
+ onToggleTableOfContents?: () => void
zoomLevel: number
onZoomIn: () => void
onZoomOut: () => void
@@ -92,12 +93,27 @@ function buildMoveSavedViewCommand(
}
}
+function buildToggleTableOfContentsCommand(
+ hasActiveNote: boolean,
+ onToggleTableOfContents?: () => void,
+): CommandAction {
+ return {
+ id: 'toggle-table-of-contents',
+ label: 'Toggle Table of Contents',
+ group: 'View',
+ shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewToggleTableOfContents),
+ keywords: ['toc', 'outline', 'headings', 'contents', 'panel'],
+ enabled: hasActiveNote && !!onToggleTableOfContents,
+ execute: () => onToggleTableOfContents?.(),
+ }
+}
+
export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
const {
hasActiveNote, activeNoteModified,
onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor,
noteWidth = DEFAULT_NOTE_WIDTH_MODE, defaultNoteWidth = DEFAULT_NOTE_WIDTH_MODE,
- onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat,
+ onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onToggleTableOfContents,
zoomLevel, onZoomIn, onZoomOut, onZoomReset,
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
selectedViewName, onMoveSelectedViewUp, onMoveSelectedViewDown,
@@ -116,6 +132,7 @@ export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
buildSetDefaultNoteWidthCommand('normal', defaultNoteWidth, onSetDefaultNoteWidth),
buildSetDefaultNoteWidthCommand('wide', defaultNoteWidth, onSetDefaultNoteWidth),
{ id: 'toggle-ai-panel', label: 'Toggle AI Panel', group: 'View', shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewToggleAiChat), keywords: ['ai', 'agent', 'chat', 'assistant', 'contextual'], enabled: true, execute: () => onToggleAIChat?.() },
+ buildToggleTableOfContentsCommand(hasActiveNote, onToggleTableOfContents),
{ id: 'new-ai-chat', label: 'New AI chat', group: 'View', keywords: ['ai', 'agent', 'chat', 'assistant', 'new', 'fresh', 'conversation', 'reset'], enabled: true, execute: requestNewAiChat },
{ id: 'toggle-backlinks', label: 'Toggle Backlinks', group: 'View', keywords: ['backlinks', 'references', 'links', 'mentions', 'incoming'], enabled: hasActiveNote, execute: onToggleInspector },
buildMoveSavedViewCommand('Up', selectedViewName, onMoveSelectedViewUp, canMoveSelectedViewUp),
diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts
index 4f89d731..2c8111df 100644
--- a/src/hooks/useAppCommands.ts
+++ b/src/hooks/useAppCommands.ts
@@ -75,6 +75,7 @@ interface AppCommandsConfig {
onInitializeGit?: () => void
onCreateType?: () => void
onToggleAIChat?: () => void
+ onToggleTableOfContents?: () => void
onCheckForUpdates?: () => void
onRemoveActiveVault?: () => void
onRestoreGettingStarted?: () => void
@@ -176,6 +177,7 @@ type CommandRegistryCoreActions = Pick<
| 'onSetNoteWidth'
| 'onSetDefaultNoteWidth'
| 'onToggleAIChat'
+ | 'onToggleTableOfContents'
>
type CommandRegistryVaultActions = Pick<
CommandRegistryConfig,
@@ -256,6 +258,7 @@ function createKeyboardActions(
onGoBack: config.onGoBack,
onGoForward: config.onGoForward,
onToggleAIChat: config.onToggleAIChat,
+ onToggleTableOfContents: config.onToggleTableOfContents,
onToggleRawEditor: config.onToggleRawEditor,
onToggleInspector: config.onToggleInspector,
onToggleFavorite: config.onToggleFavorite,
@@ -302,6 +305,7 @@ function createMenuEventActionHandlers(
| 'onToggleRawEditor'
| 'onToggleDiff'
| 'onToggleAIChat'
+ | 'onToggleTableOfContents'
| 'onToggleOrganized'
| 'onGoBack'
| 'onGoForward'
@@ -328,6 +332,7 @@ function createMenuEventActionHandlers(
onToggleRawEditor: config.onToggleRawEditor,
onToggleDiff: config.onToggleDiff,
onToggleAIChat: config.onToggleAIChat,
+ onToggleTableOfContents: config.onToggleTableOfContents,
onToggleOrganized: config.onToggleOrganized,
onGoBack: config.onGoBack,
onGoForward: config.onGoForward,
@@ -452,6 +457,7 @@ function createCommandRegistryCoreConfig(
onSetNoteWidth: config.onSetNoteWidth,
onSetDefaultNoteWidth: config.onSetDefaultNoteWidth,
onToggleAIChat: config.onToggleAIChat,
+ onToggleTableOfContents: config.onToggleTableOfContents,
}
}
diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts
index 0c592fd3..d6b353de 100644
--- a/src/hooks/useCommandRegistry.ts
+++ b/src/hooks/useCommandRegistry.ts
@@ -98,6 +98,7 @@ interface CommandRegistryConfig {
onSetNoteWidth?: (mode: NoteWidthMode) => void
onSetDefaultNoteWidth?: (mode: NoteWidthMode) => void
onToggleAIChat?: () => void
+ onToggleTableOfContents?: () => void
activeNoteModified: boolean
onCheckForUpdates?: () => void
onZoomIn: () => void
@@ -129,7 +130,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
onQuickOpen, onCreateNote, onCreateNoteOfType, onSave, onPastePlainText, onOpenSettings, onOpenFeedback,
onDeleteNote, onArchiveNote, onUnarchiveNote,
onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onFindInNote, onReplaceInNote,
- noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onOpenVault, onCreateEmptyVault,
+ noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onToggleTableOfContents, onOpenVault, onCreateEmptyVault,
selectedViewName, onMoveSelectedViewUp, onMoveSelectedViewDown, canMoveSelectedViewUp, canMoveSelectedViewDown,
activeNoteModified,
onZoomIn, onZoomOut, onZoomReset, zoomLevel,
@@ -224,12 +225,12 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
const viewCommands = useMemo(() => buildViewCommands({
hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector,
- onToggleDiff, onToggleRawEditor, noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, zoomLevel, onZoomIn, onZoomOut, onZoomReset,
+ onToggleDiff, onToggleRawEditor, noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onToggleTableOfContents, zoomLevel, onZoomIn, onZoomOut, onZoomReset,
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
selectedViewName, onMoveSelectedViewUp, onMoveSelectedViewDown, canMoveSelectedViewUp, canMoveSelectedViewDown,
}), [
hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector,
- onToggleDiff, onToggleRawEditor, noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat,
+ onToggleDiff, onToggleRawEditor, noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onToggleTableOfContents,
zoomLevel, onZoomIn, onZoomOut, onZoomReset,
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
selectedViewName, onMoveSelectedViewUp, onMoveSelectedViewDown, canMoveSelectedViewUp, canMoveSelectedViewDown,
diff --git a/src/shared/appCommandManifest.json b/src/shared/appCommandManifest.json
index 4aed67e1..239f9ffe 100644
--- a/src/shared/appCommandManifest.json
+++ b/src/shared/appCommandManifest.json
@@ -182,6 +182,19 @@
"requiresManualNativeAcceleratorQa": true
}
},
+ "viewToggleTableOfContents": {
+ "id": "view-toggle-table-of-contents",
+ "route": { "kind": "handler", "handler": "onToggleTableOfContents" },
+ "menuOwned": true,
+ "shortcut": {
+ "combo": "command-or-ctrl-shift",
+ "key": "t",
+ "code": "KeyT",
+ "display": "⌘⇧T",
+ "accelerator": "CmdOrCtrl+Shift+T",
+ "requiresManualNativeAcceleratorQa": true
+ }
+ },
"viewToggleBacklinks": {
"id": "view-toggle-backlinks",
"route": { "kind": "handler", "handler": "onToggleInspector" },
@@ -474,6 +487,7 @@
{ "kind": "command", "command": "noteOpenInNewWindow", "label": "Open in New Window" },
{ "kind": "separator" },
{ "kind": "command", "command": "editToggleRawEditor", "label": "Toggle Raw Editor" },
+ { "kind": "command", "command": "viewToggleTableOfContents", "label": "Toggle Table of Contents" },
{ "kind": "command", "command": "viewToggleAiChat", "label": "Toggle AI Panel" },
{ "kind": "command", "command": "viewToggleBacklinks", "label": "Toggle Backlinks" }
]
@@ -510,6 +524,7 @@
{ "command": "noteDelete" },
{ "command": "editToggleRawEditor" },
{ "command": "editToggleDiff" },
+ { "command": "viewToggleTableOfContents" },
{ "command": "viewToggleBacklinks" },
{ "command": "noteOpenInNewWindow" }
],