diff --git a/docs/roadmap.md b/docs/roadmap.md index 908d868..70d35eb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -28,8 +28,7 @@ Color text by grammatical role to help writers spot weak phrasing during editing ### Tasks 1. **NLP tokenizer integration** - - Evaluate lightweight browser-side NLP libraries (compromise, wink-nlp) for POS tagging - - Alternatively, implement Rust-side tagging via `nlprule` + - Use wink-nlp (TS) 2. **POS decoration layer** - Map POS tags → color tokens: - Nouns → Red, Verbs → Blue, Adjectives → Brown, Adverbs → Purple, Conjunctions → Green diff --git a/src/App.tsx b/src/App.tsx index 6fd431b..90c1c5a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -386,6 +386,7 @@ function App() { syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled, editorFontSize: layoutState.editorFontSize, editorFontFamily: layoutState.editorFontFamily, + focusModeSettings: layoutState.focusModeSettings, onSetSidebarCollapsed: layoutActions.setSidebarCollapsed, onSetTopBarsCollapsed: layoutActions.setTopBarsCollapsed, onSetStatusBarCollapsed: layoutActions.setStatusBarCollapsed, @@ -394,28 +395,11 @@ function App() { onSetSyntaxHighlightingEnabled: layoutActions.setSyntaxHighlightingEnabled, onSetEditorFontSize: layoutActions.setEditorFontSize, onSetEditorFontFamily: layoutActions.setEditorFontFamily, + onSetTypewriterScrollingEnabled: layoutActions.setTypewriterScrollingEnabled, + onSetFocusDimmingMode: layoutActions.setFocusDimmingMode, onClose: handleSettingsClose, }), - [ - isLayoutSettingsOpen, - layoutState.sidebarCollapsed, - layoutState.topBarsCollapsed, - layoutState.statusBarCollapsed, - layoutState.lineNumbersVisible, - layoutState.textWrappingEnabled, - layoutState.syntaxHighlightingEnabled, - layoutState.editorFontSize, - layoutState.editorFontFamily, - layoutActions.setSidebarCollapsed, - layoutActions.setTopBarsCollapsed, - layoutActions.setStatusBarCollapsed, - layoutActions.setLineNumbersVisible, - layoutActions.setTextWrappingEnabled, - layoutActions.setSyntaxHighlightingEnabled, - layoutActions.setEditorFontSize, - layoutActions.setEditorFontFamily, - handleSettingsClose, - ], + [isLayoutSettingsOpen, layoutState, layoutActions, handleSettingsClose], ); const focusModePanelProps = useMemo( @@ -434,6 +418,7 @@ function App() { editorFontSize: layoutState.editorFontSize, editorFontFamily: layoutState.editorFontFamily, statusBarCollapsed: layoutState.statusBarCollapsed, + focusModeSettings: layoutState.focusModeSettings, onExit: handleExit, onEditorChange: handleEditorChange, onSave: handleSave, @@ -441,7 +426,7 @@ function App() { onSelectionChange: handleSelectionChange, }), [ - layoutState.theme, + layoutState, editorModel.text, activeDocMeta, editorModel.cursorLine, @@ -449,12 +434,6 @@ function App() { wordCount, charCount, selectionCount, - layoutState.lineNumbersVisible, - layoutState.textWrappingEnabled, - layoutState.syntaxHighlightingEnabled, - layoutState.editorFontSize, - layoutState.editorFontFamily, - layoutState.statusBarCollapsed, handleExit, handleEditorChange, handleSave, diff --git a/src/__tests__/Editor.test.tsx b/src/__tests__/Editor.test.tsx index 59e8fce..0d3ae7e 100644 --- a/src/__tests__/Editor.test.tsx +++ b/src/__tests__/Editor.test.tsx @@ -225,5 +225,38 @@ describe(Editor, () => { expect(container.querySelector(".cm-content")).toBe(document.activeElement); }); }); + + it("should apply typewriter scrolling when enabled", () => { + const { container } = render(); + expect(container.querySelector(".cm-editor")).toBeInTheDocument(); + }); + + it("should apply focus dimming in sentence mode", () => { + const { container } = render(); + expect(container.querySelector(".cm-editor")).toBeInTheDocument(); + }); + + it("should apply focus dimming in paragraph mode", () => { + const { container } = render(); + expect(container.querySelector(".cm-editor")).toBeInTheDocument(); + }); + + it("should recreate editor when typewriterScrollingEnabled changes", () => { + const { container, rerender } = render(); + const firstEditorRoot = container.querySelector(".cm-editor"); + + rerender(); + const secondEditorRoot = container.querySelector(".cm-editor"); + expect(secondEditorRoot).not.toBe(firstEditorRoot); + }); + + it("should recreate editor when focusDimmingMode changes", () => { + const { container, rerender } = render(); + const firstEditorRoot = container.querySelector(".cm-editor"); + + rerender(); + const secondEditorRoot = container.querySelector(".cm-editor"); + expect(secondEditorRoot).not.toBe(firstEditorRoot); + }); }); }); diff --git a/src/__tests__/appStore.test.ts b/src/__tests__/appStore.test.ts index c6e76bb..ad70193 100644 --- a/src/__tests__/appStore.test.ts +++ b/src/__tests__/appStore.test.ts @@ -256,4 +256,35 @@ describe("appStore", () => { expect(tabsState.current.tabs[0].id).toBe(secondTabId); expect(tabsState.current.tabs[1].id).toBe(firstTabId); }); + + it("should update typewriter scrolling setting", () => { + const { result } = renderHook(() => useAppStore()); + + act(() => { + result.current.setTypewriterScrollingEnabled(false); + }); + + expect(result.current.focusModeSettings.typewriterScrollingEnabled).toBe(false); + }); + + it("should update focus dimming mode", () => { + const { result } = renderHook(() => useAppStore()); + + act(() => { + result.current.setFocusDimmingMode("paragraph"); + }); + + expect(result.current.focusModeSettings.dimmingMode).toBe("paragraph"); + }); + + it("should toggle typewriter scrolling", () => { + const { result } = renderHook(() => useAppStore()); + const initialValue = result.current.focusModeSettings.typewriterScrollingEnabled; + + act(() => { + result.current.toggleTypewriterScrolling(); + }); + + expect(result.current.focusModeSettings.typewriterScrollingEnabled).toBe(!initialValue); + }); }); diff --git a/src/__tests__/focus-dimming.test.ts b/src/__tests__/focus-dimming.test.ts new file mode 100644 index 0000000..164bad9 --- /dev/null +++ b/src/__tests__/focus-dimming.test.ts @@ -0,0 +1,36 @@ +import { focusDimming, focusDimmingTheme } from "$editor/focus-dimming"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { describe, expect, it } from "vitest"; + +describe("focusDimming", () => { + it("should apply dimming decorations in sentence mode", () => { + const state = EditorState.create({ + doc: "First sentence. Second sentence. Third sentence.", + extensions: [focusDimming("sentence"), focusDimmingTheme], + }); + const view = new EditorView({ state }); + + expect(view.state.doc.toString()).toBe("First sentence. Second sentence. Third sentence."); + view.destroy(); + }); + + it("should apply dimming decorations in paragraph mode", () => { + const state = EditorState.create({ + doc: "Paragraph one.\n\nParagraph two.", + extensions: [focusDimming("paragraph"), focusDimmingTheme], + }); + const view = new EditorView({ state }); + + expect(view.state.doc.toString()).toBe("Paragraph one.\n\nParagraph two."); + view.destroy(); + }); + + it("should not apply dimming when mode is off", () => { + const state = EditorState.create({ doc: "Some text here.", extensions: [focusDimming("off")] }); + const view = new EditorView({ state }); + + expect(view.state.doc.toString()).toBe("Some text here."); + view.destroy(); + }); +}); diff --git a/src/__tests__/typewriter-scroll.test.ts b/src/__tests__/typewriter-scroll.test.ts new file mode 100644 index 0000000..f8ae32a --- /dev/null +++ b/src/__tests__/typewriter-scroll.test.ts @@ -0,0 +1,26 @@ +import { typewriterScroll } from "$editor/typewriter-scroll"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("typewriterScroll", () => { + let view: EditorView; + + beforeEach(() => { + const state = EditorState.create({ + doc: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", + extensions: [typewriterScroll()], + }); + + const originalError = console.error; + console.error = vi.fn(); + view = new EditorView({ state }); + console.error = originalError; + + view.scrollDOM.scrollTo = vi.fn(); + }); + + it("should create editor with typewriter scroll extension", () => { + expect(view.state.doc.toString()).toBe("Line 1\nLine 2\nLine 3\nLine 4\nLine 5"); + }); +}); diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 59a1fe0..d2ea819 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,4 +1,8 @@ -import type { AppTheme, EditorFontFamily } from "$types"; +import { focusDimming, focusDimmingTheme } from "$editor/focus-dimming"; +import { typewriterScroll } from "$editor/typewriter-scroll"; +import { oxocarbonDark } from "$themes/oxocarbon-dark"; +import { oxocarbonLight } from "$themes/oxocarbon-light"; +import type { AppTheme, EditorFontFamily, FocusDimmingMode } from "$types"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; import { EditorState as CMEditorState } from "@codemirror/state"; @@ -7,9 +11,6 @@ import type { ViewUpdate } from "@codemirror/view"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties } from "react"; -import { oxocarbonDark } from "../themes/oxocarbon-dark"; -import { oxocarbonLight } from "../themes/oxocarbon-light"; - export type EditorTheme = AppTheme; export type EditorProps = { @@ -23,6 +24,8 @@ export type EditorProps = { fontFamily?: EditorFontFamily; placeholder?: string; debounceMs?: number; + typewriterScrollingEnabled?: boolean; + focusDimmingMode?: FocusDimmingMode; onChange?: (text: string) => void; onSave?: () => void; onCursorMove?: (line: number, column: number) => void; @@ -42,6 +45,8 @@ type CreateEditorStateOptions = { placeholder?: string; updateListener: ReturnType; onSave: () => void; + typewriterScrollingEnabled: boolean; + focusDimmingMode: FocusDimmingMode; }; const EDITOR_FONT_FAMILY_MAP: Record = { @@ -67,6 +72,8 @@ function createEditorState( placeholder, updateListener, onSave, + typewriterScrollingEnabled, + focusDimmingMode, }: CreateEditorStateOptions, ): CMEditorState { const themeExtension = theme === "dark" ? oxocarbonDark : oxocarbonLight; @@ -93,6 +100,8 @@ function createEditorState( saveKeymap, EditorView.editable.of(!disabled), placeholder ? EditorView.theme({ ".cm-placeholder": { color: "#888" } }) : [], + ...(typewriterScrollingEnabled ? [typewriterScroll()] : []), + ...(focusDimmingMode === "off" ? [] : [focusDimming(focusDimmingMode), focusDimmingTheme]), ], }); } @@ -109,6 +118,8 @@ export function Editor( fontFamily = "IBM Plex Mono", placeholder, debounceMs = 500, + typewriterScrollingEnabled = false, + focusDimmingMode = "off", onChange, onSave, onCursorMove, @@ -129,6 +140,8 @@ export function Editor( textWrappingEnabled, syntaxHighlightingEnabled, placeholder, + typewriterScrollingEnabled, + focusDimmingMode, }); const [isReady, setIsReady] = useState(false); @@ -193,6 +206,8 @@ export function Editor( placeholder: currentPresentation.placeholder, updateListener: createUpdateListener(), onSave: () => callbacksRef.current.onSave?.(), + typewriterScrollingEnabled: currentPresentation.typewriterScrollingEnabled, + focusDimmingMode: currentPresentation.focusDimmingMode, }); const view = new EditorView({ state, parent: containerRef.current }); @@ -238,6 +253,8 @@ export function Editor( && previousPresentation.textWrappingEnabled === textWrappingEnabled && previousPresentation.syntaxHighlightingEnabled === syntaxHighlightingEnabled && previousPresentation.placeholder === placeholder + && previousPresentation.typewriterScrollingEnabled === typewriterScrollingEnabled + && previousPresentation.focusDimmingMode === focusDimmingMode ) { return; } @@ -249,6 +266,8 @@ export function Editor( textWrappingEnabled, syntaxHighlightingEnabled, placeholder, + typewriterScrollingEnabled, + focusDimmingMode, }; const view = viewRef.current; @@ -268,7 +287,17 @@ export function Editor( } viewRef.current = nextView; - }, [createView, disabled, placeholder, showLineNumbers, textWrappingEnabled, syntaxHighlightingEnabled, theme]); + }, [ + createView, + disabled, + placeholder, + showLineNumbers, + textWrappingEnabled, + syntaxHighlightingEnabled, + theme, + typewriterScrollingEnabled, + focusDimmingMode, + ]); useEffect(() => { const view = viewRef.current; diff --git a/src/components/layout/FocusModePanel.tsx b/src/components/layout/FocusModePanel.tsx index 8204267..f98c041 100644 --- a/src/components/layout/FocusModePanel.tsx +++ b/src/components/layout/FocusModePanel.tsx @@ -1,7 +1,7 @@ +import { Editor, type EditorTheme } from "$components/Editor"; +import { StatusBar } from "$components/StatusBar"; import { FocusIcon } from "$icons"; -import type { DocMeta, EditorFontFamily } from "$types"; -import { Editor, type EditorTheme } from "../Editor"; -import { StatusBar } from "../StatusBar"; +import type { DocMeta, EditorFontFamily, FocusModeSettings } from "$types"; type FocusModePanelProps = { theme: EditorTheme; @@ -18,6 +18,7 @@ type FocusModePanelProps = { editorFontSize: number; editorFontFamily: EditorFontFamily; statusBarCollapsed: boolean; + focusModeSettings: FocusModeSettings; onExit: () => void; onEditorChange: (text: string) => void; onSave: () => void; @@ -55,6 +56,7 @@ export const FocusModePanel = ( editorFontSize, editorFontFamily, statusBarCollapsed, + focusModeSettings, onExit, onEditorChange, onSave, @@ -74,6 +76,8 @@ export const FocusModePanel = ( syntaxHighlightingEnabled={syntaxHighlightingEnabled} fontSize={editorFontSize} fontFamily={editorFontFamily} + typewriterScrollingEnabled={focusModeSettings.typewriterScrollingEnabled} + focusDimmingMode={focusModeSettings.dimmingMode} onChange={onEditorChange} onSave={onSave} onCursorMove={onCursorMove} diff --git a/src/components/layout/LayoutSettingsPanel.tsx b/src/components/layout/LayoutSettingsPanel.tsx index 7f139e8..0eab46b 100644 --- a/src/components/layout/LayoutSettingsPanel.tsx +++ b/src/components/layout/LayoutSettingsPanel.tsx @@ -1,5 +1,5 @@ import { XIcon } from "$icons"; -import type { EditorFontFamily } from "$types"; +import type { EditorFontFamily, FocusDimmingMode, FocusModeSettings } from "$types"; import { type ChangeEvent, useCallback } from "react"; type LayoutSettingsPanelProps = { @@ -12,6 +12,7 @@ type LayoutSettingsPanelProps = { syntaxHighlightingEnabled: boolean; editorFontSize: number; editorFontFamily: EditorFontFamily; + focusModeSettings: FocusModeSettings; onSetSidebarCollapsed: (value: boolean) => void; onSetTopBarsCollapsed: (value: boolean) => void; onSetStatusBarCollapsed: (value: boolean) => void; @@ -20,6 +21,8 @@ type LayoutSettingsPanelProps = { onSetSyntaxHighlightingEnabled: (value: boolean) => void; onSetEditorFontSize: (value: number) => void; onSetEditorFontFamily: (value: EditorFontFamily) => void; + onSetTypewriterScrollingEnabled: (enabled: boolean) => void; + onSetFocusDimmingMode: (mode: FocusDimmingMode) => void; onClose: () => void; }; @@ -82,36 +85,28 @@ const EDITOR_FONT_OPTIONS: Array<{ label: string; value: EditorFontFamily }> = [ { label: "Monaspace Xenon", value: "Monaspace Xenon" }, ]; -type FontFamilyRowProps = { - fontFamily: EditorFontFamily; - handleFontChange: (event: ChangeEvent) => void; -}; +type FontFamilyRowProps = { value: EditorFontFamily; setter: (event: ChangeEvent) => void }; -const FontFamilyRow = ({ fontFamily, handleFontChange }: FontFamilyRowProps) => ( +const FontFamilyRow = ({ value, setter }: FontFamilyRowProps) => (
); -const FontSizeRow = ( - { fontSize, handleFontSizeChange }: { - fontSize: number; - handleFontSizeChange: (event: ChangeEvent) => void; - }, -) => ( +const FontSizeRow = ({ value, setter }: { value: number; setter: (event: ChangeEvent) => void }) => (
- {fontSize}px + {value}px
); +type DimmingModeRowProps = { value: FocusDimmingMode; setter: (dimmingMode: FocusDimmingMode) => void }; + +function DimmingModeRow({ value, setter }: DimmingModeRowProps) { + const handleDimmingModeChange = useCallback((event: ChangeEvent) => { + setter(event.target.value as FocusDimmingMode); + }, [setter]); + + const options = [{ value: "off", label: "Off" }, { value: "sentence", label: "Current Sentence" }, { + value: "paragraph", + label: "Current Paragraph", + }]; + + return ( +
+ + +

Dim all text except the active region.

+
+ ); +} + export function LayoutSettingsPanel( { isVisible, @@ -136,6 +157,7 @@ export function LayoutSettingsPanel( syntaxHighlightingEnabled, editorFontSize, editorFontFamily, + focusModeSettings, onSetSidebarCollapsed, onSetTopBarsCollapsed, onSetStatusBarCollapsed, @@ -144,6 +166,8 @@ export function LayoutSettingsPanel( onSetSyntaxHighlightingEnabled, onSetEditorFontSize, onSetEditorFontFamily, + onSetTypewriterScrollingEnabled, + onSetFocusDimmingMode, onClose, }: LayoutSettingsPanelProps, ) { @@ -220,8 +244,19 @@ export function LayoutSettingsPanel( description="Enable Markdown syntax colors and token styling." isVisible={syntaxHighlightingEnabled} onToggle={toggleSyntaxHighlighting} /> - - + + + +
+

Focus Mode

+ + + +
); diff --git a/src/editor/focus-dimming.ts b/src/editor/focus-dimming.ts new file mode 100644 index 0000000..583017e --- /dev/null +++ b/src/editor/focus-dimming.ts @@ -0,0 +1,84 @@ +import { RangeSetBuilder } from "@codemirror/state"; +import type { Extension } from "@codemirror/state"; +import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view"; +import type { FocusDimmingMode } from "../types"; + +const DIMMED_CLASS = "cm-dimmed-text"; + +function findSentenceBoundaries(text: string): Array<{ start: number; end: number }> { + const sentences: Array<{ start: number; end: number }> = []; + const sentenceRegex = /[^.!?]+[.!?]+\s*/g; + let match; + + while ((match = sentenceRegex.exec(text)) !== null) { + sentences.push({ start: match.index, end: match.index + match[0].length }); + } + + if (sentences.length === 0 || sentences.at(-1)!.end < text.length) { + const lastStart = sentences.length > 0 ? sentences.at(-1)!.end : 0; + if (lastStart < text.length) { + sentences.push({ start: lastStart, end: text.length }); + } + } + + return sentences; +} + +function createDimmingDecorations(view: EditorView, mode: FocusDimmingMode): DecorationSet { + if (mode === "off") { + return Decoration.none; + } + + const cursorPos = view.state.selection.main.head; + const doc = view.state.doc; + const builder = new RangeSetBuilder(); + + for (let i = 1; i <= doc.lines; i++) { + const line = doc.line(i); + const lineText = line.text; + + if (lineText.trim().length === 0) continue; + + let regions: Array<{ start: number; end: number }>; + + if (mode === "sentence") { + regions = findSentenceBoundaries(lineText); + regions = regions.map((r) => ({ start: line.from + r.start, end: line.from + r.end })); + } else { + regions = [{ start: line.from, end: line.to }]; + } + + const activeRegion = regions.find((r) => cursorPos >= r.start && cursorPos <= r.end); + + for (const region of regions) { + if (!activeRegion || region.start !== activeRegion.start) { + builder.add(region.start, region.end, Decoration.mark({ class: DIMMED_CLASS })); + } + } + } + + return builder.finish(); +} + +export function focusDimming(mode: FocusDimmingMode): Extension { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(private view: EditorView) { + this.decorations = createDimmingDecorations(view, mode); + } + + update(update: ViewUpdate) { + if (update.selectionSet || update.docChanged || update.viewportChanged) { + this.decorations = createDimmingDecorations(this.view, mode); + } + } + }, + { decorations: (v) => v.decorations }, + ); +} + +export const focusDimmingTheme = EditorView.theme({ + [`.${DIMMED_CLASS}`]: { opacity: "0.3", transition: "opacity 0.15s ease-in-out" }, +}); diff --git a/src/editor/typewriter-scroll.ts b/src/editor/typewriter-scroll.ts new file mode 100644 index 0000000..b40d52f --- /dev/null +++ b/src/editor/typewriter-scroll.ts @@ -0,0 +1,36 @@ +import type { Extension } from "@codemirror/state"; +import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view"; + +export function typewriterScroll(): Extension { + return ViewPlugin.fromClass( + class { + constructor(private view: EditorView) { + this.centerCursor(); + } + + update(update: ViewUpdate) { + if (update.selectionSet || update.docChanged) { + this.centerCursor(); + } + } + + private centerCursor() { + const view = this.view; + const cursorPos = view.state.selection.main.head; + const coords = view.coordsAtPos(cursorPos); + + if (!coords) return; + + const scroller = view.scrollDOM; + const scrollerRect = scroller.getBoundingClientRect(); + const scrollerHeight = scrollerRect.height; + const cursorY = coords.top - scrollerRect.top; + const targetY = scrollerHeight / 2; + + const scrollOffset = cursorY - targetY + scroller.scrollTop; + + scroller.scrollTo({ top: scrollOffset, behavior: "smooth" }); + } + }, + ); +} diff --git a/src/state/appStore.ts b/src/state/appStore.ts index 7d79903..e79d668 100644 --- a/src/state/appStore.ts +++ b/src/state/appStore.ts @@ -1,4 +1,13 @@ -import type { AppTheme, DocMeta, DocRef, EditorFontFamily, LocationDescriptor, Tab } from "$types"; +import type { + AppTheme, + DocMeta, + DocRef, + EditorFontFamily, + FocusDimmingMode, + FocusModeSettings, + LocationDescriptor, + Tab, +} from "$types"; import { create, type StateCreator } from "zustand"; import { useShallow } from "zustand/react/shallow"; @@ -24,6 +33,7 @@ export type LayoutState = { isPreviewVisible: boolean; showSearch: boolean; theme: AppTheme; + focusModeSettings: FocusModeSettings; }; export type LayoutActions = { @@ -45,6 +55,10 @@ export type LayoutActions = { toggleSplitView: () => void; setFocusMode: (value: boolean) => void; toggleFocusMode: () => void; + setFocusModeSettings: (settings: FocusModeSettings) => void; + setTypewriterScrollingEnabled: (enabled: boolean) => void; + setFocusDimmingMode: (mode: FocusDimmingMode) => void; + toggleTypewriterScrolling: () => void; setPreviewVisible: (value: boolean) => void; togglePreviewVisible: () => void; setShowSearch: (value: boolean) => void; @@ -99,6 +113,7 @@ const getInitialLayoutState = (): LayoutState => ({ isPreviewVisible: true, showSearch: false, theme: "dark", + focusModeSettings: { typewriterScrollingEnabled: true, dimmingMode: "sentence" }, }); const getInitialWorkspaceState = (): WorkspaceState => ({ @@ -149,6 +164,19 @@ const createLayoutSlice: StateCreator set({ isFocusMode: value }), toggleFocusMode: () => set((state) => ({ isFocusMode: !state.isFocusMode })), + setFocusModeSettings: (settings) => set({ focusModeSettings: settings }), + setTypewriterScrollingEnabled: (enabled) => + set((state) => ({ focusModeSettings: { ...state.focusModeSettings, typewriterScrollingEnabled: enabled } })), + setFocusDimmingMode: (mode) => + set((state) => ({ focusModeSettings: { ...state.focusModeSettings, dimmingMode: mode } })), + toggleTypewriterScrolling: () => + set((state) => ({ + focusModeSettings: { + ...state.focusModeSettings, + typewriterScrollingEnabled: !state.focusModeSettings.typewriterScrollingEnabled, + }, + })), + setPreviewVisible: (value) => set({ isPreviewVisible: value }), togglePreviewVisible: () => set((state) => ({ isPreviewVisible: !state.isPreviewVisible })), @@ -305,6 +333,7 @@ export const useLayoutState = () => isPreviewVisible: state.isPreviewVisible, showSearch: state.showSearch, theme: state.theme, + focusModeSettings: state.focusModeSettings, })), ); @@ -329,6 +358,10 @@ export const useLayoutActions = () => toggleSplitView: state.toggleSplitView, setFocusMode: state.setFocusMode, toggleFocusMode: state.toggleFocusMode, + setFocusModeSettings: state.setFocusModeSettings, + setTypewriterScrollingEnabled: state.setTypewriterScrollingEnabled, + setFocusDimmingMode: state.setFocusDimmingMode, + toggleTypewriterScrolling: state.toggleTypewriterScrolling, setPreviewVisible: state.setPreviewVisible, togglePreviewVisible: state.togglePreviewVisible, setShowSearch: state.setShowSearch, diff --git a/src/types.ts b/src/types.ts index b300052..5b95854 100644 --- a/src/types.ts +++ b/src/types.ts @@ -76,3 +76,7 @@ export type SearchHit = { }; export type AppError = { code: ErrorCode; message: string; context?: string }; + +export type FocusDimmingMode = "off" | "sentence" | "paragraph"; + +export type FocusModeSettings = { typewriterScrollingEnabled: boolean; dimmingMode: FocusDimmingMode }; diff --git a/tsconfig.json b/tsconfig.json index b5151e8..9adffdc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,8 +21,10 @@ "$types": ["./types.ts"], "$logger": ["./logger.ts"], "$icons": ["./components/icons.tsx"], + "$editor/*": ["./editor/*"], "$pdf/*": ["./pdf/*"], - "$components/*": ["./components/*"] + "$components/*": ["./components/*"], + "$themes/*": ["./themes/*"] } }, "include": ["src"], diff --git a/vite.config.ts b/vite.config.ts index ad269c7..7e921bb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,16 +20,6 @@ const test: ViteUserConfig["test"] = { const host = process.env.TAURI_DEV_HOST; const resolveFromRoot = (path: string) => fileURLToPath(new URL(path, import.meta.url)); -// TODO: aliases here and in tsconfig.json -// - $hooks -// - $state -// - $ports -// - $state -// - $utils -// - $types -// - $logger -// - $components -// - $icons export default defineConfig({ plugins: [react(), tailwindcss()], clearScreen: false, @@ -49,6 +39,8 @@ export default defineConfig({ "$icons": resolveFromRoot("./src/components/icons.tsx"), "$pdf": resolveFromRoot("./src/pdf"), "$components": resolveFromRoot("./src/components"), + "$editor": resolveFromRoot("./src/editor"), + "$themes": resolveFromRoot("./src/themes"), }, }, });