Files
design-v3/iteration-2/source/pages/02-filament.html
T
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

115 lines
14 KiB
HTML
Raw 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>
.toolbar { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.toolbar .row1 { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.searchbox { position: relative; flex: 1; min-width: 220px; }
.searchbox svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-secondary); }
.searchbox .input { padding-left: 36px; }
.ctl { display: inline-flex; gap: 8px; align-items: center; font-size: var(--fs-label); color: var(--text-secondary); }
.ctl .select { min-height: 36px; padding: 6px 10px; width: auto; font-size: 13px; }
.grp td { background: var(--surface-inset); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); font-weight: var(--fw-bold); color: var(--text-secondary); padding: 8px 12px; }
.spcell { display: flex; gap: var(--space-3); align-items: center; min-width: 220px; }
.remcell { min-width: 160px; }
.remcell .top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
.rowact { display: flex; gap: 6px; justify-content: flex-end; }
.rowact .btn { min-height: 30px; padding: 4px 10px; }
.weigh-hint { display: flex; gap: var(--space-3); align-items: center; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); background: var(--accent-wash); border: 1px solid var(--text-accent); font-size: 13px; }
@media (max-width: 760px) { .hide-m { display: none; } .spcell { min-width: 0; } .remcell { min-width: 110px; } .rowact .menu-wrap .btn { padding: 4px 8px; } }
</style>
<script>
(function () {
var x = X;
var q = new URLSearchParams(location.search);
var S = { view: q.get('f') || 'all', q: q.get('q') || '', mat: '', sort: 'rem', dir: 1, cols: 'stock', group: 'none' };
var VIEWS = [
['all', 'All', function () { return true; }],
['low', 'Low', x.low],
['loaded', 'Loaded', function (s) { return !!s.cont; }],
['storage', 'In storage', function (s) { return !s.cont; }],
['tare', 'Tare unknown', function (s) { return s.tare == null; }]
];
function viewFn(k) { for (var i = 0; i < VIEWS.length; i++) if (VIEWS[i][0] === k) return VIEWS[i]; return VIEWS[0]; }
function list() {
var f = viewFn(S.view)[2], qq = S.q.toLowerCase();
var l = SPOOLS.filter(function (s) { return f(s) && (!S.mat || s.mat === S.mat) && (!qq || ('#' + s.id + ' ' + x.fullName(s) + ' ' + s.color + ' ' + x.where(s).text).toLowerCase().indexOf(qq) >= 0); });
var key = { rem: function (s) { return s.rem; }, name: function (s) { return (s.color + s.brand).toLowerCase(); }, value: function (s) { return x.value(s); }, gross: function (s) { return x.gross(s) == null ? -1 : x.gross(s); } }[S.sort];
l.sort(function (a, b) { var A = key(a), B = key(b); return (A < B ? -1 : A > B ? 1 : 0) * S.dir; });
if (S.group !== 'none') { var gk = S.group === 'mat' ? function (s) { return s.mat; } : function (s) { return x.where(s).loaded ? x.where(s).printer.name : 'Storage'; }; l.sort(function (a, b) { return gk(a) < gk(b) ? -1 : gk(a) > gk(b) ? 1 : 0; }); l.gk = gk; }
return l;
}
function th(label, key, cls) { var s = S.sort === key ? (S.dir === 1 ? 'ascending' : 'descending') : 'none'; return '<th class="' + (cls || '') + '" aria-sort="' + s + '"><button class="sort" type="button" data-sort="' + key + '">' + label + '</button></th>'; }
function rowMenu(s) {
return '<div class="menu-wrap"><button class="btn btn-sm btn-icon" type="button" data-menu="rm' + s.id + '" aria-haspopup="menu" aria-expanded="false" aria-label="Actions for ' + x.esc(s.color + ' ' + s.brand) + '">⋯</button><div class="menu" id="rm' + s.id + '" role="menu">' +
'<button role="menuitem" type="button" data-spool="' + s.id + '">Open<span class="hint">↵</span></button><button role="menuitem" type="button" data-spool="' + s.id + '" data-tab-open="weigh">Weigh</button><button role="menuitem" type="button" data-spool="' + s.id + '" data-tab-open="move">' + (s.cont ? 'Move / unload' : 'Load into printer') + '</button><button role="menuitem" type="button" data-spool="' + s.id + '" data-tab-open="edit">Edit</button>' +
(s.label ? '' : '<button role="menuitem" type="button" data-fake="Label PDF generated for #' + s.id + '">Print label</button>') + '<hr><button role="menuitem" type="button" class="danger" data-spool="' + s.id + '" data-then-delete>Delete…</button></div></div>';
}
function rows(l) {
var out = '', last = null, cols = S.cols === 'stock' ? 5 : 5;
l.forEach(function (s) {
if (l.gk) { var gv = l.gk(s); if (gv !== last) { var n = l.filter(function (y) { return l.gk(y) === gv; }), tot = n.reduce(function (a, y) { return a + y.rem; }, 0); out += '<tr class="grp"><td colspan="' + cols + '">' + x.esc(gv) + ' · ' + n.length + ' spools · ' + x.kg(tot) + '</td></tr>'; last = gv; } }
var w = x.where(s), gr = x.gross(s);
out += '<tr class="clickable" tabindex="0" data-row-spool="' + s.id + '" aria-label="' + x.esc(s.color + ', ' + x.fullName(s) + ', ' + s.rem + ' grams') + '">' +
'<td><div class="spcell">' + x.sw(s.c) + '<span style="min-width:0"><span class="strong">' + x.esc(s.color) + '</span> <span class="sub" style="display:inline">#' + s.id + '</span><span class="sub">' + x.esc(x.fullName(s)) + '</span></span></div></td>' +
'<td><div class="remcell"><div class="top"><b class="num"' + (x.low(s) ? ' style="color:var(--status-warning)"' : '') + '>' + x.g(s.rem) + '</b><span class="sub" style="display:inline">of ' + x.g(s.tot) + '</span></div>' + x.meter(s) + '</div></td>' +
(S.cols === 'stock'
? '<td class="hide-m">' + (w.loaded ? '<span class="st run" style="font-weight:500">' + x.esc(w.short) + '</span>' : '<span class="muted">' + x.esc(w.text) + '</span>') + '</td><td class="r hide-m"><span class="num">' + x.usd(x.value(s)) + '</span><span class="sub">' + x.usd(s.kg) + '/kg</span></td>'
: '<td class="r hide-m">' + (s.tare == null ? '<span class="prov warn"><i>?</i>unknown</span>' : '<span class="num">' + x.g(s.tare) + '</span><span class="sub">' + x.tareProv(s) + '</span>') + '</td><td class="r hide-m">' + (gr == null ? '<span class="muted">unknown</span>' : '<b class="num">' + x.g(gr) + '</b><span class="sub">' + x.remProv(s) + '</span>') + '</td>') +
'<td><div class="rowact">' + (s.tare != null ? '<button class="btn btn-sm hide-m" type="button" data-spool="' + s.id + '" data-tab-open="weigh">⚖ Weigh</button>' : '') + rowMenu(s) + '</div></td></tr>';
});
return out;
}
function render() {
var l = list(), all = SPOOLS.length;
x.D.list = l.map(function (s) { return s.id; });
var chips = [];
if (S.view !== 'all') chips.push(['view', viewFn(S.view)[1]]);
if (S.mat) chips.push(['mat', S.mat]);
if (S.q) chips.push(['q', '“' + S.q + '”']);
var weighable = l.filter(function (s) { return s.tare != null; });
return '<header class="page-head"><div><h1>Filament</h1><p class="sum"><b>' + CANARY.spools + '</b> spools · <b>' + CANARY.kg.toFixed(2) + ' kg</b> net · <b>' + x.usd(CANARY.value) + '</b> · sample page of ' + all + '</p></div>' +
'<div class="actions"><div class="menu-wrap"><button class="btn" type="button" data-menu="pgmenu" aria-haspopup="menu" aria-expanded="false">More ▾</button><div class="menu" id="pgmenu" role="menu"><button role="menuitem" type="button" data-fake="Labels PDF for 4 spools generated">Print ' + SPOOLS.filter(function (s) { return !s.label; }).length + ' missing labels</button><button role="menuitem" type="button" data-soon="Lists">Manage lists (brands, materials…)</button></div></div>' +
'<button class="btn btn-primary primary-desktop" type="button" data-add-spool>+ Add spool</button></div></header>' +
'<section class="toolbar nm-raised" aria-label="Find spools">' +
'<div class="tabs" role="tablist" aria-label="Saved views">' + VIEWS.map(function (v) { var n = SPOOLS.filter(v[2]).length; return '<button role="tab" type="button" data-view="' + v[0] + '" aria-selected="' + (S.view === v[0]) + '">' + v[1] + ' <span class="n' + ((v[0] === 'low' || v[0] === 'tare') && n ? ' warn' : '') + '">' + n + '</span></button>'; }).join('') + '</div>' +
'<div class="row1"><label class="searchbox">' + X.ICON.search + '<input class="input" id="fq" type="search" value="' + x.esc(S.q) + '" placeholder="Filter by color, brand, #id, location…" aria-label="Filter spools"></label>' +
'<label class="ctl">Material <select class="select" id="fmat"><option value="">Any</option>' + ['PLA', 'PETG', 'ASA', 'TPU', 'PA6', 'ABS'].map(function (m) { return '<option' + (S.mat === m ? ' selected' : '') + '>' + m + '</option>'; }).join('') + '</select></label>' +
'<label class="ctl">Group <select class="select" id="fgroup"><option value="none">None</option><option value="mat"' + (S.group === 'mat' ? ' selected' : '') + '>Material</option><option value="loc"' + (S.group === 'loc' ? ' selected' : '') + '>Location</option></select></label>' +
'<div class="tabs" role="group" aria-label="Columns"><button type="button" data-cols="stock" aria-selected="' + (S.cols === 'stock') + '">Stock</button><button type="button" data-cols="weights" aria-selected="' + (S.cols === 'weights') + '">Weights</button></div></div>' +
(chips.length ? '<div class="fchips" aria-label="Active filters"><span class="sub" style="display:inline">' + l.length + ' of ' + all + ' shown</span>' + chips.map(function (c) { return '<span class="fchip">' + x.esc(c[1]) + '<button type="button" data-unchip="' + c[0] + '" aria-label="Remove filter ' + x.esc(c[1]) + '">✕</button></span>'; }).join('') + '<button class="linkish small" type="button" data-unchip="all">Clear all</button></div>' : '') +
'</section>' +
(weighable.length > 1 && S.view !== 'all' ? '<div class="weigh-hint"><b>⚖</b><span style="flex:1">Weighing these? Step through all ' + weighable.length + ' with a scale: each result shows before you save, then the next one opens.</span><button class="btn btn-sm btn-primary" type="button" data-weigh-session>Start weighing</button></div>' : '') +
'<section class="nm-raised table-card" aria-label="Spools">' + (l.length ? '<div class="table-scroll"><table class="tbl"><thead><tr>' + th('Spool', 'name') + th('Remaining <abbr title="net filament">net</abbr>', 'rem') +
(S.cols === 'stock' ? '<th class="hide-m">Location</th>' + th('Value', 'value', 'r hide-m') : '<th class="r hide-m">Tare</th>' + th('Gross (scale)', 'gross', 'r hide-m')) + '<th class="r"><span class="sr-only">Actions</span></th></tr></thead><tbody>' + rows(l) + '</tbody></table></div>' +
'<div class="table-foot"><span class="muted">Click a row or press ↵ to open · <span class="kbd">j</span>/<span class="kbd">k</span> step through in the panel</span><span class="muted">1–' + l.length + ' of ' + (S.view === 'all' && !S.q && !S.mat ? CANARY.spools : l.length) + '</span></div>'
: '<div class="empty"><h3>No spools match</h3><p>' + (S.q ? 'Nothing matches “' + x.esc(S.q) + '”' : 'Nothing in this view') + (S.mat ? ' in ' + S.mat : '') + '.</p><button class="btn btn-sm" type="button" data-unchip="all">Clear filters</button></div>') + '</section>' +
'<button class="btn btn-primary fab" type="button" data-add-spool aria-label="Add spool">+</button>';
}
function redraw(keepFocus) {
var a = document.activeElement, id = a && a.id, pos = a && a.selectionStart;
document.getElementById('page').innerHTML = render();
if (keepFocus && id && document.getElementById(id)) { var el = document.getElementById(id); el.focus(); if (pos != null && el.setSelectionRange) el.setSelectionRange(pos, pos); }
var p = new URLSearchParams(); if (S.view !== 'all') p.set('f', S.view); if (S.q) p.set('q', S.q);
history.replaceState(history.state, '', location.pathname + (p.toString() ? '?' + p : '') + location.hash);
if (x.D.kind === 'spool') document.querySelectorAll('[data-row-spool="' + x.D.id + '"]').forEach(function (r) { r.classList.add('is-selected'); });
}
document.addEventListener('click', function (e) {
var t = e.target, a;
if ((a = t.closest('[data-view]'))) { S.view = a.getAttribute('data-view'); return redraw(); }
if ((a = t.closest('[data-cols]'))) { S.cols = a.getAttribute('data-cols'); return redraw(); }
if ((a = t.closest('[data-sort]'))) { var k = a.getAttribute('data-sort'); S.dir = S.sort === k ? -S.dir : 1; S.sort = k; return redraw(); }
if ((a = t.closest('[data-unchip]'))) { var c = a.getAttribute('data-unchip'); if (c === 'view' || c === 'all') S.view = 'all'; if (c === 'mat' || c === 'all') S.mat = ''; if (c === 'q' || c === 'all') S.q = ''; return redraw(); }
if (t.closest('[data-weigh-session]')) { var first = list().filter(function (s) { return s.tare != null; }); x.D.list = first.map(function (s) { return s.id; }); x.openDrawer('spool', first[0].id, 'weigh'); return; }
if ((a = t.closest('[data-then-delete]'))) { setTimeout(function () { var b = document.querySelector('#drawer [data-delete]'); if (b) b.click(); }, 50); return; }
if (t.closest('button, a, .menu')) return;
if ((a = t.closest('[data-row-spool]'))) { x.D.list = list().map(function (s) { return s.id; }); x.openDrawer('spool', +a.getAttribute('data-row-spool')); }
});
document.addEventListener('input', function (e) {
if (e.target.id === 'fq') { S.q = e.target.value; redraw(true); }
});
document.addEventListener('change', function (e) {
if (e.target.id === 'fmat') { S.mat = e.target.value; redraw(); }
if (e.target.id === 'fgroup') { S.group = e.target.value; redraw(); }
});
X.init({ name: 'filament', refresh: function () { redraw(); } }, render);
})();
</script>