+
{isLoadingNewTab && showEditor && }
)
}
return (
-
+
onRenameFilename?: (path: string, newFilenameStem: string) => void
+ noteLayout?: NoteLayout
+ onToggleNoteLayout?: () => void
isConflicted?: boolean
onKeepMine?: (path: string) => void
onKeepTheirs?: (path: string) => void
diff --git a/src/hooks/commands/viewCommands.ts b/src/hooks/commands/viewCommands.ts
index 8e6dbdd1..c6dcb79e 100644
--- a/src/hooks/commands/viewCommands.ts
+++ b/src/hooks/commands/viewCommands.ts
@@ -1,8 +1,16 @@
import { APP_COMMAND_IDS, getAppCommandShortcutDisplay } from '../appCommandCatalog'
import type { CommandAction } from './types'
import type { ViewMode } from '../useViewMode'
+import type { NoteLayout } from '../../types'
import { requestNewAiChat } from '../../utils/aiPromptBridge'
+const NOTE_LAYOUT_COMMAND_LABELS: Record = {
+ centered: 'Use Left-Aligned Note Layout',
+ left: 'Use Centered Note Layout',
+}
+
+const noop = () => {}
+
interface ViewCommandsConfig {
hasActiveNote: boolean
activeNoteModified: boolean
@@ -10,6 +18,8 @@ interface ViewCommandsConfig {
onToggleInspector: () => void
onToggleDiff?: () => void
onToggleRawEditor?: () => void
+ noteLayout?: NoteLayout
+ onToggleNoteLayout?: () => void
onToggleAIChat?: () => void
zoomLevel: number
onZoomIn: () => void
@@ -20,10 +30,21 @@ interface ViewCommandsConfig {
noteListColumnsLabel: string
}
+function buildNoteLayoutCommand(noteLayout: NoteLayout, onToggleNoteLayout?: () => void): CommandAction {
+ return {
+ id: 'toggle-note-layout',
+ label: NOTE_LAYOUT_COMMAND_LABELS[noteLayout],
+ group: 'View',
+ keywords: ['layout', 'note', 'column', 'wide', 'left', 'centered', 'reading'],
+ enabled: Boolean(onToggleNoteLayout),
+ execute: onToggleNoteLayout ?? noop,
+ }
+}
+
export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
const {
hasActiveNote, activeNoteModified,
- onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat,
+ onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, noteLayout = 'centered', onToggleNoteLayout, onToggleAIChat,
zoomLevel, onZoomIn, onZoomOut, onZoomReset,
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
} = config
@@ -35,6 +56,7 @@ export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
{ id: 'toggle-inspector', label: 'Toggle Properties Panel', group: 'View', shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewToggleProperties), keywords: ['properties', 'inspector', 'panel', 'right', 'sidebar'], enabled: true, execute: onToggleInspector },
{ id: 'toggle-diff', label: 'Toggle Diff Mode', group: 'View', keywords: ['diff', 'changes', 'git', 'compare', 'version'], enabled: hasActiveNote && activeNoteModified, execute: () => onToggleDiff?.() },
{ id: 'toggle-raw-editor', label: 'Toggle Raw Editor', group: 'View', keywords: ['raw', 'source', 'markdown', 'frontmatter', 'code', 'textarea'], enabled: hasActiveNote && !!onToggleRawEditor, execute: () => onToggleRawEditor?.() },
+ buildNoteLayoutCommand(noteLayout, onToggleNoteLayout),
{ 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?.() },
{ 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 },
diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts
index 85d17dd0..7f8802b3 100644
--- a/src/hooks/useAppCommands.ts
+++ b/src/hooks/useAppCommands.ts
@@ -6,7 +6,7 @@ import { useCommandRegistry } from './useCommandRegistry'
import type { CommandAction } from './useCommandRegistry'
import { useKeyboardNavigation } from './useKeyboardNavigation'
import { useMenuEvents } from './useMenuEvents'
-import type { SidebarSelection, SidebarFilter, VaultEntry } from '../types'
+import type { NoteLayout, SidebarSelection, SidebarFilter, VaultEntry } from '../types'
import { requestAddRemote } from '../utils/addRemoteEvents'
import type { NoteListFilter } from '../utils/noteListHelpers'
import type { ViewMode } from './useViewMode'
@@ -38,6 +38,8 @@ interface AppCommandsConfig {
onToggleInspector: () => void
onToggleDiff?: () => void
onToggleRawEditor?: () => void
+ noteLayout?: NoteLayout
+ onToggleNoteLayout?: () => void
activeNoteModified: boolean
onZoomIn: () => void
onZoomOut: () => void
@@ -135,6 +137,8 @@ type CommandRegistryCoreActions = Pick<
| 'onToggleInspector'
| 'onToggleDiff'
| 'onToggleRawEditor'
+ | 'noteLayout'
+ | 'onToggleNoteLayout'
| 'onToggleAIChat'
>
type CommandRegistryVaultActions = Pick<
@@ -378,6 +382,8 @@ function createCommandRegistryCoreConfig(
onToggleInspector: config.onToggleInspector,
onToggleDiff: config.onToggleDiff,
onToggleRawEditor: config.onToggleRawEditor,
+ noteLayout: config.noteLayout,
+ onToggleNoteLayout: config.onToggleNoteLayout,
onToggleAIChat: config.onToggleAIChat,
}
}
diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts
index 83604cc5..9e234510 100644
--- a/src/hooks/useCommandRegistry.test.ts
+++ b/src/hooks/useCommandRegistry.test.ts
@@ -25,6 +25,8 @@ function makeConfig(overrides: Record = {}) {
onToggleInspector: vi.fn(),
onToggleDiff: vi.fn(),
onToggleRawEditor: vi.fn(),
+ noteLayout: 'centered',
+ onToggleNoteLayout: vi.fn(),
onToggleAIChat: vi.fn(),
onOpenVault: vi.fn(),
activeNoteModified: false,
@@ -260,6 +262,29 @@ describe('useCommandRegistry', () => {
expect(findCommand(result.current, 'toggle-raw-editor')?.enabled).toBe(false)
})
+ it('exposes a command palette action for the note layout preference', () => {
+ const onToggleNoteLayout = vi.fn()
+ const config = makeConfig({ noteLayout: 'centered', onToggleNoteLayout })
+ const { result } = renderHook(() => useCommandRegistry(config))
+ const cmd = findCommand(result.current, 'toggle-note-layout')
+
+ expect(cmd).toBeDefined()
+ expect(cmd!.group).toBe('View')
+ expect(cmd!.label).toBe('Use Left-Aligned Note Layout')
+ expect(cmd!.keywords).toContain('wide')
+
+ cmd!.execute()
+
+ expect(onToggleNoteLayout).toHaveBeenCalledOnce()
+ })
+
+ it('updates note layout command copy when left alignment is active', () => {
+ const config = makeConfig({ noteLayout: 'left' })
+ const { result } = renderHook(() => useCommandRegistry(config))
+
+ expect(findCommand(result.current, 'toggle-note-layout')?.label).toBe('Use Centered Note Layout')
+ })
+
it('includes a New AI chat command that opens and resets the panel session', () => {
const config = makeConfig()
const dispatchSpy = vi.spyOn(window, 'dispatchEvent')
diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts
index 4aa28d8e..9a046b4d 100644
--- a/src/hooks/useCommandRegistry.ts
+++ b/src/hooks/useCommandRegistry.ts
@@ -1,7 +1,7 @@
import { useMemo } from 'react'
import type { AiAgentId, AiAgentsStatus } from '../lib/aiAgents'
import type { VaultAiGuidanceStatus } from '../lib/vaultAiGuidance'
-import type { SidebarSelection, VaultEntry } from '../types'
+import type { NoteLayout, SidebarSelection, VaultEntry } from '../types'
import type { NoteListFilter } from '../utils/noteListHelpers'
import type { ViewMode } from './useViewMode'
import { buildNavigationCommands } from './commands/navigationCommands'
@@ -72,6 +72,8 @@ interface CommandRegistryConfig {
onToggleInspector: () => void
onToggleDiff?: () => void
onToggleRawEditor?: () => void
+ noteLayout?: NoteLayout
+ onToggleNoteLayout?: () => void
onToggleAIChat?: () => void
activeNoteModified: boolean
onCheckForUpdates?: () => void
@@ -101,7 +103,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
activeTabPath, entries, modifiedCount,
onQuickOpen, onCreateNote, onCreateNoteOfType, onSave, onOpenSettings, onOpenFeedback,
onDeleteNote, onArchiveNote, onUnarchiveNote,
- onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, onCreateEmptyVault,
+ onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, noteLayout, onToggleNoteLayout, onToggleAIChat, onOpenVault, onCreateEmptyVault,
activeNoteModified,
onZoomIn, onZoomOut, onZoomReset, zoomLevel,
onSelect, onRenameFolder, onDeleteFolder,
@@ -169,7 +171,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
}),
...buildViewCommands({
hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector,
- onToggleDiff, onToggleRawEditor, onToggleAIChat, zoomLevel, onZoomIn, onZoomOut, onZoomReset,
+ onToggleDiff, onToggleRawEditor, noteLayout, onToggleNoteLayout, onToggleAIChat, zoomLevel, onZoomIn, onZoomOut, onZoomReset,
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
}),
...buildSettingsCommands({
@@ -193,7 +195,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
hasActiveNote, activeTabPath, isArchived, modifiedCount, activeNoteModified,
onQuickOpen, onCreateNote, onCreateNoteOfType, onCreateType, onSave, onOpenSettings, onOpenFeedback,
onDeleteNote, onArchiveNote, onUnarchiveNote,
- onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, onCreateEmptyVault, config.canAddRemote, config.onAddRemote,
+ onCommitPush, onPull, onResolveConflicts, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, noteLayout, onToggleNoteLayout, onToggleAIChat, onOpenVault, onCreateEmptyVault, config.canAddRemote, config.onAddRemote,
onCheckForUpdates,
onZoomIn, onZoomOut, onZoomReset, zoomLevel,
onSelect, onRenameFolder, onDeleteFolder,
diff --git a/src/hooks/useNoteLayout.test.ts b/src/hooks/useNoteLayout.test.ts
new file mode 100644
index 00000000..52e59132
--- /dev/null
+++ b/src/hooks/useNoteLayout.test.ts
@@ -0,0 +1,51 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+import { useNoteLayout } from './useNoteLayout'
+import { bindVaultConfigStore, getVaultConfig, resetVaultConfigStore } from '../utils/vaultConfigStore'
+
+describe('useNoteLayout', () => {
+ beforeEach(() => {
+ resetVaultConfigStore()
+ bindVaultConfigStore(
+ { zoom: null, view_mode: null, editor_mode: null, note_layout: null, tag_colors: null, status_colors: null, property_display_modes: null },
+ vi.fn(),
+ )
+ })
+
+ it('defaults to centered note layout', () => {
+ const { result } = renderHook(() => useNoteLayout())
+
+ expect(result.current.noteLayout).toBe('centered')
+ })
+
+ it('loads persisted left note layout from vault config', () => {
+ resetVaultConfigStore()
+ bindVaultConfigStore(
+ { zoom: null, view_mode: null, editor_mode: null, note_layout: 'left', tag_colors: null, status_colors: null, property_display_modes: null },
+ vi.fn(),
+ )
+
+ const { result } = renderHook(() => useNoteLayout())
+
+ expect(result.current.noteLayout).toBe('left')
+ })
+
+ it('sets note layout and persists it to vault config', () => {
+ const { result } = renderHook(() => useNoteLayout())
+
+ act(() => result.current.setNoteLayout('left'))
+
+ expect(result.current.noteLayout).toBe('left')
+ expect(getVaultConfig().note_layout).toBe('left')
+ })
+
+ it('toggles between centered and left note layout', () => {
+ const { result } = renderHook(() => useNoteLayout())
+
+ act(() => result.current.toggleNoteLayout())
+ expect(result.current.noteLayout).toBe('left')
+
+ act(() => result.current.toggleNoteLayout())
+ expect(result.current.noteLayout).toBe('centered')
+ })
+})
diff --git a/src/hooks/useNoteLayout.ts b/src/hooks/useNoteLayout.ts
new file mode 100644
index 00000000..045eb753
--- /dev/null
+++ b/src/hooks/useNoteLayout.ts
@@ -0,0 +1,33 @@
+import { useCallback, useEffect, useState } from 'react'
+import type { NoteLayout } from '../types'
+import { getVaultConfig, subscribeVaultConfig, updateVaultConfigField } from '../utils/vaultConfigStore'
+
+function isNoteLayout(value: string | null | undefined): value is NoteLayout {
+ return value === 'centered' || value === 'left'
+}
+
+function loadNoteLayout(): NoteLayout {
+ const stored = getVaultConfig().note_layout
+ return isNoteLayout(stored) ? stored : 'centered'
+}
+
+export function useNoteLayout() {
+ const [noteLayout, setNoteLayoutState] = useState(loadNoteLayout)
+
+ useEffect(() => {
+ return subscribeVaultConfig(() => {
+ setNoteLayoutState(loadNoteLayout())
+ })
+ }, [])
+
+ const setNoteLayout = useCallback((layout: NoteLayout) => {
+ setNoteLayoutState(layout)
+ updateVaultConfigField('note_layout', layout)
+ }, [])
+
+ const toggleNoteLayout = useCallback(() => {
+ setNoteLayout(noteLayout === 'left' ? 'centered' : 'left')
+ }, [noteLayout, setNoteLayout])
+
+ return { noteLayout, setNoteLayout, toggleNoteLayout }
+}
diff --git a/src/types.ts b/src/types.ts
index 0c15423d..99c1b1c9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -149,11 +149,15 @@ export interface AllNotesConfig {
noteListProperties: string[] | null
}
+/** Vault-scoped UI configuration stored locally per vault path. */
+export type NoteLayout = 'centered' | 'left'
+
/** Vault-scoped UI configuration stored locally per vault path. */
export interface VaultConfig {
zoom: number | null
view_mode: string | null
editor_mode: string | null
+ note_layout?: NoteLayout | null
tag_colors: Record | null
status_colors: Record | null
property_display_modes: Record | null
diff --git a/src/utils/vaultConfigStore.ts b/src/utils/vaultConfigStore.ts
index 5def078b..abf87879 100644
--- a/src/utils/vaultConfigStore.ts
+++ b/src/utils/vaultConfigStore.ts
@@ -4,7 +4,7 @@ type SaveFn = (config: VaultConfig) => void
type Listener = () => void
const DEFAULT_CONFIG: VaultConfig = {
- zoom: null, view_mode: null, editor_mode: null,
+ zoom: null, view_mode: null, editor_mode: null, note_layout: null,
tag_colors: null, status_colors: null, property_display_modes: null,
inbox: null,
}