Skip to content

Repository files navigation

🖋 For writers · 🛠 For developers · 🛡 Reliability first · ⬇ Download

A desktop writing room for long-form fiction — offline, crash-safe, and warm to look at.
Electron · React · TypeScript · TipTap. Single user, offline-first, Windows.


What it is

Scriptorium Writer is a local-first desktop app for writing long books. It looks like a warm parchment page in a leather frame, saves constantly and safely, keeps a full version history of every chapter, and checks spelling in Russian and English at the same time — all completely offline.

Your stories live in a plain, syncable folder on your own computer — one file per chapter — not locked inside a database. Priorities, in order: (1) reliability of your data, (2) comfort of writing, (3) warm "book" aesthetics.

Why it's different — reliability first

Most of this app exists to make sure you never lose a word. Manual Save and every kind of autosave share one write path, and that path is always atomic — so a crash, a full disk, or closing the laptop mid-save can never corrupt what you already wrote.

  • JSON canon + Markdown shadow. Each chapter is stored as lossless ProseMirror JSON (the only file the app reads back) plus a best-effort .md copy any tool can read. The .md write can fail without ever endangering the canon.
  • Atomic writes only. Temp file → fsyncrename. Never written in place.
  • Version snapshots on every successful save, pruned to a configurable cap — roll any chapter back to an older draft from inside the app.
  • Never blanks corrupt data. A missing or unparseable canon triggers a one-click recovery from the newest snapshot; the bad file is never overwritten silently.
  • Quit guard. Closing the app waits for a final flush (or a 5-second safety timeout) so a stuck window can never lose your last keystrokes — or wedge shut.

What's inside

