A formatting bar, and two ways to take a document with you
The bar is fixtures/toolbar.mjs, a module of its own: undo and redo, a
style select (body text, headings 1-3), a font select and a size select,
and buttons for bold, italic, code, both list kinds and quote. It holds
no EditorView — attemptResync destroys and rebuilds one wholesale, so
every command reads getView() at the moment it runs — and it carries no
copy: views/show.html renders every label from the catalog, the rule the
save-status indicator already followed.
Button state is kept honest by a ProseMirror plugin rather than by
listening to its own clicks. The selection also moves under the arrow
keys, under a click on the paper, and under a remote edit arriving
through the CRDT; a bar that only updated on its own buttons would be
wrong most of the time.
GET /d/{sqid}/export.md and /d/{sqid}/export.pdf sit inside the same
RequireSession group and resolve through the same owner-scoped Store.Get
as every other document route, so someone else's document 404s
identically to one that does not exist. The download filename is derived
from the title and reduced to a conservative ASCII set — a title is
arbitrary text somebody typed, and a Content-Disposition header is the
wrong place to discover that.
The twelve faces are served at content-hashed paths and declared as
@font-face on the document page, so the screen sets a document in the
same font programs the PDF embeds. That is what makes the two agree.
internal/docs/export_test.go's TestPageGeometryMatchesTheStylesheet
reads show.html and fails if its numbers stop matching internal/export's;
it fails loudly when it cannot FIND what it is checking, rather than
passing because a restructured stylesheet no longer matches its patterns.
## The bug this branch made, and how it was found
Between writing the toolbar and this commit, the editor stopped working
entirely — not the toolbar, the editor. Typing did nothing. Bold did
nothing. No error, in the page or in the server log.
y-prosemirror's exported undo is:
export const undo = state =>
yUndoPluginKey.getState(state)?.undoManager?.undo() != null
It ignores dispatch. A ProseMirror command's contract is that a null
dispatch performs nothing and only reports whether it could, and that is
exactly how a toolbar decides whether to grey a button out — on every
state update, because that is when the answer changes. So asking "could
I undo?" performed an undo, every time anything changed, and every edit
was reverted a moment after it was made. The library ships
undoCommand/redoCommand, which honour the contract; the keymap keeps the
bare undo/redo, correctly, because a keymap never passes a null dispatch.
None of this was visible to the test suite, which was green throughout,
and none of it was visible in a screenshot, where the bar renders
correctly and the document looks untouched because it IS untouched. It
was found by driving a real browser through the DevTools protocol —
double-click a word, click the button, read the document back — which is
the gate spec §12 asks for and this repo does not have yet. The script
that found it is committed as fixtures/browser-drive.mjs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YYB9fUAfxXNzkeC3fsFYzR
15 files changed,
+1630
−36
fixtures/browser-drive.mjs+229 −0fixtures/editor-entry.js+71 −14fixtures/toolbar.mjs+257 −0internal/docs/app.go+10 −0internal/docs/assets.go+39 −0internal/docs/export.go+259 −0internal/docs/export_test.go+333 −0internal/docs/handlers.go+20 −0internal/docs/render.go+44 −0internal/docs/static/editor.98af6ed37bba8906.js+0 −14internal/docs/static/editor.b09632ce9455588a.js+14 −0internal/docs/store.go+23 −0internal/docs/views/show.html+238 −8internal/export/pdf.go+7 −0scripts/browser-drive.sh+86 −0
fixtures/browser-drive.mjsLoading diff…
fixtures/editor-entry.jsLoading diff…
fixtures/toolbar.mjsLoading diff…
internal/docs/app.goLoading diff…
internal/docs/assets.goLoading diff…
internal/docs/export.goLoading diff…
internal/docs/export_test.goLoading diff…
internal/docs/handlers.goLoading diff…
internal/docs/render.goLoading diff…
internal/docs/static/editor.98af6ed37bba8906.jsLoading diff…
internal/docs/static/editor.b09632ce9455588a.jsLoading diff…
internal/docs/store.goLoading diff…
internal/docs/views/show.htmlLoading diff…
internal/export/pdf.goLoading diff…
scripts/browser-drive.shLoading diff…