Spec review: shared chrome (round 2), spec at 8d7bd9b
Reviewing model: Claude Fable 5.1 (claude-fable-5-1), as required by AGENTS.md Process. Read against: polish-audit/PLAN.md and the seven audit reports; estilo AGENTS.md and internal/lint; rastrillo v0.23.0 ui/ (module cache); home fix/r1-signout-ends-id-session (signout.go, directory.go, origins.go, runtime.go, docs_chooser.go, directory_catalog.go, connect/routes.go); and each app's origin/main layout, router and auth code (Docs 7543cf5, Sheets bf59232, Slides a88f7ff, Calendar f6aa41c, Memoria 02cb530, Calls 3846ca4). Nothing edited.
VERDICT: APPROVE-WITH-CHANGES
The design holds. The bar, menu, account and error-page primitives fit what the apps actually have (every app already renders a server-side <details> menubar or dropdown, a chi router without NotFound, and a Home connect path that already understands return_to). The sign-out chain is sound in principle: Referer-bound admission, redirect target derived from the matched origin, no parameter. What needs changing before a plan is written: one lint tripwire the spec itself would trip, the switcher's real cost (which is larger than stated, and half of the "Home change" already exists), a set of stale or wrong code citations that would send the first adopter to the wrong places, and a handful of mechanism details (exclusivity groups, stacking contexts, return_to validation) that are cheaper to fix in the spec than in Sheets' corrections.
Findings
1. [high] §9.3 — the spec's own CSS trips the prefix lint. [estilo-signin] [rst-form] { max-inline-size: none; } matches internal/lint's \[\s*rst- regex; make test on branch 0 fails on the spec's own example. Fix: [estilo-signin] form { … } or give the form an estilo-signin-form attribute. Worth a sentence in §0.1: estilo CSS may not descend into rastrillo markup either, only stand beside it.
2. [high] §6.2 — the switcher's cost is understated, and the "Home change that pays it" already exists. docs_chooser.go:56-83 (Entry) already does case 1: c.redirect(...): a single team skips the chooser today. What the spec doesn't say is what that redirect is: choice.Origin + "/auth/start?return_to=/" (line 155). So every switch re-runs the assertion exchange and re-mints the target app's session even when it is live. Worse, Entry requires a directory session (subject() → sessions.Current, line 132), which a person who signed into an app from a deep link never acquires. First switch from Docs to Sheets is therefore: Docs → directory /apps/sheets → /auth/start → id. authorize → interstitial → directory assertion → finish → /apps/sheets → Sheets /auth/start → id. authorize → interstitial → Sheets assertion → finish → /. Two exchanges, two interstitials, one click. Two more facts: Entry 404s on any query string (r.URL.RawQuery != "", line 65), so the ?team=<label> refinement needs an Entry change, not just a chooser change; and HOME_APP_PREVIEW_ORIGINS / HOME_DOCS_PREVIEW_ORIGIN override only the catalog page's hrefs (directory_catalog.go:12-30), not /apps/<slug>, so in a preview deployment every switcher tile lands on "No teams available". Fix (Home branch, row 1): (a) single-team redirect goes to choice.Origin + "/" and lets the app's own guard start an exchange only if it has no session; (b) /apps/<slug> honours the preview overrides the way the catalog does; (c) the spec states the directory-session hop honestly and makes §9.3's interstitial hide a prerequisite for rendering tiles. See Q2 below for what to ship in R2.
3. [high] §3.3 / §12 — the http.Error inventory and several file:line citations are stale or wrong, and one is a trap. Memoria: the calls are at handlers.go:752 and :1048, not 749/1045, and 752 is writeOutcome, the JSON answer to the upload tray, which reads a non-200 as "no answer" on purpose (comment at 748-751). Turning it into an HTML page breaks the tray. Docs: there is no /api/ prefix at all; its byte and JSON endpoints are /d/{sqid}/updates, /stream, /documents/search, so "paths under /api/" is not a usable rule there; say "route group or Accept". Sheets: a.notFound (handlers_sheets.go:264) is shared by HTML pages and /api through sheetFromURL, so the split is inside one helper. Slides and Calendar already have an errors.html and set opts.ErrorPage for rastrillo.Serve's recoverPanics; Docs, Sheets and Calls never set it, so a panic today is plain text from outside chi. The spec's r.Use(a.recoverToErrorPage) is right, but add: set Options.ErrorPage too, and mount the chi recoverer inside the session middleware or the 500 page renders signed-out chrome (Calendar's ErrorPage bug, repeated). Docs: [docs-menu-*] runs 856–1050 (not 887–982), and [docs-chrome] at 849-855 is the sticky wrapper (z-index 3).
4. [high] §9.5 — the chain is sound; three edges need naming. (a) Chromium enforces CSP form-action on the redirect target of a form submission, so the issuer must be in form-action for the 307 to be followed. connect.Middleware sets FormPolicy(issuer) (verified routes.go:76-79) and Docs' homeFormPolicy does the same, so this is already true; say so, because the Sheets report I got asserted the opposite. (b) Referer is the only evidence on the redirected hop (Origin is null). Calls' credits page sends Referrer-Policy: no-referrer as a page header (credits_test.go:19-23 requires it), so Sign out from the account menu on that page is refused and lands on "Finish signing out". Either the page-level header goes (the credit links already carry referrerpolicy="no-referrer" individually) or the account menu on that page omits Sign out. Grep every app for page-level Referrer-Policy before adoption. (c) The refused POST should render the same branded "Finish signing out" page with status 403, not http.Error text; the spec only specifies the GET. Residual, accepted: any page on a consumer origin can POST to id.*/signout (logout CSRF needs user-injected forms on a public page; none of Docs public, Sheets /l/, Slides share render user HTML forms today, but note it).
5. [med] §7.1 — two exclusivity groups is a regression from Sheets' deliberate design. name="estilo-menus" for bar triggers and name="estilo-menubar" for the menubar means File can be open while Account is open. Sheets shares rst-menus across the header and the menubar on purpose (sheet.html:113-121, pinned by guardstest/menugroup_test.go), and rastrillo's dropdown doc says the same. Use one group for every top-level <details>: bar triggers, menubar titles and the narrow Menu sheet (it can join, unlike rastrillo's shell menu, because the account isn't inside it). Only flyouts and nested groups get another name.
6. [med] §5.2 — stacking and sticky offsets aren't specified, and every app has something that will collide. [estilo-bar] { position: relative; z-index: 20 } makes the bar a stacking context, so the panels' z-index: 30 is relative inside it; any page element above 20 in the root context paints over an open account menu. Today: Calendar [cal-quick-form] z 30, Docs [docs-chrome] sticky z 3, Sheets' jspreadsheet overlays, Slides' sticky strip/inspector with top: var(--rst-sp-3), and Calendar [cal-column-head] { position: sticky; top: 0; z-index: 2 }, which will slide under a sticky bar. Add two rules to §5.2: --estilo-bar-block-size is the value apps use for scroll-padding-top and for any sticky top below the bar; and nothing in-page exceeds the bar's z-index except modals and dialogs (give the numbers). Also add a bar-width gate at ~800–840px: that is where nav + context + actions + switcher + account first compete; 1440 and 390 don't exercise it.
7. [med] §12 Docs — the first adopter is off the connect path, and the menubar rewrite hides a JS rebuild. Docs doesn't import home/connect at all (its own home_*.go on handoff), so connect.Credentials.Directory never reaches it; checklist item 5 is wrong for Docs. Either Docs reads HOME_DIRECTORY_ORIGIN itself in R2 or its Switcher stays nil until R3's move onto connect. fixtures/menu.mjs, shortcuts.mjs and editor-entry.js:326,349 all query [docs-menu*], so replacing the markup means rebuilding the committed hashed bundle (fixtures/build-bundles.mjs). And menu.go:98-99's claim that menu.mjs keeps aria-pressed in step is false today; say the View check reflects server state (a reload) unless the adopter adds the toggle code. Local-mode return_to (§9.4): Docs auth_navigation.go:55-56 says the destination "never comes from return_to" as a deliberate choice. Overturning it is fine under SafePath, but cite the reasoning rather than silently reversing it.
8. [med] §8.3 — return_to validation is a re-implementation, and weaker than the ones that exist. "Starts with /, not // or /\" misses the control-character case sessions.SafeReturn documents (browsers strip \t\r\n before parsing, so /\t/evil.example passes a // prefix check). Use sessions.SafeReturn or handoff.SafePath verbatim, as §9.4 already does. Also state that /appearance sits under csrf.Protect (Sheets' /mode is outside the session group, which is right, but must stay inside Protect), and that tools/drive/drop.mjs:227,239 sets sheets_mode and needs updating.
9. [med] §12 Memoria — the deletion range is wrong and would break other pages. memoria.css:9 is the 46rem page width, :33 [rst-inline] is used by the delete form and the Notion forms, :35-44 is the page search form (the header has no search box). Only 11–26 are header/brand rules. Also: the footer's rst-refresh="passages" must survive the new layout (drop.js:445-460 aborts the swap if any named region is missing); /connectors is behind requireOwner and 404s otherwise, so the "Sources" nav link is conditional; "Import finished, but…" is a plain <p> (notion.html:76), not a callout; and the rgb(158,158,255) is Chrome's dark-mode UA default, there is no main a rule to delete.
10. [med] §5 Sheets — the bar must host an editable title, and the 100dvh grid keys on the wrapper the spec removes. doctitle puts <form data-doc-title> with an <input> into the bar (sheet.html:49-58); the spec's Context is a read-only span. Since Docs also can't rename (audit, "Untitled document forever"), an editable-context slot is a two-caller need now, not a Sheets correction later: add Bar.ContextEdit (or a estilo-bar-context slot) to the API. [rst-shell-topbar]:has([data-sheet-surface]) { height: 100dvh } (sheets.css:32-36) keys on the wrapper; either the spec keeps <div rst-shell-topbar> as an optional outer wrapper (harmless: the rules it scopes target [rst-shell-bar]/[rst-shell-menu], which are gone) or names an estilo wrapper. Guard tests (shellmenu_test, menugroup_test, dropdownanchor_test, csstokens_test) and tools/drive/*.mjs pin the current markup; list them as expected casualties.
11. [med] §4 / §12 Calls — the table and the CSS range need correcting. "Link copied." is JS text in <p id="share-status" role="status"> (share-link.js:11), not an rst-notice. settings.html:7 and meeting_settings.html:6 ("…saved.", role="status") are missing from the table (→ positive). calls-shell.css is 28 lines; 3–6 and 13–28 are the workarounds, 7–12 are lobby and call-header typography that must stay. "Credits resolves the viewer" conflicts with credits_test.go:19-23 ("credits shell may retain account context": no cookies, no-referrer); decide it explicitly rather than let the adopter discover it. requireSession for Calls must keep the guest path (guest.go:15-25: anonymous sessions allowed under /m/) and answer the stream/signal/ice fetches the way room.js:1066-1101 expects; tests pin the bare /signin Location (api_test.go:208, join_test.go:64; Slides signin_test.go:20).
12. [med] §9.2 — "Already signed in, GET /signin → 303" needs a code change nobody owns. connect.Routes.Signin never reads the session; it always remembers return_to and 303s to /auth/start, and Slides' local signin (auth.go:553-582) never checks either. Put it on the Home branch (row 1) as a connect change plus an app-side check for local mode.
13. [low] §2 — verified: the base link rule does not bleed into rastrillo components. [rst-row-main] > a, [rst-shell-nav] a, [rst-dropdown-menu] a, [rst-back-nav] a, [rst-seg-tabs] a, [rst-shell-brand], .rst-nm and rst-btn all declare color, and those that matter declare text-decoration. Calendar's week headers and agenda headings are bare <a> and will become accent-and-underlined, which the spec intends; the Calendar report wants them muted, which is R3. Add one line to the Docs browser gate: a list-row title's computed colour equals --rst-text.
14. [low] §0.5 — stylesheet order. Every app today links brand.css before <meta charset> (Calendar L13, Sheets :19, Memoria :7, Slides :7, Calls :7). The spec's order is fine; add "charset first" to checklist item 2 since every adopter touches that line anyway.
15. [low] §12 pins. The Slides/Calls module is amadan.net/rastrillo/rastrillo (two segments), not amadan.net/rastrillo. Their vendored_test.go pins tokens.css byte-identical to their pin's ui.VendoredAssets("day"), which is the right hook for the two-pin check the spec asks for.
16. [low] grammar. [estilo-bar~="sticky"] uses ~= (AGENTS.md ruling 4) but [estilo-menu="sheet"] uses =. Pick ~= throughout.
17. [low] §3.3. A custom MethodNotAllowed handler must set Allow itself; chi's default does, yours won't.
Feasibility summary per app (question 1)
- Docs: fits. One base template cloned per page (
render.go:507-559), server-rendered<details name="docs-menubar">menubar frommenu.go,[docs-chrome]already sticky. Costs the spec omits: findings 3, 7. Sign-in page today hides the bar with CSS (signin.html:57-59); the spec's brand-only bar replaces that. - Sheets: fits, with finding 10 as the expected correction set.
homeRequirealready sendsreturn_to;theme.gois exactly the design §8.3 promotes. - Slides: fits. Presenter/remote/projector/pair are capability pages on
anon.html(render.go:95-100), so "no bar" is a template-group change.errors.htmlis in the public group and needs moving to get signed-in chrome. Its guard isamadan.net/rastrillo/rastrillo/authv0.26.1, Memoria's is carlosframework v0.23.0; the threerequireSessionwrappers are not one copy-paste. - Calendar: fits.
/mode,calendar_mode,return_toanddata-themeare already there; Booking pages is only linked from settings. Sticky column heads: finding 6. - Memoria: fits, with finding 9.
signinOutcomealready reads?sent=/?err=on main (R1 landed). - Calls: fits. Nothing in the room is fixed, sticky or height-measured; the "500px control drop" is
room.jsrevealing#controlslate (R3, not chrome). Findings 4b, 11. - Home: the directory has no header at all today (
layout.html:14-19); §12 row 8 is a build, not a swap.signoutFromHomewidening is a small diff on the branch as it stands.
Theme constraint (question 4)
Clean. Every new value is an --estilo-* property in a component file; TestMatchesDayByteForByte is untouched; the only rst-* in estilo CSS is finding 1. The UseDefinedTokens pattern (callui/tokens_test.go) covers --rst-shadow-pop, --rst-radius-sm, --rst-fs-xs, --rst-text-faint, --rst-sp-1..6, --rst-accent-soft, all of which exist in v0.23.0. Restyling rastrillo-rendered chrome is avoided by not rendering it: the bar, menus and account are estilo markup, and the error page and callouts are rastrillo markup left unstyled by estilo. That is the right split.
Sequencing and CLS (question 5)
The order is right and the "Docs pins the branch pseudo-version" trick works with GOPRIVATE. Two risks: the Home branch (row 1) is now bigger than listed (findings 2, 4c, 12) and gates Docs, so it should be cut first, not in parallel; and the 52px promise depends on finding 6 (sticky offsets) and finding 10 (an input in the bar must not grow it: give it a fixed block-size in the slot's CSS). The CLS gate as written (< 0.01 across sign-in → first page, list → object → list, error → home) is the right proof.
Gaps against the audits (question 6)
Nothing high-severity in the chrome scope is missed. Noted for R3 or as explicit exclusions: Sheets' comment bodies (B6) and vendor context menu (B7) are R1; Calls' room reflow (CLS 0.0725) and Slides' editor overflow are per-app; Home's front page (static/site/) is excluded on purpose. One audit ask the spec silently drops: Sheets' and Slides' "Connect Home account" shows even for Home sessions today (Home §F.2 footnote); §8.2 says it's hidden "as Docs, Calls and Memoria do", which implies a fix for the other two. Say so.
§13: are these the right questions, and recommendations (question 7)
The six are the right decisions for Paul, with one reframing and one missing question.
- Q1 event chips: square everywhere. Agree with the spec. A placed block is a span of time that must meet the hour rules; the 3px inline-start rule carries identity. One rule, no exemption, and the quick-add pane is a float (9px) anyway.
- Q2 switcher: reframe, then yes. The question conflates "render the block" with "the mechanism". Recommend: build the block in estilo (it costs nothing and Paul wants to see it), and in R2 render tiles only once Home's
/apps/<slug>change (finding 2a–b) has merged; until then render "All apps" alone. State the directory-session double-hop in the spec so the decision is made with the cost visible. Label: "All apps"; "Oficina" as a menu label collides with the brand on the hub's own bar. - Q3 wording: "your Oficina account", never name the hub. Agree. "Connect your Oficina account" is fine; the alternative "Use your Oficina account" reads better as a verb but loses the link-two-accounts meaning. Keep "Connect".
- Q4 radii scope: as the spec has it. Cards, inputs and bordered buttons keep
day; only in-flow highlights (including borderless toolbar buttons) go square. Anything more is a theme divergence and needs its decisions row. Accept the one visual seam this leaves (a square borderless hover next to a 6px bordered button in the same toolbar). - Q5 suite-wide sign-out: no, not in R2. The id.* fix closes the "new assertion" hole, which is the security-relevant one; front-channel logout needs Home to track consumers and is a Home design of its own. The §9.2 signed-out copy is already written to be true without it.
- Q6 appearance: per app. A
Domain=oficina.workscookie fails for previews, is readable by every team instance and id.*, and is a preference, not identity. Per-host now; if Paul finds re-choosing per app annoying, the assertion can carry a preference later. - Missing Q7: is an editable title part of the bar? Sheets renames in the bar today and Docs needs rename (audit). That's an API decision for
estilo-bar(finding 10), and it changes the 52px proof, so it belongs in §13 rather than in Sheets' corrections.
What I did not check
I did not run any app or the Home split-origin harness; feasibility is from reading origin/main. I did not verify Firefox/WebKit behaviour for Origin: null on a 307-carried POST; the spec's three-engine e2e gate is the right place for that.