Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the self-hosted JetBrains Mono fonts, and interactive mockups of every screen across three iterations. Iteration 3 is final: phone, tablet and desktop tiers, device preview, and "empty spool weight" terminology. The build scripts regenerate the screens byte-identically from source. Implementation is tracked in Maestro feature UI Refresh - V3.0 (224b72a1-8d84-4970-97ea-5dafa4d8ea3b). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77 lines
9.6 KiB
HTML
77 lines
9.6 KiB
HTML
<style>
|
||
.dash { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr); gap: var(--space-6); align-items: start; }
|
||
.stats .stat { gap: 4px; }
|
||
.stats a.stat { border: 1px solid var(--border-hairline); }
|
||
.day { font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); font-weight: var(--fw-bold); padding: 14px 16px 6px; }
|
||
.feed { display: flex; flex-direction: column; }
|
||
.feed-row { all: unset; box-sizing: border-box; cursor: pointer; display: grid; grid-template-columns: 110px minmax(0, 1fr) auto auto; gap: 4px var(--space-4); align-items: center; padding: 10px 16px; border-top: 1px solid var(--border-hairline); font-size: 13px; }
|
||
.feed-row:hover { background: var(--accent-wash); }
|
||
.feed-row:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: -2px; }
|
||
.feed-row .job { font-weight: var(--fw-bold); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.feed-row .r { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.side-row { all: unset; box-sizing: border-box; cursor: pointer; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-3); align-items: center; padding: 10px 16px; border-top: 1px solid var(--border-hairline); font-size: 13px; width: 100%; }
|
||
.side-row:hover { background: var(--accent-wash); }
|
||
.side-row:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: -2px; }
|
||
.card-h { display: flex; align-items: center; gap: var(--space-3); padding: 14px 16px 10px; }
|
||
.card-h h2 { margin: 0; font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); }
|
||
.card-h a { margin-left: auto; font-size: var(--fs-label); }
|
||
.swrow { display: inline-flex; margin-left: 6px; vertical-align: middle; }
|
||
.swrow .swatch { margin-left: -6px; width: 18px; height: 18px; }
|
||
@media (max-width: 1180px) { .dash { grid-template-columns: minmax(0, 1fr); } }
|
||
@media (max-width: 620px) { .feed-row { grid-template-columns: minmax(0, 1fr) auto; } .feed-row .when { grid-column: 1 / -1; order: -1; } .feed-row .hide-s { display: none; } .attn { grid-template-columns: 36px minmax(0, 1fr); } .attn .ico { width: 36px; height: 36px; } .attn .btn { grid-column: 1 / -1; justify-content: center; } }
|
||
</style>
|
||
<script>
|
||
(function () {
|
||
var x = X;
|
||
function attention() {
|
||
var lowS = SPOOLS.filter(x.low), tareS = SPOOLS.filter(function (s) { return s.tare == null; }), need = x.printsNeeding(), down = PRINTERS.filter(function (p) { return !p.connected; });
|
||
var withSug = need.filter(function (p) { return p.suggested || (p.filaments || []).some(function (f) { return f.suggested; }); }).length;
|
||
var items = [];
|
||
down.forEach(function (p) { items.push('<div class="attn err"><span class="ico" aria-hidden="true">⚡</span><div><h3>' + x.esc(p.name) + ' lost its connection</h3><p>Last contact ' + x.ago(p.age) + '. Print capture is paused; <b>' + x.esc(p.job) + '</b> is still open and will be reconciled on reconnect.</p></div><a class="btn btn-sm" href="04-printers.html#printer=' + p.id + '">Open printer</a></div>'); });
|
||
if (need.length) items.push('<div class="attn warn"><span class="ico" aria-hidden="true">!</span><div><h3>' + need.length + ' prints need a spool or weight</h3><p>Their filament hasn\'t been deducted and their cost is incomplete.' + (withSug ? ' ' + withSug + ' have a same-color suggestion ready.' : '') + '</p></div><a class="btn btn-sm btn-primary" href="03-prints.html?tab=needs">Review ' + need.length + '</a></div>');
|
||
if (lowS.length) items.push('<div class="attn warn"><span class="ico" aria-hidden="true">▁</span><div><h3>' + lowS.length + ' spools under 200 g <span class="swrow">' + lowS.map(function (s) { return x.sw(s.c); }).join('') + '</span></h3><p>' + lowS.filter(function (s) { return s.cont; }).length + ' of them are loaded in a printer right now.</p></div><a class="btn btn-sm" href="02-filament.html?f=low">See low stock</a></div>');
|
||
if (tareS.length) items.push('<div class="attn info"><span class="ico" aria-hidden="true">?</span><div><h3>' + tareS.length + ' spools can\'t be weighed yet</h3><p>No empty-spool weight is known, so a scale reading can\'t be turned into filament. Picking a spool type fixes most.</p></div><a class="btn btn-sm" href="02-filament.html?f=tare">Set tares</a></div>');
|
||
return items.length ? items.join('') : '<div class="attn-done nm-inset">✓ Nothing needs you right now.</div>';
|
||
}
|
||
function feed() {
|
||
var groups = [];
|
||
PRINTS.slice(0, 7).forEach(function (p) { var w = x.when(p.start), d = /^(Today|Yesterday)/.test(w) ? w.split(' ')[0] : w.split(' ').slice(0, 2).join(' '); var last = groups[groups.length - 1]; if (!last || last.d !== d) groups.push(last = { d: d, list: [] }); last.list.push(p); });
|
||
return groups.map(function (grp) {
|
||
return '<div class="day">' + grp.d + '</div>' + grp.list.map(function (p) {
|
||
var n = x.needs(p), pr = x.printer(p.printer);
|
||
return '<button class="feed-row" type="button" data-print="' + p.id + '"><span class="when">' + x.printSt(p) + '<span class="sub">' + x.when(p.start).split(' ').pop() + '</span></span>' +
|
||
'<span style="min-width:0"><span class="job" style="display:block">' + x.esc(p.job) + '</span><span class="sub">' + x.esc(pr.model) + (p.open && !pr.connected ? ' · <span class="prov warn"><i>⚡</i>open across outage</span>' : '') + (n.length ? ' · <span class="prov warn"><i>!</i>needs ' + n.map(function (i) { return i.t; }).join(', ') + '</span>' : '') + '</span></span>' +
|
||
'<span class="r hide-s">' + x.durCell(p) + '</span><span class="r">' + (p.grams == null ? '<span class="muted">—</span>' : '<b>' + x.g(p.grams) + '</b><span class="sub">' + x.weightProv(p) + '</span>') + '</span></button>';
|
||
}).join('');
|
||
}).join('');
|
||
}
|
||
function printers() {
|
||
return PRINTERS.map(function (p) {
|
||
return '<a class="side-row" href="04-printers.html#printer=' + p.id + '"><span>' + x.printerSt(p) + '</span><span style="min-width:0"><b style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + x.esc(p.name) + '</b><span class="sub">' + (p.connected ? (p.job ? x.esc(p.job) : 'last: Benchy_x4') : 'last known: printing ' + x.esc(p.job)) + '</span></span><span class="sub">' + (p.connected ? x.ago(p.age) : '<span class="prov warn">' + x.ago(p.age) + '</span>') + '</span></a>';
|
||
}).join('');
|
||
}
|
||
function lows() {
|
||
return SPOOLS.filter(x.low).sort(function (a, b) { return a.rem - b.rem; }).map(function (s) {
|
||
return '<button class="side-row" type="button" data-spool="' + s.id + '">' + x.sw(s.c) + '<span style="min-width:0"><b>' + x.esc(s.color) + '</b> <span class="sub" style="display:inline">#' + s.id + '</span><span class="sub">' + x.esc(x.fullName(s)) + ' · ' + x.esc(x.where(s).short) + '</span></span><span class="num strong" style="color:var(--status-warning)">' + x.g(s.rem) + '</span></button>';
|
||
}).join('');
|
||
}
|
||
function render() {
|
||
var today = PRINTS.filter(function (p) { return /^Today/.test(x.when(p.start)) && p.grams != null; });
|
||
var usedG = today.reduce(function (a, p) { return a + p.grams; }, 0), usedC = today.reduce(function (a, p) { return a + (p.cost || 0); }, 0);
|
||
return '<header class="page-head"><div><h1>Dashboard</h1><p class="sum">Friday 18 September · <b>' + PRINTERS.length + '</b> printers · <b>' + CANARY.spools + '</b> spools</p></div><button class="btn btn-primary primary-desktop" type="button" data-add-spool>+ Add spool</button></header>' +
|
||
'<section aria-labelledby="needs-h" class="stack" style="gap:12px"><h2 class="section" id="needs-h" style="margin:0">Needs you</h2><div class="attn-list">' + attention() + '</div></section>' +
|
||
'<section class="stats" aria-label="Stock">' +
|
||
'<a class="stat nm-raised tile-link" href="02-filament.html"><span class="stat-label">Spools</span><span class="stat-value">' + CANARY.spools + '</span><span class="stat-foot">' + CANARY.brands + ' brands · 6 materials →</span></a>' +
|
||
'<div class="stat nm-raised"><span class="stat-label">Filament left</span><span class="stat-value">' + CANARY.kg.toFixed(2) + ' kg</span><span class="stat-foot">net: filament only, no spools</span></div>' +
|
||
'<div class="stat nm-raised"><span class="stat-label">Stock value</span><span class="stat-value">' + x.usd(CANARY.value) + '</span><span class="stat-foot">what\'s left × each $/kg</span></div>' +
|
||
'<a class="stat nm-raised tile-link" href="03-prints.html"><span class="stat-label">Used today</span><span class="stat-value">' + x.g(usedG) + '</span><span class="stat-foot">' + x.usd(usedC) + ' · ' + today.length + ' done · 2 running →</span></a>' +
|
||
'</section>' +
|
||
'<div class="dash"><section class="nm-raised" style="padding:0;overflow:hidden" aria-labelledby="act-h"><div class="card-h"><h2 id="act-h">Recent prints</h2><a href="03-prints.html">All prints →</a></div><div class="feed">' + feed() + '</div></section>' +
|
||
'<div class="stack" style="gap:24px"><section class="nm-raised" style="padding:0;overflow:hidden" aria-labelledby="pr-h"><div class="card-h"><h2 id="pr-h">Printers</h2><a href="04-printers.html">Open →</a></div>' + printers() + '</section>' +
|
||
'<section class="nm-raised" style="padding:0;overflow:hidden" aria-labelledby="low-h"><div class="card-h"><h2 id="low-h">Running low</h2><a href="02-filament.html?f=low">All →</a></div>' + lows() + '</section></div></div>' +
|
||
'<button class="btn btn-primary fab" type="button" data-add-spool aria-label="Add spool">+</button>';
|
||
}
|
||
X.init({ name: 'dashboard', refresh: function () { document.getElementById('page').innerHTML = render(); } }, render);
|
||
})();
|
||
</script>
|