Refuse a measurement that makes the label stop existing
Some checks failed
CI / backend (push) Failing after 7m11s
CI / naming (push) Failing after 7m10s
CI / frontend (push) Failing after 7m18s
CI / migrations-mysql (push) Failing after 7m5s

Reported as a barcode rendering 176 by 0. A bar height of zero is what does it:
the number went straight into a CSS length, height: 0in is perfectly valid, and
an image sized height: 100% of a zero-height box is zero pixels tall. The label
comes out full width with nothing on it, which reads as a rendering failure
rather than as a field doing exactly what it was told.

A cleared field was the same bug wearing a different hat. An emptied number
input holds '', not 0, so the page emitted `NaNin`; the browser dropped the
declaration and the box fell back to auto height. That one merely looked wrong
instead of vanishing, which is arguably worse for finding it.

Measurements now go through labelVars(), which will not emit NaN, an empty
length, or a zero for anything whose zero means the box stops existing. Padding,
the quiet zone and the gap still take zero, because there zero is a real answer,
and offsets stay signed. It is a separate module because this is exactly the
kind of arithmetic that needs tests rather than a preview: every case in
labelVars.spec.js is a value a number input can actually hold.

The stylesheet now carries fallbacks on its var() reads as well, but they are a
net, not the fix: a custom property set to garbage is substituted and then
dropped at computed value time, and the fallback does not apply - it only covers
a property that is absent entirely.
This commit is contained in:
cproudlock
2026-08-21 12:09:26 -04:00
parent 3703412baf
commit f09d14d28c
4 changed files with 214 additions and 44 deletions

View File

