Files
design-v3/06-labels.html
2026-09-25 10:26:24 -04:00

77 lines
9.3 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>
.lab { aspect-ratio: 51 / 21; width: 100%; max-width: 340px; background: #fff; color: #111; border-radius: 6px; display: grid; grid-template-columns: 37.25% 1fr; align-items: center; padding: 2% 3% 2% 2%; box-shadow: 0 1px 0 rgba(0,0,0,.2), var(--nm-raised-1); border: 1px solid var(--border-control); font-family: var(--font-mono); overflow: hidden; }
.lab svg { width: 92%; height: auto; display: block; }
.lab .t { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.lab .h { font-weight: 800; font-size: clamp(10px, 3.6cqw, 15px); line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lab .d { font-size: clamp(8px, 2.6cqw, 11px); line-height: 1.2; }
.lab .id { font-size: clamp(9px, 3cqw, 12px); font-weight: 700; }
.labwrap { container-type: inline-size; }
.lgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: var(--space-4); }
.lcard { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.lcard.off { opacity: .55; }
.lcard .top { display: flex; gap: var(--space-3); align-items: center; }
.chk { width: 20px; height: 20px; accent-color: var(--text-accent); }
.bar { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; padding: var(--space-3) var(--space-4); }
.spec { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: var(--fs-label); margin: 0; }
.spec dt { color: var(--text-secondary); }
.spec dd { margin: 0; }
</style>
<script>
(function () {
var x = X;
var q = new URLSearchParams(location.search);
var S = { tab: q.get('tab') || 'needed', sel: {} };
SPOOLS.filter(function (s) { return !s.label; }).forEach(function (s) { S.sel[s.id] = true; });
/* A deterministic QR-looking pattern (not a real code) with the three finder squares. */
function qr(seed) {
var n = 25, cells = '', r = seed * 9301 + 49297;
function rnd() { r = (r * 9301 + 49297) % 233280; return r / 233280; }
function finder(ox, oy) { return '<rect x="' + ox + '" y="' + oy + '" width="7" height="7" fill="#111"/><rect x="' + (ox + 1) + '" y="' + (oy + 1) + '" width="5" height="5" fill="#fff"/><rect x="' + (ox + 2) + '" y="' + (oy + 2) + '" width="3" height="3" fill="#111"/>'; }
for (var yy = 0; yy < n; yy++) for (var xx = 0; xx < n; xx++) {
var inF = (xx < 8 && yy < 8) || (xx > n - 9 && yy < 8) || (xx < 8 && yy > n - 9);
if (!inF && rnd() > 0.52) cells += '<rect x="' + xx + '" y="' + yy + '" width="1" height="1"/>';
}
return '<svg viewBox="-1 -1 ' + (n + 2) + ' ' + (n + 2) + '" aria-hidden="true" shape-rendering="crispEdges"><rect x="-1" y="-1" width="' + (n + 2) + '" height="' + (n + 2) + '" fill="#fff"/><g fill="#111">' + cells + '</g>' + finder(0, 0) + finder(n - 7, 0) + finder(0, n - 7) + '</svg>';
}
function spoolLabel(s) {
var d = [s.mat, s.type, s.speed, s.fin].filter(Boolean).join(' ');
return '<div class="labwrap"><div class="lab" role="img" aria-label="Label preview: ' + x.esc(s.brand + ' / ' + s.color + ', ' + d + ', #' + s.id) + '">' + qr(s.id) + '<div class="t"><span class="h">' + x.esc(s.brand + ' / ' + s.color) + '</span><span class="d">' + x.esc(d) + '</span><span class="id">#' + s.id + '</span></div></div></div>';
}
function holderLabel(pr, c) {
var kind = { ams: 'AMS', ams_lite: 'AMS Lite', external: 'External' }[c.kind], id = X.holderIds[c.id];
return '<div class="labwrap"><div class="lab" role="img" aria-label="Holder label preview">' + qr(100 + id) + '<div class="t"><span class="h" style="font-size:clamp(9px,3cqw,13px)">Printer: ' + x.esc(pr.name) + '</span><span class="d">Container: ' + x.esc(c.label + ' / ' + kind) + '</span><span class="id">#' + id + '</span></div></div></div>';
}
function needed() {
var l = SPOOLS.filter(function (s) { return !s.label; }), n = l.filter(function (s) { return S.sel[s.id]; }).length;
if (!l.length) return '<div class="nm-raised empty"><h3>✓ Every spool has a label</h3><p>New spools land here until their label is generated.</p></div>';
return '<div class="bar nm-inset"><label class="row small" style="gap:8px"><input type="checkbox" class="chk" data-all ' + (n === l.length ? 'checked' : '') + '> All ' + l.length + '</label><span class="spacer"></span><span class="small muted">One PDF, one 51 × 21 mm page per label</span><button class="btn btn-primary" type="button" ' + (n ? 'data-gen="' + n + '"' : 'disabled') + '>Download ' + n + ' label' + (n === 1 ? '' : 's') + ' (PDF)</button></div>' +
'<div class="lgrid">' + l.map(function (s) { return '<article class="lcard nm-raised' + (S.sel[s.id] ? '' : ' off') + '"><div class="top"><input type="checkbox" class="chk" data-s="' + s.id + '" ' + (S.sel[s.id] ? 'checked' : '') + ' aria-label="Include ' + x.esc(s.color) + ' #' + s.id + '"><span style="flex:1;min-width:0"><b>' + x.esc(s.color) + '</b> <span class="sub" style="display:inline">#' + s.id + '</span><span class="sub">' + x.esc(x.where(s).text) + '</span></span><button class="btn btn-sm" type="button" data-spool="' + s.id + '">Open</button></div>' + spoolLabel(s) + '</article>'; }).join('') + '</div>' +
'<p class="small muted" style="margin:0">A spool counts as labeled once its PDF is generated successfully. Editing the spool later doesn\'t clear that. Reprint any label from the <b>All spools</b> tab.</p>';
}
function all() {
return '<section class="nm-raised table-card"><div class="table-scroll"><table class="tbl"><thead><tr><th>Spool</th><th class="hide-m">Label text</th><th>Status</th><th class="r"><span class="sr-only">Actions</span></th></tr></thead><tbody>' +
SPOOLS.map(function (s) { return '<tr><td><div class="row" style="gap:10px;flex-wrap:nowrap">' + x.sw(s.c, 'swatch-sm') + '<span><b>' + x.esc(s.color) + '</b> <span class="sub" style="display:inline">#' + s.id + '</span></span></div></td><td class="hide-m"><span class="small">' + x.esc(s.brand + ' / ' + s.color) + '</span><span class="sub">' + x.esc([s.mat, s.type, s.speed, s.fin].filter(Boolean).join(' ')) + '</span></td><td>' + (s.label ? '<span class="st ok">Printed</span>' : '<span class="st warn">Not printed</span>') + '</td><td class="r"><button class="btn btn-sm" type="button" data-fake="Label PDF for #' + s.id + ' generated">' + (s.label ? 'Reprint' : 'Print') + '</button></td></tr>'; }).join('') + '</tbody></table></div></section>';
}
function holders() {
return '<p class="small muted" style="margin:0">Stick one on each AMS and external holder. Scanning it with a phone starts a <a href="09-scan-holder.html">holder scan session</a>: every spool label scanned next is loaded, returned or moved there. No mode to pick.</p>' +
PRINTERS.map(function (pr) { return '<section class="stack" style="gap:10px"><h2 class="section" style="margin:0">' + x.esc(pr.name) + '</h2><div class="lgrid">' + pr.containers.map(function (c) { return '<article class="lcard nm-raised"><div class="top"><b style="flex:1">' + x.esc(c.label) + '</b><span class="sub">' + x.contents(c.id).length + ' / ' + c.cap + '</span><button class="btn btn-sm" type="button" data-fake="Holder label #' + X.holderIds[c.id] + ' PDF generated">Print</button></div>' + holderLabel(pr, c) + '</article>'; }).join('') + '</div></section>'; }).join('');
}
function render() {
var need = SPOOLS.filter(function (s) { return !s.label; }).length;
return '<header class="page-head"><div><h1>Labels</h1><p class="sum">51 × 21 mm labels with a QR code. Scanning a spool label opens it; scanning a holder label starts loading into it.</p></div></header>' +
'<div class="tabs" role="tablist" aria-label="Label lists">' + [['needed', 'Needs a label', need, 1], ['all', 'All spools', SPOOLS.length], ['holders', 'Holders', 6]].map(function (t) { return '<button role="tab" type="button" data-ltab="' + t[0] + '" aria-selected="' + (S.tab === t[0]) + '">' + t[1] + ' <span class="n' + (t[3] && t[2] ? ' warn' : '') + '">' + t[2] + '</span></button>'; }).join('') + '</div>' +
(S.tab === 'needed' ? needed() : S.tab === 'all' ? all() : holders()) +
'<details class="nm-inset panel"><summary class="small strong" style="cursor:pointer">Label format</summary><dl class="spec" style="margin-top:10px"><dt>Size</dt><dd>51 × 21 mm, one label per PDF page</dd><dt>QR</dt><dd>19 mm, left; encodes <code>/s/‹id›</code> or <code>/c/‹id›</code></dd><dt>Line 1</dt><dd>Brand / Color (holder: Printer: name)</dd><dt>Line 2</dt><dd>Material, type, speed, finish (holder: Container: label / kind)</dd><dt>Line 3</dt><dd>#id</dd></dl></details>';
}
function redraw() { document.getElementById('page').innerHTML = render(); history.replaceState(null, '', location.pathname + '?tab=' + S.tab); }
document.addEventListener('click', function (e) {
var t = e.target, a;
if ((a = t.closest('[data-ltab]'))) { S.tab = a.getAttribute('data-ltab'); return redraw(); }
if (t.matches('[data-s]')) { S.sel[t.getAttribute('data-s')] = t.checked; return redraw(); }
if (t.matches('[data-all]')) { SPOOLS.filter(function (s) { return !s.label; }).forEach(function (s) { S.sel[s.id] = t.checked; }); return redraw(); }
if ((a = t.closest('[data-gen]'))) { SPOOLS.forEach(function (s) { if (S.sel[s.id]) s.label = true; }); X.toast('Downloaded <b>extrudex-labels.pdf</b> · ' + a.getAttribute('data-gen') + ' labels, now marked printed'); return redraw(); }
});
X.init({ name: 'labels', refresh: redraw }, render);
})();
</script>