fix(chrome): the grid screen, on a phone
The same four things Paul named on the Docs screenshots, answered the same way here so the two apps do not drift. The app title goes on the grid page, where "Sheets" sat beside the sheet's own name and said nothing a person needed. Scoped by :has([data-doc-title]) rather than by a page marker, because the shell is one template and on /sheets the brand IS the screen heading — the selector states the actual condition instead of naming today's page. Clipped, not removed: the link home keeps its accessible name. The route back is unaffected either way; "Your sheets" was already in the nav the collapsed tail holds. The bar's disclosure is a flat icon and a floating panel. The word moves into .rst-sr-only in the template, and the borders and the panel come off the vendored tokens.css — this app owns that file, so the edits sit beside the rules they change, each marked and dated, rather than being layered on from sheets.css where the next person to read the framework's rule would not find them. The open menu's title squares its corners. Paul: "active state looks wrong because of the rounded edges on the active state" — measured against the Data menu in his own screenshot, where a rounded highlight sat flush on a square-topped panel and left two notches of bar at the seam. And the formatting strip no longer wraps: below 30rem the two colour menus and the three alignments go behind a "..." at the trailing end and open as one panel under the chrome, while undo, the number format, bold and italic stay on the strip. Above 30rem the wrapper is `display: contents` and the desktop strip is byte-for-byte what it was. 30rem matches the breakpoint the Docs app collapses its own bar at, which is worth more than either app's own arithmetic. Two things the screenshots caught: the "..." needed the strip itself to grow before an auto margin had an end to push it to (it first landed beside italic, not opposite it), and rastrillo's [rst-dropdown] > summary padding out-specifies [data-tool]'s, so at 18px the control measured 41px in a strip of 28px ones and pushed the whole grid down — the toolbar row is 36px again at 320, 390 and 1280, the same as the desktop. Verified in Chromium at 320, 390 and 1280 against a real instance: no horizontal scroll at any of them, one toolbar row at all three, the brand clipped on the sheet and untouched on the list, and an open Insert menu whose own hit test lands on the panel. make test-vet/fmt/build/ migrations and go test ./internal/... all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jpLiN9xwBrZ5dJw92aYmF
4 files changed,
+287
−3
internal/sheets/static/sheets.css+179 −0internal/sheets/static/tokens.css+41 −2internal/sheets/templates/layout.html+19 −1internal/sheets/templates/sheet.html+48 −0
CI passed — run details
Intent
diff --git a/internal/sheets/static/sheets.css b/internal/sheets/static/sheets.css| index 81a8c4c..d403b1e 100644 |
| --- a/internal/sheets/static/sheets.css |
| +++ b/internal/sheets/static/sheets.css |
| @@ -65,6 +65,60 @@ |
| color: var(--rst-text); |
| } |
| +/* [rst-shell-bar] is the containing block for the shell menu's dropdown |
| + * panel — see tokens.css's own note on the rule that positions it. It is |
| + * here rather than there because it is not a change to a framework rule, |
| + * it is a new one this app needs. */ |
| +[rst-shell-bar] { |
| + position: relative; |
| +} |
| + |
| +/* The app's name goes on a phone, on the one screen where a document's |
| + * own name is beside it. |
| + * |
| + * Paul, on the screenshots: "Lose the app titles, they're redundant." |
| + * On the grid page he is right — "Sheets" sits next to "My sheet", |
| + * which is the only title a person opening a sheet needs, and the two |
| + * of them plus the menu left the bar with nothing but chrome in it. |
| + * |
| + * Scoped by :has([data-doc-title]) and not by a page class, because the |
| + * shell is one template (templates/layout.html) shared by every screen |
| + * and the brand is the ONLY title the others have: on /sheets it is the |
| + * screen heading, and a rule that reached it would leave a signed-in |
| + * person looking at an empty bar. The selector says the actual |
| + * condition — "there is already a title here" — rather than naming the |
| + * page that happens to satisfy it today. |
| + * |
| + * Clipped, not removed. [rst-shell-brand] is still the link home, and a |
| + * link with no accessible name is worse than one with a name nobody can |
| + * see. This is .rst-sr-only's own technique, written out because it has |
| + * to be undone again below. |
| + * |
| + * Written mobile-first and reverted inside rastrillo's own |
| + * `min-width: 800px` — the width its topbar collapses the tail at — so |
| + * the two states are exhaustive by construction. A max-width: 799px |
| + * rule would leave the fractional viewports that browser zoom really |
| + * does produce uncovered by either. */ |
| +[rst-shell-bar]:has([data-doc-title]) [rst-shell-brand] { |
| + position: absolute; |
| + inline-size: 1px; |
| + block-size: 1px; |
| + overflow: hidden; |
| + clip-path: inset(50%); |
| + white-space: nowrap; |
| +} |
| + |
| +@media (min-width: 800px) { |
| + [rst-shell-bar]:has([data-doc-title]) [rst-shell-brand] { |
| + position: static; |
| + inline-size: auto; |
| + block-size: auto; |
| + overflow: visible; |
| + clip-path: none; |
| + white-space: normal; |
| + } |
| +} |
| + |
| /* The document title, in the top bar beside the brand. It truncates |
| * rather than wrapping: the bar is a fixed height, and a two-line title |
| * would push the account menu out of it. */ |
| @@ -89,8 +143,19 @@ |
| padding: var(--rst-sp-1) var(--rst-sp-2); |
| } |
| +/* The open menu's title squares its corners. |
| + * |
| + * Paul, on the open Data menu: "active state looks wrong because of the |
| + * rounded edges on the active state." It is a real defect and not a |
| + * preference — a rounded highlight sitting flush on a square-topped |
| + * panel leaves two notches of bar showing through at the seam, so the |
| + * word reads as a pill parked on a menu instead of as the menu's own |
| + * tab. Radius and adjacency cannot both be right and the panel is the |
| + * bigger shape. The closed state keeps its radius: that is the house |
| + * shape, and it has nothing under it to line up against. */ |
| [data-menubar] > [rst-dropdown][open] > summary { |
| background: var(--rst-accent-soft); |
| + border-radius: 0; |
| color: var(--rst-accent); |
| } |
| @@ -346,6 +411,120 @@ |
| inline-size: 0.75rem; |
| } |
| +/* The overflow wrapper and its control, in their WIDE state: the |
| + * wrapper generates no box, and the control is not there at all. See |
| + * templates/sheet.html's own comment for the argument; the media query |
| + * below is where the two become a panel. */ |
| +[data-tool-extra] { |
| + display: contents; |
| +} |
| + |
| +[data-tool-overflow] { |
| + display: none; |
| +} |
| + |
| +/* The strip at the width it never fitted. |
| + * |
| + * Measured on the phone screenshot Paul sent: undo, the number format, |
| + * bold, italic, the two colour menus, three alignments and four rules |
| + * come to about 410px of content, so below that the strip took two |
| + * rows and the last alignment button sat alone under everything else. |
| + * Paul: "that formatting rail shouldn't wrap." 30rem is 480px, chosen |
| + * over the measured 410 for the usual reason — the number formats and |
| + * the tooltips are English today — and it is the same breakpoint the |
| + * Docs app collapses its own formatting bar at, which is worth more |
| + * than either app's arithmetic on its own. |
| + * |
| + * nowrap plus one control that reveals the rest, and NOT an overflow-x |
| + * scroller: a strip that scrolls sideways with no affordance hides half |
| + * of itself behind a gesture, which Paul ruled out in the same |
| + * sentence. |
| + * |
| + * :has() and not the `+` combinator the shell uses for the same trick. |
| + * `+` would force the "..." to sit BEFORE the controls it reveals, which |
| + * puts it in the middle of the wide strip and in the middle of the |
| + * order commands.js walks for the roving tabindex. :has() leaves the |
| + * DOM in reading order with the control at the end. tokens.css already |
| + * relies on :has() for the empty-account-menu rule, so this is the |
| + * file's existing floor. |
| + * |
| + * The panel hangs from [data-chrome], which is the containing block |
| + * because it is the nearest positioned ancestor once the rule below |
| + * makes it one — and it is the whole slab of chrome, so the panel opens |
| + * clear of the strip rather than over it. */ |
| +@media (max-width: 30rem) { |
| + [data-chrome] { |
| + position: relative; |
| + } |
| + |
| + /* Both of these, and the second is the one that is easy to miss: the |
| + * auto margin below pushes the "..." to the end of [data-toolbar], and |
| + * the grow makes [data-toolbar] reach the end of its row in |
| + * [data-chrome] for it to be pushed to. Without it the strip is a flex |
| + * item sized to its own content and "the end" is two buttons along — |
| + * which is exactly where the "..." first landed, beside italic rather |
| + * than opposite it. An overflow control belongs at the edge the eye |
| + * reads out to and the thumb rests on. */ |
| + [data-toolbar] { |
| + flex: 1 1 auto; |
| + flex-wrap: nowrap; |
| + } |
| + |
| + [data-tool-extra] { |
| + display: none; |
| + } |
| + |
| + [data-toolbar]:has(> [data-tool-overflow][open]) > [data-tool-extra] { |
| + align-items: center; |
| + background: var(--rst-surface); |
| + border-block-start: 1px solid var(--rst-line); |
| + box-shadow: var(--rst-shadow-pop); |
| + display: flex; |
| + flex-wrap: wrap; |
| + gap: var(--rst-sp-2); |
| + inset-block-start: 100%; |
| + inset-inline: 0; |
| + padding: var(--rst-sp-3); |
| + position: absolute; |
| + z-index: 20; |
| + } |
| + |
| + /* The rule that separated the colour menus from what came before them |
| + * is the panel's first child now, so it would draw a stray hairline |
| + * down the panel's leading edge. */ |
| + [data-toolbar]:has(> [data-tool-overflow][open]) > [data-tool-extra] > [data-tool-rule]:first-child { |
| + display: none; |
| + } |
| + |
| + [data-tool-overflow] { |
| + display: block; |
| + margin-inline-start: auto; |
| + } |
| + |
| + /* The dots are text, and at --rst-fs-sm the glyph sits low enough in |
| + * its box to read as a row of hyphens. |
| + * |
| + * padding: 0 is the load-bearing half. rastrillo's [rst-dropdown] > |
| + * summary carries 0.4rem/0.6rem and is (0,2,0) against [data-tool]'s |
| + * (0,1,0), so it wins — which is why [data-tool-menu] > summary above |
| + * already zeroes it for the number-format and colour menus. This |
| + * <details> is not one of those, so it kept the padding and, at 18px, |
| + * measured 41px tall in a strip of 28px controls: the toolbar row went |
| + * from 36px to 49px and pushed the grid down with it, on the screen |
| + * where the chrome was being cut back in the first place. */ |
| + [data-tool-overflow] > summary { |
| + font-size: 18px; |
| + line-height: 1; |
| + padding: 0; |
| + } |
| + |
| + [data-tool-overflow][open] > summary { |
| + background: var(--rst-accent-soft); |
| + border-radius: 0; |
| + color: var(--rst-accent); |
| + } |
| +} |
| + |
| /* The two colour chips on the toolbar show what the anchor cell has, |
| * so the button says what it will do rather than only what it is. |
| * --sheet-h and --sheet-c are set by commands.js from the selection. */ |
diff --git a/internal/sheets/static/tokens.css b/internal/sheets/static/tokens.css| index 16a8809..109f39f 100644 |
| --- a/internal/sheets/static/tokens.css |
| +++ b/internal/sheets/static/tokens.css |
| @@ -1396,7 +1396,24 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| documents ("a nested group MUST name a different one"), met here for |
| the first time in the framework's own shells. */ |
| .rst-shell-topbar .rst-shell__menu, [rst-shell-topbar] [rst-shell-menu] { margin-inline-start: auto; } |
| -.rst-shell-topbar .rst-shell__menu > summary, [rst-shell-topbar] [rst-shell-menu] > summary { align-items: center; border: 1px solid var(--rst-line); border-radius: var(--rst-radius-sm); cursor: pointer; display: flex; font-size: var(--rst-fs-sm); font-weight: 550; gap: 0.35rem; list-style: none; min-block-size: 24px; padding-block: 0.3rem; padding-inline: 0.55rem; } |
| +/* CHANGED FROM THE FRAMEWORK DEFAULT, 2026-09-05. This file is |
| + vendored — "From then on it is yours", says the header — and these |
| + three declarations are the edit, kept here beside the rule they |
| + change rather than layered on from sheets.css so there is one place |
| + the shell's chrome is described. |
| + rastrillo draws this summary as a bordered button carrying the menu |
| + icon and the word "Menu": border, 0.55rem of inline padding, and a |
| + 24px block floor. Paul, on the phone screenshot: "Menu should be a |
| + dropdown, doesn't need to be a button. Maybe just a flat icon?" So: |
| + no border, a 32px square target instead of a 24px lozenge (SC 2.5.8's |
| + floor is 24 and this is now the only route to sign-out on a phone, |
| + at the very edge of the screen where a thumb is least accurate), and |
| + the label moved into .rst-sr-only by templates/layout.html so the |
| + control keeps its accessible name with nothing showing but the |
| + glyph. Everything else in the rule is the framework's. */ |
| +.rst-shell-topbar .rst-shell__menu > summary, [rst-shell-topbar] [rst-shell-menu] > summary { align-items: center; border: 0; border-radius: var(--rst-radius-sm); color: var(--rst-text-muted); cursor: pointer; display: flex; font-size: var(--rst-fs-sm); font-weight: 550; gap: 0.35rem; justify-content: center; list-style: none; min-block-size: 32px; min-inline-size: 32px; padding: 0; } |
| +.rst-shell-topbar .rst-shell__menu > summary:hover, [rst-shell-topbar] [rst-shell-menu] > summary:hover { background: var(--rst-accent-soft); color: var(--rst-text); } |
| +.rst-shell-topbar .rst-shell__menu[open] > summary, [rst-shell-topbar] [rst-shell-menu][open] > summary { background: var(--rst-accent-soft); color: var(--rst-accent); } |
| .rst-shell-topbar .rst-shell__menu > summary::-webkit-details-marker, [rst-shell-topbar] [rst-shell-menu] > summary::-webkit-details-marker { display: none; } |
| .rst-shell-topbar .rst-shell__tail, [rst-shell-topbar] [rst-shell-tail] { display: none; } |
| /* The account's auto margin has to be undone here, and stretch does not |
| @@ -1406,7 +1423,22 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| edge of the collapsed stack — the same trailing-edge shove that made |
| the wrapped bar look wrong in the first place. The wide query below |
| puts it back. */ |
| -.rst-shell-topbar .rst-shell__menu[open] + .rst-shell__tail, [rst-shell-topbar] [rst-shell-menu][open] + [rst-shell-tail] { display: flex; flex-basis: 100%; flex-direction: column; gap: var(--rst-sp-3); } |
| +/* CHANGED FROM THE FRAMEWORK DEFAULT, 2026-09-05 — the second half of |
| + the same note as the summary above. |
| + rastrillo opens the tail as a second ROW of the bar (flex-basis: |
| + 100%), which is right for a shell with real navigation in it and |
| + wrong for this one: the tail here holds one link and an account |
| + menu, and pushing the whole grid down the screen to show them is a |
| + lot of movement for that. Paul asked for a dropdown, so it is one — |
| + absolutely positioned against [rst-shell-bar], which sheets.css |
| + makes `position: relative` for it. flex-basis is ignored on an |
| + out-of-flow box, so the framework's declaration and this one do not |
| + fight; it is left in place so this rule stays a diff of that one. |
| + The wide query at the foot of this block sets `position: static` |
| + back, because above 800px the tail is `display: contents` and an |
| + out-of-flow box with that display is a question engines have |
| + answered differently. */ |
| +.rst-shell-topbar .rst-shell__menu[open] + .rst-shell__tail, [rst-shell-topbar] [rst-shell-menu][open] + [rst-shell-tail] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius-sm); box-shadow: var(--rst-shadow-pop); display: flex; flex-basis: 100%; flex-direction: column; gap: var(--rst-sp-3); inset-block-start: calc(100% + var(--rst-sp-2)); inset-inline-end: 0; min-inline-size: 12rem; padding: var(--rst-sp-2); position: absolute; z-index: 20; } |
| .rst-shell-topbar .rst-shell__tail > .rst-shell__account, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-account] { margin-inline-start: 0; } |
| /* flex-start so a nav link's aria-current underline is as wide as its |
| own label rather than as wide as the bar, where it reads as a rule |
| @@ -1428,6 +1460,13 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| [rst-shell-topbar] [rst-shell-tail], |
| .rst-shell-topbar .rst-shell__menu[open] + .rst-shell__tail, |
| [rst-shell-topbar] [rst-shell-menu][open] + [rst-shell-tail] { display: contents; } |
| + /* Undoes the dropdown box above, for the same reason the two rules |
| + either side of it are restated here: a media query adds no |
| + specificity, so the (0,4,0) narrow rule would otherwise keep the |
| + tail a floating panel for anyone who opened the menu and then |
| + widened the window. */ |
| + .rst-shell-topbar .rst-shell__menu[open] + .rst-shell__tail, |
| + [rst-shell-topbar] [rst-shell-menu][open] + [rst-shell-tail] { background: none; border: 0; box-shadow: none; min-inline-size: 0; padding: 0; position: static; z-index: auto; } |
| .rst-shell-topbar .rst-shell__tail > .rst-shell__account, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-account] { margin-inline-start: auto; } |
| .rst-shell-topbar .rst-shell__tail > .rst-shell__nav, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] { align-items: normal; flex-direction: row; gap: var(--rst-sp-4); } |
| .rst-shell-topbar .rst-shell__tail > .rst-shell__nav a, [rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a { padding-inline: 0; } |
diff --git a/internal/sheets/templates/layout.html b/internal/sheets/templates/layout.html| index 6920a26..d3f9c1d 100644 |
| --- a/internal/sheets/templates/layout.html |
| +++ b/internal/sheets/templates/layout.html |
| @@ -70,8 +70,26 @@ |
| nav, and a control that discloses nothing is worse than no control. |
| rastrillo hides it itself above 800px, so there is no media query |
| of ours here. */}} |
| +{{/* A flat icon, not a bordered button labelled "Menu". |
| + |
| + Paul, on the phone screenshot: "Menu should be a dropdown, doesn't |
| + need to be a button. Maybe just a flat icon?" On a bar that now |
| + carries the sheet's own name and nothing else beside it, a boxed |
| + word was the loudest thing on the screen and the least interesting. |
| + |
| + The word survives as the summary's accessible name — .rst-sr-only |
| + moves it out of the picture and leaves it in the accessibility |
| + tree — because the icon carries no name of its own: rastrillo's |
| + {{icon}} helper emits an aria-hidden SVG, on the correct |
| + assumption that a glyph in a control is decoration and the control |
| + is what gets named. |
| + |
| + The border, the padding and the panel behaviour are in |
| + static/tokens.css, which this app vendors and therefore owns; the |
| + rule this replaces is the framework's default and the comment |
| + there says what changed and why. */}} |
| <details rst-shell-menu> |
| -<summary>Menu</summary> |
| +<summary>{{icon "menu"}}<span class="rst-sr-only">Menu</span></summary> |
| </details> |
| {{end}} |
| <div rst-shell-tail> |
diff --git a/internal/sheets/templates/sheet.html b/internal/sheets/templates/sheet.html| index e29d0d0..f42cf1e 100644 |
| --- a/internal/sheets/templates/sheet.html |
| +++ b/internal/sheets/templates/sheet.html |
| @@ -314,6 +314,34 @@ |
| <button type="button" data-tool rst-tip data-cmd="bold" aria-pressed="false" aria-label="Bold" data-tip="Bold (Ctrl+B)"><b>B</b></button> |
| <button type="button" data-tool rst-tip data-cmd="italic" aria-pressed="false" aria-label="Italic" data-tip="Italic (Ctrl+I)"><i>I</i></button> |
| +{{/* Everything from here to the end of the strip, wrapped so a phone |
| + can put it behind one control. |
| + |
| + Paul: "that formatting rail shouldn't wrap... instead of |
| + horizontally scrolling, it should have a '...' menu that provides a |
| + dropdown." At 320px this strip measured two rows deep — the |
| + screenshot has the last alignment button sitting alone under |
| + everything else — and a rail whose height depends on the viewport |
| + is not a rail. |
| + |
| + THIS ELEMENT DRAWS NOTHING above 30rem: it is `display: contents` |
| + there, so the two colour menus, the rule and the three alignments |
| + are flex items of [data-toolbar] exactly as they were before it |
| + existed, in the same order. The wide strip is unchanged. It is a |
| + wrapper rather than a per-control attribute because the narrow |
| + state needs ONE box to become ONE panel, and siblings cannot. |
| + |
| + The split falls here because it is the last point that is both |
| + contiguous in the DOM and leaves something worth having on the |
| + strip. What stays is undo, the number format, bold and italic — |
| + the four a person reaches for without thinking; what goes is |
| + colour, which is a considered choice made rarely, and alignment, |
| + which is also in the Format menu. Moving the boundary further left |
| + would need the DOM reordered, and commands.js walks this order for |
| + the toolbar's roving tabindex, so visual order and focus order |
| + would stop agreeing (WCAG 2.4.3). */}} |
| +<div data-tool-extra> |
| + |
| <span data-tool-rule aria-hidden="true"></span> |
| <details rst-dropdown name="rst-tools" data-tool-menu> |
| @@ -344,6 +372,26 @@ |
| <button type="button" data-tool rst-tip data-cmd="align" data-value="center" aria-pressed="false" aria-label="Align centre" data-tip="Align centre">{{template "glyph-align-center"}}</button> |
| <button type="button" data-tool rst-tip data-cmd="align" data-value="right" aria-pressed="false" aria-label="Align right" data-tip="Align right">{{template "glyph-align-right"}}</button> |
| +</div> |
| + |
| +{{/* The "..." itself, at the trailing end and not rendered at all above |
| + 30rem. |
| + |
| + A <details rst-dropdown> like every other menu here, in the |
| + toolbar's own "rst-tools" exclusive group so that opening it closes |
| + the colour menus and vice versa — one panel at a time, which is |
| + what the group is for. rastrillo.js gives it click-away and Escape |
| + with nothing added. |
| + |
| + Three dots and not icons.go's "kebab": kebab is three STACKED dots |
| + and its settled meaning across this design system is "more actions |
| + on this row", which is not what this is. The accessible name is on |
| + the summary because the glyph is a literal ellipsis with nothing to |
| + announce. */}} |
| +<details rst-dropdown name="rst-tools" data-tool-overflow> |
| +<summary data-tool rst-tip aria-label="More formatting" data-tip="More formatting">…</summary> |
| +</details> |
| + |
| </div> |
| {{end}} |