Files
margin-docs/docs/conventions.md
T
2026-08-28 09:22:58 +05:30

103 lines
5.0 KiB
Markdown

# 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.