77 lines
9.3 KiB
HTML
77 lines
9.3 KiB
HTML
<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>
|