🖋 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.
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.
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
.mdcopy any tool can read. The.mdwrite can fail without ever endangering the canon. - Atomic writes only. Temp file →
fsync→rename. 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.
| 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. |
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.
- Open the Releases page.
- Under Assets, download
Scriptorium Writer Setup <version>.exe. - Run the downloaded file.
- 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".
- Go through the normal install wizard — the defaults are fine. A shortcut appears on the desktop and in the Start menu.
- 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.
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).
- 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, в которой удобно писать длинные истории: тёплое оформление под книжную страницу, автосохранение, история версий (можно откатиться к более старому варианту главы), сноски, орфография сразу на русском и английском — и всё это работает без интернета.
- Откройте страницу Releases.
- В разделе Assets скачайте файл
Scriptorium Writer Setup <версия>.exe. - Запустите скачанный файл.
- Windows, скорее всего, покажет предупреждение «Windows защитил ваш компьютер» — это нормально, просто у установщика пока нет платной цифровой подписи. Нажмите «Дополнительно», затем «Выполнить в любом случае».
- Пройдите обычный мастер установки — можно оставить все настройки по умолчанию. На рабочем столе и в меню «Пуск» появится ярлык.
- Запустите Scriptorium Writer. При первом запуске откроется демонстрационная глава — просто чтобы было с чем начать. Её можно удалить и начать свою историю.
Все истории лежат в обычной папке на компьютере:
Документы/Scriptorium-Writer/
Это обычная папка, а не закрытая база данных где-то внутри программы. Её можно:
- скопировать на флешку;
- синхронизировать через Яндекс.Диск, Dropbox, Google Диск или любой похожий сервис;
- открыть в проводнике и посмотреть, что внутри (там по одному файлу на главу).
- Каждое сохранение создаёт снимок версии — старые варианты главы никуда не пропадают, их можно посмотреть и вернуть через историю версий внутри программы.
- Программа никогда не сохраняет повреждённый файл поверх исправного — если что-то не так, она предложит восстановить главу из последнего исправного снимка.
- Если закрыть программу во время печати, она сама дождётся, пока сохранение закончится, и только потом закроется.
Если проблема не решается этими шагами — создайте issue на GitHub или напишите автору напрямую.
Architecture, scripts, the on-disk data format, and how the pieces fit together.
Design source of truth: docs/SPEC.md.
- 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:winfails withERR_REQUIRE_ESM.- Windows install:
winget install OpenJS.NodeJS.LTS(currently installs Node 24 LTS), then open a new terminal.
- Windows install:
- npm 10+ (bundled with Node; Node 24 ships npm 11).
npm install
npm run dev # opens the book-themed editor on a seeded demo chapternpm 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 installmay not download the Electron binary. Ifnpm run devfails withError: Electron uninstall, fetch it once withnode node_modules/electron/install.js(or allow it vianpm approve-scripts electron), then re-runnpm run dev.
| 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. |
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.tsx → createWebPlatform()), no Electron, no preload.
npm run dev:web— serves the web build at the root URL (http://localhost:5173/), with--hostso 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 intodist-web/.
The persistent browser FsPort — OpfsFsPort (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.
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, WorkboxgenerateSW) 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 desktopUpdateNotice, sharing only the visual style. - Dev over HTTPS:
npm run dev:webusesvite-plugin-mkcertto 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:iconemits the desktop.icoand the PWA PNGs (192, 512, and a 512 maskable) intosrc/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.
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 handsFileServicea 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 injectedFsPort(src/data/fs-port.ts) — no direct Node imports, with one remaining exception:exportLibraryArchivestill reaches intosrc/main/library-archive(Node +archiver), to be hoisted behind a port in a later milestone. - platform (
src/platform/node/,src/platform/web/) — per-platformFsPortimplementations.NodeFsPort(a thin adapter overnode:fs/promises) for Electron;OpfsFsPort(+opfs-worker.ts) over the browser Origin Private File System, plusMemoryFsPortscaffolding. Android (Capacitor) plugs in here later without touching the data layer. - preload (
src/preload/) — a typedcontextBridgewindow.apisurface; thin wrappers overipcRenderer.invoke. - renderer (
src/renderer/) — React UI. Never importsfs,path, or any Node built-in. All disk/OS work goes throughwindow.api. Enforced bycontextIsolation: 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).
- 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.zipin one click. - Settings — per-machine, in Electron's
userData/settings.json, which holdslibraryPathso 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.
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).
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) anddocx(canon →.docxwith 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).
- None. The build toolchain runs on
electron-vite@5/vite@7/vitest@3, andnpm auditreports 0 vulnerabilities.
Issues & questions → github.com/julia-shtal/scriptorium-writer/issues