retail-print-kit 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +45 -1
  3. package/dist/label-design/builtins-data.d.ts +2 -0
  4. package/dist/label-design/builtins-data.js +1631 -0
  5. package/dist/label-design/builtins.d.ts +13 -0
  6. package/dist/label-design/builtins.js +124 -0
  7. package/dist/label-design/encode.d.ts +11 -0
  8. package/dist/label-design/encode.js +151 -0
  9. package/dist/label-design/fields.d.ts +30 -0
  10. package/dist/label-design/fields.js +115 -0
  11. package/dist/label-design/fonts.d.ts +6 -0
  12. package/dist/label-design/fonts.js +35 -0
  13. package/dist/label-design/image.d.ts +4 -0
  14. package/dist/label-design/image.js +48 -0
  15. package/dist/label-design/index.d.ts +12 -0
  16. package/dist/label-design/index.js +72 -0
  17. package/dist/label-design/printer-text.d.ts +12 -0
  18. package/dist/label-design/printer-text.js +18 -0
  19. package/dist/label-design/render.d.ts +30 -0
  20. package/dist/label-design/render.js +419 -0
  21. package/dist/label-design/types.d.ts +137 -0
  22. package/dist/label-design/types.js +2 -0
  23. package/dist/label-design/units.d.ts +5 -0
  24. package/dist/label-design/units.js +17 -0
  25. package/dist/label-design/validate.d.ts +15 -0
  26. package/dist/label-design/validate.js +97 -0
  27. package/dist/label-designer/designer.d.ts +26 -0
  28. package/dist/label-designer/designer.js +1349 -0
  29. package/dist/label-designer/index.d.ts +4 -0
  30. package/dist/label-designer/index.js +10 -0
  31. package/dist/label-designer/preview.d.ts +14 -0
  32. package/dist/label-designer/preview.js +73 -0
  33. package/dist/label-designer/styles.d.ts +1 -0
  34. package/dist/label-designer/styles.js +4 -0
  35. package/dist/label-fonts/FONTS.md +11 -0
  36. package/dist/label-fonts/archivo-narrow-400.woff2 +0 -0
  37. package/dist/label-fonts/archivo-narrow-700.woff2 +0 -0
  38. package/dist/label-fonts/archivo-narrow-OFL.txt +93 -0
  39. package/dist/label-fonts/arimo-400-italic.woff2 +0 -0
  40. package/dist/label-fonts/arimo-400.woff2 +0 -0
  41. package/dist/label-fonts/arimo-700.woff2 +0 -0
  42. package/dist/label-fonts/arimo-OFL.txt +93 -0
  43. package/dist/label-fonts/oswald-500.woff2 +0 -0
  44. package/dist/label-fonts/oswald-700.woff2 +0 -0
  45. package/dist/label-fonts/oswald-OFL.txt +93 -0
  46. package/dist/label-fonts/roboto-slab-400.woff2 +0 -0
  47. package/dist/label-fonts/roboto-slab-700.woff2 +0 -0
  48. package/dist/label-fonts/roboto-slab-LICENSE.txt +202 -0
  49. package/package.json +34 -2
