A menu bar, and export stops being a button in the formatting bar
Paul's brief called them "random buttons": PDF and MD, two ghost links
pinned to the trailing end of the formatting bar. The objection is
structural rather than aesthetic — export is a document-level verb and
the formatting bar is a group of run-level controls, so it was the one
place a person would not look for it. They are File menu items now.
Six menus, File through Tools, above the formatting bar and sharing its
surface so the chrome reads as one slab rather than two strips.
Each menu is a <details>, and that is the load-bearing decision rather
than a convenience. The export links work with scripts disabled and the
read view is built on that guarantee; a JS-driven role="menubar" would
have moved the only route to a person's own document behind a control
that never opens for them. name="docs-menubar" makes the six an
exclusive group with no state for anything to keep, and fixtures/menu.mjs
only ADDS — arrow keys, Escape, click-away, a roving tabindex so the bar
is one tab stop. It stays a disclosure rather than claiming menu
semantics it does not fully implement.
No submenus anywhere: a nested <details> in a name= group collapses its
parent, so Download is two sibling items rather than a child menu.
The menu and the toolbar are one command table, not two. Menu items
carry the same data-docs-command names the toolbar's buttons carry, run
through the same table in toolbar.mjs, and are enabled by the same
plugin — an item is available exactly when its command reports it would
do something, which is the null-dispatch contract every command here is
already held to. select_all and clear_formatting are new and live in
toolbar.mjs with the rest rather than beside the menu, because a command
implemented next to one surface is a command the other cannot offer.
Items whose feature does not exist yet are rendered and marked
data-docs-unavailable rather than omitted, each annotated in menu.go
with the spec section it waits on. menu.mjs never enables those; without
the flag "not built" and "not mounted yet" look identical in the markup.
The old TestToolbarOptionValuesMatchTheBundle did not fail when this
landed, and could not have: it greps the template SOURCE, where the
menu's names are {{.Command}} expressions matching no name pattern. It
silently skipped an entire second surface while reporting success —
AGENTS.md's own "cannot distinguish no from I looked at nothing". Its
comment now says what it does and does not cover, and the menu's half is
checked from the Go model in menu_test.go. Both new tests were
mutation-checked: a misspelled catalog key and a misspelled command name
each fail them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrHJFx6wn4VvTzc3drkTp4
12 files changed,
+1370
−97
fixtures/editor-entry.js+23 −0fixtures/indent.mjs+153 −0fixtures/menu.mjs+255 −0fixtures/toolbar.mjs+55 −4internal/docs/export_test.go+47 −4internal/docs/handlers.go+12 −12internal/docs/menu.go+260 −0internal/docs/menu_test.go+245 −0internal/docs/render.go+61 −4internal/docs/static/editor.22f9ae5f330c892f.js+14 −0internal/docs/static/editor.7b30ca1e00c791c0.js+0 −14internal/docs/views/show.html+245 −59
fixtures/editor-entry.jsLoading diff…
fixtures/indent.mjsLoading diff…
fixtures/menu.mjsLoading diff…
fixtures/toolbar.mjsLoading 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/render.goLoading diff…
internal/docs/static/editor.22f9ae5f330c892f.jsLoading diff…
internal/docs/static/editor.7b30ca1e00c791c0.jsLoading diff…
internal/docs/views/show.htmlLoading diff…