diff --git a/src/App.tsx b/src/App.tsx
index 83cd170..641850b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -18,10 +18,19 @@ import { usePreview } from "./hooks/usePreview";
import { useSearchController } from "./hooks/useSearchController";
import { useWorkspaceController } from "./hooks/useWorkspaceController";
import { useWorkspaceSync } from "./hooks/useWorkspaceSync";
-import { useLayoutActions, useLayoutState } from "./state/appStore";
+import {
+ useEditorPresentationActions,
+ useEditorPresentationState,
+ useLayoutChromeActions,
+ useLayoutChromeState,
+ usePdfExportActions,
+ usePdfExportState,
+ useViewModeState,
+ useWriterToolsActions,
+ useWriterToolsState,
+} from "./state/appStore";
import "@fontsource-variable/ibm-plex-sans";
import "./App.css";
-import { PatternCategory } from "$editor/pattern-matcher";
// TODO: make shared utils module
function formatDraftDate(date: Date): string {
@@ -72,7 +81,9 @@ const ShowButton = ({ clickHandler, title, label }: { clickHandler: () => void;
function App() {
const { model: editorModel, dispatch: editorDispatch, openDoc } = useEditor();
const { model: previewModel, render: renderPreview, syncLine: syncPreviewLine, setDoc: setPreviewDoc } = usePreview();
- const { state: pdfExportState, exportPdf, reset: resetPdfExport } = usePdfExport();
+ const exportPdf = usePdfExport();
+ const { isExportingPdf, pdfExportError } = usePdfExportState();
+ const { resetPdfExport } = usePdfExportActions();
const { missingLocations, conflicts } = useBackendEvents();
const [isLayoutSettingsOpen, setIsLayoutSettingsOpen] = useState(false);
const [isPdfExportDialogOpen, setIsPdfExportDialogOpen] = useState(false);
@@ -81,8 +92,19 @@ function App() {
useWorkspaceSync();
useLayoutHotkeys();
- const layoutState = useLayoutState();
- const layoutActions = useLayoutActions();
+ const layoutChrome = useLayoutChromeState();
+ const { setSidebarCollapsed, setTopBarsCollapsed, setStatusBarCollapsed } = useLayoutChromeActions();
+ const editorPresentation = useEditorPresentationState();
+ const {
+ setLineNumbersVisible,
+ setTextWrappingEnabled,
+ setSyntaxHighlightingEnabled,
+ setEditorFontSize,
+ setEditorFontFamily,
+ } = useEditorPresentationActions();
+ const { isFocusMode } = useViewModeState();
+ const { styleCheckSettings } = useWriterToolsState();
+ const { setStyleCheckSettings } = useWriterToolsActions();
const workspace = useWorkspaceController(openDoc);
const search = useSearchController(workspace.handleSelectDocument);
@@ -157,10 +179,8 @@ function App() {
setIsPdfExportDialogOpen(true);
}, [activeTab, resetPdfExport]);
- const handleOpenSearch = useCallback(() => layoutActions.setShowSearch(true), [layoutActions]);
- const handleShowSidebar = useCallback(() => layoutActions.setSidebarCollapsed(false), [layoutActions]);
- const handleShowStatusBar = useCallback(() => layoutActions.setStatusBarCollapsed(false), [layoutActions]);
- const handleExit = useCallback(() => layoutActions.setFocusMode(false), [layoutActions]);
+ const handleShowSidebar = useCallback(() => setSidebarCollapsed(false), [setSidebarCollapsed]);
+ const handleShowStatusBar = useCallback(() => setStatusBarCollapsed(false), [setStatusBarCollapsed]);
const handleCancelPdfExport = useCallback(() => {
setIsPdfExportDialogOpen(false);
@@ -182,7 +202,7 @@ function App() {
);
});
- const didExport = await exportPdf(renderResult, options, layoutState.editorFontFamily);
+ const didExport = await exportPdf(renderResult, options, editorPresentation.editorFontFamily);
if (didExport) {
setIsPdfExportDialogOpen(false);
resetPdfExport();
@@ -190,110 +210,72 @@ function App() {
} catch (error) {
logger.error("Failed to export PDF", { error: error instanceof Error ? error.message : String(error) });
}
- }, [activeTab, editorModel.text, exportPdf, layoutState.editorFontFamily, resetPdfExport]);
+ }, [activeTab, editorModel.text, exportPdf, editorPresentation.editorFontFamily, resetPdfExport]);
- const showToggleControls = useMemo(() => layoutState.sidebarCollapsed || layoutState.statusBarCollapsed, [
- layoutState.sidebarCollapsed,
- layoutState.statusBarCollapsed,
+ const showToggleControls = useMemo(() => layoutChrome.sidebarCollapsed || layoutChrome.statusBarCollapsed, [
+ layoutChrome.sidebarCollapsed,
+ layoutChrome.statusBarCollapsed,
]);
- const layoutProps = useMemo(
- () => ({
- sidebarCollapsed: layoutState.sidebarCollapsed,
- topBarsCollapsed: layoutState.topBarsCollapsed,
- statusBarCollapsed: layoutState.statusBarCollapsed,
- isSplitView: layoutState.isSplitView,
- isPreviewVisible: layoutState.isPreviewVisible,
- }),
- [layoutState],
- );
-
const sidebarProps = useMemo(
() => ({
- locations: workspace.locations,
- selectedLocationId: workspace.selectedLocationId,
- selectedDocPath: workspace.selectedDocPath,
- documents: workspace.locationDocuments,
- isLoading: workspace.isSidebarLoading,
- filterText: workspace.sidebarFilter,
- onAddLocation: workspace.handleAddLocation,
- onRemoveLocation: workspace.handleRemoveLocation,
- onSelectLocation: workspace.handleSelectLocation,
- onSelectDocument: workspace.handleSelectDocument,
- onFilterChange: workspace.setSidebarFilter,
+ handleAddLocation: workspace.handleAddLocation,
+ handleRemoveLocation: workspace.handleRemoveLocation,
+ handleSelectDocument: workspace.handleSelectDocument,
}),
- [workspace],
+ [workspace.handleAddLocation, workspace.handleRemoveLocation, workspace.handleSelectDocument],
);
const toolbarProps = useMemo(
() => ({
saveStatus: editorModel.saveStatus,
- isSplitView: layoutState.isSplitView,
- isFocusMode: layoutState.isFocusMode,
- isPreviewVisible: layoutState.isPreviewVisible,
onSave: handleSave,
- onToggleSplitView: layoutActions.toggleSplitView,
- onToggleFocusMode: layoutActions.toggleFocusMode,
- onTogglePreview: layoutActions.togglePreviewVisible,
onExportPdf: handleOpenPdfExport,
- isExportingPdf: pdfExportState.isExporting,
+ isExportingPdf: isExportingPdf,
isPdfExportDisabled: !activeTab,
onOpenSettings: handleOpenSettings,
}),
- [
- editorModel.saveStatus,
- layoutState.isSplitView,
- layoutState.isFocusMode,
- layoutState.isPreviewVisible,
- layoutActions.toggleSplitView,
- layoutActions.toggleFocusMode,
- layoutActions.togglePreviewVisible,
- handleOpenPdfExport,
- pdfExportState.isExporting,
- activeTab,
- handleSave,
- handleOpenSettings,
- ],
+ [editorModel.saveStatus, handleOpenPdfExport, isExportingPdf, activeTab, handleSave, handleOpenSettings],
);
const tabProps = useMemo(
() => ({
tabs: workspace.tabs,
activeTabId: workspace.activeTabId,
- onSelectTab: workspace.handleSelectTab,
- onCloseTab: workspace.handleCloseTab,
- onReorderTabs: workspace.handleReorderTabs,
+ handleSelectTab: workspace.handleSelectTab,
+ handleCloseTab: workspace.handleCloseTab,
+ handleReorderTabs: workspace.handleReorderTabs,
}),
- [workspace],
+ [
+ workspace.tabs,
+ workspace.activeTabId,
+ workspace.handleSelectTab,
+ workspace.handleCloseTab,
+ workspace.handleReorderTabs,
+ ],
);
const editorProps = useMemo(
() => ({
initialText: editorModel.text,
- theme: layoutState.theme,
- showLineNumbers: layoutState.lineNumbersVisible,
- textWrappingEnabled: layoutState.textWrappingEnabled,
- syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled,
- fontSize: layoutState.editorFontSize,
- fontFamily: layoutState.editorFontFamily,
- posHighlightingEnabled: layoutState.posHighlightingEnabled,
- styleCheckSettings: layoutState.styleCheckSettings,
onChange: handleEditorChange,
onSave: handleSave,
onCursorMove: handleCursorMove,
onSelectionChange: handleSelectionChange,
}),
- [editorModel.text, layoutState, handleEditorChange, handleSave, handleCursorMove, handleSelectionChange],
+ [editorModel.text, handleEditorChange, handleSave, handleCursorMove, handleSelectionChange],
);
const statusBarProps = useMemo(
() => ({
docMeta: activeDocMeta,
- cursorLine: editorModel.cursorLine,
- cursorColumn: editorModel.cursorColumn,
- wordCount,
- charCount,
- selectionCount,
+ stats: {
+ cursorLine: editorModel.cursorLine,
+ cursorColumn: editorModel.cursorColumn,
+ wordCount,
+ charCount,
+ selectionCount,
+ },
}),
[activeDocMeta, editorModel.cursorLine, editorModel.cursorColumn, wordCount, charCount, selectionCount],
);
@@ -301,112 +283,66 @@ function App() {
const previewProps = useMemo(
() => ({
renderResult: previewModel.renderResult,
- theme: layoutState.theme,
+ theme: editorPresentation.theme,
editorLine: editorModel.cursorLine,
onScrollToLine: syncPreviewLine,
}),
- [previewModel.renderResult, layoutState.theme, editorModel.cursorLine, syncPreviewLine],
+ [previewModel.renderResult, editorPresentation.theme, editorModel.cursorLine, syncPreviewLine],
);
const searchProps = useMemo(
() => ({
- isVisible: layoutState.showSearch,
- sidebarCollapsed: layoutState.sidebarCollapsed,
- topOffset: 48,
- query: search.searchQuery,
- results: search.searchResults,
- isSearching: search.isSearching,
locations: workspace.locations,
+ searchQuery: search.searchQuery,
+ searchResults: search.searchResults,
+ isSearching: search.isSearching,
filters: search.filters,
- onQueryChange: search.handleSearch,
- onFiltersChange: search.setFilters,
- onSelectResult: search.handleSelectSearchResult,
- onClose: () => layoutActions.setShowSearch(false),
+ handleSearch: search.handleSearch,
+ setFilters: search.setFilters,
+ handleSelectSearchResult: search.handleSelectSearchResult,
}),
- [layoutState.showSearch, layoutState.sidebarCollapsed, search, workspace.locations, layoutActions],
+ [
+ workspace.locations,
+ search.searchQuery,
+ search.searchResults,
+ search.isSearching,
+ search.filters,
+ search.handleSearch,
+ search.setFilters,
+ search.handleSelectSearchResult,
+ ],
);
const handleSettingsClose = useCallback(() => {
setIsLayoutSettingsOpen(false);
}, []);
- const settingsPanelProps = useMemo(
- () => ({
- isVisible: isLayoutSettingsOpen,
- sidebarCollapsed: layoutState.sidebarCollapsed,
- topBarsCollapsed: layoutState.topBarsCollapsed,
- statusBarCollapsed: layoutState.statusBarCollapsed,
- lineNumbersVisible: layoutState.lineNumbersVisible,
- textWrappingEnabled: layoutState.textWrappingEnabled,
- syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled,
- editorFontSize: layoutState.editorFontSize,
- editorFontFamily: layoutState.editorFontFamily,
- focusModeSettings: layoutState.focusModeSettings,
- onSetSidebarCollapsed: layoutActions.setSidebarCollapsed,
- onSetTopBarsCollapsed: layoutActions.setTopBarsCollapsed,
- onSetStatusBarCollapsed: layoutActions.setStatusBarCollapsed,
- onSetLineNumbersVisible: layoutActions.setLineNumbersVisible,
- onSetTextWrappingEnabled: layoutActions.setTextWrappingEnabled,
- onSetSyntaxHighlightingEnabled: layoutActions.setSyntaxHighlightingEnabled,
- onSetEditorFontSize: layoutActions.setEditorFontSize,
- onSetEditorFontFamily: layoutActions.setEditorFontFamily,
- onSetTypewriterScrollingEnabled: layoutActions.setTypewriterScrollingEnabled,
- onSetFocusDimmingMode: layoutActions.setFocusDimmingMode,
- posHighlightingEnabled: layoutState.posHighlightingEnabled,
- onSetPosHighlightingEnabled: layoutActions.setPosHighlightingEnabled,
- styleCheckSettings: layoutState.styleCheckSettings,
- onSetStyleCheckEnabled: (enabled: boolean) =>
- layoutActions.setStyleCheckSettings({ ...layoutState.styleCheckSettings, enabled }),
- onSetStyleCheckCategory: (category: PatternCategory, enabled: boolean) =>
- layoutActions.setStyleCheckCategory(category, enabled),
- onAddCustomPattern: (pattern: { text: string; category: PatternCategory; replacement?: string }) =>
- layoutActions.addCustomPattern(pattern),
- onRemoveCustomPattern: (index: number) => layoutActions.removeCustomPattern(index),
- onClose: handleSettingsClose,
- }),
- [isLayoutSettingsOpen, layoutState, layoutActions, handleSettingsClose],
- );
-
- const focusModePanelProps = useMemo(
- () => ({
- theme: layoutState.theme,
- text: editorModel.text,
- docMeta: activeDocMeta,
- cursorLine: editorModel.cursorLine,
- cursorColumn: editorModel.cursorColumn,
- wordCount: wordCount,
- charCount: charCount,
- selectionCount: selectionCount,
- lineNumbersVisible: layoutState.lineNumbersVisible,
- textWrappingEnabled: layoutState.textWrappingEnabled,
- syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled,
- editorFontSize: layoutState.editorFontSize,
- editorFontFamily: layoutState.editorFontFamily,
- statusBarCollapsed: layoutState.statusBarCollapsed,
- focusModeSettings: layoutState.focusModeSettings,
- posHighlightingEnabled: layoutState.posHighlightingEnabled,
- onExit: handleExit,
- onEditorChange: handleEditorChange,
- onSave: handleSave,
- onCursorMove: handleCursorMove,
- onSelectionChange: handleSelectionChange,
- }),
- [
- layoutState,
- editorModel.text,
- activeDocMeta,
- editorModel.cursorLine,
- editorModel.cursorColumn,
- wordCount,
- charCount,
- selectionCount,
- handleExit,
- handleEditorChange,
- handleSave,
- handleCursorMove,
- handleSelectionChange,
- ],
- );
+ const focusModePanelProps = useMemo(() => {
+ const pos = { cursorLine: editorModel.cursorLine, cursorColumn: editorModel.cursorColumn };
+ const stats = { ...pos, wordCount, charCount, selectionCount };
+ return ({
+ editor: {
+ initialText: editorModel.text,
+ onChange: handleEditorChange,
+ onSave: handleSave,
+ onCursorMove: handleCursorMove,
+ onSelectionChange: handleSelectionChange,
+ },
+ statusBar: { docMeta: activeDocMeta, stats },
+ });
+ }, [
+ editorModel.text,
+ activeDocMeta,
+ editorModel.cursorLine,
+ editorModel.cursorColumn,
+ wordCount,
+ charCount,
+ selectionCount,
+ handleEditorChange,
+ handleSave,
+ handleCursorMove,
+ handleSelectionChange,
+ ]);
useEffect(() => {
workspace.markActiveTabModified(editorModel.saveStatus === "Dirty");
@@ -438,14 +374,14 @@ function App() {
return;
}
- layoutActions.setSidebarCollapsed(settings.sidebar_collapsed);
- layoutActions.setTopBarsCollapsed(settings.top_bars_collapsed);
- layoutActions.setStatusBarCollapsed(settings.status_bar_collapsed);
- layoutActions.setLineNumbersVisible(settings.line_numbers_visible);
- layoutActions.setTextWrappingEnabled(settings.text_wrapping_enabled);
- layoutActions.setSyntaxHighlightingEnabled(settings.syntax_highlighting_enabled);
- layoutActions.setEditorFontSize(settings.editor_font_size);
- layoutActions.setEditorFontFamily(settings.editor_font_family);
+ setSidebarCollapsed(settings.sidebar_collapsed);
+ setTopBarsCollapsed(settings.top_bars_collapsed);
+ setStatusBarCollapsed(settings.status_bar_collapsed);
+ setLineNumbersVisible(settings.line_numbers_visible);
+ setTextWrappingEnabled(settings.text_wrapping_enabled);
+ setSyntaxHighlightingEnabled(settings.syntax_highlighting_enabled);
+ setEditorFontSize(settings.editor_font_size);
+ setEditorFontFamily(settings.editor_font_family);
setLayoutSettingsHydrated(true);
}, () => {
if (!isCancelled) {
@@ -458,7 +394,7 @@ function App() {
return;
}
- layoutActions.setStyleCheckSettings({
+ setStyleCheckSettings({
enabled: settings.enabled,
categories: settings.categories,
customPatterns: settings.custom_patterns,
@@ -468,7 +404,17 @@ function App() {
return () => {
isCancelled = true;
};
- }, [layoutActions]);
+ }, [
+ setEditorFontFamily,
+ setEditorFontSize,
+ setLineNumbersVisible,
+ setSidebarCollapsed,
+ setStatusBarCollapsed,
+ setStyleCheckSettings,
+ setSyntaxHighlightingEnabled,
+ setTextWrappingEnabled,
+ setTopBarsCollapsed,
+ ]);
useEffect(() => {
if (!layoutSettingsHydrated) {
@@ -478,14 +424,14 @@ function App() {
void runCmd(
uiLayoutSet(
{
- sidebar_collapsed: layoutState.sidebarCollapsed,
- top_bars_collapsed: layoutState.topBarsCollapsed,
- status_bar_collapsed: layoutState.statusBarCollapsed,
- line_numbers_visible: layoutState.lineNumbersVisible,
- text_wrapping_enabled: layoutState.textWrappingEnabled,
- syntax_highlighting_enabled: layoutState.syntaxHighlightingEnabled,
- editor_font_size: layoutState.editorFontSize,
- editor_font_family: layoutState.editorFontFamily,
+ sidebar_collapsed: layoutChrome.sidebarCollapsed,
+ top_bars_collapsed: layoutChrome.topBarsCollapsed,
+ status_bar_collapsed: layoutChrome.statusBarCollapsed,
+ line_numbers_visible: editorPresentation.lineNumbersVisible,
+ text_wrapping_enabled: editorPresentation.textWrappingEnabled,
+ syntax_highlighting_enabled: editorPresentation.syntaxHighlightingEnabled,
+ editor_font_size: editorPresentation.editorFontSize,
+ editor_font_family: editorPresentation.editorFontFamily,
},
() => {},
() => {},
@@ -493,14 +439,14 @@ function App() {
);
}, [
layoutSettingsHydrated,
- layoutState.sidebarCollapsed,
- layoutState.topBarsCollapsed,
- layoutState.statusBarCollapsed,
- layoutState.lineNumbersVisible,
- layoutState.textWrappingEnabled,
- layoutState.syntaxHighlightingEnabled,
- layoutState.editorFontSize,
- layoutState.editorFontFamily,
+ layoutChrome.sidebarCollapsed,
+ layoutChrome.topBarsCollapsed,
+ layoutChrome.statusBarCollapsed,
+ editorPresentation.lineNumbersVisible,
+ editorPresentation.textWrappingEnabled,
+ editorPresentation.syntaxHighlightingEnabled,
+ editorPresentation.editorFontSize,
+ editorPresentation.editorFontFamily,
]);
useEffect(() => {
@@ -511,9 +457,9 @@ function App() {
void runCmd(
styleCheckSet(
{
- enabled: layoutState.styleCheckSettings.enabled,
- categories: layoutState.styleCheckSettings.categories,
- custom_patterns: layoutState.styleCheckSettings.customPatterns,
+ enabled: styleCheckSettings.enabled,
+ categories: styleCheckSettings.categories,
+ custom_patterns: styleCheckSettings.customPatterns,
},
() => {},
() => {},
@@ -521,25 +467,13 @@ function App() {
);
}, [
layoutSettingsHydrated,
- layoutState.styleCheckSettings.enabled,
- layoutState.styleCheckSettings.categories,
- layoutState.styleCheckSettings.customPatterns,
+ styleCheckSettings.enabled,
+ styleCheckSettings.categories,
+ styleCheckSettings.customPatterns,
]);
- const appHeaderBarProps = useMemo(
- () => ({
- onToggleSidebar: layoutActions.toggleSidebarCollapsed,
- onToggleTabBar: layoutActions.toggleTabBarCollapsed,
- onOpenSearch: handleOpenSearch,
- tabBarCollapsed: layoutState.topBarsCollapsed,
- }),
- [layoutActions, handleOpenSearch, layoutState.topBarsCollapsed],
- );
-
const workspacePanelProps = useMemo(
() => ({
- layout: layoutProps,
- onToggleSidebar: layoutActions.toggleSidebarCollapsed,
sidebar: sidebarProps,
toolbar: toolbarProps,
tabs: tabProps,
@@ -547,7 +481,7 @@ function App() {
preview: previewProps,
statusBar: statusBarProps,
}),
- [layoutProps, layoutActions, sidebarProps, toolbarProps, tabProps, editorProps, previewProps, statusBarProps],
+ [sidebarProps, toolbarProps, tabProps, editorProps, previewProps, statusBarProps],
);
useEffect(() => {
@@ -565,33 +499,33 @@ function App() {
return () => globalThis.removeEventListener("keydown", closeOnEscape);
}, [isLayoutSettingsOpen]);
- if (layoutState.isFocusMode) {
+ if (isFocusMode) {
return ;
}
return (
{showToggleControls && (
- {layoutState.sidebarCollapsed && (
+ {layoutChrome.sidebarCollapsed && (
)}
- {layoutState.statusBarCollapsed && (
+ {layoutChrome.statusBarCollapsed && (
)}
)}
-
+
-
+
diff --git a/src/__tests__/Editor.test.tsx b/src/__tests__/Editor.test.tsx
index 39f0b18..bc0a379 100644
--- a/src/__tests__/Editor.test.tsx
+++ b/src/__tests__/Editor.test.tsx
@@ -1,3 +1,4 @@
+// oxlint-disable react_perf/jsx-no-new-object-as-prop
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { Editor } from "../components/Editor";
@@ -23,7 +24,7 @@ describe(Editor, () => {
});
it("should set data-theme attribute", () => {
- render(
);
+ render(
);
const container = screen.getByTestId("editor-container");
expect(container).toHaveAttribute("data-theme", "light");
});
@@ -40,7 +41,7 @@ describe(Editor, () => {
});
it("should hide line numbers when disabled via prop", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-lineNumbers")).not.toBeInTheDocument();
});
@@ -50,12 +51,12 @@ describe(Editor, () => {
});
it("should disable text wrapping when disabled via prop", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-lineWrapping")).not.toBeInTheDocument();
});
it("should apply custom font family and size variables", () => {
- render(
);
+ render(
);
const container = screen.getByTestId("editor-container");
expect(container).toHaveStyle(
"--editor-font-family: \"Writer IBM Plex Sans\", \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, sans-serif",
@@ -80,13 +81,13 @@ describe(Editor, () => {
describe("theme switching", () => {
it("should support dark theme", () => {
- render(
);
+ render(
);
const container = screen.getByTestId("editor-container");
expect(container).toHaveAttribute("data-theme", "dark");
});
it("should support light theme", () => {
- render(
);
+ render(
);
const container = screen.getByTestId("editor-container");
expect(container).toHaveAttribute("data-theme", "light");
});
@@ -170,10 +171,10 @@ describe(Editor, () => {
});
it("recreates the editor view when presentation props change", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
);
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(screen.getByTestId("editor-container")).toHaveAttribute("data-theme", "light");
@@ -183,9 +184,11 @@ describe(Editor, () => {
});
it("recreates the editor view when line number visibility changes", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
+
,
+ );
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(secondEditorRoot).toBeInTheDocument();
@@ -195,9 +198,11 @@ describe(Editor, () => {
});
it("recreates the editor view when text wrapping changes", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
+
,
+ );
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(secondEditorRoot).toBeInTheDocument();
@@ -207,9 +212,11 @@ describe(Editor, () => {
});
it("recreates the editor view when syntax highlighting mode changes", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
+
,
+ );
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(secondEditorRoot).toBeInTheDocument();
@@ -227,53 +234,53 @@ describe(Editor, () => {
});
it("should apply typewriter scrolling when enabled", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-editor")).toBeInTheDocument();
});
it("should apply focus dimming in sentence mode", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-editor")).toBeInTheDocument();
});
it("should apply focus dimming in paragraph mode", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-editor")).toBeInTheDocument();
});
it("should recreate editor when typewriterScrollingEnabled changes", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
);
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(secondEditorRoot).not.toBe(firstEditorRoot);
});
it("should recreate editor when focusDimmingMode changes", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
);
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(secondEditorRoot).not.toBe(firstEditorRoot);
});
it("should apply POS highlighting when enabled", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-editor")).toBeInTheDocument();
});
it("should not apply POS highlighting when disabled", () => {
- const { container } = render(
);
+ const { container } = render(
);
expect(container.querySelector(".cm-editor")).toBeInTheDocument();
});
it("should recreate editor when posHighlightingEnabled changes", () => {
- const { container, rerender } = render(
);
+ const { container, rerender } = render(
);
const firstEditorRoot = container.querySelector(".cm-editor");
- rerender(
);
+ rerender(
);
const secondEditorRoot = container.querySelector(".cm-editor");
expect(secondEditorRoot).not.toBe(firstEditorRoot);
});
diff --git a/src/__tests__/WorkspacePanel.test.tsx b/src/__tests__/WorkspacePanel.test.tsx
index bbc44ab..3b17a9a 100644
--- a/src/__tests__/WorkspacePanel.test.tsx
+++ b/src/__tests__/WorkspacePanel.test.tsx
@@ -1,46 +1,82 @@
/* oxlint-disable eslint-plugin-react-perf/jsx-no-new-object-as-prop */
+import { WorkspacePanel } from "$components/layout/WorkspacePanel";
+import {
+ useEditorPresentationState,
+ useSidebarState,
+ useToolbarState,
+ useWorkspacePanelModeState,
+ useWorkspacePanelSidebarState,
+ useWorkspacePanelStatusBarCollapsed,
+ useWorkspacePanelTopBarsCollapsed,
+} from "$state/panel-selectors";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
-import { WorkspacePanel } from "../components/layout/WorkspacePanel";
+
+vi.mock(
+ "$state/panel-selectors",
+ () => ({
+ useSidebarState: vi.fn(),
+ useToolbarState: vi.fn(),
+ useEditorPresentationState: vi.fn(),
+ useWorkspacePanelSidebarState: vi.fn(),
+ useWorkspacePanelModeState: vi.fn(),
+ useWorkspacePanelTopBarsCollapsed: vi.fn(),
+ useWorkspacePanelStatusBarCollapsed: vi.fn(),
+ }),
+);
describe("WorkspacePanel", () => {
it("renders preview-only mode when preview is enabled without split view", () => {
- const layout = {
- sidebarCollapsed: true,
- topBarsCollapsed: true,
- statusBarCollapsed: true,
- isSplitView: false,
- isPreviewVisible: true,
- } as const;
- const sidebar = {
+ vi.mocked(useSidebarState).mockReturnValue({
locations: [],
selectedLocationId: undefined,
selectedDocPath: undefined,
documents: [],
isLoading: false,
filterText: "",
- onAddLocation: vi.fn(),
- onRemoveLocation: vi.fn(),
- onSelectLocation: vi.fn(),
- onSelectDocument: vi.fn(),
- onFilterChange: vi.fn(),
- };
- const toolbar = {
- saveStatus: "Idle" as const,
+ setFilterText: vi.fn(),
+ selectLocation: vi.fn(),
+ toggleSidebarCollapsed: vi.fn(),
+ });
+ vi.mocked(useToolbarState).mockReturnValue({
isSplitView: false,
isFocusMode: false,
isPreviewVisible: true,
- onSave: vi.fn(),
- onToggleSplitView: vi.fn(),
- onToggleFocusMode: vi.fn(),
- onTogglePreview: vi.fn(),
- onOpenSettings: vi.fn(),
+ toggleSplitView: vi.fn(),
+ toggleFocusMode: vi.fn(),
+ togglePreviewVisible: vi.fn(),
+ });
+ vi.mocked(useEditorPresentationState).mockReturnValue({
+ theme: "dark",
+ showLineNumbers: true,
+ textWrappingEnabled: true,
+ syntaxHighlightingEnabled: true,
+ fontSize: 16,
+ fontFamily: "IBM Plex Mono",
+ typewriterScrollingEnabled: false,
+ focusDimmingMode: "off",
+ posHighlightingEnabled: false,
+ styleCheckSettings: {
+ enabled: false,
+ categories: { filler: true, redundancy: true, cliche: true },
+ customPatterns: [],
+ },
+ });
+ vi.mocked(useWorkspacePanelSidebarState).mockReturnValue({ sidebarCollapsed: true });
+ vi.mocked(useWorkspacePanelModeState).mockReturnValue({ isSplitView: false, isPreviewVisible: true });
+ vi.mocked(useWorkspacePanelTopBarsCollapsed).mockReturnValue(true);
+ vi.mocked(useWorkspacePanelStatusBarCollapsed).mockReturnValue(true);
+ const sidebar = { handleAddLocation: vi.fn(), handleRemoveLocation: vi.fn(), handleSelectDocument: vi.fn() };
+ const toolbar = { saveStatus: "Idle" as const, onSave: vi.fn(), onOpenSettings: vi.fn() };
+ const tabs = {
+ tabs: [],
+ activeTabId: null,
+ handleSelectTab: vi.fn(),
+ handleCloseTab: vi.fn(),
+ handleReorderTabs: vi.fn(),
};
- const tabs = { tabs: [], activeTabId: null, onSelectTab: vi.fn(), onCloseTab: vi.fn(), onReorderTabs: vi.fn() };
const editor = {
initialText: "# Hidden",
- theme: "dark" as const,
- showLineNumbers: true,
onChange: vi.fn(),
onSave: vi.fn(),
onCursorMove: vi.fn(),
@@ -55,12 +91,10 @@ describe("WorkspacePanel", () => {
editorLine: 1,
onScrollToLine: vi.fn(),
};
- const statusBar = { cursorLine: 1, cursorColumn: 1, wordCount: 0, charCount: 0 };
+ const statusBar = { stats: { cursorLine: 1, cursorColumn: 1, wordCount: 0, charCount: 0 } };
const { container } = render(
{
it("renders sidebar controls and supports resizing", () => {
const onToggleSidebar = vi.fn();
- const layout = {
- sidebarCollapsed: false,
- topBarsCollapsed: true,
- statusBarCollapsed: true,
- isSplitView: false,
- isPreviewVisible: false,
- } as const;
- const sidebar = {
+ vi.mocked(useSidebarState).mockReturnValue({
locations: [],
selectedLocationId: undefined,
selectedDocPath: undefined,
documents: [],
isLoading: false,
filterText: "",
- onAddLocation: vi.fn(),
- onRemoveLocation: vi.fn(),
- onSelectLocation: vi.fn(),
- onSelectDocument: vi.fn(),
- onFilterChange: vi.fn(),
- };
- const toolbar = {
- saveStatus: "Idle" as const,
+ setFilterText: vi.fn(),
+ selectLocation: vi.fn(),
+ toggleSidebarCollapsed: onToggleSidebar,
+ });
+ vi.mocked(useToolbarState).mockReturnValue({
isSplitView: false,
isFocusMode: false,
isPreviewVisible: false,
- onSave: vi.fn(),
- onToggleSplitView: vi.fn(),
- onToggleFocusMode: vi.fn(),
- onTogglePreview: vi.fn(),
- onOpenSettings: vi.fn(),
+ toggleSplitView: vi.fn(),
+ toggleFocusMode: vi.fn(),
+ togglePreviewVisible: vi.fn(),
+ });
+ vi.mocked(useEditorPresentationState).mockReturnValue({
+ theme: "dark",
+ showLineNumbers: true,
+ textWrappingEnabled: true,
+ syntaxHighlightingEnabled: true,
+ fontSize: 16,
+ fontFamily: "IBM Plex Mono",
+ typewriterScrollingEnabled: false,
+ focusDimmingMode: "off",
+ posHighlightingEnabled: false,
+ styleCheckSettings: {
+ enabled: false,
+ categories: { filler: true, redundancy: true, cliche: true },
+ customPatterns: [],
+ },
+ });
+ vi.mocked(useWorkspacePanelSidebarState).mockReturnValue({ sidebarCollapsed: false });
+ vi.mocked(useWorkspacePanelModeState).mockReturnValue({ isSplitView: false, isPreviewVisible: false });
+ vi.mocked(useWorkspacePanelTopBarsCollapsed).mockReturnValue(true);
+ vi.mocked(useWorkspacePanelStatusBarCollapsed).mockReturnValue(true);
+ const sidebar = { handleAddLocation: vi.fn(), handleRemoveLocation: vi.fn(), handleSelectDocument: vi.fn() };
+ const toolbar = { saveStatus: "Idle" as const, onSave: vi.fn(), onOpenSettings: vi.fn() };
+ const tabs = {
+ tabs: [],
+ activeTabId: null,
+ handleSelectTab: vi.fn(),
+ handleCloseTab: vi.fn(),
+ handleReorderTabs: vi.fn(),
};
- const tabs = { tabs: [], activeTabId: null, onSelectTab: vi.fn(), onCloseTab: vi.fn(), onReorderTabs: vi.fn() };
const editor = {
initialText: "# Visible",
- theme: "dark" as const,
- showLineNumbers: true,
onChange: vi.fn(),
onSave: vi.fn(),
onCursorMove: vi.fn(),
@@ -125,12 +173,10 @@ describe("WorkspacePanel", () => {
editorLine: 1,
onScrollToLine: vi.fn(),
};
- const statusBar = { cursorLine: 1, cursorColumn: 1, wordCount: 0, charCount: 0 };
+ const statusBar = { stats: { cursorLine: 1, cursorColumn: 1, wordCount: 0, charCount: 0 } };
render(
{
});
it("renders split mode and supports resizing between editor and preview", () => {
- const layout = {
- sidebarCollapsed: true,
- topBarsCollapsed: true,
- statusBarCollapsed: true,
- isSplitView: true,
- isPreviewVisible: true,
- } as const;
- const sidebar = {
+ vi.mocked(useSidebarState).mockReturnValue({
locations: [],
selectedLocationId: undefined,
selectedDocPath: undefined,
documents: [],
isLoading: false,
filterText: "",
- onAddLocation: vi.fn(),
- onRemoveLocation: vi.fn(),
- onSelectLocation: vi.fn(),
- onSelectDocument: vi.fn(),
- onFilterChange: vi.fn(),
- };
- const toolbar = {
- saveStatus: "Idle" as const,
+ setFilterText: vi.fn(),
+ selectLocation: vi.fn(),
+ toggleSidebarCollapsed: vi.fn(),
+ });
+ vi.mocked(useToolbarState).mockReturnValue({
isSplitView: true,
isFocusMode: false,
isPreviewVisible: true,
- onSave: vi.fn(),
- onToggleSplitView: vi.fn(),
- onToggleFocusMode: vi.fn(),
- onTogglePreview: vi.fn(),
- onOpenSettings: vi.fn(),
+ toggleSplitView: vi.fn(),
+ toggleFocusMode: vi.fn(),
+ togglePreviewVisible: vi.fn(),
+ });
+ vi.mocked(useEditorPresentationState).mockReturnValue({
+ theme: "dark",
+ showLineNumbers: true,
+ textWrappingEnabled: true,
+ syntaxHighlightingEnabled: true,
+ fontSize: 16,
+ fontFamily: "IBM Plex Mono",
+ typewriterScrollingEnabled: false,
+ focusDimmingMode: "off",
+ posHighlightingEnabled: false,
+ styleCheckSettings: {
+ enabled: false,
+ categories: { filler: true, redundancy: true, cliche: true },
+ customPatterns: [],
+ },
+ });
+ vi.mocked(useWorkspacePanelSidebarState).mockReturnValue({ sidebarCollapsed: true });
+ vi.mocked(useWorkspacePanelModeState).mockReturnValue({ isSplitView: true, isPreviewVisible: true });
+ vi.mocked(useWorkspacePanelTopBarsCollapsed).mockReturnValue(true);
+ vi.mocked(useWorkspacePanelStatusBarCollapsed).mockReturnValue(true);
+ const sidebar = { handleAddLocation: vi.fn(), handleRemoveLocation: vi.fn(), handleSelectDocument: vi.fn() };
+ const toolbar = { saveStatus: "Idle" as const, onSave: vi.fn(), onOpenSettings: vi.fn() };
+ const tabs = {
+ tabs: [],
+ activeTabId: null,
+ handleSelectTab: vi.fn(),
+ handleCloseTab: vi.fn(),
+ handleReorderTabs: vi.fn(),
};
- const tabs = { tabs: [], activeTabId: null, onSelectTab: vi.fn(), onCloseTab: vi.fn(), onReorderTabs: vi.fn() };
const editor = {
initialText: "# Split",
- theme: "dark" as const,
- showLineNumbers: true,
onChange: vi.fn(),
onSave: vi.fn(),
onCursorMove: vi.fn(),
@@ -205,12 +265,10 @@ describe("WorkspacePanel", () => {
editorLine: 1,
onScrollToLine: vi.fn(),
};
- const statusBar = { cursorLine: 1, cursorColumn: 1, wordCount: 0, charCount: 0 };
+ const statusBar = { stats: { cursorLine: 1, cursorColumn: 1, wordCount: 0, charCount: 0 } };
render(
{
@@ -143,94 +149,100 @@ describe("appStore", () => {
expect(useAppStore.getState().tabs).toStrictEqual([]);
});
- it("layout selector hooks expose and update layout state", () => {
- const { result: layoutState } = renderHook(() => useLayoutState());
- const { result: layoutActions } = renderHook(() => useLayoutActions());
-
- expect(layoutState.current.sidebarCollapsed).toBeFalsy();
- expect(layoutState.current.topBarsCollapsed).toBeFalsy();
- expect(layoutState.current.statusBarCollapsed).toBeFalsy();
- expect(layoutState.current.lineNumbersVisible).toBeTruthy();
- expect(layoutState.current.textWrappingEnabled).toBeTruthy();
- expect(layoutState.current.syntaxHighlightingEnabled).toBeTruthy();
- expect(layoutState.current.editorFontSize).toBe(16);
- expect(layoutState.current.editorFontFamily).toBe("IBM Plex Mono");
- expect(layoutState.current.isSplitView).toBeFalsy();
- expect(layoutState.current.isFocusMode).toBeFalsy();
+ it("focused layout hooks expose and update layout state", () => {
+ const { result: chromeState } = renderHook(() => useLayoutChromeState());
+ const { result: chromeActions } = renderHook(() => useLayoutChromeActions());
+ const { result: editorState } = renderHook(() => useEditorPresentationState());
+ const { result: editorActions } = renderHook(() => useEditorPresentationActions());
+ const { result: viewModeState } = renderHook(() => useViewModeState());
+ const { result: viewModeActions } = renderHook(() => useViewModeActions());
+
+ expect(chromeState.current.sidebarCollapsed).toBeFalsy();
+ expect(chromeState.current.topBarsCollapsed).toBeFalsy();
+ expect(chromeState.current.statusBarCollapsed).toBeFalsy();
+ expect(editorState.current.lineNumbersVisible).toBeTruthy();
+ expect(editorState.current.textWrappingEnabled).toBeTruthy();
+ expect(editorState.current.syntaxHighlightingEnabled).toBeTruthy();
+ expect(editorState.current.editorFontSize).toBe(16);
+ expect(editorState.current.editorFontFamily).toBe("IBM Plex Mono");
+ expect(viewModeState.current.isSplitView).toBeFalsy();
+ expect(viewModeState.current.isFocusMode).toBeFalsy();
act(() => {
- layoutActions.current.toggleSidebarCollapsed();
- layoutActions.current.toggleTabBarCollapsed();
- layoutActions.current.toggleStatusBarCollapsed();
- layoutActions.current.toggleLineNumbersVisible();
- layoutActions.current.toggleTextWrappingEnabled();
- layoutActions.current.toggleSyntaxHighlightingEnabled();
- layoutActions.current.setEditorFontSize(20);
- layoutActions.current.setEditorFontFamily("Monaspace Neon");
- layoutActions.current.setSplitView(true);
- layoutActions.current.toggleFocusMode();
- layoutActions.current.setPreviewVisible(false);
- layoutActions.current.setShowSearch(true);
+ chromeActions.current.toggleSidebarCollapsed();
+ chromeActions.current.toggleTabBarCollapsed();
+ chromeActions.current.toggleStatusBarCollapsed();
+ chromeActions.current.setShowSearch(true);
+ editorActions.current.toggleLineNumbersVisible();
+ editorActions.current.toggleTextWrappingEnabled();
+ editorActions.current.toggleSyntaxHighlightingEnabled();
+ editorActions.current.setEditorFontSize(20);
+ editorActions.current.setEditorFontFamily("Monaspace Neon");
+ viewModeActions.current.setSplitView(true);
+ viewModeActions.current.toggleFocusMode();
+ viewModeActions.current.setPreviewVisible(false);
});
- expect(layoutState.current.sidebarCollapsed).toBeTruthy();
- expect(layoutState.current.topBarsCollapsed).toBeTruthy();
- expect(layoutState.current.statusBarCollapsed).toBeTruthy();
- expect(layoutState.current.lineNumbersVisible).toBeFalsy();
- expect(layoutState.current.textWrappingEnabled).toBeFalsy();
- expect(layoutState.current.syntaxHighlightingEnabled).toBeFalsy();
- expect(layoutState.current.editorFontSize).toBe(20);
- expect(layoutState.current.editorFontFamily).toBe("Monaspace Neon");
- expect(layoutState.current.isSplitView).toBeTruthy();
- expect(layoutState.current.isFocusMode).toBeTruthy();
- expect(layoutState.current.isPreviewVisible).toBeFalsy();
- expect(layoutState.current.showSearch).toBeTruthy();
- expect(layoutState.current.theme).toBe("dark");
+ expect(chromeState.current.sidebarCollapsed).toBeTruthy();
+ expect(chromeState.current.topBarsCollapsed).toBeTruthy();
+ expect(chromeState.current.statusBarCollapsed).toBeTruthy();
+ expect(chromeState.current.showSearch).toBeTruthy();
+ expect(editorState.current.lineNumbersVisible).toBeFalsy();
+ expect(editorState.current.textWrappingEnabled).toBeFalsy();
+ expect(editorState.current.syntaxHighlightingEnabled).toBeFalsy();
+ expect(editorState.current.editorFontSize).toBe(20);
+ expect(editorState.current.editorFontFamily).toBe("Monaspace Neon");
+ expect(editorState.current.theme).toBe("dark");
+ expect(viewModeState.current.isSplitView).toBeTruthy();
+ expect(viewModeState.current.isFocusMode).toBeTruthy();
+ expect(viewModeState.current.isPreviewVisible).toBeFalsy();
});
it("enabling split view forces preview visible", () => {
- const { result: layoutState } = renderHook(() => useLayoutState());
- const { result: layoutActions } = renderHook(() => useLayoutActions());
+ const { result: viewModeState } = renderHook(() => useViewModeState());
+ const { result: viewModeActions } = renderHook(() => useViewModeActions());
act(() => {
- layoutActions.current.setPreviewVisible(false);
- layoutActions.current.toggleSplitView();
+ viewModeActions.current.setPreviewVisible(false);
+ viewModeActions.current.toggleSplitView();
});
- expect(layoutState.current.isSplitView).toBeTruthy();
- expect(layoutState.current.isPreviewVisible).toBeTruthy();
+ expect(viewModeState.current.isSplitView).toBeTruthy();
+ expect(viewModeState.current.isPreviewVisible).toBeTruthy();
});
- it("workspace selector hooks expose and update workspace state", () => {
- const { result: workspaceState } = renderHook(() => useWorkspaceState());
- const { result: workspaceActions } = renderHook(() => useWorkspaceActions());
+ it("focused workspace hooks expose and update workspace state", () => {
+ const { result: locationsState } = renderHook(() => useWorkspaceLocationsState());
+ const { result: locationsActions } = renderHook(() => useWorkspaceLocationsActions());
+ const { result: documentsState } = renderHook(() => useWorkspaceDocumentsState());
+ const { result: documentsActions } = renderHook(() => useWorkspaceDocumentsActions());
act(() => {
- workspaceActions.current.setLoadingLocations(false);
- workspaceActions.current.setSidebarFilter("draft");
- workspaceActions.current.setDocuments([{
+ locationsActions.current.setLoadingLocations(false);
+ locationsActions.current.setSidebarFilter("draft");
+ documentsActions.current.setDocuments([{
location_id: 1,
rel_path: "a.md",
title: "A",
updated_at: "2024-01-01T00:00:00Z",
word_count: 10,
}]);
- workspaceActions.current.setLoadingDocuments(true);
- workspaceActions.current.addLocation({ id: 9, name: "N", root_path: "/n", added_at: "2024-01-01" });
- workspaceActions.current.removeLocation(9);
+ documentsActions.current.setLoadingDocuments(true);
+ locationsActions.current.addLocation({ id: 9, name: "N", root_path: "/n", added_at: "2024-01-01" });
+ locationsActions.current.removeLocation(9);
});
- expect(workspaceState.current.isLoadingLocations).toBeFalsy();
- expect(workspaceState.current.sidebarFilter).toBe("draft");
- expect(workspaceState.current.documents).toStrictEqual([{
+ expect(locationsState.current.isLoadingLocations).toBeFalsy();
+ expect(locationsState.current.sidebarFilter).toBe("draft");
+ expect(documentsState.current.documents).toStrictEqual([{
location_id: 1,
rel_path: "a.md",
title: "A",
updated_at: "2024-01-01T00:00:00Z",
word_count: 10,
}]);
- expect(workspaceState.current.isLoadingDocuments).toBeTruthy();
- expect(workspaceState.current.locations).toStrictEqual([]);
+ expect(documentsState.current.isLoadingDocuments).toBeTruthy();
+ expect(locationsState.current.locations).toStrictEqual([]);
});
it("tabs selector hooks expose and update tab state", () => {
diff --git a/src/__tests__/pos-highlighting.test.ts b/src/__tests__/pos-highlighting.test.ts
index 717f0fc..b1c97ce 100644
--- a/src/__tests__/pos-highlighting.test.ts
+++ b/src/__tests__/pos-highlighting.test.ts
@@ -16,10 +16,7 @@ describe("posHighlighting", () => {
});
it("should handle empty text", () => {
- const state = EditorState.create({
- doc: "",
- extensions: [posHighlighting(), posHighlightingTheme],
- });
+ const state = EditorState.create({ doc: "", extensions: [posHighlighting(), posHighlightingTheme] });
const view = new EditorView({ state });
expect(view.state.doc.toString()).toBe("");
@@ -27,10 +24,7 @@ describe("posHighlighting", () => {
});
it("should handle whitespace-only text", () => {
- const state = EditorState.create({
- doc: " \n\n ",
- extensions: [posHighlighting(), posHighlightingTheme],
- });
+ const state = EditorState.create({ doc: " \n\n ", extensions: [posHighlighting(), posHighlightingTheme] });
const view = new EditorView({ state });
expect(view.state.doc.toString()).toBe(" \n\n ");
@@ -39,10 +33,7 @@ describe("posHighlighting", () => {
it("should handle long text with viewport", () => {
const longText = "The quick brown fox jumps over the lazy dog. ".repeat(100);
- const state = EditorState.create({
- doc: longText,
- extensions: [posHighlighting(), posHighlightingTheme],
- });
+ const state = EditorState.create({ doc: longText, extensions: [posHighlighting(), posHighlightingTheme] });
const view = new EditorView({ state });
expect(view.state.doc.toString()).toBe(longText);
diff --git a/src/__tests__/usePdfExport.test.tsx b/src/__tests__/usePdfExport.test.tsx
index 0f75714..a3905e0 100644
--- a/src/__tests__/usePdfExport.test.tsx
+++ b/src/__tests__/usePdfExport.test.tsx
@@ -8,6 +8,7 @@ import { writeFile } from "@tauri-apps/plugin-fs";
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { usePdfExport } from "../hooks/usePdfExport";
+import { useAppStore } from "../state/appStore";
vi.mock("$components/pdf/MarkdownPdfDocument", () => ({ MarkdownPdfDocument: () => null }));
@@ -58,6 +59,7 @@ describe(usePdfExport, () => {
beforeEach(() => {
vi.clearAllMocks();
toBlobMock.mockReset();
+ useAppStore.getState().resetPdfExport();
});
it("retries with built-in fonts on any custom render failure", async () => {
@@ -70,7 +72,7 @@ describe(usePdfExport, () => {
let didExport = false;
await act(async () => {
- didExport = await result.current.exportPdf(renderResult, DEFAULT_OPTIONS, "IBM Plex Sans Variable");
+ didExport = await result.current(renderResult, DEFAULT_OPTIONS, "IBM Plex Sans Variable");
});
expect(didExport).toBeTruthy();
@@ -102,14 +104,14 @@ describe(usePdfExport, () => {
const { result } = renderHook(() => usePdfExport());
await act(async () => {
- await expect(result.current.exportPdf(renderResult, DEFAULT_OPTIONS, "IBM Plex Sans Variable")).rejects.toThrow(
+ await expect(result.current(renderResult, DEFAULT_OPTIONS, "IBM Plex Sans Variable")).rejects.toThrow(
"Failed to render PDF using both custom and built-in fonts. Check logs for details.",
);
});
expect(vi.mocked(save)).not.toHaveBeenCalled();
- expect(result.current.state.isExporting).toBeFalsy();
- expect(result.current.state.error).toBe(
+ expect(useAppStore.getState().isExportingPdf).toBeFalsy();
+ expect(useAppStore.getState().pdfExportError).toBe(
"Failed to render PDF using both custom and built-in fonts. Check logs for details.",
);
expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
diff --git a/src/components/DocumentTabs/DocumentTabs.tsx b/src/components/DocumentTabs/DocumentTabs.tsx
index 456575c..b4fd70d 100644
--- a/src/components/DocumentTabs/DocumentTabs.tsx
+++ b/src/components/DocumentTabs/DocumentTabs.tsx
@@ -5,12 +5,14 @@ import { DocumentTab } from "./DocumentTab";
export type DocumentTabsProps = {
tabs: Tab[];
activeTabId: string | null;
- onSelectTab: (tabId: string) => void;
- onCloseTab: (tabId: string) => void;
- onReorderTabs?: (tabs: Tab[]) => void;
+ handleSelectTab: (tabId: string) => void;
+ handleCloseTab: (tabId: string) => void;
+ handleReorderTabs?: (tabs: Tab[]) => void;
};
-export function DocumentTabs({ tabs, activeTabId, onSelectTab, onCloseTab, onReorderTabs }: DocumentTabsProps) {
+export function DocumentTabs(
+ { tabs, activeTabId, handleSelectTab, handleCloseTab, handleReorderTabs }: DocumentTabsProps,
+) {
const [draggingTab, setDraggingTab] = useState(null);
const [dragOverTab, setDragOverTab] = useState(null);
const [contextMenu, setContextMenu] = useState<{ tabId: string; x: number; y: number } | null>(null);
@@ -38,19 +40,19 @@ export function DocumentTabs({ tabs, activeTabId, onSelectTab, onCloseTab, onReo
const handleDrop = useCallback((e: React.DragEvent, targetTabId: string) => {
e.preventDefault();
- if (draggingTab && draggingTab !== targetTabId && onReorderTabs) {
+ if (draggingTab && draggingTab !== targetTabId && handleReorderTabs) {
const newTabs = [...tabs];
const fromIndex = newTabs.findIndex((t) => t.id === draggingTab);
const toIndex = newTabs.findIndex((t) => t.id === targetTabId);
if (fromIndex !== -1 && toIndex !== -1) {
const [movedTab] = newTabs.splice(fromIndex, 1);
newTabs.splice(toIndex, 0, movedTab);
- onReorderTabs(newTabs);
+ handleReorderTabs(newTabs);
}
}
setDraggingTab(null);
setDragOverTab(null);
- }, [draggingTab, onReorderTabs, tabs]);
+ }, [draggingTab, handleReorderTabs, tabs]);
const handleContextMenu = useCallback((e: React.MouseEvent, tabId: string) => {
e.preventDefault();
@@ -59,28 +61,28 @@ export function DocumentTabs({ tabs, activeTabId, onSelectTab, onCloseTab, onReo
const closeContextMenu = useCallback(() => {
if (contextMenu) {
- onCloseTab(contextMenu.tabId);
+ handleCloseTab(contextMenu.tabId);
setContextMenu(null);
}
- }, [contextMenu, onCloseTab]);
+ }, [contextMenu, handleCloseTab]);
const closeOthers = useCallback(() => {
if (contextMenu) {
for (const t of tabs) {
if (t.id !== contextMenu.tabId) {
- onCloseTab(t.id);
+ handleCloseTab(t.id);
}
}
setContextMenu(null);
}
- }, [contextMenu, onCloseTab, tabs]);
+ }, [contextMenu, handleCloseTab, tabs]);
const closeAll = useCallback(() => {
for (const t of tabs) {
- onCloseTab(t.id);
+ handleCloseTab(t.id);
}
setContextMenu(null);
- }, [tabs, onCloseTab]);
+ }, [tabs, handleCloseTab]);
const contextMenuStyle = useMemo(() => contextMenu ? ({ left: contextMenu.x, top: contextMenu.y }) : {}, [
contextMenu,
@@ -109,8 +111,8 @@ export function DocumentTabs({ tabs, activeTabId, onSelectTab, onCloseTab, onReo
handleDragOver={handleDragOver}
handleDrop={handleDrop}
handleContextMenu={handleContextMenu}
- onSelectTab={onSelectTab}
- onCloseTab={onCloseTab} />
+ onSelectTab={handleSelectTab}
+ onCloseTab={handleCloseTab} />
))}
{contextMenu && (
diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx
index 5e063b1..d1165ba 100644
--- a/src/components/Editor.tsx
+++ b/src/components/Editor.tsx
@@ -1,7 +1,8 @@
import { focusDimming, focusDimmingTheme } from "$editor/focus-dimming";
import { posHighlighting, posHighlightingTheme } from "$editor/pos-highlighting";
-import { type StyleMatch, styleCheck, styleCheckTheme } from "$editor/style-check";
+import { styleCheck, styleCheckTheme, type StyleMatch } from "$editor/style-check";
import { typewriterScroll } from "$editor/typewriter-scroll";
+import { useEditorPresentationState } from "$state/panel-selectors";
import { oxocarbonDark } from "$themes/oxocarbon-dark";
import { oxocarbonLight } from "$themes/oxocarbon-light";
import type { AppTheme, EditorFontFamily, FocusDimmingMode, StyleCheckSettings } from "$types";
@@ -15,21 +16,27 @@ import type { CSSProperties } from "react";
export type EditorTheme = AppTheme;
+export type EditorPresentationOverrides = Partial<
+ {
+ theme: EditorTheme;
+ showLineNumbers: boolean;
+ textWrappingEnabled: boolean;
+ syntaxHighlightingEnabled: boolean;
+ fontSize: number;
+ fontFamily: EditorFontFamily;
+ typewriterScrollingEnabled: boolean;
+ focusDimmingMode: FocusDimmingMode;
+ posHighlightingEnabled: boolean;
+ styleCheckSettings: StyleCheckSettings;
+ }
+>;
+
export type EditorProps = {
initialText?: string;
- theme?: EditorTheme;
disabled?: boolean;
- showLineNumbers?: boolean;
- textWrappingEnabled?: boolean;
- syntaxHighlightingEnabled?: boolean;
- fontSize?: number;
- fontFamily?: EditorFontFamily;
placeholder?: string;
debounceMs?: number;
- typewriterScrollingEnabled?: boolean;
- focusDimmingMode?: FocusDimmingMode;
- posHighlightingEnabled?: boolean;
- styleCheckSettings?: StyleCheckSettings;
+ presentation?: EditorPresentationOverrides;
onChange?: (text: string) => void;
onSave?: () => void;
onCursorMove?: (line: number, column: number) => void;
@@ -38,7 +45,10 @@ export type EditorProps = {
className?: string;
};
-type EditorCallbacks = Pick;
+type EditorCallbacks = Pick<
+ EditorProps,
+ "onChange" | "onSave" | "onCursorMove" | "onSelectionChange" | "onStyleMatchesChange"
+>;
type CreateEditorStateOptions = {
doc: string;
@@ -138,19 +148,10 @@ function createEditorState(
export function Editor(
{
initialText = "",
- theme = "dark",
disabled = false,
- showLineNumbers = true,
- textWrappingEnabled = true,
- syntaxHighlightingEnabled = true,
- fontSize = 16,
- fontFamily = "IBM Plex Mono",
placeholder,
debounceMs = 500,
- typewriterScrollingEnabled = false,
- focusDimmingMode = "off",
- posHighlightingEnabled = false,
- styleCheckSettings = { enabled: false, categories: { filler: true, redundancy: true, cliche: true }, customPatterns: [] },
+ presentation,
onChange,
onSave,
onCursorMove,
@@ -159,9 +160,27 @@ export function Editor(
className = "",
}: EditorProps,
) {
+ const defaults = useEditorPresentationState();
+ const theme = presentation?.theme ?? defaults.theme;
+ const showLineNumbers = presentation?.showLineNumbers ?? defaults.showLineNumbers;
+ const textWrappingEnabled = presentation?.textWrappingEnabled ?? defaults.textWrappingEnabled;
+ const syntaxHighlightingEnabled = presentation?.syntaxHighlightingEnabled ?? defaults.syntaxHighlightingEnabled;
+ const fontSize = presentation?.fontSize ?? defaults.fontSize;
+ const fontFamily = presentation?.fontFamily ?? defaults.fontFamily;
+ const typewriterScrollingEnabled = presentation?.typewriterScrollingEnabled ?? defaults.typewriterScrollingEnabled;
+ const focusDimmingMode = presentation?.focusDimmingMode ?? defaults.focusDimmingMode;
+ const posHighlightingEnabled = presentation?.posHighlightingEnabled ?? defaults.posHighlightingEnabled;
+ const styleCheckSettings = presentation?.styleCheckSettings ?? defaults.styleCheckSettings;
+
const containerRef = useRef(null);
const viewRef = useRef(null);
- const callbacksRef = useRef({ onChange, onSave, onCursorMove, onSelectionChange, onStyleMatchesChange });
+ const callbacksRef = useRef({
+ onChange,
+ onSave,
+ onCursorMove,
+ onSelectionChange,
+ onStyleMatchesChange,
+ });
const debounceMsRef = useRef(debounceMs);
const onChangeTimeoutRef = useRef | null>(null);
const initialTextRef = useRef(initialText);
diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx
index ebc2c36..07da568 100644
--- a/src/components/Sidebar/Sidebar.tsx
+++ b/src/components/Sidebar/Sidebar.tsx
@@ -1,5 +1,5 @@
-import { CollapseIcon, LibraryIcon } from "$icons";
-import type { DocMeta, LocationDescriptor } from "$types";
+import { CollapseIcon } from "$icons";
+import { useSidebarState } from "$state/panel-selectors";
import type { ChangeEventHandler, MouseEventHandler } from "react";
import { useCallback, useMemo, useState } from "react";
import { AddButton } from "./AddButton";
@@ -9,26 +9,16 @@ import { SidebarLocationItem } from "./SidebarLocationItem";
import { Title } from "./Title";
export type SidebarProps = {
- locations: LocationDescriptor[];
- selectedLocationId?: number;
- selectedDocPath?: string;
- documents: DocMeta[];
- isCollapsed?: boolean;
- isLoading?: boolean;
- onAddLocation: () => void;
- onRemoveLocation: (locationId: number) => void;
- onSelectLocation: (locationId: number) => void;
- onSelectDocument: (locationId: number, path: string) => void;
- filterText?: string;
- onFilterChange?: (text: string) => void;
- onToggleCollapse?: () => void;
+ handleAddLocation: () => void;
+ handleRemoveLocation: (locationId: number) => void;
+ handleSelectDocument: (locationId: number, path: string) => void;
};
type SidebarActionsProps = {
onAddLocation: () => void;
handleMouseEnter: MouseEventHandler;
handleMouseLeave: MouseEventHandler;
- onToggleCollapse?: () => void;
+ onToggleCollapse: () => void;
};
const HideSidebarButton = ({ onToggleCollapse }: { onToggleCollapse: () => void }) => (
@@ -47,30 +37,30 @@ const SidebarActions = (
) => (
- {onToggleCollapse ?
: null}
+
);
-export function Sidebar(
- {
+export function Sidebar({ handleAddLocation, handleRemoveLocation, handleSelectDocument }: SidebarProps) {
+ const {
locations,
selectedLocationId,
selectedDocPath,
documents,
- isCollapsed = false,
- isLoading = false,
- onAddLocation,
- onRemoveLocation,
- onSelectLocation,
- onSelectDocument,
- filterText = "",
- onFilterChange,
- onToggleCollapse,
- }: SidebarProps,
-) {
+ isLoading,
+ filterText,
+ setFilterText,
+ selectLocation,
+ toggleSidebarCollapsed,
+ } = useSidebarState();
const [expandedLocations, setExpandedLocations] = useState>(() => new Set(locations.map((l) => l.id)));
const [showLocationMenu, setShowLocationMenu] = useState(null);
+ const locationDocuments = useMemo(
+ () => (selectedLocationId ? documents.filter((doc) => doc.location_id === selectedLocationId) : []),
+ [documents, selectedLocationId],
+ );
+
const toggleLocation = useCallback((locationId: number) => {
setExpandedLocations((prev) => {
const next = new Set(prev);
@@ -86,12 +76,12 @@ export function Sidebar(
const filteredDocuments = useMemo(
() =>
filterText
- ? documents.filter((doc) =>
+ ? locationDocuments.filter((doc) =>
doc.title.toLowerCase().includes(filterText.toLowerCase())
|| doc.rel_path.toLowerCase().includes(filterText.toLowerCase())
)
- : documents,
- [documents, filterText],
+ : locationDocuments,
+ [locationDocuments, filterText],
);
const handleMouseEnter: MouseEventHandler = useCallback((e) => {
@@ -103,35 +93,23 @@ export function Sidebar(
}, []);
const handleInputChange: ChangeEventHandler = useCallback((e) => {
- onFilterChange?.(e.currentTarget.value);
- }, [onFilterChange]);
-
- if (isCollapsed) {
- return (
-
- );
- }
+ setFilterText(e.currentTarget.value);
+ }, [setFilterText]);
return (
);
diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx
index 2829d51..19d66b4 100644
--- a/src/components/StatusBar.tsx
+++ b/src/components/StatusBar.tsx
@@ -1,12 +1,16 @@
import type { DocMeta, LineEnding } from "$types";
-export type StatusBarProps = {
- docMeta?: DocMeta | null;
+export type StatusBarStats = {
cursorLine: number;
cursorColumn: number;
wordCount: number;
charCount: number;
selectionCount?: number;
+};
+
+export type StatusBarProps = {
+ docMeta?: DocMeta | null;
+ stats: StatusBarStats;
encoding?: string;
lineEnding?: LineEnding;
};
@@ -70,10 +74,8 @@ const SelectedCount = ({ selectionCount }: { selectionCount: number }) => (
>
);
-export function StatusBar(
- { docMeta, cursorLine, cursorColumn, wordCount, charCount, selectionCount, encoding = "utf8", lineEnding = "LF" }:
- StatusBarProps,
-) {
+export function StatusBar({ docMeta, stats, encoding = "utf8", lineEnding = "LF" }: StatusBarProps) {
+ const { cursorLine, cursorColumn, wordCount, charCount, selectionCount } = stats;
return (