vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Remove the retired UI plan document
7defd797e7f48d340b6986f925be59f853733714 · Keenan Nemetz · 10d ago · parent 258ed801
Remove the retired UI plan document The UI plan was working notes for the web surface, and the rules that still matter now live in AGENTS.md, which is where a contributor already reads the copy guidance. Drop the file, and the package comment that sent readers to it.
docs/ui-plan.md −417
| @@ -1,417 +0,0 @@ | |||
| 1 | # UI plan | ||
| 2 | |||
| 3 | Working notes for Vole's web surface. Decisions here are the brief; change them | ||
| 4 | deliberately rather than by drift. | ||
| 5 | |||
| 6 | ## Direction | ||
| 7 | |||
| 8 | - **Compact, not sparse.** Take Linear's information density and restraint. | ||
| 9 | Avoid GitHub's whitespace-heavy chrome; avoid Sourcehut's bareness. A row of | ||
| 10 | data should be readable at a glance, and a page should fit on one screen when | ||
| 11 | it can. | ||
| 12 | - **Everforest is the theme.** Every colour comes from a custom property on | ||
| 13 | `:root`; `static/everforest.css` supplies six variants over the same layout: | ||
| 14 | dark and light, each in hard, medium, and soft contrast. The picker lives on | ||
| 15 | the Account page, and `static/theme.js` applies the saved variant before the | ||
| 16 | first paint. Its `System` default follows the operating system's dark or | ||
| 17 | light preference and uses the medium contrast; the base sheet's tokens remain | ||
| 18 | the fallback. | ||
| 19 | - **Readability first.** Lexend is the UI face and JetBrains Mono is the code | ||
| 20 | face; both are self-hosted WOFF2 under `static/fonts/` under the SIL Open Font | ||
| 21 | License 1.1, with the system stacks as fallback. The medium (100%) base is | ||
| 22 | 15.4px with a 1.55 line height and subpixel-friendly smoothing. Every explicit | ||
| 23 | font size is multiplied by `--font-scale`; the Account page's Text size presets | ||
| 24 | are 90%, 100%, and 110%, stored in this browser and applied before first paint. | ||
| 25 | Medium is the default and matches the former Large size. | ||
| 26 | - **Server rendered.** Go `html/template` produces the page. HTMX is the default | ||
| 27 | dynamic layer, vendored into the binary; JavaScript is required — passkeys, | ||
| 28 | clipboard copy, and rich widgets such as the label/assignee token input all | ||
| 29 | need it. No SPA, no client-side router, no npm build. | ||
| 30 | - **JavaScript required.** There is no no-script fallback: a browser without | ||
| 31 | JavaScript gets a page it cannot operate. Forms post through HTMX, and handlers | ||
| 32 | answer a fragment when `HX-Request: true` and a redirect otherwise. | ||
| 33 | - **Identity later.** Keep the `vole` wordmark as a placeholder and keep colour, | ||
| 34 | type, radius, and spacing in tokens so a real identity is a token swap. | ||
| 35 | - **Single tenant.** No organisation/workspace switcher. The global bar is | ||
| 36 | limited to Home, Activity, search, and the account menu; the repository owns | ||
| 37 | its own horizontal tabs. | ||
| 38 | |||
| 39 | ## Navigation model | ||
| 40 | |||
| 41 | Global bar (slim): `vole` · `Home` · `Activity` · search · account menu. The | ||
| 42 | wordmark links home too, but the text destination keeps the mark from being the | ||
| 43 | only label. The account menu carries Account, Status for instance admins, and | ||
| 44 | Sign out; the current destination is marked. Search is a field on wide screens | ||
| 45 | and takes its own row on narrow ones. | ||
| 46 | |||
| 47 | Repository header: the repository name and its short description sit above the | ||
| 48 | tabs at the home-page header scale, with a lock for a private repository or a | ||
| 49 | globe for a public one. Repository-level actions sit at the right of that name | ||
| 50 | line: Clone always, and Site only when the repository has a deployed site. The | ||
| 51 | tab's own header is a smaller section title because the tabs, not the page, are | ||
| 52 | the navigation. The tabs are: | ||
| 53 | |||
| 54 | `Overview` · `Activity` · `Code` · `Issues` · `Changes` · `Releases` · `Runs` · `Locks` · `Settings` | ||
| 55 | |||
| 56 | No sidebar. A sidebar only pays for its width when there are many persistent | ||
| 57 | destinations; here the repository has several and the global bar is separate, | ||
| 58 | and they fit on one row at desktop widths. On narrow screens the tab row scrolls | ||
| 59 | horizontally rather than collapsing into a menu. | ||
| 60 | |||
| 61 | Every tab opens with the same page header: a title and one-line summary on the | ||
| 62 | left, and that tab's actions (or branch picker) grouped on the right. Filters and | ||
| 63 | list controls stay below the header; the header is the only place a tab's primary | ||
| 64 | action lives. | ||
| 65 | |||
| 66 | The repository tab opens Overview: README in the main column; recent commits, open | ||
| 67 | issues and change requests, recent runs, releases, contributors, and languages in the | ||
| 68 | sidebar. Activity is the chronological feed: issues, change requests, comments, and | ||
| 69 | releases with type filters. Code is an editor-style browser: a persistent tree on the left and the | ||
| 70 | open file in the large right-hand pane; file and folder links swap only that pane | ||
| 71 | through HTMX. Markdown opens as source; a Source/Preview toggle renders it at full | ||
| 72 | pane width. The branch crumb returns to the Code overview rather than opening a | ||
| 73 | second directory listing. Settings appears for the viewer when the repository | ||
| 74 | ACL gives them the admin role. | ||
| 75 | |||
| 76 | A blob is served as escaped lines with their numbers and `id="L<n>"` anchors, and | ||
| 77 | the browser colours it: `static/prism-worker.js` holds Prism and every language it | ||
| 78 | ships, so the server names the language (an extension table, not a detector) and | ||
| 79 | does no lexing. The worker starts only on a page with a blob; a file the table | ||
| 80 | does not claim is served plain. | ||
| 81 | |||
| 82 | ## Surfaces | ||
| 83 | |||
| 84 | Status: `done` exists today · `wip` in progress · `todo` missing. | ||
| 85 | |||
| 86 | ### Global | ||
| 87 | |||
| 88 | - done — repository list (`/`) | ||
| 89 | - done — sign-in, registration, enrollment link | ||
| 90 | - done — account (name, theme, text size, keys, passkeys, sessions) | ||
| 91 | - done — styled 404 page everywhere, including missing detail resources and | ||
| 92 | download paths | ||
| 93 | - done — styled 500 page everywhere; a handler panic before the first byte is | ||
| 94 | recovered into it, and the detail is logged rather than rendered | ||
| 95 | - done — search across visible repositories, by name and description, from the | ||
| 96 | chrome on every page | ||
| 97 | - done — landing page at `/`: instance identity, repository cards with top | ||
| 98 | language, last commit, issue/change-request counts, and latest release, with a short | ||
| 99 | activity rail beside them | ||
| 100 | - done — instance-wide activity feed at `/activity`, merged from every | ||
| 101 | repository the viewer can read, with the repository named on each event, a | ||
| 102 | type filter, and 50-event pages | ||
| 103 | - done — instance admin: repository creation and access rules, accounts, | ||
| 104 | enrollment links, admin grants, keys, and sessions | ||
| 105 | - done — instance-admin status page at `/status` with storage and filesystem | ||
| 106 | usage, LFS/artifact breakdowns, page-view and unique-user counts, runner | ||
| 107 | presence and queue depth, and an effective configuration summary. The page is | ||
| 108 | split into Overview, Storage, Runners, and Configuration tabs; `/healthz` stays | ||
| 109 | the plain-text probe and old `/runners` links redirect to the Runners tab. | ||
| 110 | |||
| 111 | ### Repository | ||
| 112 | |||
| 113 | - done — code browser: persistent file tree, HTMX file/folder swaps, blob with | ||
| 114 | line numbers, raw, markdown source with an explicit full-width preview | ||
| 115 | - done — a README's relative links and images resolve against the document's own | ||
| 116 | directory and open on the code surface, so the same README works from the | ||
| 117 | overview and from the code browser | ||
| 118 | - done — commit history, commit view, parent links, and a diff per changed file | ||
| 119 | in the same file-tree-and-pane layout as a change's Files tab | ||
| 120 | - done — issues list/detail, comments, filters, close/reopen, labels and assignees (create and edit) | ||
| 121 | - done — change-request list/detail with Conversation, Files and Runs tabs; one-level threaded discussion UI, approvals, file tree and diff, inline review comments, close/reopen, and linked issues in an Issues section with a search picker and unlink control | ||
| 122 | - done — releases list/detail, artifacts, archives | ||
| 123 | - done — locks | ||
| 124 | - done — run detail, log, artifacts | ||
| 125 | - done — runs list per repository (`/r/{name}/runs`) with status filters | ||
| 126 | - done — repository activity feed on `/r/{name}/activity`: issues, change requests, | ||
| 127 | comments, releases, type filters, and HTMX fragment updates | ||
| 128 | - done — activity events from metadata-log state changes (closed/reopened), | ||
| 129 | actor filter, and assignee filter | ||
| 130 | - done — repository settings: description, default branch, visibility | ||
| 131 | - done — danger zone: rename and delete with typed confirmation | ||
| 132 | - done — compare view (`/r/{name}/compare/{base}...{head}`), with three-dot | ||
| 133 | merge-base or two-dot endpoint semantics, commit list, and diff | ||
| 134 | - done — file history and blame, with links from the blob toolbar | ||
| 135 | - done — language-aware file icons in the code tree, directory list, and file | ||
| 136 | search results | ||
| 137 | - done — code search within a repository: file names and plain-text contents at | ||
| 138 | a chosen branch, with line anchors | ||
| 139 | - done — release create/edit from the browser: annotated tags with notes; | ||
| 140 | notes editable until files are attached | ||
| 141 | - done — inline review comments render beneath the diff line; the composer opens | ||
| 142 | in place and a posted comment returns to its line note. Conversation comments | ||
| 143 | carry a collapsed excerpt from the blob they anchored to | ||
| 144 | |||
| 145 | ### CI | ||
| 146 | |||
| 147 | - done — runner list, queue, run detail/log/artifacts | ||
| 148 | - done — contributor-branch CI approval on the change request, not the Runs tab or code browser | ||
| 149 | - done — workflow/job list per repository at `/r/{name}/workflows`: triggers, | ||
| 150 | jobs, runner labels, image, steps, outputs, and the latest run for each job | ||
| 151 | - done — cancel a queued run and rerun a finished one as another attempt; a | ||
| 152 | running job is not interrupted | ||
| 153 | |||
| 154 | ## Slice order | ||
| 155 | |||
| 156 | 1. ~~**Foundations**~~ — embedded template files, tokenised CSS, slim shell, | ||
| 157 | repository tabs, styled 404 and 500. | ||
| 158 | 2. **Code browsing** — done: overview/code split, stats, contributors, runs | ||
| 159 | list, editor-style tree, HTMX navigation, line-numbered blobs, commit/diff, | ||
| 160 | syntax highlighting. | ||
| 161 | 3. **Issues and change requests** — done: list/detail layout, composer, filters, | ||
| 162 | close/reopen, discussion, label/assignee triage, writing inline review | ||
| 163 | comments. | ||
| 164 | 4. **Activity and settings** — done: repository activity feed with state | ||
| 165 | change requests and actor/assignee filters, repository settings (description, default | ||
| 166 | branch, visibility, danger zone), and the instance-admin access section on the | ||
| 167 | same Settings page. | ||
| 168 | 5. **Instance admin** — done: repository create at `/repos/new`, access rules on | ||
| 169 | each repository's Settings tab, rename/delete for repository admins, and | ||
| 170 | `/users` for accounts, enrollment links, admin grants, retirement, keys, and | ||
| 171 | sessions. | ||
| 172 | 6. **CI** — done: workflow/job list, cancel a queued run, rerun a finished | ||
| 173 | one as another attempt. | ||
| 174 | 7. **Polish** — done: repository search, code search, issue/change-request pagination, | ||
| 175 | empty states, a light token block, skip link, focus-visible outlines, and | ||
| 176 | narrow-screen table handling. Remaining: a deliberate responsive and | ||
| 177 | accessibility audit at 1440/1024/390 px with a real browser. | ||
| 178 | |||
| 179 | ## Dynamic layer | ||
| 180 | |||
| 181 | HTMX 2.0.11 is pinned at `server/web/static/htmx.min.js` (Zero-Clause BSD, | ||
| 182 | license beside it). It is served from the embedded static assets and loaded on | ||
| 183 | every page. Interactions are converted one flow at a time: | ||
| 184 | |||
| 185 | - done — issue close/reopen, labels, assignees, and comments update their | ||
| 186 | section in place. Labels and assignees save on token change; there is no | ||
| 187 | visible save button. | ||
| 188 | - done — change approval updates the live status regions and the page actions, | ||
| 189 | comments update the discussion fragment, and close/reopen are page actions. | ||
| 190 | - done — the change page polls one live endpoint that returns out-of-band | ||
| 191 | updates for the status regions; the target picker, issue search, and | ||
| 192 | discussion are outside that swap. | ||
| 193 | - done — the change Issues section links and unlinks issues in place. | ||
| 194 | - done — Issues and Change requests filters swap their list fragment and push the URL. | ||
| 195 | - done — repository activity feed swaps its event fragment for type filters and | ||
| 196 | pushes the URL. | ||
| 197 | - done — diff lines open an anchored composer in place; posted anchors are | ||
| 198 | stored with the reviewed blob and returned to beneath the line. | ||
| 199 | - done — the Files tab swaps only the selected file's diff pane from its tree, | ||
| 200 | and pushes the file URL. | ||
| 201 | - done — the Runs tab lists the head commit's workflow results and links to each | ||
| 202 | run. | ||
| 203 | |||
| 204 | The token input and passkey ceremonies stay custom JavaScript: HTMX moves HTML | ||
| 205 | from the server, it does not manage client-side chip state or credential APIs. | ||
| 206 | |||
| 207 | ### Dynamic regions | ||
| 208 | |||
| 209 | Every dynamic area is one of two kinds. A **state region** holds a form, input, | ||
| 210 | open popover, or editor and is replaced only by the action that owns it. A | ||
| 211 | **live region** is read-only status or data and may be refreshed in the | ||
| 212 | background. A poll response may contain live regions only; if a widget needs | ||
| 213 | both, its data child is a separate live region and its controls are left alone. | ||
| 214 | |||
| 215 | The change sidebar follows this split. `GET /r/{name}/requests/{id}/live` | ||
| 216 | returns `request-review-status`, `request-changes`, `request-approvals`, | ||
| 217 | `request-checks`, `request-combined`, and `request-merge-note` with | ||
| 218 | `hx-swap-oob="outerHTML"`. The poll element itself uses `hx-swap="none"`, so the | ||
| 219 | target picker, issue search, and comment composers keep their state while the | ||
| 220 | status updates. Write responses use the same out-of-band group when they change | ||
| 221 | more than one live region, or target one state region directly when the write | ||
| 222 | belongs to that widget. The page action group is updated by writes, not by the | ||
| 223 | poll, so a poll never replaces a form. The live handler runs the status loader | ||
| 224 | only; linked issues, numbering, and branch-picker options are loaded by the full | ||
| 225 | sidebar and the writes that own those state regions. Trigger conditions are for | ||
| 226 | visibility and throttling, not for pausing a poll because a widget happens to be | ||
| 227 | open. | ||
| 228 | |||
| 229 | ## Activity feed | ||
| 230 | |||
| 231 | No notification table and no per-account inbox. The feed is derived from Git, the | ||
| 232 | metadata branch, tags, and the run queue. Events carry a type, time, actor, | ||
| 233 | title, link, and (for issue/change-request events) assignees, so the same feed can be | ||
| 234 | filtered by actor and assignee. | ||
| 235 | |||
| 236 | Repository feed is done on `/r/{name}/activity`; Overview stays the repository | ||
| 237 | summary. The instance feed is `/activity`, over visible repositories, with the | ||
| 238 | repository named on each event, a type filter, and 50-event pages. The landing | ||
| 239 | page shows the ten most recent events as a rail beside the repository grid, | ||
| 240 | because a forge's front door should show its repositories first. State changes such as closed and reopened | ||
| 241 | come from the metadata branch log, which carries a canonical | ||
| 242 | `issue: <id> <state>` and `request: <id> <state>` subject. The feed covers | ||
| 243 | issues, change requests, comments, approvals, state changes, and releases. Commits and | ||
| 244 | CI runs are not events: the overview and the runs tab already show them. | ||
| 245 | |||
| 246 | ## Admin scope | ||
| 247 | |||
| 248 | Instance admin covers the operations that used to be CLI-only: | ||
| 249 | |||
| 250 | - accounts: create with an enrollment link, retire, grant/revoke admin; | ||
| 251 | inspect/revoke keys and sessions | ||
| 252 | - repositories: create, rename, delete | ||
| 253 | - per-repository ACL: set roles, default role, approvals, checks, runners | ||
| 254 | |||
| 255 | The Accounts page will not ask an admin to paste an account's SSH key. Account | ||
| 256 | creation uses the existing `identity.CreateWithInvite` path and returns a | ||
| 257 | one-time enrollment link; browser sign-in must be enabled for that link to work. | ||
| 258 | |||
| 259 | Repository admins still own repository settings (description, default branch, | ||
| 260 | visibility, danger zone). Instance admins see an additional access section on the | ||
| 261 | same Settings page: roles, the default role, approvals, checks, and runners. | ||
| 262 | Repository creation stays an instance-admin operation, but it is a standalone | ||
| 263 | page reached from the home page rather than a repository-list admin screen. | ||
| 264 | |||
| 265 | Accounts live at `/users`, reached from an Accounts item in the top navigation | ||
| 266 | that is shown only to instance admins. Every admin write uses the existing stores, | ||
| 267 | CSRF, and the audit log; the web is a second surface over the same operations, | ||
| 268 | not a second implementation. | ||
| 269 | |||
| 270 | ## Where the code lives | ||
| 271 | |||
| 272 | `server/web` is the whole HTML surface. It is one package, split by what a file | ||
| 273 | answers rather than by layer: | ||
| 274 | |||
| 275 | | File | Holds | | ||
| 276 | |---|---| | ||
| 277 | | `web.go` | the `Web` type, `New`, `render`, `open`, the error pages, `htmxRequest` | | ||
| 278 | | `page.go` | `Page`, `pageSection`, `who`, `nav` — the data every page and fragment starts from | | ||
| 279 | | `templatefuncs.go` | the template function map, with a note on escaping (`esc` vs `pathurl`) | | ||
| 280 | | `ui.go` | relative time, avatar initials and tone, breadcrumbs, `safePath`, the `pager` | | ||
| 281 | | `markdown.go` | the goldmark + bluemonday boundary, `#42` / `#<ulid>` cross-references, and resolving a tree document's relative links against its own directory | | ||
| 282 | | `auth.go`, `auth_handlers.go` | passkeys, sessions, CSRF, the middleware, the sign-in pages | | ||
| 283 | | `templates.go`, `icons.go`, `file_icons.go` | embedding, the icon set, language classes | | ||
| 284 | | `templates/` | one file per destination; `chrome.html`, `components.html`, `diff.html` and `role-picker.html` hold what two surfaces share | | ||
| 285 | | `home.go`, `activity.go` | the landing page and the two activity feeds | | ||
| 286 | | `overview.go`, `codebrowser.go`, `code_tree.go`, `code_search.go`, `commit.go`, `compare.go`, `diff.go`, `file_history.go`, `highlight.go` | one code-side read surface each | | ||
| 287 | | `issues.go`, `requests.go`, `request_view.go` | the issue pages, the change-request list and new form, the change conversation page | | ||
| 288 | | `request_files.go` | a change's Files tab and its inline review comments | | ||
| 289 | | `releases.go`, `runs.go`, `workflows.go`, `locks.go` | release, run, workflow and lock pages | | ||
| 290 | | `comments.go` | the comment view, the thread builder, the action menus, the snippet | | ||
| 291 | | `comment_writes.go` | add, edit and delete a comment, for both surfaces | | ||
| 292 | | `tokens.go`, `rolepicker.go` | the label/assignee token input and the instance/repository role control | | ||
| 293 | | `write_context.go`, `capabilities.go`, `audit.go` | the write gate, the ACL rules a page and a handler share, the audit log | | ||
| 294 | | `writes.go`, `release_actions.go`, `run_actions.go`, `ci.go`, `account.go` | the browser's writes, by what they change | | ||
| 295 | | `settings.go`, `admin_access.go` | repository settings, and the instance-admin access rules on the same page | | ||
| 296 | | `admin.go`, `admin_repos.go`, `admin_users.go`, `status.go`, `runners.go` | the instance-admin gate, accounts, repository creation, status, runners | | ||
| 297 | |||
| 298 | Read handler files answer a page or an HTMX fragment; `*_actions` and `writes.go` | ||
| 299 | answer a form POST and redirect. A handler resolves the repository through | ||
| 300 | `w.open`, which is also what makes a private repository indistinguishable from a | ||
| 301 | missing one, and builds its data with `w.page` so the chrome always agrees with | ||
| 302 | the ACL. | ||
| 303 | |||
| 304 | Every write starts at `beginWrite`, which establishes the session, the body bound, | ||
| 305 | the CSRF token, the repository and its rules, and parses the form in that order — | ||
| 306 | `beginPost` and `beginAdminPost` are the account-scoped and instance-admin | ||
| 307 | equivalents. The handler then asks `capabilities.go` for the one rule it needs | ||
| 308 | (`canDiscuss`, `canOpenChangeRequest`, `canTriage`, `canApprove`, `canMaintain`, | ||
| 309 | `canModerateComment`), which is the same function the page used to decide whether | ||
| 310 | to draw the control. A rejected write answers through `w.refuse`, a 403 that never | ||
| 311 | re-renders; a write that fails for another reason answers through `w.fail`, which | ||
| 312 | re-renders the section with the message for HTMX and returns the message as text | ||
| 313 | otherwise. | ||
| 314 | |||
| 315 | A comment is the same object on an issue and on a change request: | ||
| 316 | `metadata.Comment` is one type for both, so `comments.go` has one view, one thread | ||
| 317 | builder and one set of action rules, and `comment_writes.go` has one | ||
| 318 | implementation each of add, edit and delete. A `commentTopic` names what the two | ||
| 319 | surfaces do differently — which writer runs, which section re-renders, where a | ||
| 320 | plain form post lands — and a change request adds the one thing an issue cannot | ||
| 321 | have: a comment anchored to a line of the diff, with a snippet of the lines it was | ||
| 322 | written about. | ||
| 323 | |||
| 324 | Templates live under `server/web/templates/`, one file per destination, with a | ||
| 325 | page's fragments in the page's own file so a fragment and the element it swaps | ||
| 326 | are read together. Four files hold what more than one surface uses: `chrome.html` | ||
| 327 | (shell, global bar, repository header, error page), `components.html` (token | ||
| 328 | input, commit list, file tree, pager), `diff.html`, and `role-picker.html`. All | ||
| 329 | templates share one namespace, so a fragment name is prefixed with its surface | ||
| 330 | (`issue_labels`, `code_main`, `review_main`, `commit_main`); a fragment both | ||
| 331 | surfaces render keeps the shared name (`comment_thread`, `comment_card`). | ||
| 332 | |||
| 333 | The file tree is one widget for all three browsers. The code browser, a change's | ||
| 334 | Files tab and a commit page pass the same tree nodes through `treeContext` or | ||
| 335 | `commitTreeContext`, and `treeView.Href` decides whether a row opens a blob at a | ||
| 336 | ref, the file's diff in a change, or the file's diff in a commit. A commit is the | ||
| 337 | same shape as the Files tab because it answers the same question one file at a | ||
| 338 | time: the tree is built from `git show --name-status`, a single row's patch is | ||
| 339 | read on demand, and only the pane is swapped when a row is opened. The pager is | ||
| 340 | likewise one control for issues, change requests, and the instance feed: the | ||
| 341 | handler supplies the URLs, the label, and (for HTMX) the fragment to swap. | ||
| 342 | |||
| 343 | Static assets are served from `/static/` and embedded in the binary at build | ||
| 344 | time; nothing is loaded from a CDN and no template inlines CSS or JS. The passkey | ||
| 345 | script is an ordinary asset (`static/auth.js`) and runs under the same CSP as the | ||
| 346 | rest: `script-src 'self'`. | ||
| 347 | |||
| 348 | One asset is fetched on demand. Landing a change request redirects to the request | ||
| 349 | page with `?merged=1`, and `static/app.js` answers that parameter by loading | ||
| 350 | `static/confetti.browser.js` (canvas-confetti 1.9.3, ISC, license at | ||
| 351 | `server/web/static/canvas-confetti.LICENSE`) and firing a one-second version of | ||
| 352 | the library's "fireworks" volley from the left and right edges of the viewport. | ||
| 353 | Nothing bursts from the centre, so the celebration frames the page it lands on | ||
| 354 | instead of covering it. The parameter is | ||
| 355 | read once and removed from the address with `replaceState`, so a reload, a bookmark | ||
| 356 | or a shared link is an ordinary visit; the library honours | ||
| 357 | `prefers-reduced-motion` through `disableForReducedMotion`. Every other page pays | ||
| 358 | nothing for it, which is why the 25 KB bundle is acceptable where a | ||
| 359 | loaded-always library would not be. | ||
| 360 | |||
| 361 | The burst always comes from an instance created with `useWorker: false`. The | ||
| 362 | library's default export opts into a Web Worker built from a `blob:` URL, which | ||
| 363 | `default-src 'self'` blocks — and it transfers its canvas to that worker before | ||
| 364 | drawing, so the canvas appears and stays empty with no error. Drawing on the main | ||
| 365 | thread avoids the CSP question entirely instead of widening the policy with | ||
| 366 | `worker-src blob:` for a celebration. | ||
| 367 | |||
| 368 | The route table is `server/routes.go`, one function per surface (assets, global, | ||
| 369 | auth, admin, repository, git). Routes are grouped by prefix rather than by | ||
| 370 | registration order because `ServeMux` ranks by pattern specificity, and the | ||
| 371 | catch-all 404 is registered last only so a reader finds it there. | ||
| 372 | |||
| 373 | ## Icons | ||
| 374 | |||
| 375 | Icons are Lucide 0.468.0 SVGs (ISC, license at `server/web/static/lucide.LICENSE`), | ||
| 376 | stored as path data in `server/web/icons.go` and rendered inline with | ||
| 377 | `{{icon "name"}}`. No icon font, no CDN, no network request; they inherit | ||
| 378 | `currentColor` and size with the surrounding text. A test checks that every icon | ||
| 379 | named in a template exists. | ||
| 380 | |||
| 381 | The header emblem is `static/vole-head.png` used as a CSS mask and tinted with the | ||
| 382 | theme's ink, so it follows the selected Everforest variant. The favicon is a | ||
| 383 | separate transparent mark: `favicon.svg` and the light/dark PNG fallbacks switch | ||
| 384 | between black and white from `prefers-color-scheme` (the browser window's mode), | ||
| 385 | not from the in-app theme choice. That keeps the tab icon legible against the | ||
| 386 | browser's own tab colour. | ||
| 387 | |||
| 388 | ## Fonts | ||
| 389 | |||
| 390 | Lexend (UI) and JetBrains Mono (code) are self-hosted variable WOFF2 subsets under | ||
| 391 | `server/web/static/fonts/`, split into Latin and Latin-ext. Their OFL 1.1 license | ||
| 392 | texts sit beside them, and the system stacks in `--font`/`--mono` cover scripts | ||
| 393 | outside those ranges and the pre-load state. No CDN and no runtime font request | ||
| 394 | outside `/static/`. | ||
| 395 | |||
| 396 | ## Rules | ||
| 397 | |||
| 398 | - Every state-changing form carries the CSRF hidden input. | ||
| 399 | - A write handler opens with `beginWrite` (or `beginPost`/`beginAdminPost`); it | ||
| 400 | never reads the session, the CSRF token, the repo ACL, or the form by hand. | ||
| 401 | - A control and the handler that accepts it ask the same function in | ||
| 402 | `capabilities.go`; neither restates the rule. | ||
| 403 | - A private repository answers 404, never 403. | ||
| 404 | - A rejected write answers through `refuse` (403) or `fail` (the section with the | ||
| 405 | message for HTMX, plain text otherwise). A failed write is not a place for the | ||
| 406 | styled error page: that page is for a 404 or a 500, where there is no form to | ||
| 407 | return to. | ||
| 408 | - No inline CSS or JS in templates; assets live in `server/web/static/`. | ||
| 409 | - One file per destination under `templates/`, with the page's fragments beside | ||
| 410 | it; anything two surfaces share goes in `chrome.html`, `components.html`, | ||
| 411 | `diff.html` or its own component file. | ||
| 412 | - Stylesheet sections are listed in the table of contents at the top of | ||
| 413 | `vole.css`; add a line there when a section is added or moved. The base sheet | ||
| 414 | stays the authority; every Everforest rule in `everforest.css` must stay | ||
| 415 | scoped to `[data-theme^="everforest-"]`. | ||
| 416 | - Tables are for tabular data; navigation and page chrome are not tables. | ||
| 417 | - Test the page at 1440, 1024, and 390 px wide before calling it done. | ||