Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Reorganise the web package around one implementation per rule

c717d4eb92b4257b05fa065918b0071f6302f53e · Keenan Nemetz · 13d ago · parent 4506a4d3

Reorganise the web package around one implementation per rule

server/web had grown by feature, so several rules and widgets existed twice: the
comment stack for issues and change requests, the pager, the file tree, the write
preamble, and the ACL predicates. Each pair agreed today and would drift
tomorrow. No route, stored format, or permission rule changes; the few behaviour
differences found along the way are listed at the end.

Files now say what they answer:

  web.go          the Web type, render, open, the error pages
  page.go         Page, pageSection, who, nav
  templatefuncs.go the template function map, with the escaping rules noted
  ui.go           relative time, avatars, breadcrumbs, the pager
  markdown.go     the goldmark and bluemonday boundary
  codebrowser.go  the code browser, out of web.go
  tokens.go       the label/assignee token input
  rolepicker.go   the role control, out of admin.go
  comments.go     the comment view, thread builder, action menus, snippet
  comment_writes.go  add, edit and delete a comment, for both surfaces
  write_context.go   beginWrite/beginPost/beginAdminPost, fail, refuse
  capabilities.go    the ACL rules a page and a handler share
  audit.go        the audit log
  request_view.go the change conversation page, out of requests.go
  admin_access.go the instance-admin access rules, out of settings.go

A comment is one thing, so it has one view and one thread builder: metadata.Comment
is the same type on both surfaces and only a change request can anchor one to a
diff line. Adding, editing and deleting are one implementation each, with a
commentTopic naming what differs per surface. The pager is one control for issues,
changes and the instance feed; the file tree is one widget for the code browser
and a change's Files tab.

Every write starts at beginWrite (session, body bound, CSRF, repository, rules,
form) and asks capabilities.go for the one rule it needs, so the control a page
draws and the check a handler makes cannot disagree. A rejected write answers
through refuse; a write that fails for another reason answers through fail, which
re-renders the section for HTMX and returns text otherwise.

Templates follow the same split: chrome.html, components.html, diff.html and
role-picker.html hold what more than one surface renders, and each page owns its
fragments. The route table moved to routes.go, one function per surface.

Behaviour changes found while unifying:

  - A reply cycle (two comments answering each other, possible through a pushed
    metadata branch) used to drop both comments from the page; anything the walk
    does not reach is now emitted as a root.
  - A rejected anchored comment keeps its "Commenting on path:line" note and
    hidden anchor fields, so the reviewer does not have to pick the line again.
  - A repository whose access rules cannot be read answers 500 rather than 403;
    the account was being blamed for a broken instance.
  - Pager wording is now "Showing a-b of n / page x of y" with Previous/Next
    everywhere, and the instance feed's page links keep their kind filter.
  - The activity feed, issue list and change list share one page control.

Docs: docs/ui-plan.md gains a "Where the code lives" map, the write gate, the
shared comment model, the route table, and rules for rejected writes, shared
template fragments, and the stylesheet's section order. The passkey script is now
an ordinary asset (static/auth.js) under the same CSP as the rest.

server/web/static/vole.css +73−17

