The things a word processor is expected to already do
A menu bar instead of two export buttons pinned to the formatting bar,
Tab that indents, "- " that makes a bullet, real point sizes, pages you
can switch between, and the toolbar's selected state drawn as a ring
rather than a rule that curls into the corner.
THE MENU IS SIX <details> ELEMENTS, and that is the load-bearing
decision rather than a convenience. The export links worked with scripts
disabled before they moved, and this screen's whole premise is that it
works without JavaScript; a JS-driven role="menubar" would have put the
only route to a person's own document behind a control that never opens
for them. name="docs-menubar" gives the exclusive group for free, and
menu.mjs only adds — arrow keys, Escape, click-away, a roving tabindex.
It stays an honest disclosure rather than claiming menu semantics it
does not fully implement. No submenus anywhere: a nested <details> in a
name= group collapses its parent.
FONT SIZE IS ELEVEN MARKS, NOT ONE MARK CARRYING A NUMBER.
fragment_html's output is written into the page unescaped, which is safe
only because no byte from a document reaches attribute position, and
POST /d/{sqid}/updates accepts arbitrary Yjs bytes that never met the
schema. A mark spelling "14pt" would spend the property the whole
renderer rests on; eleven attribute-free marks spend nothing. Absolute
points rather than multipliers, because 14pt inside a heading meant
1.6 x 14. The four old named sizes stay in the vocabulary forever,
mapped to their nearest step — documents in the wild carry them, and a
mark the renderer stops knowing is text that silently loses its size.
INDENT IS THE FIRST NUMERIC BLOCK ATTRIBUTE, so it is parsed and clamped
the way heading.level already is rather than looked up in a table like
align — the existing route to the same guarantee, not a third one. Both
the editor and the renderer take digits only and clamp, deliberately, so
they agree on inputs no editor can produce and a raw POST can.
PAGES ARE A VIEW. Breaks are widget decorations over measured block
heights; nothing is persisted and nothing is shared, because a persisted
break would be an edit that merges and two people typing would fight
over where page three starts. The mode is a cookie rather than
localStorage so the server can render it in the first paint instead of
reflowing the document a moment after it appears.
Also: bullet, quote and heading autoformat with the Backspace that gives
back what you typed — without which every accidental "- " is a list you
have to fight; Enter in a list, never bound before; the three toolbar
selects as design-system comboboxes; print; word count that segments
with Intl.Segmenter, because splitting on whitespace reports 1 for a
whole Japanese sentence and this app declares twelve locales; and a
shortcuts sheet read out of the menu bar's own DOM, so it cannot drift.
Not built, and each greyed in the menu naming the section it waits on:
links (Plan 7's valued mark and scheme allowlist — the one item where
half-done is a vulnerability rather than a gap), find and replace, PDF
page numbers, tab stops and the ruler, document tabs.
NONE OF THIS WAS SEEN IN A BROWSER. Chromium will not start in the
sandbox this was built in, so the bars, the gaps, the comboboxes and
320px were reasoned about and never looked at. browser-drive.mjs's
tab-order bound is re-derived rather than measured — it goes DOWN, from
5 to 4, because the menu bar adds one stop while the two export links
leave the order entirely inside a closed <details>. The first real run
checks that derivation as much as it checks the page. Run
scripts/browser-drive.sh before trusting any of it.
Reviewed by Fable as one diff. The first verdict was do not merge, and
it was right: four defects the gate could not see, because all four only
fail in a browser. The worst left paged, infinite, word count and the
shortcuts sheet permanently disabled — sync read a module constant where
it should have read the merged table, which is the exact bug the comment
above that merge claimed to prevent. Discussion 25 has the full record,
including the two the re-check caught and the one I found myself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.amadan/ci.d/10-gate+55 −0core/src/fragment.rs+182 −6core/tests/fragment.rs+149 −0docs/superpowers/specs/2026-09-02-word-processor-basics-design.md+729 −0fixtures/autoformat.mjs+100 −0fixtures/autoformat.test.mjs+164 −0fixtures/browser-drive.mjs+33 −13fixtures/editor-entry.js+198 −7fixtures/generate-fragment.mjs+85 −0fixtures/indent.mjs+168 −0fixtures/indent.test.mjs+88 −0fixtures/menu.mjs+304 −0fixtures/menu.test.mjs+135 −0fixtures/package-lock.json+11 −0fixtures/package.json+3 −2fixtures/pages.mjs+238 −0fixtures/shortcuts.mjs+85 −0fixtures/sizes.mjs+156 −0fixtures/sizes.test.mjs+93 −0fixtures/toolbar-roving.mjs+21 −2fixtures/toolbar.mjs+242 −9fixtures/vectors/fragment/indented.binbinaryfixtures/vectors/fragment/sizes.binbinaryfixtures/wordcount.mjs+118 −0fixtures/wordcount.test.mjs+92 −0internal/core/testdata/docs_core.wasmbinaryinternal/docs/export.go+63 −14internal/docs/export_test.go+109 −8internal/docs/handlers.go+81 −12internal/docs/menu.go+326 −0internal/docs/menu_test.go+259 −0internal/docs/pages_test.go+169 −0internal/docs/render.go+103 −4internal/docs/selects_test.go+132 −0internal/docs/sizes_test.go+134 −0internal/docs/static/editor.7b30ca1e00c791c0.js+0 −14internal/docs/static/editor.d8d3d5d91b1c09c4.js+14 −0internal/docs/views/show.html+723 −88internal/export/document.go+10 −2internal/export/pdf.go+112 −2
CI passed — run details
Intent
Attributed to this commit: 1720 g CO₂e (211843111 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on merge-wpb, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
.amadan/ci.d/10-gateLoading diff…
core/src/fragment.rsLoading diff…
core/tests/fragment.rsLoading diff…
docs/superpowers/specs/2026-09-02-word-processor-basics-design.mdLoading diff…
fixtures/autoformat.mjsLoading diff…
fixtures/autoformat.test.mjsLoading diff…
fixtures/browser-drive.mjsLoading diff…
fixtures/editor-entry.jsLoading diff…
fixtures/generate-fragment.mjsLoading diff…
fixtures/indent.mjsLoading diff…
fixtures/indent.test.mjsLoading diff…
fixtures/menu.mjsLoading diff…
fixtures/menu.test.mjsLoading diff…
fixtures/package-lock.jsonLoading diff…
fixtures/package.jsonLoading diff…
fixtures/pages.mjsLoading diff…
fixtures/shortcuts.mjsLoading diff…
fixtures/sizes.mjsLoading diff…
fixtures/sizes.test.mjsLoading diff…
fixtures/toolbar-roving.mjsLoading diff…
fixtures/toolbar.mjsLoading diff…
fixtures/vectors/fragment/indented.binLoading diff…
fixtures/vectors/fragment/sizes.binLoading diff…
fixtures/wordcount.mjsLoading diff…
fixtures/wordcount.test.mjsLoading diff…
internal/core/testdata/docs_core.wasmLoading diff…
internal/docs/export.goLoading diff…
internal/docs/export_test.goLoading diff…
internal/docs/handlers.goLoading diff…
internal/docs/menu.goLoading diff…
internal/docs/menu_test.goLoading diff…
internal/docs/pages_test.goLoading diff…
internal/docs/render.goLoading diff…
internal/docs/selects_test.goLoading diff…
internal/docs/sizes_test.goLoading diff…
internal/docs/static/editor.7b30ca1e00c791c0.jsLoading diff…
internal/docs/static/editor.d8d3d5d91b1c09c4.jsLoading diff…
internal/docs/views/show.htmlLoading diff…
internal/export/document.goLoading diff…
internal/export/pdf.goLoading diff…