oficina / docs Public

Bring the spec to what shipped, and correct three stale comments

The review's remaining should-fixes, all of the same shape: something
asserted that is not true, in a repo whose rule is that nothing stays
asserted and untrue.

§3.4 specified collapsing the bar to a single Menu button at 320px —
a design that contradicted the same section's own rule against nested
<details>, which is the rst-menus trap Sheets hit first. It wraps
instead, which keeps one component in one arrangement at every width.
The dead Collapsed field and its catalog key went with it.

§3.3 said the plugin sets aria-checked. It sets aria-pressed, correctly:
these menus are disclosures whose items are ordinary buttons, and
aria-checked belongs to menuitemcheckbox and menuitemradio.

§7.1 said Tab mid-paragraph inserts a literal tab. It indents the block,
and the row is corrected rather than the code: a literal tab needs
white-space: break-spaces on [docs-text] (reversing a decision that has
its own comment) and pdf.go measures \t as a glyph the core faces do not
have, so it exports zero-width plus a missing-glyph report — §7.2's own
fear, happening. It arrives with the ruler.

§9.2 said localStorage, and said in the same sentence that the mode is
"applied before first paint". Those cannot both be true; localStorage is
invisible to the server. It is a cookie. §9.2 also said paged repeats
[docs-page] per page, which ProseMirror's {mount: el} forbids — it owns
everything inside the editable element. show.html's own comment said the
same thing and is corrected too.

§3.2 lists the menu as designed, and seven of its items were never
built. A short section now says what shipped, what is greyed, and what
is absent, so no unmarked line reads as a promise that was kept — with
a note that Cut/Copy/Paste should stay absent, because a button cannot
drive the clipboard reliably and an item that sometimes fails is worse
than one that is not there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 18fbc4ffca21d0da890d785cae565419c266cfef parent 368e86e
3 files changed, +84 −31
  • docs/superpowers/specs/2026-09-02-word-processor-basics-design.md +71 −19
  • internal/docs/pages_test.go +0 −5
  • internal/docs/views/show.html +13 −7
