oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1{{define "content"}}
2{{/* Adding content, on its own page (2026-09-29, Paul: "remove the Add
3 forms from the index … that should be a separate Add content form").
4 The form, the drop zone, the upload tray and the outcome message moved
5 here unchanged; POST /documents still receives them, and now redirects
6 back HERE with its outcome rather than to the index. The index keeps
7 search and the document list, and links here. */}}
8<nav rst-breadcrumb><a href="/">← Back to search</a></nav>
9<section>
10 <h1>Add content</h1>
11 <form method="post" action="/documents" enctype="multipart/form-data" rst-stack>
12 <label for="title">Title <span rst-muted rst-hint>{{.Drop.TitleHint}}</span></label>
13 <input type="text" id="title" name="title">
14 <label for="body">Paste text</label>
15 <textarea id="body" name="body" rows="8" placeholder="Paste a document, notes, minutes…"></textarea>
16 <label for="file">{{.Drop.FileLabel}}</label>
17 {{/* The drop zone WRAPS the file input, it does not replace it. With
18 no script the div is an unstyled box, the hint and the note stay
19 hidden, and this is the same form it was: choose files, press
20 Store it. static/drop.js adds rst-dropzone="ready", which is what
21 the styling keys off — so a page whose script did not load never
22 draws a drop target that cannot take a drop.
23
24 What a DROP does has changed: it no longer fills this input and
25 wait for Store it. Each dropped file is posted on its own, in
26 the background, to this form's own action, and the tray below
27 the form says how each one went. Choosing files here and
28 pressing Store it is untouched and still posts the whole form.
29
30 The data attributes are the handler's own limits and the
31 handler's own words (see dropZone in handlers.go). The browser
32 reads them from here rather than carrying a second copy, so it
33 cannot come to refuse what the server would accept.
34
35 The input is `multiple`, and that is a plain HTML affordance
36 with no script behind it: createDocument iterates every part
37 named "file" and stores one document each, so a scriptless
38 chooser that hands it five files gets five documents. It was
39 deliberately single-file while the handler read only the first
40 part — four of five would have vanished without a word. */}}
41 {{with .Drop}}
42 <div rst-dropzone
43 data-max-bytes="{{.MaxBytes}}"
44 data-max-total-bytes="{{.MaxTotalBytes}}"
45 data-max-files="{{.MaxFiles}}"
46 data-sniff-bytes="{{.SniffBytes}}"
47 data-max-in-flight="{{.MaxInFlight}}"
48 data-refresh-header="{{.RefreshHeader}}"
49 data-msg-toomany="{{.TooManyFiles}}"
50 data-msg-toototal="{{.TooMuchAtOnce}}"
51 data-msg-folder="{{.Folder}}"
52 data-msg-blocked="{{.Blocked}}"
53 data-msg-waiting="{{.Waiting}}"
54 data-msg-uploading="{{.Uploading}}"
55 data-msg-storing="{{.Storing}}"
56 data-msg-row-stored="{{.RowStored}}"
57 data-msg-stored-one="{{.StoredOne}}"
58 data-msg-stored-many="{{.StoredMany}}"
59 data-msg-none-stored="{{.NoneStored}}"
60 data-msg-fresh="{{.Fresh}}"
61 data-msg-refused-one="{{.RefusedOne}}"
62 data-msg-refused-many="{{.RefusedMany}}"
63 data-msg-unsure-one="{{.UnsureOne}}"
64 data-msg-unsure-many="{{.UnsureMany}}"
65 data-reason-toobig="{{.ReasonTooBig}}"
66 data-reason-binary="{{.ReasonBinary}}"
67 data-reason-empty="{{.ReasonEmpty}}"
68 data-reason-unreadable="{{.ReasonUnreadable}}"
69 data-reason-noanswer="{{.ReasonNoAnswer}}">
70 <input type="file" id="file" name="file" multiple accept=".txt,.md,.markdown,.csv,text/plain,text/markdown">
71 {{/* Muted by memoria.css on [rst-dropzone-hint]. (Not the design
72 system's muted-cell utility: that is a class, and spelled as an
73 attribute it styled nothing at all — nineteen places looked
74 muted in the markup and read as body text on the screen. They
75 are rst-muted now, which memoria.css styles.)
76
77 Both paragraphs are `hidden` so a scriptless page promises no
78 drop target. drop.js unhides the note while it is still EMPTY,
79 before it ever writes into it: a live region that enters the
80 accessibility tree in the same tick as its first message is
81 commonly not announced at all, and a refusal nobody hears is
82 the one outcome this feature must not have. */}}
83 <p rst-dropzone-hint hidden>{{.Hint}}</p>
84 <p rst-dropzone-note role="status" aria-live="polite" hidden></p>
85 </div>
86 {{end}}
87 <div rst-form-bar><button rst-btn="primary">Store it</button></div>
88 </form>
89 {{if .Flash}}<div rst-callout rst-tone="{{.FlashKind | tone}}"{{if eq .FlashKind "danger"}} role="alert"{{end}}><div rst-callout-body><p>{{.Flash}}</p></div></div>{{end}}
90
91 {{/* The upload tray. It lives OUTSIDE the form on purpose: it is not
92 something the form submits, and a scriptless browser must find the
93 form exactly as it was. Scriptless it is `hidden` and empty, so it
94 is not on the page at all.
95
96 drop.js unhides it — still empty — at its ready step, for the same
97 reason it unhides the note there: [rst-tray-status] is the live
98 region that announces what a drop did, and a live region that
99 enters the accessibility tree in the same tick as its first
100 message is commonly not announced at all. Only [rst-tray-status]
101 is live. The rows are not, and must not be: five files uploading
102 is five rows changing several times a second, and a live region
103 over that is a screen reader nobody can use.
104
105 Every word here comes from dropZone in handlers.go, like the data
106 attributes above — the tray and the flash sentence a scriptless
107 browser sees are the same strings, so they cannot drift.
108
109 The document list is on the index, not here, so once anything is
110 stored the tray points there with a real link. Nothing follows it
111 on its own: someone mid-drop does not want the ground moving under
112 them. When a drop settles drop.js re-fetches this page and swaps
113 its one rst-refresh region, the passage count in the footer. */}}
114 {{with .Drop}}
115 <div rst-tray hidden role="region" aria-labelledby="tray-title">
116 <div rst-tray-head>
117 <span rst-tray-title id="tray-title">{{.TrayTitle}}</span>
118 <button type="button" rst-btn="quiet compact" rst-tray-dismiss>{{.TrayDismiss}}</button>
119 </div>
120 <ul rst-tray-list></ul>
121 <p rst-tray-status role="status" aria-live="polite"></p>
122 <p rst-tray-listed hidden>{{.Listed}} <a href="/">{{.ViewDocs}}</a></p>
123 </div>
124 {{end}}
125</section>
126{{end}}
127