Files
pngx-sync/app/templates/base.html
domverse 7b1a85bc84
All checks were successful
Deploy / deploy (push) Successful in 39s
feat: full UI overhaul — dark theme with custom design system
Replace Pico CSS with a custom dark design system:
- Deep navy background (#090b12) with violet-indigo accent (#7b6cf5)
- Outfit font for UI, JetBrains Mono for code/URLs
- Custom nav with SVG icons and active-state highlighting
- Pill badges with colored status dots
- Uppercase letter-spaced table headers
- Stat cards on replica detail page
- h3 section headings with extending rule line
- Refined dialog/modal styling with blur backdrop
- Terminal-style SSE log stream
- Progress bar with accent gradient animation
- Page entrance fade-in animation
- All Pico CSS variable references replaced with custom tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:52:42 +01:00

554 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}pngx-controller{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
<style>
/* ── TOKENS ── */
:root {
--bg: #090b12;
--surface: #0f1220;
--surface-2: #161926;
--surface-3: #1d2235;
--border: #252840;
--border-hi: #333653;
--accent: #7b6cf5;
--accent-soft: rgba(123,108,245,.13);
--accent-hi: #9588f7;
--ok: #3dd68c;
--ok-soft: rgba(61,214,140,.1);
--warn: #f9c443;
--warn-soft: rgba(249,196,67,.1);
--err: #f47067;
--err-soft: rgba(244,112,103,.1);
--sus: #ff8c42;
--sus-soft: rgba(255,140,66,.1);
--info: #5499e4;
--info-soft: rgba(84,153,228,.1);
--text: #dce0f0;
--text-2: #8892b0;
--text-3: #4a526e;
--r: 6px;
--r2: 4px;
--r3: 10px;
--font: 'Outfit', system-ui, sans-serif;
--mono: 'JetBrains Mono', 'Fira Code', monospace;
}
/* ── RESET ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
/* ── BASE ── */
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; line-height: 1.6; }
a { color: var(--accent); text-decoration: none; transition: color .15s; }
a:hover { color: var(--accent-hi); }
strong { font-weight: 600; }
hr { border: none; border-top: 1px solid var(--border); margin: 1.5rem 0; }
p { color: var(--text-2); margin-bottom: .75rem; }
small { font-size: .8rem; }
code {
font-family: var(--mono);
font-size: .82em;
background: var(--surface-3);
color: var(--accent-hi);
padding: .15em .4em;
border-radius: var(--r2);
}
pre {
font-family: var(--mono);
font-size: .8em;
white-space: pre-wrap;
background: var(--surface-3);
padding: .75rem 1rem;
border-radius: var(--r2);
border: 1px solid var(--border);
}
/* ── TYPOGRAPHY ── */
h2 { font-size: 1.35rem; font-weight: 700; letter-spacing: -.02em; color: var(--text); }
h3 {
font-size: .7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--text-3);
margin: 2rem 0 .9rem;
display: flex;
align-items: center;
gap: .75rem;
}
h3::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* ── NAV ── */
.app-nav {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 1.5rem;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 200;
}
.nav-brand {
display: flex;
align-items: center;
gap: .55rem;
font-weight: 700;
font-size: .92rem;
color: var(--text) !important;
letter-spacing: -.01em;
text-decoration: none;
}
.nav-brand svg { color: var(--accent); flex-shrink: 0; }
.nav-brand-dim { color: var(--text-3); font-weight: 400; }
.nav-links {
display: flex;
align-items: center;
gap: .15rem;
list-style: none;
}
.nav-links a {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .35rem .7rem;
border-radius: var(--r2);
font-size: .83rem;
font-weight: 500;
color: var(--text-2);
transition: all .15s;
text-decoration: none;
}
.nav-links a svg { opacity: .7; }
.nav-links a:hover { color: var(--text); background: var(--surface-2); }
.nav-links a:hover svg { opacity: 1; }
.nav-links a.active { color: var(--text); background: var(--surface-3); }
.nav-links a.active svg { opacity: 1; color: var(--accent); }
.nav-meta { display: flex; gap: .9rem; }
.nav-meta a { font-size: .72rem; color: var(--text-3); }
.nav-meta a:hover { color: var(--text-2); }
/* ── LAYOUT ── */
.container { max-width: 1180px; margin: 0 auto; padding: 2rem 1.5rem; animation: fadein .2s ease; }
@keyframes fadein { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
.page-hd {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.75rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--border);
}
.page-hd h2 { margin: 0; }
.page-hd-sub { color: var(--text-3); font-size: .8rem; margin-top: .2rem; }
.page-actions { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
/* ── ARTICLE / CARD ── */
article {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 1.25rem;
margin-bottom: 1rem;
}
article > header:first-child {
border-bottom: 1px solid var(--border);
padding-bottom: .85rem;
margin-bottom: 1rem;
}
/* ── TABLE ── */
.overflow-auto { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: .875rem; }
thead th {
padding: .55rem 1rem;
border-bottom: 1px solid var(--border);
text-align: left;
font-size: .68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--text-3);
white-space: nowrap;
}
tbody td {
padding: .7rem 1rem;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: var(--surface-2); }
.actions { white-space: nowrap; display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
/* ── BUTTONS ── */
button, a[role="button"] {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .45rem .95rem;
font-family: var(--font);
font-size: .83rem;
font-weight: 500;
border-radius: var(--r2);
border: 1px solid transparent;
cursor: pointer;
transition: all .15s;
text-decoration: none;
line-height: 1.4;
white-space: nowrap;
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
button:hover, a[role="button"]:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: #fff; }
button.secondary { background: var(--surface-3); color: var(--text-2); border-color: var(--border-hi); }
button.secondary:hover { border-color: var(--accent); color: var(--text); background: var(--surface-3); }
button.outline { background: transparent; color: var(--text-2); border-color: var(--border-hi); }
button.outline:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
button.secondary.outline, a.secondary.outline[role="button"] {
background: transparent; color: var(--text-2); border-color: var(--border-hi);
}
button.secondary.outline:hover, a.secondary.outline[role="button"]:hover {
border-color: var(--accent); color: var(--accent); background: var(--accent-soft);
}
button.contrast { background: var(--err-soft); color: var(--err); border-color: var(--err); }
button.contrast:hover { background: var(--err); color: #fff; }
button.contrast.outline { background: transparent; color: var(--err); border-color: var(--err); }
button.contrast.outline:hover { background: var(--err-soft); }
button[disabled], button[disabled]:hover { opacity: .4; cursor: not-allowed; pointer-events: none; }
button[aria-busy="true"] { opacity: .7; pointer-events: none; }
button[aria-busy="true"]::after {
content: '';
display: inline-block;
width: 11px; height: 11px;
border: 1.5px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin .55s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* dialog close button */
button[rel="prev"], button[aria-label="Close"] {
background: transparent !important;
border-color: transparent !important;
color: var(--text-3) !important;
padding: .2rem .4rem;
font-size: 1.1rem;
line-height: 1;
float: right;
margin-left: 1rem;
}
button[rel="prev"]::before { content: '×'; }
button[rel="prev"]:hover, button[aria-label="Close"]:hover { color: var(--text) !important; }
/* ── BADGES ── */
.badge {
display: inline-flex;
align-items: center;
gap: .28rem;
padding: .18em .55em;
border-radius: 2em;
font-size: .7rem;
font-weight: 600;
letter-spacing: .03em;
text-transform: uppercase;
}
.badge::before { content: ''; width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
.badge-synced, .badge-ok { background: var(--ok-soft); color: var(--ok); }
.badge-synced::before, .badge-ok::before { background: var(--ok); }
.badge-syncing, .badge-info { background: var(--info-soft); color: var(--info); }
.badge-syncing::before, .badge-info::before { background: var(--info); }
.badge-error { background: var(--err-soft); color: var(--err); }
.badge-error::before { background: var(--err); }
.badge-suspended, .badge-warning { background: var(--warn-soft); color: var(--warn); }
.badge-suspended::before, .badge-warning::before { background: var(--warn); }
.badge-pending { background: var(--surface-3); color: var(--text-2); }
.badge-pending::before { background: var(--text-3); }
/* ── FORMS ── */
label {
display: block;
font-size: .8rem;
font-weight: 500;
color: var(--text-2);
margin-bottom: .25rem;
}
input[type="text"], input[type="url"], input[type="number"],
input[type="password"], input[type="email"], select, textarea {
display: block;
width: 100%;
background: var(--surface-2);
border: 1px solid var(--border-hi);
border-radius: var(--r2);
padding: .45rem .7rem;
color: var(--text);
font-size: .85rem;
font-family: var(--font);
transition: border-color .15s, box-shadow .15s;
line-height: 1.5;
margin-top: .3rem;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
input::placeholder { color: var(--text-3); }
select option { background: var(--surface-2); }
input[type="checkbox"] { width: 1rem; height: 1rem; accent-color: var(--accent); cursor: pointer; display: inline; margin: 0; margin-top: 0 !important; }
label small { display: block; margin-top: .25rem; color: var(--text-3); font-size: .74rem; }
.muted, small.muted { color: var(--text-3); }
/* ── PROGRESS ── */
progress {
display: block; width: 100%; height: 4px;
border-radius: 2px; border: none;
background: var(--surface-3); overflow: hidden; margin: .5rem 0;
appearance: none; -webkit-appearance: none;
}
progress::-webkit-progress-bar { background: var(--surface-3); }
progress::-webkit-progress-value { background: linear-gradient(90deg, var(--accent), var(--accent-hi)); border-radius: 2px; transition: width .3s; }
progress::-moz-progress-bar { background: linear-gradient(90deg, var(--accent), var(--accent-hi)); border-radius: 2px; }
progress:not([value])::after {
/* shimmer for indeterminate */
}
#progress-bar { display: none; }
#progress-bar.active { display: block; }
#progress-bar article {
border-left: 3px solid var(--accent);
background: var(--surface-2);
padding: .85rem 1.1rem;
}
/* ── DIALOG ── */
dialog {
background: var(--surface);
border: 1px solid var(--border-hi);
border-radius: var(--r3);
color: var(--text);
padding: 0;
max-width: 540px;
width: calc(100% - 2rem);
box-shadow: 0 24px 60px rgba(0,0,0,.7);
}
dialog::backdrop { background: rgba(0,0,0,.75); backdrop-filter: blur(3px); }
dialog article { border: none; border-radius: var(--r3); margin: 0; padding: 0; }
dialog article > header:first-child {
padding: 1.15rem 1.4rem .9rem;
margin: 0;
border-bottom: 1px solid var(--border);
}
dialog article h3 { margin: 0; font-size: .95rem; font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--text); }
dialog article h3::after { display: none; }
dialog article > :not(header):not(footer) { padding: 1.15rem 1.4rem; }
dialog article footer {
border-top: 1px solid var(--border);
padding: .9rem 1.4rem;
margin: 0;
}
/* form > footer inside dialog */
dialog form footer { border-top: 1px solid var(--border); padding: .9rem 1.4rem; margin: 0; }
/* ── DETAILS / SUMMARY ── */
details { border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; margin-top: 1.5rem; }
summary {
padding: .7rem 1rem;
cursor: pointer;
user-select: none;
font-size: .83rem;
font-weight: 500;
color: var(--text-2);
list-style: none;
display: flex;
align-items: center;
gap: .5rem;
}
summary::-webkit-details-marker { display: none; }
summary::before { content: '▶'; font-size: .6rem; color: var(--text-3); transition: transform .18s; }
details[open] summary::before { transform: rotate(90deg); }
details > :not(summary) { padding: 0 1rem 1rem; }
details[open] summary { border-bottom: 1px solid var(--border); }
/* ── BREADCRUMB ── */
nav[aria-label="breadcrumb"] ul {
display: flex; align-items: center; gap: .4rem;
list-style: none; margin: 0 0 1.25rem; font-size: .78rem; color: var(--text-3);
}
nav[aria-label="breadcrumb"] li { display: flex; align-items: center; gap: .4rem; }
nav[aria-label="breadcrumb"] li:not(:last-child)::after { content: '/'; color: var(--border-hi); }
nav[aria-label="breadcrumb"] li a { color: var(--text-2); }
nav[aria-label="breadcrumb"] li a:hover { color: var(--accent); }
nav[aria-label="breadcrumb"] li:last-child { color: var(--text); }
/* ── FLASH / ALERT ── */
[role="alert"] {
padding: .7rem 1rem;
margin-bottom: 1.25rem;
background: var(--info-soft);
color: var(--info);
border-radius: var(--r2);
border-left: 3px solid var(--info);
font-size: .875rem;
}
/* ── LOG TABLE ── */
.log-entry-error td { background: rgba(244,112,103,.04); }
.log-entry-warning td { background: rgba(249,196,67,.04); }
/* ── SSE LOG ── */
.sse-terminal {
font-family: var(--mono);
font-size: .78rem;
max-height: 280px;
overflow-y: auto;
background: var(--surface-3);
border: 1px solid var(--border);
border-radius: var(--r);
padding: .75rem;
line-height: 1.7;
}
/* ── GRIDS ── */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.grid-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
.grid-form-sidebar { display: grid; grid-template-columns: 1fr 340px; gap: 2rem; align-items: start; }
/* ── STAT CARDS ── */
.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: .85rem; margin: 1.25rem 0; }
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 1rem 1.1rem;
}
.stat-card-label { font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-3); margin-bottom: .35rem; }
.stat-card-value { font-size: 1rem; font-weight: 600; color: var(--text); }
.stat-card-sub { font-size: .75rem; color: var(--text-3); margin-top: .2rem; }
/* ── DANGER ZONE ── */
.danger-zone { border: 1px solid rgba(244,112,103,.3); border-radius: var(--r); padding: 1rem 1.1rem; }
.danger-zone p { font-size: .83rem; color: var(--text-2); margin-bottom: .75rem; }
/* ── ENV DETECTION CARD ── */
.env-detected {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r);
padding: .75rem 1rem;
margin-bottom: 1.25rem;
}
.env-detected-label {
font-size: .65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--text-3);
margin-bottom: .5rem;
}
/* ── ASIDE ── */
aside article { background: var(--surface-2); }
/* ── MISC ── */
.info-banner {
padding: .7rem 1rem;
background: var(--accent-soft);
border-left: 3px solid var(--accent);
border-radius: var(--r2);
font-size: .83rem;
color: var(--text-2);
}
@media (max-width: 768px) {
.grid-form-sidebar { grid-template-columns: 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.app-nav { padding: 0 1rem; }
.container { padding: 1.25rem 1rem; }
.nav-meta { display: none; }
}
</style>
</head>
<body>
<header class="app-nav">
<a href="/" class="nav-brand">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M10 2H4.5A1.5 1.5 0 003 3.5v11A1.5 1.5 0 004.5 16h9a1.5 1.5 0 001.5-1.5V7l-5-5z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M10 2v5h4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 11.5l2 2 2-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 13.5v-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<span>pngx<span class="nav-brand-dim">-controller</span></span>
</a>
<ul class="nav-links">
<li><a href="/">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><rect x=".75" y=".75" width="4.5" height="4.5" rx=".75" stroke="currentColor" stroke-width="1.3"/><rect x="7.75" y=".75" width="4.5" height="4.5" rx=".75" stroke="currentColor" stroke-width="1.3"/><rect x=".75" y="7.75" width="4.5" height="4.5" rx=".75" stroke="currentColor" stroke-width="1.3"/><rect x="7.75" y="7.75" width="4.5" height="4.5" rx=".75" stroke="currentColor" stroke-width="1.3"/></svg>
Dashboard
</a></li>
<li><a href="/replicas">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><rect x=".75" y="1.75" width="11.5" height="3.5" rx=".75" stroke="currentColor" stroke-width="1.3"/><rect x=".75" y="7.75" width="11.5" height="3.5" rx=".75" stroke="currentColor" stroke-width="1.3"/><circle cx="10.25" cy="3.5" r=".75" fill="currentColor"/><circle cx="10.25" cy="9.5" r=".75" fill="currentColor"/></svg>
Replicas
</a></li>
<li><a href="/logs">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1.5 3h10M1.5 6.5h7M1.5 10h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
Logs
</a></li>
<li><a href="/settings">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><circle cx="6.5" cy="6.5" r="1.75" stroke="currentColor" stroke-width="1.3"/><path d="M6.5 1v1.25m0 8.5V12M1 6.5h1.25m8.5 0H12M2.87 2.87l.88.88m5 5 .88.88M2.87 10.13l.88-.88m5-5 .88-.88" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
Settings
</a></li>
</ul>
<div class="nav-meta">
<a href="/healthz">health</a>
<a href="/metrics">metrics</a>
</div>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<script>
(function() {
const path = window.location.pathname;
document.querySelectorAll('.nav-links a').forEach(function(a) {
const href = a.getAttribute('href');
if (href === '/' ? path === '/' : path.startsWith(href)) {
a.classList.add('active');
}
});
})();
</script>
</body>
</html>