From 65411d3ce8915c89de5234e3d004a7f5ba0217ba Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 6 Oct 2026 14:18:49 -0400 Subject: [PATCH] feat(ui): base stylesheet on the Steward design-system tokens; readable status colours (#5092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5092 "Tokens". base.html's inline palette was its own: white text on the gold button (1.81:1), a red at 3.26:1 on a card, a warning yellow at hue 42 beside the gold accent at 46, a lavender page title, serif labels at 13px and weights up to 700. - static/theme.css is design system 6's generated sheet (FabledSword + the Steward accent). static/steward.css is base.html's stylesheet moved out, with :root now aliases onto those tokens (--bg, --text-muted, --green…) so every template keeps rendering; it states no colour of its own. - Status text colours are Steward overrides recorded in the design system (tokens 182-184): success #5FB87A, warning #E08A3C, error #EC7A6C. The house -fg mixes read "up" as grey hint text (dE 8.2) and warning as the accent (dE 11.5); these are >= 4.5:1 on page, card, hover and their own 15-25% tints. --orange is now the midpoint of warning and error. - Primary button and active range button: dark text on gold (7.82:1). Danger button on the house destructive fill. Badges and alerts take the -fg colour on a tint; the gold badge uses --fs-accent-fg. - Type: Inter body, Fraunces only for page titles and h1/h2, JetBrains Mono for code; section and widget titles become the tiny caps label in the body face; weights capped at 500. - Removed: the lavender title and share-view gradient, #606080, and the undefined --card-bg, --accent-faint, --accent-bg, --bg-alt, --border-low and --row-alt (now real tokens); the dead settings/index.html. - Guards (tests/test_style_tokens.py): every var(--x) used is defined; steward.css states no colour or weight over 500; hex colours in templates may only go down (ceiling 75, from 81 before this change), each with a self-test that it can fail. Co-Authored-By: Claude Opus 5.5 --- .../host_agent/templates/settings_list.html | 2 +- plugins/snmp/templates/snmp/index.html | 2 +- plugins/snmp/templates/snmp/widget.html | 2 +- plugins/traefik/templates/traefik/rows.html | 4 +- .../traefik/templates/traefik/traffic.html | 2 +- plugins/unifi/templates/unifi/events.html | 2 +- plugins/unifi/templates/unifi/rows.html | 2 +- steward/static/steward.css | 275 ++++++++++++++++++ steward/static/theme.css | 176 +++++++++++ steward/templates/ansible/browse.html | 2 +- steward/templates/ansible/run_started.html | 2 +- steward/templates/base.html | 266 +---------------- steward/templates/dashboard/share_view.html | 2 +- .../templates/settings/_ansible_sources.html | 2 +- steward/templates/settings/index.html | 157 ---------- tests/test_style_tokens.py | 73 +++++ 16 files changed, 538 insertions(+), 433 deletions(-) create mode 100644 steward/static/steward.css create mode 100644 steward/static/theme.css delete mode 100644 steward/templates/settings/index.html create mode 100644 tests/test_style_tokens.py diff --git a/plugins/host_agent/templates/settings_list.html b/plugins/host_agent/templates/settings_list.html index fd429a7..6c2ad0d 100644 --- a/plugins/host_agent/templates/settings_list.html +++ b/plugins/host_agent/templates/settings_list.html @@ -12,7 +12,7 @@

{% if new_token %} -
+

New token — copy this install command now

This is the only time the raw token is shown. Rotate if you lose it. diff --git a/plugins/snmp/templates/snmp/index.html b/plugins/snmp/templates/snmp/index.html index ef5979c..c379c5a 100644 --- a/plugins/snmp/templates/snmp/index.html +++ b/plugins/snmp/templates/snmp/index.html @@ -31,7 +31,7 @@ {% for oid_cfg in device.oids %} {% set label = oid_cfg.label if oid_cfg.label else oid_cfg.oid %} {% set val = device.readings.get(label) %} -

+
{{ label }}
{% if val is not none %}
diff --git a/plugins/snmp/templates/snmp/widget.html b/plugins/snmp/templates/snmp/widget.html index fc4d593..2fd0204 100644 --- a/plugins/snmp/templates/snmp/widget.html +++ b/plugins/snmp/templates/snmp/widget.html @@ -36,7 +36,7 @@ {% endif %}
{% if not loop.last %} -
+
{% endif %} {% endfor %} {% if total > devices|length %} diff --git a/plugins/traefik/templates/traefik/rows.html b/plugins/traefik/templates/traefik/rows.html index c02b962..a8aee7f 100644 --- a/plugins/traefik/templates/traefik/rows.html +++ b/plugins/traefik/templates/traefik/rows.html @@ -2,7 +2,7 @@ {# ── Global stats bar ──────────────────────────────────────────────────────── #} {% if global_stat %} -
+
{% if global_stat.open_conns_total is not none %} @@ -122,6 +122,6 @@
{% else %}
-

No Traefik metrics yet. Configure metrics_url in config.yaml under plugins.traefik.

+

No Traefik metrics yet. Configure metrics_url in config.yaml under plugins.traefik.

{% endif %} diff --git a/plugins/traefik/templates/traefik/traffic.html b/plugins/traefik/templates/traefik/traffic.html index dcdcb96..73b48e4 100644 --- a/plugins/traefik/templates/traefik/traffic.html +++ b/plugins/traefik/templates/traefik/traffic.html @@ -2,7 +2,7 @@ {% if summary %} {# ── Overview bar ─────────────────────────────────────────────────────────── #} -
+
Total {{ summary.total }} diff --git a/plugins/unifi/templates/unifi/events.html b/plugins/unifi/templates/unifi/events.html index 79c4a21..653b89b 100644 --- a/plugins/unifi/templates/unifi/events.html +++ b/plugins/unifi/templates/unifi/events.html @@ -23,7 +23,7 @@ Message {% for ev in events %} - + {{ ev.event_time.strftime("%Y-%m-%d %H:%M:%S") }} diff --git a/plugins/unifi/templates/unifi/rows.html b/plugins/unifi/templates/unifi/rows.html index 9de46ce..93a8914 100644 --- a/plugins/unifi/templates/unifi/rows.html +++ b/plugins/unifi/templates/unifi/rows.html @@ -2,7 +2,7 @@ {# ── WAN health bar ───────────────────────────────────────────────────────── #} {% if latest_wan %} -
+
WAN diff --git a/steward/static/steward.css b/steward/static/steward.css new file mode 100644 index 0000000..0623587 --- /dev/null +++ b/steward/static/steward.css @@ -0,0 +1,275 @@ +/* steward/static/steward.css — Steward's base stylesheet. + * + * Values come from theme.css, the stylesheet design system 6 ("Steward", + * inheriting FabledSword) generates. The short names below are Steward's + * aliases onto those tokens, kept so templates written against them still + * render; new work writes the --fs-* token. Contrast measured 2026-10-06 + * (#5092): every text colour here is at least 4.5:1 on page, card, hover + * and its own 15-25% tint. + */ +:root { + --bg: var(--fs-surface-page); + --bg-card: var(--fs-surface-raised); + --bg-elevated: var(--fs-surface-hover); + --border: color-mix(in srgb, var(--fs-border-color) 70%, var(--fs-surface-raised)); + --border-mid: var(--fs-border-color); + --text: var(--fs-text-primary); + --text-muted: var(--fs-text-secondary); + --text-dim: var(--fs-text-tertiary); + --gold: var(--fs-accent); + --gold-hover: color-mix(in srgb, var(--fs-accent) 85%, var(--fs-text-primary)); + --gold-dim: var(--fs-accent-soft); + --accent: var(--fs-accent); + --accent-hover: var(--gold-hover); + /* Status: the -fg colour is the text and indicator; the -dim tint sits behind it. */ + --green: var(--fs-success-fg); + --green-dim: color-mix(in srgb, var(--fs-success) 20%, transparent); + --yellow: var(--fs-warning-fg); + --yellow-dim: color-mix(in srgb, var(--fs-warning) 20%, transparent); + /* Between warning and error: the worse-than-warning band (slow, degraded). */ + --orange: color-mix(in srgb, var(--fs-warning-fg) 50%, var(--fs-error-fg)); + --orange-dim: color-mix(in srgb, var(--fs-warning) 20%, transparent); + --red: var(--fs-error-fg); + --red-dim: color-mix(in srgb, var(--fs-error) 20%, transparent); + --font-serif: var(--fs-font-display); +} +* { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: var(--fs-font-body); background: var(--bg); color: var(--text); line-height: var(--fs-leading-body); } +a { color: var(--accent); text-decoration: none; } +a:hover { color: var(--accent-hover); } +code { font-family: var(--fs-font-mono); font-size: 0.875em; background: var(--bg-elevated); padding: 0.1em 0.35em; border-radius: 3px; } + +#candle-glow { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background: + radial-gradient(ellipse 80% 60% at 50% 40%, color-mix(in srgb, var(--fs-accent) 4.5%, transparent) 0%, transparent 70%), + radial-gradient(ellipse 50% 40% at 20% 80%, color-mix(in srgb, var(--fs-accent) 2.5%, transparent) 0%, transparent 65%); +} + +/* ── App shell: persistent left sidebar + content column ───────────────────── */ +.app-shell { display: flex; min-height: 100vh; position: relative; z-index: 1; } +.sidebar { + width: 220px; flex-shrink: 0; background: var(--bg-card); + border-right: 1px solid var(--border-mid); + display: flex; flex-direction: column; + position: sticky; top: 0; height: 100vh; z-index: 20; +} +.sidebar .brand { + font-family: var(--font-serif); font-weight: 500; font-size: 1.15rem; + letter-spacing: 0.01em; color: var(--gold); text-decoration: none; + display: flex; align-items: center; gap: 0.5rem; + padding: 0.85rem 1.25rem; border-bottom: 1px solid var(--border); +} +.sidebar .brand svg { flex-shrink: 0; } +/* Only the nav list scrolls when long — the brand and user/logout stay pinned. */ +.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 0.75rem 0; } +.nav-group { margin-bottom: 0.9rem; } +.nav-group-label { + font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em; + color: var(--text-dim); font-weight: 500; padding: 0 1.25rem; margin-bottom: 0.3rem; +} +.side-nav a { + display: flex; align-items: center; gap: 0.5rem; + padding: 0.4rem 1.25rem; font-size: 0.9rem; color: var(--text-muted); + border-left: 2px solid transparent; text-decoration: none; + transition: color .12s, background .12s, border-color .12s; +} +.side-nav a:hover { color: var(--text); background: var(--bg-elevated); } +.side-nav a.active { + color: var(--gold); border-left-color: var(--gold); + background: color-mix(in srgb, var(--gold) 8%, transparent); +} +.side-user { border-top: 1px solid var(--border); padding: 0.7rem 1.25rem; } +.side-user form { margin: 0; } +.side-user button { + color: var(--text-muted); font-size: 0.84rem; text-decoration: none; + display: flex; align-items: center; gap: 0.4rem; + background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer; +} +.side-user button:hover { color: var(--text); } +/* Keyboard focus: a clear inset ring on every interactive sidebar element. */ +.sidebar .brand:focus-visible, +.side-nav a:focus-visible, +.side-user button:focus-visible { + outline: 2px solid var(--accent); outline-offset: -2px; color: var(--text); +} +.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; } +.topbar { display: none; } /* slim mobile bar; shown only under 900px */ +.nav-scrim { display: none; } + +/* Layout */ +main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; width: 100%; box-sizing: border-box; position: relative; z-index: 1; } + +/* Responsive: sidebar slides off-canvas under 900px, toggled by ☰ */ +@media (max-width: 900px) { + .sidebar { + position: fixed; left: 0; top: 0; bottom: 0; height: 100%; + transform: translateX(-100%); transition: transform .2s ease; + box-shadow: var(--fs-shadow-3); + } + body.nav-open .sidebar { transform: translateX(0); } + .topbar { + display: flex; align-items: center; gap: 0.75rem; height: 48px; padding: 0 1rem; + background: var(--bg-card); border-bottom: 1px solid var(--border-mid); + position: sticky; top: 0; z-index: 15; + } + .sidebar-toggle { + background: none; border: none; color: var(--text-muted); font-size: 1.3rem; + cursor: pointer; line-height: 1; padding: 0.25rem; border-radius: 4px; + } + .sidebar-toggle:hover { color: var(--text); } + .sidebar-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } + .topbar .brand-sm { + font-family: var(--font-serif); color: var(--gold); font-weight: 500; + font-size: 1.15rem; text-decoration: none; + } + body.nav-open .nav-scrim { + display: block; position: fixed; inset: 0; background: var(--fs-overlay); z-index: 18; + } +} + +@media (prefers-reduced-motion: reduce) { + .sidebar { transition: none; } + .side-nav a { transition: none; } +} + +/* Alerts */ +.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; } +.alert-error { background: var(--red-dim); border: 1px solid color-mix(in srgb, var(--fs-error) 45%, transparent); color: var(--red); } +.alert-success { background: var(--green-dim); border: 1px solid color-mix(in srgb, var(--fs-success) 60%, transparent); color: var(--green); } + +/* Cards */ +.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; } +.card-flush { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 1rem; } + +/* Typography */ +.page-title { font-family: var(--font-serif); font-size: 1.6rem; font-weight: 500; color: var(--text); line-height: var(--fs-leading-heading); margin-bottom: 1.5rem; } +/* The tiny micro-label: body face (display face only at 18px and up), caps, tracked. */ +.section-title { font-family: var(--fs-font-body); font-size: var(--fs-size-tiny); font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); } +h1, h2 { font-family: var(--font-serif); font-weight: 500; } +h3, h4 { font-family: var(--fs-font-body); font-weight: 500; } +strong, b, th { font-weight: 500; } + +/* Tables */ +.table { width: 100%; border-collapse: collapse; } +.table th { text-align: left; padding: 0.6rem 1rem; color: var(--text-muted); font-weight: 500; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border-mid); } +.table td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; } +.table tbody tr:last-child td { border-bottom: none; } +.table tbody tr:hover td { background: var(--bg-elevated); } +.table .td-actions { text-align: right; white-space: nowrap; } + +/* Forms */ +.form-group { margin-bottom: 1rem; } +label { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--text-muted); font-family: var(--fs-font-body); } +input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea { + width: 100%; padding: 0.5rem 0.75rem; background: var(--bg); border: 1px solid var(--border-mid); + border-radius: 5px; color: var(--text); font-size: 0.9rem; font-family: inherit; + transition: border-color .15s; +} +input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); } +select { cursor: pointer; } +textarea { resize: vertical; } + +/* Buttons */ +.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 1rem; background: var(--accent); color: var(--fs-surface-page); border: none; border-radius: 5px; cursor: pointer; font-size: 0.875rem; font-family: inherit; font-weight: 500; transition: background .15s; text-decoration: none; } +.btn:hover { background: var(--accent-hover); color: var(--fs-surface-page); } +.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.8rem; } +.btn-ghost { background: transparent; color: var(--text-muted); border: 1px solid var(--border-mid); } +.btn-ghost:hover { background: var(--bg-elevated); color: var(--text); } +.btn-danger { background: var(--fs-action-destructive); color: var(--fs-text-on-action); } +.btn-danger:hover { background: var(--fs-action-destructive-hover); color: var(--fs-text-on-action); } +.btn-warn { background: var(--yellow-dim); color: var(--yellow); border: 1px solid color-mix(in srgb, var(--fs-warning) 60%, transparent); } +.btn-warn:hover { background: color-mix(in srgb, var(--fs-warning) 32%, transparent); } + +/* Badges / Status */ +.badge { display: inline-block; padding: 0.2em 0.55em; border-radius: var(--fs-radius-sm); font-size: 0.75rem; font-weight: 500; letter-spacing: 0.02em; } +.badge-green { background: var(--green-dim); color: var(--green); } +.badge-red { background: var(--red-dim); color: var(--red); } +.badge-yellow { background: var(--yellow-dim); color: var(--yellow); } +.badge-orange { background: var(--orange-dim); color: var(--orange); } +.badge-gold { background: var(--gold-dim); color: var(--fs-accent-fg); } +.badge-dim { background: var(--bg-elevated); color: var(--text-muted); } +.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.dot-up { background: var(--green); box-shadow: 0 0 4px var(--green); } +.dot-down { background: var(--red); } +.dot-warn { background: var(--yellow); } +.dot-dim { background: var(--text-dim); } + +/* Empty state */ +.empty { color: var(--text-muted); font-size: 0.9rem; padding: 2rem; text-align: center; } + +/* Breadcrumbs */ +/* Breadcrumb renders as a kicker line tucked directly above the page header + (.page-title), so nested and top-level views share one consistent header. */ +.breadcrumb { display:flex; align-items:center; flex-wrap:wrap; gap:0.35rem; font-size:0.78rem; color:var(--text-dim); margin:0 0 0.45rem; } +.breadcrumb a { color:var(--text-muted); } +.breadcrumb a:hover { color:var(--text); } +.breadcrumb-sep { color:var(--border-mid); } +.breadcrumb-cur { color:var(--text-muted); } + +/* Stat cards (dashboard) */ +.stat-val { font-size: 2rem; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; } +.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-left: 0.3rem; } + +/* Ping pills */ +.ping-card { padding: 0.75rem 1.25rem; } +.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid var(--border); } +.ping-row:last-child { border-bottom:none; } +.ping-meta { min-width:120px; max-width:180px; flex-shrink:1; overflow:hidden; } +.ping-name { font-size:0.875rem; font-weight:500; display:block; overflow-wrap:anywhere; } +a.ping-name:hover { text-decoration:underline; } +/* Dashboard widget grid — Gridstack-rendered. Static (positioned but inert) in + view mode; the dashboard itself becomes the edit surface when Edit is toggled, + so drag-resize happens over live widget data. The bottom drawer is a fixed + overlay, so the dashboard width is identical entering/leaving edit mode. */ +.grid-stack-item-content { + background:var(--bg-card); border:1px solid var(--border-mid); border-radius:8px; + padding:0.8rem 1rem; display:flex; flex-direction:column; overflow:hidden; +} +.grid-stack-item-content .dash-cell-body { flex:1; min-height:0; overflow:auto; } +/* The widget body is a query container, so fragments can restyle to their OWN + panel width (not the viewport) — widgets adapt as you resize them. */ +.dash-cell-body { container-type:inline-size; } +/* Host-list widgets flow into columns as the panel widens: use the width, drop + the vertical deadspace; stay single-column (and untruncated) when narrow. */ +.host-blocks { display:grid; grid-template-columns:1fr; gap:0 1.5rem; } +@container (min-width:520px) { .host-blocks { grid-template-columns:1fr 1fr; } } +@container (min-width:900px) { .host-blocks { grid-template-columns:1fr 1fr 1fr; } } +.panel-chrome { display:none; } +.panel-drag { cursor:grab; color:var(--text-dim); user-select:none; } +.panel-drag:active { cursor:grabbing; } +body.dash-editing .panel-chrome { display:inline-flex; align-items:center; } +body.dash-editing .grid-stack-item-content { outline:1px dashed var(--border-mid); outline-offset:-1px; } +body.dash-editing main { padding-bottom:46vh; } +.widget-drawer { + position:fixed; left:0; right:0; bottom:0; transform:translateY(100%); + transition:transform .25s ease; max-height:44vh; overflow-y:auto; + background:var(--bg-card); border-top:1px solid var(--border-mid); + box-shadow:var(--fs-shadow-3); z-index:60; padding:1rem 1.5rem 1.5rem; +} +body.dash-editing .widget-drawer { transform:translateY(0); } +.ping-addr { display:block; color:var(--text-muted); font-size:0.75rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.ping-pills { display:flex; gap:2px; flex:1; min-width:0; overflow:hidden; align-items:center; } +.pill { display:inline-block; width:7px; height:22px; border-radius:2px; cursor:default; transition:opacity .1s; } +.pill:hover { opacity:.75; } +.pill-empty { background:var(--bg-elevated); } +.ping-cur { min-width:72px; text-align:right; font-size:0.85rem; font-variant-numeric:tabular-nums; } +.ping-cur-good { color:var(--green); } +.ping-cur-warn { color:var(--yellow); } +.ping-cur-bad { color:var(--orange); } +.ping-cur-down { color:var(--red); font-weight:500; } +.ping-cur-nd { color:var(--text-dim); } + +/* Widget headers */ +.widget-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem; } +.widget-title { font-family:var(--fs-font-body); font-size:var(--fs-size-tiny); font-weight:500; color:var(--text-muted); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); } +.widget-link { font-size:0.8rem; color:var(--text-muted); } +.widget-link:hover { color:var(--text); } + +/* Time range selector */ +.range-btn { background:none; border:1px solid var(--border-mid); border-radius:4px; color:var(--text-muted); padding:0.18rem 0.5rem; cursor:pointer; font-size:0.75rem; font-family:inherit; transition:background .1s,color .1s,border-color .1s; } +.range-btn:hover { background:var(--bg-elevated); color:var(--text); border-color:var(--accent); } +.range-btn.active { background:var(--accent); border-color:var(--accent); color:var(--fs-surface-page); } diff --git a/steward/static/theme.css b/steward/static/theme.css new file mode 100644 index 0000000..348611e --- /dev/null +++ b/steward/static/theme.css @@ -0,0 +1,176 @@ +/* + * Steward — generated stylesheet + * Source: design system 6. + * + * Purpose tokens only. This sheet declares what values MEAN; it styles + * no elements. Buttons, tables and input schemes live as snippets that + * reference these names, so each value is stated once and reused rather + * than restated per element. + * + * Generated — edit the design system, not this file. + */ + +:root { + /* accent */ + --fs-accent: #C8A840; /* Steward's single signature colour */ + --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); /* Pill, tag and badge backgrounds */ + --fs-accent-faint: color-mix(in srgb, var(--fs-accent) 8%, transparent); /* The faintest accent wash */ + --fs-accent-deep: color-mix(in srgb, var(--fs-accent) 70%, black); /* The accent, darkened — the far end of a CTA gradient */ + --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 22%, transparent); /* The heaviest accent tint — selection and emphasis washes */ + --fs-gradient-cta: linear-gradient(135deg, var(--fs-accent), var(--fs-accent-deep)); /* Gradient for a deliberate brand moment */ + --fs-glow-cta: 0 2px 10px color-mix(in srgb, var(--fs-accent) 35%, transparent); /* Glow under a brand-moment CTA */ + --fs-glow-cta-hover: 0 4px 24px color-mix(in srgb, var(--fs-accent) 65%, transparent); /* Glow under a hovered brand-moment CTA */ + --fs-accent-fg: color-mix(in srgb, var(--fs-accent) 45%, var(--fs-text-primary)); /* Accent-coloured TEXT sitting on an accent tint — the readable partner of --fs-accent-soft / -faint / -wash */ + + /* action */ + --fs-action-primary: #4A5D3F; /* Save, Submit, Confirm */ + --fs-action-secondary: #8B7355; /* Non-destructive alternates */ + --fs-action-tertiary: var(--fs-border-color); /* Ghost / outline actions */ + --fs-action-destructive: var(--fs-destructive); /* Destructive actions — delete, purge, revoke */ + --fs-action-primary-hover: color-mix(in srgb, var(--fs-action-primary) 88%, white); /* Primary action, hovered */ + --fs-action-secondary-hover: color-mix(in srgb, var(--fs-action-secondary) 88%, white); /* Secondary action, hovered */ + --fs-action-destructive-hover: color-mix(in srgb, var(--fs-action-destructive) 88%, white); /* Destructive action, hovered */ + + /* border */ + --fs-border-color: #3F4651; /* Borders, dividers and ghost outlines */ + --fs-border: 0.5px solid var(--fs-border-color); /* The default structural border */ + --fs-border-hover: 0.5px solid color-mix(in srgb, var(--fs-text-secondary) 30%, transparent); /* Border on hover or emphasis */ + --fs-border-active: 2px solid var(--fs-accent); /* Selected card or active tab only */ + + /* elevation */ + --fs-shadow-1: 0 1px 0 rgba(0,0,0,0.4); /* Hairline lift */ + --fs-shadow-2: 0 4px 12px rgba(0,0,0,0.35); /* Dropdowns and popovers */ + --fs-shadow-3: 0 16px 40px rgba(0,0,0,0.5); /* Modals */ + + /* focus */ + --fs-focus-ring: 0 0 0 2px var(--fs-accent); /* Focus indication, as a box-shadow */ + + /* font */ + --fs-font-display: 'Fraunces', Georgia, serif; /* Display, headings and wordmarks */ + --fs-font-body: 'Inter', system-ui, sans-serif; /* Body text, UI and labels */ + --fs-font-mono: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; /* Code, terminal output and monospaced data */ + + /* icon */ + --fs-icon-stroke: 1.5px; /* Icon stroke width at 24px */ + --fs-icon-stroke-sm: 1px; /* Icon stroke width at 16px */ + + /* layout */ + --fs-layout-page-max: 1200px; /* Maximum page width */ + --fs-layout-page-pad: 1rem; /* Horizontal page padding */ + --fs-layout-sidebar: 260px; /* Sidebar / nav rail width */ + --fs-layout-header: 52px; /* App header height */ + + /* motion */ + --fs-ease: cubic-bezier(0.2, 0.6, 0.2, 1); /* The one easing curve, used by every transition */ + --fs-dur-fast: 120ms; /* Hovers, colour and border changes */ + --fs-dur-base: 180ms; /* Most state changes */ + --fs-dur-slow: 280ms; /* Larger surface or layout shifts */ + + /* radius */ + --fs-radius-sm: 4px; /* Pills, tags and code spans */ + --fs-radius-md: 8px; /* Buttons, inputs and small cards */ + --fs-radius-lg: 12px; /* Cards, panels and modals */ + --fs-radius-xl: 16px; /* Hero containers and major surfaces */ + --fs-radius-pill: 9999px; /* Fully rounded — avatars, toggles */ + + /* semantic */ + --fs-success: var(--fs-action-primary); /* Success states */ + --fs-warning: #8B6F1E; /* Warnings */ + --fs-error: #C04A1F; /* Errors — something went wrong */ + --fs-info: #3D5A6E; /* Informational states */ + --fs-destructive: #6B2118; /* Irreversible actions */ + --fs-error-fg: #EC7A6C; /* Error / down / firing as TEXT or a status indicator */ + --fs-warning-fg: #E08A3C; /* Warning / degraded / pending as TEXT or a status indicator */ + --fs-info-fg: color-mix(in srgb, var(--fs-info) 50%, var(--fs-text-primary)); /* Info colour when it is TEXT: a status pill's label, an informational line. Not for fills or borders. */ + --fs-success-fg: #5FB87A; /* Success / up / ok as TEXT or a status indicator: badge labels, status dots, "up" counts */ + + /* space */ + --fs-space-1: 4px; /* Spacing step 1 */ + --fs-space-2: 8px; /* Spacing step 2 */ + --fs-space-3: 12px; /* Spacing step 3 */ + --fs-space-4: 16px; /* Spacing step 4 */ + --fs-space-5: 20px; /* Spacing step 5 */ + --fs-space-6: 24px; /* Spacing step 6 */ + --fs-space-7: 32px; /* Spacing step 7 */ + --fs-space-8: 48px; /* Spacing step 8 */ + --fs-space-9: 64px; /* Spacing step 9 */ + --fs-space-10: 96px; /* Spacing step 10 */ + + /* state */ + --fs-disabled-opacity: 0.5; /* Opacity for disabled controls */ + --fs-overlay: rgba(0, 0, 0, 0.65); /* Scrim behind modals and dialogs */ + + /* surface */ + --fs-surface-page: #14171A; /* Page background, the deepest surface */ + --fs-surface-raised: #1E2228; /* Cards and raised elements */ + --fs-surface-hover: #2C313A; /* Hovered surfaces, secondary elevation */ + --fs-surface-code: var(--fs-surface-page); /* Code block background */ + --fs-surface-code-inline: var(--fs-surface-raised); /* Inline code span background */ + --fs-table-stripe: color-mix(in srgb, var(--fs-text-primary) 3%, transparent); /* Alternating table row tint */ + + /* text */ + --fs-text-primary: #E8E4D8; /* Primary text on a page or raised surface — body, headings, labels */ + --fs-text-secondary: #C2BFB4; /* Secondary text and captions */ + --fs-text-tertiary: #9C9A92; /* Hints and metadata */ + --fs-text-on-action: #E8E4D8; /* Text on a filled coloured surface — action buttons, semantic badges, the accent */ + --fs-text-tertiary-fg: color-mix(in srgb, var(--fs-text-tertiary) 55%, var(--fs-text-primary)); /* Tertiary text on a tertiary tint — a muted chip that stays readable */ + --fs-text-secondary-fg: color-mix(in srgb, var(--fs-text-secondary) 90%, var(--fs-text-primary)); /* Secondary text on a secondary tint — completes the pattern; the value barely moves */ + + /* type */ + --fs-size-display: 40px; /* Display — display face, medium weight */ + --fs-size-h1: 32px; /* H1 — display face, medium weight */ + --fs-size-h2: 24px; /* H2 — display face, medium weight */ + --fs-size-h3: 18px; /* H3 — body face, medium weight */ + --fs-size-body: 15px; /* Body — body face, regular weight */ + --fs-size-body-sm: 13px; /* Small body — body face, regular weight */ + --fs-size-label: 12px; /* Label — body face, medium weight */ + --fs-size-code: 13px; /* Code — mono face, regular weight */ + --fs-size-tiny: 11px; /* Tiny micro-label — body face, medium weight */ + --fs-weight-regular: 400; /* Regular weight */ + --fs-weight-medium: 500; /* Medium weight — the heaviest the system goes */ + --fs-leading-heading: 1.3; /* Line height for headings */ + --fs-leading-body: 1.5; /* Line height for body text */ + --fs-leading-longform: 1.7; /* Line height for long-form reading surfaces */ + --fs-tracking-tiny: 0.08em; /* Letter-spacing on the tiny uppercase label */ +} + +[data-theme="light"] { + /* accent */ + --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 20%, transparent); + --fs-glow-cta-hover: 0 4px 20px color-mix(in srgb, var(--fs-accent) 55%, transparent); + + /* border */ + --fs-border-color: #D9D6CE; + + /* state */ + --fs-overlay: rgba(0, 0, 0, 0.45); + + /* surface */ + --fs-surface-page: #F5F1E8; + --fs-surface-raised: #FBF8F0; + --fs-surface-hover: #EFEAE0; + --fs-surface-code: #EBEDF0; + --fs-surface-code-inline: #EBEDF0; + + /* text */ + --fs-text-primary: #14171A; + --fs-text-secondary: #5A5852; + --fs-text-tertiary: #9A9890; +} + +/* SUPERSEDES — write the token, not the literal. + * #c84048 -> --fs-error-fg + * #ff8090 -> --fs-error-fg + * #ffb0b8 -> --fs-error-fg + * #ffb0b0 -> --fs-error-fg + * #d4a840 -> --fs-warning-fg + * #c87840 -> --fs-warning-fg + * var(--fs-info) -> --fs-info-fg + * #4aa86a -> --fs-success-fg + * #90f0b0 -> --fs-success-fg + * #fff -> --fs-text-on-action + * #ffffff -> --fs-text-on-action + * white -> --fs-text-on-action + * bold -> --fs-weight-medium + * bolder -> --fs-weight-medium + */ diff --git a/steward/templates/ansible/browse.html b/steward/templates/ansible/browse.html index a7cea6d..96aa26d 100644 --- a/steward/templates/ansible/browse.html +++ b/steward/templates/ansible/browse.html @@ -56,7 +56,7 @@
{% if not sd.playbooks %} -

No playbooks found at this path.

+

No playbooks found at this path.

{% else %} diff --git a/steward/templates/ansible/run_started.html b/steward/templates/ansible/run_started.html index d8aa1ee..3d958ac 100644 --- a/steward/templates/ansible/run_started.html +++ b/steward/templates/ansible/run_started.html @@ -7,7 +7,7 @@
     
-
+
Streaming output…
- + + {% block head %}{% endblock %} diff --git a/steward/templates/dashboard/share_view.html b/steward/templates/dashboard/share_view.html index d510a5f..855eb98 100644 --- a/steward/templates/dashboard/share_view.html +++ b/steward/templates/dashboard/share_view.html @@ -75,7 +75,7 @@
- + {{ dashboard.name }} Steward · read-only view diff --git a/steward/templates/settings/_ansible_sources.html b/steward/templates/settings/_ansible_sources.html index 610102c..6a449aa 100644 --- a/steward/templates/settings/_ansible_sources.html +++ b/steward/templates/settings/_ansible_sources.html @@ -14,7 +14,7 @@ {{ src.type }} diff --git a/steward/templates/settings/index.html b/steward/templates/settings/index.html deleted file mode 100644 index a8e0a84..0000000 --- a/steward/templates/settings/index.html +++ /dev/null @@ -1,157 +0,0 @@ -{# steward/templates/settings/index.html #} -{% extends "base.html" %} -{% block title %}Settings — Steward{% endblock %} -{% block content %} -

Settings

-
{{ csrf_field() }} -
- - {# ── General ──────────────────────────────────────────────────────────── #} -
-

General

-
- - -
-
- - -
-
- - -
-
- - {# ── SMTP ─────────────────────────────────────────────────────────────── #} -
-

Email (SMTP)

-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - {# ── Webhook ──────────────────────────────────────────────────────────── #} -
-

Webhook

-
- - -
-
- - -
-
- - {# ── Ansible Sources ──────────────────────────────────────────────────── #} -
-

Ansible Sources

-

JSON array of source objects. Each requires name, type (git or local), and either url or path.

- -
- - {# ── Plugins ──────────────────────────────────────────────────────────── #} - {% if discovered_plugins %} -
-

Plugins

- {% for plugin in discovered_plugins %} -
-
- - -
- {% for cfg_key, cfg_default in plugin.get('config', {}).items() %} - {% if cfg_default is mapping %} - {# Nested dict — render each sub-key as its own field #} -
-
{{ cfg_key }}
- {% set sub_cfg = plugin._config.get(cfg_key, cfg_default) if plugin._config.get(cfg_key, cfg_default) is mapping else cfg_default %} - {% for sub_key, sub_default in cfg_default.items() %} -
- {% if sub_default is sameas false or sub_default is sameas true %} -
- - -
- {% else %} - - - {% endif %} -
- {% endfor %} -
- {% elif cfg_default is sameas false or cfg_default is sameas true %} -
- - -
- {% else %} -
- - -
- {% endif %} - {% endfor %} -
- {% endfor %} -
- {% endif %} - -
-
- - - Changes take effect immediately. Monitor interval, plugin enable/disable require a restart. - -
- -{% endblock %} diff --git a/tests/test_style_tokens.py b/tests/test_style_tokens.py new file mode 100644 index 0000000..09bad2f --- /dev/null +++ b/tests/test_style_tokens.py @@ -0,0 +1,73 @@ +"""Every colour a page draws comes from the design-system tokens (#5092). + +- Every var(--x) a template or stylesheet uses is defined somewhere Steward + serves, so nothing silently falls back to the browser default. +- The base stylesheet states no colour or heavy weight of its own: values + are theme.css tokens (generated from design system 6) or aliases of them. +- Hex colours written into templates can only go down: each page moved onto + the tokens lowers HEX_CEILING (milestone 477 retires the rest). +""" +import pathlib +import re + +import steward + +_REPO = pathlib.Path(steward.__file__).parent.parent +_STATIC = _REPO / "steward" / "static" +_COMMENTS = re.compile(r"\{#.*?#\}||/\*.*?\*/", re.S) +_DEFINED = re.compile(r"(--[\w-]+)\s*:") +_USED = re.compile(r"var\(\s*(--[\w-]+)") +# A hex colour where a colour goes: after a CSS ':', '(' or ',', or as a quoted JS +# string -- but not an HTML attribute's value (href="#abc" is a link to an id). +_HEX = re.compile(r"""(?:[:(,]\s*|(? set[str]: + defined, used = set(), set() + for t in texts: + defined |= set(_DEFINED.findall(t)) + used |= set(_USED.findall(t)) + return used - defined + + +def hex_colours(text: str) -> list[str]: + return _HEX.findall(_COMMENTS.sub("", text)) + + +def test_the_guards_can_fail(): + assert undefined_vars([":root { --a: 1; } .x { color: var(--a); background: var(--b); }"]) == {"--b"} + page = '
' + assert len(hex_colours(page)) == 2 + assert hex_colours("{# color:#fff #}") == [] + + +def test_every_css_variable_used_is_defined(): + texts = [_text(p) for p in [*_templates(), *_sheets()]] + assert not undefined_vars(texts) + + +def test_the_base_stylesheet_states_no_colour_or_heavy_weight_of_its_own(): + css = _text(_STATIC / "steward.css") + assert not re.findall(r"#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(", css) + assert not re.findall(r"font-weight\s*:\s*(?:[6-9]00|bold)", css) + + +def test_hex_colours_in_templates_only_go_down(): + count = sum(len(hex_colours(p.read_text())) for p in _templates()) + assert count <= HEX_CEILING, ( + f"{count} hex colours in templates, ceiling {HEX_CEILING}: use a token (theme.css / steward.css)")