Left, centre and right alignment for paragraphs. Opening before the first commit.
Why this is bigger than it sounds
This is the first block-level formatting in the app, and it runs straight into a property the read view depends on.
fragment_html emits its markup unescaped into the page (show.html writes {{.HTML}} as template.HTML), and that is only safe because no document bytes ever reach attribute position. The existing formatting vocabulary is built to preserve that: the marks are a closed set of class names (docs-font-sans, docs-size-lg), and fragment.rs's own comment is explicit that a mark carrying a value would spend exactly that property.
Alignment is not a mark — it belongs to the block, not the run — so it needs the same discipline in a new place.
The shape
A paragraph carries an align attribute in the CRDT, and fragment.rs maps it through a closed allowlist to a class:
left → no class at all (the default costs nothing)center → docs-align-centerright → docs-align-right
Anything else — a value from a hand-crafted update, a future version's justify, a typo — is dropped, not passed through. The class names are literals in the renderer; the document's own bytes never appear in the output. That keeps the unescaped-emission property exactly as it is today.
Five places have to agree, the same list the font marks already span:
fixtures/editor-entry.js — the paragraph node gains an align attribute, and toolbar commands set itcore/src/fragment.rs — the allowlist and the class, in HTML; alignment in fragment_json so exporters can see itinternal/docs/views/show.html — the three class rulesinternal/export/pdf.go — alignment is a layout property, so a centred line is measured and placed differentlyfixtures/toolbar.mjs — three controls, in the existing toolbar's idiom
Markdown has no representation for alignment. It will be dropped there, and that is the honest answer rather than inventing a convention; the exporter should be explicit about it.
What I will be careful about
y-prosemirror stores node attributes as Yjs XML attributes, so an aligned paragraph is a genuine change to what a document contains, not just how it renders. Two consequences: an old document with no attribute must render exactly as it does today, and a document aligned by a future version must not break this one — which is what the allowlist's drop-unknown rule is for.
The PDF is where this is most likely to be wrong, because alignment there is arithmetic rather than a CSS property: a centred line has to be measured and offset within the content width, and the content width is itself margin-relative after the fix that just shipped. A test that only checks the HTML would not notice a PDF that centres against the page rather than the text column.
Not in this branch
The sans-serif default, which is agreed and follows next. It restyles every existing document, so it wants to land when nothing else is in flight.