oficina / docs Public

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
Paul Campbell pushed by paul@keymail.dev 7bc0e869803a59f14167c5dc5c4cddcda616b924 parent 7df9143
15 files changed, +1630 −36
  • fixtures/browser-drive.mjs +229 −0
  • fixtures/editor-entry.js +71 −14
  • fixtures/toolbar.mjs +257 −0
  • internal/docs/app.go +10 −0
  • internal/docs/assets.go +39 −0
  • internal/docs/export.go +259 −0
  • internal/docs/export_test.go +333 −0
  • internal/docs/handlers.go +20 −0
  • internal/docs/render.go +44 −0
  • internal/docs/static/editor.98af6ed37bba8906.js +0 −14
  • internal/docs/static/editor.b09632ce9455588a.js +14 −0
  • internal/docs/store.go +23 −0
  • internal/docs/views/show.html +238 −8
  • internal/export/pdf.go +7 −0
  • scripts/browser-drive.sh +86 −0
fixtures/browser-drive.mjs

Loading diff…

fixtures/editor-entry.js

Loading diff…

fixtures/toolbar.mjs

Loading diff…

internal/docs/app.go

Loading diff…

internal/docs/assets.go

Loading diff…

internal/docs/export.go

Loading diff…

internal/docs/export_test.go

Loading diff…

internal/docs/handlers.go

Loading diff…

internal/docs/render.go

Loading diff…

internal/docs/static/editor.98af6ed37bba8906.js

Loading diff…

internal/docs/static/editor.b09632ce9455588a.js

Loading diff…

internal/docs/store.go

Loading diff…

internal/docs/views/show.html

Loading diff…

internal/export/pdf.go

Loading diff…

scripts/browser-drive.sh

Loading diff…