跳转到内容

Freezer UI (Clearlooks)

Related: Freezer · Linux desktop shells (skeuomorph era) · README

Kind: redesign SPEC (layout / tokens / copy / AC). No application code. Stack not yet chosen — Freezer is being rewritten from scratch; this note is the UI target, not a patch plan against any existing codebase.

Direction (Noa, 2026-10-08): GNOME 2.x Clearlooks chrome as structure, not costume. One hand-written stylesheet carrying the tokens; no CSS framework. No JS required; tiny progressive enhancement optional. Light only (ADR 0022). Thin ops surface (ADR 0015). No in-app auth (ADR 0011).

Inputs: vault Clearlooks refs; Quickshell Clearlooks prototype tokens (pelikanade/flake PR #25 quickshell.prototype.html).

Mock: freezer-clearlooks.html — single self-contained HTML, no CDN, no JS. Screens via :target (#index, #job-running, #job-failed, #job-stopped, #job-interrupted, #job-nothing, #job-hit, #job-truncated, #empty, #e404, #e409). Prefixed #shot-* hides the mock palette for screenshots.


Attachments/design-linux-clearlooks-widgets.png

Clearlooks 2.20 widget gallery (Commons) — titlebars, toolbar, notebook tabs, sunken list, selection, Fermer button.

Attachments/design-linux-gnome2-fedora-clearlooks.png

Fedora Core 5 GNOME 2 + Clearlooks — dual panels, Nautilus chrome.

Attachments/design-clearlooks-era-ubuntu-clearlooks.png

Ubuntu Clearlooks desktop — soft plastic panels + blue titlebars.

Steal: soft grey window plate, Metacity blue titlebar, raised gradient buttons, sunken treeview with rules-hint, notebook/location buttons, GtkInfoBar strip, statusbar panes, DejaVu Sans ~10pt, monospace sunken text view. Reject: Adwaita flat, Compiz glass as default, Crystal candy icons, framework cards/pills, modern capsule chrome.


Attachments/design-freezer-clearlooks-index.png

Index @ 1280×800 — repositories treeview + recent jobs grouped by commit + Build-now infobar.

Attachments/design-freezer-clearlooks-job-running.png

Running job — toolbar Stop, pulse progress, cache/paths still pending.

Attachments/design-freezer-clearlooks-job-failed.png

Failed job — error InfoBar + transcript opened at the end (no grey chips).

Attachments/design-freezer-clearlooks-mobile.png

Index @ 390×844 — stacked chrome, status word under each entry, touch-sized Build/Stop.

Attachments/design-freezer-clearlooks-mobile-job-failed.png

Failed job @ 390×844 — props collapse to one pair per row; transcript still opens at its end.

Hero for the braid: index desktop. Failed + running prove the state vocabulary; mobile proves the collapse.


┌─ Metacity titlebar (page title) ─┐
├─ toolbar (Reload · Auto · Stop) ─┤
├─ pathbar (job pages only) ───────┤
├─ optional GtkInfoBar ────────────┤
│ content (repos + jobs / job) │
├─ statusbar ──────────────────────┤
└──────────────────────────────────┘
  • Navigation is toolbar + pathbar (GNOME-app grammar). Repos live in the index content as a treeview; #repo-{Name} anchors stay.
  • Titlebar is CSS chrome (not a real WM). Page title stays {Title} · freezer.
  • Statusbar at the bottom: live counts, refresh note, last-rendered clock.
RegionTarget
ReposTreeview first (ops write surface): Name · Forge · Last built · Latest job · Build now
JobsRecent jobs as a sunken GtkTreeView below repos: columns Status · Entry · Job · Started · Duration · Result · Action; group rows by (repo, commit) with a rowspan identity column
FeedbackOptional InfoBar from ?notice= (see § Feedback)
EmptyEmpty row inside the jobs treeview; repos empty-state is a separate dashed well only if config is empty
RegionTarget
HeaderHero: status icon + entry + one-line summary; Stop lives on the toolbar when running
KVLabel:value grid, 3×2 then 2×2 ≤1100px, 1×2 ≤640px; full SHA wraps as a block
AlertsGtkInfoBar: error failed; question stopped; warning interrupted / matched-nothing; info Build-now / Stop acknowledged
RunningPulse progress bar + “Ns so far”; Cache and Published show pending, not miss/0
TranscriptSunken text view (no grey per-line chips), opens at the end (CSS column-reverse or #end + optional tiny PE); truncated line callout; “Open as plain text” → /jobs/{id}/log
Cache hitEmpty text-view well: “Already in the cache…”
Interrupted empty logEmpty well + warning InfoBar (restart explanation)

GtkMessageDialog card: icon + primary + hint + HTTP code + default Back button.

  • Titlebar + toolbar compress (hide long labels).
  • Pathbar crumbs truncate (entry crumb hides the monospace fragment).
  • Treeview headers hide; each row becomes icon + entry link + secondary line (status · repo · short sha · when · duration); Build now / Stop stay visible.
  • Props collapse to 2 columns. Statusbar drops the middle pane.

Custom properties in one hand-written stylesheet (and only there). No CSS framework. Values sourced below; implementer may nudge ±1–2 for AA if measured differently.

TokenValueSource
--fz-bg#edecebClearlooks gtkrc bg_color (gnome-themes 2.x / Arch bbs Clearlooks threads)
--fz-base#ffffffgtkrc base_color
--fz-stripe#f4f3f2rules-hint row = mix(base, bg); sampled Clearlooks 2.20 list
--fz-prelight#eaf1farow hover = base tinted toward selected
--fz-text#2e3436Tango Aluminium 6 (QS prototype --text) — 10.7:1 on bg
--fz-muted#555753Tango Aluminium 5 — 6.2:1 on bg
--fz-dim#888a85Aluminium 4 — decoration / line-numbers only, never primary text
--fz-link#204a87Tango Sky Blue 3 — 8.8:1 on base
--fz-selected#86abd9gtkrc selected_bg_color (classic Clearlooks)
--fz-selected-hi#a9c6eatop stop of selection / progress candy
--fz-accent / --fz-focus#3465a4Tango Sky Blue 2 (QS --accent / --focus)
--fz-edge#8a8680button outline — ≥ 3:1 on bg
--fz-edge-soft#bdbab8treeview header separators
--fz-hilight#fffffftop bevel
--fz-btnlinear-gradient(#fff → #f6f5f4 → #e3e1de)Clearlooks button fill (sampled + QS --fill)
--fz-btn-hovermix toward selected (light blue wash)gtkrc button PRELIGHT
--fz-btn-activelinear-gradient(#c9c7c4 → #dedcd9)shade(0.85, bg)
--fz-headerlight vertical gradient #fcfcfc → #e7e6e5treeview column header (sampled)
--fz-toolbar#f4f3f2 → #e4e3e2toolbar strip (sampled Nautilus/Clearlooks)
--fz-title#4f80c2 → #3f72b3 → #3767a6 → #2c5a97Metacity active title sampled at #7db6eb → #4c91d1 (FC5 Nautilus) and #9cbde5 → #7ba2d2 (Clearlooks 2.20); darkened ~25% so white bold title text is ≈4.5–7:1 across the text band (white on #86abd9 is only 2.4:1)
--fz-selectselected-hi → selectedtree selected row
--fz-track / --fz-fillgrey trough / blue candyprogress (QS --track / --track-fill)
InfoBar infobg #ffffbf edge #b8ad9dGtkInfoBar GTK 2.24 defaults
InfoBar warningbg #fcaf3e edge #b07a2bsame
InfoBar questionbg #c6d8ee edge #627bd9same (lightened fill for AA)
InfoBar errorbg #f6cdc9 edge #a82727lightened from GTK default #f03838 for AA with --fz-text
Status inksok #4e9a06 · fail #cc0000 · run #3465a4 · stop #555753 · int #ce5c00 · nothing #8f5902Tango
--fz-font"DejaVu Sans", "Bitstream Vera Sans", Verdana, sans-serifGNOME 2 default face
--fz-mono"DejaVu Sans Mono", …transcript
--fz-size13px (~ Sans 10 @ 96dpi)
--fz-radius3pxClearlooks buttons/tabs

Deliberate deviation: Clearlooks drew selected rows with white text on #86abd9 (2.4:1, fails AA). Here, selected and focused rows keep --fz-text on the selection gradient (5.3–7.2:1).

Contrast gates (must hold): body ≥ 7:1; link ≥ 4.5:1; muted text ≥ 4.5:1 on bg and stripe; button border ≥ 3:1; focus ring 2px solid --fz-focus with offset; :focus-visible on all interactive controls.

Out: framework utility classes, structural status selectors, system-ui 15px stack, card hover lift.


ComponentClass / patternNotes
Window frame.titlebarDecorative; icon + page title. Not interactive. Full-bleed, with no floating window or desktop wallpaper (that would be costume). Skip link (.skip) sits above it.
Toolbar.toolbar + .tbReload, Auto-refresh toggle (aria-pressed), Stop (running job), Jobs (detail). Real <button> / <a>.
Pathbar.pathbar + .btn-pathJob pages: freezer → repo → #id entry. Current crumb aria-current="page". Omit on index.
Treeview.tv tableSunken border, sticky header, rules-hint stripes, hover prelight, selected row uses --fz-select. Explicit .fx-status (or .st) class — never structural selectors.
Status icon+word.st / .st-{status}Icon + word; never colour alone. Words: running · succeeded · failed · stopped · interrupted · matched nothing (display only; underlying status stays succeeded for matched-nothing).
Progress.pbarPulse candy for running (respect prefers-reduced-motion).
Button.btn / .btn-sm / .btn-defaultGradient Clearlooks; default-button ring for the dialog primary.
InfoBar.infobar.{info,warning,question,error}Icon + primary + secondary + optional acts. role="alert" only for failed; running/notice use role="status".
Statusbar.statusbarPanes: summary · refresh note · clock.
Log text view.textviewSunken; max-height:min(50vh,400px); display:flex; flex-direction:column-reverse so it opens at the end; gutter line numbers + .l.err (lines starting error:) / .l.cmd (lines starting $ ) are P2 (need per-line spans and a --first counter offset); empty well for no-transcript cases.
Message dialog.dialog404 / 409 / 400 / 500.
IconsInline SVG <symbol> + <use>Original drawings in the Tango palette. Do not hotlink or vendor the Tango / Crystal icon files. Tango palette is public domain; note that in the CSS header. Size 16/20/24/48.

WhereCut / change
Index empty CTAShorten to one sentence + point at the repos treeview (no “Need help getting started?”).
Job danger alert for stoppedDo not use “The job did not succeed” for operator stop. Use question InfoBar: “You stopped this build.”
InterruptedWarning InfoBar: freezer restarted; commit not recorded; Build now CTA.
Matched nothingWarning InfoBar even though status is succeeded: “The glob matched nothing.” Cite the entry; point at .freezer.yaml.
FailedKeep exact string The job did not succeed. Secondary line: the raw job error.
Running cache/pathsReplace premature miss / 0 with decided when the job finishes / —.
Cache hit bodyKeep the explanation; render inside the empty text-view well, not a bare paragraph beside an empty pre.
404 hintDrop “deleted by retention” until retention exists; say the job is missing or never existed.
Stylesheet ADR citeEither land ADR 0022 or drop the cite.
Index durationNew column (FinishedAt−StartedAt); running shows elapsed + pulse.
Index resultTruncated job error or a short derived phrase (published N paths, cached — nothing built, nothing verified or published, stopped by the operator, freezer restarted mid-build).

StateTreatment
No jobsEmpty well in jobs treeview + Build now still usable
No reposEmpty well (boot normally rejects this; keep for honesty)
Never-built repoItalic “never built”; Latest = “no jobs”
RunningSpinner + pulse bar; Stop on row + toolbar; meta refresh 10s (job) / 15s (index if any running); Cache/Published pending
Succeeded (built)Green check; duration; published count
Succeeded + cache hitDistinct hit icon (check + badge); no transcript; empty well
Succeeded + matched nothingWarning InfoBar + “matched nothing” label; underlying status remains succeeded; 0 paths; cache “not probed”
FailedError InfoBar with The job did not succeed; transcript opens at end; red error lines
StoppedQuestion InfoBar (not danger); grey stop icon; Build now CTA
InterruptedWarning InfoBar; orange triangle; empty transcript well; no FinishedAt
Truncated log“Showing the last N of M lines.” + link to full plain text
404 / 409 / 400 / 500Dialog chrome; preserve status codes
Mobile ~390pxCollapse as §1
Feedback after Build now / StopInfoBar via ?notice=build&repo= / ?notice=stop&id= (see below)

Actions answer with 303 See Other and pages are not cached (Cache-Control: no-store):

  • POST /build/{repo} → 303 to /?notice=build&repo={name}
  • POST /jobs/{id}/stop → prefer 303 to /jobs/{id}?notice=stop (stay on the job); if / is kept, use /?notice=stop&id={id}
  • Index/job render an InfoBar when those query params are present; links “Dismiss” → same path without query.

No cookies, no CSRF (unchanged).

Group consecutive jobs that share (Repo, Commit) with a rowspan identity cell (repo icon + short SHA). No new schema. Event/batch id remains unavailable.

Default stays no-JS: <meta http-equiv="refresh"> on the running job page (10 s) and on the index only while a job runs (15 s). Add a visible toolbar toggle and statusbar note (“Reloads every 15 s while a job runs · Pause”) that links to the same URL with ?refresh=0, which suppresses the meta tag. Cost: a full reload resets scroll on long lists and makes screen readers re-read the page, which is why the running notice uses role="status" (not role="alert") and why Pause exists. An optional script of 20 lines or fewer could restore scroll position after reload (sessionStorage). Recommendation: don’t ship it in v1, because the page works without it and the product is no-JS.

Primary: CSS-only — wrap the transcript in a flex column-reverse scroller so the last line is initially visible. Secondary optional PE (≤20 lines): if #end is in the URL or the job failed, scrollIntoView once; document the tradeoff (AT focus jump, must be optional). Do not reverse the log text.


  1. One hand-written stylesheet carrying the Clearlooks tokens above; no CSS framework; AA contrast held for body, link, muted, borders, focus.
  2. Index shows repos treeview + jobs treeview; jobs grouped by (repo, commit) when contiguous; Duration and a Result column present.
  3. Status icons distinguish failed / stopped / interrupted / matched-nothing / cache-hit / running / succeeded; words remain; colour never alone.
  4. Matched-nothing jobs carry a warning InfoBar and do not read as ordinary success.
  5. Stopped jobs never use the failed danger copy; interrupted jobs explain restart and show an empty-well (no blank transcript).
  6. Failed jobs say exactly The job did not succeed; transcript has no per-line grey chips; initial scroll is at the end.
  7. Running jobs do not claim Cache miss / 0 paths; show a pulse progress affordance.
  8. Build now / Stop answer with 303; redirect carries a notice the next HTML render can show as an InfoBar.
  9. Live HTML pages are not cached (Cache-Control: no-store); a missing job is 404.
  10. Job page shows the full commit SHA and, when a transcript exists, a $ nix copy (or $ nix copy --to) substring.
  11. Mobile ≤640px: usable without horizontal overflow; Build now / Stop reachable; status word still present (in the secondary line if the icon column hides the word).
  12. Skip link to main content; :focus-visible rings; real submit buttons; status role="alert" only where appropriate.
  13. Icons are original SVG or public-domain Tango-palette drawings — no hotlinked icon themes.
  14. Stop redirects to /jobs/{id}?notice=stop and the job page shows the stopped InfoBar.
  15. Queued builds render as per-repo “queued” rows (not job rows); distinct status icon and the word “queued”.
  16. Job list pages (Newer/Older) with a fixed page size; grouping holds within a page.
  17. Transcript shows line numbers and tinted error lines with a non-colour marker; one text node per line.

  • Auth, CSRF, users/roles (ADR 0011).
  • Metrics, retries, store GC, retention UI (until retention exists).
  • Live streaming logs / HTMX / SPA / JS framework.
  • Dark mode.
  • Filters/search on the job list (paging only in v1).
  • Forging links into GitHub beyond showing owner/name.
  • Vendoring the Tango or Crystal icon theme files.
  • Changing nix / webhook / SQLite behaviour.

8. Decisions (Noa, 2026-10-08) and remaining open questions

Section titled “8. Decisions (Noa, 2026-10-08) and remaining open questions”

Decided:

  1. Stop redirect: Stop stays on the job page. POST /jobs/{id}/stop → 303 to /jobs/{id}?notice=stop.
  2. Auto-refresh: keep the no-JS <meta refresh> with the visible Pause link (?refresh=0). No scroll-restore script in v1.
  3. Queued builds: show them. Expose the build waiting for its turn per repo (read-only) so the index can show a “queued” row per repo (received time, plus the pushed sha if the event carries one; the head is still resolved through git at build start) above that repo’s running job. A queued row is not a job row — give it its own status icon and the word “queued”. Superseded events simply disappear (the slot holds only the newest).
  4. Paging: the job list gets paging. Keep 100 (or fewer) per page; add Newer / Older links in the jobs treeview footer and statusbar (/?page=N or ?before={id}; keyset on id preferred). Commit grouping applies within a page.
  5. Transcript line numbers + error tint: ship in v1. One element per line (≤2000); error lines (error: prefix, failed with exit code) get the red tint plus a non-colour marker (bold or ⚠ gutter) so colour isn’t the only cue. The plain-text log link stays.

Still open (mock’s defaults stand unless Noa says otherwise):

  1. Titlebar: keep the CSS Metacity-style title bar (default) or drop it.
  2. Icons: original Tango-palette SVGs (default) vs text-only marks.
  3. Index order: repositories above jobs (default) or jobs first.

  1. No JS required; progressive enhancement optional (scroll-to-end, scroll-restore after meta refresh) and must degrade cleanly.
  2. Icons as original SVGs in the Tango palette — do not vendor icon theme files.
  3. Open the mock while implementing: Design/prototypes/freezer-clearlooks.html (static HTML; fake data is fine).
  4. Detect matched-nothing as succeeded with no cache hit, zero paths, and a log that says the glob matched nothing (or store a flag later); do not invent a new persisted status without an ADR.