Files
ci/dashboard.css
T
zeekayandHanzo Dev b18038925e ci: take the design from @hanzo/brand instead of copying it
The dashboard carried its own :root block — a hand-copy of the house
palette, and being a copy it had already drifted off it. The status
colours were GitHub Primer's (#3fb950 / #f85149 / #d29922) where the
house says #10b981 / #ef4444 / #f59e0b. Every surface black was a shade
wrong and blue-tinted (#0b0b0d) against a scale that is strictly neutral
(--surface-0 #080808). The hairline border was an opaque #25252b where
the house hairline is a 6% white wash. The font stacks named neither
Geist Sans nor JetBrains Mono. Of nine colours exactly one — the accent
— still matched. That is what a second component source looks like a few
months in, and it is why this page had to stop being one.

The values now come from @hanzo/brand, and they arrive as that package's
own published artifact rather than as hex codes retyped here:
styles/variables.css, which it ships as a plain custom-property sheet
(exports["./styles/*"], documented for a bare <link>), vendored verbatim
and go:embed-ed. dashboard.css holds what is left over — layout: what is
a row, what sticks, what collapses on a phone — and names no colour,
radius or type size of its own.

Not a @hanzo/gui port, deliberately. gui is React over Tamagui and needs
a bundler, which would put npm and a JS build on the path that ships the
board you read when the builds are broken, and would trade one request
that returns the answer for a shell that fetches it a second later. What
this page ever needed from the design system was its token vocabulary,
not its components — and gui's own shell reads these same var(--hanzo-*)
names, so a Go binary and a React app now spend one vocabulary from one
source. go.mod stays empty; the image stays the binary and a CA bundle.

Two offline gates keep it honest, because vendoring alone would only
move the copy rather than end it:

  - TestBrandCSSIsUpstreamBytes pins the sheet to the sha256 of the
    version it claims to be. Without it, "just darken that one border"
    is a one-character local edit that silently rebuilds the second
    palette and nothing ever catches it. This is go.sum's argument.
  - TestDashboardCSSNamesNoColours fails on any hex or rgb() the page
    writes for itself. The old :root block did not arrive wrong; it
    arrived one reasonable exception at a time.

Neither gate touches the network, so proving we use one design system
costs the pipeline no npm and no registry.

TestRenderedPageShowsOnlyTheViewersOrg additionally pins at the HTML
layer what scope_test.go pins at the predicate layer: a lux viewer's
rendered page contains no other org's rows and no other org's name in
the nav. The renderer is where that leak lived, and it is now the
renderer that is asserted.

Rendered and checked in a browser, not just compiled: every token
resolves (surface-0 #080808, text-primary #fafafa, accent-muted #a78bfa,
success/error/warning #10b981/#ef4444/#f59e0b, hairline rgba(255,255,255,
.06), radius-card 8px, JetBrains Mono), color-scheme comes out dark via
the sheet's own .dark hook, and header, chips, nav, table and footer now
sit on one --space-6 gutter instead of the table drifting 12px left.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 17:00:53 -07:00

72 lines
4.3 KiB
CSS

/* dashboard.css — this page's own rules: what is a row, what sticks, what
collapses on a phone. It names no colour, radius or type size of its own;
every such value is a var() into @hanzo/brand (see brand.go). That is the
difference between consuming the design system and being a second copy of it,
and it is asserted, not merely intended — see TestDashboardCSSNamesNoColours. */
*{box-sizing:border-box}
body{margin:0;background:var(--surface-0);color:var(--text-primary);
font-family:var(--font-sans);font-size:var(--font-size-base);line-height:1.5}
header{display:flex;align-items:center;gap:var(--space-4);
padding:var(--space-4) var(--space-6);background:var(--surface-1);
border-bottom:1px solid var(--border-hairline)}
h1{margin:0;font-size:var(--font-size-lg);font-weight:600;letter-spacing:-.01em}
h1 span{color:var(--hanzo-accent-muted)}
.meta{margin-left:auto;color:var(--text-secondary);font-size:var(--font-size-sm);text-align:right}
.strip{display:flex;gap:var(--space-2);padding:var(--space-4) var(--space-6);flex-wrap:wrap}
.chip{padding:var(--space-2) var(--space-3);background:var(--surface-1);
border:1px solid var(--border-hairline);border-radius:var(--radius-card);
font-size:var(--font-size-sm);color:var(--text-secondary)}
.chip b{color:var(--text-primary);font-weight:600}
.chip.ok b{color:var(--success)} .chip.fail b{color:var(--error)}
.chip.run b{color:var(--warning)} .chip.cancel b{color:var(--text-muted)}
nav{display:flex;gap:var(--space-2);padding:0 var(--space-6) var(--space-4);flex-wrap:wrap}
nav a{padding:var(--space-1) var(--space-3);background:var(--surface-1);
border:1px solid var(--border-hairline);border-radius:var(--radius-full);
color:var(--text-secondary);text-decoration:none;font-size:var(--font-size-sm)}
/* --hanzo-accent-soft is the house "selected row" fill; the active tab is the
one place on this page that is a selection, so it is the one place it is used. */
nav a.on{border-color:var(--hanzo-accent);background:var(--hanzo-accent-soft);color:var(--text-primary)}
nav .who{margin-left:auto;align-self:center;color:var(--text-muted);font-size:var(--font-size-sm)}
/* The stale banner tints its own border colour rather than introducing an amber
of its own — @hanzo/brand ships no warning-surface token, and inventing one
here is exactly the drift this file exists to stop. */
.warn{margin:0 var(--space-6) var(--space-4);padding:var(--space-3) var(--space-4);
border:1px solid var(--warning);border-radius:var(--radius-card);
background:color-mix(in srgb, var(--warning) 10%, transparent);
color:var(--warning-light);font-size:var(--font-size-sm)}
table{width:100%;border-collapse:collapse}
th{position:sticky;top:0;background:var(--surface-1);text-align:left;
font-size:var(--font-size-xs);text-transform:uppercase;letter-spacing:.06em;
font-weight:600;color:var(--text-muted);padding:var(--space-2) var(--space-3);
border-bottom:1px solid var(--border-hairline-strong)}
td{padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--border-hairline);
vertical-align:top}
/* The table is full-bleed but its text has to sit on the same gutter as the
header, chips and nav above it, which are all --space-6 in. */
th:first-child,td:first-child{padding-left:var(--space-6)}
th:last-child,td:last-child{padding-right:var(--space-6)}
tr:hover td{background:var(--surface-2)}
a{color:inherit}
.dot{display:inline-block;width:8px;height:8px;border-radius:var(--radius-full);
margin-right:var(--space-2)}
.dot.success{background:var(--success)} .dot.failure{background:var(--error)}
.dot.running{background:var(--warning);animation:p 1.4s ease-in-out infinite}
.dot.cancelled{background:var(--text-disabled)}
@keyframes p{50%{opacity:.35}}
.repo{font-weight:600}
.org{color:var(--text-muted)}
.title{color:var(--text-secondary);max-width:42ch;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mono{font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--text-secondary)}
.empty{padding:var(--space-12) var(--space-6);text-align:center;color:var(--text-secondary)}
footer{padding:var(--space-4) var(--space-6);color:var(--text-muted);
font-size:var(--font-size-sm);border-top:1px solid var(--border-hairline)}
@media(max-width:760px){.hide-sm{display:none}}