oficina / docs Public

The greyed-out menu items, and the four notes from the screenshots open

On branch docs-finish

Paul's four notes from the screenshots, plus every greyed-out menu item that could honestly be built. Six commits, gate green, 58/58 in a real browser.

What Paul asked for

"Remove the 'title' beside new document and remove the 'search documents' button .. the field is enough -> can it live search?"

Both gone, and they turned out to be one change with the third note. The title field could set a name and never correct one, because nothing in the app could rename a document at all — so removing it is only defensible alongside the thing that replaces it.

Live search fetches GET /?q=… — the same URL the form submits to, the same handler, the same template — and swaps in that response's [docs-results] region. It renders no rows, formats no dates and holds no copy. The alternative was building the list from the JSON the palette already uses, which would have put "No documents match", the recency group labels and the relative-time strings into a bundle, in twelve locales. Enter still submits for real so the query lands in history; the live update uses replaceState so typing does not.

"'Untitled document' should be editable in place"

A real <form method="post"> around a real <input>, posting to a new POST /d/{sqid}/title. It works with no JavaScript: the submit button that :focus-within reveals posts and 303s back with the new name in the bar, the tab and <title>. fixtures/title.mjs adds Enter, blur and Escape on top, and when its fetch fails it submits the form for real rather than showing a message about it.

The field shows what the server STORED, never what was typed — normaliseTitle collapses whitespace, turns control characters into spaces and caps at 200 runes (by rune; slicing by byte cuts a multi-byte character in half). A client that trusted its own string would show a name the server does not have.

Store.Rename scopes by owner in its WHERE clause like Get and List, so a stranger's document is never selected rather than found and refused: 404 either way, spec §4.2.

The File menu scrolling sideways

rastrillo's tokens.css deliberately has no * { box-sizing } reset — it says so, with reasons — so [docs-menu-item] was content-box and inline-size: 100% plus its padding overflowed every panel by 24px. It showed up on File and only on File because a <button> gets border-box from the UA stylesheet and an <a> does not, and File is the one menu that is mostly links. The horizontal scrollbar came from [docs-menu-items]'s own overflow-y: auto, which forces the other axis to compute to auto. Nothing sets overflow-x: hidden; that would have hidden this one and the next one.

The greyed-out items

Built: horizontal line, page break, find and replace, link, ruler. Still greyed, with reasons now in menu.go: image and table.

Horizontal line and page break are two nodes, not one with a flag. An <hr> is prose — a thematic break, announced as a separator, --- in Markdown, a drawn line in the PDF. A page break is layout with no meaning in the text: aria-hidden, no Markdown spelling at all (an inert comment, not a second ---, which would turn it into a thematic break permanently on any round trip). One node with a flag means every consumer reading that flag and branching, and any that forgot draws somebody's page break as a decorative line.

Find and replace is a panel, not a modal. show(), not showModal(): it is a control you work with while the document changes underneath, and a modal makes everything outside inert. The hard part is that a match has to become a SELECTION, so every match needs document positions rather than string offsets — a textblock's positions include its open token, marks split one run into several text nodes, and an inline atom occupies a position while contributing no character. Ctrl-F takes the key from the browser's own find bar, deliberately, and says so on the menu item.

Links are the first valued mark, and link_href is the allowlist, sited exactly where mark_names' own comment said it had to go. Three barriers: the value must be a map with a string href; the scheme must be one of https/http/mailto, matched at position zero with no trimming anywhere; and whatever survives still goes through escape(). A refused href drops the anchor and keeps the text.

The ruler measures; it does not drag. Dragging the indent marker would work today, but a ruler where one marker drags and clicking anywhere else does nothing invites the gesture that would place a tab stop, and tab stops still do not exist (spec §7.3).

Three things that were already broken

fragment_text was emitting markup. It returned <strong>bold words</strong> for a bold paragraph — yrs' XmlTextRef::get_string serialises a formatted run's attributes as XML tags. Latent (Store.Text has no caller in a handler) and found by a link test, because <link href=…> is long enough to notice where <strong> had sat unnoticed since marks existed. No test had ever asked that renderer what a marked run looks like.

A vector recorded the bug as the contract. formatted.expected.json said "plain <strong>bold</strong> plain". generate-fragment.mjs put it there deliberately: a first version used toDelta (mark-free), it disagreed with Rust, and the disagreement was resolved by changing the generator. Its comment defends this — inline pseudo-tags are "XmlText's own plain text convention in both implementations" — and both halves of that are true. The conclusion is not. They agreed because they share a library's convention, not because either was right.

aria-pressed never moved. menu.go renders it on the View menu's paged/infinite pair from the cookie, so the first paint is right, and nothing updated it afterwards. Switching to paged left "Infinite" still reporting itself as pressed for the life of the page. menu.mjs has setPressed now; the pair and the ruler both use it.

What the browser caught that the gate could not

The gate was green through both of these.

The ruler was BESIDE the page, not above it — [docs-canvas] is a flex row, and a second child laid the two out side by side (measured: ruler 16..606, page 606..1249). And its inch labels scrolled the document sideways at 320px, scrollWidth 655 against a 320 viewport: absolute positioning does not size the parent but does extend the scrollable overflow area. WCAG 2.2 SC 1.4.10, introduced by this branch.

The breakpoint that fixes the second one did nothing at first, and the drive said so: it sat with the other media queries, two hundred lines above the rule it was overriding, at identical specificity — so source order won. It is immediately after that rule now, and there is an overflow: hidden underneath it, because one of those is a threshold and the other is an invariant.

Verification

  • .amadan/ci.d/10-gate exit 0, read from $? and never through a pipe.
  • scripts/browser-drive.sh 58/58, including fourteen new checks: the File panel's scrollWidth against its clientWidth, the rename reaching the server rather than only the field, the ruler's alignment and its aria-pressed moving, find opening non-modal and highlighting real matches.
  • Mutation-tested rather than assumed: removing either link allowlist's scheme check turns tests red (two in Rust, three in JS); removing the forced-page-break branch turns three pages tests red; a one-position error in find's index turns nine red; removing the Cancel guard in the link dialog turns one red.
  • vectors/fragment/linked.bin is generated through prosemirrorToYXmlFragment, per spec §10.4 — and it earned that. The Rust tests were hand-built first, encoding the same assumption link_href makes, so code and tests would have agreed while being wrong together. The real encoding carries {href, title: null}: two keys, not one.

Left for Paul

  • Image needs a decision no branch can take: there is no asset store, no upload route and no sealing key, and a remote URL would put a third party's request log in the middle of somebody's document.
  • Table needs the PDF. Schema, CRDT nodes and a Markdown pipe table are tractable; laying a table out on paper is not a smaller job than the rest of internal/export. Shipping the first three without the fourth means a table that looks right on screen and exports as a stack of loose paragraphs.
  • A clickable /Link annotation in the PDF. Links are underlined and not blue there, because internal/pdf's colour channel is a single greyscale value. The underline carries the meaning; the annotation is a real gap.
  • Mac shortcut labels still say Ctrl while the bindings are Mod-. Wrong on every Mac, and older than this branch.