feat: add note layout preference
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
51
src/hooks/useNoteLayout.test.ts
Normal file
51
src/hooks/useNoteLayout.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
33
src/hooks/useNoteLayout.ts
Normal file
33
src/hooks/useNoteLayout.ts
Normal 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 }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user