116 lines
14 KiB
HTML
116 lines
14 KiB
HTML
<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: 1180px) { .wl { display: none; } .rowact .btn { white-space: nowrap; } }
|
||
@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: ({ tare: 'unverified' })[q.get('f')] || 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; }],
|
||
['unverified', 'Empty spool weight unverified', x.unverified]
|
||
];
|
||
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 hide-t"><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" aria-label="Weigh ' + x.esc(s.color) + ' #' + s.id + '">⚖<span class="wl"> Weigh</span></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] === 'unverified') && 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? Weigh all ' + weighable.length + ', lightest first. Each step shows where the spool lives and the result before you save.</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 hide-t') : '<th class="r hide-m">Empty spool</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; }).sort(function (a, b) { return a.rem - b.rem; }); x.D.list = first.map(function (s) { return s.id; }); x.D.session = true; x.openDrawer('spool', first[0].id, 'weigh'); x.D.session = true; x.D.list = first.map(function (s) { return s.id; }); 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>
|