@ai-matrx/print 0.1.0

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.
package/dist/react.js ADDED
@@ -0,0 +1,1539 @@
1
+ "use client";
2
+
3
+ // src/react.tsx
4
+ import * as React from "react";
5
+ import { useEffect, useMemo, useState } from "react";
6
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
7
+ import { Drawer as DrawerPrimitive } from "vaul";
8
+ import { twMerge } from "tailwind-merge";
9
+
10
+ // src/core.ts
11
+ function escapeHtml(str) {
12
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13
+ }
14
+ function openPrintWindow(htmlDocument, filename = "print") {
15
+ const win = window.open("", "_blank", "width=920,height=720,scrollbars=yes");
16
+ if (!win) {
17
+ const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
18
+ const url = URL.createObjectURL(blob);
19
+ const a = document.createElement("a");
20
+ a.href = url;
21
+ a.download = `${filename.replace(/\s+/g, "-").toLowerCase()}.html`;
22
+ document.body.appendChild(a);
23
+ a.click();
24
+ document.body.removeChild(a);
25
+ URL.revokeObjectURL(url);
26
+ return;
27
+ }
28
+ win.document.open();
29
+ win.document.write(htmlDocument);
30
+ win.document.close();
31
+ }
32
+
33
+ // src/qr.ts
34
+ var QR_MIN_QUIET_ZONE_MODULES = 4;
35
+ var DEFAULT_QR_EC_LEVEL = "M";
36
+ var QR_MIN_VERSION = 1;
37
+ var QR_MAX_VERSION = 40;
38
+ var BYTE_CAPACITY = [
39
+ /* v1 */
40
+ [17, 14, 11, 7],
41
+ /* v2 */
42
+ [32, 26, 20, 14],
43
+ /* v3 */
44
+ [53, 42, 32, 24],
45
+ /* v4 */
46
+ [78, 62, 46, 34],
47
+ /* v5 */
48
+ [106, 84, 60, 44],
49
+ /* v6 */
50
+ [134, 106, 74, 58],
51
+ /* v7 */
52
+ [154, 122, 86, 64],
53
+ /* v8 */
54
+ [192, 152, 108, 84],
55
+ /* v9 */
56
+ [230, 180, 130, 98],
57
+ /* v10 */
58
+ [271, 213, 151, 119],
59
+ /* v11 */
60
+ [321, 251, 177, 137],
61
+ /* v12 */
62
+ [367, 287, 203, 155],
63
+ /* v13 */
64
+ [425, 331, 241, 177],
65
+ /* v14 */
66
+ [458, 362, 258, 194],
67
+ /* v15 */
68
+ [520, 412, 292, 220],
69
+ /* v16 */
70
+ [586, 450, 322, 250],
71
+ /* v17 */
72
+ [644, 504, 364, 280],
73
+ /* v18 */
74
+ [718, 560, 394, 310],
75
+ /* v19 */
76
+ [792, 624, 442, 338],
77
+ /* v20 */
78
+ [858, 666, 482, 382],
79
+ /* v21 */
80
+ [929, 711, 509, 403],
81
+ /* v22 */
82
+ [1003, 779, 565, 439],
83
+ /* v23 */
84
+ [1091, 857, 611, 461],
85
+ /* v24 */
86
+ [1171, 911, 661, 511],
87
+ /* v25 */
88
+ [1273, 997, 715, 535],
89
+ /* v26 */
90
+ [1367, 1059, 751, 593],
91
+ /* v27 */
92
+ [1465, 1125, 805, 625],
93
+ /* v28 */
94
+ [1528, 1190, 868, 658],
95
+ /* v29 */
96
+ [1628, 1264, 908, 698],
97
+ /* v30 */
98
+ [1732, 1370, 982, 742],
99
+ /* v31 */
100
+ [1840, 1452, 1030, 790],
101
+ /* v32 */
102
+ [1952, 1538, 1112, 842],
103
+ /* v33 */
104
+ [2068, 1628, 1168, 898],
105
+ /* v34 */
106
+ [2188, 1722, 1228, 958],
107
+ /* v35 */
108
+ [2303, 1809, 1283, 983],
109
+ /* v36 */
110
+ [2431, 1911, 1351, 1051],
111
+ /* v37 */
112
+ [2563, 1989, 1423, 1093],
113
+ /* v38 */
114
+ [2699, 2099, 1499, 1139],
115
+ /* v39 */
116
+ [2809, 2213, 1579, 1219],
117
+ /* v40 */
118
+ [2953, 2331, 1663, 1273]
119
+ ];
120
+ var EC_COLUMN = { L: 0, M: 1, Q: 2, H: 3 };
121
+ function assertEcLevel(ecLevel) {
122
+ const column = EC_COLUMN[ecLevel];
123
+ if (column === void 0) {
124
+ throw new Error(`Unknown QR error correction level "${String(ecLevel)}". Expected one of "L", "M", "Q", "H".`);
125
+ }
126
+ return column;
127
+ }
128
+ function assertVersion(version) {
129
+ if (typeof version !== "number" || !Number.isInteger(version) || version < QR_MIN_VERSION || version > QR_MAX_VERSION) {
130
+ throw new Error(
131
+ `QR version must be an integer between ${QR_MIN_VERSION} and ${QR_MAX_VERSION}, got ${String(version)}.`
132
+ );
133
+ }
134
+ return version;
135
+ }
136
+ function qrByteCapacity(version, ecLevel) {
137
+ const column = assertEcLevel(ecLevel);
138
+ const row = BYTE_CAPACITY[assertVersion(version) - 1];
139
+ if (row === void 0) {
140
+ throw new Error(`No QR capacity row for version ${version}.`);
141
+ }
142
+ return row[column];
143
+ }
144
+ function qrModuleCount(version) {
145
+ return 17 + 4 * assertVersion(version);
146
+ }
147
+ function minQrVersion(payloadByteLen, ecLevel) {
148
+ const column = assertEcLevel(ecLevel);
149
+ if (typeof payloadByteLen !== "number" || !Number.isInteger(payloadByteLen) || payloadByteLen < 0) {
150
+ throw new Error(`QR payload byte length must be a non-negative integer, got ${String(payloadByteLen)}.`);
151
+ }
152
+ for (let version = QR_MIN_VERSION; version <= QR_MAX_VERSION; version += 1) {
153
+ const row = BYTE_CAPACITY[version - 1];
154
+ if (row !== void 0 && row[column] >= payloadByteLen) {
155
+ return version;
156
+ }
157
+ }
158
+ const max = qrByteCapacity(QR_MAX_VERSION, ecLevel);
159
+ throw new Error(
160
+ `QR payload of ${payloadByteLen} bytes does not fit any QR version at error correction level "${ecLevel}" (version ${QR_MAX_VERSION} holds ${max} bytes). Shorten the payload \u2014 a short resolver URL is the intended fix \u2014 or, only for a large clean code, drop to a lower error correction level.`
161
+ );
162
+ }
163
+ function resolveEcLevel(ecLevel) {
164
+ if (ecLevel === void 0) return DEFAULT_QR_EC_LEVEL;
165
+ assertEcLevel(ecLevel);
166
+ return ecLevel;
167
+ }
168
+ function resolveQuietZone(quietZoneModules) {
169
+ if (typeof quietZoneModules !== "number" || !Number.isFinite(quietZoneModules)) {
170
+ return QR_MIN_QUIET_ZONE_MODULES;
171
+ }
172
+ return Math.max(QR_MIN_QUIET_ZONE_MODULES, Math.floor(quietZoneModules));
173
+ }
174
+ function resolveWidthPx(widthPx) {
175
+ if (widthPx === void 0) return void 0;
176
+ if (typeof widthPx !== "number" || !Number.isFinite(widthPx) || widthPx <= 0) {
177
+ throw new Error(`QR widthPx must be a positive number, got ${String(widthPx)}.`);
178
+ }
179
+ return Math.round(widthPx);
180
+ }
181
+ function assertValue(value) {
182
+ if (typeof value !== "string" || value.length === 0) {
183
+ throw new Error("QR value must be a non-empty string.");
184
+ }
185
+ return value;
186
+ }
187
+ async function generateQrDataUrl(value, opts) {
188
+ assertValue(value);
189
+ const width = resolveWidthPx(opts?.widthPx);
190
+ const options = {
191
+ errorCorrectionLevel: resolveEcLevel(opts?.ecLevel),
192
+ margin: resolveQuietZone(opts?.quietZoneModules)
193
+ };
194
+ if (width !== void 0) options.width = width;
195
+ const QRCode = await import("qrcode");
196
+ return QRCode.toDataURL(value, options);
197
+ }
198
+
199
+ // src/labels.ts
200
+ var AVERY_5160 = {
201
+ id: "avery-5160",
202
+ name: 'Address 1" \xD7 2\u215D" \u2014 30 per sheet',
203
+ stockCode: "Avery 5160 / 8160",
204
+ sheetWIn: 8.5,
205
+ sheetHIn: 11,
206
+ marginTopIn: 0.5,
207
+ marginLeftIn: 0.1875,
208
+ labelWIn: 2.625,
209
+ labelHIn: 1,
210
+ cols: 3,
211
+ rows: 10,
212
+ gutterXIn: 0.125,
213
+ gutterYIn: 0,
214
+ cornerRadiusIn: 0.1
215
+ };
216
+ var LABEL_TEMPLATES = [
217
+ AVERY_5160,
218
+ {
219
+ id: "avery-5163",
220
+ name: 'Shipping 2" \xD7 4" \u2014 10 per sheet',
221
+ stockCode: "Avery 5163 / 8163",
222
+ sheetWIn: 8.5,
223
+ sheetHIn: 11,
224
+ marginTopIn: 0.5,
225
+ marginLeftIn: 0.15625,
226
+ labelWIn: 4,
227
+ labelHIn: 2,
228
+ cols: 2,
229
+ rows: 5,
230
+ gutterXIn: 0.1875,
231
+ gutterYIn: 0,
232
+ cornerRadiusIn: 0.125
233
+ },
234
+ {
235
+ id: "avery-5164",
236
+ name: 'Shipping 3\u2153" \xD7 4" \u2014 6 per sheet',
237
+ stockCode: "Avery 5164 / 8164",
238
+ sheetWIn: 8.5,
239
+ sheetHIn: 11,
240
+ marginTopIn: 0.5,
241
+ marginLeftIn: 0.15625,
242
+ labelWIn: 4,
243
+ labelHIn: 3.3333333,
244
+ cols: 2,
245
+ rows: 3,
246
+ gutterXIn: 0.1875,
247
+ gutterYIn: 0,
248
+ cornerRadiusIn: 0.125
249
+ },
250
+ {
251
+ id: "avery-22806",
252
+ name: 'Square 2" \xD7 2" \u2014 12 per sheet',
253
+ stockCode: "Avery 22806",
254
+ sheetWIn: 8.5,
255
+ sheetHIn: 11,
256
+ marginTopIn: 0.75,
257
+ marginLeftIn: 0.5,
258
+ labelWIn: 2,
259
+ labelHIn: 2,
260
+ cols: 3,
261
+ rows: 4,
262
+ gutterXIn: 0.75,
263
+ gutterYIn: 0.5,
264
+ cornerRadiusIn: 0.09375
265
+ },
266
+ {
267
+ id: "avery-22807",
268
+ name: 'Round 2" \u2014 12 per sheet',
269
+ stockCode: "Avery 22807",
270
+ sheetWIn: 8.5,
271
+ sheetHIn: 11,
272
+ marginTopIn: 0.75,
273
+ marginLeftIn: 0.5,
274
+ labelWIn: 2,
275
+ labelHIn: 2,
276
+ cols: 3,
277
+ rows: 4,
278
+ gutterXIn: 0.75,
279
+ gutterYIn: 0.5,
280
+ cornerRadiusIn: 1,
281
+ round: true
282
+ },
283
+ {
284
+ id: "brother-dk-1202",
285
+ name: 'Shipping roll 2.4" \xD7 3.9" \u2014 die-cut',
286
+ stockCode: "Brother DK-1202",
287
+ sheetWIn: 2.4,
288
+ sheetHIn: 3.9,
289
+ marginTopIn: 0,
290
+ marginLeftIn: 0,
291
+ labelWIn: 2.4,
292
+ labelHIn: 3.9,
293
+ cols: 1,
294
+ rows: 1,
295
+ gutterXIn: 0,
296
+ gutterYIn: 0,
297
+ kind: "roll"
298
+ },
299
+ {
300
+ id: "brother-dk-11201",
301
+ name: 'Address roll 1.14" \xD7 3.5" \u2014 die-cut',
302
+ stockCode: "Brother DK-11201",
303
+ sheetWIn: 1.14,
304
+ sheetHIn: 3.5,
305
+ marginTopIn: 0,
306
+ marginLeftIn: 0,
307
+ labelWIn: 1.14,
308
+ labelHIn: 3.5,
309
+ cols: 1,
310
+ rows: 1,
311
+ gutterXIn: 0,
312
+ gutterYIn: 0,
313
+ kind: "roll"
314
+ },
315
+ {
316
+ id: "dymo-30252",
317
+ name: 'Address roll 1\u215B" \xD7 3\xBD"',
318
+ stockCode: "DYMO 30252",
319
+ sheetWIn: 1.125,
320
+ sheetHIn: 3.5,
321
+ marginTopIn: 0,
322
+ marginLeftIn: 0,
323
+ labelWIn: 1.125,
324
+ labelHIn: 3.5,
325
+ cols: 1,
326
+ rows: 1,
327
+ gutterXIn: 0,
328
+ gutterYIn: 0,
329
+ kind: "roll"
330
+ },
331
+ {
332
+ id: "roll-4x6",
333
+ name: 'Shipping roll 4" \xD7 6"',
334
+ stockCode: "Thermal 4x6",
335
+ sheetWIn: 4,
336
+ sheetHIn: 6,
337
+ marginTopIn: 0,
338
+ marginLeftIn: 0,
339
+ labelWIn: 4,
340
+ labelHIn: 6,
341
+ cols: 1,
342
+ rows: 1,
343
+ gutterXIn: 0,
344
+ gutterYIn: 0,
345
+ kind: "roll"
346
+ }
347
+ ];
348
+ function getLabelTemplate(id) {
349
+ return LABEL_TEMPLATES.find((t) => t.id === id);
350
+ }
351
+ function assertTemplateGeometry(t) {
352
+ const w = t.marginLeftIn * 2 + t.cols * t.labelWIn + (t.cols - 1) * t.gutterXIn;
353
+ const h = t.marginTopIn * 2 + t.rows * t.labelHIn + (t.rows - 1) * t.gutterYIn;
354
+ const tol = 1 / 64;
355
+ if (Math.abs(w - t.sheetWIn) > tol || Math.abs(h - t.sheetHIn) > tol) {
356
+ console.error(
357
+ `[label-print] Template ${t.id} does not tile its sheet: grid ${w.toFixed(4)}\xD7${h.toFixed(4)}in vs sheet ${t.sheetWIn}\xD7${t.sheetHIn}in`
358
+ );
359
+ }
360
+ }
361
+ if (typeof process === "undefined" || true) {
362
+ LABEL_TEMPLATES.forEach(assertTemplateGeometry);
363
+ }
364
+ var DEFAULTS = {
365
+ startAtLabel: 1,
366
+ copiesPerCode: 1,
367
+ showCaption: true,
368
+ showLines: true,
369
+ rangeFrom: 0,
370
+ rangeTo: 0,
371
+ ecLevel: "M",
372
+ calibrationPage: false
373
+ };
374
+ function resolveSettings(s) {
375
+ const num = (v, d, min = 0) => typeof v === "number" && !Number.isNaN(v) ? Math.max(min, v) : d;
376
+ const ec = s?.ecLevel;
377
+ return {
378
+ startAtLabel: num(s?.startAtLabel, DEFAULTS.startAtLabel, 1),
379
+ copiesPerCode: num(s?.copiesPerCode, DEFAULTS.copiesPerCode, 1),
380
+ showCaption: s?.showCaption ?? DEFAULTS.showCaption,
381
+ showLines: s?.showLines ?? DEFAULTS.showLines,
382
+ rangeFrom: num(s?.rangeFrom, 0),
383
+ rangeTo: num(s?.rangeTo, 0),
384
+ ecLevel: ec === "L" || ec === "Q" ? ec : "M",
385
+ calibrationPage: s?.calibrationPage ?? false
386
+ };
387
+ }
388
+ var PRINT_DPI = 300;
389
+ var QUIET_ZONE_MODULES = 4;
390
+ async function generateQrDataUri(value, ecLevel, sizeInches) {
391
+ return generateQrDataUrl(value, {
392
+ ecLevel,
393
+ quietZoneModules: QUIET_ZONE_MODULES,
394
+ widthPx: Math.round(sizeInches * PRINT_DPI)
395
+ });
396
+ }
397
+ function computeCellLayout(t, showCaption, hasBadge = false) {
398
+ const padIn = Math.min(0.08, t.labelHIn * 0.06);
399
+ const wide = t.labelWIn / t.labelHIn >= 1.8;
400
+ const badgeZoneIn = hasBadge ? Math.min(0.4, t.labelHIn * 0.22) : 0;
401
+ if (wide) {
402
+ return { wide, qrIn: t.labelHIn - padIn * 2, padIn, badgeZoneIn };
403
+ }
404
+ const inset = t.round ? t.labelWIn * 0.15 : 0;
405
+ const captionZone = showCaption ? Math.min(0.28, t.labelHIn * 0.16) : 0;
406
+ const qrIn = Math.min(
407
+ t.labelWIn - padIn * 2 - inset * 2,
408
+ t.labelHIn - padIn * 2 - inset * 2 - captionZone - badgeZoneIn
409
+ );
410
+ return { wide, qrIn, padIn, badgeZoneIn };
411
+ }
412
+ function sheetStyles(t) {
413
+ const radius = t.round ? `${t.labelWIn / 2}in` : t.cornerRadiusIn ? `${t.cornerRadiusIn}in` : "0";
414
+ return `
415
+ @page { size: ${t.sheetWIn}in ${t.sheetHIn}in; margin: 0; }
416
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
417
+ html, body { width: ${t.sheetWIn}in; background: #fff; color: #000;
418
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
419
+
420
+ .lp-page {
421
+ width: ${t.sheetWIn}in; height: ${t.sheetHIn}in;
422
+ position: relative; overflow: hidden;
423
+ page-break-after: always; break-after: page;
424
+ border-bottom: 1px solid #e2e8f0; /* screen-only separator */
425
+ }
426
+ .lp-page:last-child { page-break-after: auto; break-after: auto; border-bottom: none; }
427
+
428
+ .lp-cell {
429
+ position: absolute;
430
+ width: ${t.labelWIn}in; height: ${t.labelHIn}in;
431
+ border-radius: ${radius};
432
+ overflow: hidden;
433
+ -webkit-print-color-adjust: exact; print-color-adjust: exact;
434
+ }
435
+ .lp-qr {
436
+ display: block;
437
+ image-rendering: pixelated; /* no resampling blur on crisp modules */
438
+ }
439
+ .lp-cell-wide { display: flex; align-items: center; }
440
+ .lp-cell-stacked { display: flex; flex-direction: column; align-items: center; justify-content: center; }
441
+
442
+ .lp-text { flex: 1 1 auto; min-width: 0; overflow: hidden; align-self: stretch;
443
+ display: flex; flex-direction: column; justify-content: center; }
444
+ .lp-fit-wrap { overflow: hidden; }
445
+ .lp-caption { font-weight: 700; line-height: 1.15; word-break: break-word; }
446
+ .lp-line { line-height: 1.25; word-break: break-word; }
447
+ .lp-caption-stacked { text-align: center; width: 100%; }
448
+
449
+ /* Badge \u2014 the LARGE token above the caption (garment size, bin letter). */
450
+ .lp-badge { font-weight: 800; line-height: 1.05; word-break: break-word;
451
+ -webkit-print-color-adjust: exact; print-color-adjust: exact; }
452
+ .lp-badge-stacked { text-align: center; width: 100%; }
453
+
454
+ /* Durable http(s) artwork in the lines zone. */
455
+ .lp-img { display: block; width: 100%; object-fit: contain; }
456
+
457
+ /* Calibration */
458
+ .lp-cal-cell { border: 0.5pt dashed #94a3b8; background: transparent; }
459
+ .lp-cal-label { position: absolute; inset: 0; display: flex; align-items: center;
460
+ justify-content: center; font-size: 7pt; color: #94a3b8; text-align: center; padding: 4px; }
461
+ .lp-crop { position: absolute; background: #94a3b8;
462
+ -webkit-print-color-adjust: exact; print-color-adjust: exact; }
463
+
464
+ @media print {
465
+ html, body { height: auto; overflow: visible; }
466
+ .lp-page { border-bottom: none; }
467
+ .screen-only { display: none !important; }
468
+ }
469
+ `;
470
+ }
471
+ function cellPosition(t, index) {
472
+ const col = index % t.cols;
473
+ const row = Math.floor(index / t.cols);
474
+ return {
475
+ x: t.marginLeftIn + col * (t.labelWIn + t.gutterXIn),
476
+ y: t.marginTopIn + row * (t.labelHIn + t.gutterYIn)
477
+ };
478
+ }
479
+ var LP_FIT_SCRIPT = `
480
+ <script>
481
+ (function () {
482
+ function fit(el, box) {
483
+ var size = parseFloat(window.getComputedStyle(el).fontSize);
484
+ var min = 4;
485
+ while (size > min && (el.scrollWidth > box.clientWidth + 1 || el.scrollHeight > box.clientHeight + 1)) {
486
+ size -= 0.5;
487
+ el.style.fontSize = size + "px";
488
+ }
489
+ }
490
+ function run() {
491
+ var els = document.querySelectorAll(".lp-fit-wrap .lp-fit");
492
+ for (var i = 0; i < els.length; i++) fit(els[i], els[i].parentElement);
493
+ }
494
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", run);
495
+ else setTimeout(run, 0);
496
+ })();
497
+ <\/script>`;
498
+ function isHttpUrl(value) {
499
+ return typeof value === "string" && /^https?:\/\//i.test(value.trim());
500
+ }
501
+ function buildLabelCellHtml(label, qrDataUri, t, layout, s) {
502
+ const qr = qrDataUri ? `<img class="lp-qr" src="${qrDataUri}" alt="" style="width:${layout.qrIn}in;height:${layout.qrIn}in;" />` : "";
503
+ const captionText = s.showCaption ? label.caption ?? label.qrValue : "";
504
+ const badgeText = label.badge?.trim() ? label.badge.trim() : "";
505
+ if (layout.wide) {
506
+ const lines = s.showLines && label.lines?.length ? label.lines.filter((l) => l.trim()).map(
507
+ (l) => `<div class="lp-line" style="font-size:8pt;">${escapeHtml(l)}</div>`
508
+ ).join("") : "";
509
+ const caption2 = captionText ? `<div class="lp-fit-wrap" style="max-height:${(t.labelHIn * 0.3).toFixed(3)}in;"><div class="lp-fit lp-caption" style="font-size:12pt;">${escapeHtml(captionText)}</div></div>` : "";
510
+ const badge2 = badgeText ? `<div class="lp-fit-wrap" style="max-height:${layout.badgeZoneIn.toFixed(3)}in;"><div class="lp-fit lp-badge" style="font-size:24pt;">${escapeHtml(badgeText)}</div></div>` : "";
511
+ const image = isHttpUrl(label.imageUrl) ? `<img class="lp-img" src="${escapeHtml(label.imageUrl)}" alt="" style="height:${(t.labelHIn * 0.35).toFixed(3)}in;" />` : "";
512
+ return `<div class="lp-cell-wide" style="height:100%;padding:${layout.padIn}in;gap:${layout.padIn}in;">
513
+ ${qr}
514
+ <div class="lp-text">${badge2}${caption2}${lines}${image}</div>
515
+ </div>`;
516
+ }
517
+ const caption = captionText ? `<div class="lp-fit-wrap lp-caption-stacked" style="height:${Math.min(0.28, t.labelHIn * 0.16).toFixed(3)}in;"><div class="lp-fit lp-caption" style="font-size:9pt;">${escapeHtml(captionText)}</div></div>` : "";
518
+ const badge = badgeText ? `<div class="lp-fit-wrap lp-badge-stacked" style="height:${layout.badgeZoneIn.toFixed(3)}in;"><div class="lp-fit lp-badge" style="font-size:18pt;">${escapeHtml(badgeText)}</div></div>` : "";
519
+ return `<div class="lp-cell-stacked" style="height:100%;padding:${layout.padIn}in;">
520
+ ${qr}
521
+ ${badge}
522
+ ${caption}
523
+ </div>`;
524
+ }
525
+ function renderPages(t, cells) {
526
+ const perPage = t.cols * t.rows;
527
+ const pages = [];
528
+ for (let p = 0; p < Math.max(1, Math.ceil(cells.length / perPage)); p++) {
529
+ const batch = cells.slice(p * perPage, (p + 1) * perPage);
530
+ const cellHtml = batch.map((c, i) => {
531
+ const { x, y } = cellPosition(t, i);
532
+ return `<div class="lp-cell" style="left:${x}in;top:${y}in;">${c.html}</div>`;
533
+ }).join("\n");
534
+ pages.push(`<div class="lp-page">${cellHtml}</div>`);
535
+ }
536
+ return pages.join("\n");
537
+ }
538
+ function openSheetWindow(t, bodyHtml, title, withFitScript, warning) {
539
+ const warningHtml = warning ? `<div class="screen-only lp-warning" style="font-family:sans-serif;font-size:12px;font-weight:600;padding:9px 16px;background:#fef2f2;color:#991b1b;border-bottom:1px solid #fecaca;">${escapeHtml(warning)}</div>` : "";
540
+ const html = `<!DOCTYPE html>
541
+ <html lang="en">
542
+ <head>
543
+ <meta charset="UTF-8">
544
+ <title>${escapeHtml(title)} \u2014 ${escapeHtml(t.stockCode)}</title>
545
+ <style>${sheetStyles(t)}</style>
546
+ </head>
547
+ <body>
548
+ ${warningHtml}
549
+ <div class="screen-only" style="font-family:sans-serif;font-size:12px;padding:10px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
550
+ <button onclick="window.print()" style="padding:7px 18px;background:#111;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Print / Save PDF</button>
551
+ <button onclick="window.close()" style="padding:7px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;">Close</button>
552
+ <span style="color:#64748b;font-size:11px;">${escapeHtml(t.stockCode)} \xB7 ${escapeHtml(t.name)} \xB7 set printer to <strong>100% scale (no "fit to page"), no margins, Portrait</strong></span>
553
+ </div>
554
+ ${bodyHtml}
555
+ ${withFitScript ? LP_FIT_SCRIPT : ""}
556
+ </body>
557
+ </html>`;
558
+ openPrintWindow(html, title);
559
+ }
560
+ function expandLabels(labels, s) {
561
+ const from = s.rangeFrom > 0 ? s.rangeFrom : 1;
562
+ const to = s.rangeTo > 0 ? Math.min(s.rangeTo, labels.length) : labels.length;
563
+ const subset = labels.slice(from - 1, to);
564
+ if (s.copiesPerCode <= 1) return subset;
565
+ const out = [];
566
+ for (const l of subset)
567
+ for (let i = 0; i < s.copiesPerCode; i++) out.push(l);
568
+ return out;
569
+ }
570
+ function buildCalibrationBody(t) {
571
+ const perPage = t.cols * t.rows;
572
+ const cells = [];
573
+ for (let i = 0; i < perPage; i++) {
574
+ const { x, y } = cellPosition(t, i);
575
+ cells.push(
576
+ `<div class="lp-cell lp-cal-cell" style="left:${x}in;top:${y}in;"><div class="lp-cal-label">${i + 1}</div></div>`
577
+ );
578
+ }
579
+ const gridR = t.sheetWIn - t.marginLeftIn;
580
+ const gridB = t.sheetHIn - t.marginTopIn;
581
+ const mark = (x, y, h) => `<div class="lp-crop" style="left:${x}in;top:${y}in;width:${h ? 0.25 : 0.0104}in;height:${h ? 0.0104 : 0.25}in;"></div>`;
582
+ const marks = [
583
+ mark(t.marginLeftIn - 0.25, t.marginTopIn, true),
584
+ mark(t.marginLeftIn, t.marginTopIn - 0.25, false),
585
+ mark(gridR, t.marginTopIn, true),
586
+ mark(gridR - 0.0104, t.marginTopIn - 0.25, false),
587
+ mark(t.marginLeftIn - 0.25, gridB - 0.0104, true),
588
+ mark(t.marginLeftIn, gridB, false),
589
+ mark(gridR, gridB - 0.0104, true),
590
+ mark(gridR - 0.0104, gridB, false)
591
+ ].join("");
592
+ const banner = `<div style="position:absolute;left:0;right:0;top:${Math.max(0, t.marginTopIn - 0.3).toFixed(2)}in;text-align:center;font-size:7pt;color:#94a3b8;">Calibration \u2014 ${escapeHtml(t.stockCode)} \xB7 hold against your label sheet: every outline must sit on a label</div>`;
593
+ return `<div class="lp-page">${banner}${marks}${cells.join("\n")}</div>`;
594
+ }
595
+ function printCalibrationSheet(template) {
596
+ openSheetWindow(
597
+ template,
598
+ buildCalibrationBody(template),
599
+ "Label calibration",
600
+ false
601
+ );
602
+ }
603
+ var MM_PER_IN = 25.4;
604
+ var MIN_SCANNABLE_MODULE_MM = 0.4;
605
+ function assertScannable(template, payloadByteLen, ecLevel = DEFAULTS.ecLevel) {
606
+ const layout = computeCellLayout(template, true);
607
+ const printedSideMm = layout.qrIn * MM_PER_IN;
608
+ let version;
609
+ try {
610
+ version = minQrVersion(payloadByteLen, ecLevel);
611
+ } catch (err) {
612
+ return {
613
+ ok: false,
614
+ version: 0,
615
+ moduleSizeMm: 0,
616
+ minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,
617
+ message: err instanceof Error ? err.message : String(err)
618
+ };
619
+ }
620
+ const modulesPerSide = qrModuleCount(version) + QUIET_ZONE_MODULES * 2;
621
+ const moduleSizeMm = printedSideMm / modulesPerSide;
622
+ if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {
623
+ return {
624
+ ok: true,
625
+ version,
626
+ moduleSizeMm,
627
+ minModuleSizeMm: MIN_SCANNABLE_MODULE_MM
628
+ };
629
+ }
630
+ return {
631
+ ok: false,
632
+ version,
633
+ moduleSizeMm,
634
+ minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,
635
+ message: `A ${payloadByteLen}-byte payload needs a version ${version} QR, which prints at ${moduleSizeMm.toFixed(3)}mm per module on ${template.stockCode} \u2014 below the ${MIN_SCANNABLE_MODULE_MM}mm floor a scanner can resolve. Shorten the payload (a short resolver URL is the intended fix) or move to larger label stock.`
636
+ };
637
+ }
638
+ function batchScannabilityWarning(t, labels, ecLevel) {
639
+ const encoder = new TextEncoder();
640
+ let worst = -1;
641
+ for (const l of labels) {
642
+ if (!l.qrValue) continue;
643
+ const len = encoder.encode(l.qrValue).length;
644
+ if (len > worst) worst = len;
645
+ }
646
+ if (worst < 0) return void 0;
647
+ const report = assertScannable(t, worst, ecLevel);
648
+ return report.ok ? void 0 : report.message;
649
+ }
650
+ function resolveTemplate(data, variantId) {
651
+ if (variantId && variantId !== "custom") {
652
+ const t = getLabelTemplate(variantId);
653
+ if (t) return t;
654
+ }
655
+ if (data.templateId === "custom" && data.customTemplate)
656
+ return data.customTemplate;
657
+ return getLabelTemplate(data.templateId) ?? data.customTemplate ?? AVERY_5160;
658
+ }
659
+ async function printQrLabelSheet(data, variantId, rawSettings) {
660
+ const t = resolveTemplate(data, variantId);
661
+ const s = resolveSettings(rawSettings);
662
+ if (s.calibrationPage) {
663
+ printCalibrationSheet(t);
664
+ return;
665
+ }
666
+ const labels = expandLabels(data.labels ?? [], s);
667
+ if (!labels.length) {
668
+ openSheetWindow(
669
+ t,
670
+ `<div class="lp-page"><div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:11pt;">No labels to print.</div></div>`,
671
+ "QR labels",
672
+ false
673
+ );
674
+ return;
675
+ }
676
+ const hasBadge = labels.some((l) => (l.badge ?? "").trim() !== "");
677
+ const layout = computeCellLayout(t, s.showCaption, hasBadge);
678
+ const uriByValue = /* @__PURE__ */ new Map();
679
+ for (const l of labels) {
680
+ if (l.qrValue && !uriByValue.has(l.qrValue)) {
681
+ uriByValue.set(
682
+ l.qrValue,
683
+ await generateQrDataUri(l.qrValue, s.ecLevel, layout.qrIn)
684
+ );
685
+ }
686
+ }
687
+ const leadingBlanks = Math.max(0, s.startAtLabel - 1) % (t.cols * t.rows);
688
+ const cells = [
689
+ ...Array.from({ length: leadingBlanks }, () => ({ html: "" })),
690
+ ...labels.map((l) => ({
691
+ html: buildLabelCellHtml(
692
+ l,
693
+ uriByValue.get(l.qrValue) ?? "",
694
+ t,
695
+ layout,
696
+ s
697
+ )
698
+ }))
699
+ ];
700
+ const perPage = t.cols * t.rows;
701
+ const tail = cells.length % perPage;
702
+ if (tail > 0)
703
+ for (let i = 0; i < perPage - tail; i++) cells.push({ html: "" });
704
+ openSheetWindow(
705
+ t,
706
+ renderPages(t, cells),
707
+ "QR labels",
708
+ true,
709
+ batchScannabilityWarning(t, labels, s.ecLevel)
710
+ );
711
+ }
712
+ var qrLabelsPrinter = {
713
+ label: "Print QR label sheet",
714
+ variants: LABEL_TEMPLATES.map((t) => ({
715
+ id: t.id,
716
+ label: t.name,
717
+ description: t.stockCode
718
+ })),
719
+ settings: [
720
+ {
721
+ type: "number",
722
+ id: "startAtLabel",
723
+ label: "Start at label #",
724
+ description: "Skip already-used positions on a partial sheet",
725
+ defaultValue: 1,
726
+ min: 1
727
+ },
728
+ {
729
+ type: "number",
730
+ id: "copiesPerCode",
731
+ label: "Copies per code",
732
+ defaultValue: 1,
733
+ min: 1,
734
+ max: 999
735
+ },
736
+ {
737
+ type: "boolean",
738
+ id: "showCaption",
739
+ label: "Show caption",
740
+ description: "Print the caption (or the code value) beside each QR",
741
+ defaultValue: true
742
+ },
743
+ {
744
+ type: "boolean",
745
+ id: "showLines",
746
+ label: "Show detail lines",
747
+ description: "Extra text lines on wide labels",
748
+ defaultValue: true
749
+ },
750
+ {
751
+ type: "range",
752
+ id: "labelRange",
753
+ label: "Label range",
754
+ description: "Print only labels N to M (blank = all)",
755
+ fromId: "rangeFrom",
756
+ toId: "rangeTo",
757
+ defaultFrom: 0,
758
+ defaultTo: 0
759
+ },
760
+ {
761
+ type: "select",
762
+ id: "ecLevel",
763
+ label: "Error correction",
764
+ description: "Higher survives more damage; M is the warehouse default",
765
+ options: [
766
+ { value: "L", label: "L" },
767
+ { value: "M", label: "M" },
768
+ { value: "Q", label: "Q" }
769
+ ],
770
+ defaultValue: "M"
771
+ },
772
+ {
773
+ type: "boolean",
774
+ id: "calibrationPage",
775
+ label: "Calibration test page",
776
+ description: "Print outlines + crop marks only to verify alignment",
777
+ defaultValue: false
778
+ }
779
+ ],
780
+ print: (data, variantId, settings) => printQrLabelSheet(data, variantId, settings)
781
+ };
782
+
783
+ // src/react.tsx
784
+ import { jsx, jsxs } from "react/jsx-runtime";
785
+ function cn(...values) {
786
+ return twMerge(values.filter(Boolean).join(" "));
787
+ }
788
+ function LucideIcon({
789
+ lucideClass,
790
+ className,
791
+ children,
792
+ ...props
793
+ }) {
794
+ return /* @__PURE__ */ jsx(
795
+ "svg",
796
+ {
797
+ xmlns: "http://www.w3.org/2000/svg",
798
+ width: 24,
799
+ height: 24,
800
+ viewBox: "0 0 24 24",
801
+ fill: "none",
802
+ stroke: "currentColor",
803
+ strokeWidth: 2,
804
+ strokeLinecap: "round",
805
+ strokeLinejoin: "round",
806
+ className: cn(lucideClass, className),
807
+ "aria-hidden": "true",
808
+ ...props,
809
+ children
810
+ }
811
+ );
812
+ }
813
+ function Printer(props) {
814
+ return /* @__PURE__ */ jsxs(LucideIcon, { lucideClass: "lucide lucide-printer", ...props, children: [
815
+ /* @__PURE__ */ jsx("path", { d: "M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" }),
816
+ /* @__PURE__ */ jsx("path", { d: "M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6" }),
817
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "14", width: "12", height: "8", rx: "1" })
818
+ ] });
819
+ }
820
+ function X(props) {
821
+ return /* @__PURE__ */ jsxs(LucideIcon, { lucideClass: "lucide lucide-x", ...props, children: [
822
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
823
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
824
+ ] });
825
+ }
826
+ function Settings2(props) {
827
+ return /* @__PURE__ */ jsxs(
828
+ LucideIcon,
829
+ {
830
+ lucideClass: "lucide lucide-settings2 lucide-settings-2",
831
+ ...props,
832
+ children: [
833
+ /* @__PURE__ */ jsx("path", { d: "M14 17H5" }),
834
+ /* @__PURE__ */ jsx("path", { d: "M19 7h-9" }),
835
+ /* @__PURE__ */ jsx("circle", { cx: "17", cy: "17", r: "3" }),
836
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "3" })
837
+ ]
838
+ }
839
+ );
840
+ }
841
+ var MOBILE_BREAKPOINT = 768;
842
+ function useIsMobile() {
843
+ const [isMobile, setIsMobile] = useState(false);
844
+ const [hasMounted, setHasMounted] = useState(false);
845
+ useEffect(() => {
846
+ setHasMounted(true);
847
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
848
+ const onChange = () => {
849
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
850
+ };
851
+ onChange();
852
+ mql.addEventListener("change", onChange);
853
+ return () => mql.removeEventListener("change", onChange);
854
+ }, []);
855
+ if (!hasMounted) {
856
+ return false;
857
+ }
858
+ return isMobile;
859
+ }
860
+ var DialogModalContext = React.createContext(true);
861
+ function Dialog({
862
+ children,
863
+ ...props
864
+ }) {
865
+ const modal = props.modal ?? true;
866
+ return /* @__PURE__ */ jsx(DialogModalContext.Provider, { value: modal, children: /* @__PURE__ */ jsx(DialogPrimitive.Root, { ...props, modal, children }) });
867
+ }
868
+ function DialogOverlay({
869
+ className,
870
+ ...props
871
+ }) {
872
+ return /* @__PURE__ */ jsx(
873
+ DialogPrimitive.Overlay,
874
+ {
875
+ "data-slot": "dialog-overlay",
876
+ className: cn(
877
+ // Preserve readable page context behind the modal; separation comes from a light scrim, not blur.
878
+ "fixed inset-0 z-[10000] bg-black/20 dark:bg-black/30 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
879
+ className
880
+ ),
881
+ ...props
882
+ }
883
+ );
884
+ }
885
+ var DIALOG_DESKTOP_CLASSES = "fixed left-[50%] top-[50%] z-[10000] grid min-w-0 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 overflow-x-clip border bg-background p-6 shadow-lg [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg";
886
+ var DIALOG_MOBILE_SHEET_CLASSES = "fixed inset-x-0 bottom-0 left-0 right-0 top-auto z-[10000] flex min-w-0 flex-col w-full max-w-full max-h-[90dvh] translate-x-0 translate-y-0 gap-4 overflow-x-clip border-t bg-background p-4 pb-safe shadow-lg [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full duration-200 rounded-t-2xl rounded-b-none overflow-y-auto overscroll-contain data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom";
887
+ var DIALOG_MOBILE_SHEET_OVERRIDE = "inset-x-0 bottom-0 left-0 right-0 top-auto translate-x-0 translate-y-0 w-full max-w-full max-h-[90dvh] rounded-b-none rounded-t-2xl overflow-y-auto";
888
+ function DialogContent({
889
+ className,
890
+ children,
891
+ mobileSheet = true,
892
+ ...props
893
+ }) {
894
+ const isMobile = useIsMobile();
895
+ const isModal = React.useContext(DialogModalContext);
896
+ const asSheet = mobileSheet && isMobile;
897
+ return /* @__PURE__ */ jsxs(DialogPrimitive.Portal, { children: [
898
+ isModal ? /* @__PURE__ */ jsx(DialogOverlay, {}) : null,
899
+ /* @__PURE__ */ jsxs(
900
+ DialogPrimitive.Content,
901
+ {
902
+ "aria-modal": isModal || void 0,
903
+ className: cn(
904
+ asSheet ? DIALOG_MOBILE_SHEET_CLASSES : DIALOG_DESKTOP_CLASSES,
905
+ className,
906
+ asSheet && DIALOG_MOBILE_SHEET_OVERRIDE,
907
+ // A non-modal Dialog stays below the WindowPanel manager's z=1000 floor.
908
+ !isModal && "z-[900]"
909
+ ),
910
+ ...props,
911
+ children: [
912
+ children,
913
+ /* @__PURE__ */ jsxs(
914
+ DialogPrimitive.Close,
915
+ {
916
+ "data-slot": "dialog-close",
917
+ className: "absolute right-2 top-4 flex h-10 w-10 items-center justify-center rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground sm:right-4",
918
+ children: [
919
+ /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
920
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
921
+ ]
922
+ }
923
+ )
924
+ ]
925
+ }
926
+ )
927
+ ] });
928
+ }
929
+ function DialogHeader({
930
+ className,
931
+ ...props
932
+ }) {
933
+ return /* @__PURE__ */ jsx(
934
+ "div",
935
+ {
936
+ "data-slot": "dialog-header",
937
+ className: cn(
938
+ // DialogContent owns an absolute close control. Reserve its hit area in
939
+ // every header so trailing actions never render underneath it.
940
+ "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
941
+ className
942
+ ),
943
+ ...props
944
+ }
945
+ );
946
+ }
947
+ function DialogTitle({
948
+ className,
949
+ ...props
950
+ }) {
951
+ return /* @__PURE__ */ jsx(
952
+ DialogPrimitive.Title,
953
+ {
954
+ className: cn(
955
+ "text-lg font-semibold leading-none tracking-tight",
956
+ className
957
+ ),
958
+ ...props
959
+ }
960
+ );
961
+ }
962
+ function DialogDescription({
963
+ className,
964
+ ...props
965
+ }) {
966
+ return /* @__PURE__ */ jsx(
967
+ DialogPrimitive.Description,
968
+ {
969
+ className: cn("text-sm text-muted-foreground", className),
970
+ ...props
971
+ }
972
+ );
973
+ }
974
+ function Drawer({
975
+ children,
976
+ shouldScaleBackground = true,
977
+ ...props
978
+ }) {
979
+ return /* @__PURE__ */ jsx(
980
+ DrawerPrimitive.Root,
981
+ {
982
+ modal: true,
983
+ shouldScaleBackground,
984
+ ...props,
985
+ children
986
+ }
987
+ );
988
+ }
989
+ function DrawerOverlay({
990
+ className,
991
+ ...props
992
+ }) {
993
+ return /* @__PURE__ */ jsx(
994
+ DrawerPrimitive.Overlay,
995
+ {
996
+ className: cn("fixed inset-0 z-50 bg-black/80", className),
997
+ ...props
998
+ }
999
+ );
1000
+ }
1001
+ function DrawerContent({
1002
+ className,
1003
+ children,
1004
+ ...props
1005
+ }) {
1006
+ return /* @__PURE__ */ jsxs(DrawerPrimitive.Portal, { children: [
1007
+ /* @__PURE__ */ jsx(DrawerOverlay, {}),
1008
+ /* @__PURE__ */ jsxs(
1009
+ DrawerPrimitive.Content,
1010
+ {
1011
+ "aria-modal": "true",
1012
+ className: cn(
1013
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background pb-safe",
1014
+ className
1015
+ ),
1016
+ "aria-describedby": void 0,
1017
+ ...props,
1018
+ children: [
1019
+ /* @__PURE__ */ jsx("div", { className: "mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" }),
1020
+ children
1021
+ ]
1022
+ }
1023
+ )
1024
+ ] });
1025
+ }
1026
+ function DrawerHeader({
1027
+ className,
1028
+ ...props
1029
+ }) {
1030
+ return /* @__PURE__ */ jsx(
1031
+ "div",
1032
+ {
1033
+ className: cn("grid gap-1.5 p-4 text-center sm:text-left", className),
1034
+ ...props
1035
+ }
1036
+ );
1037
+ }
1038
+ function DrawerTitle({
1039
+ className,
1040
+ ...props
1041
+ }) {
1042
+ return /* @__PURE__ */ jsx(
1043
+ DrawerPrimitive.Title,
1044
+ {
1045
+ className: cn(
1046
+ "text-lg font-semibold leading-none tracking-tight",
1047
+ className
1048
+ ),
1049
+ ...props
1050
+ }
1051
+ );
1052
+ }
1053
+ var BUTTON_BASE = "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
1054
+ var BUTTON_VARIANTS = {
1055
+ default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
1056
+ ghost: "hover:bg-accent hover:text-accent-foreground"
1057
+ };
1058
+ var BUTTON_SIZES = {
1059
+ default: "h-9 px-4 py-2",
1060
+ sm: "h-8 rounded-md px-3 text-xs",
1061
+ icon: "h-9 w-9"
1062
+ };
1063
+ function PrintDialogButton({
1064
+ className,
1065
+ variant = "default",
1066
+ size = "default",
1067
+ ...props
1068
+ }) {
1069
+ return /* @__PURE__ */ jsx(
1070
+ "button",
1071
+ {
1072
+ className: cn(
1073
+ BUTTON_BASE,
1074
+ BUTTON_VARIANTS[variant],
1075
+ BUTTON_SIZES[size],
1076
+ className
1077
+ ),
1078
+ ...props
1079
+ }
1080
+ );
1081
+ }
1082
+ function buildDefaultSettings(settings) {
1083
+ const out = {};
1084
+ for (const s of settings) {
1085
+ if (s.type === "boolean") {
1086
+ out[s.id] = s.defaultValue;
1087
+ } else if (s.type === "range") {
1088
+ out[s.fromId] = s.defaultFrom;
1089
+ out[s.toId] = s.defaultTo;
1090
+ } else if (s.type === "number" || s.type === "select") {
1091
+ out[s.id] = s.defaultValue;
1092
+ }
1093
+ }
1094
+ return out;
1095
+ }
1096
+ function visibleSettings(settings, variantId) {
1097
+ return settings.filter(
1098
+ (s) => !s.appliesTo || s.appliesTo.includes(variantId)
1099
+ );
1100
+ }
1101
+ function PrintOptionsContent({
1102
+ printer,
1103
+ data,
1104
+ onClose
1105
+ }) {
1106
+ const [selectedVariant, setSelectedVariant] = useState(
1107
+ printer.variants[0]?.id ?? ""
1108
+ );
1109
+ const [isPrinting, setIsPrinting] = useState(false);
1110
+ const allSettings = printer.settings ?? [];
1111
+ const [settingValues, setSettingValues] = useState(
1112
+ () => buildDefaultSettings(allSettings)
1113
+ );
1114
+ const currentSettings = useMemo(
1115
+ () => visibleSettings(allSettings, selectedVariant),
1116
+ [allSettings, selectedVariant]
1117
+ );
1118
+ const toggleSetting = (id) => {
1119
+ setSettingValues((prev) => ({ ...prev, [id]: !prev[id] }));
1120
+ };
1121
+ const setNumberSetting = (id, value) => {
1122
+ setSettingValues((prev) => ({ ...prev, [id]: value }));
1123
+ };
1124
+ const handlePrint = async (variantId, useDefaults = false) => {
1125
+ setIsPrinting(true);
1126
+ try {
1127
+ const settings = useDefaults ? buildDefaultSettings(allSettings) : settingValues;
1128
+ await printer.print(data, variantId, settings);
1129
+ } finally {
1130
+ setIsPrinting(false);
1131
+ onClose();
1132
+ }
1133
+ };
1134
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 flex-1", children: [
1135
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto min-h-0 py-2 space-y-4", children: [
1136
+ printer.variants.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
1137
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground mb-2 px-0.5", children: "Print format" }),
1138
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: printer.variants.map((variant) => /* @__PURE__ */ jsxs(
1139
+ "button",
1140
+ {
1141
+ onClick: () => setSelectedVariant(variant.id),
1142
+ className: `w-full flex flex-col items-start px-3 py-2.5 rounded-lg border text-left transition-colors ${selectedVariant === variant.id ? "border-primary bg-primary/5 text-primary" : "border-border bg-background hover:bg-accent"}`,
1143
+ children: [
1144
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: variant.label }),
1145
+ variant.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mt-0.5", children: variant.description })
1146
+ ]
1147
+ },
1148
+ variant.id
1149
+ )) })
1150
+ ] }),
1151
+ currentSettings.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
1152
+ /* @__PURE__ */ jsxs("p", { className: "text-sm font-medium text-foreground mb-2 px-0.5 flex items-center gap-1.5", children: [
1153
+ /* @__PURE__ */ jsx(Settings2, { className: "w-3.5 h-3.5" }),
1154
+ "Options"
1155
+ ] }),
1156
+ /* @__PURE__ */ jsx("div", { className: "space-y-1", children: currentSettings.map((setting) => {
1157
+ if (setting.type === "boolean") {
1158
+ const checked = settingValues[setting.id] === true;
1159
+ return /* @__PURE__ */ jsxs(
1160
+ "button",
1161
+ {
1162
+ onClick: () => toggleSetting(setting.id),
1163
+ className: "w-full flex items-start gap-3 px-3 py-2.5 rounded-lg border border-border bg-background hover:bg-accent text-left transition-colors",
1164
+ children: [
1165
+ /* @__PURE__ */ jsx(
1166
+ "span",
1167
+ {
1168
+ className: `mt-0.5 shrink-0 w-4 h-4 rounded border-2 flex items-center justify-center transition-colors ${checked ? "bg-primary border-primary" : "border-muted-foreground/40"}`,
1169
+ children: checked && /* @__PURE__ */ jsx(
1170
+ "svg",
1171
+ {
1172
+ className: "w-2.5 h-2.5 text-primary-foreground",
1173
+ viewBox: "0 0 10 10",
1174
+ fill: "none",
1175
+ children: /* @__PURE__ */ jsx(
1176
+ "path",
1177
+ {
1178
+ d: "M1.5 5l2.5 2.5 4.5-4.5",
1179
+ stroke: "currentColor",
1180
+ strokeWidth: "1.8",
1181
+ strokeLinecap: "round",
1182
+ strokeLinejoin: "round"
1183
+ }
1184
+ )
1185
+ }
1186
+ )
1187
+ }
1188
+ ),
1189
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col min-w-0", children: [
1190
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: setting.label }),
1191
+ setting.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mt-0.5", children: setting.description })
1192
+ ] })
1193
+ ]
1194
+ },
1195
+ setting.id
1196
+ );
1197
+ }
1198
+ if (setting.type === "range") {
1199
+ const min = setting.min ?? 0;
1200
+ const fromVal = settingValues[setting.fromId] ?? 0;
1201
+ const toVal = settingValues[setting.toId] ?? 0;
1202
+ const parse = (raw) => {
1203
+ if (raw === "") return 0;
1204
+ const n = parseInt(raw, 10);
1205
+ return Number.isNaN(n) ? 0 : Math.max(min, n);
1206
+ };
1207
+ return /* @__PURE__ */ jsxs(
1208
+ "div",
1209
+ {
1210
+ className: "w-full flex items-start gap-3 px-3 py-2.5 rounded-lg border border-border bg-background",
1211
+ children: [
1212
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col min-w-0 flex-1", children: [
1213
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: setting.label }),
1214
+ setting.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mt-0.5", children: setting.description })
1215
+ ] }),
1216
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 shrink-0 pt-0.5", children: [
1217
+ /* @__PURE__ */ jsx(
1218
+ "input",
1219
+ {
1220
+ type: "number",
1221
+ inputMode: "numeric",
1222
+ min,
1223
+ value: fromVal === 0 ? "" : fromVal,
1224
+ onChange: (e) => setNumberSetting(
1225
+ setting.fromId,
1226
+ parse(e.target.value)
1227
+ ),
1228
+ placeholder: setting.fromPlaceholder ?? "start",
1229
+ "aria-label": `${setting.label} \u2014 from`,
1230
+ className: "w-14 h-7 text-xs text-center rounded border border-input bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
1231
+ }
1232
+ ),
1233
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "to" }),
1234
+ /* @__PURE__ */ jsx(
1235
+ "input",
1236
+ {
1237
+ type: "number",
1238
+ inputMode: "numeric",
1239
+ min,
1240
+ value: toVal === 0 ? "" : toVal,
1241
+ onChange: (e) => setNumberSetting(
1242
+ setting.toId,
1243
+ parse(e.target.value)
1244
+ ),
1245
+ placeholder: setting.toPlaceholder ?? "end",
1246
+ "aria-label": `${setting.label} \u2014 to`,
1247
+ className: "w-14 h-7 text-xs text-center rounded border border-input bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
1248
+ }
1249
+ )
1250
+ ] })
1251
+ ]
1252
+ },
1253
+ setting.id
1254
+ );
1255
+ }
1256
+ if (setting.type === "number") {
1257
+ const min = setting.min ?? 0;
1258
+ const val = settingValues[setting.id] ?? setting.defaultValue;
1259
+ const parse = (raw) => {
1260
+ if (raw === "") return setting.defaultValue;
1261
+ const n = parseInt(raw, 10);
1262
+ if (Number.isNaN(n)) return setting.defaultValue;
1263
+ const bounded = Math.max(min, n);
1264
+ return setting.max !== void 0 ? Math.min(setting.max, bounded) : bounded;
1265
+ };
1266
+ return /* @__PURE__ */ jsxs(
1267
+ "div",
1268
+ {
1269
+ className: "w-full flex items-start gap-3 px-3 py-2.5 rounded-lg border border-border bg-background",
1270
+ children: [
1271
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col min-w-0 flex-1", children: [
1272
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: setting.label }),
1273
+ setting.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mt-0.5", children: setting.description })
1274
+ ] }),
1275
+ /* @__PURE__ */ jsx(
1276
+ "input",
1277
+ {
1278
+ type: "number",
1279
+ inputMode: "numeric",
1280
+ min,
1281
+ max: setting.max,
1282
+ value: val,
1283
+ onChange: (e) => setNumberSetting(setting.id, parse(e.target.value)),
1284
+ "aria-label": setting.label,
1285
+ className: "w-16 h-7 text-xs text-center rounded border border-input bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-ring shrink-0 mt-0.5"
1286
+ }
1287
+ )
1288
+ ]
1289
+ },
1290
+ setting.id
1291
+ );
1292
+ }
1293
+ if (setting.type === "select") {
1294
+ const val = settingValues[setting.id] ?? setting.defaultValue;
1295
+ return /* @__PURE__ */ jsxs(
1296
+ "div",
1297
+ {
1298
+ className: "w-full flex items-start gap-3 px-3 py-2.5 rounded-lg border border-border bg-background",
1299
+ children: [
1300
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col min-w-0 flex-1", children: [
1301
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: setting.label }),
1302
+ setting.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mt-0.5", children: setting.description })
1303
+ ] }),
1304
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5 shrink-0 rounded-md border border-input p-0.5 mt-0.5", children: setting.options.map((opt) => /* @__PURE__ */ jsx(
1305
+ "button",
1306
+ {
1307
+ onClick: () => setSettingValues((prev) => ({
1308
+ ...prev,
1309
+ [setting.id]: opt.value
1310
+ })),
1311
+ className: `px-2 h-6 text-xs rounded transition-colors ${val === opt.value ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-accent"}`,
1312
+ children: opt.label
1313
+ },
1314
+ opt.value
1315
+ )) })
1316
+ ]
1317
+ },
1318
+ setting.id
1319
+ );
1320
+ }
1321
+ return null;
1322
+ }) })
1323
+ ] })
1324
+ ] }),
1325
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pt-3 pb-1 border-t border-border mt-2 shrink-0", children: [
1326
+ /* @__PURE__ */ jsxs(
1327
+ PrintDialogButton,
1328
+ {
1329
+ onClick: () => handlePrint(selectedVariant || void 0),
1330
+ disabled: isPrinting,
1331
+ className: "w-full",
1332
+ children: [
1333
+ /* @__PURE__ */ jsx(Printer, { className: "w-4 h-4 mr-2" }),
1334
+ isPrinting ? "Opening print view..." : "Print / Save PDF"
1335
+ ]
1336
+ }
1337
+ ),
1338
+ printer.variants.length > 0 && /* @__PURE__ */ jsx(
1339
+ PrintDialogButton,
1340
+ {
1341
+ variant: "ghost",
1342
+ size: "sm",
1343
+ onClick: () => handlePrint(void 0, true),
1344
+ disabled: isPrinting,
1345
+ className: "w-full text-muted-foreground text-xs",
1346
+ children: "Print with defaults"
1347
+ }
1348
+ )
1349
+ ] })
1350
+ ] });
1351
+ }
1352
+ function PrintOptionsDialog({
1353
+ printer,
1354
+ data,
1355
+ open,
1356
+ onOpenChange
1357
+ }) {
1358
+ const isMobile = useIsMobile();
1359
+ const handleClose = () => onOpenChange(false);
1360
+ if (isMobile) {
1361
+ return /* @__PURE__ */ jsx(Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DrawerContent, { className: "flex flex-col max-h-[85dvh]", children: [
1362
+ /* @__PURE__ */ jsxs(DrawerHeader, { className: "flex items-center justify-between pb-2 shrink-0", children: [
1363
+ /* @__PURE__ */ jsxs(DrawerTitle, { className: "flex items-center gap-2", children: [
1364
+ /* @__PURE__ */ jsx(Printer, { className: "w-4 h-4" }),
1365
+ printer.label
1366
+ ] }),
1367
+ /* @__PURE__ */ jsx(
1368
+ PrintDialogButton,
1369
+ {
1370
+ variant: "ghost",
1371
+ size: "icon",
1372
+ onClick: handleClose,
1373
+ className: "h-7 w-7",
1374
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
1375
+ }
1376
+ )
1377
+ ] }),
1378
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 flex-1 overflow-hidden px-4 pb-4", children: /* @__PURE__ */ jsx(
1379
+ PrintOptionsContent,
1380
+ {
1381
+ printer,
1382
+ data,
1383
+ onClose: handleClose
1384
+ }
1385
+ ) })
1386
+ ] }) });
1387
+ }
1388
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm flex flex-col max-h-[85dvh]", children: [
1389
+ /* @__PURE__ */ jsxs(DialogHeader, { className: "shrink-0", children: [
1390
+ /* @__PURE__ */ jsxs(DialogTitle, { className: "flex items-center gap-2", children: [
1391
+ /* @__PURE__ */ jsx(Printer, { className: "w-4 h-4" }),
1392
+ printer.label
1393
+ ] }),
1394
+ /* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: "Choose a print format and options for this content." })
1395
+ ] }),
1396
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx(
1397
+ PrintOptionsContent,
1398
+ {
1399
+ printer,
1400
+ data,
1401
+ onClose: handleClose
1402
+ }
1403
+ ) })
1404
+ ] }) });
1405
+ }
1406
+ function usePrintOptions(printer, data) {
1407
+ const [open, setOpen] = useState(false);
1408
+ const triggerPrint = async () => {
1409
+ if (!printer) return;
1410
+ const hasOptions = printer.variants.length > 0 || (printer.settings?.length ?? 0) > 0;
1411
+ if (!hasOptions) {
1412
+ await printer.print(data);
1413
+ } else {
1414
+ setOpen(true);
1415
+ }
1416
+ };
1417
+ return { open, setOpen, triggerPrint };
1418
+ }
1419
+ function LabelSheetPreview({
1420
+ template: t,
1421
+ labels,
1422
+ startAtLabel = 1,
1423
+ showCaption = true,
1424
+ ecLevel = "M",
1425
+ calibration = false,
1426
+ pageIndex = 0,
1427
+ className
1428
+ }) {
1429
+ const [uris, setUris] = useState(/* @__PURE__ */ new Map());
1430
+ const perPage = t.cols * t.rows;
1431
+ const leadingBlanks = Math.max(0, startAtLabel - 1) % perPage;
1432
+ const pageStart = pageIndex * perPage - leadingBlanks;
1433
+ const layout = computeCellLayout(t, showCaption);
1434
+ const cells = Array.from({ length: perPage }, (_, i) => {
1435
+ const li = pageStart + i;
1436
+ return li >= 0 && li < labels.length ? labels[li] ?? null : null;
1437
+ });
1438
+ const valuesKey = Array.from(
1439
+ new Set(
1440
+ cells.filter((c) => c !== null).map((c) => c.qrValue)
1441
+ )
1442
+ ).join("\0");
1443
+ const qrIn = layout.qrIn;
1444
+ useEffect(() => {
1445
+ if (calibration) return;
1446
+ let cancelled = false;
1447
+ const values = valuesKey ? valuesKey.split("\0") : [];
1448
+ void (async () => {
1449
+ const next = /* @__PURE__ */ new Map();
1450
+ for (const v of values) {
1451
+ try {
1452
+ next.set(v, await generateQrDataUri(v, ecLevel, qrIn));
1453
+ } catch (err) {
1454
+ console.error("[LabelSheetPreview] QR generation failed:", err);
1455
+ }
1456
+ }
1457
+ if (!cancelled) setUris(next);
1458
+ })();
1459
+ return () => {
1460
+ cancelled = true;
1461
+ };
1462
+ }, [calibration, ecLevel, qrIn, valuesKey]);
1463
+ const pct = (v, base) => `${v / base * 100}%`;
1464
+ const radiusPct = t.round ? "50%" : t.cornerRadiusIn ? `${t.cornerRadiusIn / t.labelWIn * 100}% / ${t.cornerRadiusIn / t.labelHIn * 100}%` : "0";
1465
+ return /* @__PURE__ */ jsx(
1466
+ "div",
1467
+ {
1468
+ className: `relative w-full bg-white border border-border rounded-md shadow-sm overflow-hidden ${className ?? ""}`,
1469
+ style: { aspectRatio: `${t.sheetWIn} / ${t.sheetHIn}` },
1470
+ "aria-label": `Label sheet preview \u2014 ${t.stockCode}`,
1471
+ children: cells.map((label, i) => {
1472
+ const col = i % t.cols;
1473
+ const row = Math.floor(i / t.cols);
1474
+ const x = t.marginLeftIn + col * (t.labelWIn + t.gutterXIn);
1475
+ const y = t.marginTopIn + row * (t.labelHIn + t.gutterYIn);
1476
+ const uri = label ? uris.get(label.qrValue) : void 0;
1477
+ const wide = layout.wide;
1478
+ return /* @__PURE__ */ jsx(
1479
+ "div",
1480
+ {
1481
+ className: calibration || !label ? "absolute border border-dashed border-slate-300" : "absolute border border-slate-200",
1482
+ style: {
1483
+ left: pct(x, t.sheetWIn),
1484
+ top: pct(y, t.sheetHIn),
1485
+ width: pct(t.labelWIn, t.sheetWIn),
1486
+ height: pct(t.labelHIn, t.sheetHIn),
1487
+ borderRadius: radiusPct,
1488
+ overflow: "hidden"
1489
+ },
1490
+ children: calibration ? /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center text-[8px] text-slate-400", children: i + 1 }) : label ? /* @__PURE__ */ jsxs(
1491
+ "div",
1492
+ {
1493
+ className: `w-full h-full flex ${wide ? "flex-row items-center" : "flex-col items-center justify-center"} gap-[2%] p-[3%]`,
1494
+ children: [
1495
+ uri ? /* @__PURE__ */ jsx(
1496
+ "img",
1497
+ {
1498
+ src: uri,
1499
+ alt: "",
1500
+ className: "block",
1501
+ style: wide ? { height: "88%", aspectRatio: "1" } : {
1502
+ width: `${layout.qrIn / t.labelWIn * 100}%`,
1503
+ aspectRatio: "1"
1504
+ }
1505
+ }
1506
+ ) : /* @__PURE__ */ jsx(
1507
+ "div",
1508
+ {
1509
+ className: "bg-slate-100 animate-pulse rounded-sm",
1510
+ style: wide ? { height: "88%", aspectRatio: "1" } : { width: "70%", aspectRatio: "1" }
1511
+ }
1512
+ ),
1513
+ /* @__PURE__ */ jsxs(
1514
+ "div",
1515
+ {
1516
+ className: `min-w-0 overflow-hidden text-black leading-tight ${wide ? "flex-1 text-left" : "text-center w-full"}`,
1517
+ style: { fontSize: "min(1.4vw, 10px)" },
1518
+ children: [
1519
+ showCaption && /* @__PURE__ */ jsx("div", { className: "font-bold truncate", children: label.caption ?? label.qrValue }),
1520
+ wide && label.lines?.filter((l) => l.trim()).map((l, li) => /* @__PURE__ */ jsx("div", { className: "truncate text-slate-600", children: l }, li))
1521
+ ]
1522
+ }
1523
+ )
1524
+ ]
1525
+ }
1526
+ ) : null
1527
+ },
1528
+ i
1529
+ );
1530
+ })
1531
+ }
1532
+ );
1533
+ }
1534
+ export {
1535
+ LabelSheetPreview,
1536
+ PrintOptionsDialog,
1537
+ usePrintOptions
1538
+ };
1539
+ //# sourceMappingURL=react.js.map