@@ -342,6 +342,7 @@ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { LABEL_PRESETS, qrModuleCount, qrFit, qrSvgDataUri, barcodeSvgDataUri }
from '@/utils/codes'
import { parseCsv, buildPages, MAX_LABELS } from '../labelPages'
import { labelVars, LABEL_VAR_NAMES } from '../labelVars'
const PRESETS = LABEL_PRESETS
@@ -591,43 +592,44 @@ async function renderAll() {
// @page cannot read a scoped style, so the sizing lives in custom properties on
// the root element. Chromium honors var() in @page size.
const VAR_NAMES = [
'--tool-label-w', '--tool-label-h', '--tool-pad', '--tool-code', '--tool-barh',
'--tool-quiet', '--tool-gap', '--tool-font', '--tool-align', '--tool-valign',
'--tool-content-x', '--tool-content-y', '--tool-nudge-x', '--tool-nudge-y',
'--tool-pic', '--tool-pic-opacity',
]
//
// The values are built in ../labelVars.js, which is also where the rule lives
// that none of them may be NaN or empty: a cleared number field used to emit
// `NaNin`, the browser dropped the declaration, and a box that fell back to
// auto height rendered the barcode 176 wide by 0 high.
function applyVars() {
const root = document.documentElement
root.style.setProperty('--tool-label-w', labelwidth.value + 'in')
root.style.setProperty('--tool-label-h', labelheight.value + 'in')
root.style.setProperty('--tool-pad', padding.value + 'in')
root.style.setProperty('--tool-code', codesize.value + 'in')
root.style.setProperty('--tool-barh', barheight.value + 'in')
// Both symbologies need clear space: a QR wants a quiet zone around it, and
// CODE128 wants one at each end (10x the narrow bar). It is the code's own
// margin and nothing else's - the space between the code and the caption is
// --tool-gap, so a tight caption no longer means shaving the quiet zone.
root.style.setProperty('--tool-quiet', quiet.value + 'in')
root.style.setProperty('--tool-gap', gap.value + 'in')
root.style.setProperty('--tool-font', labelfont.value + 'pt')
root.style.setProperty('--tool-align', align.value)
root.style.setProperty('--tool-valign', valign.value)
// Content offset moves what is printed WITHIN the label. Media offset moves
// the label box itself on the stock. They are different fixes: the first
// composes a label, the second corrects a printer whose origin is off.
root.style.setProperty('--tool-content-x', contentx.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-y', nudgey.value + 'in')
root.style.setProperty('--tool-pic', picturesize.value + 'in')
root.style.setProperty('--tool-pic-opacity', String(pictureopacity.value))
const vars = labelVars({
labelwidth: labelwidth.value,
labelheight: labelheight.value,
padding: padding.value,
codesize: codesize.value,
barheight: barheight.value,
// Both symbologies need clear space: a QR wants a quiet zone around it, and
// CODE128 wants one at each end (10x the narrow bar). It is the code's own
// margin and nothing else's - the space between the code and the caption is
// the gap, so a tight caption no longer means shaving the quiet zone.
quiet: quiet.value,
gap: gap.value,
labelfont: labelfont.value,
align: align.value,
valign: valign.value,
// Content offset moves what is printed WITHIN the label. Media offset moves
// the label box itself on the stock. They are different fixes: the first
// composes a label, the second corrects a printer whose origin is off.
contentx: contentx.value,
contenty: contenty.value,
nudgex: nudgex.value,
nudgey: nudgey.value,
picturesize: picturesize.value,
pictureopacity: pictureopacity.value,
})
for (const [name, value] of Object.entries(vars)) root.style.setProperty(name, value)
}
function clearVars() {
const root = document.documentElement
for (const name of VAR_NAMES) root.style.removeProperty(name)
for (const name of LABEL_VAR_NAMES) root.style.removeProperty(name)
}
function applyPreset() {
@@ -748,7 +750,7 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
<style scoped>
/* One page per side. Chromium reads var() here; the print dialog must be set
to Margins = None and Scale = 100%. */
@page { size: var(--tool-label-w) var(--tool-label-h); margin: 0; }
@page { size: var(--tool-label-w, 1in) var(--tool-label-h, 0.5in); margin: 0; }
.code-generator { width: 100%; }
@@ -875,9 +877,9 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
media offset moves it, and that exists for a printer whose origin is off. */
.label {
box-sizing: border-box;
width: var(--tool-label-w);
height: var(--tool-label-h);
padding: var(--tool-pad);
width: var(--tool-label-w, 1in);
height: var(--tool-label-h, 0.5in);
padding: var(--tool-pad, 0);
background: #fff;
color: #000;
overflow: hidden;
@@ -898,7 +900,7 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.content {
display: flex;
align-items: center;
gap: var(--tool-gap);
gap: var(--tool-gap, 0.03in);
position: relative;
z-index: 1;
transform: translate(var(--tool-content-x, 0), var(--tool-content-y, 0));
@@ -908,7 +910,7 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.codegroup {
display: flex;
align-items: center;
gap: var(--tool-gap);
gap: var(--tool-gap, 0.03in);
}
.label.layout-stack .codegroup { flex-direction: column; justify-content: center; }
@@ -923,8 +925,8 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.picturebox {
flex: 0 0 auto;
width: var(--tool-pic);
height: var(--tool-pic);
width: var(--tool-pic, 0.5in);
height: var(--tool-pic, 0.5in);
display: flex;
align-items: center;
justify-content: center;
@@ -952,9 +954,9 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.codebox {
flex: 0 0 auto;
margin: var(--tool-quiet);
width: var(--tool-code);
height: var(--tool-code);
margin: var(--tool-quiet, 0.035in);
width: var(--tool-code, 0.43in);
height: var(--tool-code, 0.43in);
display: flex;
align-items: center;
justify-content: center;
@@ -969,7 +971,7 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
/* Never below 40% of the label: past that a CODE128 is too few narrow bars
to scan, so it is better for the caption to wrap or clip. */
min-width: 40%;
height: var(--tool-barh);
height: var(--tool-barh, 0.3in);
}
.label.layout-stack.type-barcode .codebox { min-width: 100%; }
@@ -998,7 +1000,7 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
min-width: 0;
text-align: center;
font-family: "Arial Narrow", Arial, Helvetica, sans-serif;
font-size: var(--tool-font);
font-size: var(--tool-font, 7pt);
font-weight: 700;
line-height: 1.05;
overflow-wrap: anywhere;