Stop a caption from eating the barcode, and put the labels beside the settings
Some checks failed
CI / naming (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / migrations-mysql (push) Has been cancelled
CI / backend (push) Has been cancelled

Reported as the CODE128 not appearing in the preview. It was appearing, in 44
of the 45 preset and layout combinations swept - as a 36 pixel stub. The code
box and the caption were both allowed to grow, so a caption of any length took
half the label and then kept taking, and on 1.00 x 0.50in stock a few words of
caption left a barcode too narrow to scan. Present, useless, and indisinguishable
from a failed render. The caption now takes the room it needs and no more, and a
CODE128 holds at least 40% of the label, or all of it when stacked: past that
there are too few narrow bars left to read, and a caption that wraps or clips is
the better trade.

So that a real failure can never again look like this one, an empty code box now
says so in red in the preview. It is screen-only - the marker must never reach a
printed label.

The settings were a narrow column with the preview trailing below them, which
put the labels being adjusted off the bottom of the screen. Settings on the
left, labels on the right in a column that stays in view and scrolls on its own,
folding back to one column on a narrow window.

That scrolling column is a print hazard worth naming: an ancestor with its own
overflow clips a multi-page print to whatever happened to be on screen. Print
resets it, and the check is a six-page two-sided run coming out as six pages
with the label as the only clipping box in the chain.
This commit is contained in:
cproudlock
2026-08-21 11:55:36 -04:00
parent b7cfd2b198
commit e9fcd1e598
2 changed files with 141 additions and 34 deletions

View File

@@ -51,6 +51,14 @@ ADR-007 and ADR-002.
### Fixed ### Fixed
- **A caption could eat the barcode.** The code box and the caption both grew,
so any caption took half the label and kept going: "BAY 12 PRESS LINE 1" on
1.00 x 0.50in stock left a 36px stub, which is a code that is present,
unscannable, and reads as one that failed to render. The caption now takes
only the room it needs and a CODE128 keeps at least 40% of the label, all of
it when stacked. A code that genuinely fails to render says so in the preview
now, in red, rather than leaving white space that looks like a label with no
code on it.
- **The generator prints from inside the app shell.** It was a full-screen - **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 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 an ordinary page - navigation, full width - and `style.css` hides the shell at
@@ -64,6 +72,14 @@ ADR-007 and ADR-002.
### Changed ### Changed
- **Settings on the left, labels on the right.** The controls were a narrow
column with the preview trailing underneath, so the labels being adjusted were
off the bottom of the screen. The preview is now a column of its own that
stays in view and scrolls by itself, and it folds back to one column on a
narrow window. Note for anyone changing it: that scrolling column is exactly
the kind of ancestor that clips a multi-page print to whatever was on screen,
so print resets its overflow - verified by printing a six-page two-sided run,
not by looking at the preview.
- CSV parsing and page building moved out of the generator component into - CSV parsing and page building moved out of the generator component into
`plugins/tools/frontend/labelPages.js`, where the column contract and the `plugins/tools/frontend/labelPages.js`, where the column contract and the
two-sided page order are covered by tests. two-sided page order are covered by tests.

View File

@@ -1,12 +1,13 @@
<template> <template>
<div class="code-generator"> <div class="code-generator">
<div class="no-print"> <div class="page-header no-print">
<div class="page-header"> <h1>Barcode / QR Generator</h1>
<h1>Barcode / QR Generator</h1> <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"> <div class="workspace">
<div class="options no-print">
<div class="controls card">
<!-- What to encode --> <!-- What to encode -->
<div class="control-row"> <div class="control-row">
<label> <label>
@@ -285,33 +286,52 @@
{{ overlong.length }} row(s) could not be encoded as CODE128 or were empty: {{ overlong.length }} row(s) could not be encoded as CODE128 or were empty:
{{ overlong.slice(0, 3).join(', ') }}{{ overlong.length > 3 ? ' ...' : '' }} {{ overlong.slice(0, 3).join(', ') }}{{ overlong.length > 3 ? ' ...' : '' }}
</p> </p>
</div>
</div>
<!-- Preview doubles as the print surface: what is on screen is what prints. -->
<div class="sheet">
<div
v-for="item in visible"
:key="item.index"
class="label"
:class="labelClasses(item.page)"
>
<img v-if="showPicture(item.page) && pictureposition === 'behind'"
:src="pictureUrl" class="picture-behind" alt="" />
<div class="content">
<div v-if="showPicture(item.page) && pictureposition !== 'behind'" class="picturebox">
<img :src="pictureUrl" class="picture-img" alt="" />
</div>
<div class="codegroup">
<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">{{ item.page.side }}</span> </div>
<!-- Preview doubles as the print surface: what is on screen is what
prints, so everything around it is chrome that print must strip. -->
<div class="preview">
<div class="preview-head no-print">
<h4 class="section-heading">Preview</h4>
<span class="preview-count">
{{ cardCount }} label{{ cardCount === 1 ? '' : 's' }}<span
v-if="backenabled">, {{ pages.length }} pages</span>
</span>
</div>
<div class="sheet">
<div
v-for="item in visible"
:key="item.index"
class="label"
:class="labelClasses(item.page)"
>
<img v-if="showPicture(item.page) && pictureposition === 'behind'"
:src="pictureUrl" class="picture-behind" alt="" />
<div class="content">
<div v-if="showPicture(item.page) && pictureposition !== 'behind'" class="picturebox">
<img :src="pictureUrl" class="picture-img" alt="" />
</div>
<div class="codegroup">
<div v-if="layoutFor(item.page) !== 'textonly'" class="codebox">
<img v-if="images[item.index]" :src="images[item.index]" class="code-img" alt="" />
<!-- An empty code box is indistinguishable from a label that
simply has no code on it, which is how a failed render
reads as "the barcode does not appear". Say so, on screen
only - the printed label must never carry this. -->
<span v-else class="code-missing">no code</span>
</div>
<div v-if="layoutFor(item.page) !== 'codeonly' && item.page.label" class="label-text">
{{ item.page.label }}
</div>
</div>
</div>
<span v-if="backenabled" class="side-flag">{{ item.page.side }}</span>
</div>
<p v-if="!pages.length" class="preview-empty no-print">
Nothing to print yet - enter content above.
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -732,6 +752,47 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.code-generator { width: 100%; } .code-generator { width: 100%; }
/* Settings on the left, the labels themselves on the right, where they stay in
view while the settings that change them are being worked. The preview
column scrolls on its own so a 500-label run cannot push the controls off
the screen. */
.workspace {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(300px, 2fr);
gap: 20px;
align-items: start;
}
.preview {
position: sticky;
top: 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px 16px;
max-height: calc(100vh - var(--main-pad-top) - 24px);
overflow: auto;
}
.preview-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.preview-head .section-heading { margin-top: 0.25rem; }
.preview-count { font-size: 0.8125rem; color: var(--text-light); }
.preview-empty { color: var(--text-light); font-size: 0.8125rem; margin: 0; }
/* One column once the two would be too narrow to work in. */
@media (max-width: 1200px) {
.workspace { grid-template-columns: minmax(0, 1fr); }
.preview { position: static; max-height: none; }
}
.controls { .controls {
background: var(--bg-card); background: var(--bg-card);
color: var(--text); color: var(--text);
@@ -905,18 +966,35 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
.label.type-barcode .codebox { .label.type-barcode .codebox {
flex: 1 1 auto; flex: 1 1 auto;
width: auto; width: auto;
min-width: 0; /* 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);
} }
.label.layout-stack.type-barcode .codebox { min-width: 100%; }
/* Screen-only, and deliberately loud: an empty box means the render failed. */
.code-missing {
font-family: Arial, Helvetica, sans-serif;
font-size: 6px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--danger);
}
.code-img { .code-img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
/* The caption takes what it needs and no more. Both boxes used to grow, so a
caption of any length halved the barcode and then kept going - at 1x0.5in
with a couple of words it left a 36px stub that reads as "the code did not
render". A code that cannot be scanned is not a label. */
.label-text { .label-text {
flex: 1 1 auto; flex: 0 1 auto;
min-width: 0; min-width: 0;
text-align: center; text-align: center;
font-family: "Arial Narrow", Arial, Helvetica, sans-serif; font-family: "Arial Narrow", Arial, Helvetica, sans-serif;
@@ -943,7 +1021,20 @@ watch([pages, codetype, backcodetype, errorcorrection], renderAll)
@media print { @media print {
.side-flag { display: none !important; } .side-flag { display: none !important; }
.code-missing { display: none !important; }
.sheet { padding: 0; display: block; gap: 0; } .sheet { padding: 0; display: block; gap: 0; }
/* The preview column is chrome. Its overflow matters most: a scrolling
ancestor clips a multi-page print to whatever was on screen. */
.workspace { display: block; }
.preview {
position: static;
max-height: none;
overflow: visible;
border: 0;
border-radius: 0;
padding: 0;
background: none;
}
/* The back's dotted variant outspecifies a bare .label reset, so it printed /* The back's dotted variant outspecifies a bare .label reset, so it printed
a dashed rule down the edge of every second card. */ a dashed rule down the edge of every second card. */
.label, .label,