Capability What it does
Book-themed editor TipTap/ProseMirror parchment page: italic/bold/strike, alignment, undo/redo, a first-line-indent toggle, ✳✳✳ scene dividers, and a distraction-free focus mode.
Autosave & quit guard 2 s debounce + 2 min dirty-interval + lifecycle flush, all sharing manual Save's single write path; the quit guard never lets the app exit with unsaved work.
Version history A timestamped snapshot per save; preview any older draft read-only, or restore it (restoring snapshots the current state first).
Footnotes Inline footnotes with [N] markers auto-numbered by document order — hover to read, select to edit; round-trip losslessly.
Offline spellcheck (RU + EN) Simultaneous Russian + English Chromium spellcheck from bundled dictionaries, with native suggestions and add-to-dictionary — no network.
Navigation & views Sidebar router: Editor, Chapters (native drag-to-reorder), Story info, Notes codex, Statistics + writing streak, Library, Settings — settings apply live.
Cleanup wand Runs pluggable text-cleanup rules (spacing, punctuation, - em dash, straight " → «guillemets») over a selection or the whole chapter as one undoable transaction behind a diff preview.
Find & Replace Non-modal bar with live highlighting, case-sensitive / whole-word (Cyrillic-aware) toggles, and a single-transaction Replace-all.
Full-text search Read-only «Поиск» view sweeps every chapter's canon and all Notes sections for a phrase, listing per-source hit counts with context snippets; a chapter hit opens the chapter with the Find bar seeded to highlight in place.
Import / Export Import a .md/.docx file as one chapter or split by headings; export any chapter or the whole story to .docx/.md.
Library backup One click zips the entire library folder (stories, snapshots, notes, .trash/) to a chosen path — atomic, read-only against your data.
Auto-update Packaged builds check GitHub Releases in the background and offer a dismissible restart that routes through the same quit-guard flush.
UI language (RU / EN) Switch the whole interface between Русский and English live from Settings — no restart. Defaults to Russian; only the app's own chrome and system messages change, never your story text.

🖋 For Writers / Для писателей

If you just want to open the app and write — this part is for you. No code: download, open, write. Available in English and Russian.

🇬🇧 Read in English

What this is. A Windows app for writing long fiction comfortably: a warm, book-page look, autosave, version history (roll back to an older draft of a chapter), footnotes, and spellcheck in Russian and English at the same time — all of it working offline.

How to install

  1. Open the Releases page.
  2. Under Assets, download Scriptorium Writer Setup <version>.exe.
  3. Run the downloaded file.
  4. Windows will likely show a "Windows protected your PC" warning — that's expected; the installer just doesn't have a paid code-signing certificate yet. Click "More info", then "Run anyway".
  5. Go through the normal install wizard — the defaults are fine. A shortcut appears on the desktop and in the Start menu.
  6. Launch Scriptorium Writer. The first run opens a demo chapter, just so there's something to look at — delete it and start your own story whenever you like.

Where your stories live

Every story lives in a plain folder on your computer:

Documents/Scriptorium-Writer/

It's a normal folder, not a locked-away database inside the app. You can:

  • copy it to a USB drive;
  • sync it through OneDrive, Dropbox, Google Drive, or anything similar;
  • open it in File Explorer and see what's inside (one file per chapter).

If something goes wrong

  • Every save creates a version snapshot — older drafts of a chapter never disappear; you can view and restore them from version history inside the app.
  • The app never overwrites a good file with a broken one — if something's wrong, it offers to restore the chapter from its last good snapshot.
  • If you close the app while it's still saving, it waits for the save to finish before it actually closes.

If a problem isn't solved by any of this, open an issue on GitHub or reach out to the author directly.

🇷🇺 Читать по-русски

Что это. Программа для Windows, в которой удобно писать длинные истории: тёплое оформление под книжную страницу, автосохранение, история версий (можно откатиться к более старому варианту главы), сноски, орфография сразу на русском и английском — и всё это работает без интернета.

Как установить

  1. Откройте страницу Releases.
  2. В разделе Assets скачайте файл Scriptorium Writer Setup <версия>.exe.
  3. Запустите скачанный файл.
  4. Windows, скорее всего, покажет предупреждение «Windows защитил ваш компьютер» — это нормально, просто у установщика пока нет платной цифровой подписи. Нажмите «Дополнительно», затем «Выполнить в любом случае».
  5. Пройдите обычный мастер установки — можно оставить все настройки по умолчанию. На рабочем столе и в меню «Пуск» появится ярлык.
  6. Запустите Scriptorium Writer. При первом запуске откроется демонстрационная глава — просто чтобы было с чем начать. Её можно удалить и начать свою историю.

Где хранятся ваши истории

Все истории лежат в обычной папке на компьютере:

Документы/Scriptorium-Writer/

Это обычная папка, а не закрытая база данных где-то внутри программы. Её можно:

  • скопировать на флешку;
  • синхронизировать через Яндекс.Диск, Dropbox, Google Диск или любой похожий сервис;
  • открыть в проводнике и посмотреть, что внутри (там по одному файлу на главу).

Если что-то пошло не так

  • Каждое сохранение создаёт снимок версии — старые варианты главы никуда не пропадают, их можно посмотреть и вернуть через историю версий внутри программы.
  • Программа никогда не сохраняет повреждённый файл поверх исправного — если что-то не так, она предложит восстановить главу из последнего исправного снимка.
  • Если закрыть программу во время печати, она сама дождётся, пока сохранение закончится, и только потом закроется.

Если проблема не решается этими шагами — создайте issue на GitHub или напишите автору напрямую.


🛠 For Developers

Architecture, scripts, the on-disk data format, and how the pieces fit together. Design source of truth: docs/SPEC.md.

Requirements

  • Node.js ≥ 22.12 (LTS) — required by the current Electron 43 / electron-builder 26 toolchain, which require()s ESM-only dependencies (only supported on Node 22.12+). On older Node, npm run build:win fails with ERR_REQUIRE_ESM.
    • Windows install: winget install OpenJS.NodeJS.LTS (currently installs Node 24 LTS), then open a new terminal.
  • npm 10+ (bundled with Node; Node 24 ships npm 11).

Getting started (from source)

npm install
npm run dev      # opens the book-themed editor on a seeded demo chapter

npm run dev opens the book-framed writing surface. On first run the app seeds a demo story so there is something to edit; edits persist through the window.api IPC bridge, proving the main → preload (contextBridge) → renderer path end to end.

npm 11 note: npm 11 blocks package install scripts by default, so npm install may not download the Electron binary. If npm run dev fails with Error: Electron uninstall, fetch it once with node node_modules/electron/install.js (or allow it via npm approve-scripts electron), then re-run npm run dev.

Scripts

Script What it does
npm run dev Launch the app in development with HMR (renderer) and DevTools.
npm run dev:web Launch the browser build (MP3) on a LAN-reachable dev server (see below).
npm run build Typecheck, then build main / preload / renderer into out/.
npm run build:web Typecheck, then build the browser bundle into dist-web/.
npm run build:win Full build + package a Windows NSIS installer into release/.
npm run start Preview the production build (electron-vite preview).
npm run typecheck Type-check the node (main/preload/shared) and web (renderer) projects.
npm run lint ESLint over src (.ts/.tsx).
npm run test Run the Vitest unit suite (data layer, Node env) once.
npm run test:watch Run Vitest in watch mode.
npm run test:browser Run the browser-only suite (*.browser.test.ts, e.g. the OPFS FsPort contract) in real Chromium via Vitest browser mode.
npm run format Prettier-format src.

Web build (MP3)

The same React renderer also builds as a plain browser app — a second Vite target (vite.web.config.ts) that reuses src/renderer with a browser composition root (main.web.tsxcreateWebPlatform()), no Electron, no preload.

  • npm run dev:web — serves the web build at the root URL (http://localhost:5173/), with --host so a tablet on the same LAN can reach it. Storage is OPFS-backed (Origin Private File System), so your stories persist across page reloads.
  • npm run build:web — typechecks, then bundles the browser build into dist-web/.

The persistent browser FsPortOpfsFsPort (Origin Private File System) — is implemented in src/platform/web/, proven against the shared FsPort contract in real Chromium, and wired into createWebPlatform() (MP4). Because the reliable OPFS write path (createSyncAccessHandle) is worker-only, writes are delegated to opfs-worker.ts (with per-path serialization so overlapping saves to the same file never collide); reads/dir-ops run on the main thread. OPFS has no separate fsync barrier, so each writeFile flush()es before close — durability lives inside the write, which keeps atomicWriteFile's tmp+rename atomic.

Import/export work in the web build too: import reads a .md/.docx via a file picker, chapter/story/library export download files to the browser's Downloads folder (library export is a .zip built with fflate; desktop still uses archiver). The .docx importer sanitises Word's HTML by parsing it against the editor schema — the same boundary on every platform.

PWA (MP7)

The web build is an installable, offline-capable PWA:

  • Install: open the served URL in Chrome and choose "Установить приложение" / add-to-home-screen. It launches standalone (no browser chrome) with the book icon and the leather-frame splash colour (--book-frame).
  • Offline: a service worker (vite-plugin-pwa, Workbox generateSW) precaches the app shell (JS/CSS/bundled fonts/icons). After the first load the app opens with no network — the library lives in OPFS. There is intentionally no runtime network caching, because the app makes no network requests after load.
  • Updates: registerType: 'prompt' — a new deploy shows a dismissible "Доступна новая версия" strip (WebUpdateNotice) instead of reloading unprompted; the user taps "Обновить" when ready. This is separate from the desktop UpdateNotice, sharing only the visual style.
  • Dev over HTTPS: npm run dev:web uses vite-plugin-mkcert to serve HTTPS, required for service-worker registration and OPFS persistence when testing from a tablet on the LAN. The first time, install the mkcert local CA on the tablet (or tap through the certificate warning).
  • Icons: npm run gen:icon emits the desktop .ico and the PWA PNGs (192, 512, and a 512 maskable) into src/renderer/public/icons/ from one source of truth.

On touch devices the web build adapts without changing the desktop UI: hover-only controls become visible, tap targets grow to ~44px, and the editor column uses dynamic viewport height (dvh + interactive-widget=resizes-content) so the soft keyboard never covers the Find & Replace or cleanup bars. These are gated on (pointer: coarse), so the desktop layout is unchanged.

The Electron build is unchanged and still owns src/renderer/index.html (→ main.electron.tsx).

Spellcheck in the web build is provided by the device's own system (e.g. the Android keyboard), not by the app's bundled dictionaries — the desktop build is unaffected and still checks offline with the bundled Hunspell dictionaries.

Architecture

Standard Electron three-process split. The security boundary is strict and must not be weakened:

  • main (src/main/) — window lifecycle, IPC handlers, spellcheck, docx/zip, and the Electron wiring that hands FileService a Node filesystem port.
  • data (src/data/) — the platform-neutral data layer: FileService (atomic writes, snapshots, scan/restore), Markdown backup, path helpers. All disk work goes through an injected FsPort (src/data/fs-port.ts) — no direct Node imports, with one remaining exception: exportLibraryArchive still reaches into src/main/library-archive (Node + archiver), to be hoisted behind a port in a later milestone.
  • platform (src/platform/node/, src/platform/web/) — per-platform FsPort implementations. NodeFsPort (a thin adapter over node:fs/promises) for Electron; OpfsFsPort (+ opfs-worker.ts) over the browser Origin Private File System, plus MemoryFsPort scaffolding. Android (Capacitor) plugs in here later without touching the data layer.
  • preload (src/preload/) — a typed contextBridge window.api surface; thin wrappers over ipcRenderer.invoke.
  • renderer (src/renderer/) — React UI. Never imports fs, path, or any Node built-in. All disk/OS work goes through window.api. Enforced by contextIsolation: true / nodeIntegration: false.

Shared domain types live once in src/shared/types.ts and are imported by all three processes; schema-version constants live in src/shared/schema.ts. The book theme is driven entirely by CSS tokens in src/renderer/theme/book.css (palette + page-stack texture — no hard-coded colours elsewhere).

Where data lives

  • Library (your stories) — a plain, syncable folder. Default: Documents/Scriptorium-Writer/. A normal directory you can back up, sync, or open in a file manager. Settings → «Экспортировать библиотеку» also bundles the whole folder into a single .zip in one click.
  • Settings — per-machine, in Electron's userData/settings.json, which holds libraryPath so each machine knows where its library is. Deliberate: settings stay per-machine while the library travels.

On-disk layout

Scriptorium-Writer/               ← library root (libraryPath)
  stories/
    <story-id>/
      story.json                  ← StoryMeta + chapterOrder + schemaVersion
      chapters/
        01-slug.json              ← Chapter canon (ProseMirror JSON) — SOURCE OF TRUTH
        01-slug.md                ← Human-readable Markdown backup (lossy, write-only)
      versions/
        <chapterId>/
          2026-07-09T10-15-00-123Z.json   ← per-chapter snapshots (pruned to a cap)
      notes/
        notes.json
  .trash/                         ← soft-deleted stories/chapters land here

NN-slug filenames are for human legibility only; the app always resolves chapters by the stable id stored inside each file, never by trusting a filename — so files may be renamed safely. Alignment lives in the JSON canon (Markdown can't carry it), which is why the canon is JSON, not Markdown.

Project layout

electron.vite.config.ts   # main / preload / renderer build config
electron-builder.yml      # Windows NSIS packaging + GitHub publish (auto-update) config
src/
  main/                   # Electron wiring: IPC, spellcheck, docx/zip, auto-update, library-archive
  data/                   # Platform-neutral data layer: FileService, atomic-write, snapshots, markdown, paths (injected FsPort — no Node imports)
  platform/node/          # NodeFsPort — the Node/Electron FsPort implementation (only place in the data path that touches node:fs)
  platform/web/           # OpfsFsPort (+ opfs-worker) over OPFS, and MemoryFsPort scaffolding — browser FsPort implementations (no node: imports)
  preload/                # contextBridge → window.api (typed, decodes AppError)
  renderer/
    theme/book.css        # book theme tokens + page-stack texture
    store/                # zustand stores: editorStore, storyStore, settingsStore, uiStore
    editor/               # TipTap editor, toolbar, footer, SceneDivider + Footnote, cleanup wand, find & replace, import
    views/                # Editor, Library, Chapters, StoryInfo, Notes, Statistics, Settings, VersionHistory
    components/           # AppFrame (leather frame + grid), Sidebar
  shared/                 # types.ts (IPC contract), schema.ts, errors.ts, word-count.ts, footnote-markdown.ts
out/                      # build output (gitignored)
release/                  # packaged installers (gitignored)
assets/readme/            # README hero + diagrams (SVG)

Unit tests live next to their modules as *.test.ts (run by Vitest).

Feature reference (deep dive)

Expand — per-feature notes on how each capability works

Editor core. A TipTap 2 / ProseMirror surface styled as a parchment page (src/renderer/editor/), themed via book.css. Content is ProseMirror JSON — the same canon the data layer persists. The Tab control toggles a global .indent-on first-line indent (a per-chapter view preference, not stored tab characters); the scene divider is a real custom block node (SceneDivider.ts). State lives in two small Zustand stores (editorStore, uiStore); word counting is single-sourced in src/shared/word-count.ts so the on-screen count equals what main computes on save.

Autosave & version history. Manual Save and autosave share one flush() write path: a 2 s debounce, a 2 min dirty-interval, and lifecycle flushes (chapter switch, window blur, before quit). A main-process quit guard delays exit until the renderer confirms a final flush, or a 5 s safety timeout elapses. Version History opens the snapshot list for the current chapter — preview read-only, or restore (which snapshots the current state first). On startup, scanLibrary() flags any chapter whose canon is missing or won't parse and offers a one-click restore from its newest snapshot; the corrupt file is never silently overwritten.

Footnotes. A custom inline-atom node (Footnote.ts); the toolbar [?] inserts one. Each footnote stores only its text in the canon; the visible [N] marker is derived at render by document order (footnote-numbering.ts), never stored — so inserting, deleting, or moving footnotes always renumbers correctly. Hover a marker to read it; select it to edit. Footnote text lives in an attribute, so it does not count toward the word count. The Markdown mapping ([^n] + definitions) lives in src/shared/footnote-markdown.ts, reused by the .md backup serializer.

Spellcheck (offline, RU + EN). Editor-only — it affects Chromium's underlines and context menu, never persistence. Main starts a loopback HTTP server on 127.0.0.1 and points session.setSpellCheckerDictionaryDownloadURL at it; the server matches Chromium's version-suffixed request filename by language prefix, so the bundled .bdic is served regardless of suffix — surviving Chromium version bumps. Dictionaries (resources/dictionaries/*.bdic) are bundled via electron-builder extraResources so offline spellcheck works in packaged builds too.

Navigation & views. The sidebar drives a view router keyed on uiStore.activeView: РАБОТА (Editor, Chapters, Story info, Version history, Notes, Search, Statistics) and ОБЩЕЕ (Library, Settings). Chapters supports native HTML5 drag-to-reorder (no dependency). Notes is a per-story codex (characters / locations / world / timeline + scratchpad), saved debounced. Statistics shows totals, a per-chapter breakdown, and a daily writing streak (streak data in renderer localStorage, not part of the canon). Settings apply live via settingsStore — font, autosave interval, spellcheck languages (through an applySpellLanguages IPC), and UI language (RU / EN) all take effect without a restart.

UI localization (RU / EN). Every static interface string and renderer-surfaced system/error message lives in a hand-rolled, dependency-free dictionary at src/renderer/i18n/strings.ts (parallel ru / en trees), read through the reactive useT() hook. The active language is Settings.language. On a genuine first run it is seeded from the OS locale (app.getLocale() — English → 'en', otherwise 'ru'); any existing install keeps its stored value, and one predating the language key stays Russian (readSettings' merge-over-defaults never applies the OS seed). en is typed against typeof ru, so a missing/extra key fails npm run typecheck, and strings.test.ts asserts identical ru/en key sets at runtime. Dates render per-locale (ru-RU / en-US), and counts are properly pluralized per language — a tiny plural() engine (src/renderer/i18n/plural.ts) applies the Russian one/few/many rule and English singular/plural, so word/chapter counts read "1 слово / 2 слова / 5 слов" and "1 word / 2 words". Author content — story/chapter/notes text, titles, footnote text — is never routed through the dictionary and never changes with the switch. New chapters seed a language-appropriate default title at creation time («Новая глава» / "New chapter"), which then becomes ordinary author data; existing titles are never rewritten. The first-run demo story stays Russian by design (its body is a Russian writing sample).

Cleanup wand. The toolbar wand (src/renderer/editor/cleanup/) runs an ordered set of pure (text) => string rules over the selection — or the whole chapter when nothing is selected — and applies them as one undoable transaction behind an inline diff preview. Rules: collapse multiple spaces, normalize punctuation spacing, fix stray spaces in hyphenated words, - → em dash , trailing-whitespace trim, and straight double quotes " → Russian guillemets «…». A separate paragraph-start pass also converts a leading dialogue hyphen (- Слово— Слово), applied by computeSpans.ts only to a paragraph's first text node so mid-paragraph hyphens and -нибудь stay untouched. Span computation is a hand-rolled char-level diff (no new dependency); text content only — marks and node structure are never altered. Preview is decoration-only and suppresses autosave so no snapshot of the uncommitted state is taken.

Find & Replace. A non-modal bar for the open chapter (Ctrl+F / Ctrl+H, or the 🔍 toolbar button) rendered below the text so it shrinks the page rather than covering a match. Live highlighting via ProseMirror decorations, an accurate N / M counter, Enter/F3 navigation (wrapping), case-sensitive and whole-word (Cyrillic-aware) toggles. Matching is literal substring, never spanning a paragraph break or footnote. Replace-all runs in one transaction (one Ctrl+Z) reusing the wand's shared span-replace builder, so autosave/dirty/snapshots react automatically.

Full-text search. The «Поиск» sidebar view runs a read-only, in-memory sweep of the open story on submit: every chapter's canon plus every Notes section (characters, locations, world, timeline, scratch). Chapters and notes are read through window.api and never written — a pure read that cannot corrupt the library; a chapter that fails to read is skipped and surfaced as a soft "couldn't read part of the work" notice rather than blanking results. Matching is literal, case-insensitive substring over the same canon text-walker word count uses (extractPlainText), so the two always agree on "the text"; results are one row per source with an occurrence count and a context snippet. Clicking a chapter result opens it in the editor and seeds the M15 Find bar so the matches highlight in place; a notes result lands on the Notes view.

Import & export (.docx / .md). Import a single .md/.docx file as one chapter, or split it into one chapter per top-level heading, with a preview dialog before confirming. Imported chapters go through the exact createChapter + saveChapter path (atomic writes

  • snapshots). Node-side file work stays in main — mammoth (.docx → HTML) and docx (canon → .docx with native Word footnotes); doc-model parsing stays in the renderer. Import is a one-time, lossy conversion (tables/images/comments dropped, with an honest notice); marks, scene dividers and footnotes round-trip. Export reads canon only and writes with the same temp-then-rename atomic write.

Library export. Settings → «Экспортировать библиотеку» streams the entire library folder into one .zip (via archiver), including .trash/, reproducing the on-disk stories/<story-id>/… layout exactly. Read-only against the library; writes to a .part file and renames over the destination only once complete, so a failure never touches the source or leaves a truncated archive.

Markdown backup (.md shadow). Every successful chapter save also writes a human-readable Markdown copy beside the .json canon, through the same temp-then-rename atomic write (src/data/markdown.ts). Bold/italic/strike map to standard Markdown, the scene divider to ---, and footnotes to [^n] markers plus a definitions block (reusing src/shared/footnote-markdown.ts); paragraph alignment is intentionally dropped — which is why the canon stays JSON. The .md write is best-effort: a failure never fails the save or touches the canon, surfacing only as a soft «копия .md не сохранена» warning, and soft-delete / reorder keep the .md sibling in sync with its .json. v1 never re-imports from .md.

Typographic quotes. A cleanup-wand rule turns straight double quotes " into Russian guillemets «…», pairing by open/close alternation per text node. Narrow by design: only " (U+0022) is touched; single quotes/apostrophes are left alone. Runs after the em-dash rule, through the same preview + single-transaction path.

Auto-update. Packaged builds check GitHub Releases in the background via electron-updater's GitHub provider (no separate update server); electron-builder.yml carries the publish block, and npm run build:win emits a latest.yml manifest. The check never blocks launch (skipped in dev, fire-and-forget in packaged builds). A downloaded update shows a dismissible footer notice; "Restart now" routes through the same quit-guard flush as a normal quit, so no unsaved chapter is lost.

Minimal footer mode. A persistent «Минимальная нижняя панель» preference hides the footer's info line (word count, save status, spellcheck badge) while keeping the «Сохранить» button. Stored as Settings.hideEditorFooterInfo, toggled from a Settings checkbox and a chapter-header icon. A save error always overrides the hidden state and brings the full info line (with «повторить») back, so failures are never silent.

Milestone history (M0–M23)

M23 — Cleanup wand: leading dialogue dash. A separate paragraph-start pass converts a leading dialogue hyphen (- Слово— Слово, the Russian dialogue convention) to an em dash, applied by computeSpans.ts only to a paragraph's first text node — so mid-paragraph hyphens (still the existing - rule's job) and hyphenated words like -нибудь are left untouched.

M22 — Stable «Сохранить» button. Removes the save-button opacity flicker that fired on every autosave tick (most visible in minimal footer mode, where the button is the only footer element). Purely cosmetic — the underlying flush() already serializes concurrent saves, so nothing in the data-layer save path changed.

M21 — Editor scroll containment. Contains scrolling to the writing surface so the toolbar and chapter header (title, chapter switcher, history/focus/export icons) stay put while typing near the bottom of a long chapter, with reliable caret auto-follow. Scoped to the editor view only; other views' scrolling is unchanged.

M20 — Minimal footer mode. A persistent «Минимальная нижняя панель» preference (Settings.hideEditorFooterInfo) that hides the footer's info line (word count, save status, spellcheck badge) while keeping the «Сохранить» button. A save error overrides the hidden state so failures are never silent.

M17 — Quote typography (wand rule). A new wand rule converting straight double quotes " to Russian guillemets «…» by open/close alternation per text node; narrow by design — only " (U+0022) is touched, apostrophes/single quotes are left alone. Runs after the em-dash rule through the same preview + single-transaction path.

M16 — Full-text search. A read-only «Поиск» sidebar view that sweeps the whole open story on submit — every chapter's canon plus every Notes section — for a literal, case-insensitive substring, returning one row per source with an occurrence count and a context snippet. Reuses the shared canon walker (extractPlainText, factored out of the word-count code so search and word count agree on "the text") and never calls a write API, so it cannot corrupt the library; a chapter that fails to read is skipped and reported as a soft notice. A chapter hit opens the chapter and seeds the M15 Find bar to highlight matches in place; a notes hit lands on the Notes view.

M15 — Find & Replace. A non-modal in-chapter find/replace bar (Ctrl+F / Ctrl+H) with live decoration highlighting, an accurate N / M counter, Enter/F3 wrapping navigation, case-sensitive and whole-word (Cyrillic-aware) toggles, and a single-transaction Replace-all reusing the wand's shared span-replace builder — so autosave/dirty/snapshots react automatically.

M14 — Import & export (.docx / .md). Import a .md/.docx file as one chapter or split it by top-level headings, and export any chapter or the whole story to .docx with native Word footnotes (also includes the M14.1 UX consolidation). Imports go through the exact createChapter + saveChapter atomic-write path; export reads canon only and never touches the source library.

M13 — Library export. A one-click "Экспортировать библиотеку" action in Settings streams the whole library folder into a single .zip via archiver, including .trash/, reproducing the on-disk layout exactly on extraction; writes to a .part file and renames over the destination only once complete.

M12 — Auto-update. Packaged builds check GitHub Releases in the background via electron-updater's GitHub provider (no separate update server). The check never blocks or delays launch (skipped in dev, fire-and-forget in packaged builds); a downloaded update shows a dismissible restart notice that routes through the M5 quit-guard flush.

M10 — electron-vite 5 / Vite upgrade. Moved the build toolchain onto electron-vite@5 (Vite 7 / Vitest 3), clearing the old dev-server-only Vite advisory flagged under Known residual; the M4 spellcheck dict-server and configureSpellcheck() ordering were verified unchanged, and npm audit now reports 0 vulnerabilities.

M9 — Packaging & release. A Windows NSIS installer with app icon and per-user install, bundling resources/dictionaries so offline spellcheck works in the packaged app, plus a "Reveal library in Explorer" action in Settings.

M8 — Cleanup wand (minimal rules). The toolbar wand runs a pluggable, ordered set of pure text-cleanup rules over the selection — or the whole chapter when nothing is selected — behind an inline diff preview, applied as a single undoable transaction that preserves marks and never alters node structure.

M7 — Markdown backup export. Every successful chapter save also writes a human-readable .md backup beside the .json canon (paragraph alignment intentionally dropped). Best-effort: a failure never fails the save or touches the canon, and surfaces as a soft "копия .md не сохранена" warning. Soft-delete and reordering keep the .md sibling in sync with its .json.

M6 — Navigation & views. The sidebar-driven view router and all remaining views (Library, Chapters with drag-to-reorder, Story info, Notes, Statistics, Settings) around the editor + version-history views. Settings apply live (font, autosave interval, spellcheck languages via a new applySpellLanguages IPC).

M5 — Autosave + version history. A single flush() write path shared by manual Save, a 2 s debounce, a 2 min dirty-interval, and lifecycle flushes; a main-process quit guard that delays exit until the renderer confirms a final flush (or a 5 s timeout); a Version History view; and a startup crash-recovery prompt that restores a corrupt/missing chapter from its newest snapshot without ever silently overwriting the bad file.

M4 — Spellcheck (RU + EN, offline). Simultaneous Russian + English Chromium spellcheck served from bundled .bdic dictionaries by a loopback prefix-matching server, with a native suggestion / add-to-dictionary context menu.

M3 — Footnotes. A custom inline-atom footnote node with an auto-numbered [N] marker (numbering derived by document order, never stored), a hover popover to read the text and a select-to-edit field to change it, wired to the toolbar [?] button. Text is stored losslessly in the canon and survives save/reload and version snapshots.

M2 — Editor core. The TipTap writing surface: book-themed parchment page, toolbar (marks, alignment, undo/redo, indent toggle, scene divider), live word count, focus mode, a collapsible sidebar, and a manual Save that persists through window.api and reloads identically. A demo story is seeded on first run.

M1 — Data layer. The reliability backbone: FileService with atomic writes, library/story/chapter read-write, version snapshots + pruning, soft delete to .trash/, corrupt/missing-canon startup scan, and the full typed window.api over IPC. Unit-tested with Vitest.

M0 — Project scaffold (toolchain, window, IPC ping, shared type skeleton).

Known residual

  • None. The build toolchain runs on electron-vite@5 / vite@7 / vitest@3, and npm audit reports 0 vulnerabilities.

Issues & questions → github.com/julia-shtal/scriptorium-writer/issues

About

A local-first desktop editor for writing long-form fiction, styled like the warm pages of a book. Offline, crash-safe autosave, RU/EN spellcheck.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Packages

Contributors

Languages