Put a code on the back of a card, a picture on the front, and the page in the app
Some checks failed
CI / backend (push) Failing after 7m17s
CI / naming (push) Failing after 7m14s
CI / migrations-mysql (push) Has been cancelled
CI / frontend (push) Has been cancelled

The back side landed last commit carrying text by default, and a back laid out
as text-only renders no code box at all. So a two-sided run came out with a
blank reverse, which reads as the code having failed rather than as a setting
doing exactly what it said. The back now defaults to carrying a code, takes its
own content per row, and chooses its own symbology - a QR on the front that
opens the record, a CODE128 on the back that the bench scanner already reads.
Text-only is refused while the back carries a code, and the contradiction is
reconciled on load, so a saved setting from the broken combination repairs
itself rather than printing another blank box of cards.

A picture can go on a label: above the code, beside it, or behind everything as
a watermark, on either side or both. The file is read in the browser and never
sent anywhere, which is what makes it usable at an air-gapped site, and it is
deliberately not saved between visits - a data URL runs to megabytes and would
take every sizing value with it when the quota gave out. The watermark says
plainly that it costs the scanner contrast.

The generator was a full-screen route with no navigation and a narrow column of
controls against the left edge. That shape exists so a print cannot put the
sidebar on label stock, which is a real hazard and the reason core's label pages
are built that way - but it is a hazard the print stylesheet can answer, and
style.css now hides the shell at print time. A tool someone spends ten minutes
in gets the navigation and the full width; the stock still gets nothing but
labels, confirmed against a print PDF rather than a preview.

Two smaller things found while checking the above. A test print paired each page
with a rendered code by loop position, while the codes are keyed to position in
the whole run, so a two-sided test card could carry the wrong code; the index
now travels with the page. And the back's dotted preview outline outspecified
the print reset, drawing a dashed rule down the edge of every second card.
This commit is contained in:
cproudlock
2026-08-21 10:59:28 -04:00
parent 1593ee8204
commit 1abb6430f5
4 changed files with 303 additions and 73 deletions

View File

@@ -31,10 +31,37 @@ ADR-007 and ADR-002.
- **Contents can be positioned inside the label without moving the label.** - **Contents can be positioned inside the label without moving the label.**
Align and Vertical place the code and caption block anywhere on the stock, Align and Vertical place the code and caption block anywhere on the stock,
and Content X/Y nudge it from there. The existing X/Y nudge kept its and Content X/Y nudge it from there. The existing X/Y nudge kept its
behaviour and is now labelled Media X/Y: it moves the whole printed area, for behavior and is now labeled Media X/Y: it moves the whole printed area, for
a printer whose origin is off, which is a different problem from composing a a printer whose origin is off, which is a different problem from composing a
label. label.
- **A picture can go on the label.** Pick an image once and it prints on every
label - above the code, beside it, or behind everything as a watermark - on
the front, the back, or both. The file never leaves the browser, so an
air-gapped site can use it, and it is deliberately not saved between visits:
a data URL runs to megabytes and would take every sizing value with it when
the storage quota gave out.
- **The back can carry its own code, in its own symbology.** "Back carries" now
defaults to a code rather than text, takes per-row content from the CSV
`back` column, and has its own QR/CODE128 choice - a QR on the front that
opens the record, a CODE128 on the back that the bench scanner already reads.
A back set to carry a code can no longer be laid out as text-only: that
combination printed a blank card that looked like the code had failed, and it
is corrected on load for anyone who already saved it.
### Fixed
- **The generator prints from inside the app shell.** It was a full-screen
route with no sidebar and a narrow column of controls on the left. It is now
an ordinary page - navigation, full width - and `style.css` hides the shell at
print time, so the stock still gets nothing but labels. Verified against a
real print PDF rather than a preview.
- A test print paired each page with a code by loop position while the codes
are keyed to position in the whole run, so a two-sided test card could carry
the wrong code. The page index now travels with the page.
- The back label's dotted preview outline outspecified the print reset and drew
a dashed rule down the edge of every second card.
### Changed ### Changed
- CSV parsing and page building moved out of the generator component into - CSV parsing and page building moved out of the generator component into