@@ -1,5 +1,41 @@
1 1 /* Vole's web surface. One stylesheet, token-driven: a light theme is another
2 block of variables, and a real identity is a change to the top block only. */
2 block of variables, and a real identity is a change to the top block only.
3
4 Sections, in file order. Rules cascade in this order too, so a later section
5 can override an earlier one and a section cannot be moved without checking
6 what it was quietly overriding:
7
8 tokens custom properties, dark and light (:root)
9 chrome document shell, global bar, account menu
10 repository header name, visibility, tab row
11 panels and tables the bordered surfaces lists and pages sit on
12 controls buttons, inputs, selects, form layout
13 labels, flashes, empty pages
14 auth sign-in, registration, invite, account
15 repository home clone button, readme, the home row layout
16 diffs and markdown code/diff tables, hunks, comment cells, .md
17 shared responsive rules the few rules that apply to every surface
18 repository overview summary page: main column and sidebar
19 code tab bar and crumbs the bar above the open file
20 code browser and file tree the persistent tree beside the pane
21 syntax highlighting chroma classes
22 repository overview sidebar compact commit and run rows
23 issues and changes list pages, thread, forms, metadata sidebar
24 icons inline SVG sizing
25 activity feed event rows for one repository and the instance
26 accessibility skip link, focus rings, reduced motion
27 light theme the second token block
28 pager previous/next between list pages
29 code search per-file result blocks
30 blame per-line commit column
31 landing page hero, repository grid, feed rail
32 status page instance storage, runners, configuration
33 language colours file-icon and language-dot palette
34
35 Note the two pairs that are easy to confuse: "repository home" is the clone
36 button and readme on /r/<name>, while "landing page" is the instance home at
37 /; "repository overview" is the summary page, while "repository overview
38 sidebar" is the small commit and run rows it uses. */
3 39
4 40 :root {
5 41 color-scheme: dark;
@@ -487,7 +523,7 @@ a.label:hover { color: var(--accent); border-color: var(--accent); text-decorati
487 523 .auth { max-width: 34rem; margin-top: 8vh; }
488 524 .auth h1 { margin-bottom: 6px; }
489 525
490 /* ---------- repo home ---------- */
526 /* ---------- repository home ---------- */
491 527
492 528 details.clone { position: relative; }
493 529 details.clone > summary {
@@ -526,7 +562,7 @@ details.clone[open] > summary { border-color: var(--accent); }
526 562
527 563 .readme { margin: 16px 0; }
528 564
529 /* ---------- code, diffs, markdown ---------- */
565 /* ---------- diffs and markdown ---------- */
530 566
531 567 code, pre, kbd {
532 568 font-family: var(--mono);
@@ -702,7 +738,7 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; }
702 738 .inline-comment-form .form-actions { margin-top: 0; }
703 739 .diff .binary { padding: 10px 12px; color: var(--fg-muted); font-size: 12px; margin: 0; }
704 740
705 /* ---------- responsive ---------- */
741 /* ---------- shared responsive rules ---------- */
706 742
707 743 @media (max-width: 720px) {
708 744 h1 { font-size: 16px; }
@@ -711,7 +747,7 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; }
711 747 .nowrap { white-space: normal; }
712 748 }
713 749
714 /* ---------- overview ---------- */
750 /* ---------- repository overview ---------- */
715 751
716 752 .overview {
717 753 display: grid;
@@ -862,7 +898,7 @@ td.fit { width: 1%; white-space: nowrap; }
862 898 .overview-side section:first-child { padding-top: 12px; }
863 899 }
864 900
865 /* ---------- code browsing ---------- */
901 /* ---------- code tab bar and crumbs ---------- */
866 902
867 903 /* The tab bar and the tree header share a height so their content starts on the
868 904 same line. The bar is the file's identity, not a separate title block. */
@@ -918,7 +954,7 @@ td.fit { width: 1%; white-space: nowrap; }
918 954 border-top-right-radius: 0;
919 955 }
920 956
921 /* ---------- code browser ---------- */
957 /* ---------- code browser and file tree ---------- */
922 958
923 959 .code-browser {
924 960 display: grid;
@@ -1145,7 +1181,9 @@ details[open] > summary .tree-chevron { transform: rotate(90deg); }
1145 1181 .chroma .gi { color: var(--good); }
1146 1182 .chroma .err { color: var(--bad); text-decoration: underline; }
1147 1183
1148 /* Compact activity rows in the overview sidebar. */
1184 /* ---------- repository overview sidebar ---------- */
1185
1186 /* Compact commit and run rows beside the overview. */
1149 1187
1150 1188 .side-commit,
1151 1189 .side-run {
@@ -1751,8 +1789,10 @@ button.link, .link { min-height: 0; }
1751 1789 }
1752 1790 .label-option:hover, .label-option:focus { background: var(--bg-hover); filter: none; }
1753 1791
1754 /* Icons are inline SVG from Lucide. They inherit currentColor and size with
1755 the text around them. */
1792 /* ---------- icons ---------- */
1793
1794 /* Inline SVG from Lucide. Icons inherit currentColor and size with the text
1795 around them. */
1756 1796 .icon { width: 14px; height: 14px; flex: 0 0 auto; }
1757 1797 .tabs a { display: inline-flex; align-items: center; gap: 5px; }
1758 1798 h2 .icon { width: 13px; height: 13px; margin-right: 4px; vertical-align: -2px; color: var(--fg-faint); }
@@ -1791,7 +1831,7 @@ form.htmx-request .label-tokens { opacity: 0.6; }
1791 1831 .anchor-note { margin: 0 0 8px; font-size: 12px; }
1792 1832 @media (max-width: 720px) { .diff td.comment-cell a { opacity: 0.8; } }
1793 1833
1794 /* Repository activity feed. */
1834 /* ---------- activity feed ---------- */
1795 1835 .activity-list {
1796 1836 border: 1px solid var(--border);
1797 1837 border-radius: var(--radius-lg);
@@ -1839,6 +1879,8 @@ form.htmx-request .label-tokens { opacity: 0.6; }
1839 1879 .state.active { color: var(--good); }
1840 1880 .state.retired { color: var(--fg-faint); }
1841 1881
1882 /* ---------- accessibility ---------- */
1883
1842 1884 /* Keyboard users get out of the chrome before the tabs. */
1843 1885 .skip-link {
1844 1886 position: absolute;
@@ -1870,6 +1912,8 @@ textarea:focus-visible {
1870 1912 .panel > table { min-width: 34rem; }
1871 1913 }
1872 1914
1915 /* ---------- light theme ---------- */
1916
1873 1917 /* A light theme is the same layout with a second set of tokens. It follows the
1874 1918 operating system until a real theme switch exists. */
1875 1919 @media (prefers-color-scheme: light) {
@@ -1896,10 +1940,14 @@ textarea:focus-visible {
1896 1940 --warn-soft: rgba(148, 98, 0, 0.12);
1897 1941 }
1898 1942 }
1943 /* ---------- pager ---------- */
1944
1899 1945 .pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 12px 0; }
1900 1946 .pager .button { font-size: 12px; }
1901 1947
1902 /* Code search results: one block per file, line numbers linked to blob anchors. */
1948 /* ---------- code search ---------- */
1949
1950 /* One block per file, with line numbers linked to blob anchors. */
1903 1951 .search-code input[type="search"] { width: 10rem; }
1904 1952 .search-group { margin: 14px 0; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-panel); overflow: hidden; }
1905 1953 .search-group h3 { margin: 0; padding: 7px 10px; background: var(--bg-raised); border-bottom: 1px solid var(--border); font-size: 12px; }
@@ -1911,7 +1959,9 @@ textarea:focus-visible {
1911 1959 @media (max-width: 720px) { .search-code { display: none; } }
1912 1960 .code-main tr:target { background: var(--accent-soft); }
1913 1961
1914 /* Blame: a commit column spans the consecutive lines it last touched. */
1962 /* ---------- blame ---------- */
1963
1964 /* A commit column spans the consecutive lines it last touched. */
1915 1965 .blame-panel { overflow-x: auto; }
1916 1966 .blame { min-width: 40rem; }
1917 1967 .blame td.blame-meta {
@@ -1926,9 +1976,11 @@ textarea:focus-visible {
1926 1976 .blame td.ln { width: 6ch; text-align: right; color: var(--fg-faint); font-family: var(--mono); font-size: 11px; }
1927 1977 .blame td.code { font-family: var(--mono); font-size: 12px; white-space: pre; }
1928 1978
1929 /* Landing page: identity, a repository grid, and the merged feed as a rail. The
1930 feed has a full page at /activity; here it is a sign the forge is alive, not
1931 the subject of the page. */
1979 /* ---------- landing page ---------- */
1980
1981 /* Identity, a repository grid, and the merged feed as a rail. The feed has a
1982 full page at /activity; here it is a sign the forge is alive, not the subject
1983 of the page. */
1932 1984 .hero {
1933 1985 display: flex;
1934 1986 align-items: flex-end;
@@ -2057,7 +2109,9 @@ textarea:focus-visible {
2057 2109 .repo-grid { grid-template-columns: 1fr; }
2058 2110 }
2059 2111
2060 /* Status page: no rule under the header itself; the tab row keeps its rule. */
2112 /* ---------- status page ---------- */
2113
2114 /* No rule under the header itself; the tab row keeps its rule. */
2061 2115 .status-head { border-bottom: 0; margin-bottom: 8px; }
2062 2116 .status-tabs { margin-bottom: 14px; border-bottom: 1px solid var(--border); }
2063 2117 .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 10px; margin: 14px 0; }
@@ -2087,6 +2141,8 @@ textarea:focus-visible {
2087 2141 }
2088 2142 .config-group:hover td { background: var(--bg-raised); }
2089 2143
2144 /* ---------- language colours ---------- */
2145
2090 2146 /* Language hints for file rows. Active tree rows still win through specificity. */
2091 2147 .tree-icon.lang-go, .dirlist a.file.lang-go .icon { color: #00add8; }
2092 2148 .tree-icon.lang-rust, .dirlist a.file.lang-rust .icon { color: #dea584; }