Files
2026-08-28 09:22:58 +05:30

5.0 KiB

Conventions

This project is a sibling to ../margin and ../margin-calendar and follows their conventions deliberately rather than inventing new ones. When something here is unclear, the answer is almost always "do what margin does" for the editor, the token layer and the visual language, and "do what margin-calendar does" for the scaffold underneath: the store, the CSS rules and the shape of the IPC boundary.

Rust

Result<T, String> everywhere. No anyhow, no custom error enums.

DTOs crossing the IPC boundary live in src-tauri/src/dto.rs and are marked #[serde(rename_all = "camelCase")]. That file is the contract and is frozen: implementation modules add bodies, not fields. Its mirror is src/ipc.ts.

Every write to a document is atomic: write to a temp file beside it, flush, then rename over the original. A crash or a full disk must never leave a half-written file sitting where the user's file used to be. Deletions go through the trash crate, never fs::remove_file, since these are the user's own files and this app does not get to be the reason one of them is gone for good.

Comments are rare and explain why, never what.

TypeScript

One zustand store per domain in src/store/. No middleware. One selector call per field (useThing((s) => s.field), never a destructured object), actions as inline arrow properties, and set((s) => ...) returning {} to no-op.

Async actions use a string phase union, never boolean loading flags. Errors stringify with String(e) and surface as a toast.

Side effects that touch disk, the DOM or Tauri live in a sibling module, never inside the store.

Typed IPC wrappers live in src/api/, one module per domain, one thin function per command, mirroring dto.rs field for field.

Markdown

The serializer has one house style, fixed in one place under src/markdown/ and never varied per document or per call site: one heading style, one list marker, one rule for when a link gets angle brackets. A file saved twice with no edits in between produces byte-identical output, and a file opened and then closed with no edits is never written at all.

No markdown construct is ever dropped on a round trip. Anything the editor cannot model, rather than being reformatted into the nearest thing it understands or silently discarded, becomes a raw node: the exact source bytes, preserved and shown as an editable monospace block. Losing a construct silently is worse than never having supported it, because the loss is invisible until the file is opened somewhere else and the diff shows a missing paragraph.

The editor never writes a file the user has not edited. Opening a document, looking at it, switching away from it and closing the app again must leave the file on disk untouched, and that is worth an actual round-trip test rather than an assumption.

An edit that has no spelling is refused, never approximated. Where a keystroke or a paste would produce bytes the reader hands back as a different document, the answer is to decline it and say so in a toast, or to write the block from the source the file gave it and leave the edit off that save. Both cost the user one gesture. Reflowing their text into the nearest thing that does have a spelling costs them the file, and they find out somewhere else.

Tests

A guard is proved by running the program, not by calling the guard. Four rounds of review found a guard that answered correctly and was never reached, which is a guard that ships green and does nothing, so anything that claims to refuse a gesture is asserted through the gesture: the real key, the real paste, the real drag, in tests/. A unit test that builds the document by hand cannot see what happens between the keystroke and the serializer, and that is where every data loss bug in this project has been.

A sweep that appends its own marker to the corpus asserts that no corpus file already contains it. The word is otherwise a collision waiting for somebody to add a fixture, and a collided sweep does not fail loudly, it quietly stops sweeping.

Never weaken, skip or delete a test to reach green. A test pinning behaviour that has since changed is rewritten to the new truth with the reason written next to it.

CSS

Flat kebab-case class names, not BEM. State is a data-* attribute, never an is- class.

Every colour, radius and size goes through a token in src/styles/tokens.css. If a value is not in there, add a token rather than a literal.

Dark mode is data-theme on <html>, with both palettes defining an identical variable set. Never a media query for theme.

Transitions name explicit properties and use var(--ease). Never transition: all.

Icons

Inline Feather-style 24x24 stroke d strings passed to <Icon d={...} />. There is no icon set and no registry, and there will not be one.

Storage keys

Anything in localStorage is prefixed margindocs-, following margin and margin-calendar's own prefixes.

Never

No CSS framework, no component library, no router, no zustand middleware, no directory trees in any document, and no em dashes or en dashes anywhere, including code comments.