Files
werkator/docs/prs/2026-09-08-PR#35-ui-kin-design.md
0e5d6a3bb1 ui: the blog's visual language (RFC 0001, option G) — palette, IBM Plex, logo, header, view titles, icons (#35)
The first of RFC 0001's two pull requests: werkator.css rewritten around the
RFC's tokens (white paper, blue-shifted petrol as the only accent, clay for
failures, a neutral dark mode), IBM Plex Sans and Mono bundled as WOFF2 under
static/fonts/, the two-check W as logo and favicon, a site header with the
wordmark, the repository crumb and the instance navigation, view tabs followed
by the view's title and its explanation, statuses as dot and word, stroke
glyphs as CSS masks instead of emoji, and the phone cards in the same
vocabulary. No data change; the repository switcher stays until the overview PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #35
2026-09-08 17:06:59 +02:00

124 lines
9.3 KiB
Markdown

> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## Related Links
- [RFC 0001: Web UI Redesign — the Instrument Panel, revised as Kin](../rfcs/0001-web-ui-instrument-panel.md), and its [drafts](../rfcs/0001-web-ui-instrument-panel/drafts/README.md).
- PR#34 (the RFC's round four), which decided the direction this PR implements.
## The Problem
The web UI was a functional port of the legacy generated pages: a system font, blue links, pill badges, emoji as icons, and a page title that named the view and the repository in one line.
It looked like every other CI page and had no visual kinship with the author's other sites.
RFC 0001 chose a direction — the blog's paper and IBM Plex, a blue-shifted petrol as the only accent, a title hierarchy that names the view first and explains it second, and a new logo — and decided to roll it out in two pull requests: the design first, the cross-repository overview second.
This PR is the first of the two.
## Non-Goals
- No Repositories page, no `GET /api/repos`, no repository line: the `<select>` switcher stays until the second PR.
- No data change: every page shows exactly the fields it showed before, from the same API, in the same formats (`UiFormats` and `werkator.js` are untouched in what they format).
- No manual theme toggle; `prefers-color-scheme` decides, as before.
- No `Current` tab: the view is reached from a running row's live link, as decided in PR#13; the tab appears only while the reader is on it.
- No version bump and no release-notes entry: the author bumps the version with the deployment.
## The Scenarios
### Feature: the web UI in the blog's visual language
#### Background
- The palette and typography are the ones RFC 0001 specifies: the blog's neutrals, the accent `#0E6B7D` (light) / `#63BCCB` (dark), clay for failures, ghost grey for cancelled, interrupted and unknown.
- A *repository view* is Latest, Branches, History or Current; an *instance page* is System or the release notes; the artifact page belongs to a repository but is no view.
#### Scenario#35.01: every page shares the site header, and names its view first
So that a reader sees where they are before what they see, and finds the repository, the instance pages and the live state in the same place on every page.
- **Given** any page of the UI
- **When** it renders
- **Then** it carries the site header: the logo and the wordmark linking to the served repository, the repository as a crumb (the switcher with several served repositories), `System` as the instance navigation, the live indicator as an outlined chip, and the reload button
- **and** below it the view's tabs (repository views only), then the view's title and its one-line explanation (`Latest` — one build per branch, newest first; `Branches` — every origin branch and its latest build; `History` — all stored builds, newest first; `Current` — running builds with their live log; `System` — instance metrics since first start)
- **and** next to the title the last poll and the watcher state, filled from `/api/watcher`
- **and** the instance pages carry no repository crumb and no tabs
- **and** the footer names the version as the link to the release notes, which are not navigation.
##### Verified by
- [UiControllerTest — every page carries the site header with the wordmark and the view's title and explanation](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
- [UiControllerTest — the system page is an instance page: its own title, no repository crumb](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
- [UiControllerTest — with one served repository the pages keep their existing URLs and show no switcher](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) and its multi-repository sibling, unchanged.
#### Scenario#35.02: statuses are dots and words, icons are glyphs, and both are the same from the server and from the script
So that a failed row is serious without shouting, a running row is visibly alive, and a page re-rendered by `werkator.js` looks exactly like the page the server rendered.
- **Given** a build table with running, pending, successful and failed rows
- **When** the server renders it, and when `werkator.js` re-renders it from the API
- **Then** each status is a 7 px dot and a Mono word in its status color — the dot outlined for pending, pulsing for running — with `status status-<name>` as the only markup
- **and** the running row sits on the accent tint, the failed row on the clay tint
- **and** the artifact, log, permalink, live, copy, restart, delete and cancel controls are stroke glyphs from the stylesheet (`icon icon-<name>`), titled for assistive technology, and no emoji appears anywhere.
##### Verified by
- [UiControllerTest — the icons are stroke glyphs from the stylesheet, not emoji](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
- [UiControllerTest — latest view renders rows with badge, Gitea links, artifact link, actions, and token](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) and the other row-rendering tests, unchanged: the status classes and action attributes they assert are the ones the design keeps.
- The script side has no test harness; `werkator.js` was checked in the browser against the server-rendered page (Latest, History, Current, System, artifact page, light and dark, desktop and 390 px).
#### Scenario#35.03: the fonts come with the jar
So that the UI looks the same on every host and loads nothing from a third party — the deployments sit behind a reverse proxy and a traffic quota.
- **Given** a browser without IBM Plex installed
- **When** it opens any page
- **Then** the stylesheet declares IBM Plex Sans (400, 500, 600, 700) and IBM Plex Mono (400, 500) by `@font-face` from `/fonts/*.woff2`
- **and** the server serves those six files (latin subset, about 215 KB in total) from the jar's `static/fonts/`
- **and** a host without them falls back to the stack `"Segoe UI", system-ui, sans-serif` and `ui-monospace, Consolas, monospace` without any layout change.
##### Verified by
- [UiControllerTest — the fonts are bundled: the stylesheet's faces are served from the jar](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
#### Scenario#35.04: the phone keeps the cards, in the new vocabulary
So that the existing small-screen behaviour stays valid.
- **Given** a viewport below 680 px
- **When** a build table renders
- **Then** the rows become cards with their column labels, the running and failed cards with their tint as border and ground
- **and** the header keeps the logo, the crumb, `System`, the live dot and the reload button on one line
- **and** the action buttons are 44 px targets.
##### Verified by
- Checked in the browser at 390 px (Latest, System); the breakpoint's markup is the one the existing tests render.
## The Solution
- **`werkator.css`** is rewritten around the RFC's tokens: the neutrals and the petrol as custom properties on `:root`, redefined for `prefers-color-scheme: dark`; IBM Plex by `@font-face`; the header, tabs, view title, tables, statuses, cards, system page and footer as the drafts draw them.
Class names shared with `werkator.js` and the templates are kept, so the script's rendering did not have to change where the markup did not.
- **`fragments.html`** replaces the `h1` with three fragments: `siteHeader` (the blog's header), `viewHead` (tabs, title, explanation, the `view-meta` line and the watcher banner), and `footer`.
The fragments are named `siteHeader` and `viewHead`, not `header` and `nav`, because Thymeleaf resolves `fragments :: nav` to the first `<nav>` *element* before the fragment of that name.
- **Icons** are 24 px stroke paths as CSS masks (`.icon-<name>` with a `--icon` data URI), colored by `currentColor`; the templates write the spans, `werkator.js` gains `icon(name)` and `iconLink(...)`, and `actionButton` takes the glyph name and an optional label instead of a symbol.
The live link built by the script is now repository-scoped like the template's (it pointed at `/current` without the repository prefix before).
- **`favicon.svg`** is the RFC's tile: the two-check W in white on a petrol tile; the header uses the bare mark inline, stroked in `--accent` so it follows the theme.
- **`static/fonts/`** holds the six WOFF2 files (latin subset, from the Google Fonts build of IBM Plex, SIL Open Font License).
- The `view-meta` line is filled by the existing watcher poll: `polled HH:MM:SS · watcher running|stopped`.
## Open Questions
- The static resources are served with `Cache-Control: no-cache, must-revalidate` and a `Last-Modified` header, as before, so a browser revalidates the six font files on every page load (six conditional requests answered with 304).
A long `max-age` for `/fonts/` alone would need a resource-handler configuration; left out until the traffic logs say it matters.
## Additional Changes
- The architecture skill's Web UI section describes the fragments, the tokens, the icons and the fonts.
## Prerequisite PRs
- PR#34 — RFC 0001, round four: the decisions this PR implements.
## Follow-up PRs
- The second PR of RFC 0001: `GET /api/repos`, the Repositories page, and the repository line replacing the `<select>`.