Commit eee0cf49cb

eee0cf49cb75c3b91be8d5ae8839e8256641f136

parent: 1c88ef4eb4

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-17 05:54 UTC

docs: web design foundation spec

Ref #218
docs/specs/2026-09-17-web-design-foundation-design.md added +282
@@ -0,0 +1,282 @@
1# Web design foundation
2
3Closes #218. A rewrite of the stylesheet around a measured token set, one
4control family with authored states, content widths chosen per page, and
5four reference pages recomposed: landing, repository overview, merge
6request, repository settings. The copy fixes from the September 2026 UI
7review ride along. No JavaScript: the instance CSP is `script-src 'none'`
8and stays so.
9
10The review's findings came from dark-scheme screenshots. The stylesheet is
11light-first with dark through `prefers-color-scheme`. Both schemes are
12designed here, dark as the audited one, and every token is measured in
13each.
14
15Mockups of the four pages, with the tokens and components below, were
16reviewed in the browser before this was written. They are the visual
17reference for the implementation, not the templates: the implementation
18keeps the template structure and class names where a component survives.
19
20## Rules
21
22- **Two accents, two jobs.** Blue is what you can do: links, primary
23 buttons, focus, pressed toggles. Orange is where you are and what wants
24 you: the current tab's bar, rail counts, pending state, unverified
25 signatures. Orange is never a button ground.
26- **Links and fills are different tokens.** `--link` is text on a ground
27 and must clear 4.5:1 on canvas, surface and hover. `--fill` is a
28 button ground and must carry white at 4.5:1 and stand 3:1 against
29 surface. One blue for both fails one of the two jobs in each scheme.
30- **Every colour is a token on `:root`**, redefined in the dark media
31 query. No colour defined only in the media query. A test parses the
32 two blocks out of `style.css` and enforces the floors in the table
33 below; the hex values are starting points and the test is the contract.
34- **Every class in a template has a selector.** A test parses every
35 template for class names and fails on one with no rule in `style.css`.
36 Classes that chroma emits and classes tests pin are on an allowlist
37 only if they have no styling; today none qualifies.
38- **States are authored, never hover-only.** Hover, focus, active,
39 pressed, disabled, empty, success and error each have a rule. Essential
40 meaning is never in a `title` attribute alone.
41- **The product name is lowercase** in templates, docs and the
42 CHANGELOG. The instance title is `[web] title` in the daemon's
43 config; the Admin wiki page says to set it lowercase.
44
45## Tokens
46
47Colour. Light and dark per token; floors are WCAG contrast ratios.
48
49| Token | Role | Light | Dark | Floor |
50| --- | --- | --- | --- | --- |
51| `--canvas` | page | `#ffffff` | `#101114` | |
52| `--surface` | cards, lists, aside groups | `#f6f7f9` | `#17191d` | distinct from canvas: luminance ratio at least 1.15 |
53| `--inset` | inputs, code, terminal blocks | `#ffffff` | `#0b0c0e` | distinct from surface |
54| `--hover` | row and control hover | `#eef0f3` | `#1e2126` | |
55| `--line` | control and card borders | `#d0d5db` | `#2f343b` | 3:1 against canvas |
56| `--faint` | row separators | `#e6e9ed` | `#22252b` | |
57| `--fg` | text | `#1c1f24` | `#e9eaec` | 7:1 on canvas and surface |
58| `--muted` | metadata | `#5a6270` | `#a3a9b3` | 4.5:1 on canvas and surface |
59| `--link` | text links | `#0033d6` | `#8b9bff` | 4.5:1 on canvas, surface, hover |
60| `--fill` | primary button ground | `#0000f0` | `#3b4fe8` | white on it 4.5:1; 3:1 against surface |
61| `--mark` | current-tab bar, counts | `#e4572e` | `#ff6b3d` | 3:1 non-text |
62| `--warn` | the same hue at text size | `#b8400f` | `#ff8a63` | 4.5:1 |
63| `--ok` | success | `#0a7d3c` | `#3fce7a` | 4.5:1 on canvas and surface |
64| `--bad` | failure, destructive | `#c62828` | `#ff6b6b` | 4.5:1 on canvas and surface |
65| `--done` | merged | `#6b21a8` | `#c084fc` | 4.5:1 on canvas and surface |
66| `--neutral` | closed, draft, unknown | `--muted` | `--muted` | as muted |
67| `--focus` | ring | `--link` | `--link` | 3:1 against canvas |
68
69The rail keeps `--shell-*`: black ground and its own text, muted, line,
70hover and mark values in both schemes. The dark values above are neutral
71greys so the pane and the black rail read as two grounds.
72
73The syntax palettes and the diff tints stay as they are; the existing
74`TestSyntaxPaletteContrast` pins them. The chroma grounds it measures
75against become `--inset` and the diff tints.
76
77Type. Base 16px. Atkinson Hyperlegible Next and Mono, self-hosted, as
78now. Sizes `--fs-0` to `--fs-6`: 12, 13, 14, 16, 20, 24, 32px. Weights
79400, 500, 600 only. Body line-height 1.5, headings 1.25. Metadata that
80carries a decision, such as a branch, a timestamp or a check result, is
81never below 13px. Monospace is for commands, hashes, paths, refs and
82code; rail labels, dates and section labels use the sans face.
83
84Spacing. `--sp-1` to `--sp-7`: 4, 8, 12, 16, 24, 32, 48px. Between
85sections 32, within a group 8 or 12, page padding 24 by 32.
86
87Radius. 4px on controls and chips, 6px on cards, lists and code blocks.
88One shadow, on the ref menu dropdown only.
89
90## Components
91
92Buttons. All 32px tall at 14px, 500 weight, same horizontal padding, so
93they align in a row. Four treatments:
94
95- Primary: `--fill` ground, white text. One per form group, the action
96 the page exists for: Create account, Comment, Merge, the identity
97 form's Save.
98- Secondary (`.btn`): canvas ground, `--line` border, `--fg` text, hover
99 to `--hover`. Pin, Watch, Bookmark, Fork, Filter, per-field Save.
100- Quiet (`.linklike`): no ground or border, `--link` text, underline on
101 hover. Inline actions: clear, remove, make primary.
102- Destructive (`.danger`): canvas ground, `--bad` border and text; hover
103 fills `--bad` with white text. Delete, close without merging,
104 unprotect, archive. Beside the typed confirmation field where one
105 exists today.
106
107Toggles keep `aria-pressed`. Pressed: `--surface` ground, `--link` border
108and text, label in the participle as now.
109
110Links: `--link`, underline on hover; inside rendered prose always
111underlined. Row titles in lists stay `--fg` and turn `--link` on hover.
112
113Focus: one ring, 2px `--focus` with 2px offset, on every focusable
114element including inputs. The rail uses `--shell-mark`.
115
116Chips: 4px radius, 12px, 500 weight, 1px border at 40% of the state
117colour, 10% tint ground. Variants as now; topic chips use `--link`.
118
119Tabs: current tab `--fg` at 600 with the 2px `--mark` bar. Counts in
120tabs are plain `--muted` text. The rail's count keeps the orange ground.
121
122Forms: inputs on `--inset`, `--line` border, 32px tall, 14px. Label above
123the field at 500 weight; the hint in `--muted` under the label. Checkbox
12416px, label to its right on the same line. `::placeholder` in `--muted`.
125
126Feedback: `.error` and `.notice` become one `.flash` family with a
127`role`, a 3px left edge in `--bad`, `--ok` or `--warn`, a 6% tint ground
128and `--fg` text. `class="error"` stays on the error variant, since e2e
129tests pin it.
130
131Containers: one recipe for lists, tables and cards: `--surface` ground,
132`--line` border, 6px radius, `--faint` row separators, `--hover` row
133hover. Table headers 13px, 500, `--muted`. A card is a bounded group with
134a header: README, a comment, an aside group. Bordered boxes around whole
135page regions go.
136
137Code and diffs: `--inset` ground, existing gutters and chroma classes.
138
139## Shell and layout
140
141Rail: 14rem, black, same sections. Labels sans, 12px, uppercase, tracked.
142Under 52rem it becomes the top strip it is today, search kept, pinned
143group hidden.
144
145Content width: a class on `main`, set from the page struct, one of:
146
147- `wide`, no maximum: tree, blob, blame, log, commit and MR diffs,
148 builds, code search.
149- `reading`, 72rem: dashboard, issue and MR lists and pages, explore,
150 profile, releases, wiki, notifications, bookmarks, snippets. The
151 default.
152- `bounded`, 48rem: landing, login, register, new repository, new issue,
153 new MR, repository settings, account settings, admin.
154
155Two-column pages (issue, MR, dashboard): the aside is 18rem, the gap
15632px, its top aligned with the first content block below the page
157header. Prose in the conversation column is capped at 78ch. Under 62rem
158the aside stacks above the conversation. The dashboard's pinned group
159goes; the rail has it.
160
161Repository header: two levels. The identity row holds owner/name, the
162visibility and archived chips, and the toggles at the right. On the code
163tab it also shows the description with the topic chips on the same line,
164one `--muted` line for website and mirrors, and for a signed-in viewer
165one `--muted` line explaining Pin, Watch and Bookmark. On every other tab
166only the identity row and the tab bar render. The header is identical on
167every page within a tab.
168
169Page header: `h1` at 24px, page controls on the same row at the right,
17024px before content. Filters are secondary buttons in a group with the
171active one pressed, each a GET link; a text filter has an explicit Search
172button and a result count line above the list.
173
174Responsive: the four reference pages are checked at 375px, 768px, and
1751280px at 200% zoom. Tables under 40rem drop the last-commit column and
176scroll inside their wrapper. Clone commands are two labelled code blocks
177that wrap.
178
179## Reference pages
180
181Landing, `bounded`. Order: mark and name as the title; proposition;
182quickstart; picture; three facets; routes; sign-in line. Copy:
183
184- Proposition: "A git forge you drive from the terminal. Repositories,
185 issues, merge requests and CI over SSH, with a fast, readable web view
186 of the same state."
187- Quickstart: `ssh git@{host} help` with the comment "every command, no
188 client to install", and `git clone ssh://git@{host}/owner/repo.git`.
189- Picture: a `<picture>` with a dark source under
190 `(prefers-color-scheme: dark)` and a light `img`, of the krz/gitbay
191 merge request page at 1280 wide, captured from gitbay.org after the
192 redesign deploys. Files under `internal/web/static/img/`, routes
193 generated from the directory as fonts are, served with the same cache
194 headers as the stylesheet.
195- Facets. Read: "Browse and clone any public repository over HTTPS or
196 git://, no account. Every commit shows whether its signature
197 verified." Write: "Push over SSH with the key you already have. Create
198 a repository, file an issue, open and merge a request, all as
199 commands." Review: "Read a diff, comment on a line, approve, merge, in
200 the browser or the terminal."
201- Routes: primary "Explore repositories"; secondary "Create an account"
202 when registration is open.
203- Sign-in line, `--muted`: "Have an account? Sign in with an emailed
204 link, or run `ssh git@{host} web login`." When email login is off the
205 first clause is dropped.
206
207Register, `bounded`. Labels above fields, one primary button. Key field
208label "SSH public key", hint "Paste the contents of your public key
209file, usually ~/.ssh/id_ed25519.pub. It starts with ssh-ed25519 or
210ssh-rsa." and a link to the wiki page `SSH-keys`, new, which shows
211`ssh-keygen -t ed25519` and where the public half lands. The terminal
212alternative stays as one line at the bottom.
213
214Repository overview, `wide`. Under the header: the path bar with the ref
215menu, crumbs, and at the right Search code, History, Download as
216secondary buttons. The file table with the tip commit as its first row.
217The README card. Then, at reading width, two columns: Clone on the left
218as two labelled blocks, SSH first; About on the right with commits,
219branches, tags, contributors, licence, latest release, build state in a
220four-column grid, then the language bar with its legend. The Find file
221link becomes Search code, since the route searches contents.
222
223Merge request, `reading`. Title row: title, number in `--muted`. Next
224line: state chip, then "opened by X on DATE", "merged by X on DATE" or
225"closed without merging by X on DATE", then source into target in code.
226Subtabs as now with plain counts. Aside order: Merge, Checks, Reviews,
227Reviewers, Source and target, Milestone. The diff view's empty case
228renders "No changes between the source and target." followed, when the
229head is an ancestor of the target, by "The source branch was already
230merged or fast-forwarded into TARGET." A related-request link needs a
231data model and is not in this spec.
232
233Repository settings, `bounded`. A flash at the top after a save, "Saved
234the FIELD.", rendered on the redirected GET through the existing flash
235cookie; an error renders the flash error variant and the field keeps the
236submitted value. A row of anchor links to the sections. Each section an
237`h2` and a stack of rows. A row is a grid: label column 14rem with the
238hint beneath the label, control column bounded at 28rem, Save at the
239end. Every control with a consequence carries one hint sentence:
240visibility, git:// serving, default branch, each merge gate, require-MR,
241archive. The identity form's Save is primary; every other Save is
242secondary; Unprotect and Archive are destructive. Topics become one
243prefilled text field, comma separated.
244
245## Copy
246
247Buttons and headings in sentence case. "Sign in" and "Log out". "Search"
248as the rail field's placeholder and every search button's label. The
249homepage no longer calls `repo create` the whole onboarding. The wiki's
250Users page records the vocabulary.
251
252## Landing in order
253
254Four merge requests onto `main`, each green on bay1 before merging, each
255referencing #218:
256
2571. Stylesheet rewrite: tokens, components, shell, and every template
258 adjusted where a component's markup changes. The contrast test and
259 the class-coverage test land here. Every page family screenshotted at
260 1280 and 375 in both schemes before merge.
2612. Layout and pages: the `main` width class, the two-level repository
262 header, and the four reference pages. An e2e test posts a settings
263 form and checks the flash and the retained value on error.
2643. Copy and the `SSH-keys` wiki page. Rides with 2 if the diff stays
265 readable.
2664. Screenshots, captured after 2 deploys, committed with their routes
267 and the landing `<picture>`.
268
269Release v1.23.0. The CHANGELOG and the Admin wiki page note the
270lowercase title.
271
272## Verification
273
274Before each merge: build, vet, the unit tests of `internal/httpd` and
275`internal/web`, and the e2e tests that pin class names. Then in the
276browser: a keyboard walk of the four reference pages, a 200% zoom pass,
277and a comparison against the mockups.
278
279## Not in this spec
280
281Triage and builds (D01, D02, D04, D05, M01, M04), profiles (P01 to
282P03), and the benchmark (B08). Each is filed as its own issue.