feat: add note layout preference

This commit is contained in:
lucaronin
2026-04-26 04:41:18 +02:00
parent 5fd8f8fb40
commit c7edc71a97
19 changed files with 277 additions and 16 deletions

View File

@@ -38,6 +38,7 @@ import { planNewTypeCreation } from './hooks/useNoteCreation'
import { useCommitFlow } from './hooks/useCommitFlow'
import { useGitRemoteStatus } from './hooks/useGitRemoteStatus'
import { useViewMode, type ViewMode } from './hooks/useViewMode'
import { useNoteLayout } from './hooks/useNoteLayout'
import { useEntryActions } from './hooks/useEntryActions'
import { useAppCommands } from './hooks/useAppCommands'
import { triggerCommitEntryAction } from './utils/commitEntryAction'
@@ -1044,6 +1045,7 @@ function App() {
const diffToggleRef = useRef<() => void>(() => {})
const { setViewMode, sidebarVisible, noteListVisible } = useViewMode(noteWindowParams ? 'editor-only' : undefined)
const { noteLayout, toggleNoteLayout } = useNoteLayout()
const zoom = useZoom()
const buildNumber = useBuildNumber()
@@ -1297,6 +1299,8 @@ function App() {
onToggleInspector: handleToggleInspector,
onToggleDiff: toggleDiffCommand,
onToggleRawEditor: toggleRawEditorCommand,
noteLayout,
onToggleNoteLayout: toggleNoteLayout,
onZoomIn: zoom.zoomIn, onZoomOut: zoom.zoomOut, onZoomReset: zoom.zoomReset,
zoomLevel: zoom.zoomLevel,
onSelect: handleSetSelection,
@@ -1502,6 +1506,8 @@ function App() {
onContentChange={handleTrackedContentChange}
onSave={handleTrackedSave}
onRenameFilename={activeDeletedFile ? undefined : appSave.handleFilenameRename}
noteLayout={noteLayout}
onToggleNoteLayout={toggleNoteLayout}
rawToggleRef={rawToggleRef}
diffToggleRef={diffToggleRef}
canGoBack={canGoBack}

View File

@@ -306,3 +306,26 @@ describe('BreadcrumbBar — raw editor toggle', () => {
expect(screen.getByRole('button', { name: 'Open the raw editor' })).toBeInTheDocument()
})
})
describe('BreadcrumbBar — note layout toggle', () => {
it('shows the left-align layout action while centered', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} noteLayout="centered" onToggleNoteLayout={vi.fn()} />)
expect(screen.getByRole('button', { name: 'Switch to left-aligned note layout' })).toBeInTheDocument()
})
it('shows the centered layout action while left-aligned', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} noteLayout="left" onToggleNoteLayout={vi.fn()} />)
expect(screen.getByRole('button', { name: 'Switch to centered note layout' })).toBeInTheDocument()
})
it('calls onToggleNoteLayout when the layout button is clicked', () => {
const onToggleNoteLayout = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} noteLayout="centered" onToggleNoteLayout={onToggleNoteLayout} />)
fireEvent.click(screen.getByRole('button', { name: 'Switch to left-aligned note layout' }))
expect(onToggleNoteLayout).toHaveBeenCalledOnce()
})
})

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from 'react'
import type { VaultEntry } from '../types'
import type { NoteLayout, VaultEntry } from '../types'
import { cn } from '@/lib/utils'
import { formatShortcutDisplay } from '../hooks/appCommandCatalog'
import { Button } from '@/components/ui/button'
@@ -17,6 +17,8 @@ import {
Star,
CheckCircle,
ArrowsClockwise,
TextAlignCenter,
TextAlignLeft,
} from '@phosphor-icons/react'
import { NoteTitleIcon } from './NoteTitleIcon'
import { slugify } from '../hooks/useNoteCreation'
@@ -43,6 +45,8 @@ interface BreadcrumbBarProps {
onArchive?: () => void
onUnarchive?: () => void
onRenameFilename?: (path: string, newFilenameStem: string) => void
noteLayout?: NoteLayout
onToggleNoteLayout?: () => void
/** Ref for direct DOM manipulation — avoids re-render on scroll. */
barRef?: React.Ref<HTMLDivElement>
}
@@ -181,6 +185,29 @@ function RawToggleButton({ rawMode, onToggleRaw }: { rawMode?: boolean; onToggle
)
}
function NoteLayoutAction({
noteLayout = 'centered',
onToggleNoteLayout,
}: {
noteLayout?: NoteLayout
onToggleNoteLayout?: () => void
}) {
if (!onToggleNoteLayout) return null
const isLeftAligned = noteLayout === 'left'
return (
<IconActionButton
copy={{ label: isLeftAligned ? 'Switch to centered note layout' : 'Switch to left-aligned note layout' }}
onClick={onToggleNoteLayout}
className={cn(isLeftAligned ? 'text-foreground' : 'hover:text-foreground')}
>
{isLeftAligned
? <TextAlignLeft size={16} className={BREADCRUMB_ICON_CLASS} />
: <TextAlignCenter size={16} className={BREADCRUMB_ICON_CLASS} />}
</IconActionButton>
)
}
function FavoriteAction({ favorite, onToggleFavorite }: { favorite: boolean; onToggleFavorite?: () => void }) {
return (
<ToggleIconAction
@@ -498,6 +525,8 @@ function BreadcrumbActions({
rawMode,
onToggleRaw,
forceRawMode,
noteLayout,
onToggleNoteLayout,
showAIChat,
onToggleAIChat,
inspectorCollapsed,
@@ -519,6 +548,7 @@ function BreadcrumbActions({
onToggleDiff={onToggleDiff}
/>
{!forceRawMode && <RawToggleButton rawMode={rawMode} onToggleRaw={onToggleRaw} />}
<NoteLayoutAction noteLayout={noteLayout} onToggleNoteLayout={onToggleNoteLayout} />
<AIChatAction showAIChat={showAIChat} onToggleAIChat={onToggleAIChat} />
<ArchiveAction archived={entry.archived} onArchive={onArchive} onUnarchive={onUnarchive} />
<DeleteAction onDelete={onDelete} />

View File

@@ -204,6 +204,22 @@
width: 100%;
}
.editor-content-layout--left .editor-content-wrapper {
margin-left: clamp(16px, 6%, 96px);
margin-right: auto;
}
.raw-editor-codemirror {
max-width: var(--editor-max-width, 760px);
width: 100%;
margin: 0 auto;
}
.editor-content-layout--left .raw-editor-codemirror {
margin-left: clamp(16px, 6%, 96px);
margin-right: auto;
}
/* --- Note Icon Area --- */
.note-icon-area {
display: flex;
@@ -322,3 +338,11 @@
padding: 12px 0;
}
}
@container editor (max-width: 900px) {
.editor-content-layout--left .editor-content-wrapper,
.editor-content-layout--left .raw-editor-codemirror {
margin-left: auto;
margin-right: auto;
}
}

View File

@@ -5,7 +5,7 @@ import '@blocknote/mantine/style.css'
import 'katex/dist/katex.min.css'
import { uploadImageFile } from '../hooks/useImageDrop'
import { DEFAULT_AI_AGENT, type AiAgentId } from '../lib/aiAgents'
import type { VaultEntry, GitCommit, NoteStatus } from '../types'
import type { VaultEntry, GitCommit, NoteLayout, NoteStatus } from '../types'
import type { NoteListItem } from '../utils/ai-context'
import type { FrontmatterValue } from './Inspector'
import { ResizeHandle } from './ResizeHandle'
@@ -73,6 +73,8 @@ interface EditorProps {
onSave?: () => void
/** Called when the user explicitly renames the filename from the breadcrumb. */
onRenameFilename?: (path: string, newFilenameStem: string) => void
noteLayout?: NoteLayout
onToggleNoteLayout?: () => void
canGoBack?: boolean
canGoForward?: boolean
onGoBack?: () => void
@@ -299,6 +301,8 @@ function EditorLayout({
rawModeContent,
rawLatestContentRef,
onRenameFilename,
noteLayout,
onToggleNoteLayout,
isConflicted,
onKeepMine,
onKeepTheirs,
@@ -353,6 +357,8 @@ function EditorLayout({
rawModeContent: string | null
rawLatestContentRef: React.MutableRefObject<string | null>
onRenameFilename?: (path: string, newFilenameStem: string) => void
noteLayout?: NoteLayout
onToggleNoteLayout?: () => void
isConflicted?: boolean
onKeepMine?: (path: string) => void
onKeepTheirs?: (path: string) => void
@@ -412,6 +418,8 @@ function EditorLayout({
rawModeContent={rawModeContent}
rawLatestContentRef={rawLatestContentRef}
onRenameFilename={onRenameFilename}
noteLayout={noteLayout}
onToggleNoteLayout={onToggleNoteLayout}
isConflicted={isConflicted}
onKeepMine={onKeepMine}
onKeepTheirs={onKeepTheirs}
@@ -467,6 +475,7 @@ export const Editor = memo(function Editor(props: EditorProps) {
vaultPath, noteList, noteListFilter,
onToggleFavorite, onToggleOrganized, onDeleteNote, onArchiveNote, onUnarchiveNote,
onContentChange, onSave, onRenameFilename,
noteLayout, onToggleNoteLayout,
onFileCreated, onFileModified, onVaultChanged,
isConflicted, onKeepMine, onKeepTheirs,
flushPendingRawContentRef,
@@ -527,6 +536,8 @@ export const Editor = memo(function Editor(props: EditorProps) {
rawModeContent={rawModeContent}
rawLatestContentRef={rawLatestContentRef}
onRenameFilename={onRenameFilename}
noteLayout={noteLayout}
onToggleNoteLayout={onToggleNoteLayout}
isConflicted={isConflicted}
onKeepMine={onKeepMine}
onKeepTheirs={onKeepTheirs}

View File

@@ -373,7 +373,7 @@ export function RawEditorView({ content, path, entries, onContentChange, onSave,
<RawEditorYamlErrorBanner error={pendingChanges.yamlError} />
<div
ref={containerRef}
className="flex flex-1 min-h-0"
className="raw-editor-codemirror flex flex-1 min-h-0"
data-testid="raw-editor-codemirror"
aria-label="Raw editor"
/>

View File

@@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'
import { EditorContentLayout } from './EditorContentLayout'
vi.mock('../BreadcrumbBar', () => ({
BreadcrumbBar: () => <div data-testid="breadcrumb-bar" />,
BreadcrumbBar: ({ noteLayout }: { noteLayout?: string }) => <div data-testid="breadcrumb-bar" data-note-layout={noteLayout} />,
}))
vi.mock('../ArchivedNoteBanner', () => ({
@@ -63,6 +63,8 @@ function createModel(overrides: Record<string, unknown> = {}) {
onEditorChange: vi.fn(),
isDeletedPreview: false,
rawLatestContentRef: { current: null },
noteLayout: 'centered',
onToggleNoteLayout: vi.fn(),
forceRawMode: false,
showAIChat: false,
onToggleAIChat: vi.fn(),
@@ -99,4 +101,11 @@ describe('EditorContentLayout', () => {
expect(container.querySelector('.animate-pulse')).not.toBeNull()
expect(screen.queryByTestId('title-field-input')).not.toBeInTheDocument()
})
it('marks the editor content root and breadcrumb with the note layout preference', () => {
const { container } = render(<EditorContentLayout {...createModel({ noteLayout: 'left' })} />)
expect(container.firstElementChild).toHaveClass('editor-content-layout--left')
expect(screen.getByTestId('breadcrumb-bar')).toHaveAttribute('data-note-layout', 'left')
})
})

View File

@@ -1,4 +1,5 @@
import type React from 'react'
import { cn } from '@/lib/utils'
import { DiffView } from '../DiffView'
import { BreadcrumbBar } from '../BreadcrumbBar'
import { ArchivedNoteBanner } from '../ArchivedNoteBanner'
@@ -28,6 +29,8 @@ type BreadcrumbActions = Pick<
| 'onArchiveNote'
| 'onUnarchiveNote'
| 'onRenameFilename'
| 'noteLayout'
| 'onToggleNoteLayout'
>
function EditorLoadingSkeleton() {
@@ -128,6 +131,8 @@ function ActiveTabBreadcrumb({
onArchive={bindPath(actions.onArchiveNote, path)}
onUnarchive={bindPath(actions.onUnarchiveNote, path)}
onRenameFilename={actions.onRenameFilename}
noteLayout={actions.noteLayout}
onToggleNoteLayout={actions.onToggleNoteLayout}
/>
)
}
@@ -228,18 +233,23 @@ export function EditorContentLayout(model: EditorContentModel) {
isDeletedPreview,
rawLatestContentRef,
rawModeContent,
noteLayout,
} = model
const rootClassName = cn(
'flex flex-1 flex-col min-w-0 min-h-0',
noteLayout === 'left' ? 'editor-content-layout--left' : 'editor-content-layout--centered',
)
if (!activeTab) {
return (
<div className="flex flex-1 flex-col min-w-0 min-h-0">
<div className={rootClassName}>
{isLoadingNewTab && showEditor && <EditorLoadingSkeleton />}
</div>
)
}
return (
<div className="flex flex-1 flex-col min-w-0 min-h-0">
<div className={rootClassName}>
<ActiveTabBreadcrumb
activeTab={activeTab}
barRef={breadcrumbBarRef}
@@ -263,6 +273,8 @@ export function EditorContentLayout(model: EditorContentModel) {
onArchiveNote: model.onArchiveNote,
onUnarchiveNote: model.onUnarchiveNote,
onRenameFilename: model.onRenameFilename,
noteLayout: model.noteLayout,
onToggleNoteLayout: model.onToggleNoteLayout,
}}
/>
<EditorChrome

View File

@@ -1,7 +1,7 @@
import type React from 'react'
import { useRef } from 'react'
import type { useCreateBlockNote } from '@blocknote/react'
import type { NoteStatus, VaultEntry } from '../../types'
import type { NoteLayout, NoteStatus, VaultEntry } from '../../types'
import { useEditorTheme } from '../../hooks/useTheme'
import { deriveEditorContentState } from './editorContentState'
@@ -40,6 +40,8 @@ export interface EditorContentProps {
rawModeContent?: string | null
rawLatestContentRef?: React.MutableRefObject<string | null>
onRenameFilename?: (path: string, newFilenameStem: string) => void
noteLayout?: NoteLayout
onToggleNoteLayout?: () => void
isConflicted?: boolean
onKeepMine?: (path: string) => void
onKeepTheirs?: (path: string) => void

View File

@@ -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<NoteLayout, string> = {
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 },

View File

@@ -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,
}
}

View File

@@ -25,6 +25,8 @@ function makeConfig(overrides: Record<string, unknown> = {}) {
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')

View File

@@ -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,

View File

@@ -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')
})
})

View File

@@ -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<NoteLayout>(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 }
}

View File

@@ -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<string, string> | null
status_colors: Record<string, string> | null
property_display_modes: Record<string, string> | null

View File

@@ -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,
}