oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

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