deploy: add single-barcode printing to Code Lab
Some checks failed
CI / verify (push) Has been cancelled
CI / print-rust (push) Has been cancelled

This commit is contained in:
Admin 2026-09-24 02:58:09 +00:00
parent 13b3de661b
commit 30bb21dcc4
2 changed files with 173 additions and 16 deletions

View File

@ -144,4 +144,102 @@
.ean-lab-fields b { .ean-lab-fields b {
font-size: 5.8px; font-size: 5.8px;
} }
} }
.code-card-heading-actions {
display: flex;
align-items: center;
gap: 9px;
}
.code-print-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
min-height: 30px;
padding: 0 9px;
border: 1px solid var(--line);
border-radius: 5px;
color: var(--ink);
background: #fff;
font-size: 9px;
font-weight: 900;
white-space: nowrap;
}
.code-print-button:hover {
border-color: #c9a72d;
background: #fff8d7;
}
.barcode-print-sheet {
display: none;
}
@media print {
@page {
margin: 12mm;
}
body * {
visibility: hidden !important;
}
.barcode-print-sheet,
.barcode-print-sheet * {
visibility: visible !important;
}
.barcode-print-sheet {
position: fixed;
inset: 0;
display: grid !important;
place-items: center;
padding: 0;
background: #fff;
}
.barcode-print-label {
display: grid;
width: min(170mm, 100%);
place-items: center;
gap: 8mm;
color: #000;
background: #fff;
text-align: center;
}
.barcode-print-label strong {
font: 700 14pt/1.2 Arial, Helvetica, sans-serif;
}
.barcode-print-label canvas {
display: block;
max-width: 100%;
height: auto;
}
.barcode-print-label code {
max-width: 100%;
overflow-wrap: anywhere;
color: #000;
font: 11pt/1.3 "Courier New", monospace;
}
}
@media (max-width: 760px) {
.code-card-heading {
align-items: flex-start;
}
.code-card-heading-actions {
align-items: flex-end;
flex-direction: column;
gap: 5px;
}
.code-print-button {
min-height: 29px;
}
}

View File

