oficina / docs Public

worktree-align: left, centre and right alignment, without putting document bytes in attribute position closed

On branch worktree-align

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-center
  • right → 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:

  1. fixtures/editor-entry.js — the paragraph node gains an align attribute, and toolbar commands set it
  2. core/src/fragment.rs — the allowlist and the class, in HTML; alignment in fragment_json so exporters can see it
  3. internal/docs/views/show.html — the three class rules
  4. internal/export/pdf.go — alignment is a layout property, so a centred line is measured and placed differently
  5. fixtures/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.

Drove the real editor to close the one gap the implementer flagged: every seam is unit-pinned, but nothing covered the round trip — toolbar command, ProseMirror transaction, Yjs attribute, save queue, POST, server replay, fresh editor. That join is where an attribute name agreed in four places separately can still be spelled differently in the fifth.

Headless Chromium against the built bundle:

  • three controls found by accessible name: Align left (pressed), Align centre, Align right
  • typed CENTREME, clicked the centre control with a real mousedown: class docs-align-center, computed text-align center, aria-pressed moved to the centre button and off left
  • reloaded — full round trip through fragment.rs — and the paragraph came back with the same class and the same computed alignment

No console errors, no uncaught exceptions.

One thing for Paul rather than for review: the labels read 'Align centre'. The catalog has no other British or US spelling anywhere, so there is nothing to anchor it, and Paul wrote 'center' when asking for this. The attribute value is 'center' either way, since that is CSS's spelling — this is only the human label. Easy to change; worth a decision rather than my guess. These three strings have also not been through copy-review, which is a mandatory gate for user-facing language and is not runnable non-interactively.

Merged as f4bc155 and deployed; docs.bds.oncarlos.com is serving it. Landing recorded.

Review fixed three things, none behavioural: a toolbar guard that never ran in CI while its comment claimed it ran at module load, an assertion comparing a document to itself and so unable to fail, and comments claiming the pressed state changes weight as well as colour when font-weight does nothing to an SVG. The pressed state now carries an underline, which repairs the three older toolbar buttons as well.

Two things worth keeping from the round. The implementer's own accessibility guard passed against the shipped defect on its first draft — it excluded colour properties, so it counted font-weight as a cue, which is neither a colour nor visible on an icon. It caught that by running the test against the broken state before trusting it. And the assertion that could not fail was the seventh found in this repo; the recurring shape is an assertion comparing a value to itself or to something derived from itself.

Still open and not merged into anything: the three catalog labels have not been through copy-review, and 'Align centre' against the 'center' attribute value is Paul's call.