Files
Joshua KingandClaude Opus 5 d9ed1898b9 Add the Extrudex Design System v3 bundle
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>
2026-09-19 11:31:50 -04:00

77 lines
9.6 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.
<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>