Build and push server image / build-and-push (push) Successful in 35s
The right column had grown to 5 stacked cards (Now displaying, Device, Battery history, Firmware update, Stats) against the left's single Settings form, running noticeably longer. Moved the collapsed Stats card into a new left-column stack under Settings -- it's already de-emphasized by design, so pairing it with the other occasionally-checked column reads naturally, and it's small enough (collapsed by default) to roughly balance the two columns' heights without overcorrecting the other way.
339 lines
12 KiB
HTML
339 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>{% block title %}ESPresso Frame{% endblock %}</title>
|
|
<script>
|
|
// Applied before first paint so there's no flash of the wrong theme.
|
|
(function () {
|
|
try {
|
|
var stored = localStorage.getItem('theme');
|
|
if (stored === 'light' || stored === 'dark') {
|
|
document.documentElement.setAttribute('data-theme', stored);
|
|
}
|
|
} catch (e) { /* localStorage unavailable (private mode, etc.) */ }
|
|
})();
|
|
</script>
|
|
<style>
|
|
:root {
|
|
--bg: #f5f6f8;
|
|
--surface: #ffffff;
|
|
--surface-alt: #f0f1f4;
|
|
--border: #e3e5e9;
|
|
--text: #16181d;
|
|
--text-muted: #666d7a;
|
|
--accent: #2563eb;
|
|
--accent-hover: #1d4ed8;
|
|
--focus-ring: rgba(37, 99, 235, 0.35);
|
|
--success-bg: #dcfce7;
|
|
--success-text: #166534;
|
|
--danger-bg: #fee2e2;
|
|
--danger-text: #991b1b;
|
|
--warn-bg: #fef9c3;
|
|
--warn-text: #854d0e;
|
|
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
|
|
--shadow-hover: 0 8px 20px rgba(16, 24, 40, 0.10);
|
|
--overlay: rgba(0, 0, 0, 0.55);
|
|
--overlay-hover: rgba(153, 27, 27, 0.85);
|
|
--color-scheme: light;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #0d1016;
|
|
--surface: #161a22;
|
|
--surface-alt: #1d222b;
|
|
--border: #2a2f3a;
|
|
--text: #e8eaed;
|
|
--text-muted: #9aa2b1;
|
|
--accent: #4c8dff;
|
|
--accent-hover: #6ea1ff;
|
|
--focus-ring: rgba(76, 141, 255, 0.4);
|
|
--success-bg: rgba(34, 197, 94, 0.16);
|
|
--success-text: #4ade80;
|
|
--danger-bg: rgba(239, 68, 68, 0.16);
|
|
--danger-text: #f87171;
|
|
--warn-bg: rgba(234, 179, 8, 0.16);
|
|
--warn-text: #fbbf24;
|
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.35);
|
|
--shadow-hover: 0 10px 24px rgba(0, 0, 0, 0.45);
|
|
--overlay: rgba(0, 0, 0, 0.55);
|
|
--overlay-hover: rgba(248, 113, 113, 0.35);
|
|
--color-scheme: dark;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: #0d1016;
|
|
--surface: #161a22;
|
|
--surface-alt: #1d222b;
|
|
--border: #2a2f3a;
|
|
--text: #e8eaed;
|
|
--text-muted: #9aa2b1;
|
|
--accent: #4c8dff;
|
|
--accent-hover: #6ea1ff;
|
|
--focus-ring: rgba(76, 141, 255, 0.4);
|
|
--success-bg: rgba(34, 197, 94, 0.16);
|
|
--success-text: #4ade80;
|
|
--danger-bg: rgba(239, 68, 68, 0.16);
|
|
--danger-text: #f87171;
|
|
--warn-bg: rgba(234, 179, 8, 0.16);
|
|
--warn-text: #fbbf24;
|
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.35);
|
|
--shadow-hover: 0 10px 24px rgba(0, 0, 0, 0.45);
|
|
--overlay: rgba(0, 0, 0, 0.55);
|
|
--overlay-hover: rgba(248, 113, 113, 0.35);
|
|
--color-scheme: dark;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html {
|
|
color-scheme: var(--color-scheme);
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
-webkit-font-smoothing: antialiased;
|
|
transition: background-color .15s ease, color .15s ease;
|
|
}
|
|
|
|
.page {
|
|
max-width: 1080px;
|
|
margin: 0 auto;
|
|
padding: 28px 20px 72px;
|
|
}
|
|
.page.page-narrow {
|
|
max-width: 420px;
|
|
padding-top: 88px;
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.brand { display: flex; align-items: flex-start; gap: 12px; }
|
|
.brand-mark {
|
|
font-size: 26px;
|
|
line-height: 1;
|
|
margin-top: 2px;
|
|
}
|
|
h1 { font-size: 21px; margin: 0; letter-spacing: -0.01em; font-weight: 700; }
|
|
p.sub { color: var(--text-muted); font-size: 13.5px; margin: 5px 0 0; line-height: 1.5; }
|
|
|
|
.icon-btn {
|
|
flex: none;
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 50%;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
box-shadow: var(--shadow);
|
|
transition: background-color .15s ease, border-color .15s ease, transform .1s ease;
|
|
}
|
|
.icon-btn:hover { background: var(--surface-alt); }
|
|
.icon-btn:active { transform: scale(0.94); }
|
|
|
|
h2.card-title, summary.card-title {
|
|
font-size: 14.5px;
|
|
font-weight: 650;
|
|
margin: 0 0 14px;
|
|
letter-spacing: 0.01em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
summary.card-title { cursor: pointer; margin-bottom: 0; }
|
|
details.card[open] summary.card-title { margin-bottom: 14px; }
|
|
details.card .sub { margin-top: 8px; }
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 20px 22px 22px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.card + .card { margin-top: 20px; }
|
|
|
|
label { display: block; margin-top: 16px; font-size: 13px; font-weight: 600; color: var(--text); }
|
|
label:first-child { margin-top: 0; }
|
|
|
|
input, select {
|
|
width: 100%;
|
|
padding: 9px 10px;
|
|
box-sizing: border-box;
|
|
margin-top: 5px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: inherit;
|
|
transition: border-color .15s ease, box-shadow .15s ease;
|
|
}
|
|
input:focus, select:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
}
|
|
|
|
.checkbox-row { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
|
|
.checkbox-row input { width: auto; margin-top: 0; }
|
|
.checkbox-row label { margin-top: 0; font-weight: normal; }
|
|
|
|
button {
|
|
margin-top: 20px;
|
|
padding: 10px 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: var(--accent);
|
|
color: white;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
transition: background-color .15s ease, transform .1s ease;
|
|
}
|
|
button:hover { background: var(--accent-hover); }
|
|
button:active { transform: translateY(1px); }
|
|
button.secondary {
|
|
background: transparent;
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
margin-right: 8px;
|
|
}
|
|
button.secondary:hover { background: var(--surface-alt); }
|
|
|
|
.status { margin-top: 16px; padding: 10px 12px; border-radius: 8px; font-size: 14px; }
|
|
.status.ok { background: var(--success-bg); color: var(--success-text); }
|
|
.status.err { background: var(--danger-bg); color: var(--danger-text); }
|
|
.info-box {
|
|
margin-bottom: 20px;
|
|
padding: 12px 14px;
|
|
border-radius: 10px;
|
|
font-size: 13px;
|
|
background: var(--surface-alt);
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.info-box.warn { background: var(--warn-bg); color: var(--warn-text); border-color: transparent; }
|
|
|
|
code {
|
|
background: var(--surface-alt);
|
|
color: var(--text);
|
|
padding: 2px 5px;
|
|
border-radius: 4px;
|
|
font-size: 0.92em;
|
|
}
|
|
.info-box code, .info-box.warn code { background: rgba(127, 127, 127, 0.18); color: inherit; }
|
|
|
|
.thumb { width: 160px; max-width: 100%; border-radius: 8px; display: block; }
|
|
|
|
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; margin-top: 8px; }
|
|
.photo-card {
|
|
position: relative; cursor: grab; border-radius: 8px; overflow: hidden; aspect-ratio: 1;
|
|
background: var(--surface-alt); border: 1px solid var(--border);
|
|
box-shadow: var(--shadow);
|
|
transition: box-shadow .15s ease, transform .15s ease;
|
|
/* pan-y (not none): lets a normal touch-scroll of the page work
|
|
when you touch a card without meaning to drag it. Dragging on
|
|
touch instead requires a brief hold first (see the JS below),
|
|
which switches this to "none" for the rest of that touch --
|
|
only once we're sure it's a deliberate drag, not a scroll. */
|
|
touch-action: pan-y;
|
|
/* Without this, a press-and-drag gesture also triggers the
|
|
browser's native text/content selection (the blue highlight) --
|
|
distracting, and on some browsers it fights the pointer-based
|
|
drag tracking below closely enough to break it outright. */
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
.photo-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-1px); }
|
|
.photo-card:active { cursor: grabbing; }
|
|
.photo-card.dragging { opacity: 0.35; }
|
|
.photo-card.drag-armed { box-shadow: 0 0 0 3px var(--focus-ring) inset; }
|
|
.photo-card.drag-over { outline: 3px solid var(--accent); outline-offset: -3px; }
|
|
.photo-card img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
|
|
.photo-card .badge { position: absolute; top: 6px; left: 6px; background: var(--overlay); color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; }
|
|
.photo-card .remove-btn, .thumb-wrap .remove-btn {
|
|
position: absolute; top: 6px; right: 6px; width: 22px; height: 22px; margin: 0; padding: 0;
|
|
line-height: 20px; text-align: center; font-size: 13px; border-radius: 50%;
|
|
background: var(--overlay); color: white;
|
|
}
|
|
.photo-card .remove-btn:hover, .thumb-wrap .remove-btn:hover { background: var(--overlay-hover); }
|
|
.photo-card .show-next {
|
|
position: absolute; bottom: 6px; left: 6px; right: 6px; margin: 0; padding: 5px 0;
|
|
font-size: 11px; text-align: center; background: rgba(37, 99, 235, 0.85); border-radius: 4px;
|
|
}
|
|
.thumb-wrap { position: relative; display: inline-block; }
|
|
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
|
gap: 20px;
|
|
align-items: start;
|
|
}
|
|
.main-col, .side-col { display: flex; flex-direction: column; }
|
|
@media (max-width: 860px) {
|
|
.layout { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<div class="page {% block page_class %}{% endblock %}">
|
|
<header class="topbar">
|
|
<div class="brand">
|
|
<span class="brand-mark" aria-hidden="true">☕</span>
|
|
<div>
|
|
<h1>ESPresso Frame</h1>
|
|
{% block subtitle %}{% endblock %}
|
|
</div>
|
|
</div>
|
|
<button type="button" id="theme-toggle" class="icon-btn" title="Toggle dark mode" aria-label="Toggle dark mode">🌓</button>
|
|
</header>
|
|
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
|
|
<script>
|
|
// Shared theme toggle: explicit choice wins over the OS preference and
|
|
// is remembered; with no explicit choice, the CSS above falls back to
|
|
// prefers-color-scheme on its own.
|
|
(function () {
|
|
var btn = document.getElementById('theme-toggle');
|
|
if (!btn) return;
|
|
|
|
function currentTheme() {
|
|
var stored = null;
|
|
try { stored = localStorage.getItem('theme'); } catch (e) { /* ignore */ }
|
|
if (stored === 'light' || stored === 'dark') return stored;
|
|
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
}
|
|
|
|
function apply(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
try { localStorage.setItem('theme', theme); } catch (e) { /* ignore */ }
|
|
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme: theme } }));
|
|
}
|
|
|
|
btn.addEventListener('click', function () {
|
|
apply(currentTheme() === 'dark' ? 'light' : 'dark');
|
|
});
|
|
})();
|
|
</script>
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|