| 1 | {{/* list — GET /, the document list (spec §6.4). handlers.go's list |
| 2 | builds the view data: Title and the group labels already resolved |
| 3 | through rastrillo.T, EmptyTitle/EmptyBody the same; a row's Title |
| 4 | and When are the document's own data and are printed as-is. |
| 5 | |
| 6 | The header's create <form> is the app's one create control (fix |
| 7 | round 1 on Task 1): a plain POST to /documents, no JavaScript. It |
| 8 | is outside the {{if .Groups}} branch below on purpose: the header |
| 9 | renders unconditionally, so the control reaches a person with no |
| 10 | documents yet exactly the same way it reaches everyone else, |
| 11 | rather than only appearing once there is something to list. |
| 12 | |
| 13 | [docs-header-actions] wraps the search form and that create form |
| 14 | as one group: [rst-page-header]'s flex/space-between rule wants at |
| 15 | most two top-level children (titles, one action area), so both |
| 16 | live inside this one wrapper rather than becoming a third |
| 17 | top-level child of the header itself. They stay two separate |
| 18 | <form>s, never one, because they are unrelated actions with |
| 19 | different methods and targets (GET / vs POST /documents) and must |
| 20 | never be merged into one submit. |
| 21 | |
| 22 | Sign-out (POST /signout, auth.go) used to be a third form in that |
| 23 | wrapper. It is in the shell bar's account menu now — the same |
| 24 | control, in the same place, on every signed-in screen. It was |
| 25 | added to this header in a fix round once real sign-in existed and |
| 26 | nothing on the page could end a session; that is still the reason |
| 27 | it must exist somewhere, and a header holding search, create and |
| 28 | sign-out at once had become five controls on one line. |
| 29 | |
| 30 | Writes the bare rst-* attribute grammar throughout — the rst- |
| 31 | vocabulary always as a bare attribute, never as a class — which is |
| 32 | this app's whole-screen markup convention (Task 1's home page set |
| 33 | it). render_test.go's TestOurTemplatesUseTheAttributeGrammar walks |
| 34 | this file's own bytes to hold that; rastrillo's vendored shell and |
| 35 | partials keep their own class spelling, which is their output, not |
| 36 | this app's, and is out of that test's reach on purpose. The |
| 37 | [docs-*] selectors below are this app's own vocabulary, not |
| 38 | rastrillo's — SKILL.md §7's "ui is rastrillo's vocabulary, not |
| 39 | yours" is why they are not spelled rst-*, and they carry their |
| 40 | own tiny stylesheet here rather than a static/ file this |
| 41 | hand-built app does not otherwise have one of. */}} |
| 42 | {{/* The bar, the account menu and the <title> are the layout's |
| 43 | (views/layout.html, chrome.go): this screen defines only its content. |
| 44 | The bar names the app, "Docs"; the heading below names the screen, |
| 45 | "Documents". */}} |
| 46 | {{/* thumb — one document's page one, drawn from the line boxes |
| 47 | preview.go reports. aria-hidden because it is decorative: it is |
| 48 | wholly redundant with the document's own title beside it, and |
| 49 | there is no non-visual equivalent of "the shape of the |
| 50 | paragraphs" to offer instead. Deliberately no <title>, no role |
| 51 | and no tabindex — see AGENTS.md on a role being a promise. |
| 52 | |
| 53 | An empty .Thumb renders the bordered box with nothing in it, |
| 54 | which is what an empty document, and a document whose structure |
| 55 | could not be read, both get. */}} |
| 56 | {{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}} |
| 57 | {{define "content"}}<style> |
| 58 | /* [docs-thumb] holds a real rendering of the document's page one: |
| 59 | the <svg> inside it is the line boxes the PDF exporter's own |
| 60 | layout placed, through export.PageOne (see preview.go). It was |
| 61 | reserved and deliberately empty until that existed — "reserved |
| 62 | space, not a placeholder image" — and the box is unchanged now |
| 63 | that it is filled, which is what that reservation was for. |
| 64 | 26×34 is literal: the brief's own figure, and within a percent |
| 65 | of a US-Letter page's aspect at row scale, so the page inside it |
| 66 | is not distorted to fit. */ |
| 67 | [docs-thumb] { |
| 68 | display: block; |
| 69 | flex: none; |
| 70 | inline-size: 26px; |
| 71 | block-size: 34px; |
| 72 | border: 1px solid var(--rst-line-strong); |
| 73 | border-radius: var(--rst-radius-sm); |
| 74 | background: var(--rst-surface); |
| 75 | overflow: hidden; |
| 76 | } |
| 77 | [docs-thumb] > svg { |
| 78 | display: block; |
| 79 | inline-size: 100%; |
| 80 | block-size: 100%; |
| 81 | } |
| 82 | /* The fill is a token rather than a presentation attribute on each |
| 83 | rect: var() in an SVG presentation attribute has a patchy support |
| 84 | history, and a token here follows the theme the way every other |
| 85 | colour on this page does. Nothing about the thumbnail carries |
| 86 | meaning by colour alone — a heading reads as a heading because |
| 87 | its bar is taller, which is the real ratio between the two type |
| 88 | sizes. */ |
| 89 | [docs-thumb] rect { |
| 90 | fill: var(--rst-text-muted); |
| 91 | } |
| 92 | [docs-group-label] { |
| 93 | margin: 0; |
| 94 | padding: var(--rst-sp-3) var(--rst-sp-3) var(--rst-sp-1); |
| 95 | font-size: var(--rst-fs-xs); |
| 96 | font-weight: 700; |
| 97 | letter-spacing: 0.04em; |
| 98 | text-transform: uppercase; |
| 99 | color: var(--rst-text-muted); |
| 100 | } |
| 101 | [docs-row-when] { |
| 102 | flex: none; |
| 103 | color: var(--rst-text-muted); |
| 104 | font-size: var(--rst-fs-sm); |
| 105 | } |
| 106 | /* No row-click rule here on purpose, and it is worth saying why, |
| 107 | because the measurement that argues for one is misleading. |
| 108 | A row's title link measures 268×16 — well under the 24×24 WCAG |
| 109 | 2.2 AA target minimum, with the thumbnail and the whole right |
| 110 | half of the row apparently dead. That reading is wrong, and |
| 111 | getBoundingClientRect is what makes it wrong: it reports an |
| 112 | element's own box and cannot see an absolutely positioned |
| 113 | pseudo-element. rastrillo already stretches this link over the |
| 114 | whole row. tokens.css gives [rst-row] position: relative, gives |
| 115 | [rst-row]:hover the --rst-surface-2 tint, and gives |
| 116 | [rst-row-main] > a::after content:"" with inset: 0, under its own |
| 117 | comment: "The name link's overlay makes the whole row clickable |
| 118 | without nesting one anchor inside another." |
| 119 | Our markup already matches that selector, so the row is fully |
| 120 | clickable, fully hover-tinted and past 24×24 with nothing of ours |
| 121 | involved. An earlier version of this file added [docs-row] and |
| 122 | [docs-row-link]::after restating those exact declarations, and a |
| 123 | hit test then "confirmed" a fix that had changed nothing — the |
| 124 | probe was hitting rastrillo's overlay both before and after. |
| 125 | If you are about to add one: hit-test the row before you change |
| 126 | anything, not only after. */ |
| 127 | /* [docs-header-actions]: the header's one flex child opposite the |
| 128 | titles — [rst-page-header]'s space-between rule places it there |
| 129 | with no layout rule of its own beyond this; everything it holds |
| 130 | (the search form and the create form — sign-out moved into the |
| 131 | shell's account control, see the note at the top of this file) |
| 132 | lays out as one inline group inside it. */ |
| 133 | [docs-header-actions] { |
| 134 | display: flex; |
| 135 | flex-wrap: wrap; |
| 136 | align-items: center; |
| 137 | gap: var(--rst-sp-3); |
| 138 | } |
| 139 | /* [docs-create-form]: the title field and its submit button as one |
| 140 | inline row — nowrap, deliberately, and this is the part that took |
| 141 | two attempts. |
| 142 | A wrapping flex line does not shrink its items to fit; it breaks |
| 143 | first and shrinks only what still does not fit afterwards. So |
| 144 | with wrap on, the button dropped to a second line and the field — |
| 145 | now alone on the first one, with room to grow — sprang back to |
| 146 | its 14rem maximum, which is how a fix aimed at making the header |
| 147 | shorter measured exactly as tall as before it (175px, unchanged). |
| 148 | nowrap is what forces the field to give up width to its button |
| 149 | instead of the button giving up the line. |
| 150 | The button itself is flex: none so the whole shortfall comes out |
| 151 | of the field. Shared proportionally, both would shrink, and |
| 152 | "New document" would start wrapping or clipping inside a control |
| 153 | that had no reason to be narrower. |
| 154 | One thing to watch when the other eleven catalogs land: nowrap |
| 155 | plus a non-shrinking button means a locale whose |
| 156 | docs.list.create.label is wider than about 270px overflows the |
| 157 | 320px column rather than wrapping out of it. English is 131px and |
| 158 | is the only catalog today, so this is a note rather than a bug — |
| 159 | but it is a real edge, and the fix when it arrives is a |
| 160 | wrap allowance keyed to the label, not a smaller field. */ |
| 161 | [docs-create-form] { |
| 162 | display: flex; |
| 163 | flex-wrap: nowrap; |
| 164 | gap: var(--rst-sp-2); |
| 165 | } |
| 166 | [docs-create-form] button { |
| 167 | flex: none; |
| 168 | } |
| 169 | /* The title field shares its line with the submit button instead of |
| 170 | holding a fixed 14rem and pushing it onto a line of its own. |
| 171 | Measured at 320px — the reflow width spec §10.3 commits to — |
| 172 | before this changed: a fixed 14rem (224px) field could not sit |
| 173 | beside a 131px button in a 288px content column, so the field, |
| 174 | "New document" and "Sign out" each took a full row and the header |
| 175 | stood 175px tall, putting the first document 223px down a 720px |
| 176 | viewport. Just under a third of the screen spent on chrome before |
| 177 | a single thing a person came here to read. |
| 178 | flex-basis 10rem with room to shrink is what lets the field and |
| 179 | its button share one line in that column; the 14rem cap keeps the |
| 180 | desktop header exactly as it was, since the form still sizes to |
| 181 | its max-content there. min-inline-size: 0 is load-bearing — a |
| 182 | flex item's automatic minimum size is its content, so without it |
| 183 | an <input> refuses to shrink below its default size and the row |
| 184 | overflows rather than fitting. */ |
| 185 | [docs-create-form] [rst-input] { |
| 186 | flex: 1 1 10rem; |
| 187 | min-inline-size: 0; |
| 188 | max-inline-size: 14rem; |
| 189 | } |
| 190 | /* [docs-search-form] is the same inline row as the create form, with |
| 191 | one difference: it does NOT wrap. The header carries several |
| 192 | controls, and with wrapping allowed the search button dropped onto |
| 193 | a line of its own under its own field while the create form stayed |
| 194 | whole — which reads as two rows of unrelated chrome rather than as |
| 195 | one field and its button. |
| 196 | |
| 197 | Staying on one row is only safe because the FIELD shrinks, and |
| 198 | that is the part this rule got wrong for a while. It previously |
| 199 | carried a comment asserting that the group wrapping at |
| 200 | [docs-header-actions] meant "a 320px viewport puts each form on |
| 201 | its own line intact rather than breaking one of them in half". |
| 202 | That was never measured and was wrong. The field held a fixed |
| 203 | 11rem, its button holds a label that does not wrap, and |
| 204 | max-inline-size: 100% resolves against a form whose own width is |
| 205 | its content — so the form set a min-content floor of about 334px |
| 206 | and simply stood wider than the 288px column. Measured on |
| 207 | origin/main at 7507a33 with scripts/browser-drive.sh: the list |
| 208 | page scrolled sideways at 320px, scrollWidth 356 against a 320 |
| 209 | viewport, with this form's button the innermost offender at |
| 210 | x=356. |
| 211 | |
| 212 | That is a WCAG 2.2 SC 1.4.10 failure, which AGENTS.md allows no |
| 213 | exemption from, traded for a desktop layout nicety by a comment |
| 214 | that sounded like it had been checked. |
| 215 | |
| 216 | Note what the floor did to its NEIGHBOUR. [docs-create-form] was |
| 217 | measured over the edge too, at x=350, and it is correctly written |
| 218 | — it shrinks exactly as it should. It overflowed because a |
| 219 | wrapping flex container is at least as wide as its widest item's |
| 220 | min-content, so this form's 334px floor widened every line in the |
| 221 | group, including the one the create form had to itself. Fixing |
| 222 | the field below fixes both; there was never a second bug. Worth |
| 223 | knowing before someone "fixes" the create form as well. |
| 224 | |
| 225 | The fix is that the field takes what is left after the button |
| 226 | rather than claiming a width: flex 1 1 with a small basis, and |
| 227 | min-inline-size 0 to lift a flex item's automatic minimum, which |
| 228 | is its content size and is what an <input> otherwise refuses to |
| 229 | go below. The 11rem cap keeps the desktop header exactly as it |
| 230 | was. */ |
| 231 | [docs-search-form] { |
| 232 | display: flex; |
| 233 | flex-wrap: nowrap; |
| 234 | align-items: center; |
| 235 | gap: var(--rst-sp-2); |
| 236 | max-inline-size: 100%; |
| 237 | } |
| 238 | [docs-search-form] [rst-input] { |
| 239 | flex: 1 1 6rem; |
| 240 | min-inline-size: 0; |
| 241 | max-inline-size: 11rem; |
| 242 | } |
| 243 | /* The submit button is for a pointer and for a screen reader; a |
| 244 | keyboard user submits with Enter from the field, which a |
| 245 | single-input form does natively. It stays visible rather than |
| 246 | becoming an icon because "Search documents" is also the field's |
| 247 | accessible name and the two agreeing is worth more than the |
| 248 | width. |
| 249 | flex: 0 0 auto for the same reason: the whole shortfall comes out |
| 250 | of the field, so the button keeps its whole label. It is the |
| 251 | words that are load-bearing here, not the box. |
| 252 | The locale caveat, stated as the sequence it actually is rather |
| 253 | than as a single threshold. Because the field gives way first and |
| 254 | the button never shrinks, a wider label eats the FIELD long |
| 255 | before anything overflows: a ~200px label leaves about 80px of |
| 256 | field, ~270px leaves a sliver, and only past ~280px — the 288px |
| 257 | column less the gap — does anything cross the edge. So the first |
| 258 | failure a wide catalog produces is a search box too narrow to |
| 259 | read what you typed, which is a usability problem and not a |
| 260 | 1.4.10 one; the overflow comes later. English is 131px and is the |
| 261 | only catalog today. The fix when it arrives is a wrap allowance |
| 262 | keyed to the label, not a smaller field. |
| 263 | ([docs-create-form]'s own comment above states the same caveat as |
| 264 | a bare "wider than about 270px overflows", which is the second |
| 265 | half of this sequence and not the part that bites first.) */ |
| 266 | [docs-search-form] button { |
| 267 | white-space: nowrap; |
| 268 | flex: 0 0 auto; |
| 269 | } |
| 270 | /* The way back out of a result page, under the results rather than |
| 271 | in the header: it is about the list being shown, not about the |
| 272 | controls that produced it. */ |
| 273 | [docs-search-clear] { |
| 274 | margin-block-start: var(--rst-sp-3); |
| 275 | font-size: var(--rst-fs-sm); |
| 276 | } |
| 277 | </style> |
| 278 | <header rst-page-header> |
| 279 | <div rst-page-header-titles> |
| 280 | <h1>{{.Title}}</h1> |
| 281 | </div> |
| 282 | <div docs-header-actions> |
| 283 | {{/* Search (spec §15.2's title search). A GET form, so a search |
| 284 | is a link — bookmarkable, reloadable, and reachable with the |
| 285 | back button — and so the whole feature works with no |
| 286 | JavaScript at all. fixtures/jump-to.mjs adds a keyboard |
| 287 | shortcut on top of this; it does not replace it. |
| 288 | |
| 289 | type="search" rather than type="text": it gets the platform's |
| 290 | own clear control and its own keyboard on a phone. The value |
| 291 | is echoed back so a result page can be edited and |
| 292 | re-submitted rather than retyped. */}} |
| 293 | <form docs-search-form method="get" action="/" role="search"> |
| 294 | <input rst-input docs-search-input type="search" name="q" value="{{.Query}}" |
| 295 | placeholder="{{.SearchPlace}}" aria-label="{{.SearchLabel}}" autocomplete="off"> |
| 296 | <button rst-btn="ghost" type="submit">{{.SearchLabel}}</button> |
| 297 | </form> |
| 298 | {{if .CanCreate}} <form docs-create-form method="post" action="/documents"> |
| 299 | <input rst-input type="text" name="title" placeholder="{{.CreatePlaceholder}}" aria-label="{{.CreateLabel}}" autocomplete="off"> |
| 300 | <button rst-btn="primary" type="submit">{{.CreateLabel}}</button> |
| 301 | </form>{{end}} |
| 302 | </div> |
| 303 | </header> |
| 304 | {{if .PartialCatalog}}{{template "callout" (dict "Body" .PartialCatalog "Tone" "warning")}}{{end}} |
| 305 | {{/* Searching renders a ranked, UNGROUPED result list. Recency |
| 306 | grouping is what an unfiltered list is for; grouping ranked |
| 307 | results under "Today" and "Earlier" would order the same rows |
| 308 | twice by two different rules and show only the second. */}} |
| 309 | {{if .Searching}} |
| 310 | {{if .Results}} |
| 311 | <div rst-list> |
| 312 | {{range .Results}} |
| 313 | <div rst-row> |
| 314 | {{template "thumb" .Thumb}} |
| 315 | <span rst-row-main><a href="{{.Href}}">{{.Title}}</a></span> |
| 316 | <span docs-row-when>{{.When}}</span>{{if .SharedNote}}<span rst-muted docs-row-shared>{{.SharedNote}}</span>{{end}} |
| 317 | </div> |
| 318 | {{end}} |
| 319 | </div> |
| 320 | {{else}} |
| 321 | <div rst-empty> |
| 322 | <h2 rst-empty-title>{{.NoResultsHead}}</h2> |
| 323 | <p rst-empty-body>{{.NoResultsBody}}</p> |
| 324 | </div> |
| 325 | {{end}} |
| 326 | <p docs-search-clear><a href="/">{{.ClearLabel}}</a></p> |
| 327 | {{else if .Groups}} |
| 328 | <div rst-list> |
| 329 | {{range .Groups}} |
| 330 | <h2 docs-group-label>{{.Label}}</h2> |
| 331 | {{range .Rows}} |
| 332 | <div rst-row> |
| 333 | {{template "thumb" .Thumb}} |
| 334 | <span rst-row-main><a href="{{.Href}}">{{.Title}}</a></span> |
| 335 | <span docs-row-when>{{.When}}</span>{{if .SharedNote}}<span rst-muted docs-row-shared>{{.SharedNote}}</span>{{end}} |
| 336 | </div> |
| 337 | {{end}} |
| 338 | {{end}} |
| 339 | </div> |
| 340 | {{else}} |
| 341 | <div rst-empty> |
| 342 | <h2 rst-empty-title>{{.EmptyTitle}}</h2> |
| 343 | <p rst-empty-body>{{.EmptyBody}}</p> |
| 344 | </div> |
| 345 | {{end}} |
| 346 | {{template "docs-jumpto" .}} |
| 347 | {{end}} |
| 348 | |