@ -1,5 +1,5 @@
import bwipjs from 'bwip-js' import bwipjs from 'bwip-js'
import { RefreshCw } from 'lucide-react' import { Printer, RefreshCw } from 'lucide-react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { appendGtinCheckDigit, randomDigits } from '../../lib/barcodes' import { appendGtinCheckDigit, randomDigits } from '../../lib/barcodes'
@ -65,23 +65,27 @@ function makeEanFixtures(): EanLabelFixture[] {
})) }))
} }
function CodeCard({ fixture }: { fixture: CodeFixture }) { function drawBarcode(canvas: HTMLCanvasElement, fixture: CodeFixture, print = false) {
bwipjs.toCanvas(canvas, {
bcid: fixture.bcid,
text: fixture.value,
scale: print ? 5 : 3,
height: fixture.bcid === 'qrcode' ? (print ? 34 : 24) : (print ? 20 : 13),
includetext: fixture.bcid !== 'qrcode',
textxalign: 'center',
backgroundcolor: 'FFFFFF',
paddingwidth: print ? 12 : 8,
paddingheight: print ? 12 : 8,
})
}
function CodeCard({ fixture, onPrint }: { fixture: CodeFixture; onPrint: (fixture: CodeFixture) => void }) {
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => { useEffect(() => {
if (!canvasRef.current) return if (!canvasRef.current) return
try { try {
bwipjs.toCanvas(canvasRef.current, { drawBarcode(canvasRef.current, fixture)
bcid: fixture.bcid,
text: fixture.value,
scale: 3,
height: fixture.bcid === 'qrcode' ? 24 : 13,
includetext: fixture.bcid !== 'qrcode',
textxalign: 'center',
backgroundcolor: 'FFFFFF',
paddingwidth: 8,
paddingheight: 8,
})
} catch { } catch {
// A failed fixture remains labeled so generator errors are visible during testing. // A failed fixture remains labeled so generator errors are visible during testing.
} }
@ -89,13 +93,44 @@ function CodeCard({ fixture }: { fixture: CodeFixture }) {
return ( return (
<article className="code-card"> <article className="code-card">
<div className="code-card-heading"><strong>{fixture.name}</strong><span>VALID SAMPLE</span></div> <div className="code-card-heading">
<strong>{fixture.name}</strong>
<div className="code-card-heading-actions">
<span>VALID SAMPLE</span>
<button className="code-print-button" type="button" onClick={() => onPrint(fixture)} aria-label={`Print one ${fixture.name} barcode`}>
<Printer size={14} /> Print one
</button>
</div>
</div>
<div className="barcode-canvas"><canvas ref={canvasRef} /></div> <div className="barcode-canvas"><canvas ref={canvasRef} /></div>
<code>{fixture.value}</code> <code>{fixture.value}</code>
</article> </article>
) )
} }
function PrintableBarcode({ fixture }: { fixture: CodeFixture }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (!canvasRef.current) return
try {
drawBarcode(canvasRef.current, fixture, true)
} catch {
// Keep the value visible if rendering fails so the print is diagnosable.
}
}, [fixture])
return (
<div className="barcode-print-sheet" aria-hidden="true">
<div className="barcode-print-label">
<strong>{fixture.name}</strong>
<canvas ref={canvasRef} />
<code>{fixture.value}</code>
</div>
</div>
)
}
function EanLabelCard({ fixture }: { fixture: EanLabelFixture }) { function EanLabelCard({ fixture }: { fixture: EanLabelFixture }) {
return ( return (
<article className="ean-lab-carton"> <article className="ean-lab-carton">
@ -126,12 +161,35 @@ export function CodeLab() {
const [tab, setTab] = useState<LabTab>('barcodes') const [tab, setTab] = useState<LabTab>('barcodes')
const [fixtures, setFixtures] = useState(makeFixtures) const [fixtures, setFixtures] = useState(makeFixtures)
const [eanFixtures, setEanFixtures] = useState(makeEanFixtures) const [eanFixtures, setEanFixtures] = useState(makeEanFixtures)
const [printFixture, setPrintFixture] = useState<CodeFixture | null>(null)
const regenerate = () => { const regenerate = () => {
if (tab === 'ean') setEanFixtures(makeEanFixtures()) if (tab === 'ean') setEanFixtures(makeEanFixtures())
else setFixtures(makeFixtures()) else setFixtures(makeFixtures())
} }
useEffect(() => {
if (!printFixture) return
let cancelled = false
let frameTwo = 0
const clearPrintFixture = () => setPrintFixture(null)
window.addEventListener('afterprint', clearPrintFixture, { once: true })
const frameOne = window.requestAnimationFrame(() => {
frameTwo = window.requestAnimationFrame(() => {
if (!cancelled) window.print()
})
})
return () => {
cancelled = true
window.cancelAnimationFrame(frameOne)
if (frameTwo) window.cancelAnimationFrame(frameTwo)
window.removeEventListener('afterprint', clearPrintFixture)
}
}, [printFixture])
return ( return (
<section className="workspace-view code-lab" aria-labelledby="lab-title"> <section className="workspace-view code-lab" aria-labelledby="lab-title">
<header className="view-heading"> <header className="view-heading">
@ -153,10 +211,11 @@ export function CodeLab() {
</div> </div>
{tab === 'barcodes' ? ( {tab === 'barcodes' ? (
<div className="code-grid">{fixtures.map((fixture) => <CodeCard fixture={fixture} key={fixture.id} />)}</div> <div className="code-grid">{fixtures.map((fixture) => <CodeCard fixture={fixture} key={fixture.id} onPrint={setPrintFixture} />)}</div>
) : ( ) : (
<div className="ean-lab-grid">{eanFixtures.map((fixture) => <EanLabelCard fixture={fixture} key={fixture.id} />)}</div> <div className="ean-lab-grid">{eanFixtures.map((fixture) => <EanLabelCard fixture={fixture} key={fixture.id} />)}</div>
)} )}
{printFixture && <PrintableBarcode fixture={printFixture} />}
</section> </section>
) )
} }