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>
80 lines
10 KiB
HTML
80 lines
10 KiB
HTML
<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>
|