oficina / sheets Public

Adopt estilo's shared chrome (round 2, second adopter) open

On branch feat/r2-shared-chrome

🤖 Sheets is the second adopter of estilo's shared chrome. The spec is estilo spec/shared-chrome @ 49c7b01, §12.1 row 3; the brief is polish-audit/R2-BRIEF.md. Docs main 9f11ffc was the worked example. Where Sheets didn't fit the Docs-shaped API, I fixed estilo rather than working around it here: oficina/estilo feat/r2-sheets-corrections, discussion #11. That discussion lists every correction and the reason for it.

The spec names this branch polish/r2-shared-chrome. I used feat/r2-shared-chrome because that is the name the task gave.

Merge order: estilo first

  • go.mod pins estilo at the unmerged corrections branch head, b7988ff (v0.0.0-20260923235632-b7988ff2ac24).
  • Order: estilo #11 squash-merges. This branch then re-pins to estilo main (GOPRIVATE=amadan.net go get amadan.net/oficina/estilo.git@main && go mod tidy), pushes, and gets green CI. Only then does Sheets merge.
  • amadan.net/oficina/home/connect is bumped to Home main 01166fc. That brings in the in-app Sign out chaining to {Issuer}/signout (a 307), the signed-in /signin shortcut, and Credentials.Directory.
  • Deploy order is Home's as recorded on home #74: oficina-id before oficina, then the apps. A new Sheets must not reach an id.* host that doesn't yet admit consumer origins.

Spec citations

The spec cites Sheets bf59232; origin/main here was 300c4d1. I re-checked each citation on 300c4d1. The http.Error sites the spec lists are at the lines it gives (handlers_sheets.go:95,138,264,276, handlers_table.go:165,187,210, handlers_view.go:53, theme.go:82, render.go:169,178), and so are sheets.css:32-36, app.go:155-159,187, sheet.html:45-58,113-121 and page_test.go:56.

Found already done on main: the vendor context menu the spec lists as "goes (B7)" was removed in c55252e (contextMenu: () => false).

What changed

The bar (estilo chrome.Bar, built once per request in chrome.go) replaces the rastrillo topbar markup that layout.html wrote out.

  • It is 52px signed in and out. It was 45px signed out and 51px signed in (50.9 at 1440, 52.6 at 390), because Import and Create were taller than the bar.
  • The layout is estilo-shell > estilo-bar > <main>. The grid page uses estilo-shell="fill", the estilo correction that replaces [rst-shell-topbar]:has([data-sheet-surface]) { height: 100dvh }. That also removes the 15px dead scrollbar gutter beside the grid.
  • <meta charset> comes first. The stylesheets load in the suite's order: tokens → theme → brand → estilo base → menu → chrome → sheets.css.
  • The in-bar rename moved onto estilo's ContextEdit slot, still a real form posting to /sheets/{id}/rename. Escape restores the title and leaves the field. A reader sees the title read-only.
  • On the list, Import and Create are the bar's actions. Create is a POST both as the primary button and as a row in the narrow Menu sheet.
  • The grid wrapper gets isolation: isolate (§5.3). The drop overlay goes to z-index 50, above the bar.

The account menu holds, in order:

  • "Signed in as" and the address; the trigger shows initials.
  • Connect your Oficina account, only with Home configured, and hidden for Home sessions (it used to show for them).
  • Appearance: Light, Dark and System, with the current choice checked (aria-pressed on every option).
  • Help → Keyboard shortcuts…, a native <dialog> opened by the invoker command, so it works with scripting off.
  • The one Sign out.

Appearance moved from /mode, the sheets_mode cookie and "auto" to estilo's /appearance and the estilo_appearance cookie. It is still inside the app-wide CSRF check and outside requireSession. An old sheets_mode cookie is now ignored, so everyone starts on System once.

