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

80 lines
10 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>
.scanwrap { max-width: 560px; width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: var(--space-4); }
.hhead { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
.hhead h1 { margin: 0; font-size: var(--fs-h2); line-height: 1.2; }
.cap { display: flex; gap: 6px; }
.cap i { flex: 1; height: 12px; border-radius: 4px; border: 1px solid var(--border-control); background: var(--surface-inset); }
.cap i.on { background: var(--text-accent); border-color: var(--text-accent); }
.bigscan { min-height: 64px; font-size: 17px; justify-content: center; width: 100%; border-radius: var(--radius-lg); }
.rule { font-size: var(--fs-label); color: var(--text-secondary); display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; }
.rule b { color: var(--text-primary); }
.result { padding: var(--space-4); border-radius: var(--radius-lg); border: 2px solid var(--status-success); background: var(--wash-success); display: grid; grid-template-columns: 44px 1fr; gap: 4px 12px; align-items: center; }
.result.err { border-color: var(--status-error); background: var(--wash-error); }
.result .verb { font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); font-weight: 800; }
.result.ok .verb { color: var(--status-success); } .result.err .verb { color: var(--status-error); }
.row-sp { all: unset; box-sizing: border-box; cursor: pointer; width: 100%; display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; padding: 12px 16px; border-top: 1px solid var(--border-hairline); font-size: 14px; min-height: 56px; }
.row-sp:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: -2px; }
.cam-sheet { position: fixed; inset: 0; z-index: 80; background: #0b0b0d; display: none; flex-direction: column; color: #f4f4f6; }
.cam-sheet.open { display: flex; }
.vf { flex: 1; position: relative; display: grid; place-items: center; background: radial-gradient(ellipse at 50% 55%, #2a2a31 0, #0b0b0d 70%); }
.vf .frame { width: min(64vw, 280px); aspect-ratio: 1; border-radius: 24px; box-shadow: 0 0 0 2000px rgba(0,0,0,.45); border: 3px solid #a99af0; position: relative; }
.vf .hint { position: absolute; bottom: 18%; left: 0; right: 0; text-align: center; font-size: 14px; }
.cam-list { background: #1b1b1e; padding: 12px 16px 24px; display: flex; flex-direction: column; gap: 8px; max-height: 45vh; overflow: auto; }
.cam-list button { all: unset; cursor: pointer; display: flex; gap: 12px; align-items: center; padding: 12px; border-radius: 12px; background: #2c2c30; border: 1px solid #85858f; color: #f4f4f6; font-size: 14px; }
.cam-list button:focus-visible { outline: 2px solid #a99af0; outline-offset: 2px; }
.reader-note { display: none; }
body.as-reader .reader-note { display: flex; }
body.as-reader [data-needs-op] { opacity: .5; pointer-events: none; }
</style>
<script>
(function () {
var x = X;
var ids = X.holderIds, byNum = {}; Object.keys(ids).forEach(function (k) { byNum[ids[k]] = k; });
var q = new URLSearchParams(location.search), cid = byNum[+q.get('c')] || 'p3-lite';
var S = { last: null, log: [] };
function hold() { return x.container(cid); }
function apply(sid) {
var s = x.spool(sid), h = hold(), here = x.contents(cid), before = x.where(s).text;
if (!s) return { err: true, verb: 'Not found', text: 'No spool #' + sid + '. Scan a spool label, or type its number.' };
if (s.cont === cid) { s.cont = null; s.loc = s.loc || 'Storage'; return { verb: 'Returned to storage', s: s, text: 'Was in ' + h.c.label + '. Put it back on its shelf' + (s.loc && s.loc !== 'Storage' ? ' (' + s.loc + ')' : '') + '.' }; }
if (here.length >= h.c.cap) return { err: true, verb: 'Full', s: s, text: h.c.label + ' already holds ' + h.c.cap + '. Scan a spool that\'s in it to return that one first. Nothing changed.' };
var was = s.cont; s.cont = cid; s.v++;
return { verb: was ? 'Moved here' : 'Loaded', s: s, text: (was ? 'Was in ' : 'Was on ') + before + '.' };
}
function resultCard(r) {
if (!r) return '';
return '<div class="result ' + (r.err ? 'err' : 'ok') + '" role="status" aria-live="polite">' + (r.s ? x.sw(r.s.c, '" style="width:44px;height:44px') : '<span class="avatar" style="font-size:22px;text-align:center">?</span>') + '<span><span class="verb">' + (r.err ? '✕ ' : '✓ ') + r.verb + '</span><br>' + (r.s ? '<b>' + x.esc(r.s.color) + '</b> <span class="muted">#' + r.s.id + ' · ' + x.esc(r.s.brand) + '</span>' : '') + '</span><span></span><span class="small">' + x.esc(r.text) + '</span></div>';
}
function render() {
var h = hold(), here = x.contents(cid), free = h.c.cap - here.length;
return '<div class="scanwrap">' +
'<nav class="crumbs" aria-label="Breadcrumb"><a href="04-printers.html#printer=' + h.printer.id + '">' + x.esc(h.printer.name) + '</a> › <span>Holder #' + ids[cid] + '</span></nav>' +
'<section class="hhead nm-raised"><span class="sub">Scan session · holder #' + ids[cid] + '</span><h1>' + x.esc(h.c.label) + ' <span class="muted" style="font-weight:500">· ' + x.esc(h.printer.model) + '</span></h1>' +
'<div class="cap" aria-label="' + here.length + ' of ' + h.c.cap + ' in use">' + Array.apply(null, Array(h.c.cap)).map(function (_, i) { return '<i class="' + (i < here.length ? 'on' : '') + '"></i>'; }).join('') + '</div><span class="small">' + here.length + ' in use · <b>' + free + ' free</b></span></section>' +
'<div class="banner reader-note" role="note"><span>You\'re signed in as a <b>reader</b>. You can see what\'s recorded here; an operator or admin can move spools.</span></div>' +
'<button class="btn btn-primary bigscan" type="button" data-cam data-needs-op>▣ Scan a spool label</button>' +
'<div class="row" style="gap:8px" data-needs-op><input class="input num" id="manual" inputmode="numeric" placeholder="or type spool # (e.g. 49)" aria-label="Spool number" style="flex:1"><button class="btn" type="button" data-manual>Go</button></div>' +
resultCard(S.last) +
'<details class="nm-inset panel" ' + (S.log.length ? '' : 'open') + '><summary class="small strong" style="cursor:pointer">What a scan does</summary><div class="rule" style="margin-top:10px"><b>In storage</b><span>→ loads here (if there\'s room)</span><b>Already here</b><span>→ returns to storage</span><b>In another holder</b><span>→ moves here</span></div><p class="sub" style="margin:8px 0 0">No mode to choose. This holder stays active for this browser tab, so every spool label you scan next lands here until you tap Done.</p></details>' +
'<section class="nm-raised" style="padding:0;overflow:hidden" aria-labelledby="inh"><div class="card-h" style="display:flex;padding:14px 16px 8px"><h2 id="inh" style="margin:0;font-size:var(--fs-label);text-transform:uppercase;letter-spacing:var(--tracking-caps-wide);color:var(--text-secondary)">Recorded in ' + x.esc(h.c.label) + '</h2></div>' +
(here.length ? here.map(function (s) { return '<button class="row-sp" 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)) + '</span></span><span class="num' + (x.low(s) ? '" style="color:var(--status-warning);font-weight:700' : '') + '">' + x.g(s.rem) + '</span></button>'; }).join('') : '<p class="small muted" style="padding:0 16px 16px;margin:0">Empty.</p>') + '</section>' +
(S.log.length ? '<section class="stack" style="gap:6px"><h2 class="section" style="margin:0">This session</h2>' + S.log.map(function (r) { return '<span class="small">' + (r.err ? '<span class="prov warn"><i>✕</i></span>' : '✓') + ' ' + r.verb + (r.s ? ' · ' + x.esc(r.s.color) + ' #' + r.s.id : '') + '</span>'; }).join('') + '</section>' : '') +
'<div class="row" style="justify-content:space-between"><label class="row small" style="gap:6px"><input type="checkbox" data-reader> Demo: view as reader</label><a class="btn" href="04-printers.html#printer=' + h.printer.id + '">Done scanning</a></div></div>' +
'<div class="cam-sheet" id="cam" role="dialog" aria-modal="true" aria-label="Camera"><div class="row" style="padding:14px 16px;justify-content:space-between"><b>Point at a spool label</b><button class="btn btn-sm" type="button" data-cam-close style="background:#2c2c30;color:#f4f4f6">Close</button></div><div class="vf"><div class="frame"></div><span class="hint">Demo: tap a label below to simulate a scan</span></div><div class="cam-list">' +
SPOOLS.filter(function (s) { return [49, 41, 47, 56, 43].indexOf(s.id) >= 0; }).map(function (s) { return '<button type="button" data-scan="' + s.id + '">' + x.sw(s.c) + '<span style="flex:1"><b>#' + s.id + ' ' + x.esc(s.color) + '</b><br><small style="color:#b2b2ba">' + x.esc(x.where(s).text) + '</small></span></button>'; }).join('') + '</div></div>';
}
function redraw() { document.getElementById('page').innerHTML = render(); }
function doScan(id) { var r = apply(id); S.last = r; S.log.unshift(r); var cam = document.getElementById('cam'); if (cam) cam.classList.remove('open'); redraw(); if (navigator.vibrate) try { navigator.vibrate(r.err ? [60, 40, 60] : 30); } catch (e) {} }
document.addEventListener('click', function (e) {
var t = e.target, a;
if (t.closest('[data-cam]')) { document.getElementById('cam').classList.add('open'); var f = document.querySelector('#cam [data-scan]'); if (f) f.focus(); return; }
if (t.closest('[data-cam-close]')) { document.getElementById('cam').classList.remove('open'); return; }
if ((a = t.closest('[data-scan]'))) return doScan(+a.getAttribute('data-scan'));
if (t.closest('[data-manual]')) { var v = +document.getElementById('manual').value.replace(/\D/g, ''); if (v) doScan(v); return; }
if (t.matches('[data-reader]')) { document.body.classList.toggle('as-reader', t.checked); }
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { var c = document.getElementById('cam'); if (c && c.classList.contains('open')) c.classList.remove('open'); } if (e.key === 'Enter' && e.target.id === 'manual') { var v = +e.target.value.replace(/\D/g, ''); if (v) doScan(v); } });
X.init({ name: 'printers', refresh: redraw }, render);
})();
</script>