Commit eee0cf49cb
Verified · cmc
docs/specs/2026-09-17-web-design-foundation-design.md added +282
| @@ -0,0 +1,282 @@ | ||
| 1 | # Web design foundation | |
| 2 | ||
| 3 | Closes #218. A rewrite of the stylesheet around a measured token set, one | |
| 4 | control family with authored states, content widths chosen per page, and | |
| 5 | four reference pages recomposed: landing, repository overview, merge | |
| 6 | request, repository settings. The copy fixes from the September 2026 UI | |
| 7 | review ride along. No JavaScript: the instance CSP is `script-src 'none'` | |
| 8 | and stays so. | |
| 9 | ||
| 10 | The review's findings came from dark-scheme screenshots. The stylesheet is | |
| 11 | light-first with dark through `prefers-color-scheme`. Both schemes are | |
| 12 | designed here, dark as the audited one, and every token is measured in | |
| 13 | each. | |
| 14 | ||
| 15 | Mockups of the four pages, with the tokens and components below, were | |
| 16 | reviewed in the browser before this was written. They are the visual | |
| 17 | reference for the implementation, not the templates: the implementation | |
| 18 | keeps 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 | ||
| 47 | Colour. 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 | ||
| 69 | The rail keeps `--shell-*`: black ground and its own text, muted, line, | |
| 70 | hover and mark values in both schemes. The dark values above are neutral | |
| 71 | greys so the pane and the black rail read as two grounds. | |
| 72 | ||
| 73 | The syntax palettes and the diff tints stay as they are; the existing | |
| 74 | `TestSyntaxPaletteContrast` pins them. The chroma grounds it measures | |
| 75 | against become `--inset` and the diff tints. | |
| 76 | ||
| 77 | Type. Base 16px. Atkinson Hyperlegible Next and Mono, self-hosted, as | |
| 78 | now. Sizes `--fs-0` to `--fs-6`: 12, 13, 14, 16, 20, 24, 32px. Weights | |
| 79 | 400, 500, 600 only. Body line-height 1.5, headings 1.25. Metadata that | |
| 80 | carries a decision, such as a branch, a timestamp or a check result, is | |
| 81 | never below 13px. Monospace is for commands, hashes, paths, refs and | |
| 82 | code; rail labels, dates and section labels use the sans face. | |
| 83 | ||
| 84 | Spacing. `--sp-1` to `--sp-7`: 4, 8, 12, 16, 24, 32, 48px. Between | |
| 85 | sections 32, within a group 8 or 12, page padding 24 by 32. | |
| 86 | ||
| 87 | Radius. 4px on controls and chips, 6px on cards, lists and code blocks. | |
| 88 | One shadow, on the ref menu dropdown only. | |
| 89 | ||
| 90 | ## Components | |
| 91 | ||
| 92 | Buttons. All 32px tall at 14px, 500 weight, same horizontal padding, so | |
| 93 | they 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 | ||
| 107 | Toggles keep `aria-pressed`. Pressed: `--surface` ground, `--link` border | |
| 108 | and text, label in the participle as now. | |
| 109 | ||
| 110 | Links: `--link`, underline on hover; inside rendered prose always | |
| 111 | underlined. Row titles in lists stay `--fg` and turn `--link` on hover. | |
| 112 | ||
| 113 | Focus: one ring, 2px `--focus` with 2px offset, on every focusable | |
| 114 | element including inputs. The rail uses `--shell-mark`. | |
| 115 | ||
| 116 | Chips: 4px radius, 12px, 500 weight, 1px border at 40% of the state | |
| 117 | colour, 10% tint ground. Variants as now; topic chips use `--link`. | |
| 118 | ||
| 119 | Tabs: current tab `--fg` at 600 with the 2px `--mark` bar. Counts in | |
| 120 | tabs are plain `--muted` text. The rail's count keeps the orange ground. | |
| 121 | ||
| 122 | Forms: inputs on `--inset`, `--line` border, 32px tall, 14px. Label above | |
| 123 | the field at 500 weight; the hint in `--muted` under the label. Checkbox | |
| 124 | 16px, label to its right on the same line. `::placeholder` in `--muted`. | |
| 125 | ||
| 126 | Feedback: `.error` and `.notice` become one `.flash` family with a | |
| 127 | `role`, a 3px left edge in `--bad`, `--ok` or `--warn`, a 6% tint ground | |
| 128 | and `--fg` text. `class="error"` stays on the error variant, since e2e | |
| 129 | tests pin it. | |
| 130 | ||
| 131 | Containers: one recipe for lists, tables and cards: `--surface` ground, | |
| 132 | `--line` border, 6px radius, `--faint` row separators, `--hover` row | |
| 133 | hover. Table headers 13px, 500, `--muted`. A card is a bounded group with | |
| 134 | a header: README, a comment, an aside group. Bordered boxes around whole | |
| 135 | page regions go. | |
| 136 | ||
| 137 | Code and diffs: `--inset` ground, existing gutters and chroma classes. | |
| 138 | ||
| 139 | ## Shell and layout | |
| 140 | ||
| 141 | Rail: 14rem, black, same sections. Labels sans, 12px, uppercase, tracked. | |
| 142 | Under 52rem it becomes the top strip it is today, search kept, pinned | |
| 143 | group hidden. | |
| 144 | ||
| 145 | Content 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 | ||
| 155 | Two-column pages (issue, MR, dashboard): the aside is 18rem, the gap | |
| 156 | 32px, its top aligned with the first content block below the page | |
| 157 | header. Prose in the conversation column is capped at 78ch. Under 62rem | |
| 158 | the aside stacks above the conversation. The dashboard's pinned group | |
| 159 | goes; the rail has it. | |
| 160 | ||
| 161 | Repository header: two levels. The identity row holds owner/name, the | |
| 162 | visibility and archived chips, and the toggles at the right. On the code | |
| 163 | tab it also shows the description with the topic chips on the same line, | |
| 164 | one `--muted` line for website and mirrors, and for a signed-in viewer | |
| 165 | one `--muted` line explaining Pin, Watch and Bookmark. On every other tab | |
| 166 | only the identity row and the tab bar render. The header is identical on | |
| 167 | every page within a tab. | |
| 168 | ||
| 169 | Page header: `h1` at 24px, page controls on the same row at the right, | |
| 170 | 24px before content. Filters are secondary buttons in a group with the | |
| 171 | active one pressed, each a GET link; a text filter has an explicit Search | |
| 172 | button and a result count line above the list. | |
| 173 | ||
| 174 | Responsive: the four reference pages are checked at 375px, 768px, and | |
| 175 | 1280px at 200% zoom. Tables under 40rem drop the last-commit column and | |
| 176 | scroll inside their wrapper. Clone commands are two labelled code blocks | |
| 177 | that wrap. | |
| 178 | ||
| 179 | ## Reference pages | |
| 180 | ||
| 181 | Landing, `bounded`. Order: mark and name as the title; proposition; | |
| 182 | quickstart; 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 | ||
| 207 | Register, `bounded`. Labels above fields, one primary button. Key field | |
| 208 | label "SSH public key", hint "Paste the contents of your public key | |
| 209 | file, usually ~/.ssh/id_ed25519.pub. It starts with ssh-ed25519 or | |
| 210 | ssh-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 | |
| 212 | alternative stays as one line at the bottom. | |
| 213 | ||
| 214 | Repository overview, `wide`. Under the header: the path bar with the ref | |
| 215 | menu, crumbs, and at the right Search code, History, Download as | |
| 216 | secondary buttons. The file table with the tip commit as its first row. | |
| 217 | The README card. Then, at reading width, two columns: Clone on the left | |
| 218 | as two labelled blocks, SSH first; About on the right with commits, | |
| 219 | branches, tags, contributors, licence, latest release, build state in a | |
| 220 | four-column grid, then the language bar with its legend. The Find file | |
| 221 | link becomes Search code, since the route searches contents. | |
| 222 | ||
| 223 | Merge request, `reading`. Title row: title, number in `--muted`. Next | |
| 224 | line: 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. | |
| 226 | Subtabs as now with plain counts. Aside order: Merge, Checks, Reviews, | |
| 227 | Reviewers, Source and target, Milestone. The diff view's empty case | |
| 228 | renders "No changes between the source and target." followed, when the | |
| 229 | head is an ancestor of the target, by "The source branch was already | |
| 230 | merged or fast-forwarded into TARGET." A related-request link needs a | |
| 231 | data model and is not in this spec. | |
| 232 | ||
| 233 | Repository settings, `bounded`. A flash at the top after a save, "Saved | |
| 234 | the FIELD.", rendered on the redirected GET through the existing flash | |
| 235 | cookie; an error renders the flash error variant and the field keeps the | |
| 236 | submitted 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 | |
| 238 | hint beneath the label, control column bounded at 28rem, Save at the | |
| 239 | end. Every control with a consequence carries one hint sentence: | |
| 240 | visibility, git:// serving, default branch, each merge gate, require-MR, | |
| 241 | archive. The identity form's Save is primary; every other Save is | |
| 242 | secondary; Unprotect and Archive are destructive. Topics become one | |
| 243 | prefilled text field, comma separated. | |
| 244 | ||
| 245 | ## Copy | |
| 246 | ||
| 247 | Buttons and headings in sentence case. "Sign in" and "Log out". "Search" | |
| 248 | as the rail field's placeholder and every search button's label. The | |
| 249 | homepage no longer calls `repo create` the whole onboarding. The wiki's | |
| 250 | Users page records the vocabulary. | |
| 251 | ||
| 252 | ## Landing in order | |
| 253 | ||
| 254 | Four merge requests onto `main`, each green on bay1 before merging, each | |
| 255 | referencing #218: | |
| 256 | ||
| 257 | 1. 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. | |
| 261 | 2. 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. | |
| 264 | 3. Copy and the `SSH-keys` wiki page. Rides with 2 if the diff stays | |
| 265 | readable. | |
| 266 | 4. Screenshots, captured after 2 deploys, committed with their routes | |
| 267 | and the landing `<picture>`. | |
| 268 | ||
| 269 | Release v1.23.0. The CHANGELOG and the Admin wiki page note the | |
| 270 | lowercase title. | |
| 271 | ||
| 272 | ## Verification | |
| 273 | ||
| 274 | Before 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 | |
| 276 | browser: a keyboard walk of the four reference pages, a 200% zoom pass, | |
| 277 | and a comparison against the mockups. | |
| 278 | ||
| 279 | ## Not in this spec | |
| 280 | ||
| 281 | Triage and builds (D01, D02, D04, D05, M01, M04), profiles (P01 to | |
| 282 | P03), and the benchmark (B08). Each is filed as its own issue. | |