View File

@@ -1763,3 +1763,31 @@ td.actions .btn + .btn {
independently. */ independently. */
.clickable-row { cursor: pointer; } .clickable-row { cursor: pointer; }
.clickable-row:hover td { background: var(--bg); } .clickable-row:hover td { background: var(--bg); }
/* --- Printing from inside the app shell -------------------------------------
*
* Core's label pages are full-screen routes precisely so the shell cannot reach
* the stock. A tool someone works in for ten minutes wants the navigation
* though, so the shell is hidden here instead: the sidebar goes, the content
* column gives back the margin it was holding for it, and the notification
* banner - which is chrome, not content - goes with them.
*
* Anything the page itself must keep off the paper marks itself .no-print. */
@media print {
.sidebar,
.notification-banner,
.no-print {
display: none !important;
}
.app-layout {
display: block !important;
min-height: 0 !important;
}
.main-content {
margin-left: 0 !important;
padding: 0 !important;
overflow: visible !important;
}
}

View File

@@ -3,9 +3,12 @@
* *
* `default` = AppLayout child routes; `toplevel` = full-screen routes. * `default` = AppLayout child routes; `toplevel` = full-screen routes.
* *
* The index sits inside the app shell. Each tool that PRINTS is toplevel, the * The generator sits INSIDE the app shell, unlike the label pages in core.
* way every other label page in this repo is: printing from inside AppLayout * Those are toplevel because a print from inside AppLayout would put the
* would put the sidebar and header on the label stock. * sidebar on the label stock - which is true unless the print stylesheet says
* otherwise, and style.css now does: the shell is hidden at print time so only
* the labels reach the page. A tool someone spends ten minutes in wants the
* navigation; a one-shot print page does not.
*/ */
export default [ export default [
{ {
@@ -14,11 +17,8 @@ export default [
component: () => import('./views/ToolsIndex.vue'), component: () => import('./views/ToolsIndex.vue'),
meta: { requiresAuth: true, plugin: 'tools' } meta: { requiresAuth: true, plugin: 'tools' }
}, },
]
export const toplevel = [
{ {
path: '/tools/codes', path: 'tools/codes',
name: 'tools-codes', name: 'tools-codes',
component: () => import('./views/CodeGenerator.vue'), component: () => import('./views/CodeGenerator.vue'),
meta: { requiresAuth: true, plugin: 'tools' } meta: { requiresAuth: true, plugin: 'tools' }

View File

@@ -1,12 +1,12 @@
<template> <template>
<div> <div class="code-generator">
<div class="no-print"> <div class="no-print">
<div class="controls"> <div class="page-header">
<div class="controls-head"> <h1>Barcode / QR Generator</h1>
<h3>Barcode / QR Generator</h3>
<router-link to="/tools" class="btn btn-secondary">Back to Tech Tools</router-link> <router-link to="/tools" class="btn btn-secondary">Back to Tech Tools</router-link>
</div> </div>
<div class="controls card">
<!-- What to encode --> <!-- What to encode -->
<div class="control-row"> <div class="control-row">
<label> <label>
@@ -66,9 +66,9 @@
<p class="control-note" v-if="csvError">{{ csvError }}</p> <p class="control-note" v-if="csvError">{{ csvError }}</p>
<p class="control-hint"> <p class="control-hint">
Columns: <code>content</code> (required), <code>label</code>, <code>copies</code><span Columns: <code>content</code> (required), <code>label</code>, <code>copies</code><span
v-if="backenabled && backsource === 'column'">, <code>back</code>, <code>backlabel</code></span>. v-if="backCarriesOwnContent">, <code>back</code>, <code>backlabel</code></span>.
A header row is optional; without one the order is content, label, copies<span A header row is optional; without one the order is content, label, copies<span
v-if="backenabled && backsource === 'column'">, back, backlabel</span>. v-if="backCarriesOwnContent">, back, backlabel</span>.
</p> </p>
</div> </div>
@@ -135,7 +135,7 @@
Align Align
<select v-model="align"> <select v-model="align">
<option value="flex-start">Left</option> <option value="flex-start">Left</option>
<option value="center">Centre</option> <option value="center">Center</option>
<option value="flex-end">Right</option> <option value="flex-end">Right</option>
</select> </select>
</label> </label>
@@ -159,6 +159,47 @@
is for a printer whose origin is off, not for composing a label. is for a printer whose origin is off, not for composing a label.
</p> </p>
<!-- Picture -->
<h4 class="section-heading">Picture</h4>
<div class="control-row">
<label>
Image file
<input type="file" accept="image/*" @change="onPictureFile" />
</label>
<button v-if="pictureUrl" class="btn btn-secondary" @click="clearPicture">Remove picture</button>
<template v-if="pictureUrl">
<label>Size (in)<input v-model.number="picturesize" type="number" step="0.01" min="0.05" /></label>
<label>
Where
<select v-model="pictureposition">
<option value="above">Above the code</option>
<option value="beside">Beside the code</option>
<option value="behind">Behind everything (watermark)</option>
</select>
</label>
<label>
Which side
<select v-model="pictureside">
<option value="front">Front only</option>
<option value="back">Back only</option>
<option value="both">Both sides</option>
</select>
</label>
<label v-if="pictureposition === 'behind'">
Opacity
<input v-model.number="pictureopacity" type="number" step="0.05" min="0.05" max="1" />
</label>
</template>
</div>
<p class="control-hint">
The picture never leaves the browser, so this works on an air-gapped
site. It is also NOT remembered between visits, unlike the sizing -
an image is far too big to keep in the settings the page saves.
A watermark behind a code costs the scanner contrast: keep the opacity
low and test a scan before printing a run.
</p>
<!-- Back side --> <!-- Back side -->
<h4 class="section-heading">Back side</h4> <h4 class="section-heading">Back side</h4>
<div class="control-row"> <div class="control-row">
@@ -170,19 +211,27 @@
<label> <label>
Back carries Back carries
<select v-model="backsource"> <select v-model="backsource">
<option value="text">The same fixed text</option>
<option value="code">The same code again</option> <option value="code">The same code again</option>
<option value="column">Its own content (per row)</option> <option value="column">Its own code (per row)</option>
<option value="text">Fixed text, no code</option>
<option value="blank">Nothing - blank back</option> <option value="blank">Nothing - blank back</option>
</select> </select>
</label> </label>
<label v-if="backCarriesCode">
Back code type
<select v-model="backcodetype">
<option value="same">Same as the front</option>
<option value="qr">QR</option>
<option value="barcode">CODE128</option>
</select>
</label>
<label> <label>
Back layout Back layout
<select v-model="backlayout"> <select v-model="backlayout">
<option value="side">Code left, label right</option> <option value="side">Code left, label right</option>
<option value="stack">Code above label</option> <option value="stack">Code above label</option>
<option value="codeonly">Code only</option> <option value="codeonly">Code only</option>
<option value="textonly">Text only</option> <option value="textonly" :disabled="backCarriesCode">Text only, no code</option>
</select> </select>
</label> </label>
<label> <label>
@@ -196,15 +245,16 @@
</div> </div>
<div v-if="backenabled" class="control-row"> <div v-if="backenabled" class="control-row">
<label class="grow" v-if="backsource === 'text' || backsource === 'column'"> <label class="grow" v-if="source === 'single' && backCarriesOwnContent">
Back content (encoded on the back)
<textarea v-model="singleBack" rows="2"
placeholder="A second code for the reverse - text or URL"></textarea>
</label>
<label class="grow" v-if="backsource === 'text' || backCarriesOwnContent">
Back text Back text
<textarea v-model="backtext" rows="2" <textarea v-model="backtext" rows="2"
placeholder="Property of GE Aerospace - return to IT"></textarea> placeholder="Property of GE Aerospace - return to IT"></textarea>
</label> </label>
<label class="grow" v-if="source === 'single' && backsource === 'column'">
Back content (encoded on the back)
<input v-model="singleBack" type="text" placeholder="Text or URL for the reverse" />
</label>
</div> </div>
<p v-if="backenabled" class="control-hint"> <p v-if="backenabled" class="control-hint">
@@ -241,20 +291,27 @@
<!-- Preview doubles as the print surface: what is on screen is what prints. --> <!-- Preview doubles as the print surface: what is on screen is what prints. -->
<div class="sheet"> <div class="sheet">
<div <div
v-for="(page, index) in visiblePages" v-for="item in visible"
:key="index" :key="item.index"
class="label" class="label"
:class="['layout-' + layoutFor(page), 'type-' + codetype, 'side-' + page.side]" :class="labelClasses(item.page)"
> >
<img v-if="showPicture(item.page) && pictureposition === 'behind'"
:src="pictureUrl" class="picture-behind" alt="" />
<div class="content"> <div class="content">
<div v-if="layoutFor(page) !== 'textonly'" class="codebox"> <div v-if="showPicture(item.page) && pictureposition !== 'behind'" class="picturebox">
<img v-if="images[index]" :src="images[index]" class="code-img" alt="" /> <img :src="pictureUrl" class="picture-img" alt="" />
</div> </div>
<div v-if="layoutFor(page) !== 'codeonly' && page.label" class="label-text"> <div class="codegroup">
{{ page.label }} <div v-if="layoutFor(item.page) !== 'textonly'" class="codebox">
<img v-if="images[item.index]" :src="images[item.index]" class="code-img" alt="" />
</div>
<div v-if="layoutFor(item.page) !== 'codeonly' && item.page.label" class="label-text">
{{ item.page.label }}
</div> </div>
</div> </div>
<span v-if="backenabled" class="side-flag">{{ page.side }}</span> </div>
<span v-if="backenabled" class="side-flag">{{ item.page.side }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -298,10 +355,20 @@ const nudgex = ref(0)
const nudgey = ref(0) const nudgey = ref(0)
const dpi = ref(203) const dpi = ref(203)
// The image itself is deliberately NOT a saved setting: a data URL runs to
// megabytes and would blow the localStorage quota, taking every sizing value
// with it. Only how it is placed persists.
const pictureUrl = ref('')
const picturesize = ref(0.5)
const pictureposition = ref('above')
const pictureside = ref('front')
const pictureopacity = ref(0.15)
const backenabled = ref(false) const backenabled = ref(false)
const backsource = ref('text') const backsource = ref('code')
const backcodetype = ref('same')
const backtext = ref('') const backtext = ref('')
const backlayout = ref('textonly') const backlayout = ref('stack')
const backorder = ref('interleave') const backorder = ref('interleave')
const images = ref([]) const images = ref([])
@@ -312,13 +379,61 @@ const SETTING_KEYS = [
'codetype', 'errorcorrection', 'preset', 'labelwidth', 'labelheight', 'padding', 'codetype', 'errorcorrection', 'preset', 'labelwidth', 'labelheight', 'padding',
'quiet', 'gap', 'codesize', 'barheight', 'labelfont', 'layout', 'align', 'valign', 'quiet', 'gap', 'codesize', 'barheight', 'labelfont', 'layout', 'align', 'valign',
'contentx', 'contenty', 'nudgex', 'nudgey', 'dpi', 'contentx', 'contenty', 'nudgex', 'nudgey', 'dpi',
'backenabled', 'backsource', 'backtext', 'backlayout', 'backorder', 'picturesize', 'pictureposition', 'pictureside', 'pictureopacity',
'backenabled', 'backsource', 'backcodetype', 'backtext', 'backlayout', 'backorder',
] ]
const settingRefs = { const settingRefs = {
codetype, errorcorrection, preset, labelwidth, labelheight, padding, codetype, errorcorrection, preset, labelwidth, labelheight, padding,
quiet, gap, codesize, barheight, labelfont, layout, align, valign, quiet, gap, codesize, barheight, labelfont, layout, align, valign,
contentx, contenty, nudgex, nudgey, dpi, contentx, contenty, nudgex, nudgey, dpi,
backenabled, backsource, backtext, backlayout, backorder, picturesize, pictureposition, pictureside, pictureopacity,
backenabled, backsource, backcodetype, backtext, backlayout, backorder,
}
// --- back side --------------------------------------------------------------
const backCarriesCode = computed(() =>
backsource.value === 'code' || backsource.value === 'column')
const backCarriesOwnContent = computed(() =>
backenabled.value && backsource.value === 'column')
// A back that carries a code and a back laid out as text-only cancel each other
// out, and the result is a blank card that looks like the code failed to
// render. Keep the two honest in both directions, including for settings
// restored from a previous visit.
function reconcileBackLayout() {
if (backCarriesCode.value && backlayout.value === 'textonly') backlayout.value = 'stack'
if (!backCarriesCode.value && backlayout.value !== 'textonly') backlayout.value = 'textonly'
}
watch(backsource, reconcileBackLayout)
/** QR or CODE128 for this page. The back may deliberately differ from the
* front: a QR that opens a record, a CODE128 the bench scanner already
* understands. */
function codeTypeFor(page) {
if (page.side !== 'back') return codetype.value
return backcodetype.value === 'same' ? codetype.value : backcodetype.value
}
function layoutFor(page) {
return page.side === 'back' ? backlayout.value : layout.value
}
function showPicture(page) {
if (!pictureUrl.value) return false
if (pictureside.value === 'both') return true
return pictureside.value === page.side
}
function labelClasses(page) {
return [
'layout-' + layoutFor(page),
'type-' + codeTypeFor(page),
'side-' + page.side,
showPicture(page) && pictureposition.value !== 'behind' ? 'pic-' + pictureposition.value : '',
]
} }
// --- rows ------------------------------------------------------------------- // --- rows -------------------------------------------------------------------
@@ -366,18 +481,25 @@ const pages = computed(() => built.value.pages)
const cardCount = computed(() => built.value.fronts.length) const cardCount = computed(() => built.value.fronts.length)
const truncated = computed(() => built.value.dropped) const truncated = computed(() => built.value.dropped)
// Printing one test label prints the first card only - both of its sides when // What the preview renders, each item carrying its index into `images`. The
// there is a back, because a back that lands upside down is the whole reason // index has to travel with the page: a test print shows two of the pages and
// to print one first. // the rendered codes are keyed by position in the full run, so pairing them by
const visiblePages = computed(() => { // loop position printed the wrong code onto the test card.
if (!testMode.value) return pages.value const visible = computed(() => {
const all = pages.value.map((page, index) => ({ page, index }))
if (!testMode.value) return all
const { fronts, backs } = built.value const { fronts, backs } = built.value
return [fronts[0], backenabled.value ? backs[0] : null].filter(Boolean) if (!fronts.length) return []
const items = [{ page: fronts[0], index: 0 }]
if (backenabled.value && backs.length) {
// Where the first back sits depends on the order the run is emitted in.
items.push({
page: backs[0],
index: backorder.value === 'grouped' ? fronts.length : 1,
}) })
function layoutFor(page) {
return page.side === 'back' ? backlayout.value : layout.value
} }
return items
})
// --- fit advice ------------------------------------------------------------- // --- fit advice -------------------------------------------------------------
@@ -416,10 +538,10 @@ const fitAdvice = computed(() => {
// //
// margin 0 on the QR: the quiet zone is blank label supplied by --tool-quiet, // margin 0 on the QR: the quiet zone is blank label supplied by --tool-quiet,
// so none of the code box is spent on white we cannot then adjust. // so none of the code box is spent on white we cannot then adjust.
async function renderOne(row) { async function renderOne(page) {
const text = row.content const text = page.content
if (!text) return '' if (!text) return ''
if (codetype.value === 'qr') { if (codeTypeFor(page) === 'qr') {
return qrSvgDataUri(text, { errorCorrection: errorcorrection.value }) return qrSvgDataUri(text, { errorCorrection: errorcorrection.value })
} }
return barcodeSvgDataUri(text) return barcodeSvgDataUri(text)
@@ -453,6 +575,7 @@ const VAR_NAMES = [
'--tool-label-w', '--tool-label-h', '--tool-pad', '--tool-code', '--tool-barh', '--tool-label-w', '--tool-label-h', '--tool-pad', '--tool-code', '--tool-barh',
'--tool-quiet', '--tool-gap', '--tool-font', '--tool-align', '--tool-valign', '--tool-quiet', '--tool-gap', '--tool-font', '--tool-align', '--tool-valign',
'--tool-content-x', '--tool-content-y', '--tool-nudge-x', '--tool-nudge-y', '--tool-content-x', '--tool-content-y', '--tool-nudge-x', '--tool-nudge-y',
'--tool-pic', '--tool-pic-opacity',
] ]
function applyVars() { function applyVars() {
@@ -478,6 +601,8 @@ function applyVars() {
root.style.setProperty('--tool-content-y', contenty.value + 'in') root.style.setProperty('--tool-content-y', contenty.value + 'in')
root.style.setProperty('--tool-nudge-x', nudgex.value + 'in') root.style.setProperty('--tool-nudge-x', nudgex.value + 'in')
root.style.setProperty('--tool-nudge-y', nudgey.value + 'in') root.style.setProperty('--tool-nudge-y', nudgey.value + 'in')
root.style.setProperty('--tool-pic', picturesize.value + 'in')
root.style.setProperty('--tool-pic-opacity', String(pictureopacity.value))
} }
function clearVars() { function clearVars() {
@@ -510,6 +635,21 @@ function resetSettings() {
nudgey.value = 0 nudgey.value = 0
} }
// --- picture ----------------------------------------------------------------
function onPictureFile(event) {
const file = event.target.files && event.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => { pictureUrl.value = String(reader.result || '') }
reader.onerror = () => { pictureUrl.value = '' }
reader.readAsDataURL(file)
}
function clearPicture() {
pictureUrl.value = ''
}
// --- csv helpers ------------------------------------------------------------ // --- csv helpers ------------------------------------------------------------
function onCsvFile(event) { function onCsvFile(event) {
@@ -524,8 +664,7 @@ function onCsvFile(event) {
function downloadTemplate() { function downloadTemplate() {
// The template carries the back columns only when they would be read, so a // The template carries the back columns only when they would be read, so a
// one-sided run is not handed a CSV with two columns it must ignore. // one-sided run is not handed a CSV with two columns it must ignore.
const perRowBack = backenabled.value && backsource.value === 'column' const csv = backCarriesOwnContent.value
const csv = perRowBack
? [ ? [
'content,label,copies,back,backlabel', 'content,label,copies,back,backlabel',
'https://example.com/asset/1,Bay 12 press,1,https://example.com/asset/1/history,Service history', 'https://example.com/asset/1,Bay 12 press,1,https://example.com/asset/1/history,Service history',
@@ -571,6 +710,7 @@ onMounted(() => {
if (saved[key] !== undefined) settingRefs[key].value = saved[key] if (saved[key] !== undefined) settingRefs[key].value = saved[key]
} }
} catch { /* ignore a corrupt entry */ } } catch { /* ignore a corrupt entry */ }
reconcileBackLayout()
applyVars() applyVars()
renderAll() renderAll()
}) })
@@ -582,7 +722,7 @@ watch(SETTING_KEYS.map(key => settingRefs[key]), () => {
saveSettings() saveSettings()
}) })
watch([pages, codetype, errorcorrection], renderAll) watch([pages, codetype, backcodetype, errorcorrection], renderAll)
</script> </script>
<style scoped> <style scoped>
@@ -590,7 +730,7 @@ watch([pages, codetype, errorcorrection], renderAll)
to Margins = None and Scale = 100%. */ to Margins = None and Scale = 100%. */
@page { size: var(--tool-label-w) var(--tool-label-h); margin: 0; } @page { size: var(--tool-label-w) var(--tool-label-h); margin: 0; }
.no-print { padding: 20px; } .code-generator { width: 100%; }
.controls { .controls {
background: var(--bg-card); background: var(--bg-card);
@@ -598,19 +738,8 @@ watch([pages, codetype, errorcorrection], renderAll)
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 20px;
max-width: 66rem;
} }
.controls-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.controls-head h3 { margin: 0; }
.section-heading { .section-heading {
margin: 1.25rem 0 0.5rem; margin: 1.25rem 0 0.5rem;
font-size: 0.9rem; font-size: 0.9rem;
@@ -675,7 +804,7 @@ watch([pages, codetype, errorcorrection], renderAll)
.fit-advice { flex: 1 1 22rem; align-self: center; } .fit-advice { flex: 1 1 22rem; align-self: center; }
.sheet { .sheet {
padding: 20px; padding: 20px 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
@@ -701,21 +830,64 @@ watch([pages, codetype, errorcorrection], renderAll)
} }
/* Everything printed sits in here, so it can be aligned and nudged as one /* Everything printed sits in here, so it can be aligned and nudged as one
block without the label box following it off the stock. */ block without the label box following it off the stock. No max-width or
max-height: overflow must CLIP against the label, the way it did before this
wrapper existed. Constrain it and the flex children shrink instead, and the
one that gives way is the barcode - it is the only box allowed to flex. */
.content { .content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--tool-gap); gap: var(--tool-gap);
max-width: 100%; position: relative;
max-height: 100%; z-index: 1;
transform: translate(var(--tool-content-x, 0), var(--tool-content-y, 0)); transform: translate(var(--tool-content-x, 0), var(--tool-content-y, 0));
} }
.label.layout-stack .content { flex-direction: column; justify-content: center; } /* The code and its caption, which is the group a picture sits above or beside. */
.codegroup {
display: flex;
align-items: center;
gap: var(--tool-gap);
}
.label.layout-stack .codegroup { flex-direction: column; justify-content: center; }
.label.pic-above .content { flex-direction: column; }
.label.pic-beside .content { flex-direction: row; }
/* A CODE128 wants the width, so its block takes the label rather than sitting /* A CODE128 wants the width, so its block takes the label rather than sitting
in the middle of it. */ in the middle of it. */
.label.type-barcode .content { width: 100%; } .label.type-barcode .content,
.label.type-barcode .codegroup { width: 100%; }
.picturebox {
flex: 0 0 auto;
width: var(--tool-pic);
height: var(--tool-pic);
display: flex;
align-items: center;
justify-content: center;
}
.picture-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* A watermark sits under the code, not in it, and only as dark as the setting
says - a scanner reads contrast, and this is the control that costs it. */
.picture-behind {
position: absolute;
top: 50%;
left: 50%;
width: var(--tool-pic);
height: var(--tool-pic);
transform: translate(-50%, -50%);
object-fit: contain;
opacity: var(--tool-pic-opacity, 0.15);
z-index: 0;
}
.codebox { .codebox {
flex: 0 0 auto; flex: 0 0 auto;
@@ -764,15 +936,18 @@ watch([pages, codetype, errorcorrection], renderAll)
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-light); color: var(--text-light);
z-index: 2;
} }
.label.side-back { outline-style: dotted; } .label.side-back { outline-style: dotted; }
@media print { @media print {
.no-print { display: none !important; }
.side-flag { display: none !important; } .side-flag { display: none !important; }
.sheet { padding: 0; display: block; gap: 0; } .sheet { padding: 0; display: block; gap: 0; }
.label { /* The back's dotted variant outspecifies a bare .label reset, so it printed
a dashed rule down the edge of every second card. */
.label,
.label.side-back {
outline: none; outline: none;
break-after: page; break-after: page;
page-break-after: always; page-break-after: always;