From df058afb5e120fb318d4745923d5ad27d3805273 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 24 Sep 2026 04:52:05 +0000 Subject: [PATCH] refactor(ui): simplify FieldLens Print desktop workflow --- src/main.ts | 280 +++++++++++++++-------- src/styles.css | 591 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 714 insertions(+), 157 deletions(-) diff --git a/src/main.ts b/src/main.ts index 68ad339..346c993 100644 --- a/src/main.ts +++ b/src/main.ts @@ -96,110 +96,165 @@ let virtualPrinterStatus: VirtualPrinterStatus | null = null app.innerHTML = `
-
+
FL
-

FieldLens Print

Digital barcode capture before the label leaves the printer.

+
+

FieldLens Print

+

Capture and inspect print jobs before the printer driver renders them.

+
Loading
-
-
-

Capture

RAW + spool routes
-
-
- - - - - +
+
+
+
+ PRINTER ROUTING +

Destination

-
+ +
+ +
+ + +
+
+
P
+
+ FieldLens Print + Capture + physical print +
+
+
+
T
+
+ FieldLens Test Capture + Full pipeline, no paper +
+
+
+ +
+
System printer - Checking whether FieldLens Print is installed… - Linux creates FieldLens Print for real printing and FieldLens Test Capture for a full capture/render test with no physical output. -
-
- - - + Checking FieldLens queues…
-
- - - + +
+ +
+ +
+ Advanced capture settings +
+
+ + + + +
+ +
+ + + +
+
+
-
-

FieldLens

Device login
+
+
+
+ ACTIVITY +

FieldLens

+
+ Waiting for capture +
+
0Jobs
0Codes
0Pending
-
-
-

Recent print jobs

Waiting for capture
-
+
+
+
+ RECENT ACTIVITY +

Print jobs

+
+ Newest first +
+
+
+
-
+
` @@ -285,33 +340,74 @@ async function refreshPrinters() { } } +function formatBytes(bytes: number) { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +function friendlySource(source: string) { + if (source === 'cups-original') return 'Live print' + if (source === 'cups-test') return 'Test capture' + if (source === 'raw-proxy') return 'RAW proxy' + if (source === 'spool-folder') return 'Spool' + return source +} + function render() { const pill = el('status-pill') - pill.classList.toggle('live', (status?.running ?? false) && !captureError) + const captureLive = (status?.running ?? false) && !captureError + pill.classList.toggle('live', captureLive) + pill.classList.toggle('error-state', Boolean(captureError)) pill.querySelector('span:last-child')!.textContent = captureError ? 'Capture error' - : status?.running - ? `Capturing · ${status.mode}` - : 'Stopped' + : captureLive + ? 'Capture active' + : 'Capture stopped' + el('job-count').textContent = String(status?.jobs ?? jobs.length) el('code-count').textContent = String(jobs.reduce((sum, job) => sum + job.barcodes.length, 0)) el('pending-count').textContent = String(status?.pending ?? 0) el('job-hint').textContent = captureError - ? `Capture unavailable: ${captureError}` - : jobs.length - ? `${jobs.length} retained locally` - : status?.running - ? 'Ready for print jobs' - : 'Capture stopped' + ? 'Capture needs attention' + : status?.running + ? 'Ready' + : 'Stopped' - el('jobs').innerHTML = jobs.length ? jobs.map((job) => ` -
-
${escapeHtml(job.source)} · ${escapeHtml(job.label)}${new Date(job.capturedAt).toLocaleTimeString()}
- ${job.barcodes.length ? `${job.barcodes.length} barcode${job.barcodes.length === 1 ? '' : 's'} detected` : 'No supported barcode command detected'} -
${job.barcodes.map((code) => `${escapeHtml(code.format)} · ${escapeHtml(code.value)}`).join('')}
-
${job.bytes.toLocaleString()} bytes · ${job.forwarded ? 'forwarded' : 'not forwarded'}${job.error ? ` · ${escapeHtml(job.error)}` : ''}
-
- `).join('') : '
No jobs captured yet.
' + el('jobs').innerHTML = jobs.length ? jobs.map((job) => { + const source = friendlySource(job.source) + const isTest = job.source === 'cups-test' + const routeText = isTest ? 'No paper' : job.forwarded ? 'Printed' : 'Captured' + const routeClass = isTest ? 'test' : job.forwarded ? 'success' : 'neutral' + const decodeText = job.barcodes.length + ? `${job.barcodes.length} code${job.barcodes.length === 1 ? '' : 's'} detected` + : 'Captured · no barcode decoded yet' + + return ` +
+
+
+ ${escapeHtml(source)} + ${routeText} + +
+ ${decodeText} +
${job.barcodes.map((code) => `${escapeHtml(code.format)}${escapeHtml(code.value)}`).join('')}
+ ${job.error ? `
${escapeHtml(job.error)}
` : ''} +
+
+ ${formatBytes(job.bytes)} + ${escapeHtml(job.label)} +
+
+ ` + }).join('') : ` +
+
⇢
+ No print jobs yet + Send a job to FieldLens Print or FieldLens Test Capture. +
+ ` } function escapeHtml(value: string) { diff --git a/src/styles.css b/src/styles.css index 28de579..8849fa2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,73 +1,534 @@ * { box-sizing: border-box; } + :root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - color: #e5e7eb; - background: #0b1220; + color: #eef2f7; + background: #0b0f15; font-synthesis: none; } -body { margin: 0; min-width: 720px; min-height: 100vh; background: radial-gradient(circle at top left, #172554 0, #0b1220 38%, #070b13 100%); } -button, input, select { font: inherit; } -button { cursor: pointer; } -.shell { max-width: 1120px; margin: 0 auto; padding: 28px; } -header { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 22px; } -.brand { display: flex; align-items: center; gap: 13px; } -.mark { width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; background: linear-gradient(135deg,#22c55e,#0ea5e9); color: #07111b; font-weight: 900; } -h1 { margin: 0; font-size: 22px; letter-spacing: -.02em; } -.subtitle { margin: 3px 0 0; color: #94a3b8; font-size: 13px; } -.status { display: inline-flex; align-items: center; gap: 8px; border: 1px solid #334155; background: #111827cc; border-radius: 999px; padding: 8px 12px; font-size: 12px; color: #cbd5e1; } -.dot { width: 8px; height: 8px; border-radius: 50%; background: #64748b; } -.status.live .dot { background: #22c55e; box-shadow: 0 0 0 4px #22c55e22; } -.grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(320px, .8fr); gap: 18px; } -.card { border: 1px solid #263449; border-radius: 18px; background: #111827d9; box-shadow: 0 18px 50px #0005; overflow: hidden; } -.card-head { padding: 18px 20px 14px; border-bottom: 1px solid #223047; display: flex; align-items: center; justify-content: space-between; } -.card-head h2 { font-size: 14px; margin: 0; } -.card-body { padding: 18px 20px 20px; } -.fields { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px; } -label { display: grid; gap: 7px; color: #94a3b8; font-size: 12px; } -label.full { grid-column: 1 / -1; } -input, select { width: 100%; border: 1px solid #334155; border-radius: 10px; background: #0b1220; color: #e5e7eb; padding: 10px 11px; outline: none; } -input:focus, select:focus { border-color: #38bdf8; box-shadow: 0 0 0 3px #38bdf820; } -.help { color: #64748b; font-size: 11px; line-height: 1.45; margin-top: 5px; } -.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; } -.btn { border: 1px solid #334155; border-radius: 10px; padding: 9px 13px; background: #182235; color: #e5e7eb; } -.btn.primary { background: #16a34a; border-color: #22c55e; color: white; } -.btn.stop { background: #3f1d24; border-color: #7f1d1d; color: #fecaca; } -.btn.blue { background: #0c4a6e; border-color: #0369a1; } -.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 15px; } -.metric { border: 1px solid #263449; border-radius: 12px; padding: 12px; background: #0b1220aa; } -.metric b { display: block; font-size: 18px; margin-bottom: 3px; } -.metric span { color: #64748b; font-size: 10px; text-transform: uppercase; letter-spacing: .08em; } -.login-box { border: 1px solid #263449; border-radius: 12px; padding: 13px; background: #0b1220aa; } -.login-code { font: 700 18px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .09em; } -.muted { color: #64748b; font-size: 12px; } -.jobs { display: grid; gap: 10px; max-height: 385px; overflow: auto; } -.job { border: 1px solid #263449; border-radius: 12px; padding: 12px; background: #0b1220aa; } -.job-top { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; color: #64748b; } -.job strong { display: block; margin: 7px 0 4px; font-size: 13px; } -.codes { display: flex; flex-wrap: wrap; gap: 6px; } -.code { font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; background: #052e16; border: 1px solid #14532d; color: #bbf7d0; border-radius: 7px; padding: 4px 6px; } -.warn { color: #fbbf24; } -.error { color: #fca5a5; } -.small { font-size: 11px; } -@media (max-width: 860px) { - body { min-width: 0; } - .grid { grid-template-columns: 1fr; } - .fields { grid-template-columns: 1fr; } - label.full { grid-column: auto; } + +body { + margin: 0; + min-width: 720px; + min-height: 100vh; + background: + radial-gradient(circle at 12% -10%, rgba(44, 113, 90, .18), transparent 30rem), + #0b0f15; } -.update-box { margin-top: 14px; padding: 13px; border: 1px solid #263449; border-radius: 12px; background: #0b1220aa; } -.update-box > div:first-child { display: grid; gap: 3px; } -.update-box strong { font-size: 12px; } -.update-box small { line-height: 1.45; } -.update-actions { margin-top: 12px; } -.update-progress { height: 5px; margin-top: 12px; overflow: hidden; border-radius: 999px; background: #1e293b; } -.update-progress span { display: block; width: 0; height: 100%; border-radius: inherit; background: #38bdf8; transition: width .16s ease; } -.btn:disabled { cursor: wait; opacity: .55; } +button, input, select { font: inherit; } +button { cursor: pointer; } +button:disabled { cursor: wait; opacity: .48; } -.printer-setup { display: grid; gap: 10px; margin-top: 15px; padding: 13px; border: 1px solid #263449; border-radius: 12px; background: #0b1220aa; } -.printer-setup > div:first-child { display: grid; gap: 4px; } -.printer-setup strong { font-size: 12px; } -.printer-setup small { line-height: 1.45; } -.printer-actions { margin-top: 4px; } -#virtual-printer-state.ready { color: #86efac; } +.shell { + max-width: 1180px; + margin: 0 auto; + padding: 28px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin-bottom: 22px; +} + +.brand { display: flex; align-items: center; gap: 13px; } + +.mark { + width: 42px; + height: 42px; + border-radius: 12px; + display: grid; + place-items: center; + background: #b9f3d5; + color: #092016; + font-weight: 900; + letter-spacing: -.04em; + box-shadow: 0 0 0 1px rgba(185, 243, 213, .18); +} + +h1 { margin: 0; font-size: 21px; letter-spacing: -.025em; } +h2 { margin: 2px 0 0; font-size: 15px; letter-spacing: -.01em; } + +.subtitle { + margin: 3px 0 0; + color: #8491a3; + font-size: 12.5px; +} + +.eyebrow { + color: #667386; + font-size: 9px; + font-weight: 800; + letter-spacing: .14em; +} + +.status { + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid #28313d; + background: #111720; + border-radius: 999px; + padding: 8px 12px; + font-size: 11px; + font-weight: 700; + color: #99a5b5; +} + +.dot, +.state-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex: 0 0 auto; + background: #647083; +} + +.status.live .dot, +#virtual-printer-state.ready + .state-dot { background: #65d69a; } + +.status.live { + border-color: #254b3b; + color: #b9f3d5; + background: #0f211a; +} + +.status.error-state { + color: #ffc7c7; + border-color: #5d3131; + background: #231414; +} + +.status.error-state .dot { background: #ef7f7f; } + +.grid { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(330px, .85fr); + gap: 16px; +} + +.card { + border: 1px solid #202833; + border-radius: 16px; + background: #10151d; + overflow: hidden; + box-shadow: 0 14px 38px rgba(0, 0, 0, .2); +} + +.card-head { + min-height: 64px; + padding: 15px 18px; + border-bottom: 1px solid #1e2630; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.card-body { padding: 18px; } + +.jobs-card { grid-column: 1 / -1; } + +.field-label, +label { + display: grid; + gap: 7px; + color: #a4afbd; + font-size: 11px; + font-weight: 600; +} + +label.full { grid-column: 1 / -1; } + +input, +select { + width: 100%; + border: 1px solid #2a3441; + border-radius: 9px; + background: #0b1016; + color: #edf2f7; + padding: 10px 11px; + outline: none; +} + +input:focus, +select:focus { + border-color: #4ca77b; + box-shadow: 0 0 0 3px rgba(76, 167, 123, .12); +} + +.help { + color: #667386; + font-size: 10.5px; + font-weight: 400; + line-height: 1.45; +} + +.route-tiles { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 9px; + margin-top: 15px; +} + +.route-tile { + min-height: 62px; + display: flex; + align-items: center; + gap: 10px; + border: 1px solid #222c38; + border-radius: 11px; + padding: 10px; + background: #0d131a; +} + +.route-tile > div:last-child { + display: grid; + gap: 2px; +} + +.route-tile strong { font-size: 11.5px; } +.route-tile span { color: #667386; font-size: 10px; } + +.route-icon { + width: 28px; + height: 28px; + border-radius: 8px; + display: grid; + place-items: center; + flex: 0 0 auto; + font-size: 10px; + font-weight: 900; +} + +.live-route { background: #143326; color: #8be4b4; } +.test-route { background: #18273a; color: #9cc7f4; } + +.printer-state, +.account-row, +.update-box { + margin-top: 12px; + border: 1px solid #222c38; + border-radius: 11px; + background: #0d131a; +} + +.printer-state { + display: flex; + align-items: flex-start; + gap: 9px; + padding: 11px; +} + +.printer-state > div { + display: grid; + gap: 3px; +} + +.printer-state strong, +.account-row strong, +.update-box strong { font-size: 11px; } + +#virtual-printer-state { + color: #718094; + font-size: 10.5px; + line-height: 1.4; +} + +#virtual-printer-state.ready { color: #82dca9; } +.printer-state:has(#virtual-printer-state.ready) .state-dot { background: #65d69a; } + +.actions, +.inline-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.printer-actions { margin-top: 12px; } + +.btn, +.icon-btn { + border: 1px solid #2b3643; + border-radius: 9px; + background: #151d27; + color: #dce4ed; + font-size: 11px; + font-weight: 700; +} + +.btn { padding: 9px 12px; } +.icon-btn { width: 32px; height: 32px; padding: 0; } + +.btn:hover, +.icon-btn:hover { background: #1a2430; } + +.btn.primary { + background: #b9f3d5; + border-color: #b9f3d5; + color: #092016; +} + +.btn.blue { + background: #173552; + border-color: #24577f; + color: #d2e9ff; +} + +.btn.ghost { background: transparent; } +.btn.danger { color: #e99b9b; } + +.fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.advanced { + margin-top: 14px; + border-top: 1px solid #1e2630; + padding-top: 12px; +} + +.advanced summary { + cursor: pointer; + color: #7d8999; + font-size: 10.5px; + font-weight: 700; + user-select: none; +} + +.advanced-body { padding-top: 13px; } +.compact-actions { margin-top: 12px; } + +.metrics { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} + +.metric { + border: 1px solid #222c38; + border-radius: 11px; + padding: 12px; + background: #0d131a; +} + +.metric b { + display: block; + margin-bottom: 3px; + font-size: 21px; + letter-spacing: -.04em; +} + +.metric span { + color: #667386; + font-size: 9px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: .1em; +} + +.account-row { + min-height: 60px; + padding: 11px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.account-row > div:first-child { + display: grid; + gap: 3px; +} + +.login-code { + color: #b9f3d5; + font: 700 16px ui-monospace, SFMono-Regular, Menlo, monospace; + letter-spacing: .08em; +} + +.muted { color: #667386; font-size: 10.5px; } +.small { font-size: 10px; } + +.update-box { + padding: 11px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 10px; +} + +.update-box > div:first-child { + display: grid; + gap: 2px; +} + +.update-box small { line-height: 1.35; } + +.update-progress { + grid-column: 1 / -1; + height: 4px; + overflow: hidden; + border-radius: 999px; + background: #1a2430; +} + +.update-progress span { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: #65d69a; + transition: width .16s ease; +} + +.jobs { + display: grid; + gap: 8px; + max-height: 420px; + overflow: auto; +} + +.job { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(140px, 260px); + gap: 14px; + border: 1px solid #202a36; + border-radius: 11px; + padding: 12px; + background: #0c1117; +} + +.job-main { min-width: 0; } + +.job-title-row { + display: flex; + align-items: center; + gap: 7px; +} + +.job-title-row time { + margin-left: auto; + color: #596679; + font-size: 10px; +} + +.source-badge, +.route-badge { + display: inline-flex; + align-items: center; + min-height: 22px; + border-radius: 999px; + padding: 0 8px; + font-size: 9px; + font-weight: 800; +} + +.source-badge { + background: #17202b; + color: #a9b5c5; +} + +.route-badge.success { background: #123225; color: #8ce1b1; } +.route-badge.test { background: #172c43; color: #9fcef5; } +.route-badge.neutral { background: #2a2519; color: #e3c47d; } + +.job-result { + display: block; + margin: 9px 0 7px; + font-size: 12px; + font-weight: 700; +} + +.codes { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.code { + display: grid; + gap: 2px; + font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; + background: #102b20; + border: 1px solid #214b39; + color: #bff0d3; + border-radius: 7px; + padding: 6px 8px; +} + +.code small { + color: #68a984; + font-size: 8px; + font-weight: 800; +} + +.job-meta { + min-width: 0; + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 8px; + color: #596679; + font-size: 9.5px; + text-align: right; +} + +.job-meta span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; +} + +.job-error { + margin-top: 7px; + color: #f0a0a0; + font-size: 10px; +} + +.empty-state { + min-height: 150px; + display: grid; + place-items: center; + align-content: center; + gap: 5px; + color: #667386; + text-align: center; +} + +.empty-state strong { + color: #a9b5c5; + font-size: 12px; +} + +.empty-state span { font-size: 10.5px; } + +.empty-icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid #26313e; + border-radius: 50%; + margin-bottom: 3px; +} + +@media (max-width: 860px) { + body { min-width: 0; } + .shell { padding: 18px; } + .topbar { align-items: flex-start; } + .grid { grid-template-columns: 1fr; } + .jobs-card { grid-column: auto; } + .fields, + .route-tiles { grid-template-columns: 1fr; } + label.full { grid-column: auto; } + .job { grid-template-columns: 1fr; } + .job-meta { justify-content: flex-start; text-align: left; } +}