The switcher shows All apps only (Paul's §13 answer 2), and only when Home is configured with HOME_DIRECTORY_ORIGIN. Standalone has none.

The menu bar is rebuilt on estilo-menubar and estilo-menu:

  • Square titles with a hover background, 9px panels, and a shortcut column.
  • One estilo-menus exclusivity group shared with the bar, as Sheets chose with rastrillo's rst-menus.
  • No submenus. Format drops Number, Fill colour and Text colour, which the toolbar beside it already carries (the spec left trim-or-flyout to this adoption). Alignment becomes three plain rows. Insert's Function becomes rows under a heading.
  • Help leaves the menu bar for the account menu.
  • Items that need script stay hidden until commands.js runs. With scripting off, Edit's separators are now hidden too.
  • Labels: "Undo my last edit" is now "Undo".
  • Platform shortcut labels come from menu.Shortcut, server-side (⌘Z / Ctrl+Z). They are used in the menus, the toolbar tips ("Undo (⌘Z)"), the shortcuts dialog, and clipboard.js's "Press ⌘C instead" status line. Pages send Vary: User-Agent.

Errors (§3).

  • The router has NotFound, and a MethodNotAllowed that answers a GET of a POST-only path with the 404 page. Any other method gets a plain 405 with Allow set.
  • recoverToErrorPage sits inside the session group, and opts.ErrorPage is set in main.go.
  • a.notFound splits by Accept: text/html gets "This sheet isn’t available"; the grid's /api fetches and the native apps get the plain "Not found." they always had.
  • The HTML http.Error sites the spec lists are converted, and TestEveryPlainTextErrorIsClassified accounts for the rest.
  • 500 pages carry a reference. The log line holds the reference and the route pattern, not the path: a /l/ path is a capability.

Callouts (§4). rst-callout="warn" becomes rst-tone="warning" (the new link and the contrast-floor notice). The share, comments and history notices get positive, negative (with role="alert"), warning or info tones. The duplicate "Link created. Copy it now" notice is gone; the one warning callout is kept. Sign-in errors are negative, and an unparseable address goes in the field's own slot.

Titles and names (§10).

  • Titles now read: Sheets, Commission Q2 · Sheets, Table view · … · Sheets, Version history · …, Comments · …, Share · …, Import a spreadsheet · Sheets, Sign in · Sheets, Check your email · Sheets, Signed out · Sheets, Page not found · Sheets.
  • Sub-page h1s are the page's name ("Version history", not "History — X"); the sheet's title is the bar's context.
  • One name per destination: "All sheets" for the list (the job page said "Back to your sheets"), and "Back to sheet" for the grid (it was "Back to the sheet" and "Open the grid").
  • "Oficina Sheets" is gone everywhere. The email subject is "Sign in to Sheets".

Sign-in and sign-out (§9).

  • The sign-in pages are estilo's sign-in, sent and signed-out templates, with the §9.2 copy.
  • Sign out lands on /signed-out in both modes. With Home configured it goes 307 → id.* → /signed-out. Standalone, rastrillo's redirect to /signin is rewritten to /signed-out.
  • Already signed in: standalone GET /signin 303s to a safe return_to or the list, never to a sign-in page. With Home, connect's /signin does it. /signin/local never skips, because a Home session goes there on purpose to link an account.

Sheets' / (Home #74, decision 2): with Home configured, a signed-out visitor is sent 303 → /signin; signed in, / goes to /sheets. Standalone keeps the landing page, now titled "Sheets" with the bar's Sign in. Tests cover signed-out, Home-signed-in and locally-signed-in visitors with Home, and standalone.

Other items.

  • The Share page lists lose the browser's 40px indent and get --rst-cols: minmax(0,1fr) auto, so Remove and Turn off sit beside the row instead of under it.
  • The table-view cell link is a square highlight that fills the cell (it was a 6px pill). Borderless toolbar buttons are square (§11, Paul's answer 4).
  • Automatic numbers show at most 15 significant digits: 92.05, not 92.05000000000001. format.generalNumber was the one place it happened, for both the grid and the table. Only the display rounds; values stay IEEE doubles.
  • A picker's tooltip no longer covers its own open panel.

Expected casualties, rewritten rather than deleted:

  • shellmenu_test.go is now TestTheAccountIsReachableOnNarrowScreens.
  • menugroup_test.go gains TestTheMenusShareOneGroup.
  • csstokens_test.go gains TestEveryEstiloPropertyWeUseExists.
  • page_test.go's TestIndexIsNotACatchAll now asserts the HTML 404 page for a browser and plain text otherwise.
  • theme_test.go moves onto /appearance.
  • signin_test.go and home_navigation_test.go are updated; the latter now asserts the 307 chain.
  • dropdownanchor_test.go still guards the toolbar pickers, which stay rastrillo dropdowns.
  • tools/drive/*.mjs drive the estilo selectors, /appearance and estilo_appearance.

Left out on purpose, and disagreements with the spec

  1. There is no Open source item. Sheets has no credits page, so Help holds only the shortcuts (Docs made the same call).
  2. Standalone return_to (§9.4, local mode) is not built. rastrillo's auth.RequireSession drops the path, and a standalone link lands on /sheets. With Home configured (production), homeRequire and connect already carry return_to (TestHomeConnectionPreservesSheetsAndCurrentAdmission). This is a follow-up if dev mode needs it.
  3. The toolbar's pickers (number format, fill, ink) stay rastrillo dropdowns. They are already 9px and anchor-positioned, and §7.1 lists them under estilo-menu. Moving them is follow-up work.
  4. Two extra error rows: 400 "That didn’t work" and 413 "That’s too long". §3.2 has neither.
  5. The sent page says "that address". rastrillo's Begin redirects to ?sent=1 with no address, so the page can't echo it (as in Docs). The magic-link email body is rastrillo's, so §9.2's email body copy isn't applied; only the subject changed.
  6. Middleware refusals stay plain text: the homeRequire non-GET 403 and 503, rejectDisabledHome, and the homeLocalComplete refusals. Each is classified with its reason (R3).
  7. With Home configured, the account-link pages use connect's templates with Sheets' copy passed through connect.Page's translate func ("your Oficina account", verb "Connect"). connect.English itself still says "Home"; that's Home's to change.

Verification

Local gate: make test passes with the pinned rastrillo CLI v0.23.0 on PATH (vet, gofmt, build, rastrillo migration check, go test -count=1 ./..., and -race on store and live).

New tests, each seen red first. polish-audit/scripts/sheets-r2-mutations.py applies one mutation per test and requires that test to fail. All 28 went red; the log is polish-audit/sheets-r2-logs/mutations.log. The new tests:

  • TestHTMLErrorsRenderTheErrorPage ({unknown route, GET of a POST-only path, missing sheet, bad id, missing link, 413, panic} × {in, out}, plus plain answers for fetches and the native apps, and the 405)
  • TestErrorPageBackLinkIsSameOriginOnly, TestEveryPlainTextErrorIsClassified
  • TestPageTitlesFollowTheSuitePattern, TestEverySignedInPageHasOneSignOutInTheSameCorner, TestTheSignedOutCorner
  • TestTheSwitcherIsAllAppsOnly, TestTheBarRenamesTheSheet, TestTheBarsRenameFormRenames, TestTheGridFillsTheViewport
  • TestEstiloChromeAssetsAreLinkedInOrder, TestShortcutLabelsFollowThePlatform, TestUnbuiltMenuItemsAreHidden, TestTemplatesUseOnlyCalloutTones
  • TestConnectYourOficinaAccountIsHiddenForHomeSessions, TestTheRootStartsSignInWhenHomeIsConfigured, TestSigninWhenAlreadySignedIn, TestSignOutLandsOnTheSignedOutPage
  • TestGeneralShowsAtMostFifteenSignificantDigits, TestAppearance…/TestPostAppearance…
  • the three guards named above

One test was green at first under its mutation: that mutation didn't compile, and the harness recorded it as not red. I fixed the mutation and re-proved the test red. TestTheShareScreenIgnoresATokenInItsQuery, an existing test, went red during the adoption: the Appearance form's return_to reflected the share page's query, token included. Share now returns to its bare path.

Browser: polish-audit/r2-chrome-check.mjs with the new polish-audit/r2-sheets-check.config.mjs. It passes 989/989:

  • The bar is exactly 52px with no overflow on every page, signed in and out, at 1440/840/800/390/320. That includes sign-in, sent, the error pages, signed-out, the public link page, the grid, the table, history, comments and share.
  • Every bar control is reachable at its centre with no overlaps, including with the title focused. The floating Rename is reached at 390 and 320.
  • Also checked: the title pattern, no default-blue or visited-purple links, exactly one Sign out, the account last, [estilo-menu] at 9px, menubar titles and triggers at 0px, the switcher absent as configured, the account panel on top over the grid (elementFromPoint), list-row title colour equal to --rst-text, and unknown routes returning 404 text/html with the bar.
  • With the switcher present, 989/989 as well. Standalone has no directory, so this ran on a temporary check-only build that read R2_CHECK_DIRECTORY into the switcher. That patch was never committed.
  • CLS: sign-in → first page 0.0000; list → sheet → list 0.0000; error → home 0.0000. The bar is 0/52 on every page of every hop. The 45 → 51/52px jump is gone.
  • The same config against the pre-change build fails at once: 19 × "has [estilo-bar]", then no account menu (check-before.log).

Scripts off (Chromium): Edit shows Undo alone, Format is absent, and "Keyboard shortcuts…" opens its dialog through the invoker command.

*`tools/drive/.mjs`**, run against this build:

  • drive, chrome, menus, editing, batch-checks, tableformat and drop all pass.
  • menu-actions, autofit, contextmenu and comments pass in Chromium and WebKit. menu-actions covers 31 actions plus Escape and outside-click.
  • clipboard.mjs fails identically on the origin/main build. It is pre-existing and local to headless Chromium on this Mac (drive-clipboard-before.log), not a regression.
  • canary-batch.mjs needs the deployed HTTPS canary and wasn't run.

polish-audit/sheets-audit.mjs is now R2-aware (MARKUP=r2 PREFIX=…) and was run before and after (shots/r2-sheets-{before,after}-audit-NN-*.png). Grid first-load CLS is 0.0006, the same as the audit measured; it comes from the toolbar un-hiding.

Screenshots: polish-audit/shots/r2-sheets-{before,after}-<view>-{1440,390}.png, from polish-audit/scripts/sheets-r2-views.mjs. They cover index, sign-in, both errors, sent, both 404s both ways, the public link, the list, the grid, table, history, comments, share, import and after sign-out. Also:

  • every menubar menu open with hover
  • the account menu at 1440 and 390
  • Menu at 390 (list and grid)
  • Format at 390, and the title focused at 390
  • the shortcuts dialog, dark mode, the table-cell hover, and the scripts-off dialog

Audit findings the spec didn't cover (still open, R3)

  • Share page: the Share and Create a link buttons sit at label height; the link-sharing help runs into the next label; the owner row offers Remove on yourself.
  • Lowercase role names ("viewer", "owner") in the share selects and rows, and lowercase table-view labels ("nothing", "value").
  • Table view: the sort, filter and width controls are cramped; editing one cell stacks six unstyled selects in a tall row.
  • History: the uppercase "YOU" badge and full-size Put back buttons. Comments: 0-based Row and Column inputs instead of an A1 reference.
  • Import: the native, unstyled file input. The inline style="overflow-x:auto" wrappers remain. The link page loses cell formatting. An empty sheet's thumbnail is blank.
  • The sheet list has no row hover (measured transparent). The phone toolbar still wraps. The number-format trigger "123" has no current-format tooltip.
  • Non-grid pages keep rastrillo's reserved 15px scrollbar gutter; only the grid drops it.

Review: not yet done. This needs the whole-branch review by the highest-capability model, and estilo #11's review and merge, before it merges. I've stopped here and not merged.

VERDICT: CHANGES-REQUIRED

🤖 Whole-branch reviewer: Astra (GPT-6-Astra via Codex). Reviewed b8788ffa6e3a097dcb281a95cbeed6a56e673ce0 as one diff against fetched origin/main, following REVIEW-BRIEF.md, AGENTS.md, the shared-chrome spec and Paul's R2 rulings.

  1. P1 / high, blocking — internal/sheets/templates/layout.html:70: the new modal shortcuts dialog permits background sheet edits. Its focused Done button sends keydown events to the existing document-level handler in static/commands.js:216–234; native modal inertness does not stop that event from bubbling to document. Select A1, open Account → Keyboard shortcuts, and press Ctrl+D while Done has focus. In Chromium and WebKit the real server-rendered fixture sends a cell-write POST; Ctrl+B also sends a formatting POST. I then reproduced against the actual scratch server: A1 was Invoice, A2 was 920.5; Ctrl+D inside the open dialog POSTed {"r":1,"c":0,"raw":"Invoice","fromR":0,"fromC":0,...} to /api/sheets/1/tabs/1/cells with status 200. After reload, A2 was Invoice and its dependent formula was #VALUE!. This is a persisted, hidden content overwrite while a modal is active. Fix by preventing sheet commands from handling events originating in a modal, or while a modal is active; audit the other document-level clipboard/keyboard handlers too. Add a browser regression asserting no cell/format/undo requests and unchanged persisted cells while the dialog owns focus, while preserving native Escape, Tab and Done behavior.
  1. P2 / medium, blocking — internal/sheets/page.go:45–48: revoked Home users loop between / and /signin. identity rejects a Home session whose linked member has lost admission, so the new root redirect sends it to /signin. The bumped connect module's already-signed-in shortcut checks the still-live session cookie and sends it back to /. Both routes bypass homeRequire, which is where Sheets normally revokes that invalid session. Reproduced with newHomeFixture, homeSignedIn, then disabling admission and deleting that member's grants: six consecutive requests were / → 303 /signin → 303 / → 303 /signin → 303 / → …. A switch from All apps or opening the root now ends in a browser redirect-loop error. Fix by putting the Home-enabled root through the existing admission/revocation guard, or performing the equivalent checks before redirecting; preserve its 503 handling for resolution/storage failures. Add a regression that revokes a live Home user's admission and follows root/signin redirects to a terminating sign-in/refusal outcome with the stale session cleared.

Verification: the full make test gate passed using rastrillo CLI v0.23.0 in a temporary GOBIN, including migration checks and store/live race tests. Branch CI reports passed at b8788ff. The requested shared browser check passed 989/989 at all configured widths; all three CLS hops were 0.0000. tools/drive/menu-actions.mjs passed all 31 actions, keyboard submission, resize, Escape and outside click in Chromium and WebKit. Both engines also opened and closed the shortcuts dialog with scripts disabled. These gates do not cover finding 1. Mutating the new Home root redirect to /sheets made TestTheRootStartsSignInWhenHomeIsConfigured fail with the expected wrong Location; the scratch mutation was restored.

Security/correctness checks otherwise passed: /appearance stays inside CSRF protection and outside the session guard. A live cross-origin POST returned 403 with no preference cookie; a same-origin POST with return_to=//evil.example returned 303 to /. SafeReturn constrains the supplied return path; it is not an unchecked redirect. Root targets are fixed internal paths. The new recoverer returns generic content/reference and logs the panic's type rather than its value. Create is a real POST at both widths; Import navigates to its upload form, whose import remains a POST. No active markup or drive script links to /mode. No schema/migration changes were introduced.

The connect bump's sign-out path revokes the app session, clears handoff cookies and preserves POST with a 307 to the configured issuer; the policy permits that issuer and retains origin referrer evidence. Checked the pinned implementation and passing Go integration fixtures. I did not rerun the live id.* three-browser SSO end-to-end flow; the requested runtime checks used an isolated local instance.

Visual review of the supplied after screenshots: the shared bar, menus, sign-in and error pages look consistent and finished. The whole product still has the already-recorded R3 rough edges, particularly Share's button/label alignment, Table view's cramped form controls and History's oversized rows. Those pre-existing presentation items are not the reason for this verdict. The two reproducible regressions above are.

Estilo #11 receives APPROVE-WITH-NITS separately. Preserve the documented merge order and re-pin Sheets to the estilo main squash before merging. No branch edits, commits, pushes or merges were made; all probes and compatibility edits were confined to disposable scratch worktrees.

🤖 Fixes for Astra's review. New head e0267d8; CI passed. It pins estilo 63d2210.

  1. P1 fixed (1b2fe6e): static/modal.js loads first on every page. While any modal dialog is open, it stops key and clipboard events at the window in the capture phase; Tab, Enter on Done and Escape keep their defaults. In tools/drive/modal.mjs, Ctrl/Meta+D/B/Z/X/V, Delete, Backspace (Chromium only), a letter and Enter change nothing. Nothing is POSTed, and A1/A2 are unchanged after reload. It was red on the pre-fix build in both Chromium and WebKit (5 failures) and is green after. TestTheModalGuardIsTheFirstScript pins the load order (red under mutation).
  2. P2 fixed (a47411d): with Home configured, / goes through homeRequire. That ends a revoked user's session, keeps the 503, and sends signed-out visitors to /signin?return_to=%2F. TestARevokedHomeUserIsNotBouncedBetweenTheRootAndSignIn follows the redirects from /. Before the fix it was red: / was visited twice. Now it terminates at id.* with the session cleared. Checks rerun: make test passes; the shared check passes 989/989; menu-actions passes in both engines.

Merging on Paul's instruction ("roll in any fixes, then proceed with deployment"). Astra's two blocking findings are fixed: the modal key guard (static/modal.js, loaded first) and / going through homeRequire. The coordinating session read both fixes. The branch is re-pinned to estilo main 894afdc; CI passed at 1e12a79. Deploy order: id.* already serves Home 01166fc+.