The Dymo page: stock, grid, cut guides and a sheet preview

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vunz4VLxAYDBnU5tLciLDC
This commit is contained in:
jessikittyandClaude Opus 5 committed 2026-10-13 11:01:51 +11:00
1 parent 3b6cb77388
commit 5d118eb286
1 file changed
+267
+267
View File
@@ -0,0 +1,267 @@
{% extends "base.html" %}
{% block title %}Dymo Label Sheets - Admin{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/labels-bricklink.css') }}">
{% endblock %}
{% block content %}
<div class="bl">
{% set section = 'labels' %}
{% set bar_sub = 'Dymo LabelWriter die-cut stock' %}
{% set crumbs = [{'name': 'My Labels', 'url': url_for('labels.index')}, {'name': 'Dymo sheets'}] %}
{% include 'admin/labels/_nav.html' %}
<form method="POST" action="{{ url_for('dymo.index') }}" id="dymoForm">
<input type="hidden" name="ids" value="{{ ids }}">
{# Same hidden marker as the tape page: it is how an unticked checkbox is
told apart from a page opened with no options at all. #}
<input type="hidden" name="_opts" value="1">
<input type="hidden" name="groups" value="{{ request.values.get('groups', '') }}">
<div class="row g-2 mt-0">
<div class="col-lg-4">
<div class="bl-panel">
<div class="bl-panel-head"><span>Label stock</span></div>
<div class="bl-panel-body">
<label class="fld">Loaded in the LabelWriter</label>
<select name="stock" class="w-100 mb-2" id="stockSelect">
{% for key, preset in stocks.items() %}
<option value="{{ key }}" {% if key == sheet.stock %}selected{% endif %}>{{ preset.name }}</option>
{% endfor %}
</select>
<div class="k mb-2" style="text-transform:none;">{{ stock.note }}</div>
<label class="fld">Labels per sticker</label>
<select name="layout" class="w-100 mb-2" id="layoutSelect">
{% for key, text in layouts %}
<option value="{{ key }}" {% if key == sheet.layout %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
<div class="k mb-2" style="text-transform:none;">
{{ stock.width_mm }} &times; {{ stock.height_mm }}mm fits
<strong>{{ per_sheet }}</strong> standard 40 &times; 15mm labels
({{ grid }}). A two-part drawer label is taller, so a mixed
run fits fewer.
</div>
<div class="row g-2">
<div class="col-6">
<label class="fld">Margin (mm)</label>
<input type="number" name="margin_mm" step="0.5" min="0" max="10"
class="w-100" value="{{ sheet.margin_mm }}">
</div>
<div class="col-6">
<label class="fld">Gap (mm)</label>
<input type="number" name="gap_mm" step="0.5" min="0" max="10"
class="w-100" value="{{ sheet.gap_mm }}">
</div>
<div class="col-6">
<label class="fld">Skip slots</label>
<input type="number" name="start_at" min="0" max="50"
class="w-100" value="{{ sheet.start_at }}">
</div>
<div class="col-6">
<label class="fld">Copies each</label>
<input type="number" name="copies" min="1" max="99"
class="w-100" value="{{ options.copies }}">
</div>
</div>
<hr class="my-2">
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
<input type="checkbox" name="cut_guides" value="1"
{% if sheet.cut_guides %}checked{% endif %}>
Dashed cut guides
</label>
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
<input type="checkbox" name="centre" value="1"
{% if sheet.centre %}checked{% endif %}>
Centre on the sticker
</label>
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
<input type="checkbox" name="monochrome" value="1"
{% if sheet.monochrome %}checked{% endif %}>
Monochrome (thermal)
</label>
<div class="k mt-1" style="text-transform:none;">
A LabelWriter is direct thermal: black dots on white, no
colour and no greys of its own. Converting here means the
preview shows what will actually come out.
</div>
</div>
</div>
<div class="bl-panel">
<div class="bl-panel-head"><span>The labels themselves</span></div>
<div class="bl-panel-body">
<label class="fld">Size</label>
<select name="size_mode" class="w-100 mb-2" id="sizeSelect">
{% for key, text in size_modes %}
<option value="{{ key }}" {% if key == options.size_mode %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
<label class="d-flex align-items-center gap-2 mb-2" style="font-size:12px;">
<input type="checkbox" name="group_drawers" value="1"
{% if options.group_drawers %}checked{% endif %}>
Combine parts that share a drawer
</label>
<div class="row g-2">
<div class="col-6">
<label class="fld">Text size
<span class="num" id="fontOut">{{ '%.2f'|format(options.font_scale) }}&times;</span>
</label>
<input type="number" name="font_scale" step="0.05" min="0.5" max="2"
class="w-100" value="{{ options.font_scale }}" id="fontNum">
</div>
<div class="col-6">
<label class="fld">Image width %</label>
<input type="number" name="image_percent" min="0" max="60"
class="w-100" value="{{ options.image_percent }}">
</div>
<div class="col-12">
<input type="range" min="0.5" max="2" step="0.05" class="w-100"
value="{{ options.font_scale }}" id="fontRange">
<div class="d-flex gap-1 flex-wrap">
{% for preset, text in [(1.0, 'Normal'), (1.4, 'Large'), (2.0, 'Largest')] %}
<button type="button" class="bl-btn bl-btn-sm font-preset"
data-scale="{{ preset }}">{{ text }}</button>
{% endfor %}
</div>
<div class="k mt-1" style="text-transform:none;">
The name is sized first and the image gives way to it. The
whole name always prints, so once it spans the 40mm label
turning this up changes nothing &mdash; most names hit that by
about 1.4&times;. Untick <em>Part image</em> to give the text
the whole width.
</div>
</div>
<div class="col-6">
<label class="fld">Padding (mm)</label>
<input type="number" name="padding_mm" step="0.1" min="0" max="10"
class="w-100" value="{{ options.padding_mm }}">
</div>
<div class="col-6">
<label class="fld">Align</label>
<select name="align" class="w-100">
<option value="left" {% if options.align == 'left' %}selected{% endif %}>Left</option>
<option value="center" {% if options.align == 'center' %}selected{% endif %}>Centre</option>
</select>
</div>
</div>
</div>
</div>
<div class="bl-panel">
<div class="bl-panel-head"><span>Show on label</span></div>
<div class="bl-panel-body">
{% for value, text in field_options %}
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
<input type="checkbox" name="fields" value="{{ value }}"
{% if value in options.fields %}checked{% endif %}> {{ text }}
</label>
{% endfor %}
</div>
</div>
</div>
<div class="col-lg-8">
<div class="bl-panel">
<div class="bl-panel-head"><span>Print</span></div>
<div class="bl-panel-body">
<div class="d-flex gap-2 flex-wrap align-items-center">
<button type="submit" formaction="{{ url_for('dymo.download_pdf') }}"
class="bl-btn bl-btn-go">
<i class="bi bi-file-pdf"></i>
Download PDF &mdash; {{ pages|length }} sticker{{ '' if pages|length == 1 else 's' }}
</button>
<button type="submit" class="bl-btn">Refresh preview</button>
<a href="{{ url_for('tape.index', ids=ids) }}" class="bl-btn">
<i class="bi bi-upc"></i> Tape instead
</a>
</div>
<div class="k mt-2" style="text-transform:none;">
Each PDF page is exactly {{ stock.width_mm }} &times; {{ stock.height_mm }}mm,
so print it with the Dymo driver set to this label and
<strong>scale 100% / Actual size</strong> &mdash; not "Fit to page",
which shrinks everything by a few percent and spoils the 40mm width.
The LabelWriter 4XL is USB, so LIM can't print to it over the
network the way it does the tape printer.
</div>
</div>
</div>
<div class="bl-panel">
<div class="bl-panel-head">
<span>Preview &mdash; {{ pages|length }} sticker{{ '' if pages|length == 1 else 's' }}
from {{ plan|length }} label{{ '' if plan|length == 1 else 's' }}</span>
<span class="num">{{ stock.width_mm }} &times; {{ stock.height_mm }}mm</span>
</div>
<div class="bl-panel-body">
{% if not options.fields %}
<div class="mb-2"><span class="bl-tag warn">Nothing selected</span>
Every field is unticked, so the labels will print blank.</div>
{% endif %}
{% for page in pages[:6] %}
<div class="mb-3">
<div class="k mb-1" style="text-transform:none;">
Sticker {{ loop.index }} of {{ pages|length }} &middot;
{{ page | rejectattr('blank') | list | length }} label{{ '' if page|length == 1 else 's' }}
</div>
<img style="border:1px solid #c9d3dd;background:#fff;max-width:100%;"
alt="Dymo sticker {{ loop.index }}"
src="{{ url_for('dymo.preview', page=loop.index, **query) }}">
</div>
{% endfor %}
{% if pages|length > 6 %}
<div class="k" style="text-transform:none;">
Showing the first 6 of {{ pages|length }}. All of them are in the PDF.
</div>
{% endif %}
<div class="k mt-2" style="text-transform:none;">
300 dpi, the LabelWriter's native resolution. Drawn by the same
code as the tape labels, so a drawer label is the same
{{ '%.0f'|format(40) }} &times; 15mm sticker whichever printer makes it.
Cut along the dashed lines.
</div>
</div>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Changing the stock, the grid or the label size re-renders the sheet.
['stockSelect', 'layoutSelect', 'sizeSelect'].forEach(id => {
document.getElementById(id)?.addEventListener('change',
() => document.getElementById('dymoForm').submit());
});
// Text size: slider, box and presets all drive the one value.
(function () {
const num = document.getElementById('fontNum');
const range = document.getElementById('fontRange');
const out = document.getElementById('fontOut');
if (!num || !range) { return; }
const show = v => { if (out) { out.textContent = Number(v).toFixed(2) + '×'; } };
num.addEventListener('input', () => { range.value = num.value; show(num.value); });
range.addEventListener('input', () => { num.value = range.value; show(range.value); });
range.addEventListener('change', () => document.getElementById('dymoForm').submit());
document.querySelectorAll('.font-preset').forEach(btn => {
btn.addEventListener('click', () => {
num.value = range.value = btn.dataset.scale;
show(btn.dataset.scale);
document.getElementById('dymoForm').submit();
});
});
})();
</script>
{% endblock %}