diff --git a/docs/superpowers/specs/2026-09-02-word-processor-basics-design.md b/docs/superpowers/specs/2026-09-02-word-processor-basics-design.md
index 8d86190..1f4e859 100644
--- a/docs/superpowers/specs/2026-09-02-word-processor-basics-design.md
+++ b/docs/superpowers/specs/2026-09-02-word-processor-basics-design.md
@@ -178,6 +178,25 @@ action inside a run-level control group.
### 3.2 Contents
+**What of this actually shipped, since the list below is the plan and
+not the record.** Built and reachable: the whole bar; File's New
+document, Open, both downloads, Print and Sign out; Edit's Undo, Redo
+and Select all; View's Paged/Infinite pair; Format's marks, paragraph
+styles, alignments, lists, quote, indent/outdent and Clear formatting;
+Tools' Word count and Keyboard shortcuts. Rendered and greyed, each
+naming the section it waits on: Insert's Link, Image, Table, Page break
+and Horizontal line, Edit's Find and replace, View's Show ruler.
+
+**Not built and not in the menu at all**: Rename, Make a copy, Page
+setup, Cut/Copy/Paste, Full screen, Special characters, Spelling. The
+list below still names them because it is the design; they are called
+out here so nobody reads an unmarked line as a promise that was kept.
+Cut/Copy/Paste in particular should not be built — a `<button>` cannot
+drive the clipboard reliably (`execCommand` is deprecated,
+`navigator.clipboard.read` is permission-gated and absent in Firefox),
+and a menu item that sometimes fails is worse than one that is not
+there.
+
Real tonight unless marked. Items that are present-but-disabled are
listed because a menu that hides what the app will obviously grow is a
menu that gets redesigned; one that greys it is a menu that gets filled
@@ -240,7 +259,13 @@ it has to be *principled*, or the two drift:
- **State flows one way.** The ProseMirror plugin that already keeps
toolbar `aria-pressed` in step (`toolbar.mjs`'s plugin, updating on
every selection change including remote ones) also sets menu items'
- `aria-checked`. Nothing listens for clicks to infer state.
+ `aria-pressed`. Nothing listens for clicks to infer state.
+
+ `aria-pressed`, not the `aria-checked` this said first: these menus
+ are disclosures whose items are ordinary `<button>`s, and
+ `aria-checked` belongs to `menuitemcheckbox` and `menuitemradio`.
+ Putting an attribute on an element it does not apply to is worse than
+ putting none on at all.
### 3.4 Keyboard, ARIA, and 320px
@@ -285,14 +310,24 @@ implementation — so the bar is one tab stop.
the menu was ever opened.
- 24×24 minimum targets. The existing `[docs-tool]` is 28px and clears
this; menu items are full-width rows and clear it comfortably.
-- **At 320px the bar does not scroll and does not overflow.** Six menu
- titles do not fit 320px. Below a breakpoint the bar collapses to a
- single `Menu` button opening one `role="menu"` whose items are the
- six menus as submenus. This is one component in two arrangements, not
- two components: same command table, same names, same tests. The
- reflow check must be green with the menu bar present, and must be
- extended to cover the collapsed arrangement — extended, never
- relaxed.
+- **At 320px the bar wraps. It does not scroll and it does not
+ collapse.** Six menu titles do not fit 320px, so `flex-wrap` puts
+ them on two lines and every one stays reachable.
+
+ This originally specified collapsing to a single `Menu` button
+ opening one `role="menu"` whose items were the six menus as
+ submenus — a design that contradicted this same section's rule
+ against nested `<details>`, which is the `rst-menus` trap the Sheets
+ session hit first. Wrapping keeps ONE component in ONE arrangement:
+ same markup, same command table, same keyboard behaviour at every
+ width, nothing changing shape at a breakpoint. The only thing the
+ media query moves is the panel, which spans the bar below 30rem
+ rather than hanging off its own title, because an anchored 14rem
+ panel overflows a 320px viewport — and the page gaps, whose negative
+ margin has to match the page padding it is pulling out of.
+
+ The reflow check must be green with the menu bar present, and
+ extended, never relaxed.
### 3.5 Visual direction
@@ -534,16 +569,33 @@ own failure mode, and the one this repo has already been bitten by.
### 9.2 The toggle
-`View → Paged` / `View → Infinite`, a `menuitemradio` pair. It is a
-**per-person preference, not a document property** — how you like to
-read is not something you impose on a collaborator. Stored in
-`localStorage`, defaulting to infinite, applied before first paint to
-avoid a visible reflow.
-
-Infinite is one `[docs-page]` box that grows. Paged repeats
-`[docs-page]` once per computed page with the desk showing between them
-— the shape `show.html`'s own comment says it was built to allow
-("one box, its own padding, ready to hold more than one sibling").
+`View → Paged` / `View → Infinite`, a pair of `<button>`s carrying
+`aria-pressed` — not `menuitemradio`, for §3.3's reason: these menus are
+disclosures and their items are ordinary buttons. It is a **per-person
+preference, not a document property** — how you like to read is not
+something you impose on a collaborator. Stored in a **cookie**,
+defaulting to infinite.
+
+A cookie rather than the `localStorage` this first specified, and the
+reason is in the same sentence that specified it: "applied before first
+paint". `localStorage` is invisible to the server, so the mode could
+only be applied once the deferred bundle ran — which is a document
+visibly reflowing from infinite into pages a moment after it appears.
+The server reads the cookie and renders `data-view` on the canvas, so
+the first paint is already right.
+
+Infinite is one `[docs-page]` box that grows. Paged is the SAME single
+box, with a desk-coloured gap drawn between pages as a widget
+decoration inside the text flow.
+
+Not one `[docs-page]` per page, which is what this said first and what
+`show.html`'s own comment anticipated ("one box, its own padding, ready
+to hold more than one sibling"). ProseMirror owns everything inside the
+editable element (`{mount: el}`), so its children cannot be wrapped in
+sibling page boxes without taking the document away from it. Repeating
+the box is a layout-engine shape; a decoration is a
+pagination-as-a-view shape, which is what §9.1 already committed to.
+`show.html`'s comment is corrected in the same commit.
### 9.3 Explicit page breaks
diff --git a/internal/docs/pages_test.go b/internal/docs/pages_test.go
index f4207b7..68ed949 100644
--- a/internal/docs/pages_test.go
+++ b/internal/docs/pages_test.go
@@ -149,12 +149,7 @@ func TestTheViewMenuReportsTheCurrentMode(t *testing.T) {
// flag on after building the feature would have shipped a View menu
// that greys out the thing it just gained.
func TestTheViewModeItemsAreAvailable(t *testing.T) {
- app, err := NewForTest(t)
- if err != nil {
- t.Fatalf("NewForTest: %v", err)
- }
v := newMenuViewForTest()
- _ = app
found := 0
for _, m := range v.Menus {
diff --git a/internal/docs/views/show.html b/internal/docs/views/show.html
index 4a52a54..10ef409 100644
--- a/internal/docs/views/show.html
+++ b/internal/docs/views/show.html
@@ -22,13 +22,19 @@
var(--rst-text), which would make the "page" repaint itself dark
along with everything else on screen.
- [docs-page] is deliberately one page-shaped container with no
- page-break logic inside it yet: the brief is explicit that
- pagination arrives with a later plan's layout engine, and this
- shape (one box, its own padding, ready to hold more than one
- sibling) is what lets that land without restructuring this
- markup — a later version of this template repeats [docs-page]
- once per page rather than replacing it with something else.
+ [docs-page] is one page-shaped container, and it stays one in both
+ view modes. Paged draws desk-coloured gaps BETWEEN pages as widget
+ decorations inside the text flow (fixtures/pages.mjs) rather than
+ repeating this element.
+
+ This comment used to say a later version would repeat [docs-page]
+ once per page. It cannot: ProseMirror owns everything inside the
+ editable element ({mount: el}), so its children cannot be wrapped
+ in sibling page boxes without taking the document away from it.
+ Repeating the box is a layout-engine shape; a decoration is a
+ pagination-as-a-view shape, and a view is all pagination can be
+ while page breaks are computed from measured heights and never
+ persisted.
Writes the bare rst-* attribute grammar throughout — see
list.html's own comment on why, and