{{/* list — GET /, the document list (spec §6.4). handlers.go's list builds the view data: Title and the group labels already resolved through rastrillo.T, EmptyTitle/EmptyBody the same; a row's Title and When are the document's own data and are printed as-is. The header's create
is the app's one create control (fix round 1 on Task 1): a plain POST to /documents, no JavaScript. It is outside the {{if .Groups}} branch below on purpose: the header renders unconditionally, so the control reaches a person with no documents yet exactly the same way it reaches everyone else, rather than only appearing once there is something to list. [docs-header-actions] wraps the search form and that create form as one group: [rst-page-header]'s flex/space-between rule wants at most two top-level children (titles, one action area), so both live inside this one wrapper rather than becoming a third top-level child of the header itself. They stay two separate s, never one, because they are unrelated actions with different methods and targets (GET / vs POST /documents) and must never be merged into one submit. Sign-out (POST /signout, auth.go) used to be a third form in that wrapper. It is in the shell bar's account menu now — the same control, in the same place, on every signed-in screen. It was added to this header in a fix round once real sign-in existed and nothing on the page could end a session; that is still the reason it must exist somewhere, and a header holding search, create and sign-out at once had become five controls on one line. Writes the bare rst-* attribute grammar throughout — the rst- vocabulary always as a bare attribute, never as a class — which is this app's whole-screen markup convention (Task 1's home page set it). render_test.go's TestOurTemplatesUseTheAttributeGrammar walks this file's own bytes to hold that; rastrillo's vendored shell and partials keep their own class spelling, which is their output, not this app's, and is out of that test's reach on purpose. The [docs-*] selectors below are this app's own vocabulary, not rastrillo's — SKILL.md §7's "ui is rastrillo's vocabulary, not yours" is why they are not spelled rst-*, and they carry their own tiny stylesheet here rather than a static/ file this hand-built app does not otherwise have one of. */}} {{/* The bar, the account menu and the are the layout's (views/layout.html, chrome.go): this screen defines only its content. The bar names the app, "Docs"; the heading below names the screen, "Documents". */}} {{/* thumb — one document's page one, drawn from the line boxes preview.go reports. aria-hidden because it is decorative: it is wholly redundant with the document's own title beside it, and there is no non-visual equivalent of "the shape of the paragraphs" to offer instead. Deliberately no <title>, no role and no tabindex — see AGENTS.md on a role being a promise. An empty .Thumb renders the bordered box with nothing in it, which is what an empty document, and a document whose structure could not be read, both get. */}} {{define "thumb"}}<span docs-thumb aria-hidden="true">{{if .Bars}}<svg viewBox="0 0 {{.W}} {{.H}}" preserveAspectRatio="xMidYMin slice">{{range .Bars}}<rect x="{{.X}}" y="{{.Y}}" width="{{.W}}" height="{{.H}}"/>{{end}}</svg>{{end}}</span>{{end}} {{define "content"}}<style> /* [docs-thumb] holds a real rendering of the document's page one: the <svg> inside it is the line boxes the PDF exporter's own layout placed, through export.PageOne (see preview.go). It was reserved and deliberately empty until that existed — "reserved space, not a placeholder image" — and the box is unchanged now that it is filled, which is what that reservation was for. 26×34 is literal: the brief's own figure, and within a percent of a US-Letter page's aspect at row scale, so the page inside it is not distorted to fit. */ [docs-thumb] { display: block; flex: none; inline-size: 26px; block-size: 34px; border: 1px solid var(--rst-line-strong); border-radius: var(--rst-radius-sm); background: var(--rst-surface); overflow: hidden; } [docs-thumb] > svg { display: block; inline-size: 100%; block-size: 100%; } /* The fill is a token rather than a presentation attribute on each rect: var() in an SVG presentation attribute has a patchy support history, and a token here follows the theme the way every other colour on this page does. Nothing about the thumbnail carries meaning by colour alone — a heading reads as a heading because its bar is taller, which is the real ratio between the two type sizes. */ [docs-thumb] rect { fill: var(--rst-text-muted); } [docs-group-label] { margin: 0; padding: var(--rst-sp-3) var(--rst-sp-3) var(--rst-sp-1); font-size: var(--rst-fs-xs); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--rst-text-muted); } [docs-row-when] { flex: none; color: var(--rst-text-muted); font-size: var(--rst-fs-sm); } /* No row-click rule here on purpose, and it is worth saying why, because the measurement that argues for one is misleading. A row's title link measures 268×16 — well under the 24×24 WCAG 2.2 AA target minimum, with the thumbnail and the whole right half of the row apparently dead. That reading is wrong, and getBoundingClientRect is what makes it wrong: it reports an element's own box and cannot see an absolutely positioned pseudo-element. rastrillo already stretches this link over the whole row. tokens.css gives [rst-row] position: relative, gives [rst-row]:hover the --rst-surface-2 tint, and gives [rst-row-main] > a::after content:"" with inset: 0, under its own comment: "The name link's overlay makes the whole row clickable without nesting one anchor inside another." Our markup already matches that selector, so the row is fully clickable, fully hover-tinted and past 24×24 with nothing of ours involved. An earlier version of this file added [docs-row] and [docs-row-link]::after restating those exact declarations, and a hit test then "confirmed" a fix that had changed nothing — the probe was hitting rastrillo's overlay both before and after. If you are about to add one: hit-test the row before you change anything, not only after. */ /* [docs-header-actions]: the header's one flex child opposite the titles — [rst-page-header]'s space-between rule places it there with no layout rule of its own beyond this; everything it holds (the search form and the create form — sign-out moved into the shell's account control, see the note at the top of this file) lays out as one inline group inside it. */ [docs-header-actions] { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rst-sp-3); } /* [docs-create-form]: the title field and its submit button as one inline row — nowrap, deliberately, and this is the part that took two attempts. A wrapping flex line does not shrink its items to fit; it breaks first and shrinks only what still does not fit afterwards. So with wrap on, the button dropped to a second line and the field — now alone on the first one, with room to grow — sprang back to its 14rem maximum, which is how a fix aimed at making the header shorter measured exactly as tall as before it (175px, unchanged). nowrap is what forces the field to give up width to its button instead of the button giving up the line. The button itself is flex: none so the whole shortfall comes out of the field. Shared proportionally, both would shrink, and "New document" would start wrapping or clipping inside a control that had no reason to be narrower. One thing to watch when the other eleven catalogs land: nowrap plus a non-shrinking button means a locale whose docs.list.create.label is wider than about 270px overflows the 320px column rather than wrapping out of it. English is 131px and is the only catalog today, so this is a note rather than a bug — but it is a real edge, and the fix when it arrives is a wrap allowance keyed to the label, not a smaller field. */ [docs-create-form] { display: flex; flex-wrap: nowrap; gap: var(--rst-sp-2); } [docs-create-form] button { flex: none; } /* The title field shares its line with the submit button instead of holding a fixed 14rem and pushing it onto a line of its own. Measured at 320px — the reflow width spec §10.3 commits to — before this changed: a fixed 14rem (224px) field could not sit beside a 131px button in a 288px content column, so the field, "New document" and "Sign out" each took a full row and the header stood 175px tall, putting the first document 223px down a 720px viewport. Just under a third of the screen spent on chrome before a single thing a person came here to read. flex-basis 10rem with room to shrink is what lets the field and its button share one line in that column; the 14rem cap keeps the desktop header exactly as it was, since the form still sizes to its max-content there. min-inline-size: 0 is load-bearing — a flex item's automatic minimum size is its content, so without it an <input> refuses to shrink below its default size and the row overflows rather than fitting. */ [docs-create-form] [rst-input] { flex: 1 1 10rem; min-inline-size: 0; max-inline-size: 14rem; } /* [docs-search-form] is the same inline row as the create form, with one difference: it does NOT wrap. The header carries several controls, and with wrapping allowed the search button dropped onto a line of its own under its own field while the create form stayed whole — which reads as two rows of unrelated chrome rather than as one field and its button. Staying on one row is only safe because the FIELD shrinks, and that is the part this rule got wrong for a while. It previously carried a comment asserting that the group wrapping at [docs-header-actions] meant "a 320px viewport puts each form on its own line intact rather than breaking one of them in half". That was never measured and was wrong. The field held a fixed 11rem, its button holds a label that does not wrap, and max-inline-size: 100% resolves against a form whose own width is its content — so the form set a min-content floor of about 334px and simply stood wider than the 288px column. Measured on origin/main at 7507a33 with scripts/browser-drive.sh: the list page scrolled sideways at 320px, scrollWidth 356 against a 320 viewport, with this form's button the innermost offender at x=356. That is a WCAG 2.2 SC 1.4.10 failure, which AGENTS.md allows no exemption from, traded for a desktop layout nicety by a comment that sounded like it had been checked. Note what the floor did to its NEIGHBOUR. [docs-create-form] was measured over the edge too, at x=350, and it is correctly written — it shrinks exactly as it should. It overflowed because a wrapping flex container is at least as wide as its widest item's min-content, so this form's 334px floor widened every line in the group, including the one the create form had to itself. Fixing the field below fixes both; there was never a second bug. Worth knowing before someone "fixes" the create form as well. The fix is that the field takes what is left after the button rather than claiming a width: flex 1 1 with a small basis, and min-inline-size 0 to lift a flex item's automatic minimum, which is its content size and is what an <input> otherwise refuses to go below. The 11rem cap keeps the desktop header exactly as it was. */ [docs-search-form] { display: flex; flex-wrap: nowrap; align-items: center; gap: var(--rst-sp-2); max-inline-size: 100%; } [docs-search-form] [rst-input] { flex: 1 1 6rem; min-inline-size: 0; max-inline-size: 11rem; } /* The submit button is for a pointer and for a screen reader; a keyboard user submits with Enter from the field, which a single-input form does natively. It stays visible rather than becoming an icon because "Search documents" is also the field's accessible name and the two agreeing is worth more than the width. flex: 0 0 auto for the same reason: the whole shortfall comes out of the field, so the button keeps its whole label. It is the words that are load-bearing here, not the box. The locale caveat, stated as the sequence it actually is rather than as a single threshold. Because the field gives way first and the button never shrinks, a wider label eats the FIELD long before anything overflows: a ~200px label leaves about 80px of field, ~270px leaves a sliver, and only past ~280px — the 288px column less the gap — does anything cross the edge. So the first failure a wide catalog produces is a search box too narrow to read what you typed, which is a usability problem and not a 1.4.10 one; the overflow comes later. English is 131px and is the only catalog today. The fix when it arrives is a wrap allowance keyed to the label, not a smaller field. ([docs-create-form]'s own comment above states the same caveat as a bare "wider than about 270px overflows", which is the second half of this sequence and not the part that bites first.) */ [docs-search-form] button { white-space: nowrap; flex: 0 0 auto; } /* The way back out of a result page, under the results rather than in the header: it is about the list being shown, not about the controls that produced it. */ [docs-search-clear] { margin-block-start: var(--rst-sp-3); font-size: var(--rst-fs-sm); } </style> <header rst-page-header> <div rst-page-header-titles> <h1>{{.Title}}</h1> </div> <div docs-header-actions> {{/* Search (spec §15.2's title search). A GET form, so a search is a link — bookmarkable, reloadable, and reachable with the back button — and so the whole feature works with no JavaScript at all. fixtures/jump-to.mjs adds a keyboard shortcut on top of this; it does not replace it. type="search" rather than type="text": it gets the platform's own clear control and its own keyboard on a phone. The value is echoed back so a result page can be edited and re-submitted rather than retyped. */}} <form docs-search-form method="get" action="/" role="search"> <input rst-input docs-search-input type="search" name="q" value="{{.Query}}" placeholder="{{.SearchPlace}}" aria-label="{{.SearchLabel}}" autocomplete="off"> <button rst-btn="ghost" type="submit">{{.SearchLabel}}</button> </form> {{if .CanCreate}} <form docs-create-form method="post" action="/documents"> <input rst-input type="text" name="title" placeholder="{{.CreatePlaceholder}}" aria-label="{{.CreateLabel}}" autocomplete="off"> <button rst-btn="primary" type="submit">{{.CreateLabel}}</button> </form>{{end}} </div> </header> {{if .PartialCatalog}}{{template "callout" (dict "Body" .PartialCatalog "Tone" "warning")}}{{end}} {{/* Searching renders a ranked, UNGROUPED result list. Recency grouping is what an unfiltered list is for; grouping ranked results under "Today" and "Earlier" would order the same rows twice by two different rules and show only the second. */}} {{if .Searching}} {{if .Results}} <div rst-list> {{range .Results}} <div rst-row> {{template "thumb" .Thumb}} <span rst-row-main><a href="{{.Href}}">{{.Title}}</a></span> <span docs-row-when>{{.When}}</span>{{if .SharedNote}}<span rst-muted docs-row-shared>{{.SharedNote}}</span>{{end}} </div> {{end}} </div> {{else}} <div rst-empty> <h2 rst-empty-title>{{.NoResultsHead}}</h2> <p rst-empty-body>{{.NoResultsBody}}</p> </div> {{end}} <p docs-search-clear><a href="/">{{.ClearLabel}}</a></p> {{else if .Groups}} <div rst-list> {{range .Groups}} <h2 docs-group-label>{{.Label}}</h2> {{range .Rows}} <div rst-row> {{template "thumb" .Thumb}} <span rst-row-main><a href="{{.Href}}">{{.Title}}</a></span> <span docs-row-when>{{.When}}</span>{{if .SharedNote}}<span rst-muted docs-row-shared>{{.SharedNote}}</span>{{end}} </div> {{end}} {{end}} </div> {{else}} <div rst-empty> <h2 rst-empty-title>{{.EmptyTitle}}</h2> <p rst-empty-body>{{.EmptyBody}}</p> </div> {{end}} {{template "docs-jumpto" .}} {{end}}