diff --git a/src/main.js b/src/main.js index d16c5f6..4c74c47 100644 --- a/src/main.js +++ b/src/main.js @@ -1,29 +1,18 @@ -import { startCamera, grabFrame } from './camera.js'; -import { decodeBarcodes } from './barcode.js'; +import { renderScanView } from './ui/scan-view.js'; +import { showResult } from './ui/result-overlay.js'; +import { startCamera } from './camera.js'; const app = document.querySelector('#app'); -app.innerHTML = ` - - -
-`; - -const video = document.querySelector('#v'); -const out = document.querySelector('#out'); - -startCamera(video) - .then(() => { out.textContent = 'Kamera laeuft'; }) - .catch((error) => { out.textContent = `Kamera nicht verfuegbar: ${error.message}`; }); - -document.querySelector('#b').addEventListener('click', async () => { - try { - // grabFrame wirft, wenn das Kamerabild noch nicht bereit ist (siehe - // camera.js) - dieser Fall wird hier abgefangen und sichtbar gemacht, - // statt unbehandelt zu verpuffen. - const frame = grabFrame(video); - const codes = await decodeBarcodes(frame); - out.textContent = codes.length ? codes.join('\n') : 'kein Barcode gefunden'; - } catch (error) { - out.textContent = `Fehler: ${error.message}`; - } +const view = renderScanView(app, { + onCapture: () => + showResult(app, { + stackId: 'B', + spec: { capacityGb: 64, rank: '4DRx4', speed: 'PC4-2400', formFactor: 'LRDIMM', partNumber: 'M386A8K40BM1-CRC4Y' }, + confidence: 'green', + }), + onUndo: () => view.setLast('zurueckgenommen'), + onOpenList: () => {}, + onPickFile: () => {}, }); +view.setStacks([{ id: 'A', count: 12 }, { id: 'B', count: 4 }]); +startCamera(view.video).catch(() => view.setStatus('Kamera nicht verfuegbar', true)); diff --git a/src/styles.css b/src/styles.css index f7bb793..5807a9d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,2 +1,110 @@ -:root { color-scheme: dark; } -body { margin: 0; font-family: system-ui, sans-serif; } +:root { + color-scheme: dark; + --bg: #14161a; + --fg: #f2f4f7; + --muted: #9aa3af; + --green: #1f8a4c; + --yellow: #b8860b; + --red: #a02c2c; + --line: #2a2e36; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: system-ui, -apple-system, sans-serif; + overscroll-behavior: none; +} + +#app { display: flex; flex-direction: column; height: 100dvh; } + +.camera { position: relative; flex: 1; overflow: hidden; background: #000; } +.camera video { width: 100%; height: 100%; object-fit: cover; } + +.frame { + position: absolute; + inset: 20% 8%; + border: 3px solid rgba(255, 255, 255, 0.7); + border-radius: 8px; + pointer-events: none; +} + +.stacks { + display: flex; + gap: 8px; + padding: 10px 12px; + overflow-x: auto; + border-top: 1px solid var(--line); + min-height: 56px; + align-items: center; +} + +.stacks button { + flex: none; + min-height: 44px; + padding: 0 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: #1c2027; + color: var(--fg); + font-size: 17px; + font-weight: 600; +} + +.last { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-top: 1px solid var(--line); + min-height: 56px; + font-size: 15px; + color: var(--muted); +} + +.last span { flex: 1; } + +.action { + min-height: 56px; + border: none; + border-radius: 10px; + background: #2b6cb0; + color: #fff; + font-size: 18px; + font-weight: 600; + padding: 0 18px; +} + +.action.secondary { background: #2a2e36; } + +.capture { margin: 12px; } + +.overlay { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 18px; + padding: 24px; + text-align: center; + z-index: 10; +} + +.overlay.green { background: var(--green); } +.overlay.yellow { background: var(--yellow); } +.overlay.red { background: var(--red); } + +.overlay .stack-id { font-size: 68px; font-weight: 800; letter-spacing: 2px; } +.overlay .detail { font-size: 20px; line-height: 1.5; } +.overlay .pn { font-size: 15px; opacity: 0.85; word-break: break-all; } + +.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; } +.choices button { min-height: 64px; font-size: 20px; } + +.status { padding: 8px 12px; font-size: 14px; color: var(--muted); } +.status.warn { color: #f0b429; } diff --git a/src/ui/result-overlay.js b/src/ui/result-overlay.js new file mode 100644 index 0000000..a58d167 --- /dev/null +++ b/src/ui/result-overlay.js @@ -0,0 +1,33 @@ +function beschreibung(spec) { + const teile = [ + spec.capacityGb ? `${spec.capacityGb}GB` : null, + spec.rank, + spec.speed, + spec.formFactor, + ].filter(Boolean); + return teile.join(' ') || 'ohne Angaben'; +} + +/** + * Blendet die Treffer-Rueckmeldung ein und nach kurzer Zeit wieder aus. + * Gruen bei Barcode, gelb bei OCR. Keine Eingabe noetig. + */ +export function showResult(root, { stackId, spec, confidence }, dauerMs = 1200) { + const overlay = document.createElement('div'); + overlay.className = `overlay ${confidence}`; + overlay.innerHTML = ` +