@@ -0,0 +1,1349 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineLabelDesigner = defineLabelDesigner;
4
+ const label_design_1 = require("../label-design");
5
+ const preview_1 = require("./preview");
6
+ const styles_1 = require("./styles");
7
+ const BASIC_PALETTE = [
8
+ { kind: 'text', label: 'Text', icon: 'type' }, { kind: 'line', label: 'Line', icon: 'line' }, { kind: 'box', label: 'Box', icon: 'square' },
9
+ { kind: 'fill', label: 'Filled box', icon: 'fill' }, { kind: 'qr', label: 'QR code', icon: 'qr' }, { kind: 'image', label: 'Logo', icon: 'image' }
10
+ ];
11
+ const ICON_PATHS = {
12
+ search: '<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2"/>',
13
+ info: '<circle cx="12" cy="12" r="8.5"/><path d="M12 11v5M12 8h.01"/>',
14
+ text: '<path d="M4 6.5h16M4 12h16M4 17.5h10"/>',
15
+ type: '<path d="M5 6h14M12 6v13M9.5 19h5"/>',
16
+ dollar: '<path d="M12 3.5v17M16 7.5c-.6-1.4-2.1-2.2-4-2.2-2.3 0-4 1.2-4 3 0 4.4 8.2 2.3 8.2 6.6 0 1.9-1.8 3.1-4.2 3.1-2 0-3.7-.9-4.3-2.5"/>',
17
+ box: '<path d="M3.5 7.5L12 3.5l8.5 4v9L12 20.5l-8.5-4z"/><path d="M3.5 7.5L12 11.5l8.5-4M12 11.5v9"/>',
18
+ store: '<path d="M4.5 10.5v9h15v-9"/><path d="M3.5 10.5l1.8-6h13.4l1.8 6c0 1.4-1.2 2.3-2.6 2.3s-2.6-.9-2.6-2.3c0 1.4-1.2 2.3-2.6 2.3s-2.7-.9-2.7-2.3c0 1.4-1.2 2.3-2.6 2.3s-2.5-.9-2.5-2.3z"/><path d="M10 19.5v-4.5h4v4.5"/>',
19
+ layers: '<path d="M12 3.5l8.5 4.5-8.5 4.5L3.5 8z"/><path d="M3.5 12.5l8.5 4.5 8.5-4.5M3.5 16.5l8.5 4.5 8.5-4.5"/>',
20
+ truck: '<path d="M3 6.5h11v9H3zM14 9.5h3.5l3 3.2v2.8H14"/><circle cx="7" cy="17.5" r="1.8"/><circle cx="17" cy="17.5" r="1.8"/>',
21
+ hash: '<path d="M5 9h14M5 15h14M10.5 4l-2 16M15.5 4l-2 16"/>',
22
+ barcode: '<path d="M4 5v14M7 5v14M10.5 5v14M13 5v14M16.5 5v14M20 5v14"/>',
23
+ calendar: '<rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/>',
24
+ line: '<path d="M4 12h16"/>',
25
+ square: '<rect x="4.5" y="4.5" width="15" height="15" rx="2"/>',
26
+ fill: '<rect x="4.5" y="4.5" width="15" height="15" rx="2" fill="currentColor"/>',
27
+ qr: '<rect x="3.5" y="3.5" width="7" height="7" rx="1"/><rect x="13.5" y="3.5" width="7" height="7" rx="1"/><rect x="3.5" y="13.5" width="7" height="7" rx="1"/><path d="M14 14h2.5v2.5H14zM18 18h2.5v2.5H18zM18 14h2.5M14 18v2.5"/>',
28
+ image: '<rect x="3.5" y="5" width="17" height="14" rx="2.5"/><circle cx="9" cy="10" r="1.6"/><path d="M20.5 15.5l-4.8-4.8-8.2 8.3"/>'
29
+ };
30
+ const ELEMENT_ICONS = { text: 'type', price: 'dollar', barcode: 'barcode', qr: 'qr', box: 'square', fill: 'fill', line: 'line', image: 'image' };
31
+ function icon(name) {
32
+ return `<svg class="ld-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON_PATHS[name] || ICON_PATHS.text}</svg>`;
33
+ }
34
+ const HANDLES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
35
+ const ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
36
+ const CSS_PX_PER_MM = 96 / label_design_1.MM_PER_INCH;
37
+ const CHECK_PAGE = 200;
38
+ const CHECK_MAX_ITEMS = 6000;
39
+ const QR_HINT = 'Link or text. Add a field with {{itemName}}, {{barcode}} and so on.';
40
+ const TRASH = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg>';
41
+ function esc(value) {
42
+ return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
43
+ }
44
+ function on(flag) {
45
+ return flag ? ' on' : '';
46
+ }
47
+ function formatMoney(value) {
48
+ const n = Number(value);
49
+ return Number.isFinite(n) ? `$${n.toFixed(2)}` : '';
50
+ }
51
+ function defineLabelDesigner(tag = 'rpk-label-designer') {
52
+ if (typeof customElements === 'undefined' || customElements.get(tag))
53
+ return;
54
+ (0, preview_1.defineLabelPreview)();
55
+ class LabelDesigner extends HTMLElement {
56
+ constructor() {
57
+ super();
58
+ this.refs = {};
59
+ this.d = null;
60
+ this.label = '';
61
+ this.ro = false;
62
+ this.fonts = 'assets/label-fonts/';
63
+ this.source = null;
64
+ this.store = '';
65
+ this.errorText = '';
66
+ this.savedState = '';
67
+ this.savedAt = null;
68
+ this.undoStack = [];
69
+ this.redoStack = [];
70
+ this.drag = null;
71
+ this.mode = 'design';
72
+ this.zoom = 1;
73
+ this.unit = 'in';
74
+ this.snap = true;
75
+ this.showGrid = true;
76
+ this.selectedId = null;
77
+ this.guides = [];
78
+ this.warnings = [];
79
+ this.paletteFilter = '';
80
+ this.data = (0, label_design_1.sampleData)();
81
+ this.sampleName = 'Sample item';
82
+ this.sampleResults = [];
83
+ this.samplePickerOpen = false;
84
+ this.sampleLoading = false;
85
+ this.sampleTimer = null;
86
+ this.imageMode = 'dither';
87
+ this.imageError = '';
88
+ this.checkRunning = false;
89
+ this.checkDone = 0;
90
+ this.checkTotal = 0;
91
+ this.checkResults = [];
92
+ this.checkFilter = 'all';
93
+ this.checkSelected = null;
94
+ this.checkError = '';
95
+ this.checkToken = 0;
96
+ this.frame = 0;
97
+ this.pending = new Set();
98
+ this.listening = false;
99
+ this.onPointerMove = (event) => {
100
+ const drag = this.drag;
101
+ if (!drag || !this.d)
102
+ return;
103
+ const dx = (event.clientX - drag.startX) / this.pxPerMm;
104
+ const dy = (event.clientY - drag.startY) / this.pxPerMm;
105
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 0.3)
106
+ return;
107
+ if (!drag.moved) {
108
+ this.remember();
109
+ drag.moved = true;
110
+ }
111
+ const el = this.d.elements.find((e) => e.id === drag.id);
112
+ if (!el)
113
+ return;
114
+ const o = drag.orig;
115
+ if (drag.kind === 'move') {
116
+ const pos = this.withGuides(el, o.x + dx, o.y + dy);
117
+ el.x = pos.x;
118
+ el.y = pos.y;
119
+ }
120
+ else {
121
+ const h = drag.handle;
122
+ let x = o.x;
123
+ let y = o.y;
124
+ let w = o.w;
125
+ let hgt = o.h;
126
+ if (h.includes('e'))
127
+ w = o.w + dx;
128
+ if (h.includes('s'))
129
+ hgt = o.h + dy;
130
+ if (h.includes('w')) {
131
+ x = o.x + dx;
132
+ w = o.w - dx;
133
+ }
134
+ if (h.includes('n')) {
135
+ y = o.y + dy;
136
+ hgt = o.h - dy;
137
+ }
138
+ const min = (0, label_design_1.dotsToMm)(2, this.dpi);
139
+ if (w < min) {
140
+ if (h.includes('w'))
141
+ x -= min - w;
142
+ w = min;
143
+ }
144
+ if (hgt < min) {
145
+ if (h.includes('n'))
146
+ y -= min - hgt;
147
+ hgt = min;
148
+ }
149
+ el.x = this.snapMm(x);
150
+ el.y = this.snapMm(y);
151
+ el.w = Math.max(min, this.snapMm(w));
152
+ el.h = Math.max(min, this.snapMm(hgt));
153
+ if (el.type === 'qr')
154
+ el.h = el.w;
155
+ }
156
+ this.update('stage');
157
+ this.update('status');
158
+ };
159
+ this.onPointerUp = () => {
160
+ const drag = this.drag;
161
+ this.drag = null;
162
+ this.guides = [];
163
+ if (drag && drag.moved)
164
+ this.changed();
165
+ else
166
+ this.update('stage');
167
+ };
168
+ this.onKey = (event) => {
169
+ if (this.mode !== 'design' || !this.d || !this.isConnected)
170
+ return;
171
+ const origin = (event.composedPath()[0] || event.target);
172
+ const tag = origin && origin.tagName;
173
+ const typing = tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || (origin && origin.isContentEditable);
174
+ const mod = event.metaKey || event.ctrlKey;
175
+ const key = event.key.toLowerCase();
176
+ if (mod && key === 's') {
177
+ event.preventDefault();
178
+ this.requestSave();
179
+ return;
180
+ }
181
+ if (typing)
182
+ return;
183
+ if (mod && key === 'z') {
184
+ event.preventDefault();
185
+ if (event.shiftKey)
186
+ this.redo();
187
+ else
188
+ this.undo();
189
+ return;
190
+ }
191
+ if (mod && key === 'y') {
192
+ event.preventDefault();
193
+ this.redo();
194
+ return;
195
+ }
196
+ const el = this.selected;
197
+ if (!el || !this.canWrite) {
198
+ if (event.key === 'Escape' && this.selectedId) {
199
+ this.selectedId = null;
200
+ this.update('all');
201
+ }
202
+ return;
203
+ }
204
+ if (mod && key === 'd') {
205
+ event.preventDefault();
206
+ this.duplicateSelected();
207
+ return;
208
+ }
209
+ if (event.key === 'Delete' || event.key === 'Backspace') {
210
+ event.preventDefault();
211
+ this.removeSelected();
212
+ return;
213
+ }
214
+ if (event.key === 'Escape') {
215
+ this.selectedId = null;
216
+ this.update('all');
217
+ return;
218
+ }
219
+ const step = (0, label_design_1.dotsToMm)(event.shiftKey ? 10 : 1, this.dpi);
220
+ const moves = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] };
221
+ const mv = moves[event.key];
222
+ if (mv) {
223
+ event.preventDefault();
224
+ this.remember();
225
+ el.x = (0, label_design_1.snapToDot)(el.x + mv[0], this.dpi);
226
+ el.y = (0, label_design_1.snapToDot)(el.y + mv[1], this.dpi);
227
+ this.d.elements = [...this.d.elements];
228
+ this.changed();
229
+ }
230
+ };
231
+ this.root = this.attachShadow({ mode: 'open' });
232
+ this.root.innerHTML = `<style>${styles_1.DESIGNER_CSS}</style>${this.skeleton()}`;
233
+ this.root.querySelectorAll('[data-ref]').forEach((el) => { this.refs[el.dataset.ref] = el; });
234
+ this.root.addEventListener('click', (e) => this.onClick(e));
235
+ this.root.addEventListener('change', (e) => this.onChange(e));
236
+ this.root.addEventListener('input', (e) => this.onInput(e));
237
+ this.root.addEventListener('pointerdown', (e) => this.onPointerDown(e));
238
+ this.root.addEventListener('dragstart', (e) => this.onDragStart(e));
239
+ this.refs.stage.addEventListener('dragover', (e) => { e.preventDefault(); if (e.dataTransfer)
240
+ e.dataTransfer.dropEffect = 'copy'; });
241
+ this.refs.stage.addEventListener('drop', (e) => this.onDrop(e));
242
+ }
243
+ connectedCallback() {
244
+ if (!this.listening) {
245
+ window.addEventListener('pointermove', this.onPointerMove);
246
+ window.addEventListener('pointerup', this.onPointerUp);
247
+ window.addEventListener('keydown', this.onKey);
248
+ this.listening = true;
249
+ }
250
+ (0, label_design_1.ensureLabelFonts)(this.fonts).then(() => this.update('stage'));
251
+ this.update('all');
252
+ }
253
+ disconnectedCallback() {
254
+ window.removeEventListener('pointermove', this.onPointerMove);
255
+ window.removeEventListener('pointerup', this.onPointerUp);
256
+ window.removeEventListener('keydown', this.onKey);
257
+ this.listening = false;
258
+ this.checkToken++;
259
+ }
260
+ get design() { return this.d; }
261
+ set design(v) {
262
+ this.d = v ? this.normalizeDesign(JSON.parse(JSON.stringify(v))) : null;
263
+ this.undoStack = [];
264
+ this.redoStack = [];
265
+ this.selectedId = null;
266
+ this.checkResults = [];
267
+ this.checkSelected = null;
268
+ this.savedState = this.saveState();
269
+ this.zoom = this.fitZoom();
270
+ this.update('all');
271
+ }
272
+ get name() { return this.label; }
273
+ set name(v) {
274
+ this.label = String(v || '');
275
+ this.refs.name.value = this.label;
276
+ this.savedState = this.saveState();
277
+ this.update('top');
278
+ }
279
+ get readOnly() { return this.ro; }
280
+ set readOnly(v) { this.ro = !!v; this.refs.name.readOnly = this.ro; this.update('all'); }
281
+ get fontsUrl() { return this.fonts; }
282
+ set fontsUrl(v) { this.fonts = v || 'assets/label-fonts/'; (0, label_design_1.ensureLabelFonts)(this.fonts).then(() => this.update('stage')); }
283
+ get itemSource() { return this.source; }
284
+ set itemSource(v) { this.source = v; this.update('top'); }
285
+ get storeName() { return this.store; }
286
+ set storeName(v) { this.store = String(v || ''); this.update('check'); }
287
+ get error() { return this.errorText; }
288
+ set error(v) { this.errorText = String(v || ''); this.update('top'); }
289
+ get dirty() { return !!this.d && this.saveState() !== this.savedState; }
290
+ getDesign() { return this.d ? JSON.parse(JSON.stringify(this.d)) : null; }
291
+ markSaved() {
292
+ this.savedState = this.saveState();
293
+ this.savedAt = new Date();
294
+ this.errorText = '';
295
+ this.update('top');
296
+ }
297
+ requestSave() {
298
+ if (!this.d || this.ro)
299
+ return;
300
+ const name = this.label.trim();
301
+ if (!name) {
302
+ this.errorText = 'Give the template a name.';
303
+ this.update('top');
304
+ return;
305
+ }
306
+ this.errorText = '';
307
+ this.update('top');
308
+ this.emit('designsave', { design: this.getDesign(), name });
309
+ }
310
+ undo() {
311
+ const prev = this.undoStack.pop();
312
+ if (!prev)
313
+ return;
314
+ this.redoStack.push(this.snapshot());
315
+ this.restore(prev);
316
+ }
317
+ redo() {
318
+ const next = this.redoStack.pop();
319
+ if (!next)
320
+ return;
321
+ this.undoStack.push(this.snapshot());
322
+ this.restore(next);
323
+ }
324
+ useSample(item) {
325
+ this.data = (0, label_design_1.labelDataFromItem)(item, { limits: this.d ? this.d.limits : undefined, storeName: this.store });
326
+ this.sampleName = item.posdecs || item.item_name || 'Item';
327
+ this.samplePickerOpen = false;
328
+ this.emit('samplepick', { item });
329
+ this.update('all');
330
+ }
331
+ skeleton() {
332
+ return `<div class="ld">
333
+ <div class="ld-top">
334
+ <input class="ld-name" type="text" data-ref="name" data-input="name" maxlength="100" aria-label="Template name">
335
+ <span class="lt-chip" data-ref="size"></span>
336
+ <span class="lt-chip" data-ref="printer"></span>
337
+ <span class="ld-saved" data-ref="saved"></span>
338
+ <span class="lt-error" data-ref="error"></span>
339
+ <span class="lt-sp"></span>
340
+ <div class="lt-seg" data-ref="modes"></div>
341
+ <span class="lt-sp"></span>
342
+ <span class="ld-actions" data-ref="tools"></span>
343
+ <div class="ld-sample">
344
+ <button type="button" class="lt-btn lt-btn-sm" data-act="picker" data-ref="sampleBtn" title="Item shown on the label"></button>
345
+ <div class="ld-picker" data-ref="picker" hidden>
346
+ <input class="lt-input" type="text" placeholder="Search items or scan a UPC" data-input="sampleSearch">
347
+ <div data-ref="pickList"></div>
348
+ </div>
349
+ </div>
350
+ <span class="ld-actions"><slot name="actions"></slot></span>
351
+ </div>
352
+ <div class="ld-body" data-ref="body">
353
+ <aside class="ld-left">
354
+ <label class="ld-search">${icon('search')}<input class="lt-input" type="text" placeholder="Search fields…" aria-label="Search fields" data-input="palette"></label>
355
+ <div class="ld-group">
356
+ <div class="ld-group-head"><span class="lt-eyebrow">Item fields</span><span class="ld-count" data-ref="fieldCount"></span></div>
357
+ <div class="ld-tiles" data-ref="fields"></div>
358
+ </div>
359
+ <div class="ld-group">
360
+ <div class="ld-group-head"><span class="lt-eyebrow">Basic</span></div>
361
+ <div class="ld-tiles ld-tiles-basic" data-ref="basic"></div>
362
+ </div>
363
+ <p class="ld-tip">${icon('info')}<span>Drag a field onto the label, or click it to add it at the top left.</span></p>
364
+ <div class="ld-group">
365
+ <div class="ld-group-head"><span class="lt-eyebrow">Layers</span><span class="ld-count" data-ref="layerCount"></span></div>
366
+ <div class="ld-layers" data-ref="layers"></div>
367
+ </div>
368
+ </aside>
369
+ <section class="ld-center" data-ref="center">
370
+ <div class="ld-board">
371
+ <div class="ld-ruler-x" data-ref="rulerX"></div>
372
+ <div class="ld-ruler-y" data-ref="rulerY"></div>
373
+ <div class="ld-stage" data-ref="stage">
374
+ <canvas class="lt-canvas" data-ref="canvas"></canvas>
375
+ <div class="ld-grid" data-ref="grid"></div>
376
+ <div data-ref="elements"></div>
377
+ <div data-ref="guides"></div>
378
+ </div>
379
+ <div class="ld-stocktag" data-ref="stocktag" hidden>Pre-printed color is a guide and is not printed</div>
380
+ </div>
381
+ <div class="ld-status" data-ref="status"></div>
382
+ </section>
383
+ <aside class="ld-right" data-ref="props"></aside>
384
+ </div>
385
+ <div class="ld-check" data-ref="check" hidden></div>
386
+ <div class="ld-empty" data-ref="empty">No label to design yet.</div>
387
+ </div>`;
388
+ }
389
+ emit(type, detail) {
390
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
391
+ }
392
+ update(part) {
393
+ this.pending.add(part);
394
+ if (this.frame)
395
+ return;
396
+ this.frame = requestAnimationFrame(() => {
397
+ this.frame = 0;
398
+ const parts = this.pending;
399
+ this.pending = new Set();
400
+ const all = parts.has('all');
401
+ if (all || parts.has('top'))
402
+ this.renderTop();
403
+ if (all || parts.has('palette'))
404
+ this.renderPalette();
405
+ if (all || parts.has('layers'))
406
+ this.renderLayers();
407
+ if (all || parts.has('stage'))
408
+ this.renderStage();
409
+ if (all || parts.has('props'))
410
+ this.renderProps();
411
+ if (all || parts.has('status'))
412
+ this.renderStatus();
413
+ if (all || parts.has('check'))
414
+ this.renderCheck();
415
+ });
416
+ }
417
+ changed() {
418
+ this.emit('designchange', { design: this.getDesign(), name: this.label, dirty: this.dirty });
419
+ this.update('top');
420
+ this.update('layers');
421
+ this.update('stage');
422
+ this.update('props');
423
+ this.update('status');
424
+ }
425
+ normalizeDesign(design) {
426
+ design.elements = (design.elements || []).map((e) => ({ ...e, id: e.id || (0, label_design_1.newId)() }));
427
+ return design;
428
+ }
429
+ get pxPerMm() { return CSS_PX_PER_MM * this.zoom; }
430
+ get selected() { return this.d && this.selectedId ? this.d.elements.find((e) => e.id === this.selectedId) || null : null; }
431
+ get dpi() { return this.d ? this.d.dpi : 203; }
432
+ get gridMm() { return this.unit === 'in' ? label_design_1.MM_PER_INCH / 16 : 1; }
433
+ get canWrite() { return !this.ro && !!this.d; }
434
+ fitZoom() {
435
+ if (!this.d)
436
+ return 1;
437
+ const width = this.getBoundingClientRect().width || window.innerWidth;
438
+ const avail = Math.max(320, Math.min(width - 640, 900));
439
+ const z = Math.min(avail / (this.d.widthMm * CSS_PX_PER_MM), 260 / (this.d.heightMm * CSS_PX_PER_MM));
440
+ return ZOOMS.reduce((best, v) => (v <= z && v > best ? v : best), ZOOMS[0]);
441
+ }
442
+ fmt(mm) { return this.unit === 'in' ? (mm / label_design_1.MM_PER_INCH).toFixed(2) : mm.toFixed(1); }
443
+ parseLen(value) {
444
+ if (value === null || String(value).trim() === '')
445
+ return null;
446
+ const n = Number(value);
447
+ if (!Number.isFinite(n))
448
+ return null;
449
+ return this.unit === 'in' ? n * label_design_1.MM_PER_INCH : n;
450
+ }
451
+ fieldLabel(key) {
452
+ const f = label_design_1.FIELDS.find((x) => x.key === key);
453
+ return f ? f.label : key || '';
454
+ }
455
+ boundField(el) {
456
+ const m = String(el.value || '').match(/^\{\{(\w+)\}\}$/);
457
+ return m ? m[1] : '';
458
+ }
459
+ isNative(el) { return !!el && el.font === 'printer' && !!el.native; }
460
+ elementTitle(el) {
461
+ const f = this.boundField(el);
462
+ if (el.type === 'barcode')
463
+ return 'Barcode';
464
+ if (el.type === 'price')
465
+ return this.fieldLabel(f) || 'Price';
466
+ if (el.type === 'text')
467
+ return f ? this.fieldLabel(f) : 'Text';
468
+ return { qr: 'QR code', box: 'Box', fill: 'Filled box', line: 'Line', image: 'Image' }[el.type] || el.type;
469
+ }
470
+ snapshot() { return JSON.stringify({ design: this.d }); }
471
+ saveState() { return JSON.stringify({ name: this.label.trim(), design: this.d }); }
472
+ remember() {
473
+ this.undoStack.push(this.snapshot());
474
+ if (this.undoStack.length > 100)
475
+ this.undoStack.shift();
476
+ this.redoStack = [];
477
+ }
478
+ restore(state) {
479
+ this.d = JSON.parse(state).design;
480
+ if (this.selectedId && !this.selected)
481
+ this.selectedId = null;
482
+ this.changed();
483
+ }
484
+ snapMm(mm) {
485
+ const step = this.snap ? this.gridMm : 0;
486
+ return (0, label_design_1.snapToDot)(step ? Math.round(mm / step) * step : mm, this.dpi);
487
+ }
488
+ defaultElement(kind, field) {
489
+ const d = this.d;
490
+ const base = { id: (0, label_design_1.newId)(), type: kind, x: 2, y: 2, w: Math.min(30, d.widthMm - 4), h: 5 };
491
+ if (kind === 'text')
492
+ return { ...base, value: field ? `{{${field}}}` : 'Text', font: 'sans', size: 10, weight: field === 'itemName' ? 700 : 400, align: 'left', fit: 'cut', w: Math.min(40, d.widthMm - 4), h: 5 };
493
+ if (kind === 'price')
494
+ return { ...base, value: `{{${field || 'price'}}}`, font: 'sans', size: 28, weight: 700, align: 'center', format: 'super', w: Math.min(35, d.widthMm - 4), h: 12 };
495
+ if (kind === 'barcode')
496
+ return { ...base, value: '{{barcode}}', symbology: 'UPC_A', module: 2, hri: true, w: Math.min((0, label_design_1.dotsToMm)(115 * 2, this.dpi), d.widthMm - 2), h: 9 };
497
+ if (kind === 'qr')
498
+ return { ...base, value: 'https://', w: 12, h: 12 };
499
+ if (kind === 'line')
500
+ return { ...base, w: Math.min(30, d.widthMm - 4), h: (0, label_design_1.dotsToMm)(3, this.dpi) };
501
+ if (kind === 'box')
502
+ return { ...base, w: 20, h: 10, stroke: 0.3 };
503
+ if (kind === 'image')
504
+ return { ...base, w: 15, h: 15 };
505
+ return { ...base, w: 20, h: 6 };
506
+ }
507
+ kindFor(field) {
508
+ if (field === 'barcode')
509
+ return 'barcode';
510
+ if (field === 'price' || field === 'cashPrice' || field === 'creditPrice')
511
+ return 'price';
512
+ return 'text';
513
+ }
514
+ addElement(kind, field, at) {
515
+ if (!this.d || !this.canWrite)
516
+ return;
517
+ this.remember();
518
+ const el = this.defaultElement(kind, field);
519
+ if (at) {
520
+ el.x = this.snapMm(Math.max(0, Math.min(at.x, this.d.widthMm - el.w)));
521
+ el.y = this.snapMm(Math.max(0, Math.min(at.y, this.d.heightMm - el.h)));
522
+ }
523
+ else {
524
+ const n = this.d.elements.length % 6;
525
+ el.x = this.snapMm(2 + n * 2);
526
+ el.y = this.snapMm(2 + n * 2);
527
+ }
528
+ this.d.elements = [...this.d.elements, el];
529
+ this.selectedId = el.id;
530
+ this.changed();
531
+ }
532
+ change(mutator, keepNative = true) {
533
+ const el = this.selected;
534
+ if (!el || !this.d || !this.canWrite)
535
+ return;
536
+ this.remember();
537
+ mutator(el);
538
+ if (!keepNative && el.native) {
539
+ delete el.native;
540
+ delete el.nativeFit;
541
+ if (el.font === 'printer')
542
+ el.font = 'sans';
543
+ }
544
+ this.d.elements = [...this.d.elements];
545
+ this.changed();
546
+ }
547
+ setStyle(key, value) {
548
+ const keepNative = key === 'when' || key === 'value';
549
+ this.change((el) => { el[key] = value; }, keepNative);
550
+ }
551
+ setPos(key, value) {
552
+ const mm = this.parseLen(value);
553
+ if (mm === null) {
554
+ this.update('props');
555
+ return;
556
+ }
557
+ this.change((el) => {
558
+ el[key] = Math.max(key === 'w' || key === 'h' ? (0, label_design_1.dotsToMm)(2, this.dpi) : -1000, (0, label_design_1.snapToDot)(mm, this.dpi));
559
+ if (el.type === 'qr' && (key === 'w' || key === 'h'))
560
+ el.w = el.h = el[key];
561
+ }, key === 'x' || key === 'y');
562
+ }
563
+ setAtLeast(key, value) {
564
+ const n = Number(value);
565
+ if (value === null || String(value).trim() === '' || !Number.isFinite(n)) {
566
+ this.update('props');
567
+ return;
568
+ }
569
+ this.setStyle(key, Math.max(1, key === 'lines' ? Math.round(n) : n));
570
+ }
571
+ setDesign(key, value) {
572
+ if (!this.d || !this.canWrite)
573
+ return;
574
+ const mm = this.parseLen(value);
575
+ if (mm === null || mm < (key === 'gapMm' ? 0 : 5) || mm > (key === 'gapMm' ? 20 : 300)) {
576
+ this.update('props');
577
+ return;
578
+ }
579
+ this.remember();
580
+ this.d[key] = Math.round(mm * 100) / 100;
581
+ this.changed();
582
+ }
583
+ duplicateSelected() {
584
+ const el = this.selected;
585
+ if (!el || !this.d || !this.canWrite)
586
+ return;
587
+ this.remember();
588
+ const copy = { ...JSON.parse(JSON.stringify(el)), id: (0, label_design_1.newId)(), x: (0, label_design_1.snapToDot)(el.x + 2, this.dpi), y: (0, label_design_1.snapToDot)(el.y + 2, this.dpi) };
589
+ this.d.elements = [...this.d.elements, copy];
590
+ this.selectedId = copy.id;
591
+ this.changed();
592
+ }
593
+ removeSelected() {
594
+ if (!this.d || !this.selectedId || !this.canWrite)
595
+ return;
596
+ this.remember();
597
+ this.d.elements = this.d.elements.filter((e) => e.id !== this.selectedId);
598
+ this.selectedId = null;
599
+ this.changed();
600
+ }
601
+ moveLayer(step) {
602
+ const d = this.d;
603
+ if (!d || !this.selectedId || !this.canWrite)
604
+ return;
605
+ const i = d.elements.findIndex((e) => e.id === this.selectedId);
606
+ const j = i + step;
607
+ if (i < 0 || j < 0 || j >= d.elements.length)
608
+ return;
609
+ this.remember();
610
+ const list = [...d.elements];
611
+ [list[i], list[j]] = [list[j], list[i]];
612
+ d.elements = list;
613
+ this.changed();
614
+ }
615
+ onClick(event) {
616
+ const target = event.target.closest('[data-act]');
617
+ if (!target || target.disabled)
618
+ return;
619
+ const act = target.dataset.act;
620
+ const v = target.dataset.v;
621
+ const el = this.selected;
622
+ switch (act) {
623
+ case 'mode':
624
+ this.setMode(v);
625
+ break;
626
+ case 'undo':
627
+ this.undo();
628
+ break;
629
+ case 'redo':
630
+ this.redo();
631
+ break;
632
+ case 'zoom':
633
+ this.zoomBy(Number(v));
634
+ break;
635
+ case 'grid':
636
+ this.showGrid = !this.showGrid;
637
+ this.update('stage');
638
+ this.update('top');
639
+ break;
640
+ case 'snap':
641
+ this.snap = !this.snap;
642
+ this.update('top');
643
+ this.update('status');
644
+ break;
645
+ case 'unit':
646
+ this.unit = v;
647
+ this.update('all');
648
+ break;
649
+ case 'picker':
650
+ this.togglePicker();
651
+ break;
652
+ case 'sample':
653
+ this.useSample(this.sampleResults[Number(v)]);
654
+ break;
655
+ case 'add':
656
+ this.addElement(target.dataset.kind, target.dataset.field || undefined);
657
+ break;
658
+ case 'layer':
659
+ this.selectedId = v;
660
+ this.update('layers');
661
+ this.update('stage');
662
+ this.update('props');
663
+ this.update('status');
664
+ break;
665
+ case 'forward':
666
+ this.moveLayer(1);
667
+ break;
668
+ case 'backward':
669
+ this.moveLayer(-1);
670
+ break;
671
+ case 'duplicate':
672
+ this.duplicateSelected();
673
+ break;
674
+ case 'remove':
675
+ this.removeSelected();
676
+ break;
677
+ case 'style':
678
+ this.setStyle(target.dataset.key, v);
679
+ break;
680
+ case 'toggle':
681
+ if (el)
682
+ this.setStyle(v, v === 'hri' ? el.hri === false : !el[v]);
683
+ break;
684
+ case 'bold':
685
+ if (el)
686
+ this.setStyle('weight', (el.weight || 400) >= 700 ? 400 : 700);
687
+ break;
688
+ case 'imageMode':
689
+ this.imageMode = v;
690
+ this.update('props');
691
+ break;
692
+ case 'printer':
693
+ if (this.d && this.canWrite && this.d.printer !== v) {
694
+ this.remember();
695
+ this.d = { ...this.d, printer: v };
696
+ this.changed();
697
+ }
698
+ break;
699
+ case 'dpi':
700
+ if (this.d && this.canWrite && this.d.dpi !== Number(v)) {
701
+ this.remember();
702
+ this.d = { ...this.d, dpi: Number(v) };
703
+ this.changed();
704
+ }
705
+ break;
706
+ case 'checkFilter':
707
+ this.checkFilter = v;
708
+ this.update('check');
709
+ break;
710
+ case 'checkPick':
711
+ this.checkSelected = this.filteredProblems()[Number(v)] || null;
712
+ this.update('check');
713
+ break;
714
+ case 'checkRun':
715
+ this.runCheck();
716
+ break;
717
+ case 'letWrap':
718
+ this.letWrap(v);
719
+ break;
720
+ }
721
+ }
722
+ onChange(event) {
723
+ const target = event.target;
724
+ const key = target.dataset && target.dataset.change;
725
+ if (!key)
726
+ return;
727
+ const value = target.type === 'checkbox' ? target.checked : target.value;
728
+ switch (key) {
729
+ case 'pos':
730
+ this.setPos(target.dataset.key, value);
731
+ break;
732
+ case 'field':
733
+ this.change((el) => { el.value = value ? `{{${value}}}` : (el.type === 'text' ? 'Text' : el.value); });
734
+ break;
735
+ case 'text':
736
+ this.change((el) => { el.value = String(value); });
737
+ break;
738
+ case 'symbology':
739
+ this.setStyle('symbology', value);
740
+ break;
741
+ case 'module':
742
+ this.setStyle('module', Number(value));
743
+ break;
744
+ case 'hri': {
745
+ const el = this.selected;
746
+ if (el)
747
+ this.setStyle('hri', el.hri === false);
748
+ break;
749
+ }
750
+ case 'font':
751
+ this.setStyle('font', value);
752
+ break;
753
+ case 'size':
754
+ this.setAtLeast('size', value);
755
+ break;
756
+ case 'lines':
757
+ this.setAtLeast('lines', value);
758
+ break;
759
+ case 'fit':
760
+ this.setStyle('fit', value);
761
+ break;
762
+ case 'stroke': {
763
+ const mm = this.parseLen(value);
764
+ if (mm === null)
765
+ this.update('props');
766
+ else
767
+ this.setStyle('stroke', Math.max(0.1, mm));
768
+ break;
769
+ }
770
+ case 'when':
771
+ this.setStyle('when', value || undefined);
772
+ break;
773
+ case 'image':
774
+ this.onImageFile(target);
775
+ break;
776
+ case 'design':
777
+ this.setDesign(target.dataset.key, value);
778
+ break;
779
+ case 'stock':
780
+ this.toggleStock();
781
+ break;
782
+ case 'stockColor':
783
+ this.setStock('color', value);
784
+ break;
785
+ case 'stockFrom':
786
+ this.setStock('from', value);
787
+ break;
788
+ }
789
+ }
790
+ onInput(event) {
791
+ const target = event.target;
792
+ const key = target.dataset && target.dataset.input;
793
+ if (key === 'name') {
794
+ this.label = target.value;
795
+ if (this.label.trim())
796
+ this.errorText = '';
797
+ this.emit('designchange', { design: this.getDesign(), name: this.label, dirty: this.dirty });
798
+ this.update('top');
799
+ }
800
+ else if (key === 'palette') {
801
+ this.paletteFilter = target.value;
802
+ this.update('palette');
803
+ }
804
+ else if (key === 'sampleSearch') {
805
+ clearTimeout(this.sampleTimer);
806
+ const q = target.value;
807
+ this.sampleTimer = setTimeout(() => this.findSamples(q), 300);
808
+ }
809
+ }
810
+ onDragStart(event) {
811
+ const tile = event.target.closest('[data-act="add"]');
812
+ if (!tile || !event.dataTransfer)
813
+ return;
814
+ event.dataTransfer.setData('text/plain', JSON.stringify({ kind: tile.dataset.kind, field: tile.dataset.field || null }));
815
+ event.dataTransfer.effectAllowed = 'copy';
816
+ }
817
+ onDrop(event) {
818
+ event.preventDefault();
819
+ let spec = null;
820
+ try {
821
+ spec = JSON.parse(event.dataTransfer ? event.dataTransfer.getData('text/plain') : '');
822
+ }
823
+ catch (e) {
824
+ spec = null;
825
+ }
826
+ if (!spec || !spec.kind)
827
+ return;
828
+ const rect = this.refs.stage.getBoundingClientRect();
829
+ this.addElement(spec.kind, spec.field || undefined, { x: (event.clientX - rect.left) / this.pxPerMm, y: (event.clientY - rect.top) / this.pxPerMm });
830
+ }
831
+ onPointerDown(event) {
832
+ const target = event.target;
833
+ const handle = target.closest('[data-handle]');
834
+ const box = target.closest('[data-el]');
835
+ if (!target.closest('.ld-center'))
836
+ return;
837
+ if (!box) {
838
+ if (this.selectedId) {
839
+ this.selectedId = null;
840
+ this.update('layers');
841
+ this.update('stage');
842
+ this.update('props');
843
+ this.update('status');
844
+ }
845
+ return;
846
+ }
847
+ event.stopPropagation();
848
+ const el = this.d && this.d.elements.find((e) => e.id === box.dataset.el);
849
+ if (!el)
850
+ return;
851
+ if (this.selectedId !== el.id) {
852
+ this.selectedId = el.id;
853
+ this.update('layers');
854
+ this.update('stage');
855
+ this.update('props');
856
+ this.update('status');
857
+ }
858
+ if (!this.canWrite || event.button !== 0)
859
+ return;
860
+ this.drag = { kind: handle ? 'resize' : 'move', id: el.id, handle: handle ? handle.dataset.handle : undefined, startX: event.clientX, startY: event.clientY, orig: { ...el }, moved: false };
861
+ }
862
+ withGuides(el, x, y) {
863
+ const d = this.d;
864
+ const tol = 0.8;
865
+ const guides = [];
866
+ const xs = [d.widthMm / 2];
867
+ const ys = [d.heightMm / 2];
868
+ for (const o of d.elements) {
869
+ if (o.id === el.id)
870
+ continue;
871
+ xs.push(o.x, o.x + o.w, o.x + o.w / 2);
872
+ ys.push(o.y, o.y + o.h, o.y + o.h / 2);
873
+ }
874
+ const tryAxis = (start, size, lines, axis) => {
875
+ for (const line of lines) {
876
+ for (const [offset, edge] of [[0, start], [size / 2, start + size / 2], [size, start + size]]) {
877
+ if (Math.abs(edge - line) <= tol) {
878
+ guides.push({ axis, mm: line });
879
+ return (0, label_design_1.snapToDot)(line - offset, this.dpi);
880
+ }
881
+ }
882
+ }
883
+ return start;
884
+ };
885
+ const nx = tryAxis(this.snapMm(x), el.w, xs, 'x');
886
+ const ny = tryAxis(this.snapMm(y), el.h, ys, 'y');
887
+ this.guides = guides;
888
+ return { x: nx, y: ny };
889
+ }
890
+ zoomBy(step) {
891
+ const i = ZOOMS.indexOf(this.zoom);
892
+ this.zoom = ZOOMS[Math.max(0, Math.min(ZOOMS.length - 1, (i < 0 ? 2 : i) + step))];
893
+ this.update('top');
894
+ this.update('stage');
895
+ }
896
+ setMode(mode) {
897
+ this.mode = mode;
898
+ if (mode === 'check' && !this.checkRunning && !this.checkResults.length)
899
+ this.runCheck();
900
+ this.update('all');
901
+ }
902
+ togglePicker() {
903
+ this.samplePickerOpen = !this.samplePickerOpen;
904
+ if (this.samplePickerOpen && !this.sampleResults.length)
905
+ this.findSamples('');
906
+ this.update('top');
907
+ }
908
+ findSamples(query) {
909
+ if (!this.source || !this.source.search) {
910
+ this.update('top');
911
+ return;
912
+ }
913
+ this.sampleLoading = true;
914
+ this.update('top');
915
+ this.source.search(query).then((items) => {
916
+ this.sampleLoading = false;
917
+ this.sampleResults = items || [];
918
+ this.update('top');
919
+ }, () => { this.sampleLoading = false; this.sampleResults = []; this.update('top'); });
920
+ }
921
+ onImageFile(input) {
922
+ const file = input.files && input.files[0];
923
+ input.value = '';
924
+ const el = this.selected;
925
+ if (!file || !el || el.type !== 'image')
926
+ return;
927
+ if (!/^image\/(png|jpe?g|gif|webp|bmp|svg\+xml)$/.test(file.type)) {
928
+ this.imageError = 'Use a PNG, JPG, GIF, WebP, BMP or SVG image.';
929
+ this.update('props');
930
+ return;
931
+ }
932
+ if (file.size > 5 * 1024 * 1024) {
933
+ this.imageError = 'The image is larger than 5 MB.';
934
+ this.update('props');
935
+ return;
936
+ }
937
+ this.imageError = '';
938
+ const url = URL.createObjectURL(file);
939
+ const img = new Image();
940
+ img.onload = () => {
941
+ const w = Math.max(8, (0, label_design_1.mmToDots)(el.w, this.dpi));
942
+ const h = Math.max(8, (0, label_design_1.mmToDots)(el.h, this.dpi));
943
+ URL.revokeObjectURL(url);
944
+ if (w * h > 900 * 900) {
945
+ this.imageError = 'Make the image box smaller (it is too many printer dots).';
946
+ this.update('props');
947
+ return;
948
+ }
949
+ const bits = (0, label_design_1.imageFromSource)(img, img.naturalWidth || img.width, img.naturalHeight || img.height, w, h, this.imageMode);
950
+ const target = this.d && this.d.elements.find((e) => e.id === el.id);
951
+ if (!target || !this.d)
952
+ return;
953
+ this.remember();
954
+ target.image = bits;
955
+ this.d.elements = [...this.d.elements];
956
+ this.changed();
957
+ };
958
+ img.onerror = () => { this.imageError = 'That image could not be read.'; URL.revokeObjectURL(url); this.update('props'); };
959
+ img.src = url;
960
+ }
961
+ toggleStock() {
962
+ if (!this.d || !this.canWrite)
963
+ return;
964
+ this.remember();
965
+ const d = this.d;
966
+ d.stock = d.stock ? null : { color: '#f3e436', x: d.widthMm * 0.52, y: 0, w: d.widthMm * 0.48, h: d.heightMm };
967
+ this.changed();
968
+ }
969
+ setStock(key, value) {
970
+ const d = this.d;
971
+ if (!d || !d.stock || !this.canWrite)
972
+ return;
973
+ this.remember();
974
+ if (key === 'color')
975
+ d.stock.color = String(value);
976
+ else {
977
+ const pct = Math.max(0, Math.min(100, Number(value))) / 100;
978
+ d.stock.x = d.widthMm * pct;
979
+ d.stock.w = d.widthMm - d.stock.x;
980
+ }
981
+ this.changed();
982
+ }
983
+ runCheck() {
984
+ const source = this.source;
985
+ if (!this.d || !source || !source.page) {
986
+ this.checkError = 'Pick a store to check against its items.';
987
+ this.update('check');
988
+ return;
989
+ }
990
+ const token = ++this.checkToken;
991
+ const design = JSON.parse(JSON.stringify(this.d));
992
+ this.checkRunning = true;
993
+ this.checkError = '';
994
+ this.checkResults = [];
995
+ this.checkDone = 0;
996
+ this.checkTotal = 0;
997
+ this.checkSelected = null;
998
+ this.update('check');
999
+ const canvas = document.createElement('canvas');
1000
+ const page = (offset) => {
1001
+ source.page(offset, CHECK_PAGE).then((res) => {
1002
+ if (token !== this.checkToken)
1003
+ return;
1004
+ if (!res) {
1005
+ this.checkRunning = false;
1006
+ this.checkError = 'Could not load the store items.';
1007
+ this.update('check');
1008
+ return;
1009
+ }
1010
+ if (res.storeName)
1011
+ this.store = res.storeName;
1012
+ this.checkTotal = Math.min(res.total, CHECK_MAX_ITEMS);
1013
+ for (const item of res.items) {
1014
+ const data = (0, label_design_1.labelDataFromItem)(item, { limits: design.limits, storeName: this.store });
1015
+ this.checkResults.push({ item, data, warnings: (0, label_design_1.drawLabel)(canvas, design, data, { ppm: 3 }) });
1016
+ }
1017
+ this.checkDone = this.checkResults.length;
1018
+ const next = offset + res.items.length;
1019
+ if (res.items.length && next < this.checkTotal) {
1020
+ this.update('check');
1021
+ setTimeout(() => page(next), 0);
1022
+ }
1023
+ else {
1024
+ this.checkRunning = false;
1025
+ this.checkSelected = this.problems()[0] || this.checkResults[0] || null;
1026
+ this.update('check');
1027
+ }
1028
+ }, () => {
1029
+ if (token !== this.checkToken)
1030
+ return;
1031
+ this.checkRunning = false;
1032
+ this.checkError = 'Could not load the store items.';
1033
+ this.update('check');
1034
+ });
1035
+ };
1036
+ (0, label_design_1.ensureLabelFonts)(this.fonts).then(() => page(0));
1037
+ }
1038
+ problems() { return this.checkResults.filter((r) => r.warnings.length); }
1039
+ filteredProblems() {
1040
+ const f = this.checkFilter;
1041
+ return this.problems().filter((r) => f === 'all' || r.warnings.some((w) => w.kind === f)).slice(0, 120);
1042
+ }
1043
+ letWrap(id) {
1044
+ if (!this.d || !this.canWrite)
1045
+ return;
1046
+ this.remember();
1047
+ const target = this.d.elements.find((e) => e.id === id);
1048
+ if (target) {
1049
+ target.fit = 'wrap';
1050
+ target.lines = 2;
1051
+ if (target.native) {
1052
+ delete target.native;
1053
+ delete target.nativeFit;
1054
+ if (target.font === 'printer')
1055
+ target.font = 'sans';
1056
+ }
1057
+ }
1058
+ this.d = { ...this.d, elements: [...this.d.elements] };
1059
+ this.changed();
1060
+ this.runCheck();
1061
+ }
1062
+ itemTitle(item) { return item.posdecs || item.item_name || `Item ${item.item_id}`; }
1063
+ printerDots() { return this.d ? `${(0, label_design_1.mmToDots)(this.d.widthMm, this.dpi)} × ${(0, label_design_1.mmToDots)(this.d.heightMm, this.dpi)} dots` : ''; }
1064
+ renderTop() {
1065
+ const d = this.d;
1066
+ const r = this.refs;
1067
+ r.empty.hidden = !!d;
1068
+ r.body.hidden = !d || this.mode !== 'design';
1069
+ r.check.hidden = !d || this.mode !== 'check';
1070
+ if (!d)
1071
+ return;
1072
+ const name = r.name;
1073
+ if (name.value !== this.label && this.root.activeElement !== name)
1074
+ name.value = this.label;
1075
+ r.size.textContent = this.unit === 'in'
1076
+ ? `${+(d.widthMm / label_design_1.MM_PER_INCH).toFixed(2)} × ${+(d.heightMm / label_design_1.MM_PER_INCH).toFixed(2)} in`
1077
+ : `${+d.widthMm.toFixed(1)} × ${+d.heightMm.toFixed(1)} mm`;
1078
+ r.printer.textContent = `${d.printer === 'zpl' ? 'Zebra' : 'Godex'} · ${this.dpi} dpi`;
1079
+ const dirty = this.dirty;
1080
+ r.saved.className = `ld-saved${dirty ? ' ld-unsaved' : ''}`;
1081
+ r.saved.textContent = dirty ? 'Unsaved changes' : this.savedAt ? `Saved ${this.savedAt.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}` : '';
1082
+ r.error.textContent = this.errorText;
1083
+ r.modes.innerHTML = `<button type="button" class="${on(this.mode === 'design').trim()}" data-act="mode" data-v="design">Design</button><button type="button" class="${on(this.mode === 'check').trim()}" data-act="mode" data-v="check">Check items</button>`;
1084
+ r.tools.innerHTML = this.mode !== 'design' ? '' : `
1085
+ <button type="button" class="lt-icon-btn" data-act="undo" title="Undo (Ctrl+Z)" ${this.undoStack.length ? '' : 'disabled'}>↶</button>
1086
+ <button type="button" class="lt-icon-btn" data-act="redo" title="Redo (Ctrl+Shift+Z)" ${this.redoStack.length ? '' : 'disabled'}>↷</button>
1087
+ <span class="ld-div"></span>
1088
+ <button type="button" class="lt-icon-btn" data-act="zoom" data-v="-1" title="Zoom out">−</button>
1089
+ <span class="ld-zoom">${Math.round(this.zoom * 100)}%</span>
1090
+ <button type="button" class="lt-icon-btn" data-act="zoom" data-v="1" title="Zoom in">+</button>
1091
+ <span class="ld-div"></span>
1092
+ <button type="button" class="lt-icon-btn${on(this.showGrid)}" data-act="grid" title="Grid">#</button>
1093
+ <button type="button" class="lt-icon-btn${on(this.snap)}" data-act="snap" title="Snap to grid">⌖</button>
1094
+ <div class="lt-seg lt-seg-sm"><button type="button" class="${on(this.unit === 'in').trim()}" data-act="unit" data-v="in">in</button><button type="button" class="${on(this.unit === 'mm').trim()}" data-act="unit" data-v="mm">mm</button></div>`;
1095
+ r.sampleBtn.textContent = `Sample: ${this.sampleName} ▾`;
1096
+ r.picker.hidden = !this.samplePickerOpen;
1097
+ if (this.samplePickerOpen) {
1098
+ const hasSource = !!(this.source && this.source.search);
1099
+ r.pickList.innerHTML = !hasSource ? '<div class="ld-pick-empty">Pick a store to use its items.</div>'
1100
+ : this.sampleLoading ? '<div class="ld-pick-empty">Searching…</div>'
1101
+ : this.sampleResults.map((it, i) => `<button type="button" class="ld-pick" data-act="sample" data-v="${i}"><span>${esc(this.itemTitle(it))}</span><small>${esc(formatMoney(it.price))}</small></button>`).join('')
1102
+ || '<div class="ld-pick-empty">No items found.</div>';
1103
+ }
1104
+ }
1105
+ renderPalette() {
1106
+ const q = this.paletteFilter.trim().toLowerCase();
1107
+ const fields = label_design_1.FIELDS.filter((f) => !q || f.label.toLowerCase().includes(q));
1108
+ const dis = this.canWrite ? '' : ' disabled';
1109
+ this.refs.fields.innerHTML = fields.length
1110
+ ? fields.map((f) => `<button type="button" class="ld-tile" draggable="true" data-act="add" data-kind="${this.kindFor(f.key)}" data-field="${esc(f.key)}"${dis}><span class="ld-tile-ico">${icon(f.icon)}</span><span class="ld-tile-label">${esc(f.label)}</span></button>`).join('')
1111
+ : '<span class="lt-note ld-none">No fields match.</span>';
1112
+ this.refs.fieldCount.textContent = String(fields.length);
1113
+ this.refs.basic.innerHTML = BASIC_PALETTE.map((b) => `<button type="button" class="ld-tile" draggable="true" data-act="add" data-kind="${b.kind}"${dis}><span class="ld-tile-ico">${icon(b.icon)}</span><span class="ld-tile-label">${esc(b.label)}</span></button>`).join('');
1114
+ }
1115
+ layerIcon(el) {
1116
+ const f = this.boundField(el);
1117
+ const field = el.type === 'text' && f ? label_design_1.FIELDS.find((x) => x.key === f) : undefined;
1118
+ return field ? field.icon : ELEMENT_ICONS[el.type] || 'square';
1119
+ }
1120
+ renderLayers() {
1121
+ const d = this.d;
1122
+ if (!d)
1123
+ return;
1124
+ this.refs.layerCount.textContent = String(d.elements.length);
1125
+ this.refs.layers.innerHTML = d.elements.length
1126
+ ? d.elements.slice().reverse().map((el) => `<button type="button" class="ld-layer${on(el.id === this.selectedId)}" data-act="layer" data-v="${esc(el.id)}">${icon(this.layerIcon(el))}<span class="ld-layer-name">${esc(this.elementTitle(el))}</span>${this.isNative(el) ? '<small>printer font</small>' : ''}</button>`).join('')
1127
+ : '<span class="lt-note">Nothing on the label yet.</span>';
1128
+ }
1129
+ ticks(axis) {
1130
+ const d = this.d;
1131
+ const lenMm = axis === 'x' ? d.widthMm : d.heightMm;
1132
+ const ppm = this.pxPerMm;
1133
+ const out = [];
1134
+ const tick = (px, len, label) => {
1135
+ const pos = axis === 'x' ? `left:${px}px;height:${len}px` : `top:${px}px;width:${len}px`;
1136
+ out.push(`<i style="${pos}">${label !== null ? `<b>${label}</b>` : ''}</i>`);
1137
+ };
1138
+ if (this.unit === 'in') {
1139
+ const steps = Math.floor((lenMm / label_design_1.MM_PER_INCH) * 8 + 1e-6);
1140
+ for (let i = 0; i <= steps; i++)
1141
+ tick((i / 8) * label_design_1.MM_PER_INCH * ppm, i % 8 === 0 ? 18 : i % 4 === 0 ? 10 : i % 2 === 0 ? 6 : 4, i % 8 === 0 ? String(i / 8) : null);
1142
+ }
1143
+ else {
1144
+ for (let i = 0; i <= Math.floor(lenMm); i++)
1145
+ tick(i * ppm, i % 10 === 0 ? 18 : i % 5 === 0 ? 10 : 5, i % 10 === 0 ? String(i) : null);
1146
+ }
1147
+ return out.join('');
1148
+ }
1149
+ renderStage() {
1150
+ const d = this.d;
1151
+ if (!d || this.mode !== 'design')
1152
+ return;
1153
+ const r = this.refs;
1154
+ const ppm = this.pxPerMm;
1155
+ const w = d.widthMm * ppm;
1156
+ const h = d.heightMm * ppm;
1157
+ r.stage.style.width = `${w}px`;
1158
+ r.stage.style.height = `${h}px`;
1159
+ r.rulerX.style.width = `${w}px`;
1160
+ r.rulerY.style.height = `${h}px`;
1161
+ r.rulerX.innerHTML = this.ticks('x');
1162
+ r.rulerY.innerHTML = this.ticks('y');
1163
+ const canvas = r.canvas;
1164
+ const dpr = Math.min(3, Math.max(1, window.devicePixelRatio || 1));
1165
+ this.warnings = (0, label_design_1.drawLabel)(canvas, d, this.data, { ppm: ppm * dpr, stock: true });
1166
+ canvas.style.width = `${w}px`;
1167
+ canvas.style.height = `${h}px`;
1168
+ r.grid.hidden = !this.showGrid;
1169
+ r.grid.style.backgroundSize = `${this.gridMm * ppm}px ${this.gridMm * ppm}px`;
1170
+ const warned = new Set(this.warnings.map((x) => x.elementId));
1171
+ r.elements.innerHTML = d.elements.map((el) => {
1172
+ const handles = el.id === this.selectedId && this.canWrite ? HANDLES.map((hd) => `<span class="ld-h ld-h-${hd}" data-handle="${hd}"></span>`).join('') : '';
1173
+ return `<div class="ld-el${on(el.id === this.selectedId)}${warned.has(el.id) ? ' warn' : ''}" data-el="${esc(el.id)}" title="${esc(this.elementTitle(el))}" style="left:${el.x * ppm}px;top:${el.y * ppm}px;width:${el.w * ppm}px;height:${el.h * ppm}px">${handles}</div>`;
1174
+ }).join('');
1175
+ r.guides.innerHTML = this.guides.map((g) => `<div class="ld-guide ld-guide-${g.axis}" style="${g.axis === 'x' ? 'left' : 'top'}:${g.mm * ppm}px"></div>`).join('');
1176
+ r.stocktag.hidden = !d.stock;
1177
+ }
1178
+ renderStatus() {
1179
+ const d = this.d;
1180
+ if (!d)
1181
+ return;
1182
+ const s = this.selected;
1183
+ const warning = s ? this.warnings.find((w) => w.elementId === s.id) : undefined;
1184
+ const left = s
1185
+ ? `<b>${esc(this.elementTitle(s))}</b><span>X ${this.fmt(s.x)} · Y ${this.fmt(s.y)} · W ${this.fmt(s.w)} · H ${this.fmt(s.h)} ${this.unit}</span>${warning ? `<span class="lt-chip lt-chip-warn">${esc(warning.message)}</span>` : ''}`
1186
+ : `<span>${d.elements.length} element${d.elements.length === 1 ? '' : 's'} · grid ${this.unit === 'in' ? '1/16 in' : '1 mm'}${this.snap ? ' · snap on' : ''}</span>`;
1187
+ this.refs.status.innerHTML = `${left}<span class="lt-sp"></span><span>Arrow keys move 1 dot, Shift moves 10</span>`;
1188
+ }
1189
+ renderProps() {
1190
+ const d = this.d;
1191
+ if (!d || this.mode !== 'design')
1192
+ return;
1193
+ const el = this.selected;
1194
+ const ro = this.canWrite ? '' : ' disabled';
1195
+ const rd = this.canWrite ? '' : ' readonly';
1196
+ const sec = (body) => `<div class="ld-sec">${body}</div>`;
1197
+ if (!el) {
1198
+ this.refs.props.innerHTML = [
1199
+ '<div class="ld-sec ld-head"><div><div class="ld-title">Label</div><div class="lt-note">Select an element to change it.</div></div></div>',
1200
+ sec(`<span class="lt-eyebrow">Size (${this.unit})</span><div class="ld-two">
1201
+ <label class="lt-field"><span class="ld-label">Width</span><input class="lt-input" type="number" step="0.01" value="${this.fmt(d.widthMm)}" data-change="design" data-key="widthMm"${rd}></label>
1202
+ <label class="lt-field"><span class="ld-label">Height</span><input class="lt-input" type="number" step="0.01" value="${this.fmt(d.heightMm)}" data-change="design" data-key="heightMm"${rd}></label>
1203
+ <label class="lt-field"><span class="ld-label">Gap</span><input class="lt-input" type="number" step="0.01" value="${this.fmt(d.gapMm || 0)}" data-change="design" data-key="gapMm"${rd}></label></div>`),
1204
+ sec(`<span class="lt-eyebrow">Printer</span>
1205
+ <div class="lt-seg"><button type="button" class="${on(d.printer !== 'zpl').trim()}" data-act="printer" data-v="ezpl"${ro}>Godex</button><button type="button" class="${on(d.printer === 'zpl').trim()}" data-act="printer" data-v="zpl"${ro}>Zebra</button></div>
1206
+ <div class="lt-seg"><button type="button" class="${on(d.dpi === 203).trim()}" data-act="dpi" data-v="203"${ro}>203 dpi</button><button type="button" class="${on(d.dpi === 300).trim()}" data-act="dpi" data-v="300"${ro}>300 dpi</button></div>
1207
+ ${d.elements.some((e) => e.native && !e.native[d.printer || 'ezpl']) ? '<p class="lt-note lt-warn-text">Some elements use the other printer\'s font. They will print with a designer font on this printer.</p>' : ''}`),
1208
+ sec(`<span class="lt-eyebrow">Pre-printed stock</span>
1209
+ <label class="lt-check"><input type="checkbox" data-change="stock" ${d.stock ? 'checked' : ''}${ro}> Show a pre-printed color as a guide</label>
1210
+ ${d.stock ? `<div class="ld-two"><label class="lt-field"><span class="ld-label">Color</span><input class="lt-input" type="color" value="${esc(d.stock.color)}" data-change="stockColor"${ro}></label>
1211
+ <label class="lt-field"><span class="ld-label">Starts at (% of width)</span><input class="lt-input" type="number" min="0" max="100" value="${Math.round((d.stock.x / d.widthMm) * 100)}" data-change="stockFrom"${rd}></label></div>` : ''}`),
1212
+ sec(`<span class="lt-eyebrow">Printer area</span><p class="lt-note">${this.printerDots()} at ${d.dpi} dpi.</p>`)
1213
+ ].join('');
1214
+ return;
1215
+ }
1216
+ const typeName = el.type === 'price' ? 'Price' : el.type.charAt(0).toUpperCase() + el.type.slice(1);
1217
+ const parts = [];
1218
+ parts.push(`<div class="ld-sec ld-head"><div><div class="ld-title">${esc(this.elementTitle(el))}</div><div class="lt-note">${typeName} element</div></div><span class="lt-sp"></span>
1219
+ <button type="button" class="lt-icon-btn" data-act="forward" title="Bring forward"${ro}>▲</button>
1220
+ <button type="button" class="lt-icon-btn" data-act="backward" title="Send backward"${ro}>▼</button>
1221
+ <button type="button" class="lt-icon-btn" data-act="duplicate" title="Duplicate (Ctrl+D)"${ro}>⧉</button>
1222
+ <button type="button" class="lt-icon-btn" data-act="remove" title="Delete (Del)"${ro}>${TRASH}</button></div>`);
1223
+ if (this.isNative(el))
1224
+ parts.push(sec('<p class="lt-note">Printer font: this prints with the printer\'s own font, exactly like today\'s labels. You can move it. Changing its size or style switches it to a designer font.</p>'));
1225
+ if (el.type === 'text' || el.type === 'price' || el.type === 'barcode' || el.type === 'qr') {
1226
+ const bound = this.boundField(el);
1227
+ let data = '<span class="lt-eyebrow">Data</span>';
1228
+ if (el.type !== 'qr') {
1229
+ data += `<label class="lt-field"><span class="ld-label">Field</span><select class="lt-input" data-change="field"${ro}>
1230
+ ${el.type === 'text' ? `<option value=""${bound ? '' : ' selected'}>Fixed text</option>` : ''}
1231
+ ${label_design_1.FIELDS.map((f) => `<option value="${esc(f.key)}"${bound === f.key ? ' selected' : ''}>${esc(f.label)}</option>`).join('')}</select></label>`;
1232
+ }
1233
+ if (el.type === 'qr' || (el.type === 'text' && !bound)) {
1234
+ data += `<label class="lt-field"><span class="ld-label">${el.type === 'qr' ? esc(QR_HINT) : 'Text'}</span><input class="lt-input" type="text" value="${esc(el.value)}" data-change="text"${rd}></label>`;
1235
+ }
1236
+ if (el.type === 'price') {
1237
+ data += `<div class="lt-field"><span class="ld-label">Format</span><div class="lt-seg lt-seg-sm">
1238
+ <button type="button" class="${on(el.format === 'super').trim()}" data-act="style" data-key="format" data-v="super"${ro}>$7<sup>59</sup></button>
1239
+ <button type="button" class="${on(!el.format || el.format === 'plain').trim()}" data-act="style" data-key="format" data-v="plain"${ro}>$7.59</button>
1240
+ <button type="button" class="${on(el.format === 'nosign').trim()}" data-act="style" data-key="format" data-v="nosign"${ro}>7.59</button></div></div>`;
1241
+ }
1242
+ if (el.type === 'barcode') {
1243
+ const sym = el.symbology || 'UPC_A';
1244
+ const mod = el.module || 2;
1245
+ data += `<label class="lt-field"><span class="ld-label">Type</span><select class="lt-input" data-change="symbology"${ro}>
1246
+ ${[['UPC_A', 'UPC-A'], ['EAN13', 'EAN-13'], ['CODE128', 'Code 128']].map(([v, l]) => `<option value="${v}"${sym === v ? ' selected' : ''}>${l}</option>`).join('')}</select></label>
1247
+ <label class="lt-field"><span class="ld-label">Bar width (printer dots)</span><select class="lt-input" data-change="module"${ro}>
1248
+ ${[1, 2, 3, 4].map((n) => `<option value="${n}"${mod === n ? ' selected' : ''}>${n}</option>`).join('')}</select></label>
1249
+ <label class="lt-check"><input type="checkbox" data-change="hri" ${el.hri !== false ? 'checked' : ''}${ro}> Show the digits</label>`;
1250
+ }
1251
+ parts.push(sec(data));
1252
+ }
1253
+ if (el.type === 'text' || el.type === 'price') {
1254
+ const font = el.font || 'sans';
1255
+ const fit = el.fit || (el.type === 'price' ? 'shrink' : 'cut');
1256
+ parts.push(sec(`<span class="lt-eyebrow">Text</span><div class="ld-two">
1257
+ <label class="lt-field"><span class="ld-label">Font</span><select class="lt-input" data-change="font"${ro}>
1258
+ ${label_design_1.FONTS.filter((f) => f.key !== 'printer' || el.font === 'printer').map((f) => `<option value="${f.key}"${font === f.key ? ' selected' : ''}>${esc(f.label)}</option>`).join('')}</select></label>
1259
+ <label class="lt-field"><span class="ld-label">Size (pt)</span><input class="lt-input" type="number" min="4" max="200" step="0.5" value="${el.size ? Math.round(el.size * 10) / 10 : ''}" data-change="size"${rd}></label></div>
1260
+ <p class="lt-note">The size stays the same on every label. A long value never makes the font smaller unless you choose Shrink.</p>
1261
+ <label class="lt-field"><span class="ld-label">If it does not fit</span><select class="lt-input" data-change="fit"${this.canWrite && el.type !== 'price' ? '' : ' disabled'}>
1262
+ ${[['cut', 'Cut at the edge'], ['wrap', 'Wrap onto more lines'], ['shrink', 'Shrink that label only']].map(([v, l]) => `<option value="${v}"${fit === v ? ' selected' : ''}>${l}</option>`).join('')}</select></label>
1263
+ ${el.fit === 'wrap' ? `<label class="lt-field"><span class="ld-label">Lines</span><input class="lt-input lt-num" type="number" min="1" max="6" value="${el.lines || 2}" data-change="lines"${rd}></label>` : ''}
1264
+ <div class="ld-row"><div class="lt-seg lt-seg-sm">
1265
+ <button type="button" class="${on(!el.align || el.align === 'left').trim()}" data-act="style" data-key="align" data-v="left"${ro}>Left</button>
1266
+ <button type="button" class="${on(el.align === 'center').trim()}" data-act="style" data-key="align" data-v="center"${ro}>Center</button>
1267
+ <button type="button" class="${on(el.align === 'right').trim()}" data-act="style" data-key="align" data-v="right"${ro}>Right</button></div></div>
1268
+ <div class="ld-row">
1269
+ <button type="button" class="lt-icon-btn${on((el.weight || 400) >= 700)}" data-act="bold" title="Bold"${ro}><b>B</b></button>
1270
+ <button type="button" class="lt-icon-btn${on(el.italic)}" data-act="toggle" data-v="italic" title="Italic"${ro}><i>I</i></button>
1271
+ <button type="button" class="lt-icon-btn${on(el.upper)}" data-act="toggle" data-v="upper" title="Capitals"${ro}>AA</button>
1272
+ ${el.type === 'text' ? `<button type="button" class="lt-icon-btn${on(el.inverse)}" data-act="toggle" data-v="inverse" title="White on black"${ro}>◩</button>` : ''}
1273
+ ${el.type === 'price' ? `<button type="button" class="lt-icon-btn${on(el.strike)}" data-act="toggle" data-v="strike" title="Strike through (was price)"${ro}><s>S</s></button>` : ''}
1274
+ </div>`));
1275
+ }
1276
+ if (el.type === 'image') {
1277
+ parts.push(sec(`<span class="lt-eyebrow">Image</span><div class="lt-seg lt-seg-sm">
1278
+ <button type="button" class="${on(this.imageMode === 'dither').trim()}" data-act="imageMode" data-v="dither">Photo (dots)</button>
1279
+ <button type="button" class="${on(this.imageMode === 'threshold').trim()}" data-act="imageMode" data-v="threshold">Logo (solid)</button></div>
1280
+ <label class="lt-btn lt-btn-sm ld-upload${this.canWrite ? '' : ' disabled'}">Upload image<input type="file" accept="image/*" data-change="image" hidden${ro}></label>
1281
+ <p class="lt-note">The image is turned into black and white printer dots at the box size. Resize the box first, then upload, for the sharpest print.</p>
1282
+ ${this.imageError ? `<span class="lt-error">${esc(this.imageError)}</span>` : ''}`));
1283
+ }
1284
+ if (el.type === 'box') {
1285
+ parts.push(sec(`<span class="lt-eyebrow">Box</span><label class="lt-field"><span class="ld-label">Line thickness (${this.unit})</span><input class="lt-input lt-num" type="number" min="0" step="0.01" value="${this.fmt(el.stroke || 0.3)}" data-change="stroke"${rd}></label>`));
1286
+ }
1287
+ parts.push(sec(`<span class="lt-eyebrow">Position and size (${this.unit})</span><div class="ld-two">
1288
+ ${['x', 'y', 'w', 'h'].map((k) => `<label class="lt-field"><span class="ld-label">${k.toUpperCase()}</span><input class="lt-input" type="number" step="0.01" value="${this.fmt(el[k])}" data-change="pos" data-key="${k}"${rd}></label>`).join('')}</div>`));
1289
+ parts.push(sec(`<span class="lt-eyebrow">Show</span><select class="lt-input" data-change="when"${ro}>
1290
+ <option value=""${el.when ? '' : ' selected'}>On every label</option>
1291
+ ${label_design_1.FIELDS.map((f) => `<option value="${esc(f.key)}"${el.when === f.key ? ' selected' : ''}>Only when ${esc(f.label)} has a value</option>`).join('')}</select>`));
1292
+ this.refs.props.innerHTML = parts.join('');
1293
+ }
1294
+ renderCheck() {
1295
+ const d = this.d;
1296
+ if (!d || this.mode !== 'check')
1297
+ return;
1298
+ const problems = this.problems();
1299
+ const count = (kind) => problems.filter((r) => r.warnings.some((w) => w.kind === kind)).length;
1300
+ const pct = (n) => (this.checkResults.length ? (n / this.checkResults.length) * 100 : 0);
1301
+ const fits = this.checkResults.length - problems.length;
1302
+ const cut = new Set();
1303
+ problems.forEach((r) => r.warnings.filter((w) => w.kind === 'cut').forEach((w) => cut.add(w.elementId)));
1304
+ const cutEls = d.elements.filter((e) => cut.has(e.id) && e.type === 'text');
1305
+ const filters = [['all', 'All'], ['cut', 'Cut'], ['wrap', 'Too many lines'], ['shrink', 'Shrunk'], ['missing', 'Missing data'], ['barcode', 'Bad barcode']];
1306
+ const shown = this.filteredProblems();
1307
+ const scale = d.dpi > 250 ? 0.6 : 0.9;
1308
+ this.refs.check.innerHTML = `<div class="ld-check-main">
1309
+ <div class="lt-card ld-printer"><div class="ld-card-head"><b>Printer view</b><span class="lt-chip">${this.printerDots()}</span><span class="lt-sp"></span>
1310
+ ${this.checkSelected ? `<span class="lt-note">${esc(this.itemTitle(this.checkSelected.item))}</span>` : ''}</div>
1311
+ <div class="ld-printer-body"><rpk-label-preview data-ref-check="printer"></rpk-label-preview><p class="lt-note">What the printer puts on the label, dot for dot. A pre-printed color is not printed.</p></div></div>
1312
+ <div class="lt-card"><div class="ld-card-head"><b>Items that need a look</b><span class="lt-chip lt-chip-warn">${problems.length}</span><span class="lt-sp"></span>
1313
+ <div class="lt-seg lt-seg-sm">${filters.map(([k, l]) => `<button type="button" class="${on(this.checkFilter === k).trim()}" data-act="checkFilter" data-v="${k}">${l}${k === 'all' ? '' : ` <b>${count(k)}</b>`}</button>`).join('')}</div></div>
1314
+ ${this.checkRunning ? `<div class="lt-empty">Checking ${this.checkDone} of ${this.checkTotal || '…'} items…</div>` : ''}
1315
+ ${!this.checkRunning && this.checkError ? `<div class="lt-empty">${esc(this.checkError)}</div>` : ''}
1316
+ ${!this.checkRunning && !this.checkError && !problems.length && this.checkResults.length ? '<div class="lt-empty">Every item fits.</div>' : ''}
1317
+ ${this.checkRunning ? '' : `<div class="ld-problems">${shown.map((r, i) => `<button type="button" class="ld-problem${on(r === this.checkSelected)}" data-act="checkPick" data-v="${i}">
1318
+ <rpk-label-preview data-problem="${i}"></rpk-label-preview><span class="ld-pmsg">${esc(r.warnings[r.warnings.length - 1].message)}</span><span class="lt-note">${esc(this.itemTitle(r.item))}</span></button>`).join('')}</div>`}
1319
+ </div></div>
1320
+ <aside class="lt-card ld-summary"><span class="lt-eyebrow">Checked against</span>
1321
+ <div class="ld-metric">${this.checkResults.length.toLocaleString()} items</div>
1322
+ <div class="lt-note">${esc(this.store || 'This store')}${this.checkTotal > this.checkResults.length && !this.checkRunning ? ` · first ${this.checkResults.length}` : ''}</div>
1323
+ <div class="ld-bar"><span class="ok" style="width:${pct(fits)}%"></span><span class="bad" style="width:${pct(problems.length)}%"></span></div>
1324
+ <div class="ld-li"><i class="ok"></i>Fits as designed<b>${fits.toLocaleString()}</b></div>
1325
+ <div class="ld-li"><i class="bad"></i>Need a look<b>${problems.length.toLocaleString()}</b></div>
1326
+ ${cutEls.length && this.canWrite ? `<span class="lt-eyebrow">Quick fixes</span>${cutEls.map((e) => `<div class="ld-fix"><div><b>Let ${esc(this.elementTitle(e))} use 2 lines</b><p class="lt-note">The font size stays the same on every label.</p></div><button type="button" class="lt-btn lt-btn-sm" data-act="letWrap" data-v="${esc(e.id)}">Apply</button></div>`).join('')}` : ''}
1327
+ <button type="button" class="lt-btn" data-act="checkRun"${this.checkRunning ? ' disabled' : ''}>Check again</button>
1328
+ <p class="lt-note">Fixes change the template, never the items. Save to keep them.</p></aside>`;
1329
+ const printer = this.refs.check.querySelector('[data-ref-check="printer"]');
1330
+ if (printer) {
1331
+ printer.fontsUrl = this.fonts;
1332
+ printer.printerView = true;
1333
+ printer.printerScale = scale;
1334
+ printer.data = this.checkSelected ? this.checkSelected.data : this.data;
1335
+ printer.design = d;
1336
+ }
1337
+ this.refs.check.querySelectorAll('[data-problem]').forEach((node) => {
1338
+ const preview = node;
1339
+ const r = shown[Number(node.dataset.problem)];
1340
+ preview.fontsUrl = this.fonts;
1341
+ preview.maxWidth = 230;
1342
+ preview.maxHeight = 90;
1343
+ preview.data = r.data;
1344
+ preview.design = d;
1345
+ });
1346
+ }
1347
+ }
1348
+ customElements.define(tag, LabelDesigner);
1349
+ }