From 3ffcf4314bdc477ca3623798e62d11cab66545a2 Mon Sep 17 00:00:00 2001 From: Simone Date: Sat, 25 Jul 2026 19:35:36 +0200 Subject: [PATCH] style(web): filled buttons, bolder icons, table/row alignment fixes, brand icon - Two filled button kinds, no neutral/outline third state: every action is either "normal" (#4382c2) or destructive (#cb4b4c); border is the same neutral gray as every table/card border, not the fill colour. - Icons: bolder stroke site-wide (2 -> 2.5) and +1px, except inside buttons (2.25, no size bump) where a bold icon next to now-unbolded button text was too heavy. - tbody row hover: flat gray -> a transparent wash of the link/accent blue. - The Actions in Services/Templates was itself a flex container; some engines size that box's content slightly differently from a plain-text sibling cell, which showed up as the row's collapsed bottom border sitting 1px higher under Actions than its neighbours. The flex row now lives on an inner
instead, with its
children pinned to content width (a bare is display:flex by the site's own rule, which was letting one form claim the whole row and push the next to wrap). - Navbar brand gets an icon (a server rack -- this dashboard orchestrates hosts/VMs, not generic settings). --- src/ci_orchestrator/web/static/app.css | 238 +++++++++++------- src/ci_orchestrator/web/templates/base.html | 2 +- .../web/templates/partials/services.html | 42 ++-- 3 files changed, 164 insertions(+), 118 deletions(-) diff --git a/src/ci_orchestrator/web/static/app.css b/src/ci_orchestrator/web/static/app.css index f1b2236..869097f 100644 --- a/src/ci_orchestrator/web/static/app.css +++ b/src/ci_orchestrator/web/static/app.css @@ -91,10 +91,16 @@ dd code, dd .mono { overflow-wrap: anywhere; } } .navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.4rem; color: var(--text); font-weight: 600; font-size: 1.05rem; } +/* Gear: automation/pipeline, fits an orchestrator's brand mark better than a + * subject-specific glyph (server/box/etc. already belong to their own card). */ +.navbar-brand .icon { width: 1.3em; height: 1.3em; } .navbar-brand:hover { color: var(--accent-hover); text-decoration: none; } .navbar-links { @@ -120,63 +126,16 @@ dd code, dd .mono { overflow-wrap: anywhere; } #action-slot:empty { display: none; } #action-slot:not(:empty) { margin-bottom: 1.25rem; } -/* ── status header (A3) ──────────────────────────────────────────────── */ +/* ── resource gauges inside cards (Stage F) ──────────────────────────── */ -.status-header { - display: grid; - /* Six tiles (VMs, Runners, CPU, RAM, Disk, Cache) -- a smaller min track - * lets all six sit on one row on a wide screen, wrapping gracefully below. */ - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 1rem; - margin-bottom: 1.25rem; -} +/* The Host card's CPU / RAM / disk readouts. These lived in the retired + * status-header tiles; the `tile-*` level class names survive as what the + * gauge fill colour keys off. Stage G dropped the level word + check-icon + * strip that used to follow each gauge: the state is stated as a number + * ("42%", "20.0 / 62 GB") right above the bar, and the level word moved into + * the gauge's aria-label -- so no reading of this card depends on colour. */ -.stat-tile { - display: flex; - align-items: flex-start; - gap: 0.75rem; - background: var(--panel); - border: 1px solid var(--border); - border-left: 3px solid var(--border); - border-radius: var(--radius); - padding: 0.9rem 1rem; -} - -.stat-tile > .icon { flex-shrink: 0; margin-top: 0.15rem; color: var(--text-dim); } - -.stat-tile-body { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; flex: 1; } - -.stat-tile-label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.03em; - color: var(--text-dim); -} - -.stat-tile-value { font-size: 1.15rem; font-weight: 600; color: var(--text); } -.stat-tile-sub { font-size: 0.8rem; font-weight: 400; color: var(--warn); } - -.stat-tile-status { - display: inline-flex; - align-items: center; - gap: 0.3rem; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -/* Health must never rely on color alone -- the border accent + icon + - * label all move together, so a color-blind viewer still reads the state - * from the text/icon. */ -.tile-ok { border-left-color: var(--success); } -.tile-ok .stat-tile-status { color: var(--success); } -.tile-warn { border-left-color: var(--warn); } -.tile-warn .stat-tile-status { color: var(--warn); } -.tile-bad { border-left-color: var(--danger); } -.tile-bad .stat-tile-status { color: var(--danger); } -.tile-unknown { border-left-color: var(--border); } -.tile-unknown .stat-tile-status { color: var(--text-dim); } +.resource-value { display: block; font-variant-numeric: tabular-nums; } .gauge { width: 100%; @@ -184,24 +143,30 @@ dd code, dd .mono { overflow-wrap: anywhere; } background: var(--panel-2); border-radius: 999px; overflow: hidden; - margin: 0.15rem 0; + margin: 0.25rem 0; } .gauge-fill { height: 100%; background: var(--success); } .tile-warn .gauge-fill { background: var(--warn); } -.tile-bad .gauge-fill { background: var(--danger); } +.tile-bad .gauge-fill { background: var(--danger); } + +/* Sub-line under a gauge (Host: the load average under CPU), same role as the + * dim "GB free" beside the disk percentage. */ +.resource-sub { + display: block; + font-size: 0.78rem; + font-variant-numeric: tabular-nums; +} .dashboard-grid { display: grid; /* FIXED four-column track, NOT auto-fit. A `.card--wide` spans `1 / -1`, * i.e. occupies every column on its row -- with `auto-fit` that means no - * track is ever globally empty, so auto-fit can't collapse the compact - * row's spare tracks and the four info cards stayed narrow with dead - * columns on the right. A fixed count makes the four compact cards fill - * their row exactly, and the wide tables still span all four via - * `grid-column: 1 / -1`. minmax(0,1fr) lets a card shrink below its - * content width so a wide table scrolls inside its own .table-wrap - * instead of forcing the page to scroll. Column count steps down with - * width (below). */ + * track is ever globally empty, so auto-fit could not collapse a compact + * row's spare tracks. Since Stage F every card is `.card--wide` (the four + * compact info cards became the one Platform card), so the track count only + * matters if a compact card is ever added back. minmax(0,1fr) lets a card + * shrink below its content width so a wide table scrolls inside its own + * .table-wrap instead of forcing the page to scroll. */ grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.25rem; /* stretch: every card in a row shares the row's height, so the grid reads @@ -285,6 +250,19 @@ dd code, dd .mono { overflow-wrap: anywhere; } * the bar's right end. Compact and case-normal so it reads as a control rather * than part of the uppercase heading text, and short enough not to grow the * bar's height. */ +/* Right-hand end of a card header bar: a single near-static fact that would + * otherwise cost a whole
row (Host: uptime). `text-transform` must be + * reset -- the h2 uppercases, and "26H 03M" is not a duration anyone reads. */ +.head-meta { + margin-left: auto; + font-size: 0.8rem; + font-weight: 400; + text-transform: none; + letter-spacing: normal; + font-variant-numeric: tabular-nums; + color: var(--text-dim); +} + .head-select { margin-left: auto; padding: 0.1rem 0.35rem; @@ -295,7 +273,10 @@ dd code, dd .mono { overflow-wrap: anywhere; } color: var(--text); } -/* Per-template expandable backup list (Templates panel). */ +/* Per-template expandable backup list (Templates panel). Each row is its own + * bordered line (not just a wrapping flex list run into the next) so a + * multi-backup template reads as a small table, not a paragraph the delete + * button happened to float in. */ .backups-details > summary { cursor: pointer; color: var(--link); @@ -306,14 +287,19 @@ dd code, dd .mono { overflow-wrap: anywhere; } .backups-list { list-style: none; margin: 0; padding: 0; } .backups-list li { display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-items: baseline; - padding: 0.2rem 0; - font-size: 0.82rem; + align-items: center; + gap: 0.75rem; + padding: 0.45rem 0; + font-size: 0.85rem; + border-top: 1px solid var(--border); } +.backups-list li:first-child { border-top: none; } +/* Name + size/date as one shrinkable unit, so the row wraps as a block + * instead of the delete button drifting away from a name that wrapped. */ +.backup-info { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem; min-width: 0; flex: 1 1 auto; } +.backup-info code { overflow-wrap: anywhere; } /* Per-backup delete button pinned to the right end of its row. */ -.backup-del { margin-left: auto; align-self: center; } +.backup-del { margin-left: auto; flex: none; } .card h2 { display: flex; @@ -339,7 +325,9 @@ dd code, dd .mono { overflow-wrap: anywhere; } dl { display: grid; grid-template-columns: auto 1fr; gap: 0 0.9rem; } dt, dd { padding: 0.45rem 0; border-bottom: 1px solid var(--border); } dt { color: var(--text-dim); font-weight: 500; } -dd { margin: 0; } +/* Long unbreakable values (a cache root, a kernel string, a CPU model) + * wrap inside the card rather than widening its grid track. */ +dd { margin: 0; overflow-wrap: anywhere; } /* ── tables ───────────────────────────────────────────────────────────── */ @@ -347,7 +335,13 @@ dd { margin: 0; } table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } -th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); } +/* vertical-align:middle is explicit, not left to the UA default (baseline) -- + * keeps every cell in a row anchored the same way regardless of its content + * (a row of buttons vs. one line of text). The Actions-column border step + * this was chasing turned out to be .actions-row's flex box sitting directly + * on the (see the comment there); this rule stays for alignment, not as + * that fix. */ +th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); vertical-align: middle; } th { color: var(--text-dim); @@ -358,7 +352,7 @@ th { } tbody tr { transition: background-color 120ms ease; } -tbody tr:hover { background: var(--panel-2); } +tbody tr:hover { background: rgba(65, 131, 196, 0.12); } tr.row-orphan td { color: var(--warn); } tr.row-alert td { color: var(--danger); } @@ -396,8 +390,37 @@ tr.row-alert td { color: var(--danger); } .badge-info { background: rgba(65, 131, 196, 0.15); color: var(--accent); border: 1px solid rgba(65, 131, 196, 0.4); } .badge-muted { background: var(--panel-2); color: var(--text-dim); border: 1px solid var(--border); } +/* A badge sitting right before a dim sub-state ("loaded" + "kernel module", + * "active" + "running") only had the markup's one collapsed whitespace + * character between them -- reads as glued together. */ +.badge + .text-dim { margin-left: 0.4rem; } + +/* VMware/Runners (Stage H): "loaded"/"not loaded", "active"/"down" are + * different lengths, so the pill in each
row was a different width from + * its neighbours. Levelled to fit the longest word in either card, same + * technique as the Executions Status column. */ +#vmware-card .badge, +#runner-card .badge { + min-width: 5rem; + justify-content: flex-start; +} + +/* Executions card: the Status column mixes run outcomes (success/failure/ + * queued/cancelled...) and VM-only states (orphan/running) of very different + * word lengths -- a ragged column of differently-sized pills. Both row kinds + * put their badge in the 8th (Run, Repo, Workflow, Branch, Commit, + * Trigger, Actor, Status, ...), so one rule levels every pill in the column + * to "cancelled"'s width (the longest word) with room to spare, and + * `justify-content` keeps icon+text hugging the left edge of the now-wider + * pill rather than centering. */ +#executions-card td:nth-child(8) { width: 5rem; } +#executions-card td:nth-child(8) .badge { width: 100%; justify-content: flex-start; } + /* ── buttons ──────────────────────────────────────────────────────────── */ +/* Two filled button kinds, no neutral/outline third state: every action is + * either "normal" (blue) or destructive (red) -- the confirm gate is still + * the real safeguard on a destructive action, this is just legibility. */ .btn { display: inline-flex; align-items: center; @@ -406,26 +429,31 @@ tr.row-alert td { color: var(--danger); } border-radius: var(--radius); padding: 0.45rem 0.9rem; font-size: 0.875rem; - font-weight: 600; + font-weight: 400; font-family: inherit; cursor: pointer; - background: var(--panel-2); - color: var(--text); + background: #4382c2; + color: #ffffff; + /* Same neutral gray as every table/card border, not the fill colour -- + * a solid-colour border read as an outline duplicating the fill. */ + border-color: var(--border); transition: background-color 120ms ease, border-color 120ms ease, transform 80ms ease; } -.btn:hover { background: var(--border); } +/* Hover tint: +25 per channel, same lighten-by magnitude as --accent-hover. + * Border stays the neutral gray -- only the fill shifts on hover. */ +.btn:hover { background: #5c9bdb; } .btn:active { transform: translateY(1px); } -.btn-primary { background: var(--accent); color: #ffffff; border-color: var(--accent); } -.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } +/* A plain-weight button label leaves its icon looking thin/washed-out next + * to a solid fill -- bumped stroke-width and a couple px of extra size make + * it read as clearly as the bold text used to. */ +.btn .icon { stroke-width: 2.25; width: 1.15em; height: 1.15em; } -/* Destructive actions: a NEUTRAL button at rest (same panel surface + border - * as .btn) that only reads as red through its text/icon -- no red border or - * fill glaring on the page. Danger becomes fully visible on hover (red border - * + soft red wash). The real safeguard is the two-step confirm gate, not the - * colour. */ -.btn-danger { background: var(--panel-2); color: var(--danger); border-color: var(--border); } -.btn-danger:hover { background: rgba(248, 81, 73, 0.14); color: var(--danger); border-color: var(--danger); } +.btn-primary { background: #4382c2; color: #ffffff; border-color: var(--border); } +.btn-primary:hover { background: #5c9bdb; } + +.btn-danger { background: #cb4b4c; color: #ffffff; border-color: var(--border); } +.btn-danger:hover { background: #e46465; } /* Compact variant for buttons that live inside a table row (VM remove, * per-service start/stop/restart) so a row of them stays low-profile. */ @@ -458,8 +486,17 @@ input[type="text"], input[type="number"], select { /* Multiple one-button action forms side-by-side in one table cell (Stage D * services panel: Start/Stop/Restart). Overrides the default form's * margin-top so the row stays compact inside a . */ -.service-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; } -.service-actions form { margin-top: 0; } +/* Wraps the row of action buttons INSIDE the td rather than making the td + * itself a flex box -- a table-cell that is also a flex container computes + * its content-box height slightly differently from a plain-text sibling + * cell in some engines, which showed up as the row's collapsed bottom + * border sitting 1px higher under this column than its neighbours. */ +.actions-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; } +/* Each is itself display:flex (global form rule) -- as a flex item of + * .actions-row that can size to fill the row instead of its own content, + * pushing the next form onto a new line. flex:none pins it to content width + * so multiple forms actually sit side by side. */ +.actions-row form { margin-top: 0; flex: none; } /* Muted inline text (e.g. a relative-time suffix next to a file name). */ .text-dim { color: var(--text-dim); } @@ -600,18 +637,26 @@ input[type="text"], input[type="number"], select { * references render. currentColor lets every icon pick up whatever text * color its container already has -- no per-icon color rules needed. */ .icon { - width: 1em; - height: 1em; + /* calc(...+1px) rather than a bigger em multiplier: a flat 1px bump reads + * the same at every context size (a 0.75rem badge icon and a 15px body + * icon both just gain one pixel), where a fixed em delta would swing by + * context. .btn .icon overrides this with a plain em value -- no +1px -- + * so buttons are unaffected. */ + width: calc(1em + 1px); + height: calc(1em + 1px); vertical-align: -0.15em; fill: none; stroke: currentColor; + /* Bolder than the sprite's own stroke-width="2" attribute -- CSS wins over + * the SVG presentation attribute. Button icons dial this back down, see + * .btn .icon below. */ + stroke-width: 2.5; flex-shrink: 0; } -.card h2 .icon, -.stat-tile > .icon { - width: 1.1em; - height: 1.1em; +.card h2 .icon { + width: calc(1.1em + 1px); + height: calc(1.1em + 1px); } /* ── accessibility ────────────────────────────────────────────────────── */ @@ -637,5 +682,4 @@ input[type="text"], input[type="number"], select { .navbar-inner { flex-direction: column; align-items: flex-start; gap: 0.5rem; } form { flex-direction: column; align-items: flex-start; } dl { grid-template-columns: 1fr; } - .status-header { grid-template-columns: 1fr; } } diff --git a/src/ci_orchestrator/web/templates/base.html b/src/ci_orchestrator/web/templates/base.html index 78304ef..724430b 100644 --- a/src/ci_orchestrator/web/templates/base.html +++ b/src/ci_orchestrator/web/templates/base.html @@ -13,7 +13,7 @@ {% set logged_in_user = request.session.get("user") %}