fix: persist layout panel widths

This commit is contained in:
lucaronin
2026-04-30 15:53:32 +02:00
parent d53c3fa7e5
commit 0331b40ae2
10 changed files with 245 additions and 32 deletions

View File

@@ -702,6 +702,8 @@ Per-vault settings stored locally and scoped by vault path:
- Missing, null, and unknown AI agent permission modes normalize to `safe`; the AI panel can switch modes per vault, preserving the transcript and applying the new mode only to the next agent run
- One-time migration from localStorage (`configMigration.ts`)
Installation-local layout state that should not sync through a vault stays in localStorage. `useLayoutPanels` stores the clamped sidebar, note-list, and inspector widths under `tolaria:layout-panels` so pane sizing survives app relaunches on the same machine.
### AI Guidance Files
Tolaria tracks managed vault-level AI guidance separately from normal note content:

View File

@@ -203,14 +203,14 @@ flowchart TD
└──────────────────────────────────────────────────────────────┘
```
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The title bar exposes Back/Forward buttons wired to the same note navigation history as the command palette, menu accelerators, and mouse navigation buttons. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types: pointer users can drag the existing view row, double-click to rename it, or right-click for edit/rename/appearance/delete actions, while keyboard users can use the row context key for the same menu and command-palette move actions for ordering. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root.
- **Note List / Pulse View** (200-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Folder-backed lists also show non-Markdown files: previewable image and PDF binaries get file indicators and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
- **Sidebar** (220-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The title bar exposes Back/Forward buttons wired to the same note navigation history as the command palette, menu accelerators, and mouse navigation buttons. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types: pointer users can drag the existing view row, double-click to rename it, or right-click for edit/rename/appearance/delete actions, while keyboard users can use the row context key for the same menu and command-palette move actions for ordering. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root.
- **Note List / Pulse View** (220-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Folder-backed lists also show non-Markdown files: previewable image and PDF binaries get file indicators and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count and rich-editor width toggle, BlockNote rich text editor with wikilink support, Markdown-compatible inline/display math rendering, first-class Mermaid diagram blocks, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block`. Can toggle to diff view (modified files), raw CodeMirror view, or a wide rich-editor reading surface with preserved side margins; raw CodeMirror remains full-width and unaffected by note width mode. Inline rich-editor images open in a localized shadcn lightbox on double-click while normal single-click BlockNote selection remains untouched, and tiny tracking-style images are ignored. Binary image and PDF files render through `FilePreview` as ordinary vault files using Tauri asset URLs; external-open actions call `open_vault_file_external` so the target is validated against the active vault before the native default app opens it. Unsupported/broken binaries show explicit fallback states and keyboard focus returns to the note list on `Escape`. Decomposed into `Editor` (orchestrator), `EditorContent`, `FilePreview`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, and `useEditorSave`, plus the `useRawMode`/`RawEditorView` pair for markdown source editing. Rich BlockNote input and raw CodeMirror input both route typed `->`, `<-`, and `<->` through the shared `src/utils/arrowLigatures.ts` resolver so arrow ligatures stay consistent across mode switches while escaped ASCII sequences remain literal. Navigation history (Cmd+[/]) replaces tabs.
- **Inspector / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history) and AI Agent panel (the selected CLI agent with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. Per-note `icon` is a suggested Inspector property and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
Panels are separated by `ResizeHandle` components that support drag-to-resize.
Panels are separated by `ResizeHandle` components that support drag-to-resize. `useLayoutPanels` clamps the sidebar, note-list, and inspector widths before applying them, keeps the side panes from flex-shrinking below their protected widths, and persists the last chosen widths in installation-local localStorage under `tolaria:layout-panels`.
The main Tauri window derives its minimum width from the visible panes instead of a single fixed floor. `useMainWindowSizeConstraints` treats the editor-only shell as the 480px baseline, adds sidebar / note-list / expanded-inspector allowances on top, and calls the native `update_current_window_min_size` command whenever view mode or inspector visibility changes. That same native command also grows the current window back out when a wider pane combination is restored, while note windows skip this path and keep their dedicated 800×700 initial sizing.
The main Tauri window derives its minimum width from the visible panes instead of a single fixed floor. `useMainWindowSizeConstraints` treats the editor-only shell as the 480px baseline, adds the current sidebar / note-list / expanded-inspector widths on top with minimum floors, and calls the native `update_current_window_min_size` command whenever view mode, inspector visibility, or restored pane widths change. That same native command also grows the current window back out when a wider pane combination is restored, while note windows skip this path and keep their dedicated 800×700 initial sizing.
The main Tauri window also persists its last normal size and screen position in the app config directory as `window-state.json`. The state stores logical window points, while `window_state.rs` migrates older physical-pixel state on read so Retina and non-Retina launches restore the same user-facing bounds. On startup, the restored frame applies only to the main window and clamps to the currently available monitor work areas, so stale coordinates from a disconnected display fall back to a visible placement. Maximized, fullscreen, minimized, and detached note-window frames are not written as the restore baseline.

View File

@@ -16,8 +16,8 @@
}
.app__sidebar {
flex-shrink: 1;
min-width: 180px;
flex: 0 0 auto;
min-width: 220px;
display: flex;
flex-direction: column;
}
@@ -27,7 +27,7 @@
}
.app__note-list {
flex-shrink: 10;
flex: 0 0 auto;
min-width: 220px;
display: flex;
flex-direction: column;
@@ -39,7 +39,7 @@
.app__editor {
flex: 1;
min-width: 400px;
min-width: 480px;
min-height: 0;
overflow: hidden;
display: flex;

View File

@@ -1328,7 +1328,7 @@ describe('App', () => {
fireEvent.keyDown(window, { key: '3', metaKey: true })
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('update_current_window_min_size', {
minWidth: 880,
minWidth: 1030,
minHeight: 400,
growToFit: true,
})

View File

@@ -1217,10 +1217,13 @@ function App() {
sidebarVisible: nextSidebarVisible,
noteListVisible: nextNoteListVisible,
inspectorCollapsed: nextInspectorCollapsed,
sidebarWidth: layout.sidebarWidth,
noteListWidth: layout.noteListWidth,
inspectorWidth: layout.inspectorWidth,
})
void applyMainWindowSizeConstraints(minWidth).catch((err) => console.warn('[window] Size constraints failed:', err))
}, [layout.inspectorCollapsed, noteWindowParams])
}, [layout.inspectorCollapsed, layout.inspectorWidth, layout.noteListWidth, layout.sidebarWidth, noteWindowParams])
const handleSetViewMode = useCallback((mode: ViewMode) => {
setViewMode(mode)
@@ -1243,6 +1246,9 @@ function App() {
sidebarVisible,
noteListVisible,
inspectorCollapsed: layout.inspectorCollapsed,
sidebarWidth: layout.sidebarWidth,
noteListWidth: layout.noteListWidth,
inspectorWidth: layout.inspectorWidth,
})
const { status: updateStatus, actions: updateActions } = useUpdater(settings.release_channel)

View File

@@ -9,6 +9,7 @@ export const APP_STORAGE_KEYS = {
legacyMigrationFlag: 'tolaria:legacy-storage-migrated',
sortPreferences: 'tolaria-sort-preferences',
sidebarCollapsed: 'tolaria:sidebar-collapsed',
layoutPanels: 'tolaria:layout-panels',
welcomeDismissed: 'tolaria_welcome_dismissed',
} as const
@@ -22,6 +23,7 @@ export const LEGACY_APP_STORAGE_KEYS = {
configMigrationFlag: 'laputa:config-migrated-to-vault',
sortPreferences: 'laputa-sort-preferences',
sidebarCollapsed: 'laputa:sidebar-collapsed',
layoutPanels: 'laputa:layout-panels',
welcomeDismissed: 'laputa_welcome_dismissed',
} as const
@@ -37,6 +39,7 @@ const MIGRATABLE_STORAGE_KEYS: MigratableStorageKey[] = [
'configMigrationFlag',
'sortPreferences',
'sidebarCollapsed',
'layoutPanels',
'welcomeDismissed',
]

View File

@@ -1,10 +1,46 @@
import { describe, it, expect } from 'vitest'
import { beforeEach, describe, it, expect } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useLayoutPanels, COLUMN_MIN_WIDTHS } from './useLayoutPanels'
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS } from '../constants/appStorage'
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
clear: () => { store = {} },
}
})()
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
type ExpectedPanelWidths = {
sidebar: number
noteList: number
inspector: number
}
function storePanelWidths(key: string, widths: ExpectedPanelWidths): void {
localStorage.setItem(key, JSON.stringify(widths))
}
function expectPanelWidths(
result: { current: ReturnType<typeof useLayoutPanels> },
widths: ExpectedPanelWidths,
): void {
expect(result.current.sidebarWidth).toBe(widths.sidebar)
expect(result.current.noteListWidth).toBe(widths.noteList)
expect(result.current.inspectorWidth).toBe(widths.inspector)
}
describe('useLayoutPanels', () => {
beforeEach(() => {
localStorage.clear()
})
it('exports column minimum widths', () => {
expect(COLUMN_MIN_WIDTHS.sidebar).toBe(180)
expect(COLUMN_MIN_WIDTHS.sidebar).toBe(220)
expect(COLUMN_MIN_WIDTHS.noteList).toBe(220)
expect(COLUMN_MIN_WIDTHS.editor).toBe(800)
expect(COLUMN_MIN_WIDTHS.inspector).toBe(240)
@@ -12,9 +48,7 @@ describe('useLayoutPanels', () => {
it('returns default widths', () => {
const { result } = renderHook(() => useLayoutPanels())
expect(result.current.sidebarWidth).toBe(250)
expect(result.current.noteListWidth).toBe(300)
expect(result.current.inspectorWidth).toBe(280)
expectPanelWidths(result, { sidebar: 250, noteList: 300, inspector: 280 })
})
it('clamps sidebar resize to minimum', () => {
@@ -62,4 +96,58 @@ describe('useLayoutPanels', () => {
const { result } = renderHook(() => useLayoutPanels({ initialInspectorCollapsed: false }))
expect(result.current.inspectorCollapsed).toBe(false)
})
it('restores persisted panel widths', () => {
storePanelWidths(APP_STORAGE_KEYS.layoutPanels, {
sidebar: 280,
noteList: 360,
inspector: 320,
})
const { result } = renderHook(() => useLayoutPanels())
expectPanelWidths(result, { sidebar: 280, noteList: 360, inspector: 320 })
})
it('clamps persisted panel widths to supported ranges', () => {
storePanelWidths(APP_STORAGE_KEYS.layoutPanels, {
sidebar: 120,
noteList: 700,
inspector: 90,
})
const { result } = renderHook(() => useLayoutPanels())
expectPanelWidths(result, {
sidebar: COLUMN_MIN_WIDTHS.sidebar,
noteList: 500,
inspector: COLUMN_MIN_WIDTHS.inspector,
})
})
it('falls back to defaults when persisted panel widths are malformed', () => {
localStorage.setItem(APP_STORAGE_KEYS.layoutPanels, '{not json')
const { result } = renderHook(() => useLayoutPanels())
expectPanelWidths(result, { sidebar: 250, noteList: 300, inspector: 280 })
})
it('persists resized panel widths with the Tolaria storage key', () => {
storePanelWidths(LEGACY_APP_STORAGE_KEYS.layoutPanels, {
sidebar: 260,
noteList: 340,
inspector: 300,
})
const { result } = renderHook(() => useLayoutPanels())
act(() => result.current.handleSidebarResize(24))
expect(JSON.parse(localStorage.getItem(APP_STORAGE_KEYS.layoutPanels) ?? '{}')).toEqual({
sidebar: 284,
noteList: 340,
inspector: 300,
})
expect(localStorage.getItem(LEGACY_APP_STORAGE_KEYS.layoutPanels)).toBeNull()
})
})

View File

@@ -1,19 +1,103 @@
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS, getAppStorageItem } from '../constants/appStorage'
export const COLUMN_MIN_WIDTHS = {
sidebar: 180,
sidebar: 220,
noteList: 220,
editor: 800,
inspector: 240,
} as const
export function useLayoutPanels(options?: { initialInspectorCollapsed?: boolean }) {
const [sidebarWidth, setSidebarWidth] = useState(250)
const [noteListWidth, setNoteListWidth] = useState(300)
const [inspectorWidth, setInspectorWidth] = useState(280)
const [inspectorCollapsed, setInspectorCollapsed] = useState(options?.initialInspectorCollapsed ?? true)
const handleSidebarResize = useCallback((delta: number) => setSidebarWidth((w) => Math.max(COLUMN_MIN_WIDTHS.sidebar, Math.min(400, w + delta))), [])
const handleNoteListResize = useCallback((delta: number) => setNoteListWidth((w) => Math.max(COLUMN_MIN_WIDTHS.noteList, Math.min(500, w + delta))), [])
const handleInspectorResize = useCallback((delta: number) => setInspectorWidth((w) => Math.max(COLUMN_MIN_WIDTHS.inspector, Math.min(500, w - delta))), [])
return { sidebarWidth, noteListWidth, inspectorWidth, inspectorCollapsed, setInspectorCollapsed, handleSidebarResize, handleNoteListResize, handleInspectorResize }
const COLUMN_MAX_WIDTHS = {
sidebar: 400,
noteList: 500,
inspector: 500,
} as const
const DEFAULT_PANEL_WIDTHS = {
sidebar: 250,
noteList: 300,
inspector: 280,
} as const
type PanelWidthKey = keyof typeof DEFAULT_PANEL_WIDTHS
type PanelWidths = Record<PanelWidthKey, number>
function defaultPanelWidths(): PanelWidths {
return { ...DEFAULT_PANEL_WIDTHS }
}
function clampPanelWidth(key: PanelWidthKey, value: number): number {
return Math.max(COLUMN_MIN_WIDTHS[key], Math.min(COLUMN_MAX_WIDTHS[key], value))
}
function isPanelWidthRecord(value: unknown): value is Partial<Record<PanelWidthKey, unknown>> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function readPanelWidth(source: Partial<Record<PanelWidthKey, unknown>>, key: PanelWidthKey): number {
const value = source[key]
return typeof value === 'number' && Number.isFinite(value)
? clampPanelWidth(key, value)
: DEFAULT_PANEL_WIDTHS[key]
}
function normalizePanelWidths(value: unknown): PanelWidths {
if (!isPanelWidthRecord(value)) return defaultPanelWidths()
return {
sidebar: readPanelWidth(value, 'sidebar'),
noteList: readPanelWidth(value, 'noteList'),
inspector: readPanelWidth(value, 'inspector'),
}
}
function loadPanelWidths(): PanelWidths {
const raw = getAppStorageItem('layoutPanels')
if (!raw) return defaultPanelWidths()
try {
return normalizePanelWidths(JSON.parse(raw))
} catch {
return defaultPanelWidths()
}
}
function savePanelWidths(widths: PanelWidths): void {
try {
localStorage.setItem(APP_STORAGE_KEYS.layoutPanels, JSON.stringify(widths))
localStorage.removeItem(LEGACY_APP_STORAGE_KEYS.layoutPanels)
} catch {
// Ignore unavailable or restricted localStorage implementations.
}
}
export function useLayoutPanels(options?: { initialInspectorCollapsed?: boolean }) {
const [panelWidths, setPanelWidths] = useState(loadPanelWidths)
const [inspectorCollapsed, setInspectorCollapsed] = useState(options?.initialInspectorCollapsed ?? true)
useEffect(() => {
savePanelWidths(panelWidths)
}, [panelWidths])
const resizePanel = useCallback((key: PanelWidthKey, delta: number) => {
setPanelWidths((widths) => ({
...widths,
[key]: clampPanelWidth(key, widths[key] + delta),
}))
}, [])
const handleSidebarResize = useCallback((delta: number) => resizePanel('sidebar', delta), [resizePanel])
const handleNoteListResize = useCallback((delta: number) => resizePanel('noteList', delta), [resizePanel])
const handleInspectorResize = useCallback((delta: number) => resizePanel('inspector', -delta), [resizePanel])
return {
sidebarWidth: panelWidths.sidebar,
noteListWidth: panelWidths.noteList,
inspectorWidth: panelWidths.inspector,
inspectorCollapsed,
setInspectorCollapsed,
handleSidebarResize,
handleNoteListResize,
handleInspectorResize,
}
}

View File

@@ -23,7 +23,7 @@ describe('getMainWindowMinWidth', () => {
noteListVisible: true,
inspectorCollapsed: true,
},
expectedWidth: 880,
expectedWidth: 920,
},
{
name: 'drops to the narrower editor-only floor when only the editor is visible',
@@ -52,6 +52,18 @@ describe('getMainWindowMinWidth', () => {
},
expectedWidth: 720,
},
{
name: 'uses restored pane widths when they exceed the minimum allowances',
visibility: {
sidebarVisible: true,
noteListVisible: true,
inspectorCollapsed: false,
sidebarWidth: 360,
noteListWidth: 340,
inspectorWidth: 320,
},
expectedWidth: 1500,
},
] as const
it.each(minWidthCases)('$name', ({ visibility, expectedWidth }) => {
@@ -82,7 +94,7 @@ describe('useMainWindowSizeConstraints', () => {
noteListVisible: true,
inspectorCollapsed: false,
},
expectedWidth: 1120,
expectedWidth: 1160,
},
] as const

View File

@@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core'
const MAIN_WINDOW_MIN_HEIGHT = 400
const EDITOR_ONLY_MAIN_WINDOW_MIN_WIDTH = 480
const MAIN_WINDOW_SIDEBAR_MIN_WIDTH = 180
const MAIN_WINDOW_SIDEBAR_MIN_WIDTH = 220
const MAIN_WINDOW_NOTE_LIST_MIN_WIDTH = 220
const MAIN_WINDOW_INSPECTOR_MIN_WIDTH = 240
@@ -11,22 +11,34 @@ export type MainWindowPaneVisibility = {
sidebarVisible: boolean
noteListVisible: boolean
inspectorCollapsed: boolean
sidebarWidth?: number
noteListWidth?: number
inspectorWidth?: number
}
export function getMainWindowMinWidth({
sidebarVisible,
noteListVisible,
inspectorCollapsed,
sidebarWidth,
noteListWidth,
inspectorWidth,
}: MainWindowPaneVisibility): number {
let minWidth = EDITOR_ONLY_MAIN_WINDOW_MIN_WIDTH
if (sidebarVisible) minWidth += MAIN_WINDOW_SIDEBAR_MIN_WIDTH
if (noteListVisible) minWidth += MAIN_WINDOW_NOTE_LIST_MIN_WIDTH
if (!inspectorCollapsed) minWidth += MAIN_WINDOW_INSPECTOR_MIN_WIDTH
if (sidebarVisible) minWidth += getPaneWidth(sidebarWidth, MAIN_WINDOW_SIDEBAR_MIN_WIDTH)
if (noteListVisible) minWidth += getPaneWidth(noteListWidth, MAIN_WINDOW_NOTE_LIST_MIN_WIDTH)
if (!inspectorCollapsed) minWidth += getPaneWidth(inspectorWidth, MAIN_WINDOW_INSPECTOR_MIN_WIDTH)
return minWidth
}
function getPaneWidth(width: number | undefined, minimum: number): number {
return typeof width === 'number' && Number.isFinite(width)
? Math.max(minimum, Math.round(width))
: minimum
}
type MainWindowSizeConstraintsOptions = MainWindowPaneVisibility & {
enabled?: boolean
}
@@ -46,11 +58,17 @@ export function useMainWindowSizeConstraints({
sidebarVisible,
noteListVisible,
inspectorCollapsed,
sidebarWidth,
noteListWidth,
inspectorWidth,
}: MainWindowSizeConstraintsOptions): void {
const minWidth = getMainWindowMinWidth({
sidebarVisible,
noteListVisible,
inspectorCollapsed,
sidebarWidth,
noteListWidth,
inspectorWidth,
})
useEffect(() => {