@ai-matrx/print 0.8.69 → 0.8.70

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/zpl.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts","../src/katex-css.generated.ts","../src/labels.ts","../src/qr.ts","../src/zpl.ts"],"sourcesContent":["/**\n * @ai-matrx/print/core — the Block Print contract & print-window composition.\n *\n * Ported VERBATIM from matrx-frontend `lib/block-print/block-print-utils.ts`\n * (2026-08-29 state, including the `number` and `select` PrintSetting types the\n * commerce label lane added). Zero dependencies; touches `window` only at call\n * time, so the module stays import-inert and RSC-safe.\n *\n * BlockPrinter is the only interface a host system needs to know about.\n * Each printable unit exports its own BlockPrinter alongside its render\n * component; host code only ever calls print(data, variantId, settings).\n *\n * `buildFitTextScript` is the ONE auto-shrink script (the flashcards\n * FIT_TEXT pattern, parameterized by selector so the label lane shares it).\n */\n\n// THE ONE HTML escaper (duplication census row 14). Nine copies of\n// `escapeHtml` were live across the fleet and THEY DID NOT ESCAPE THE SAME\n// CHARACTERS — a string safe through one was an attribute break-out through\n// another. It moved to `@ai-matrx/kit/html-escape` with a decided character\n// set: an HTML escaper must not require this package's jspdf + html2canvas\n// graph. Not re-exported from here; import `@ai-matrx/kit/html-escape`.\nimport { downloadFile } from \"@ai-matrx/kit/download\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { KATEX_CSS, KATEX_CSS_MARKER } from \"./katex-css.generated\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\nexport interface PrintVariant {\n id: string;\n label: string;\n description?: string;\n}\n\n/**\n * A single user-configurable setting shown in the print dialog.\n * Extend the discriminated union for new control types\n * without breaking existing printers.\n */\nexport type PrintSetting =\n | {\n type: \"boolean\";\n id: string;\n label: string;\n description?: string;\n /** Default value shown when the dialog first opens */\n defaultValue: boolean;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /**\n * A numeric \"from → to\" range control, rendered as two small number inputs.\n * Writes two entries into the resolved PrintSettings map — one under fromId,\n * one under toId. Printers interpret 0 (or missing) as \"no bound\" by convention.\n */\n type: \"range\";\n /** Stable id used as React key — not written to the settings map */\n id: string;\n label: string;\n description?: string;\n /** Settings map key for the lower bound value */\n fromId: string;\n /** Settings map key for the upper bound value */\n toId: string;\n defaultFrom: number;\n defaultTo: number;\n /** Minimum accepted value for each input (inclusive). Default 0. */\n min?: number;\n /** Placeholder shown when the input is empty (0). Default \"start\" / \"end\". */\n fromPlaceholder?: string;\n toPlaceholder?: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A single numeric input (e.g. \"start at label #\", \"copies\"). */\n type: \"number\";\n id: string;\n label: string;\n description?: string;\n defaultValue: number;\n /** Minimum accepted value (inclusive). Default 0. */\n min?: number;\n /** Maximum accepted value (inclusive). Omit = unbounded. */\n max?: number;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A one-of-N choice rendered as a compact segmented row. */\n type: \"select\";\n id: string;\n label: string;\n description?: string;\n options: { value: string; label: string }[];\n defaultValue: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n };\n\n/** A resolved map of setting id → current value, passed to print(). */\nexport type PrintSettings = Record<string, boolean | string | number>;\n\n/**\n * The self-contained print capability that each block can export.\n * Host code only ever calls print(data, variantId, settings) — it never inspects internals.\n */\nexport interface BlockPrinter {\n /** Tooltip / aria-label for the print button in the block header */\n label: string;\n\n /**\n * Variants offered to the user before printing.\n * Empty array = no options dialog shown; prints immediately with defaults.\n */\n variants: PrintVariant[];\n\n /**\n * Optional user-configurable settings shown below the variant picker.\n * Omit entirely if the block has no configurable settings.\n */\n settings?: PrintSetting[];\n\n /**\n * Execute the print.\n * data — the block's structured data (serverData or parsed content)\n * variantId — which variant was chosen (undefined = default / no variants)\n * settings — resolved setting values chosen by the user\n *\n * MAY resolve to a PrintOutcome (\"opened\" | \"downloaded\") so the host can\n * tell the user when the popup-blocked download fallback fired; `void`\n * stays legal for printers that don't go through a print window.\n */\n print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | PrintOutcome | Promise<void | PrintOutcome>;\n}\n\n// ─── Shared Utilities ─────────────────────────────────────────────────────────\n\nconst PRINT_STYLES = `\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n font-size: 11pt;\n line-height: 1.6;\n color: #1a1a1a;\n background: #fff;\n max-width: 860px;\n margin: 0 auto;\n padding: 28px 36px;\n }\n\n .print-actions {\n display: flex;\n gap: 10px;\n margin-bottom: 24px;\n padding-bottom: 18px;\n border-bottom: 2px solid #e5e7eb;\n }\n\n .print-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n border: none;\n font-weight: 600;\n }\n\n .print-btn-primary { background: #2563eb; color: #fff; }\n .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }\n\n h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }\n h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }\n h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }\n p { margin-bottom: 0.7em; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n font-size: 10pt;\n }\n th {\n background: #1e293b;\n color: #f8fafc;\n font-weight: 600;\n text-align: left;\n padding: 7px 12px;\n border: 1px solid #334155;\n }\n td {\n padding: 6px 12px;\n border: 1px solid #e2e8f0;\n vertical-align: top;\n }\n tr:nth-child(even) td { background: #f8fafc; }\n\n @media print {\n body { max-width: 100%; padding: 0; }\n .print-actions { display: none !important; }\n th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n }\n`;\n\n/**\n * Wrap body HTML in a complete, styled print document.\n */\nexport function buildPrintDocument(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${escapeHtml(title)}</title>\n <style>\n${PRINT_STYLES}\n${extraStyles}\n </style>\n</head>\n<body>\n <div class=\"print-actions\">\n <button class=\"print-btn print-btn-primary\" onclick=\"window.print()\">Print / Save as PDF</button>\n <button class=\"print-btn print-btn-secondary\" onclick=\"window.close()\">Close</button>\n </div>\n <div class=\"content\">\n${bodyHtml}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * How a print request actually reached the user.\n *\n * \"opened\" — a print window/tab was opened and the document written into it.\n * \"downloaded\" — the popup was blocked, so the document was downloaded as an\n * .html file instead (the contract's fallback). Hosts SHOULD\n * surface this to the user (e.g. a toast saying the print file\n * was downloaded) — a blocked popup plus a silent download is\n * indistinguishable from \"nothing happened\", which is exactly\n * how QA filed it (2026-08-30, F5/F6/F8).\n */\nexport type PrintOutcome = \"opened\" | \"downloaded\";\n\n/**\n * Open a new browser window and write a complete HTML document into it.\n * Falls back to downloading an .html file if the popup is blocked.\n *\n * Returns which path was taken so callers can tell the user when the\n * fallback fired. `windowFeatures` customizes the popup geometry (the\n * flashcards fixed-geometry variants pass a wider window).\n */\n/**\n * Every printed document leaves through here, so this is where typeset math\n * gets its stylesheet: when the page carries KaTeX output and does not already\n * carry the sheet, it is added to <head>. A page with no math is untouched,\n * byte for byte (the flashcard golden master holds every printer to that).\n */\nexport function withMathStyles(htmlDocument: string): string {\n if (!htmlDocument.includes('class=\"katex\"') || htmlDocument.includes(KATEX_CSS_MARKER)) {\n return htmlDocument;\n }\n const style = `<style data-matrx-katex>${KATEX_CSS}</style>`;\n const head = htmlDocument.search(/<\\/head>/i);\n return head >= 0\n ? htmlDocument.slice(0, head) + style + htmlDocument.slice(head)\n : style + htmlDocument;\n}\n\nexport function openPrintWindow(\n rawDocument: string,\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n const win = window.open(\"\", \"_blank\", windowFeatures);\n // Popup blocked — fall back to file download.\n if (!win) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n}\n\n/** A print window opened before its document is ready (see `openPendingPrintWindow`). */\nexport interface PendingPrintWindow {\n /** Write the finished document; downloads it when the popup was blocked or closed. */\n write(htmlDocument: string): PrintOutcome;\n}\n\n/**\n * Open the print window NOW — inside the click, where popup blockers allow it —\n * and write the document later, once async preparation (drawing diagrams) is\n * done. The window says \"Preparing…\" meanwhile.\n */\nexport function openPendingPrintWindow(\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PendingPrintWindow {\n const win = window.open(\"\", \"_blank\", windowFeatures);\n if (win) {\n win.document.open();\n win.document.write(\n '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>Preparing…</title></head><body style=\"font:14px -apple-system,BlinkMacSystemFont,sans-serif;color:#555;padding:2rem\">Preparing the document for print…</body></html>',\n );\n win.document.close();\n }\n return {\n write(rawDocument: string): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n if (!win || win.closed) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n },\n };\n}\n\nfunction openPrintWindowFallback(htmlDocument: string, filename: string): PrintOutcome {\n downloadFile(`${filename.replace(/\\s+/g, \"-\").toLowerCase()}.html`, htmlDocument, \"text/html;charset=utf-8\");\n return \"downloaded\";\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n * Returns the same PrintOutcome as openPrintWindow.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): PrintOutcome {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n return openPrintWindow(doc, title);\n}\n\n/**\n * Build the auto-shrink script that runs INSIDE a printed window: for every\n * element matching each selector, its font-size is reduced (0.5pt steps, 6pt\n * floor) until it fits its parent element's box. This is the flashcards\n * FIT_TEXT behavior verbatim, parameterized by selector so every lane\n * (flashcards card text, Avery text, label captions) shares the ONE loop.\n *\n * Pass the same selectors the original used to get byte-identical behavior:\n * flashcards passes `[\".card-text .card-text-inner\", \".avery-text-wrap .avery-text\"]`.\n */\nexport function buildFitTextScript(selectors: readonly string[]): string {\n const selectorsJson = JSON.stringify([...selectors]);\n return `\n<script>\n(function() {\n function fitText(el, container) {\n // container is the bounding box we must fit inside\n var maxW = container.clientWidth;\n var maxH = container.clientHeight;\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var minSize = 6;\n // Give the element a chance to reflow at current size before measuring\n el.style.fontSize = size + \"pt\";\n while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"pt\";\n }\n }\n function runAll() {\n var selectors = ${selectorsJson};\n for (var s = 0; s < selectors.length; s++) {\n var els = document.querySelectorAll(selectors[s]);\n for (var i = 0; i < els.length; i++) {\n fitText(els[i], els[i].parentElement);\n }\n }\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", runAll);\n } else {\n // document.write pages may already be loaded\n setTimeout(runAll, 0);\n }\n})();\n</script>`;\n}\n\n","// GENERATED by scripts/gen-katex-css.mjs from katex@0.18.9 — do not edit.\nexport const KATEX_VERSION = \"0.18.9\";\n/** Leads KATEX_CSS; lets openPrintWindow see the sheet is already present. */\nexport const KATEX_CSS_MARKER = \"/* matrx-katex@0.18.9 */\";\nexport const KATEX_CSS = \"/* matrx-katex@0.18.9 */@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.ttf) format(\\\"truetype\\\")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:\\\"0.18.9\\\"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:\\\"\\\"}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:\\\"(\\\" counter(katexEqnNo) \\\")\\\";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:\\\"(\\\" counter(mmlEqnNo) \\\")\\\";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}\\n\";\n","/**\n * @ai-matrx/print/labels — the label-sheet engine.\n *\n * ONE module carrying the three matrx-frontend files this lane was extracted\n * from, ported behaviour-verbatim (2026-08-29 state):\n * lib/label-print/label-templates.ts → the template registry\n * lib/label-print/qr-labels-printer.ts → qrLabelsPrinter / printQrLabelSheet\n * lib/label-print/qr-labels-pdf.ts → downloadLabelsPdf\n *\n * Warehouse-grade QR labels on standard 8.5\" × 11\" label stock (Avery and\n * compatibles) AND on continuous/die-cut roll stock. ONE generic grid renderer\n * driven by the LabelTemplate registry — geometry is data, never per-stock CSS.\n *\n * Fidelity rules (the reasons this exists):\n * - The print window is a fresh, unauthenticated document: every QR image is\n * inlined as a data URI, never fetched.\n * - Spec-compliant quiet zone: the `qrcode` lib renders 4 modules of white\n * margin INSIDE each data URI (its default; we pass it explicitly).\n * - `print-color-adjust: exact` on code cells so the browser never \"helps\".\n * - Modules are crisp: each QR is rasterized at 300 dpi for its printed size,\n * and `image-rendering: pixelated` stops resampling blur.\n * - Captions auto-shrink to fit their box (FIT_TEXT pattern) — they never\n * overflow onto the neighboring label.\n * - `startAtLabel` inserts LEADING blank cells so a partially-used sheet can\n * be reused; trailing cells pad out the last page.\n *\n * THE IMAGE RULE — the one deliberate exception to \"data URIs only\": a label's\n * optional `imageUrl` is a DURABLE http(s) URL and is emitted as a plain\n * `<img src>`. Anything that is not http(s) is SKIPPED rather than rendered.\n * The QR itself is never a URL; it is always an inlined data URI.\n *\n * SEAM INVERSIONS versus the matrx-frontend originals (the only changes to the\n * ported behaviour):\n * 1. `@/lib/block-print/block-print-utils` → `./core` (identical export names).\n * 2. The deferred `import(\"qrcode\")` + `QRCode.toDataURL(...)` call is now\n * `generateQrDataUrl` from `./qr`, which performs the same lazy import at\n * call time and maps 1:1 — errorCorrectionLevel → ecLevel, margin →\n * quietZoneModules, width → widthPx. The original also passed\n * `color: { dark: \"#000000\", light: \"#ffffff\" }`, which is byte-for-byte the\n * `qrcode` default, so dropping it changes no output.\n * 3. LP_FIT_SCRIPT is KEPT VERBATIM rather than replaced by core's\n * `buildFitTextScript`. The two scripts are NOT behaviourally identical for\n * label cells: core's steps and writes font sizes in **pt** (and pre-assigns\n * `size + \"pt\"` from a px-derived number before measuring) with a **6pt**\n * floor, while the label script steps and writes **px** with a **4px** floor\n * and no pre-assignment. On a 9pt caption inside a 0.28in box those two\n * produce different final sizes, so swapping them would regress captions on\n * the small Avery stocks.\n *\n * Docs: lib/label-print/FEATURE.md (matrx-frontend), NOTES-CHANGELOG.md.\n */\n\nimport {\n openPrintWindow,\n type BlockPrinter,\n type PrintOutcome,\n type PrintSettings,\n} from \"./core\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { generateQrDataUrl, minQrVersion, qrModuleCount } from \"./qr\";\n\n// ─── Template registry ───────────────────────────────────────────────────────\n\n/**\n * Label-sheet template registry — the geometry source of truth for QR label\n * printing on standard 8.5\" × 11\" label stock (Avery and compatibles) and on\n * roll stock.\n *\n * ONE generic grid renderer is driven entirely by these numbers — never a\n * hand-written CSS block per stock. All dimensions in inches, taken from\n * Avery's published templates:\n * - horizontal pitch = labelWIn + gutterXIn; vertical pitch = labelHIn + gutterYIn\n * - a template is valid when marginLeft*2 + cols*labelW + (cols-1)*gutterX = sheetW\n * (same vertically) — `assertTemplateGeometry` checks this in dev.\n */\nexport interface LabelTemplate {\n id: string;\n name: string;\n /** Stock code as printed on the box (e.g. \"Avery 5160\"). */\n stockCode: string;\n sheetWIn: number;\n sheetHIn: number;\n marginTopIn: number;\n marginLeftIn: number;\n labelWIn: number;\n labelHIn: number;\n cols: number;\n rows: number;\n /** Horizontal gap BETWEEN columns (not the outer margin). */\n gutterXIn: number;\n /** Vertical gap BETWEEN rows. */\n gutterYIn: number;\n /** Corner radius; equal to labelWIn/2 for round labels. */\n cornerRadiusIn?: number;\n /** True round label (die-cut circle) — preview/calibration draw a circle. */\n round?: boolean;\n /**\n * Stock family. Absent means \"sheet\" — a multi-up page of labels. \"roll\" is\n * a continuous/die-cut roll where the sheet IS the label: margins 0, a 1×1\n * grid, and `@page` sized to the single label.\n */\n kind?: \"sheet\" | \"roll\";\n}\n\n/**\n * The registry's first row, hoisted to a named const so the \"no template\n * matched\" fallback can reference it without an unchecked index access.\n *\n * 5160 — 1\" × 2⅝\" address, 30-up (3×10). Top/bottom margin 0.5\",\n * side margin 0.1875\", 0.125\" column gutter, zero row gutter.\n */\nconst AVERY_5160: LabelTemplate = {\n id: \"avery-5160\",\n name: 'Address 1\" × 2⅝\" — 30 per sheet',\n stockCode: \"Avery 5160 / 8160\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.5,\n marginLeftIn: 0.1875,\n labelWIn: 2.625,\n labelHIn: 1,\n cols: 3,\n rows: 10,\n gutterXIn: 0.125,\n gutterYIn: 0,\n cornerRadiusIn: 0.1,\n};\n\n/**\n * Seeded templates. Sheet geometry verified against Avery's published template\n * dimensions (avery.com template pages for each SKU):\n *\n * 5160 — see AVERY_5160 above.\n * 5163 — 2\" × 4\" shipping, 10-up (2×5). Top/bottom 0.5\", side 0.15625\",\n * 0.1875\" column gutter, zero row gutter.\n * 5164 — 3⅓\" × 4\" shipping, 6-up (2×3). Same horizontal grid as 5163.\n * 22806 — 2\" × 2\" rounded square, 12-up (3×4). Margins 0.5\" sides /\n * 0.75\" top, 0.75\" column gutter, 0.5\" row gutter — i.e. Avery's\n * published 2.75\" horizontal pitch and 2.5\" vertical pitch. (The\n * matrx-frontend original had 0.375\"/0.375\" here and tiled only 7.5\"\n * of the 8.5\" sheet; see NOTES-CHANGELOG.md.)\n * 22807 — 2\" round, 12-up (3×4). Same grid as 22806, circular die-cut.\n *\n * Roll stock (`kind: \"roll\"`) — the sheet IS the label, so every roll row is a\n * 1×1 grid with zero margins and zero gutters, and its die-cut size is given\n * as width × height in the feed orientation:\n *\n * DK-1202 — Brother die-cut shipping, 62mm × 100mm = 2.4\" × 3.9\".\n * DK-11201 — Brother die-cut address, 29mm × 90mm = 1.14\" × 3.5\".\n * 30252 — DYMO address, 1⅛\" × 3½\".\n * 4x6 — the generic 4\" × 6\" thermal shipping label.\n */\nexport const LABEL_TEMPLATES: readonly LabelTemplate[] = [\n AVERY_5160,\n {\n id: \"avery-5163\",\n name: 'Shipping 2\" × 4\" — 10 per sheet',\n stockCode: \"Avery 5163 / 8163\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.5,\n marginLeftIn: 0.15625,\n labelWIn: 4,\n labelHIn: 2,\n cols: 2,\n rows: 5,\n gutterXIn: 0.1875,\n gutterYIn: 0,\n cornerRadiusIn: 0.125,\n },\n {\n id: \"avery-5164\",\n name: 'Shipping 3⅓\" × 4\" — 6 per sheet',\n stockCode: \"Avery 5164 / 8164\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.5,\n marginLeftIn: 0.15625,\n labelWIn: 4,\n labelHIn: 3.3333333,\n cols: 2,\n rows: 3,\n gutterXIn: 0.1875,\n gutterYIn: 0,\n cornerRadiusIn: 0.125,\n },\n {\n id: \"avery-22806\",\n name: 'Square 2\" × 2\" — 12 per sheet',\n stockCode: \"Avery 22806\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.75,\n marginLeftIn: 0.5,\n labelWIn: 2,\n labelHIn: 2,\n cols: 3,\n rows: 4,\n gutterXIn: 0.75,\n gutterYIn: 0.5,\n cornerRadiusIn: 0.09375,\n },\n {\n id: \"avery-22807\",\n name: 'Round 2\" — 12 per sheet',\n stockCode: \"Avery 22807\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.75,\n marginLeftIn: 0.5,\n labelWIn: 2,\n labelHIn: 2,\n cols: 3,\n rows: 4,\n gutterXIn: 0.75,\n gutterYIn: 0.5,\n cornerRadiusIn: 1,\n round: true,\n },\n {\n id: \"brother-dk-1202\",\n name: 'Shipping roll 2.4\" × 3.9\" — die-cut',\n stockCode: \"Brother DK-1202\",\n sheetWIn: 2.4,\n sheetHIn: 3.9,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 2.4,\n labelHIn: 3.9,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n {\n id: \"brother-dk-11201\",\n name: 'Address roll 1.14\" × 3.5\" — die-cut',\n stockCode: \"Brother DK-11201\",\n sheetWIn: 1.14,\n sheetHIn: 3.5,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 1.14,\n labelHIn: 3.5,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n {\n id: \"dymo-30252\",\n name: 'Address roll 1⅛\" × 3½\"',\n stockCode: \"DYMO 30252\",\n sheetWIn: 1.125,\n sheetHIn: 3.5,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 1.125,\n labelHIn: 3.5,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n {\n id: \"roll-4x6\",\n name: 'Shipping roll 4\" × 6\"',\n stockCode: \"Thermal 4x6\",\n sheetWIn: 4,\n sheetHIn: 6,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 4,\n labelHIn: 6,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n];\n\nexport function getLabelTemplate(id: string): LabelTemplate | undefined {\n return LABEL_TEMPLATES.find((t) => t.id === id);\n}\n\n/**\n * Build a `custom` template for stock not in the registry. Callers supply the\n * full geometry; id is fixed to \"custom\".\n *\n * The geometry spread comes FIRST so an explicitly-present `name: undefined` /\n * `stockCode: undefined` cannot overwrite the resolved default (see\n * NOTES-CHANGELOG.md).\n */\nexport function customLabelTemplate(\n geometry: Omit<LabelTemplate, \"id\" | \"name\" | \"stockCode\"> &\n Partial<Pick<LabelTemplate, \"name\" | \"stockCode\">>,\n): LabelTemplate {\n return {\n ...geometry,\n id: \"custom\",\n name: geometry.name ?? \"Custom stock\",\n stockCode: geometry.stockCode ?? \"custom\",\n };\n}\n\n/** Dev-time sanity check: the grid must tile the sheet exactly (±1/64\"). */\nexport function assertTemplateGeometry(t: LabelTemplate): void {\n const w =\n t.marginLeftIn * 2 + t.cols * t.labelWIn + (t.cols - 1) * t.gutterXIn;\n const h =\n t.marginTopIn * 2 + t.rows * t.labelHIn + (t.rows - 1) * t.gutterYIn;\n const tol = 1 / 64;\n if (Math.abs(w - t.sheetWIn) > tol || Math.abs(h - t.sheetHIn) > tol) {\n console.error(\n `[label-print] Template ${t.id} does not tile its sheet: grid ${w.toFixed(4)}×${h.toFixed(4)}in vs sheet ${t.sheetWIn}×${t.sheetHIn}in`,\n );\n }\n}\n\n// Pure math, no side effects beyond a console.error on drift — the module\n// stays import-inert. `process` is guarded because this ships as an npm\n// package that runs in browsers, where a bare `process.env` read throws.\nif (\n typeof process === \"undefined\" ||\n process.env.NODE_ENV !== \"production\"\n) {\n LABEL_TEMPLATES.forEach(assertTemplateGeometry);\n}\n\n// ─── Data contract ───────────────────────────────────────────────────────────\n\nexport interface QrLabel {\n /** The value encoded in the QR code (SKU, URL, id…). */\n qrValue: string;\n /** Bold headline under/next to the code. Defaults to qrValue when captions are on. */\n caption?: string;\n /** Additional small text lines (wide labels only — there is no room on tiny stock). */\n lines?: string[];\n /**\n * A single short, LARGE, bold token (a garment size, a bin letter, a grade).\n * Rendered in the caption zone ABOVE the caption at ~2× the caption size and\n * auto-shrunk by the fit script.\n */\n badge?: string;\n /**\n * Durable http(s) image URL rendered in the lines zone with\n * `object-fit: contain`. Anything that is not http(s) is skipped — THE IMAGE\n * RULE. Ignored by the PDF lane, which cannot fetch a remote image.\n */\n imageUrl?: string;\n}\n\nexport interface QrLabelPrintData {\n labels: QrLabel[];\n /** Default template; the dialog's variant picker overrides it. */\n templateId: string;\n /** Optional custom-stock geometry used when templateId === \"custom\". */\n customTemplate?: LabelTemplate;\n}\n\nexport type QrEcLevel = \"L\" | \"M\" | \"Q\";\n\nexport interface QrLabelSettings {\n startAtLabel: number; // 1-based first cell to print on\n copiesPerCode: number;\n showCaption: boolean;\n showLines: boolean;\n rangeFrom: number; // 1-based, 0 = no bound\n rangeTo: number;\n ecLevel: QrEcLevel;\n calibrationPage: boolean;\n}\n\nconst DEFAULTS: QrLabelSettings = {\n startAtLabel: 1,\n copiesPerCode: 1,\n showCaption: true,\n showLines: true,\n rangeFrom: 0,\n rangeTo: 0,\n ecLevel: \"M\",\n calibrationPage: false,\n};\n\nfunction resolveSettings(s?: PrintSettings): QrLabelSettings {\n const num = (v: unknown, d: number, min = 0) =>\n typeof v === \"number\" && !Number.isNaN(v) ? Math.max(min, v) : d;\n const ec = s?.ecLevel;\n return {\n startAtLabel: num(s?.startAtLabel, DEFAULTS.startAtLabel, 1),\n copiesPerCode: num(s?.copiesPerCode, DEFAULTS.copiesPerCode, 1),\n showCaption: (s?.showCaption ?? DEFAULTS.showCaption) as boolean,\n showLines: (s?.showLines ?? DEFAULTS.showLines) as boolean,\n rangeFrom: num(s?.rangeFrom, 0),\n rangeTo: num(s?.rangeTo, 0),\n ecLevel: ec === \"L\" || ec === \"Q\" ? ec : \"M\",\n calibrationPage: (s?.calibrationPage ?? false) as boolean,\n };\n}\n\n// ─── QR generation (data URIs, inlined) ──────────────────────────────────────\n\nconst PRINT_DPI = 300;\n/** Quiet zone: QR spec requires ≥4 modules of clear margin around the symbol. */\nconst QUIET_ZONE_MODULES = 4;\n\nexport async function generateQrDataUri(\n value: string,\n ecLevel: QrEcLevel,\n sizeInches: number,\n): Promise<string> {\n // Deferred to call time (user click) — `./qr` lazy-imports `qrcode` inside\n // generateQrDataUrl, so this module never puts it on a shell graph.\n return generateQrDataUrl(value, {\n ecLevel,\n quietZoneModules: QUIET_ZONE_MODULES,\n widthPx: Math.round(sizeInches * PRINT_DPI),\n });\n}\n\n// ─── Cell geometry ───────────────────────────────────────────────────────────\n\ninterface CellLayout {\n /** true = QR left, text right; false = QR on top, caption below. */\n wide: boolean;\n qrIn: number;\n padIn: number;\n /** Height reserved above the caption for a `badge`; 0 when there is none. */\n badgeZoneIn: number;\n}\n\nexport function computeCellLayout(\n t: LabelTemplate,\n showCaption: boolean,\n hasBadge = false,\n): CellLayout {\n const padIn = Math.min(0.08, t.labelHIn * 0.06);\n const wide = t.labelWIn / t.labelHIn >= 1.8;\n const badgeZoneIn = hasBadge ? Math.min(0.4, t.labelHIn * 0.22) : 0;\n if (wide) {\n return { wide, qrIn: t.labelHIn - padIn * 2, padIn, badgeZoneIn };\n }\n // Stacked. Round labels inscribe the content square inside the circle.\n const inset = t.round ? t.labelWIn * 0.15 : 0;\n const captionZone = showCaption ? Math.min(0.28, t.labelHIn * 0.16) : 0;\n const qrIn = Math.min(\n t.labelWIn - padIn * 2 - inset * 2,\n t.labelHIn - padIn * 2 - inset * 2 - captionZone - badgeZoneIn,\n );\n return { wide, qrIn, padIn, badgeZoneIn };\n}\n\n// ─── Generic grid renderer ───────────────────────────────────────────────────\n\nfunction sheetStyles(t: LabelTemplate): string {\n const radius = t.round\n ? `${t.labelWIn / 2}in`\n : t.cornerRadiusIn\n ? `${t.cornerRadiusIn}in`\n : \"0\";\n return `\n @page { size: ${t.sheetWIn}in ${t.sheetHIn}in; margin: 0; }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n html, body { width: ${t.sheetWIn}in; background: #fff; color: #000;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif; }\n\n .lp-page {\n width: ${t.sheetWIn}in; height: ${t.sheetHIn}in;\n position: relative; overflow: hidden;\n page-break-after: always; break-after: page;\n border-bottom: 1px solid #e2e8f0; /* screen-only separator */\n }\n .lp-page:last-child { page-break-after: auto; break-after: auto; border-bottom: none; }\n\n .lp-cell {\n position: absolute;\n width: ${t.labelWIn}in; height: ${t.labelHIn}in;\n border-radius: ${radius};\n overflow: hidden;\n -webkit-print-color-adjust: exact; print-color-adjust: exact;\n }\n .lp-qr {\n display: block;\n image-rendering: pixelated; /* no resampling blur on crisp modules */\n }\n .lp-cell-wide { display: flex; align-items: center; }\n .lp-cell-stacked { display: flex; flex-direction: column; align-items: center; justify-content: center; }\n\n .lp-text { flex: 1 1 auto; min-width: 0; overflow: hidden; align-self: stretch;\n display: flex; flex-direction: column; justify-content: center; }\n .lp-fit-wrap { overflow: hidden; }\n .lp-caption { font-weight: 700; line-height: 1.15; word-break: break-word; }\n .lp-line { line-height: 1.25; word-break: break-word; }\n .lp-caption-stacked { text-align: center; width: 100%; }\n\n /* Badge — the LARGE token above the caption (garment size, bin letter). */\n .lp-badge { font-weight: 800; line-height: 1.05; word-break: break-word;\n -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n .lp-badge-stacked { text-align: center; width: 100%; }\n\n /* Durable http(s) artwork in the lines zone. */\n .lp-img { display: block; width: 100%; object-fit: contain; }\n\n /* Calibration */\n .lp-cal-cell { border: 0.5pt dashed #94a3b8; background: transparent; }\n .lp-cal-label { position: absolute; inset: 0; display: flex; align-items: center;\n justify-content: center; font-size: 7pt; color: #94a3b8; text-align: center; padding: 4px; }\n .lp-crop { position: absolute; background: #94a3b8;\n -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n\n @media print {\n html, body { height: auto; overflow: visible; }\n .lp-page { border-bottom: none; }\n .screen-only { display: none !important; }\n }\n`;\n}\n\nfunction cellPosition(t: LabelTemplate, index: number): { x: number; y: number } {\n const col = index % t.cols;\n const row = Math.floor(index / t.cols);\n return {\n x: t.marginLeftIn + col * (t.labelWIn + t.gutterXIn),\n y: t.marginTopIn + row * (t.labelHIn + t.gutterYIn),\n };\n}\n\n/**\n * Caption auto-shrink — same pattern as the flashcards FIT_TEXT_SCRIPT.\n *\n * Kept verbatim instead of core's `buildFitTextScript`: this loop measures and\n * writes **px** with a 4px floor, core's writes **pt** with a 6pt floor and\n * pre-assigns the size before measuring. See the module header.\n */\nconst LP_FIT_SCRIPT = `\n<script>\n(function () {\n function fit(el, box) {\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var min = 4;\n while (size > min && (el.scrollWidth > box.clientWidth + 1 || el.scrollHeight > box.clientHeight + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"px\";\n }\n }\n function run() {\n var els = document.querySelectorAll(\".lp-fit-wrap .lp-fit\");\n for (var i = 0; i < els.length; i++) fit(els[i], els[i].parentElement);\n }\n if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", run);\n else setTimeout(run, 0);\n})();\n</script>`;\n\ninterface RenderedCell {\n html: string; // inner HTML of the cell, \"\" for a blank pad cell\n}\n\n/** THE IMAGE RULE: only a durable http(s) URL is ever rendered. */\nfunction isHttpUrl(value: string | undefined): value is string {\n return typeof value === \"string\" && /^https?:\\/\\//i.test(value.trim());\n}\n\nfunction buildLabelCellHtml(\n label: QrLabel,\n qrDataUri: string,\n t: LabelTemplate,\n layout: CellLayout,\n s: QrLabelSettings,\n): string {\n const qr = qrDataUri\n ? `<img class=\"lp-qr\" src=\"${qrDataUri}\" alt=\"\" style=\"width:${layout.qrIn}in;height:${layout.qrIn}in;\" />`\n : \"\";\n const captionText = s.showCaption ? (label.caption ?? label.qrValue) : \"\";\n const badgeText = label.badge?.trim() ? label.badge.trim() : \"\";\n\n if (layout.wide) {\n const lines =\n s.showLines && label.lines?.length\n ? label.lines\n .filter((l) => l.trim())\n .map(\n (l) =>\n `<div class=\"lp-line\" style=\"font-size:8pt;\">${escapeHtml(l)}</div>`,\n )\n .join(\"\")\n : \"\";\n const caption = captionText\n ? `<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>`\n : \"\";\n const badge = badgeText\n ? `<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>`\n : \"\";\n const image = isHttpUrl(label.imageUrl)\n ? `<img class=\"lp-img\" src=\"${escapeHtml(label.imageUrl)}\" alt=\"\" style=\"height:${(t.labelHIn * 0.35).toFixed(3)}in;\" />`\n : \"\";\n return `<div class=\"lp-cell-wide\" style=\"height:100%;padding:${layout.padIn}in;gap:${layout.padIn}in;\">\n ${qr}\n <div class=\"lp-text\">${badge}${caption}${lines}${image}</div>\n </div>`;\n }\n\n const caption = captionText\n ? `<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>`\n : \"\";\n const badge = badgeText\n ? `<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>`\n : \"\";\n return `<div class=\"lp-cell-stacked\" style=\"height:100%;padding:${layout.padIn}in;\">\n ${qr}\n ${badge}\n ${caption}\n </div>`;\n}\n\nfunction renderPages(t: LabelTemplate, cells: RenderedCell[]): string {\n const perPage = t.cols * t.rows;\n const pages: string[] = [];\n for (let p = 0; p < Math.max(1, Math.ceil(cells.length / perPage)); p++) {\n const batch = cells.slice(p * perPage, (p + 1) * perPage);\n const cellHtml = batch\n .map((c, i) => {\n const { x, y } = cellPosition(t, i);\n return `<div class=\"lp-cell\" style=\"left:${x}in;top:${y}in;\">${c.html}</div>`;\n })\n .join(\"\\n\");\n pages.push(`<div class=\"lp-page\">${cellHtml}</div>`);\n }\n return pages.join(\"\\n\");\n}\n\nfunction openSheetWindow(\n t: LabelTemplate,\n bodyHtml: string,\n title: string,\n withFitScript: boolean,\n warning?: string,\n): PrintOutcome {\n const warningHtml = warning\n ? `<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>`\n : \"\";\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>${escapeHtml(title)} — ${escapeHtml(t.stockCode)}</title>\n <style>${sheetStyles(t)}</style>\n</head>\n<body>\n${warningHtml}\n<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;\">\n <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>\n <button onclick=\"window.close()\" style=\"padding:7px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;\">Close</button>\n <span style=\"color:#64748b;font-size:11px;\">${escapeHtml(t.stockCode)} · ${escapeHtml(t.name)} · set printer to <strong>100% scale (no \"fit to page\"), no margins, Portrait</strong></span>\n</div>\n${bodyHtml}\n${withFitScript ? LP_FIT_SCRIPT : \"\"}\n</body>\n</html>`;\n return openPrintWindow(html, title);\n}\n\n// ─── Label selection (range → copies → startAt offset) ──────────────────────\n\nexport function expandLabels(labels: QrLabel[], s: QrLabelSettings): QrLabel[] {\n const from = s.rangeFrom > 0 ? s.rangeFrom : 1;\n const to = s.rangeTo > 0 ? Math.min(s.rangeTo, labels.length) : labels.length;\n const subset = labels.slice(from - 1, to);\n if (s.copiesPerCode <= 1) return subset;\n const out: QrLabel[] = [];\n for (const l of subset)\n for (let i = 0; i < s.copiesPerCode; i++) out.push(l);\n return out;\n}\n\n// ─── Calibration test page ───────────────────────────────────────────────────\n\nfunction buildCalibrationBody(t: LabelTemplate): string {\n const perPage = t.cols * t.rows;\n const cells: string[] = [];\n for (let i = 0; i < perPage; i++) {\n const { x, y } = cellPosition(t, i);\n cells.push(\n `<div class=\"lp-cell lp-cal-cell\" style=\"left:${x}in;top:${y}in;\"><div class=\"lp-cal-label\">${i + 1}</div></div>`,\n );\n }\n // Crop marks at the four corners of the label grid area\n const gridR = t.sheetWIn - t.marginLeftIn;\n const gridB = t.sheetHIn - t.marginTopIn;\n const mark = (x: number, y: number, h: boolean) =>\n `<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>`;\n const marks = [\n mark(t.marginLeftIn - 0.25, t.marginTopIn, true),\n mark(t.marginLeftIn, t.marginTopIn - 0.25, false),\n mark(gridR, t.marginTopIn, true),\n mark(gridR - 0.0104, t.marginTopIn - 0.25, false),\n mark(t.marginLeftIn - 0.25, gridB - 0.0104, true),\n mark(t.marginLeftIn, gridB, false),\n mark(gridR, gridB - 0.0104, true),\n mark(gridR - 0.0104, gridB, false),\n ].join(\"\");\n 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 — ${escapeHtml(t.stockCode)} · hold against your label sheet: every outline must sit on a label</div>`;\n return `<div class=\"lp-page\">${banner}${marks}${cells.join(\"\\n\")}</div>`;\n}\n\n/** Print the grid outline + crop marks only — verify alignment before burning labels. */\nexport function printCalibrationSheet(template: LabelTemplate): PrintOutcome {\n return openSheetWindow(\n template,\n buildCalibrationBody(template),\n \"Label calibration\",\n false,\n );\n}\n\n// ─── Scannability ────────────────────────────────────────────────────────────\n\nconst MM_PER_IN = 25.4;\n\n/**\n * The printed-module floor a 300 dpi thermal printer and a warehouse scanner\n * can both resolve. Below this a symbol prints, looks fine on screen, and\n * refuses to scan on the dock.\n */\nexport const MIN_SCANNABLE_MODULE_MM = 0.4;\n\nexport interface ScannabilityReport {\n ok: boolean;\n version: number;\n moduleSizeMm: number;\n minModuleSizeMm: number;\n message?: string;\n}\n\n/**\n * Will a payload of `payloadByteLen` bytes actually scan off this stock?\n *\n * The printed side comes from the SAME layout brain the printer uses\n * (`computeCellLayout`), evaluated with captions ON — the smaller, and\n * therefore conservative, QR box. Module size divides that side by the symbol's\n * modules PLUS the two 4-module quiet zones the data URI carries inside it.\n *\n * Never throws: an over-capacity payload comes back as `ok: false` with\n * `version: 0` and the capacity error as the message.\n */\nexport function assertScannable(\n template: LabelTemplate,\n payloadByteLen: number,\n ecLevel: QrEcLevel = DEFAULTS.ecLevel,\n): ScannabilityReport {\n const layout = computeCellLayout(template, true);\n const printedSideMm = layout.qrIn * MM_PER_IN;\n\n let version: number;\n try {\n version = minQrVersion(payloadByteLen, ecLevel);\n } catch (err) {\n return {\n ok: false,\n version: 0,\n moduleSizeMm: 0,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n message: err instanceof Error ? err.message : String(err),\n };\n }\n\n const modulesPerSide = qrModuleCount(version) + QUIET_ZONE_MODULES * 2;\n const moduleSizeMm = printedSideMm / modulesPerSide;\n if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {\n return {\n ok: true,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n };\n }\n return {\n ok: false,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n message:\n `A ${payloadByteLen}-byte payload needs a version ${version} QR, which prints at ` +\n `${moduleSizeMm.toFixed(3)}mm per module on ${template.stockCode} — below the ` +\n `${MIN_SCANNABLE_MODULE_MM}mm floor a scanner can resolve. Shorten the payload ` +\n `(a short resolver URL is the intended fix) or move to larger label stock.`,\n };\n}\n\n/** The batch's worst case: the longest payload decides whether we warn. */\nfunction batchScannabilityWarning(\n t: LabelTemplate,\n labels: readonly QrLabel[],\n ecLevel: QrEcLevel,\n): string | undefined {\n const encoder = new TextEncoder();\n let worst = -1;\n for (const l of labels) {\n if (!l.qrValue) continue;\n const len = encoder.encode(l.qrValue).length;\n if (len > worst) worst = len;\n }\n if (worst < 0) return undefined;\n const report = assertScannable(t, worst, ecLevel);\n return report.ok ? undefined : report.message;\n}\n\n// ─── The printer ─────────────────────────────────────────────────────────────\n\nfunction resolveTemplate(\n data: QrLabelPrintData,\n variantId?: string,\n): LabelTemplate {\n if (variantId && variantId !== \"custom\") {\n const t = getLabelTemplate(variantId);\n if (t) return t;\n }\n if (data.templateId === \"custom\" && data.customTemplate)\n return data.customTemplate;\n return (\n getLabelTemplate(data.templateId) ?? data.customTemplate ?? AVERY_5160\n );\n}\n\n/** Core render path — also used by the demo page's direct print entry point. */\nexport async function printQrLabelSheet(\n data: QrLabelPrintData,\n variantId?: string,\n rawSettings?: PrintSettings,\n): Promise<PrintOutcome> {\n const t = resolveTemplate(data, variantId);\n const s = resolveSettings(rawSettings);\n\n if (s.calibrationPage) {\n return printCalibrationSheet(t);\n }\n\n const labels = expandLabels(data.labels ?? [], s);\n if (!labels.length) {\n return openSheetWindow(\n t,\n `<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>`,\n \"QR labels\",\n false,\n );\n }\n\n const hasBadge = labels.some((l) => (l.badge ?? \"\").trim() !== \"\");\n const layout = computeCellLayout(t, s.showCaption, hasBadge);\n\n // Generate every data URI up front (dedup identical values). A label with no\n // qrValue (a text-only LabelFormat) simply carries no code.\n const uriByValue = new Map<string, string>();\n for (const l of labels) {\n if (l.qrValue && !uriByValue.has(l.qrValue)) {\n uriByValue.set(\n l.qrValue,\n await generateQrDataUri(l.qrValue, s.ecLevel, layout.qrIn),\n );\n }\n }\n\n // Leading blank cells for a partially-used sheet, then labels, then pad.\n const leadingBlanks = Math.max(0, s.startAtLabel - 1) % (t.cols * t.rows);\n const cells: RenderedCell[] = [\n ...Array.from({ length: leadingBlanks }, () => ({ html: \"\" })),\n ...labels.map((l) => ({\n html: buildLabelCellHtml(\n l,\n uriByValue.get(l.qrValue) ?? \"\",\n t,\n layout,\n s,\n ),\n })),\n ];\n const perPage = t.cols * t.rows;\n const tail = cells.length % perPage;\n if (tail > 0)\n for (let i = 0; i < perPage - tail; i++) cells.push({ html: \"\" });\n\n return openSheetWindow(\n t,\n renderPages(t, cells),\n \"QR labels\",\n true,\n batchScannabilityWarning(t, labels, s.ecLevel),\n );\n}\n\nexport const qrLabelsPrinter: BlockPrinter = {\n label: \"Print QR label sheet\",\n variants: LABEL_TEMPLATES.map((t) => ({\n id: t.id,\n label: t.name,\n description: t.stockCode,\n })),\n settings: [\n {\n type: \"number\",\n id: \"startAtLabel\",\n label: \"Start at label #\",\n description: \"Skip already-used positions on a partial sheet\",\n defaultValue: 1,\n min: 1,\n },\n {\n type: \"number\",\n id: \"copiesPerCode\",\n label: \"Copies per code\",\n defaultValue: 1,\n min: 1,\n max: 999,\n },\n {\n type: \"boolean\",\n id: \"showCaption\",\n label: \"Show caption\",\n description: \"Print the caption (or the code value) beside each QR\",\n defaultValue: true,\n },\n {\n type: \"boolean\",\n id: \"showLines\",\n label: \"Show detail lines\",\n description: \"Extra text lines on wide labels\",\n defaultValue: true,\n },\n {\n type: \"range\",\n id: \"labelRange\",\n label: \"Label range\",\n description: \"Print only labels N to M (blank = all)\",\n fromId: \"rangeFrom\",\n toId: \"rangeTo\",\n defaultFrom: 0,\n defaultTo: 0,\n },\n {\n type: \"select\",\n id: \"ecLevel\",\n label: \"Error correction\",\n description: \"Higher survives more damage; M is the warehouse default\",\n options: [\n { value: \"L\", label: \"L\" },\n { value: \"M\", label: \"M\" },\n { value: \"Q\", label: \"Q\" },\n ],\n defaultValue: \"M\",\n },\n {\n type: \"boolean\",\n id: \"calibrationPage\",\n label: \"Calibration test page\",\n description: \"Print outlines + crop marks only to verify alignment\",\n defaultValue: false,\n },\n ],\n print: (data, variantId, settings) =>\n printQrLabelSheet(data as QrLabelPrintData, variantId, settings),\n};\n\n// ─── PDF lane ────────────────────────────────────────────────────────────────\n\n/**\n * downloadLabelsPdf() renders the same registry-driven grid as the printer\n * straight into a jsPDF file, for users who want a file instead of the browser\n * print dialog.\n *\n * jspdf is imported dynamically INSIDE the function body (code-splitting rule 6\n * — click-time machinery never rides a static edge).\n */\n\nconst PT_PER_IN = 72;\n\nfunction resolvePdfSettings(s?: PrintSettings): QrLabelSettings {\n const num = (v: unknown, d: number, min = 0) =>\n typeof v === \"number\" && !Number.isNaN(v) ? Math.max(min, v) : d;\n const ec = s?.ecLevel;\n return {\n startAtLabel: num(s?.startAtLabel, 1, 1),\n copiesPerCode: num(s?.copiesPerCode, 1, 1),\n showCaption: (s?.showCaption ?? true) as boolean,\n showLines: (s?.showLines ?? true) as boolean,\n rangeFrom: num(s?.rangeFrom, 0),\n rangeTo: num(s?.rangeTo, 0),\n ecLevel: ec === \"L\" || ec === \"Q\" ? ec : \"M\",\n calibrationPage: false,\n };\n}\n\n/** jsPDF's splitTextToSize is loosely typed; take the first line safely. */\nfunction firstLine(parts: unknown): string {\n if (Array.isArray(parts)) {\n const head: unknown = parts[0];\n return typeof head === \"string\" ? head : \"\";\n }\n return typeof parts === \"string\" ? parts : \"\";\n}\n\n/**\n * Generate a PDF of QR labels for the given template and hand it to the\n * browser as a download. Same data seam as the printer: `{ labels, templateId }`.\n */\nexport async function downloadLabelsPdf(\n data: QrLabelPrintData,\n templateId?: string,\n rawSettings?: PrintSettings,\n filename = \"qr-labels.pdf\",\n): Promise<void> {\n const t =\n (templateId === \"custom\" || data.templateId === \"custom\"\n ? data.customTemplate\n : undefined) ??\n getLabelTemplate(templateId ?? data.templateId) ??\n AVERY_5160;\n const s = resolvePdfSettings(rawSettings);\n const labels = expandLabels(data.labels ?? [], s);\n if (!labels.length) return;\n\n // Never blocks the download — the PDF lane has no screen-only header to\n // carry the banner, so an unscannable batch screams on the console instead.\n const warning = batchScannabilityWarning(t, labels, s.ecLevel);\n if (warning !== undefined) console.warn(`[label-print] ${warning}`);\n\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({\n unit: \"pt\",\n format: [t.sheetWIn * PT_PER_IN, t.sheetHIn * PT_PER_IN],\n });\n\n const hasBadge = labels.some((l) => (l.badge ?? \"\").trim() !== \"\");\n const layout = computeCellLayout(t, s.showCaption, hasBadge);\n const qrPt = layout.qrIn * PT_PER_IN;\n const padPt = layout.padIn * PT_PER_IN;\n const perPage = t.cols * t.rows;\n const leadingBlanks = Math.max(0, s.startAtLabel - 1) % perPage;\n\n const uriByValue = new Map<string, string>();\n for (const l of labels) {\n if (l.qrValue && !uriByValue.has(l.qrValue)) {\n uriByValue.set(\n l.qrValue,\n await generateQrDataUri(l.qrValue, s.ecLevel, layout.qrIn),\n );\n }\n }\n\n for (const [i, label] of labels.entries()) {\n const cellIndex = i + leadingBlanks;\n if (cellIndex > 0 && cellIndex % perPage === 0) doc.addPage();\n const within = cellIndex % perPage;\n const col = within % t.cols;\n const row = Math.floor(within / t.cols);\n const x =\n (t.marginLeftIn + col * (t.labelWIn + t.gutterXIn)) * PT_PER_IN;\n const y = (t.marginTopIn + row * (t.labelHIn + t.gutterYIn)) * PT_PER_IN;\n const caption = s.showCaption ? (label.caption ?? label.qrValue) : \"\";\n const badge = label.badge?.trim() ? label.badge.trim() : \"\";\n const uri = uriByValue.get(label.qrValue) ?? \"\";\n\n if (layout.wide) {\n // QR left, text right\n const qrY = y + (t.labelHIn * PT_PER_IN - qrPt) / 2;\n if (uri) doc.addImage(uri, \"PNG\", x + padPt, qrY, qrPt, qrPt);\n const textX = x + padPt + qrPt + padPt;\n const textMaxW = t.labelWIn * PT_PER_IN - (textX - x) - padPt;\n let cursorY = y + padPt + 12;\n if (badge) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(24);\n cursorY = y + padPt + 22;\n doc.text(firstLine(doc.splitTextToSize(badge, textMaxW)), textX, cursorY);\n cursorY += 16;\n }\n if (caption) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(12);\n doc.text(firstLine(doc.splitTextToSize(caption, textMaxW)), textX, cursorY);\n cursorY += 14;\n }\n if (s.showLines && label.lines?.length) {\n doc.setFont(\"helvetica\", \"normal\");\n doc.setFontSize(8);\n for (const line of label.lines.filter((l) => l.trim())) {\n if (cursorY > y + t.labelHIn * PT_PER_IN - padPt) break;\n doc.text(firstLine(doc.splitTextToSize(line, textMaxW)), textX, cursorY);\n cursorY += 10;\n }\n }\n } else {\n // Stacked, centered\n const cx = x + t.labelWIn * PT_PER_IN / 2;\n const contentH = qrPt + (caption ? 12 : 0) + (badge ? 22 : 0);\n const qrY = y + (t.labelHIn * PT_PER_IN - contentH) / 2;\n if (uri) doc.addImage(uri, \"PNG\", cx - qrPt / 2, qrY, qrPt, qrPt);\n const maxW = (t.labelWIn - layout.padIn * 2) * PT_PER_IN;\n let textY = qrY + qrPt;\n if (badge) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(18);\n textY += 18;\n doc.text(firstLine(doc.splitTextToSize(badge, maxW)), cx, textY, {\n align: \"center\",\n });\n }\n if (caption) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(9);\n doc.text(firstLine(doc.splitTextToSize(caption, maxW)), cx, textY + 10, {\n align: \"center\",\n });\n }\n }\n }\n\n doc.save(filename);\n}\n\n// ─── Label formats (the custom-format model) ─────────────────────────────────\n\n/** Where an element's text comes from: a row column, or a fixed string. */\nexport type LabelField = { field: string } | { literal: string };\n\nexport type LabelElement =\n | { kind: \"qr\"; source: LabelField }\n | {\n kind: \"text\";\n source: LabelField;\n role: \"title\" | \"detail\" | \"badge\";\n /**\n * Cap on how many lines the value may occupy. The value is split on\n * newlines; beyond `maxLines` the extra lines are dropped and the last\n * kept line gains an ellipsis.\n */\n maxLines?: number;\n }\n /** Durable http(s) URL only — THE IMAGE RULE. A non-http value is SKIPPED. */\n | { kind: \"image\"; source: LabelField };\n\nexport interface LabelFormat {\n id: string;\n name: string;\n elements: LabelElement[];\n}\n\nfunction resolveLabelField(\n source: LabelField,\n row: Record<string, string>,\n): string {\n if (\"literal\" in source) return source.literal;\n return row[source.field] ?? \"\";\n}\n\nfunction capToLines(value: string, maxLines: number | undefined): string[] {\n const parts = value\n .split(\"\\n\")\n .map((p) => p.trim())\n .filter((p) => p !== \"\");\n if (maxLines === undefined || maxLines <= 0 || parts.length <= maxLines) {\n return parts;\n }\n const kept = parts.slice(0, maxLines);\n const last = kept[kept.length - 1];\n if (last !== undefined) kept[kept.length - 1] = `${last}…`;\n return kept;\n}\n\n/**\n * Map rows through a format into the printer's label shape.\n *\n * - the qr element's resolved value → `qrValue` (first one wins)\n * - the FIRST title-role text → `caption`\n * - the FIRST badge-role text → `badge`\n * - detail-role texts, in element order → `lines[]`\n * - the first http(s) image element → `imageUrl`\n *\n * A row missing a `field` resolves to \"\" and that element is skipped.\n */\nexport function applyLabelFormat(\n format: LabelFormat,\n rows: Record<string, string>[],\n): { labels: QrLabelPrintData[\"labels\"] } {\n const labels: QrLabel[] = [];\n for (const row of rows) {\n let qrValue = \"\";\n let caption: string | undefined;\n let badge: string | undefined;\n let imageUrl: string | undefined;\n const lines: string[] = [];\n\n for (const el of format.elements) {\n const raw = resolveLabelField(el.source, row).trim();\n if (raw === \"\") continue;\n if (el.kind === \"qr\") {\n if (qrValue === \"\") qrValue = raw;\n continue;\n }\n if (el.kind === \"image\") {\n if (imageUrl === undefined && isHttpUrl(raw)) imageUrl = raw;\n continue;\n }\n const capped = capToLines(raw, el.maxLines);\n if (capped.length === 0) continue;\n if (el.role === \"title\") {\n if (caption === undefined) caption = capped.join(\" \");\n } else if (el.role === \"badge\") {\n if (badge === undefined) badge = capped.join(\" \");\n } else {\n lines.push(...capped);\n }\n }\n\n const label: QrLabel = { qrValue };\n if (caption !== undefined) label.caption = caption;\n if (lines.length > 0) label.lines = lines;\n if (badge !== undefined) label.badge = badge;\n if (imageUrl !== undefined) label.imageUrl = imageUrl;\n labels.push(label);\n }\n return { labels };\n}\n\n/**\n * The shipped formats. Every preset reads the QR payload from the `qrValue`\n * column, so a row is always `{ qrValue, …format-specific columns }`.\n *\n * NOTE on `asset-spec`: the cell renderer auto-shrinks only the caption/badge\n * (`.lp-fit`); detail lines are fixed 8pt, so the title never loses size to a\n * long spec list.\n */\nexport const LABEL_FORMAT_PRESETS: readonly LabelFormat[] = [\n {\n id: \"qr-only\",\n name: \"QR only\",\n elements: [{ kind: \"qr\", source: { field: \"qrValue\" } }],\n },\n {\n id: \"qr-caption\",\n name: \"QR + caption\",\n elements: [\n { kind: \"qr\", source: { field: \"qrValue\" } },\n { kind: \"text\", source: { field: \"caption\" }, role: \"title\" },\n ],\n },\n {\n id: \"garment\",\n name: \"Garment tag\",\n elements: [\n { kind: \"qr\", source: { field: \"qrValue\" } },\n { kind: \"text\", source: { field: \"size\" }, role: \"badge\" },\n { kind: \"text\", source: { field: \"sku\" }, role: \"title\" },\n { kind: \"text\", source: { field: \"color\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"price\" }, role: \"detail\" },\n ],\n },\n {\n id: \"asset-spec\",\n name: \"Asset spec\",\n elements: [\n { kind: \"text\", source: { field: \"title\" }, role: \"title\" },\n { kind: \"text\", source: { field: \"spec1\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"spec2\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"spec3\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"spec4\" }, role: \"detail\" },\n { kind: \"qr\", source: { field: \"qrValue\" } },\n ],\n },\n];\n","/**\n * @ai-matrx/print/qr — QR GENERATION.\n *\n * New code (no port source). Decoding is NOT here: it lives in\n * `@ai-matrx/kit/qr`. This module only ever turns a value into a symbol, and\n * builds the two payload shapes the platform actually prints:\n *\n * 1. a SHORT RESOLVER URL (`https://origin/l/<code>`) — the default\n * e-commerce payload. The code value is an OPAQUE INPUT: minting,\n * uniqueness, collision handling and lookup are all server-side concerns.\n * Nothing in this file generates, randomizes, or validates the identity of\n * a code beyond \"it is a non-empty string we will percent-encode\".\n * 2. a GS1 DIGITAL LINK (`https://origin/01/<gtin14>[/10/<lot>][/21/<serial>][?17=YYMMDD]`)\n * for supply-chain interoperability — the thing a retailer's scanner\n * understands without knowing anything about AI Matrx.\n *\n * Two hard defaults that are NOT style choices:\n * - Error correction defaults to \"M\", never \"L\". These end up on warehouse\n * labels that get scuffed, wrapped around curved stock and read at an\n * angle; \"L\" is only ever right for a huge, clean, flat code.\n * - The quiet zone is CLAMPED to >= 4 modules (ISO/IEC 18004 §6.3.8 minimum)\n * no matter what the caller passes. A tight quiet zone is the single most\n * common cause of a label that scans on the design monitor and fails at the\n * dock door.\n *\n * The module is import-inert: `qrcode` is only ever pulled in via\n * `await import(\"qrcode\")` inside a function body, so importing this file from\n * a React Server Component (or any tree-shaken bundle that never draws a QR)\n * costs nothing. No module-level mutable state, no React, no DOM at import\n * time.\n */\n\nimport type { QRCodeToDataURLOptions, QRCodeToStringOptions } from \"qrcode\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\nexport type QrEcLevel = \"L\" | \"M\" | \"Q\" | \"H\";\n\nexport interface QrOptions {\n /** Error correction. Default \"M\" — NEVER \"L\" (warehouse labels get scuffed; L is only right for huge clean codes). */\n ecLevel?: QrEcLevel;\n /** Quiet zone in modules. Default 4. CLAMPED to >= 4 at runtime — ISO 18004 spec minimum, not a style choice. */\n quietZoneModules?: number;\n /** Rendered width in pixels (dataURL/canvas lanes). */\n widthPx?: number;\n}\n\n/** ISO/IEC 18004 §6.3.8 minimum quiet zone. Callers cannot go below this. */\nexport const QR_MIN_QUIET_ZONE_MODULES = 4;\n\n/** The default error correction level for every Matrx-printed code. */\nexport const DEFAULT_QR_EC_LEVEL: QrEcLevel = \"M\";\n\n/** Lowest and highest QR symbol versions (ISO/IEC 18004). */\nexport const QR_MIN_VERSION = 1;\nexport const QR_MAX_VERSION = 40;\n\n// ─── Capacity table ──────────────────────────────────────────────────────────\n\n/**\n * ISO/IEC 18004 BYTE-mode data capacity, versions 1–40 × [L, M, Q, H],\n * in bytes (8-bit characters).\n *\n * This is the full spec table, not an approximation. It is cross-checked in\n * `qr.test.ts` against the `qrcode` reference implementation for a spread of\n * versions at all four levels — every entry that test samples must both fit at\n * exactly `capacity` bytes and fail at `capacity + 1`.\n *\n * Anchors worth eyeballing: v1 = L17/M14/Q11/H7 · v2 M=26 · v3 M=42 ·\n * v4 M=62 · v10 M=213 · v40 = L2953/M2331/Q1663/H1273.\n */\nconst BYTE_CAPACITY: readonly (readonly [number, number, number, number])[] = [\n /* v1 */ [17, 14, 11, 7],\n /* v2 */ [32, 26, 20, 14],\n /* v3 */ [53, 42, 32, 24],\n /* v4 */ [78, 62, 46, 34],\n /* v5 */ [106, 84, 60, 44],\n /* v6 */ [134, 106, 74, 58],\n /* v7 */ [154, 122, 86, 64],\n /* v8 */ [192, 152, 108, 84],\n /* v9 */ [230, 180, 130, 98],\n /* v10 */ [271, 213, 151, 119],\n /* v11 */ [321, 251, 177, 137],\n /* v12 */ [367, 287, 203, 155],\n /* v13 */ [425, 331, 241, 177],\n /* v14 */ [458, 362, 258, 194],\n /* v15 */ [520, 412, 292, 220],\n /* v16 */ [586, 450, 322, 250],\n /* v17 */ [644, 504, 364, 280],\n /* v18 */ [718, 560, 394, 310],\n /* v19 */ [792, 624, 442, 338],\n /* v20 */ [858, 666, 482, 382],\n /* v21 */ [929, 711, 509, 403],\n /* v22 */ [1003, 779, 565, 439],\n /* v23 */ [1091, 857, 611, 461],\n /* v24 */ [1171, 911, 661, 511],\n /* v25 */ [1273, 997, 715, 535],\n /* v26 */ [1367, 1059, 751, 593],\n /* v27 */ [1465, 1125, 805, 625],\n /* v28 */ [1528, 1190, 868, 658],\n /* v29 */ [1628, 1264, 908, 698],\n /* v30 */ [1732, 1370, 982, 742],\n /* v31 */ [1840, 1452, 1030, 790],\n /* v32 */ [1952, 1538, 1112, 842],\n /* v33 */ [2068, 1628, 1168, 898],\n /* v34 */ [2188, 1722, 1228, 958],\n /* v35 */ [2303, 1809, 1283, 983],\n /* v36 */ [2431, 1911, 1351, 1051],\n /* v37 */ [2563, 1989, 1423, 1093],\n /* v38 */ [2699, 2099, 1499, 1139],\n /* v39 */ [2809, 2213, 1579, 1219],\n /* v40 */ [2953, 2331, 1663, 1273],\n];\n\nconst EC_COLUMN: Readonly<Record<QrEcLevel, 0 | 1 | 2 | 3>> = { L: 0, M: 1, Q: 2, H: 3 };\n\nfunction assertEcLevel(ecLevel: QrEcLevel): 0 | 1 | 2 | 3 {\n const column = EC_COLUMN[ecLevel];\n if (column === undefined) {\n throw new Error(`Unknown QR error correction level \"${String(ecLevel)}\". Expected one of \"L\", \"M\", \"Q\", \"H\".`);\n }\n return column;\n}\n\nfunction assertVersion(version: number): number {\n if (typeof version !== \"number\" || !Number.isInteger(version) || version < QR_MIN_VERSION || version > QR_MAX_VERSION) {\n throw new Error(\n `QR version must be an integer between ${QR_MIN_VERSION} and ${QR_MAX_VERSION}, got ${String(version)}.`,\n );\n }\n return version;\n}\n\n/**\n * BYTE-mode capacity in bytes for a given version/level.\n * Throws on an out-of-range version or unknown level.\n */\nexport function qrByteCapacity(version: number, ecLevel: QrEcLevel): number {\n const column = assertEcLevel(ecLevel);\n const row = BYTE_CAPACITY[assertVersion(version) - 1];\n if (row === undefined) {\n throw new Error(`No QR capacity row for version ${version}.`);\n }\n return row[column];\n}\n\n/** Modules per side for a QR version: 17 + 4*version. */\nexport function qrModuleCount(version: number): number {\n return 17 + 4 * assertVersion(version);\n}\n\n/**\n * Smallest QR version (1-40) whose BYTE-mode capacity at `ecLevel` fits\n * `payloadByteLen`. Throws a clear Error when nothing fits.\n *\n * NOTE: this is the *byte* length, not the character length — callers holding\n * a JS string should measure it with `new TextEncoder().encode(s).length`, not\n * `s.length`, or every non-ASCII character will silently under-count.\n */\nexport function minQrVersion(payloadByteLen: number, ecLevel: QrEcLevel): number {\n const column = assertEcLevel(ecLevel);\n if (typeof payloadByteLen !== \"number\" || !Number.isInteger(payloadByteLen) || payloadByteLen < 0) {\n throw new Error(`QR payload byte length must be a non-negative integer, got ${String(payloadByteLen)}.`);\n }\n for (let version = QR_MIN_VERSION; version <= QR_MAX_VERSION; version += 1) {\n const row = BYTE_CAPACITY[version - 1];\n if (row !== undefined && row[column] >= payloadByteLen) {\n return version;\n }\n }\n const max = qrByteCapacity(QR_MAX_VERSION, ecLevel);\n throw new Error(\n `QR payload of ${payloadByteLen} bytes does not fit any QR version at error correction level \"${ecLevel}\" ` +\n `(version ${QR_MAX_VERSION} holds ${max} bytes). Shorten the payload — a short resolver URL is the intended ` +\n `fix — or, only for a large clean code, drop to a lower error correction level.`,\n );\n}\n\n// ─── Rendering ───────────────────────────────────────────────────────────────\n\nfunction resolveEcLevel(ecLevel: QrEcLevel | undefined): QrEcLevel {\n if (ecLevel === undefined) return DEFAULT_QR_EC_LEVEL;\n assertEcLevel(ecLevel);\n return ecLevel;\n}\n\n/**\n * `margin` in the `qrcode` library IS the quiet zone measured in modules.\n * Anything below the ISO minimum is silently raised — a caller asking for 0 is\n * asking for a label that will not scan, and we would rather print a slightly\n * smaller symbol than an unreadable one.\n */\nfunction resolveQuietZone(quietZoneModules: number | undefined): number {\n if (typeof quietZoneModules !== \"number\" || !Number.isFinite(quietZoneModules)) {\n return QR_MIN_QUIET_ZONE_MODULES;\n }\n return Math.max(QR_MIN_QUIET_ZONE_MODULES, Math.floor(quietZoneModules));\n}\n\nfunction resolveWidthPx(widthPx: number | undefined): number | undefined {\n if (widthPx === undefined) return undefined;\n if (typeof widthPx !== \"number\" || !Number.isFinite(widthPx) || widthPx <= 0) {\n throw new Error(`QR widthPx must be a positive number, got ${String(widthPx)}.`);\n }\n return Math.round(widthPx);\n}\n\nfunction assertValue(value: string): string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(\"QR value must be a non-empty string.\");\n }\n return value;\n}\n\n/** PNG data URL. Lazy-imports \"qrcode\" at call time (import-inert module). */\nexport async function generateQrDataUrl(value: string, opts?: QrOptions): Promise<string> {\n assertValue(value);\n const width = resolveWidthPx(opts?.widthPx);\n const options: QRCodeToDataURLOptions = {\n errorCorrectionLevel: resolveEcLevel(opts?.ecLevel),\n margin: resolveQuietZone(opts?.quietZoneModules),\n };\n // exactOptionalPropertyTypes: omit the key entirely rather than assigning undefined.\n if (width !== undefined) options.width = width;\n\n const QRCode = await import(\"qrcode\");\n return QRCode.toDataURL(value, options);\n}\n\n/** SVG markup string. Same lazy import. */\nexport async function generateQrSvg(value: string, opts?: QrOptions): Promise<string> {\n assertValue(value);\n const width = resolveWidthPx(opts?.widthPx);\n const options: QRCodeToStringOptions = {\n type: \"svg\",\n errorCorrectionLevel: resolveEcLevel(opts?.ecLevel),\n margin: resolveQuietZone(opts?.quietZoneModules),\n };\n if (width !== undefined) options.width = width;\n\n const QRCode = await import(\"qrcode\");\n return QRCode.toString(value, options);\n}\n\n// ─── Payload builders ────────────────────────────────────────────────────────\n\n/**\n * Validate + normalize an origin for a payload builder.\n * Accepts `https://x.com`, `https://x.com/`, `https://x.com/go` — returns it\n * with every trailing slash stripped so callers can always concatenate a\n * leading-slash path without producing `//`.\n */\nfunction normalizeOrigin(origin: string): string {\n if (typeof origin !== \"string\" || origin.trim() === \"\") {\n throw new Error('QR origin is required — pass something like \"https://example.com\".');\n }\n const trimmed = origin.trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new Error(`QR origin \"${origin}\" is not a valid absolute URL. Expected something like \"https://example.com\".`);\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(`QR origin \"${origin}\" must use http or https, got \"${parsed.protocol}\".`);\n }\n if (parsed.search !== \"\" || parsed.hash !== \"\") {\n throw new Error(`QR origin \"${origin}\" must not carry a query string or fragment.`);\n }\n return trimmed.replace(/\\/+$/, \"\");\n}\n\n/**\n * Short resolver URL — the platform's default e-commerce payload.\n *\n * The CODE VALUE IS AN OPAQUE INPUT: minting and uniqueness are server-side,\n * never generated here. This function's whole job is to put a code the server\n * already owns behind a URL short enough to stay in a low QR version.\n *\n * Result looks like `https://x.com/l/AB12cd`.\n */\nexport function buildShortResolverUrl(opts: { origin: string; code: string; path?: string }): string {\n const origin = normalizeOrigin(opts.origin);\n\n const code = opts.code;\n if (typeof code !== \"string\" || code.trim() === \"\") {\n throw new Error(\"QR resolver code is required — it is minted server-side and passed in here as an opaque value.\");\n }\n\n const rawPath = opts.path === undefined ? \"/l\" : opts.path;\n if (typeof rawPath !== \"string\") {\n throw new Error(`QR resolver path must be a string, got ${String(rawPath)}.`);\n }\n if (rawPath.includes(\"?\") || rawPath.includes(\"#\")) {\n throw new Error(`QR resolver path \"${rawPath}\" must not contain a query string or fragment.`);\n }\n const cleanedPath = rawPath.trim().replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n const prefix = cleanedPath === \"\" ? \"\" : `/${cleanedPath}`;\n\n return `${origin}${prefix}/${encodeURIComponent(code)}`;\n}\n\n/**\n * GS1 mod-10 check digit over the payload (i.e. the digits WITHOUT the check\n * digit): weights 3 and 1 alternating from the right. Length-independent, so a\n * GTIN-12 and its zero-padded GTIN-14 form produce the same digit — which is\n * exactly why we validate BEFORE padding.\n */\nfunction gs1CheckDigit(payloadDigits: string): number {\n let sum = 0;\n let weight = 3;\n for (let i = payloadDigits.length - 1; i >= 0; i -= 1) {\n sum += (payloadDigits.charCodeAt(i) - 48) * weight;\n weight = weight === 3 ? 1 : 3;\n }\n return (10 - (sum % 10)) % 10;\n}\n\n/** Validate a GTIN-8/12/13/14 (own check digit) and return it zero-padded to GTIN-14. */\nfunction normalizeGtin(gtin: string): string {\n if (typeof gtin !== \"string\" || gtin.trim() === \"\") {\n throw new Error(\"GTIN is required for a GS1 Digital Link.\");\n }\n const raw = gtin.trim();\n if (!/^[0-9]+$/.test(raw)) {\n throw new Error(`GTIN \"${gtin}\" must contain digits only.`);\n }\n if (raw.length !== 8 && raw.length !== 12 && raw.length !== 13 && raw.length !== 14) {\n throw new Error(`GTIN \"${gtin}\" must be 8, 12, 13, or 14 digits, got ${raw.length}.`);\n }\n const expected = gs1CheckDigit(raw.slice(0, -1));\n const actual = raw.charCodeAt(raw.length - 1) - 48;\n if (expected !== actual) {\n throw new Error(`GTIN \"${raw}\" has an invalid GS1 check digit: expected ${expected}, got ${actual}.`);\n }\n return raw.padStart(14, \"0\");\n}\n\nfunction daysInMonth(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\n/**\n * AI 17 expiration date — STRICT \"YYMMDD\", 6 digits, nothing else.\n * `DD = 00` is honoured: GS1 defines it as \"last day of the month\".\n */\nfunction normalizeExpiry(expiry: string): string {\n if (typeof expiry !== \"string\" || !/^[0-9]{6}$/.test(expiry.trim())) {\n throw new Error(`GS1 expiry (AI 17) must be exactly 6 digits in YYMMDD form, got \"${String(expiry)}\".`);\n }\n const raw = expiry.trim();\n const year = 2000 + Number(raw.slice(0, 2));\n const month = Number(raw.slice(2, 4));\n const day = Number(raw.slice(4, 6));\n if (month < 1 || month > 12) {\n throw new Error(`GS1 expiry (AI 17) \"${raw}\" has an invalid month \"${raw.slice(2, 4)}\" — expected 01-12.`);\n }\n const max = daysInMonth(year, month);\n // DD=00 is the GS1 \"end of month\" convention and stays legal.\n if (day > max) {\n throw new Error(\n `GS1 expiry (AI 17) \"${raw}\" has an invalid day \"${raw.slice(4, 6)}\" — ${year}-${raw.slice(2, 4)} has ${max} days ` +\n `(00 is legal and means end of month).`,\n );\n }\n return raw;\n}\n\nfunction assertQualifier(value: string, ai: string, name: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new Error(`GS1 ${name} (AI ${ai}) must be a non-empty string when provided.`);\n }\n return value;\n}\n\n/** GS1 Digital Link builder for supply-chain interoperability. */\nexport function buildGs1DigitalLink(opts: {\n origin: string;\n gtin: string;\n lot?: string;\n serial?: string;\n expiry?: string;\n}): string {\n const origin = normalizeOrigin(opts.origin);\n const gtin14 = normalizeGtin(opts.gtin);\n\n // Path order is fixed by the GS1 Digital Link grammar:\n // /01/{gtin14} then the optional qualifiers /10/{lot} then /21/{serial}.\n let url = `${origin}/01/${gtin14}`;\n if (opts.lot !== undefined) {\n url += `/10/${encodeURIComponent(assertQualifier(opts.lot, \"10\", \"lot\"))}`;\n }\n if (opts.serial !== undefined) {\n url += `/21/${encodeURIComponent(assertQualifier(opts.serial, \"21\", \"serial\"))}`;\n }\n // AI 17 is a DATA ATTRIBUTE, not a path qualifier — it belongs in the query\n // string, never as a /17/ path segment.\n if (opts.expiry !== undefined) {\n url += `?17=${normalizeExpiry(opts.expiry)}`;\n }\n return url;\n}\n","/**\n * @ai-matrx/print/zpl — RAW ZPL II FOR ZEBRA-CLASS THERMAL PRINTERS.\n *\n * THE OPT-IN POWER LANE. Every other printer in this package composes an HTML\n * document and hands it to the OS print dialog, which rasterizes it. That is\n * the right default: it works on every printer with a plain driver, needs no\n * vendor SDK, and previews honestly on screen.\n *\n * It is also the wrong tool for one specific job. A browser rasterizes a QR at\n * whatever DPI the driver negotiates and then the printer re-samples it, so on\n * 203-dpi roll stock a small symbol's modules land on fractional dots and blur.\n * A ZPL `^BQ` command instead tells the printer \"draw this symbol at exactly N\n * dots per module\" — the modules are dot-aligned by construction, which is why\n * a raw-ZPL label out-scans a rasterized one at the dock door every time.\n *\n * SCOPE — deliberately narrow:\n * - ROLL STOCK ONLY. ZPL's page model is one continuous label; a 30-up Avery\n * sheet is a laser-printer artifact with no ZPL equivalent. Passing a sheet\n * template throws and names `printQrLabelSheet` instead.\n * - PURE STRING CODEGEN. Zero new dependencies, zero I/O. This module builds\n * the ZPL text; sending it to a printer (Browser Print, a raw TCP 9100\n * socket, a print server) is the host's job and stays out of the package.\n * - DETERMINISTIC. The same template + label + options always produce a\n * byte-identical string. Nothing here reads a clock, a random source, or the\n * DOM, and the module is import-inert with no mutable state.\n *\n * GEOMETRY comes from the SAME brain the HTML lane uses: `computeCellLayout`\n * from `./labels`, so the QR box, padding and badge zone are identical across\n * the two lanes and a design verified on screen prints the same on the Zebra.\n * Inches become dots by `round(inches * dpi)` — 203 dpi (8 dots/mm) is the\n * QL/ZD-class norm and the default; 300 dpi (12 dots/mm) is the other common\n * head. There is no 600-dpi option because no roll printer we recommend has one.\n *\n * Every command emitted below carries a one-line comment. For many readers of\n * this repo this file is their first ZPL, and an uncommented `^FO12,34^A0N,28,0`\n * is indistinguishable from line noise.\n *\n * Docs: FEATURE.md, NOTES-CHANGELOG.md, and\n * common-docs/projects/npm-package-extraction/PRINT-PACKAGE-DESIGN.md.\n */\n\nimport {\n computeCellLayout,\n MIN_SCANNABLE_MODULE_MM,\n type LabelTemplate,\n type QrLabel,\n type ScannabilityReport,\n} from \"./labels\";\nimport { minQrVersion, qrModuleCount, type QrEcLevel } from \"./qr\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\n/**\n * Printhead resolution in dots per inch.\n *\n * 203 = 8 dots/mm — the Zebra ZD/GK/GX and Brother QL class; the default.\n * 300 = 12 dots/mm — the higher-resolution ZD621/ZT heads.\n */\nexport type ZplDpi = 203 | 300;\n\nexport interface ZplOptions {\n /** Printhead resolution. Default 203 — the QL/ZD-class norm. */\n dpi?: ZplDpi;\n /**\n * Burn temperature offset, emitted as `^MD`. Range -30..30, where 0 is the\n * printer's configured darkness. OMITTED ENTIRELY when unset, so the\n * printer keeps whatever the operator calibrated — we never silently\n * override a tuned head.\n */\n darkness?: number;\n /** Labels printed per block, emitted as `^PQ`. Default 1 (no `^PQ` line). */\n copies?: number;\n /**\n * QR error correction. Default \"M\" — the platform default from `./qr`,\n * NEVER \"L\": these labels get scuffed and read at an angle.\n *\n * (Additive to the wave-3 spec's three fields: the HTML lane exposes error\n * correction as a first-class setting, and a ZPL lane that could not match\n * it would silently print a different symbol from the same design.)\n */\n ecLevel?: QrEcLevel;\n}\n\n/** ZPL's `^BQ` magnification is dots-per-module and is bounded 1..10 by the firmware. */\nexport const ZPL_MIN_MAGNIFICATION = 1;\nexport const ZPL_MAX_MAGNIFICATION = 10;\n\n/**\n * ISO/IEC 18004 §6.3.8 quiet zone, in modules.\n *\n * `^BQ` draws the SYMBOL ONLY — unlike the `qrcode` data URIs the HTML lane\n * inlines, the printer adds no margin of its own. The quiet zone is therefore\n * ours to reserve: we size the symbol against `modules + 8` and inset the field\n * origin by 4 magnified modules on each side.\n */\nexport const ZPL_QUIET_ZONE_MODULES = 4;\n\nexport const DEFAULT_ZPL_DPI: ZplDpi = 203;\nconst DEFAULT_ZPL_EC_LEVEL: QrEcLevel = \"M\";\n\nconst MM_PER_IN = 25.4;\n\n/** The exact guidance a sheet template earns. Exported so tests and hosts share one string. */\nexport const ZPL_SHEET_STOCK_MESSAGE =\n \"ZPL is for roll stock — use printQrLabelSheet for sheets\";\n\n// ─── Option resolution ───────────────────────────────────────────────────────\n\ninterface ResolvedZplOptions {\n dpi: ZplDpi;\n darkness: number | undefined;\n copies: number;\n ecLevel: QrEcLevel;\n}\n\nfunction resolveOptions(opts: ZplOptions | undefined): ResolvedZplOptions {\n const dpi = opts?.dpi ?? DEFAULT_ZPL_DPI;\n if (dpi !== 203 && dpi !== 300) {\n throw new Error(\n `ZPL dpi must be 203 (8 dots/mm) or 300 (12 dots/mm), got ${String(dpi)}.`,\n );\n }\n\n const copies = opts?.copies ?? 1;\n if (!Number.isInteger(copies) || copies < 1 || copies > 9999) {\n throw new Error(`ZPL copies must be an integer between 1 and 9999, got ${String(copies)}.`);\n }\n\n const rawDarkness = opts?.darkness;\n let darkness: number | undefined;\n if (rawDarkness !== undefined) {\n if (!Number.isInteger(rawDarkness) || rawDarkness < -30 || rawDarkness > 30) {\n throw new Error(\n `ZPL darkness (^MD) must be an integer between -30 and 30, got ${String(rawDarkness)}.`,\n );\n }\n darkness = rawDarkness;\n }\n\n const ecLevel = opts?.ecLevel ?? DEFAULT_ZPL_EC_LEVEL;\n if (ecLevel !== \"L\" && ecLevel !== \"M\" && ecLevel !== \"Q\" && ecLevel !== \"H\") {\n throw new Error(\n `ZPL QR error correction must be \"L\", \"M\", \"Q\" or \"H\", got ${String(ecLevel)}.`,\n );\n }\n\n // exactOptionalPropertyTypes: `darkness` is `number | undefined` by type,\n // and the emitter checks for undefined — no key is ever set to undefined.\n return { dpi, darkness, copies, ecLevel };\n}\n\nfunction assertRollStock(template: LabelTemplate): void {\n if (template.kind !== \"roll\") {\n throw new Error(\n `${ZPL_SHEET_STOCK_MESSAGE} (template \"${template.id}\" — ${template.stockCode} — is ` +\n `${template.cols}×${template.rows} sheet stock, which has no ZPL page model).`,\n );\n }\n}\n\n// ─── Field data escaping (^FH) ───────────────────────────────────────────────\n\nconst HEX = \"0123456789ABCDEF\";\n\n/**\n * Escape arbitrary text for a `^FD` field.\n *\n * THE CHOICE: we ALWAYS pair `^FD` with `^FH` (hex escape) and encode anything\n * that is not plain, safe printable ASCII as `_xx` UTF-8 bytes. The alternative\n * — swapping ZPL's control/format/delimiter characters with `^CC`/`^CT`/`^CD`\n * — mutates printer state that outlives the label, so a caption containing a\n * caret could change how every SUBSEQUENT label parses. Hex escaping is local\n * to the one field and cannot leak.\n *\n * Escaped, and why:\n * - `^` (0x5E) — the ZPL format-command prefix; unescaped it starts a command.\n * - `~` (0x7E) — the ZPL control-command prefix; same failure, immediately.\n * - `_` (0x5F) — the `^FH` escape character itself, so literal underscores in a\n * SKU survive instead of eating the next two characters.\n * - Anything outside 0x20..0x7E — control characters (a stray newline or NUL\n * would truncate the field) and every non-ASCII byte. `^CI28` puts the\n * printer in UTF-8, so an \"é\" is emitted as its two UTF-8 bytes `_C3_A9`.\n *\n * Everything else passes through verbatim, which keeps a normal SKU readable in\n * the generated ZPL — an operator debugging a label can still find the string.\n */\nexport function zplFieldData(text: string): string {\n if (typeof text !== \"string\") {\n throw new Error(`ZPL field data must be a string, got ${String(text)}.`);\n }\n if (text === \"\") return \"\";\n\n const bytes = new TextEncoder().encode(text);\n let out = \"\";\n for (const byte of bytes) {\n const safe =\n byte >= 0x20 &&\n byte <= 0x7e &&\n byte !== 0x5e /* ^ */ &&\n byte !== 0x7e /* ~ — the last printable byte, and ZPL's control prefix */ &&\n byte !== 0x5f; /* _ */\n if (safe) {\n out += String.fromCharCode(byte);\n } else {\n // `^FH`'s default escape character is `_`, followed by two hex digits.\n out += `_${HEX[(byte >> 4) & 0xf] ?? \"0\"}${HEX[byte & 0xf] ?? \"0\"}`;\n }\n }\n return out;\n}\n\n// ─── Geometry ────────────────────────────────────────────────────────────────\n\n/** Inches → printer dots. The one conversion; every coordinate below goes through it. */\nfunction dots(inches: number, dpi: ZplDpi): number {\n return Math.round(inches * dpi);\n}\n\nfunction clampMagnification(value: number): number {\n if (!Number.isFinite(value)) return ZPL_MIN_MAGNIFICATION;\n return Math.min(ZPL_MAX_MAGNIFICATION, Math.max(ZPL_MIN_MAGNIFICATION, Math.floor(value)));\n}\n\nfunction payloadByteLength(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\n/**\n * Dots per QR module for a payload inside a box of `qrBoxDots`.\n *\n * The box holds the symbol AND both quiet zones, so the divisor is\n * `qrModuleCount(version) + 2 * 4`. `floor` (never round) keeps the drawn\n * symbol inside its box; the clamp is the firmware's own 1..10 limit, which\n * means a box too small for even one dot per module still emits a valid label —\n * an unscannable one, which is exactly what `assertZplScannable` reports.\n */\nfunction magnificationFor(qrBoxDots: number, payloadBytes: number, ecLevel: QrEcLevel): QrFit {\n const version = minQrVersion(payloadBytes, ecLevel);\n const modulesPerSide = qrModuleCount(version) + ZPL_QUIET_ZONE_MODULES * 2;\n const magnification = clampMagnification(qrBoxDots / modulesPerSide);\n return { version, magnification, drawnDots: modulesPerSide * magnification };\n}\n\ninterface QrFit {\n version: number;\n magnification: number;\n /**\n * What the printer ACTUALLY draws, quiet zone included: `modules × mag`.\n *\n * This is ≤ the layout box, sometimes well below it — `mag` is a whole\n * number of dots and caps at 10, so a short payload on large stock cannot\n * fill its box. Layout uses THIS, never the box: centring a 290-dot symbol\n * inside a 455-dot box leaves it visibly off-centre with a phantom gap\n * underneath (confirmed on a Labelary render during development).\n */\n drawnDots: number;\n}\n\n/** The caption zone height, mirroring the HTML lane's two branches exactly. */\nfunction captionZoneIn(t: LabelTemplate, wide: boolean): number {\n return wide ? t.labelHIn * 0.3 : Math.min(0.28, t.labelHIn * 0.16);\n}\n\n// ─── Command emitters (one comment per command) ──────────────────────────────\n\n/**\n * A scalable-font text field.\n *\n * `^FO x,y` — field origin, top-left of the field, in dots from ^LH.\n * `^A0N,h,0` — font 0 (the built-in scalable), N = normal rotation,\n * h dots tall, width 0 = keep the font's own aspect ratio.\n * `^FB w,1,0,j` — field block: wrap/justify inside w dots, at most 1 line\n * (overflow is dropped, never spilled onto the neighbour),\n * 0 extra line spacing, j = L(eft) or C(enter).\n * `^FH` — the following ^FD uses `_xx` hex escapes.\n * `^FD…^FS` — the field data, terminated.\n */\nfunction textField(\n x: number,\n y: number,\n widthDots: number,\n fontHeightDots: number,\n justify: \"L\" | \"C\",\n text: string,\n): string {\n return (\n `^FO${x},${y}` +\n `^A0N,${fontHeightDots},0` +\n `^FB${widthDots},1,0,${justify}` +\n `^FH^FD${zplFieldData(text)}^FS`\n );\n}\n\n/**\n * A QR symbol.\n *\n * `^FO x,y` — origin of the SYMBOL, already inset by the quiet zone.\n * `^BQN,2,mag` — QR barcode: N = normal rotation, model 2 (the enhanced\n * model every current scanner reads), mag dots per module.\n * Error correction is NOT passed here: the `d` parameter is\n * firmware-dependent, while the ^FD prefix below is honoured\n * by every ZPL II printer.\n * `^FH^FD<ec>A,…` — ^FD for a QR carries switches before the data: the error\n * correction letter (H/Q/M/L), then `A` for AUTOMATIC input\n * mode (the printer picks numeric/alphanumeric/byte itself),\n * then a comma, then the payload. ^FH hex-escapes the\n * payload only — the switches are plain ASCII.\n */\nfunction qrField(x: number, y: number, magnification: number, ecLevel: QrEcLevel, value: string): string {\n return `^FO${x},${y}^BQN,2,${magnification}^FH^FD${ecLevel}A,${zplFieldData(value)}^FS`;\n}\n\n// ─── The one-label renderer ──────────────────────────────────────────────────\n\nfunction cleanLines(lines: readonly string[] | undefined): string[] {\n if (lines === undefined) return [];\n return lines.map((l) => l.trim()).filter((l) => l !== \"\");\n}\n\n/**\n * ONE roll label → a complete `^XA…^XZ` block.\n *\n * The block always opens with the same four lines so a label never inherits\n * state from whatever the printer last received:\n * `^XA` — start of label format.\n * `^CI28` — character set 28 = UTF-8, so `^FH` hex bytes decode as UTF-8.\n * `^LH0,0` — label home back to the physical origin (a previous format\n * may have shifted it; this is the classic \"everything is\n * offset by 30 dots\" bug).\n * `^PW` / `^LL` — print width and label length in dots, from the template's\n * inch fields. Roll stock IS the label, so these are the\n * die-cut dimensions.\n *\n * Zone order mirrors the HTML lane: QR, then the large badge, then the bold\n * caption, then the small detail lines — stacked and centred on tall stock,\n * QR-left/text-right on stock at least 1.8× as wide as it is tall.\n *\n * @throws if `template.kind !== \"roll\"` — see ZPL_SHEET_STOCK_MESSAGE.\n */\nexport function labelToZpl(template: LabelTemplate, label: QrLabel, opts?: ZplOptions): string {\n assertRollStock(template);\n const o = resolveOptions(opts);\n\n const captionText = (label.caption ?? label.qrValue).trim();\n const badgeText = (label.badge ?? \"\").trim();\n const lines = cleanLines(label.lines);\n const hasQr = typeof label.qrValue === \"string\" && label.qrValue !== \"\";\n\n // The SAME layout brain the HTML/PDF lanes use — never a forked copy.\n const layout = computeCellLayout(template, captionText !== \"\", badgeText !== \"\");\n\n const labelWDots = dots(template.labelWIn, o.dpi);\n const labelHDots = dots(template.labelHIn, o.dpi);\n const padDots = dots(layout.padIn, o.dpi);\n const qrBoxDots = hasQr ? dots(layout.qrIn, o.dpi) : 0;\n const badgeZoneDots = badgeText !== \"\" ? dots(layout.badgeZoneIn, o.dpi) : 0;\n const captionZoneDots =\n captionText !== \"\" ? dots(captionZoneIn(template, layout.wide), o.dpi) : 0;\n\n // Detail lines are a fixed 8pt, matching the HTML lane's `font-size:8pt`.\n const lineFontDots = Math.max(8, Math.round((o.dpi * 8) / 72));\n const lineStepDots = Math.round(lineFontDots * 1.25);\n const linesHDots = lines.length * lineStepDots;\n\n // Text is 80% of the badge zone and 72% of the caption zone; the remainder\n // is the leading that keeps a descender off the next zone.\n const badgeFontDots = Math.max(10, Math.round(badgeZoneDots * 0.8));\n const captionFontDots = Math.max(8, Math.round(captionZoneDots * 0.72));\n\n const commands: string[] = [];\n commands.push(\"^XA\"); // start of label format\n commands.push(\"^CI28\"); // UTF-8 input encoding\n commands.push(\"^LH0,0\"); // reset label home to the physical origin\n commands.push(`^PW${dots(template.labelWIn, o.dpi)}`); // print width, dots\n commands.push(`^LL${dots(template.labelHIn, o.dpi)}`); // label length, dots\n if (o.darkness !== undefined) {\n commands.push(`^MD${o.darkness}`); // burn-temperature offset, -30..30\n }\n\n const fit: QrFit = hasQr\n ? magnificationFor(qrBoxDots, payloadByteLength(label.qrValue), o.ecLevel)\n : { version: 0, magnification: 0, drawnDots: 0 };\n // The quiet zone is drawn by us, not by ^BQ: inset the symbol by 4 modules.\n const quietDots = fit.magnification * ZPL_QUIET_ZONE_MODULES;\n\n if (layout.wide) {\n // WIDE: QR on the left, vertically centred; text column to its right.\n const qrY = Math.max(padDots, Math.round((labelHDots - fit.drawnDots) / 2));\n if (hasQr) {\n commands.push(\n qrField(padDots + quietDots, qrY + quietDots, fit.magnification, o.ecLevel, label.qrValue),\n );\n }\n const textX = padDots + fit.drawnDots + padDots;\n const textW = Math.max(1, labelWDots - textX - padDots);\n let y = padDots;\n if (badgeText !== \"\") {\n commands.push(textField(textX, y, textW, badgeFontDots, \"L\", badgeText));\n y += badgeZoneDots;\n }\n if (captionText !== \"\") {\n commands.push(textField(textX, y, textW, captionFontDots, \"L\", captionText));\n y += captionZoneDots;\n }\n for (const line of lines) {\n commands.push(textField(textX, y, textW, lineFontDots, \"L\", line));\n y += lineStepDots;\n }\n } else {\n // STACKED: QR on top, everything centred, the whole block centred\n // vertically the way the HTML lane's flex column does.\n const contentH = fit.drawnDots + badgeZoneDots + captionZoneDots + linesHDots;\n const textW = Math.max(1, labelWDots - padDots * 2);\n let y = Math.max(padDots, Math.round((labelHDots - contentH) / 2));\n if (hasQr) {\n const qrX = Math.max(0, Math.round((labelWDots - fit.drawnDots) / 2));\n commands.push(\n qrField(qrX + quietDots, y + quietDots, fit.magnification, o.ecLevel, label.qrValue),\n );\n y += fit.drawnDots;\n }\n if (badgeText !== \"\") {\n commands.push(textField(padDots, y, textW, badgeFontDots, \"C\", badgeText));\n y += badgeZoneDots;\n }\n if (captionText !== \"\") {\n commands.push(textField(padDots, y, textW, captionFontDots, \"C\", captionText));\n y += captionZoneDots;\n }\n for (const line of lines) {\n commands.push(textField(padDots, y, textW, lineFontDots, \"C\", line));\n y += lineStepDots;\n }\n }\n\n if (o.copies > 1) {\n // ^PQ q,0,0,N — print quantity q, no pause/cut group, no cut-on-error.\n commands.push(`^PQ${o.copies},0,0,N`);\n }\n commands.push(\"^XZ\"); // end of label format — the printer prints here\n\n return commands.join(\"\\n\");\n}\n\n/**\n * BATCH: one ZPL document, one `^XA…^XZ` block per label.\n *\n * There is no shared preamble between blocks on purpose — each block is\n * self-sufficient (`^CI28`, `^LH`, `^PW`, `^LL`), so a stalled or re-sent block\n * still prints correctly and a host can split the document anywhere.\n *\n * An empty list returns an empty string: nothing to print is not an error.\n */\nexport function labelsToZpl(\n template: LabelTemplate,\n labels: readonly QrLabel[],\n opts?: ZplOptions,\n): string {\n assertRollStock(template);\n resolveOptions(opts); // validate once, up front, even for an empty batch\n if (labels.length === 0) return \"\";\n return labels.map((label) => labelToZpl(template, label, opts)).join(\"\\n\");\n}\n\n// ─── Scannability (the ZPL twin of labels' assertScannable) ──────────────────\n\n/**\n * Will a payload of `payloadByteLen` bytes actually scan off this roll stock in\n * the ZPL lane?\n *\n * Same verdict shape and same 0.4mm floor as `assertScannable` in `./labels`,\n * but measured through ZPL's own quantization: the printed module size is\n * `magnification × (25.4 / dpi)` mm, because `^BQ` draws a module as a whole\n * number of dots. That quantization is the entire point of this lane — and it\n * is also why a box that would have been fine as a rasterized image can round\n * DOWN to an unscannable magnification here.\n *\n * Never throws. A sheet template, an over-capacity payload, and a too-dense\n * combination all come back as `ok: false` with the reason in `message`.\n */\nexport function assertZplScannable(\n template: LabelTemplate,\n payloadByteLen: number,\n opts?: ZplOptions,\n): ScannabilityReport {\n const fail = (message: string, version = 0, moduleSizeMm = 0): ScannabilityReport => ({\n ok: false,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n message,\n });\n\n if (template.kind !== \"roll\") {\n return fail(\n `${ZPL_SHEET_STOCK_MESSAGE} (template \"${template.id}\" — ${template.stockCode} — is sheet stock).`,\n );\n }\n\n let o: ResolvedZplOptions;\n try {\n o = resolveOptions(opts);\n } catch (err) {\n return fail(err instanceof Error ? err.message : String(err));\n }\n\n // Conservative on purpose, exactly like the HTML lane: evaluate with the\n // caption ON, which is the SMALLER QR box.\n const layout = computeCellLayout(template, true);\n const qrBoxDots = dots(layout.qrIn, o.dpi);\n\n let fit: QrFit;\n try {\n fit = magnificationFor(qrBoxDots, payloadByteLen, o.ecLevel);\n } catch (err) {\n // An over-capacity payload (no QR version holds it) reports, never throws.\n return fail(err instanceof Error ? err.message : String(err));\n }\n\n const { version, magnification } = fit;\n // ONE magnified module is `mag` whole dots wide — that is the printed size.\n const moduleSizeMm = (magnification * MM_PER_IN) / o.dpi;\n\n if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {\n return {\n ok: true,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n };\n }\n return fail(\n `A ${payloadByteLen}-byte payload needs a version ${version} QR, which ZPL prints at ` +\n `magnification ${magnification} (${moduleSizeMm.toFixed(3)}mm per module) on ` +\n `${template.stockCode} at ${o.dpi}dpi — below the ${MIN_SCANNABLE_MODULE_MM}mm floor a ` +\n `scanner can resolve. Shorten the payload (a short resolver URL is the intended fix), ` +\n `move to larger roll stock, or print on a 300dpi head.`,\n version,\n moduleSizeMm,\n );\n}\n\nexport type { ScannabilityReport } from \"./labels\";\n"],"mappings":"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","names":["escapeHtml","escapeHtml","caption","badge","MM_PER_IN","version","moduleSizeMm"]}
1
+ {"version":3,"sources":["../src/core.ts","../src/katex-css.generated.ts","../src/labels.ts","../src/qr.ts","../src/zpl.ts"],"sourcesContent":["/**\n * @ai-matrx/print/core — the Block Print contract & print-window composition.\n *\n * Ported VERBATIM from matrx-frontend `lib/block-print/block-print-utils.ts`\n * (2026-08-29 state, including the `number` and `select` PrintSetting types the\n * commerce label lane added). Zero dependencies; touches `window` only at call\n * time, so the module stays import-inert and RSC-safe.\n *\n * BlockPrinter is the only interface a host system needs to know about.\n * Each printable unit exports its own BlockPrinter alongside its render\n * component; host code only ever calls print(data, variantId, settings).\n *\n * `buildFitTextScript` is the ONE auto-shrink script (the flashcards\n * FIT_TEXT pattern, parameterized by selector so the label lane shares it).\n */\n\n// THE ONE HTML escaper (duplication census row 14). Nine copies of\n// `escapeHtml` were live across the fleet and THEY DID NOT ESCAPE THE SAME\n// CHARACTERS — a string safe through one was an attribute break-out through\n// another. It moved to `@ai-matrx/kit/html-escape` with a decided character\n// set: an HTML escaper must not require this package's jspdf + html2canvas\n// graph. Not re-exported from here; import `@ai-matrx/kit/html-escape`.\nimport { downloadFile } from \"@ai-matrx/kit/download\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { KATEX_CSS, KATEX_CSS_MARKER } from \"./katex-css.generated\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\nexport interface PrintVariant {\n id: string;\n label: string;\n description?: string;\n}\n\n/**\n * A single user-configurable setting shown in the print dialog.\n * Extend the discriminated union for new control types\n * without breaking existing printers.\n */\nexport type PrintSetting =\n | {\n type: \"boolean\";\n id: string;\n label: string;\n description?: string;\n /** Default value shown when the dialog first opens */\n defaultValue: boolean;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /**\n * A numeric \"from → to\" range control, rendered as two small number inputs.\n * Writes two entries into the resolved PrintSettings map — one under fromId,\n * one under toId. Printers interpret 0 (or missing) as \"no bound\" by convention.\n */\n type: \"range\";\n /** Stable id used as React key — not written to the settings map */\n id: string;\n label: string;\n description?: string;\n /** Settings map key for the lower bound value */\n fromId: string;\n /** Settings map key for the upper bound value */\n toId: string;\n defaultFrom: number;\n defaultTo: number;\n /** Minimum accepted value for each input (inclusive). Default 0. */\n min?: number;\n /** Placeholder shown when the input is empty (0). Default \"start\" / \"end\". */\n fromPlaceholder?: string;\n toPlaceholder?: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A single numeric input (e.g. \"start at label #\", \"copies\"). */\n type: \"number\";\n id: string;\n label: string;\n description?: string;\n defaultValue: number;\n /** Minimum accepted value (inclusive). Default 0. */\n min?: number;\n /** Maximum accepted value (inclusive). Omit = unbounded. */\n max?: number;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n }\n | {\n /** A one-of-N choice rendered as a compact segmented row. */\n type: \"select\";\n id: string;\n label: string;\n description?: string;\n options: { value: string; label: string }[];\n defaultValue: string;\n /** Which variant IDs this setting applies to. Omit = applies to all. */\n appliesTo?: string[];\n };\n\n/** A resolved map of setting id → current value, passed to print(). */\nexport type PrintSettings = Record<string, boolean | string | number>;\n\n/**\n * The self-contained print capability that each block can export.\n * Host code only ever calls print(data, variantId, settings) — it never inspects internals.\n */\nexport interface BlockPrinter {\n /** Tooltip / aria-label for the print button in the block header */\n label: string;\n\n /**\n * Variants offered to the user before printing.\n * Empty array = no options dialog shown; prints immediately with defaults.\n */\n variants: PrintVariant[];\n\n /**\n * Optional user-configurable settings shown below the variant picker.\n * Omit entirely if the block has no configurable settings.\n */\n settings?: PrintSetting[];\n\n /**\n * Execute the print.\n * data — the block's structured data (serverData or parsed content)\n * variantId — which variant was chosen (undefined = default / no variants)\n * settings — resolved setting values chosen by the user\n *\n * MAY resolve to a PrintOutcome (\"opened\" | \"downloaded\") so the host can\n * tell the user when the popup-blocked download fallback fired; `void`\n * stays legal for printers that don't go through a print window.\n */\n print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | PrintOutcome | Promise<void | PrintOutcome>;\n}\n\n// ─── Shared Utilities ─────────────────────────────────────────────────────────\n\nconst PRINT_STYLES = `\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n font-size: 11pt;\n line-height: 1.6;\n color: #1a1a1a;\n background: #fff;\n max-width: 860px;\n margin: 0 auto;\n padding: 28px 36px;\n }\n\n .print-actions {\n display: flex;\n gap: 10px;\n margin-bottom: 24px;\n padding-bottom: 18px;\n border-bottom: 2px solid #e5e7eb;\n }\n\n .print-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n border: none;\n font-weight: 600;\n }\n\n .print-btn-primary { background: #2563eb; color: #fff; }\n .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }\n\n h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }\n h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }\n h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }\n p { margin-bottom: 0.7em; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n font-size: 10pt;\n }\n th {\n background: #1e293b;\n color: #f8fafc;\n font-weight: 600;\n text-align: left;\n padding: 7px 12px;\n border: 1px solid #334155;\n }\n td {\n padding: 6px 12px;\n border: 1px solid #e2e8f0;\n vertical-align: top;\n }\n tr:nth-child(even) td { background: #f8fafc; }\n\n @media print {\n body { max-width: 100%; padding: 0; }\n .print-actions { display: none !important; }\n th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n }\n`;\n\n/**\n * Wrap body HTML in a complete, styled print document.\n */\nexport function buildPrintDocument(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${escapeHtml(title)}</title>\n <style>\n${PRINT_STYLES}\n${extraStyles}\n </style>\n</head>\n<body>\n <div class=\"print-actions\">\n <button class=\"print-btn print-btn-primary\" onclick=\"window.print()\">Print / Save as PDF</button>\n <button class=\"print-btn print-btn-secondary\" onclick=\"window.close()\">Close</button>\n </div>\n <div class=\"content\">\n${bodyHtml}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * How a print request actually reached the user.\n *\n * \"opened\" — a print window/tab was opened and the document written into it.\n * \"downloaded\" — the popup was blocked, so the document was downloaded as an\n * .html file instead (the contract's fallback). Hosts SHOULD\n * surface this to the user (e.g. a toast saying the print file\n * was downloaded) — a blocked popup plus a silent download is\n * indistinguishable from \"nothing happened\", which is exactly\n * how QA filed it (2026-08-30, F5/F6/F8).\n */\nexport type PrintOutcome = \"opened\" | \"downloaded\";\n\n/**\n * Open a new browser window and write a complete HTML document into it.\n * Falls back to downloading an .html file if the popup is blocked.\n *\n * Returns which path was taken so callers can tell the user when the\n * fallback fired. `windowFeatures` customizes the popup geometry (the\n * flashcards fixed-geometry variants pass a wider window).\n */\n/**\n * Every printed document leaves through here, so this is where typeset math\n * gets its stylesheet: when the page carries KaTeX output and does not already\n * carry the sheet, it is added to <head>. A page with no math is untouched,\n * byte for byte (the flashcard golden master holds every printer to that).\n */\nexport function withMathStyles(htmlDocument: string): string {\n if (!htmlDocument.includes('class=\"katex\"') || htmlDocument.includes(KATEX_CSS_MARKER)) {\n return htmlDocument;\n }\n const style = `<style data-matrx-katex>${KATEX_CSS}</style>`;\n const head = htmlDocument.search(/<\\/head>/i);\n return head >= 0\n ? htmlDocument.slice(0, head) + style + htmlDocument.slice(head)\n : style + htmlDocument;\n}\n\nexport function openPrintWindow(\n rawDocument: string,\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n const win = window.open(\"\", \"_blank\", windowFeatures);\n // Popup blocked — fall back to file download.\n if (!win) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n}\n\n/** A print window opened before its document is ready (see `openPendingPrintWindow`). */\nexport interface PendingPrintWindow {\n /** Write the finished document; downloads it when the popup was blocked or closed. */\n write(htmlDocument: string): PrintOutcome;\n}\n\n/**\n * Open the print window NOW — inside the click, where popup blockers allow it —\n * and write the document later, once async preparation (drawing diagrams) is\n * done. The window says \"Preparing…\" meanwhile.\n */\nexport function openPendingPrintWindow(\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PendingPrintWindow {\n const win = window.open(\"\", \"_blank\", windowFeatures);\n if (win) {\n win.document.open();\n win.document.write(\n '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>Preparing…</title></head><body style=\"font:14px -apple-system,BlinkMacSystemFont,sans-serif;color:#555;padding:2rem\">Preparing the document for print…</body></html>',\n );\n win.document.close();\n }\n return {\n write(rawDocument: string): PrintOutcome {\n const htmlDocument = withMathStyles(rawDocument);\n if (!win || win.closed) return openPrintWindowFallback(htmlDocument, filename);\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n },\n };\n}\n\nfunction openPrintWindowFallback(htmlDocument: string, filename: string): PrintOutcome {\n downloadFile(`${filename.replace(/\\s+/g, \"-\").toLowerCase()}.html`, htmlDocument, \"text/html;charset=utf-8\");\n return \"downloaded\";\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n * Returns the same PrintOutcome as openPrintWindow.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): PrintOutcome {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n return openPrintWindow(doc, title);\n}\n\n/**\n * Build the auto-shrink script that runs INSIDE a printed window: for every\n * element matching each selector, its font-size is reduced (0.5pt steps, 6pt\n * floor) until it fits its parent element's box. This is the flashcards\n * FIT_TEXT behavior verbatim, parameterized by selector so every lane\n * (flashcards card text, Avery text, label captions) shares the ONE loop.\n *\n * Pass the same selectors the original used to get byte-identical behavior:\n * flashcards passes `[\".card-text .card-text-inner\", \".avery-text-wrap .avery-text\"]`.\n */\nexport function buildFitTextScript(selectors: readonly string[]): string {\n const selectorsJson = JSON.stringify([...selectors]);\n return `\n<script>\n(function() {\n function fitText(el, container) {\n // container is the bounding box we must fit inside\n var maxW = container.clientWidth;\n var maxH = container.clientHeight;\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var minSize = 6;\n // Give the element a chance to reflow at current size before measuring\n el.style.fontSize = size + \"pt\";\n while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"pt\";\n }\n }\n function runAll() {\n var selectors = ${selectorsJson};\n for (var s = 0; s < selectors.length; s++) {\n var els = document.querySelectorAll(selectors[s]);\n for (var i = 0; i < els.length; i++) {\n fitText(els[i], els[i].parentElement);\n }\n }\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", runAll);\n } else {\n // document.write pages may already be loaded\n setTimeout(runAll, 0);\n }\n})();\n</script>`;\n}\n\n","// GENERATED by scripts/gen-katex-css.mjs from katex@0.18.9 — do not edit.\nexport const KATEX_VERSION = \"0.18.9\";\n/** Leads KATEX_CSS; lets openPrintWindow see the sheet is already present. */\nexport const KATEX_CSS_MARKER = \"/* matrx-katex@0.18.9 */\";\nexport const KATEX_CSS = \"/* matrx-katex@0.18.9 */@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_AMS-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Caligraphic-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Fraktur-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Main-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-BoldItalic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Math-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:700;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Bold.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:italic;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Italic.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:\\\"KaTeX_SansSerif\\\";font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_SansSerif-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Script-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size1-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size2-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size3-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Size4-Regular.ttf) format(\\\"truetype\\\")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff2) format(\\\"woff2\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.woff) format(\\\"woff\\\"),url(https://cdn.jsdelivr.net/npm/katex@0.18.9/dist/fonts/KaTeX_Typewriter-Regular.ttf) format(\\\"truetype\\\")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:\\\"0.18.9\\\"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:\\\"\\\"}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:\\\"(\\\" counter(katexEqnNo) \\\")\\\";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:\\\"(\\\" counter(mmlEqnNo) \\\")\\\";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}\\n\";\n","/**\n * @ai-matrx/print/labels — the label-sheet engine.\n *\n * ONE module carrying the three matrx-frontend files this lane was extracted\n * from, ported behaviour-verbatim (2026-08-29 state):\n * lib/label-print/label-templates.ts → the template registry\n * lib/label-print/qr-labels-printer.ts → qrLabelsPrinter / printQrLabelSheet\n * lib/label-print/qr-labels-pdf.ts → downloadLabelsPdf\n *\n * Warehouse-grade QR labels on standard 8.5\" × 11\" label stock (Avery and\n * compatibles) AND on continuous/die-cut roll stock. ONE generic grid renderer\n * driven by the LabelTemplate registry — geometry is data, never per-stock CSS.\n *\n * Fidelity rules (the reasons this exists):\n * - The print window is a fresh, unauthenticated document: every QR image is\n * inlined as a data URI, never fetched.\n * - Spec-compliant quiet zone: the `qrcode` lib renders 4 modules of white\n * margin INSIDE each data URI (its default; we pass it explicitly).\n * - `print-color-adjust: exact` on code cells so the browser never \"helps\".\n * - Modules are crisp: each QR is rasterized at 300 dpi for its printed size,\n * and `image-rendering: pixelated` stops resampling blur.\n * - Captions auto-shrink to fit their box (FIT_TEXT pattern) — they never\n * overflow onto the neighboring label.\n * - `startAtLabel` inserts LEADING blank cells so a partially-used sheet can\n * be reused; trailing cells pad out the last page.\n *\n * THE IMAGE RULE — the one deliberate exception to \"data URIs only\": a label's\n * optional `imageUrl` is a DURABLE http(s) URL and is emitted as a plain\n * `<img src>`. Anything that is not http(s) is SKIPPED rather than rendered.\n * The QR itself is never a URL; it is always an inlined data URI.\n *\n * SEAM INVERSIONS versus the matrx-frontend originals (the only changes to the\n * ported behaviour):\n * 1. `@/lib/block-print/block-print-utils` → `./core` (identical export names).\n * 2. The deferred `import(\"qrcode\")` + `QRCode.toDataURL(...)` call is now\n * `generateQrDataUrl` from `./qr`, which performs the same lazy import at\n * call time and maps 1:1 — errorCorrectionLevel → ecLevel, margin →\n * quietZoneModules, width → widthPx. The original also passed\n * `color: { dark: \"#000000\", light: \"#ffffff\" }`, which is byte-for-byte the\n * `qrcode` default, so dropping it changes no output.\n * 3. LP_FIT_SCRIPT is KEPT VERBATIM rather than replaced by core's\n * `buildFitTextScript`. The two scripts are NOT behaviourally identical for\n * label cells: core's steps and writes font sizes in **pt** (and pre-assigns\n * `size + \"pt\"` from a px-derived number before measuring) with a **6pt**\n * floor, while the label script steps and writes **px** with a **4px** floor\n * and no pre-assignment. On a 9pt caption inside a 0.28in box those two\n * produce different final sizes, so swapping them would regress captions on\n * the small Avery stocks.\n *\n * Docs: lib/label-print/FEATURE.md (matrx-frontend), NOTES-CHANGELOG.md.\n */\n\nimport {\n openPrintWindow,\n type BlockPrinter,\n type PrintOutcome,\n type PrintSettings,\n} from \"./core\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport { generateQrDataUrl, minQrVersion, qrModuleCount } from \"./qr\";\n\n// ─── Template registry ───────────────────────────────────────────────────────\n\n/**\n * Label-sheet template registry — the geometry source of truth for QR label\n * printing on standard 8.5\" × 11\" label stock (Avery and compatibles) and on\n * roll stock.\n *\n * ONE generic grid renderer is driven entirely by these numbers — never a\n * hand-written CSS block per stock. All dimensions in inches, taken from\n * Avery's published templates:\n * - horizontal pitch = labelWIn + gutterXIn; vertical pitch = labelHIn + gutterYIn\n * - a template is valid when marginLeft*2 + cols*labelW + (cols-1)*gutterX = sheetW\n * (same vertically) — `assertTemplateGeometry` checks this in dev.\n */\nexport interface LabelTemplate {\n id: string;\n name: string;\n /** Stock code as printed on the box (e.g. \"Avery 5160\"). */\n stockCode: string;\n sheetWIn: number;\n sheetHIn: number;\n marginTopIn: number;\n marginLeftIn: number;\n labelWIn: number;\n labelHIn: number;\n cols: number;\n rows: number;\n /** Horizontal gap BETWEEN columns (not the outer margin). */\n gutterXIn: number;\n /** Vertical gap BETWEEN rows. */\n gutterYIn: number;\n /** Corner radius; equal to labelWIn/2 for round labels. */\n cornerRadiusIn?: number;\n /** True round label (die-cut circle) — preview/calibration draw a circle. */\n round?: boolean;\n /**\n * Stock family. Absent means \"sheet\" — a multi-up page of labels. \"roll\" is\n * a continuous/die-cut roll where the sheet IS the label: margins 0, a 1×1\n * grid, and `@page` sized to the single label.\n */\n kind?: \"sheet\" | \"roll\";\n}\n\n/**\n * The registry's first row, hoisted to a named const so the \"no template\n * matched\" fallback can reference it without an unchecked index access.\n *\n * 5160 — 1\" × 2⅝\" address, 30-up (3×10). Top/bottom margin 0.5\",\n * side margin 0.1875\", 0.125\" column gutter, zero row gutter.\n */\nconst AVERY_5160: LabelTemplate = {\n id: \"avery-5160\",\n name: 'Address 1\" × 2⅝\" — 30 per sheet',\n stockCode: \"Avery 5160 / 8160\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.5,\n marginLeftIn: 0.1875,\n labelWIn: 2.625,\n labelHIn: 1,\n cols: 3,\n rows: 10,\n gutterXIn: 0.125,\n gutterYIn: 0,\n cornerRadiusIn: 0.1,\n};\n\n/**\n * Seeded templates. Sheet geometry verified against Avery's published template\n * dimensions (avery.com template pages for each SKU):\n *\n * 5160 — see AVERY_5160 above.\n * 5163 — 2\" × 4\" shipping, 10-up (2×5). Top/bottom 0.5\", side 0.15625\",\n * 0.1875\" column gutter, zero row gutter.\n * 5164 — 3⅓\" × 4\" shipping, 6-up (2×3). Same horizontal grid as 5163.\n * 22806 — 2\" × 2\" rounded square, 12-up (3×4). Margins 0.5\" sides /\n * 0.75\" top, 0.75\" column gutter, 0.5\" row gutter — i.e. Avery's\n * published 2.75\" horizontal pitch and 2.5\" vertical pitch. (The\n * matrx-frontend original had 0.375\"/0.375\" here and tiled only 7.5\"\n * of the 8.5\" sheet; see NOTES-CHANGELOG.md.)\n * 22807 — 2\" round, 12-up (3×4). Same grid as 22806, circular die-cut.\n *\n * Roll stock (`kind: \"roll\"`) — the sheet IS the label, so every roll row is a\n * 1×1 grid with zero margins and zero gutters, and its die-cut size is given\n * as width × height in the feed orientation:\n *\n * DK-1202 — Brother die-cut shipping, 62mm × 100mm = 2.4\" × 3.9\".\n * DK-11201 — Brother die-cut address, 29mm × 90mm = 1.14\" × 3.5\".\n * 30252 — DYMO address, 1⅛\" × 3½\".\n * 4x6 — the generic 4\" × 6\" thermal shipping label.\n */\nexport const LABEL_TEMPLATES: readonly LabelTemplate[] = [\n AVERY_5160,\n {\n id: \"avery-5163\",\n name: 'Shipping 2\" × 4\" — 10 per sheet',\n stockCode: \"Avery 5163 / 8163\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.5,\n marginLeftIn: 0.15625,\n labelWIn: 4,\n labelHIn: 2,\n cols: 2,\n rows: 5,\n gutterXIn: 0.1875,\n gutterYIn: 0,\n cornerRadiusIn: 0.125,\n },\n {\n id: \"avery-5164\",\n name: 'Shipping 3⅓\" × 4\" — 6 per sheet',\n stockCode: \"Avery 5164 / 8164\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.5,\n marginLeftIn: 0.15625,\n labelWIn: 4,\n labelHIn: 3.3333333,\n cols: 2,\n rows: 3,\n gutterXIn: 0.1875,\n gutterYIn: 0,\n cornerRadiusIn: 0.125,\n },\n {\n id: \"avery-22806\",\n name: 'Square 2\" × 2\" — 12 per sheet',\n stockCode: \"Avery 22806\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.75,\n marginLeftIn: 0.5,\n labelWIn: 2,\n labelHIn: 2,\n cols: 3,\n rows: 4,\n gutterXIn: 0.75,\n gutterYIn: 0.5,\n cornerRadiusIn: 0.09375,\n },\n {\n id: \"avery-22807\",\n name: 'Round 2\" — 12 per sheet',\n stockCode: \"Avery 22807\",\n sheetWIn: 8.5,\n sheetHIn: 11,\n marginTopIn: 0.75,\n marginLeftIn: 0.5,\n labelWIn: 2,\n labelHIn: 2,\n cols: 3,\n rows: 4,\n gutterXIn: 0.75,\n gutterYIn: 0.5,\n cornerRadiusIn: 1,\n round: true,\n },\n {\n id: \"brother-dk-1202\",\n name: 'Shipping roll 2.4\" × 3.9\" — die-cut',\n stockCode: \"Brother DK-1202\",\n sheetWIn: 2.4,\n sheetHIn: 3.9,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 2.4,\n labelHIn: 3.9,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n {\n id: \"brother-dk-11201\",\n name: 'Address roll 1.14\" × 3.5\" — die-cut',\n stockCode: \"Brother DK-11201\",\n sheetWIn: 1.14,\n sheetHIn: 3.5,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 1.14,\n labelHIn: 3.5,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n {\n id: \"dymo-30252\",\n name: 'Address roll 1⅛\" × 3½\"',\n stockCode: \"DYMO 30252\",\n sheetWIn: 1.125,\n sheetHIn: 3.5,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 1.125,\n labelHIn: 3.5,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n {\n id: \"roll-4x6\",\n name: 'Shipping roll 4\" × 6\"',\n stockCode: \"Thermal 4x6\",\n sheetWIn: 4,\n sheetHIn: 6,\n marginTopIn: 0,\n marginLeftIn: 0,\n labelWIn: 4,\n labelHIn: 6,\n cols: 1,\n rows: 1,\n gutterXIn: 0,\n gutterYIn: 0,\n kind: \"roll\",\n },\n];\n\nexport function getLabelTemplate(id: string): LabelTemplate | undefined {\n return LABEL_TEMPLATES.find((t) => t.id === id);\n}\n\n/**\n * Build a `custom` template for stock not in the registry. Callers supply the\n * full geometry; id is fixed to \"custom\".\n *\n * The geometry spread comes FIRST so an explicitly-present `name: undefined` /\n * `stockCode: undefined` cannot overwrite the resolved default (see\n * NOTES-CHANGELOG.md).\n */\nexport function customLabelTemplate(\n geometry: Omit<LabelTemplate, \"id\" | \"name\" | \"stockCode\"> &\n Partial<Pick<LabelTemplate, \"name\" | \"stockCode\">>,\n): LabelTemplate {\n return {\n ...geometry,\n id: \"custom\",\n name: geometry.name ?? \"Custom stock\",\n stockCode: geometry.stockCode ?? \"custom\",\n };\n}\n\n/** Dev-time sanity check: the grid must tile the sheet exactly (±1/64\"). */\nexport function assertTemplateGeometry(t: LabelTemplate): void {\n const w =\n t.marginLeftIn * 2 + t.cols * t.labelWIn + (t.cols - 1) * t.gutterXIn;\n const h =\n t.marginTopIn * 2 + t.rows * t.labelHIn + (t.rows - 1) * t.gutterYIn;\n const tol = 1 / 64;\n if (Math.abs(w - t.sheetWIn) > tol || Math.abs(h - t.sheetHIn) > tol) {\n console.error(\n `[label-print] Template ${t.id} does not tile its sheet: grid ${w.toFixed(4)}×${h.toFixed(4)}in vs sheet ${t.sheetWIn}×${t.sheetHIn}in`,\n );\n }\n}\n\n// Pure math, no side effects beyond a console.error on drift — the module\n// stays import-inert. `process` is guarded because this ships as an npm\n// package that runs in browsers, where a bare `process.env` read throws.\nif (\n typeof process === \"undefined\" ||\n process.env.NODE_ENV !== \"production\"\n) {\n LABEL_TEMPLATES.forEach(assertTemplateGeometry);\n}\n\n// ─── Data contract ───────────────────────────────────────────────────────────\n\nexport interface QrLabel {\n /** The value encoded in the QR code (SKU, URL, id…). */\n qrValue: string;\n /** Bold headline under/next to the code. Defaults to qrValue when captions are on. */\n caption?: string;\n /** Additional small text lines (wide labels only — there is no room on tiny stock). */\n lines?: string[];\n /**\n * A single short, LARGE, bold token (a garment size, a bin letter, a grade).\n * Rendered in the caption zone ABOVE the caption at ~2× the caption size and\n * auto-shrunk by the fit script.\n */\n badge?: string;\n /**\n * Durable http(s) image URL rendered in the lines zone with\n * `object-fit: contain`. Anything that is not http(s) is skipped — THE IMAGE\n * RULE. Ignored by the PDF lane, which cannot fetch a remote image.\n */\n imageUrl?: string;\n}\n\nexport interface QrLabelPrintData {\n labels: QrLabel[];\n /** Default template; the dialog's variant picker overrides it. */\n templateId: string;\n /** Optional custom-stock geometry used when templateId === \"custom\". */\n customTemplate?: LabelTemplate;\n}\n\nexport type QrEcLevel = \"L\" | \"M\" | \"Q\";\n\nexport interface QrLabelSettings {\n startAtLabel: number; // 1-based first cell to print on\n copiesPerCode: number;\n showCaption: boolean;\n showLines: boolean;\n rangeFrom: number; // 1-based, 0 = no bound\n rangeTo: number;\n ecLevel: QrEcLevel;\n calibrationPage: boolean;\n}\n\nconst DEFAULTS: QrLabelSettings = {\n startAtLabel: 1,\n copiesPerCode: 1,\n showCaption: true,\n showLines: true,\n rangeFrom: 0,\n rangeTo: 0,\n ecLevel: \"M\",\n calibrationPage: false,\n};\n\nfunction resolveSettings(s?: PrintSettings): QrLabelSettings {\n const num = (v: unknown, d: number, min = 0) =>\n typeof v === \"number\" && !Number.isNaN(v) ? Math.max(min, v) : d;\n const ec = s?.ecLevel;\n return {\n startAtLabel: num(s?.startAtLabel, DEFAULTS.startAtLabel, 1),\n copiesPerCode: num(s?.copiesPerCode, DEFAULTS.copiesPerCode, 1),\n showCaption: (s?.showCaption ?? DEFAULTS.showCaption) as boolean,\n showLines: (s?.showLines ?? DEFAULTS.showLines) as boolean,\n rangeFrom: num(s?.rangeFrom, 0),\n rangeTo: num(s?.rangeTo, 0),\n ecLevel: ec === \"L\" || ec === \"Q\" ? ec : \"M\",\n calibrationPage: (s?.calibrationPage ?? false) as boolean,\n };\n}\n\n// ─── QR generation (data URIs, inlined) ──────────────────────────────────────\n\nconst PRINT_DPI = 300;\n/** Quiet zone: QR spec requires ≥4 modules of clear margin around the symbol. */\nconst QUIET_ZONE_MODULES = 4;\n\nexport async function generateQrDataUri(\n value: string,\n ecLevel: QrEcLevel,\n sizeInches: number,\n): Promise<string> {\n // Deferred to call time (user click) — `./qr` lazy-imports `qrcode` inside\n // generateQrDataUrl, so this module never puts it on a shell graph.\n return generateQrDataUrl(value, {\n ecLevel,\n quietZoneModules: QUIET_ZONE_MODULES,\n widthPx: Math.round(sizeInches * PRINT_DPI),\n });\n}\n\n// ─── Cell geometry ───────────────────────────────────────────────────────────\n\ninterface CellLayout {\n /** true = QR left, text right; false = QR on top, caption below. */\n wide: boolean;\n qrIn: number;\n padIn: number;\n /** Height reserved above the caption for a `badge`; 0 when there is none. */\n badgeZoneIn: number;\n}\n\nexport function computeCellLayout(\n t: LabelTemplate,\n showCaption: boolean,\n hasBadge = false,\n): CellLayout {\n const padIn = Math.min(0.08, t.labelHIn * 0.06);\n const wide = t.labelWIn / t.labelHIn >= 1.8;\n const badgeZoneIn = hasBadge ? Math.min(0.4, t.labelHIn * 0.22) : 0;\n if (wide) {\n return { wide, qrIn: t.labelHIn - padIn * 2, padIn, badgeZoneIn };\n }\n // Stacked. Round labels inscribe the content square inside the circle.\n const inset = t.round ? t.labelWIn * 0.15 : 0;\n const captionZone = showCaption ? Math.min(0.28, t.labelHIn * 0.16) : 0;\n const qrIn = Math.min(\n t.labelWIn - padIn * 2 - inset * 2,\n t.labelHIn - padIn * 2 - inset * 2 - captionZone - badgeZoneIn,\n );\n return { wide, qrIn, padIn, badgeZoneIn };\n}\n\n// ─── Generic grid renderer ───────────────────────────────────────────────────\n\nfunction sheetStyles(t: LabelTemplate): string {\n const radius = t.round\n ? `${t.labelWIn / 2}in`\n : t.cornerRadiusIn\n ? `${t.cornerRadiusIn}in`\n : \"0\";\n return `\n @page { size: ${t.sheetWIn}in ${t.sheetHIn}in; margin: 0; }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n html, body { width: ${t.sheetWIn}in; background: #fff; color: #000;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif; }\n\n .lp-page {\n width: ${t.sheetWIn}in; height: ${t.sheetHIn}in;\n position: relative; overflow: hidden;\n page-break-after: always; break-after: page;\n border-bottom: 1px solid #e2e8f0; /* screen-only separator */\n }\n .lp-page:last-child { page-break-after: auto; break-after: auto; border-bottom: none; }\n\n .lp-cell {\n position: absolute;\n width: ${t.labelWIn}in; height: ${t.labelHIn}in;\n border-radius: ${radius};\n overflow: hidden;\n -webkit-print-color-adjust: exact; print-color-adjust: exact;\n }\n .lp-qr {\n display: block;\n image-rendering: pixelated; /* no resampling blur on crisp modules */\n }\n .lp-cell-wide { display: flex; align-items: center; }\n .lp-cell-stacked { display: flex; flex-direction: column; align-items: center; justify-content: center; }\n\n .lp-text { flex: 1 1 auto; min-width: 0; overflow: hidden; align-self: stretch;\n display: flex; flex-direction: column; justify-content: center; }\n .lp-fit-wrap { overflow: hidden; }\n .lp-caption { font-weight: 700; line-height: 1.15; word-break: break-word; }\n .lp-line { line-height: 1.25; word-break: break-word; }\n .lp-caption-stacked { text-align: center; width: 100%; }\n\n /* Badge — the LARGE token above the caption (garment size, bin letter). */\n .lp-badge { font-weight: 800; line-height: 1.05; word-break: break-word;\n -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n .lp-badge-stacked { text-align: center; width: 100%; }\n\n /* Durable http(s) artwork in the lines zone. */\n .lp-img { display: block; width: 100%; object-fit: contain; }\n\n /* Calibration */\n .lp-cal-cell { border: 0.5pt dashed #94a3b8; background: transparent; }\n .lp-cal-label { position: absolute; inset: 0; display: flex; align-items: center;\n justify-content: center; font-size: 7pt; color: #94a3b8; text-align: center; padding: 4px; }\n .lp-crop { position: absolute; background: #94a3b8;\n -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n\n @media print {\n html, body { height: auto; overflow: visible; }\n .lp-page { border-bottom: none; }\n .screen-only { display: none !important; }\n }\n`;\n}\n\nfunction cellPosition(t: LabelTemplate, index: number): { x: number; y: number } {\n const col = index % t.cols;\n const row = Math.floor(index / t.cols);\n return {\n x: t.marginLeftIn + col * (t.labelWIn + t.gutterXIn),\n y: t.marginTopIn + row * (t.labelHIn + t.gutterYIn),\n };\n}\n\n/**\n * Caption auto-shrink — same pattern as the flashcards FIT_TEXT_SCRIPT.\n *\n * Kept verbatim instead of core's `buildFitTextScript`: this loop measures and\n * writes **px** with a 4px floor, core's writes **pt** with a 6pt floor and\n * pre-assigns the size before measuring. See the module header.\n */\nconst LP_FIT_SCRIPT = `\n<script>\n(function () {\n function fit(el, box) {\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var min = 4;\n while (size > min && (el.scrollWidth > box.clientWidth + 1 || el.scrollHeight > box.clientHeight + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"px\";\n }\n }\n function run() {\n var els = document.querySelectorAll(\".lp-fit-wrap .lp-fit\");\n for (var i = 0; i < els.length; i++) fit(els[i], els[i].parentElement);\n }\n if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", run);\n else setTimeout(run, 0);\n})();\n</script>`;\n\ninterface RenderedCell {\n html: string; // inner HTML of the cell, \"\" for a blank pad cell\n}\n\n/** THE IMAGE RULE: only a durable http(s) URL is ever rendered. */\nfunction isHttpUrl(value: string | undefined): value is string {\n return typeof value === \"string\" && /^https?:\\/\\//i.test(value.trim());\n}\n\nfunction buildLabelCellHtml(\n label: QrLabel,\n qrDataUri: string,\n t: LabelTemplate,\n layout: CellLayout,\n s: QrLabelSettings,\n): string {\n const qr = qrDataUri\n ? `<img class=\"lp-qr\" src=\"${qrDataUri}\" alt=\"\" style=\"width:${layout.qrIn}in;height:${layout.qrIn}in;\" />`\n : \"\";\n const captionText = s.showCaption ? (label.caption ?? label.qrValue) : \"\";\n const badgeText = label.badge?.trim() ? label.badge.trim() : \"\";\n\n if (layout.wide) {\n const lines =\n s.showLines && label.lines?.length\n ? label.lines\n .filter((l) => l.trim())\n .map(\n (l) =>\n `<div class=\"lp-line\" style=\"font-size:8pt;\">${escapeHtml(l)}</div>`,\n )\n .join(\"\")\n : \"\";\n const caption = captionText\n ? `<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>`\n : \"\";\n const badge = badgeText\n ? `<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>`\n : \"\";\n const image = isHttpUrl(label.imageUrl)\n ? `<img class=\"lp-img\" src=\"${escapeHtml(label.imageUrl)}\" alt=\"\" style=\"height:${(t.labelHIn * 0.35).toFixed(3)}in;\" />`\n : \"\";\n return `<div class=\"lp-cell-wide\" style=\"height:100%;padding:${layout.padIn}in;gap:${layout.padIn}in;\">\n ${qr}\n <div class=\"lp-text\">${badge}${caption}${lines}${image}</div>\n </div>`;\n }\n\n const caption = captionText\n ? `<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>`\n : \"\";\n const badge = badgeText\n ? `<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>`\n : \"\";\n return `<div class=\"lp-cell-stacked\" style=\"height:100%;padding:${layout.padIn}in;\">\n ${qr}\n ${badge}\n ${caption}\n </div>`;\n}\n\nfunction renderPages(t: LabelTemplate, cells: RenderedCell[]): string {\n const perPage = t.cols * t.rows;\n const pages: string[] = [];\n for (let p = 0; p < Math.max(1, Math.ceil(cells.length / perPage)); p++) {\n const batch = cells.slice(p * perPage, (p + 1) * perPage);\n const cellHtml = batch\n .map((c, i) => {\n const { x, y } = cellPosition(t, i);\n return `<div class=\"lp-cell\" style=\"left:${x}in;top:${y}in;\">${c.html}</div>`;\n })\n .join(\"\\n\");\n pages.push(`<div class=\"lp-page\">${cellHtml}</div>`);\n }\n return pages.join(\"\\n\");\n}\n\nfunction openSheetWindow(\n t: LabelTemplate,\n bodyHtml: string,\n title: string,\n withFitScript: boolean,\n warning?: string,\n): PrintOutcome {\n const warningHtml = warning\n ? `<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>`\n : \"\";\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>${escapeHtml(title)} — ${escapeHtml(t.stockCode)}</title>\n <style>${sheetStyles(t)}</style>\n</head>\n<body>\n${warningHtml}\n<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;\">\n <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>\n <button onclick=\"window.close()\" style=\"padding:7px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;\">Close</button>\n <span style=\"color:#64748b;font-size:11px;\">${escapeHtml(t.stockCode)} · ${escapeHtml(t.name)} · set printer to <strong>100% scale (no \"fit to page\"), no margins, Portrait</strong></span>\n</div>\n${bodyHtml}\n${withFitScript ? LP_FIT_SCRIPT : \"\"}\n</body>\n</html>`;\n return openPrintWindow(html, title);\n}\n\n// ─── Label selection (range → copies → startAt offset) ──────────────────────\n\nexport function expandLabels(labels: QrLabel[], s: QrLabelSettings): QrLabel[] {\n const from = s.rangeFrom > 0 ? s.rangeFrom : 1;\n const to = s.rangeTo > 0 ? Math.min(s.rangeTo, labels.length) : labels.length;\n const subset = labels.slice(from - 1, to);\n if (s.copiesPerCode <= 1) return subset;\n const out: QrLabel[] = [];\n for (const l of subset)\n for (let i = 0; i < s.copiesPerCode; i++) out.push(l);\n return out;\n}\n\n// ─── Calibration test page ───────────────────────────────────────────────────\n\nfunction buildCalibrationBody(t: LabelTemplate): string {\n const perPage = t.cols * t.rows;\n const cells: string[] = [];\n for (let i = 0; i < perPage; i++) {\n const { x, y } = cellPosition(t, i);\n cells.push(\n `<div class=\"lp-cell lp-cal-cell\" style=\"left:${x}in;top:${y}in;\"><div class=\"lp-cal-label\">${i + 1}</div></div>`,\n );\n }\n // Crop marks at the four corners of the label grid area\n const gridR = t.sheetWIn - t.marginLeftIn;\n const gridB = t.sheetHIn - t.marginTopIn;\n const mark = (x: number, y: number, h: boolean) =>\n `<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>`;\n const marks = [\n mark(t.marginLeftIn - 0.25, t.marginTopIn, true),\n mark(t.marginLeftIn, t.marginTopIn - 0.25, false),\n mark(gridR, t.marginTopIn, true),\n mark(gridR - 0.0104, t.marginTopIn - 0.25, false),\n mark(t.marginLeftIn - 0.25, gridB - 0.0104, true),\n mark(t.marginLeftIn, gridB, false),\n mark(gridR, gridB - 0.0104, true),\n mark(gridR - 0.0104, gridB, false),\n ].join(\"\");\n 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 — ${escapeHtml(t.stockCode)} · hold against your label sheet: every outline must sit on a label</div>`;\n return `<div class=\"lp-page\">${banner}${marks}${cells.join(\"\\n\")}</div>`;\n}\n\n/** Print the grid outline + crop marks only — verify alignment before burning labels. */\nexport function printCalibrationSheet(template: LabelTemplate): PrintOutcome {\n return openSheetWindow(\n template,\n buildCalibrationBody(template),\n \"Label calibration\",\n false,\n );\n}\n\n// ─── Scannability ────────────────────────────────────────────────────────────\n\nconst MM_PER_IN = 25.4;\n\n/**\n * The printed-module floor a 300 dpi thermal printer and a warehouse scanner\n * can both resolve. Below this a symbol prints, looks fine on screen, and\n * refuses to scan on the dock.\n */\nexport const MIN_SCANNABLE_MODULE_MM = 0.4;\n\nexport interface ScannabilityReport {\n ok: boolean;\n version: number;\n moduleSizeMm: number;\n minModuleSizeMm: number;\n message?: string;\n}\n\n/**\n * Will a payload of `payloadByteLen` bytes actually scan off this stock?\n *\n * The printed side comes from the SAME layout brain the printer uses\n * (`computeCellLayout`), evaluated with captions ON — the smaller, and\n * therefore conservative, QR box. Module size divides that side by the symbol's\n * modules PLUS the two 4-module quiet zones the data URI carries inside it.\n *\n * Never throws: an over-capacity payload comes back as `ok: false` with\n * `version: 0` and the capacity error as the message.\n */\nexport function assertScannable(\n template: LabelTemplate,\n payloadByteLen: number,\n ecLevel: QrEcLevel = DEFAULTS.ecLevel,\n): ScannabilityReport {\n const layout = computeCellLayout(template, true);\n const printedSideMm = layout.qrIn * MM_PER_IN;\n\n let version: number;\n try {\n version = minQrVersion(payloadByteLen, ecLevel);\n } catch (err) {\n return {\n ok: false,\n version: 0,\n moduleSizeMm: 0,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n message: err instanceof Error ? err.message : String(err),\n };\n }\n\n const modulesPerSide = qrModuleCount(version) + QUIET_ZONE_MODULES * 2;\n const moduleSizeMm = printedSideMm / modulesPerSide;\n if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {\n return {\n ok: true,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n };\n }\n return {\n ok: false,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n message:\n `A ${payloadByteLen}-byte payload needs a version ${version} QR, which prints at ` +\n `${moduleSizeMm.toFixed(3)}mm per module on ${template.stockCode} — below the ` +\n `${MIN_SCANNABLE_MODULE_MM}mm floor a scanner can resolve. Shorten the payload ` +\n `(a short resolver URL is the intended fix) or move to larger label stock.`,\n };\n}\n\n/** The batch's worst case: the longest payload decides whether we warn. */\nfunction batchScannabilityWarning(\n t: LabelTemplate,\n labels: readonly QrLabel[],\n ecLevel: QrEcLevel,\n): string | undefined {\n const encoder = new TextEncoder();\n let worst = -1;\n for (const l of labels) {\n if (!l.qrValue) continue;\n const len = encoder.encode(l.qrValue).length;\n if (len > worst) worst = len;\n }\n if (worst < 0) return undefined;\n const report = assertScannable(t, worst, ecLevel);\n return report.ok ? undefined : report.message;\n}\n\n// ─── The printer ─────────────────────────────────────────────────────────────\n\nfunction resolveTemplate(\n data: QrLabelPrintData,\n variantId?: string,\n): LabelTemplate {\n if (variantId && variantId !== \"custom\") {\n const t = getLabelTemplate(variantId);\n if (t) return t;\n }\n if (data.templateId === \"custom\" && data.customTemplate)\n return data.customTemplate;\n return (\n getLabelTemplate(data.templateId) ?? data.customTemplate ?? AVERY_5160\n );\n}\n\n/** Core render path — also used by the demo page's direct print entry point. */\nexport async function printQrLabelSheet(\n data: QrLabelPrintData,\n variantId?: string,\n rawSettings?: PrintSettings,\n): Promise<PrintOutcome> {\n const t = resolveTemplate(data, variantId);\n const s = resolveSettings(rawSettings);\n\n if (s.calibrationPage) {\n return printCalibrationSheet(t);\n }\n\n const labels = expandLabels(data.labels ?? [], s);\n if (!labels.length) {\n return openSheetWindow(\n t,\n `<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>`,\n \"QR labels\",\n false,\n );\n }\n\n const hasBadge = labels.some((l) => (l.badge ?? \"\").trim() !== \"\");\n const layout = computeCellLayout(t, s.showCaption, hasBadge);\n\n // Generate every data URI up front (dedup identical values). A label with no\n // qrValue (a text-only LabelFormat) simply carries no code.\n const uriByValue = new Map<string, string>();\n for (const l of labels) {\n if (l.qrValue && !uriByValue.has(l.qrValue)) {\n uriByValue.set(\n l.qrValue,\n await generateQrDataUri(l.qrValue, s.ecLevel, layout.qrIn),\n );\n }\n }\n\n // Leading blank cells for a partially-used sheet, then labels, then pad.\n const leadingBlanks = Math.max(0, s.startAtLabel - 1) % (t.cols * t.rows);\n const cells: RenderedCell[] = [\n ...Array.from({ length: leadingBlanks }, () => ({ html: \"\" })),\n ...labels.map((l) => ({\n html: buildLabelCellHtml(\n l,\n uriByValue.get(l.qrValue) ?? \"\",\n t,\n layout,\n s,\n ),\n })),\n ];\n const perPage = t.cols * t.rows;\n const tail = cells.length % perPage;\n if (tail > 0)\n for (let i = 0; i < perPage - tail; i++) cells.push({ html: \"\" });\n\n return openSheetWindow(\n t,\n renderPages(t, cells),\n \"QR labels\",\n true,\n batchScannabilityWarning(t, labels, s.ecLevel),\n );\n}\n\nexport const qrLabelsPrinter: BlockPrinter = {\n label: \"Print QR label sheet\",\n variants: LABEL_TEMPLATES.map((t) => ({\n id: t.id,\n label: t.name,\n description: t.stockCode,\n })),\n settings: [\n {\n type: \"number\",\n id: \"startAtLabel\",\n label: \"Start at label #\",\n description: \"Skip already-used positions on a partial sheet\",\n defaultValue: 1,\n min: 1,\n },\n {\n type: \"number\",\n id: \"copiesPerCode\",\n label: \"Copies per code\",\n defaultValue: 1,\n min: 1,\n max: 999,\n },\n {\n type: \"boolean\",\n id: \"showCaption\",\n label: \"Show caption\",\n description: \"Print the caption (or the code value) beside each QR\",\n defaultValue: true,\n },\n {\n type: \"boolean\",\n id: \"showLines\",\n label: \"Show detail lines\",\n description: \"Extra text lines on wide labels\",\n defaultValue: true,\n },\n {\n type: \"range\",\n id: \"labelRange\",\n label: \"Label range\",\n description: \"Print only labels N to M (blank = all)\",\n fromId: \"rangeFrom\",\n toId: \"rangeTo\",\n defaultFrom: 0,\n defaultTo: 0,\n },\n {\n type: \"select\",\n id: \"ecLevel\",\n label: \"Error correction\",\n description: \"Higher survives more damage; M is the warehouse default\",\n options: [\n { value: \"L\", label: \"L\" },\n { value: \"M\", label: \"M\" },\n { value: \"Q\", label: \"Q\" },\n ],\n defaultValue: \"M\",\n },\n {\n type: \"boolean\",\n id: \"calibrationPage\",\n label: \"Calibration test page\",\n description: \"Print outlines + crop marks only to verify alignment\",\n defaultValue: false,\n },\n ],\n print: (data, variantId, settings) =>\n printQrLabelSheet(data as QrLabelPrintData, variantId, settings),\n};\n\n// ─── PDF lane ────────────────────────────────────────────────────────────────\n\n/**\n * downloadLabelsPdf() renders the same registry-driven grid as the printer\n * straight into a jsPDF file, for users who want a file instead of the browser\n * print dialog.\n *\n * jspdf is imported dynamically INSIDE the function body (code-splitting rule 6\n * — click-time machinery never rides a static edge).\n */\n\nconst PT_PER_IN = 72;\n\nfunction resolvePdfSettings(s?: PrintSettings): QrLabelSettings {\n const num = (v: unknown, d: number, min = 0) =>\n typeof v === \"number\" && !Number.isNaN(v) ? Math.max(min, v) : d;\n const ec = s?.ecLevel;\n return {\n startAtLabel: num(s?.startAtLabel, 1, 1),\n copiesPerCode: num(s?.copiesPerCode, 1, 1),\n showCaption: (s?.showCaption ?? true) as boolean,\n showLines: (s?.showLines ?? true) as boolean,\n rangeFrom: num(s?.rangeFrom, 0),\n rangeTo: num(s?.rangeTo, 0),\n ecLevel: ec === \"L\" || ec === \"Q\" ? ec : \"M\",\n calibrationPage: false,\n };\n}\n\n/** jsPDF's splitTextToSize is loosely typed; take the first line safely. */\nfunction firstLine(parts: unknown): string {\n if (Array.isArray(parts)) {\n const head: unknown = parts[0];\n return typeof head === \"string\" ? head : \"\";\n }\n return typeof parts === \"string\" ? parts : \"\";\n}\n\n/**\n * Generate a PDF of QR labels for the given template and hand it to the\n * browser as a download. Same data seam as the printer: `{ labels, templateId }`.\n */\nexport async function downloadLabelsPdf(\n data: QrLabelPrintData,\n templateId?: string,\n rawSettings?: PrintSettings,\n filename = \"qr-labels.pdf\",\n): Promise<void> {\n const t =\n (templateId === \"custom\" || data.templateId === \"custom\"\n ? data.customTemplate\n : undefined) ??\n getLabelTemplate(templateId ?? data.templateId) ??\n AVERY_5160;\n const s = resolvePdfSettings(rawSettings);\n const labels = expandLabels(data.labels ?? [], s);\n if (!labels.length) return;\n\n // Never blocks the download — the PDF lane has no screen-only header to\n // carry the banner, so an unscannable batch screams on the console instead.\n const warning = batchScannabilityWarning(t, labels, s.ecLevel);\n if (warning !== undefined) console.warn(`[label-print] ${warning}`);\n\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({\n unit: \"pt\",\n format: [t.sheetWIn * PT_PER_IN, t.sheetHIn * PT_PER_IN],\n });\n\n const hasBadge = labels.some((l) => (l.badge ?? \"\").trim() !== \"\");\n const layout = computeCellLayout(t, s.showCaption, hasBadge);\n const qrPt = layout.qrIn * PT_PER_IN;\n const padPt = layout.padIn * PT_PER_IN;\n const perPage = t.cols * t.rows;\n const leadingBlanks = Math.max(0, s.startAtLabel - 1) % perPage;\n\n const uriByValue = new Map<string, string>();\n for (const l of labels) {\n if (l.qrValue && !uriByValue.has(l.qrValue)) {\n uriByValue.set(\n l.qrValue,\n await generateQrDataUri(l.qrValue, s.ecLevel, layout.qrIn),\n );\n }\n }\n\n for (const [i, label] of labels.entries()) {\n const cellIndex = i + leadingBlanks;\n if (cellIndex > 0 && cellIndex % perPage === 0) doc.addPage();\n const within = cellIndex % perPage;\n const col = within % t.cols;\n const row = Math.floor(within / t.cols);\n const x =\n (t.marginLeftIn + col * (t.labelWIn + t.gutterXIn)) * PT_PER_IN;\n const y = (t.marginTopIn + row * (t.labelHIn + t.gutterYIn)) * PT_PER_IN;\n const caption = s.showCaption ? (label.caption ?? label.qrValue) : \"\";\n const badge = label.badge?.trim() ? label.badge.trim() : \"\";\n const uri = uriByValue.get(label.qrValue) ?? \"\";\n\n if (layout.wide) {\n // QR left, text right\n const qrY = y + (t.labelHIn * PT_PER_IN - qrPt) / 2;\n if (uri) doc.addImage(uri, \"PNG\", x + padPt, qrY, qrPt, qrPt);\n const textX = x + padPt + qrPt + padPt;\n const textMaxW = t.labelWIn * PT_PER_IN - (textX - x) - padPt;\n let cursorY = y + padPt + 12;\n if (badge) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(24);\n cursorY = y + padPt + 22;\n doc.text(firstLine(doc.splitTextToSize(badge, textMaxW)), textX, cursorY);\n cursorY += 16;\n }\n if (caption) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(12);\n doc.text(firstLine(doc.splitTextToSize(caption, textMaxW)), textX, cursorY);\n cursorY += 14;\n }\n if (s.showLines && label.lines?.length) {\n doc.setFont(\"helvetica\", \"normal\");\n doc.setFontSize(8);\n for (const line of label.lines.filter((l) => l.trim())) {\n if (cursorY > y + t.labelHIn * PT_PER_IN - padPt) break;\n doc.text(firstLine(doc.splitTextToSize(line, textMaxW)), textX, cursorY);\n cursorY += 10;\n }\n }\n } else {\n // Stacked, centered\n const cx = x + t.labelWIn * PT_PER_IN / 2;\n const contentH = qrPt + (caption ? 12 : 0) + (badge ? 22 : 0);\n const qrY = y + (t.labelHIn * PT_PER_IN - contentH) / 2;\n if (uri) doc.addImage(uri, \"PNG\", cx - qrPt / 2, qrY, qrPt, qrPt);\n const maxW = (t.labelWIn - layout.padIn * 2) * PT_PER_IN;\n let textY = qrY + qrPt;\n if (badge) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(18);\n textY += 18;\n doc.text(firstLine(doc.splitTextToSize(badge, maxW)), cx, textY, {\n align: \"center\",\n });\n }\n if (caption) {\n doc.setFont(\"helvetica\", \"bold\");\n doc.setFontSize(9);\n doc.text(firstLine(doc.splitTextToSize(caption, maxW)), cx, textY + 10, {\n align: \"center\",\n });\n }\n }\n }\n\n doc.save(filename);\n}\n\n// ─── Label formats (the custom-format model) ─────────────────────────────────\n\n/** Where an element's text comes from: a row column, or a fixed string. */\nexport type LabelField = { field: string } | { literal: string };\n\nexport type LabelElement =\n | { kind: \"qr\"; source: LabelField }\n | {\n kind: \"text\";\n source: LabelField;\n role: \"title\" | \"detail\" | \"badge\";\n /**\n * Cap on how many lines the value may occupy. The value is split on\n * newlines; beyond `maxLines` the extra lines are dropped and the last\n * kept line gains an ellipsis.\n */\n maxLines?: number;\n }\n /** Durable http(s) URL only — THE IMAGE RULE. A non-http value is SKIPPED. */\n | { kind: \"image\"; source: LabelField };\n\nexport interface LabelFormat {\n id: string;\n name: string;\n elements: LabelElement[];\n}\n\nfunction resolveLabelField(\n source: LabelField,\n row: Record<string, string>,\n): string {\n if (\"literal\" in source) return source.literal;\n return row[source.field] ?? \"\";\n}\n\nfunction capToLines(value: string, maxLines: number | undefined): string[] {\n const parts = value\n .split(\"\\n\")\n .map((p) => p.trim())\n .filter((p) => p !== \"\");\n if (maxLines === undefined || maxLines <= 0 || parts.length <= maxLines) {\n return parts;\n }\n const kept = parts.slice(0, maxLines);\n const last = kept[kept.length - 1];\n if (last !== undefined) kept[kept.length - 1] = `${last}…`;\n return kept;\n}\n\n/**\n * Map rows through a format into the printer's label shape.\n *\n * - the qr element's resolved value → `qrValue` (first one wins)\n * - the FIRST title-role text → `caption`\n * - the FIRST badge-role text → `badge`\n * - detail-role texts, in element order → `lines[]`\n * - the first http(s) image element → `imageUrl`\n *\n * A row missing a `field` resolves to \"\" and that element is skipped.\n */\nexport function applyLabelFormat(\n format: LabelFormat,\n rows: Record<string, string>[],\n): { labels: QrLabelPrintData[\"labels\"] } {\n const labels: QrLabel[] = [];\n for (const row of rows) {\n let qrValue = \"\";\n let caption: string | undefined;\n let badge: string | undefined;\n let imageUrl: string | undefined;\n const lines: string[] = [];\n\n for (const el of format.elements) {\n const raw = resolveLabelField(el.source, row).trim();\n if (raw === \"\") continue;\n if (el.kind === \"qr\") {\n if (qrValue === \"\") qrValue = raw;\n continue;\n }\n if (el.kind === \"image\") {\n if (imageUrl === undefined && isHttpUrl(raw)) imageUrl = raw;\n continue;\n }\n const capped = capToLines(raw, el.maxLines);\n if (capped.length === 0) continue;\n if (el.role === \"title\") {\n if (caption === undefined) caption = capped.join(\" \");\n } else if (el.role === \"badge\") {\n if (badge === undefined) badge = capped.join(\" \");\n } else {\n lines.push(...capped);\n }\n }\n\n const label: QrLabel = { qrValue };\n if (caption !== undefined) label.caption = caption;\n if (lines.length > 0) label.lines = lines;\n if (badge !== undefined) label.badge = badge;\n if (imageUrl !== undefined) label.imageUrl = imageUrl;\n labels.push(label);\n }\n return { labels };\n}\n\n/**\n * The shipped formats. Every preset reads the QR payload from the `qrValue`\n * column, so a row is always `{ qrValue, …format-specific columns }`.\n *\n * NOTE on `asset-spec`: the cell renderer auto-shrinks only the caption/badge\n * (`.lp-fit`); detail lines are fixed 8pt, so the title never loses size to a\n * long spec list.\n */\nexport const LABEL_FORMAT_PRESETS: readonly LabelFormat[] = [\n {\n id: \"qr-only\",\n name: \"QR only\",\n elements: [{ kind: \"qr\", source: { field: \"qrValue\" } }],\n },\n {\n id: \"qr-caption\",\n name: \"QR + caption\",\n elements: [\n { kind: \"qr\", source: { field: \"qrValue\" } },\n { kind: \"text\", source: { field: \"caption\" }, role: \"title\" },\n ],\n },\n {\n id: \"garment\",\n name: \"Garment tag\",\n elements: [\n { kind: \"qr\", source: { field: \"qrValue\" } },\n { kind: \"text\", source: { field: \"size\" }, role: \"badge\" },\n { kind: \"text\", source: { field: \"sku\" }, role: \"title\" },\n { kind: \"text\", source: { field: \"color\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"price\" }, role: \"detail\" },\n ],\n },\n {\n id: \"asset-spec\",\n name: \"Asset spec\",\n elements: [\n { kind: \"text\", source: { field: \"title\" }, role: \"title\" },\n { kind: \"text\", source: { field: \"spec1\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"spec2\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"spec3\" }, role: \"detail\" },\n { kind: \"text\", source: { field: \"spec4\" }, role: \"detail\" },\n { kind: \"qr\", source: { field: \"qrValue\" } },\n ],\n },\n];\n","/**\n * @ai-matrx/print/qr — QR GENERATION.\n *\n * New code (no port source). Decoding is NOT here: it lives in\n * `@ai-matrx/kit/qr`. This module only ever turns a value into a symbol, and\n * builds the two payload shapes the platform actually prints:\n *\n * 1. a SHORT RESOLVER URL (`https://origin/l/<code>`) — the default\n * e-commerce payload. The code value is an OPAQUE INPUT: minting,\n * uniqueness, collision handling and lookup are all server-side concerns.\n * Nothing in this file generates, randomizes, or validates the identity of\n * a code beyond \"it is a non-empty string we will percent-encode\".\n * 2. a GS1 DIGITAL LINK (`https://origin/01/<gtin14>[/10/<lot>][/21/<serial>][?17=YYMMDD]`)\n * for supply-chain interoperability — the thing a retailer's scanner\n * understands without knowing anything about AI Matrx.\n *\n * Two hard defaults that are NOT style choices:\n * - Error correction defaults to \"M\", never \"L\". These end up on warehouse\n * labels that get scuffed, wrapped around curved stock and read at an\n * angle; \"L\" is only ever right for a huge, clean, flat code.\n * - The quiet zone is CLAMPED to >= 4 modules (ISO/IEC 18004 §6.3.8 minimum)\n * no matter what the caller passes. A tight quiet zone is the single most\n * common cause of a label that scans on the design monitor and fails at the\n * dock door.\n *\n * The module is import-inert: `qrcode` is only ever pulled in via\n * `await import(\"qrcode\")` inside a function body, so importing this file from\n * a React Server Component (or any tree-shaken bundle that never draws a QR)\n * costs nothing. No module-level mutable state, no React, no DOM at import\n * time.\n */\n\nimport type { QRCodeToDataURLOptions, QRCodeToStringOptions } from \"qrcode\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\nexport type QrEcLevel = \"L\" | \"M\" | \"Q\" | \"H\";\n\nexport interface QrOptions {\n /** Error correction. Default \"M\" — NEVER \"L\" (warehouse labels get scuffed; L is only right for huge clean codes). */\n ecLevel?: QrEcLevel;\n /** Quiet zone in modules. Default 4. CLAMPED to >= 4 at runtime — ISO 18004 spec minimum, not a style choice. */\n quietZoneModules?: number;\n /** Rendered width in pixels (dataURL/canvas lanes). */\n widthPx?: number;\n}\n\n/** ISO/IEC 18004 §6.3.8 minimum quiet zone. Callers cannot go below this. */\nexport const QR_MIN_QUIET_ZONE_MODULES = 4;\n\n/** The default error correction level for every Matrx-printed code. */\nexport const DEFAULT_QR_EC_LEVEL: QrEcLevel = \"M\";\n\n/** Lowest and highest QR symbol versions (ISO/IEC 18004). */\nexport const QR_MIN_VERSION = 1;\nexport const QR_MAX_VERSION = 40;\n\n// ─── Capacity table ──────────────────────────────────────────────────────────\n\n/**\n * ISO/IEC 18004 BYTE-mode data capacity, versions 1–40 × [L, M, Q, H],\n * in bytes (8-bit characters).\n *\n * This is the full spec table, not an approximation. It is cross-checked in\n * `qr.test.ts` against the `qrcode` reference implementation for a spread of\n * versions at all four levels — every entry that test samples must both fit at\n * exactly `capacity` bytes and fail at `capacity + 1`.\n *\n * Anchors worth eyeballing: v1 = L17/M14/Q11/H7 · v2 M=26 · v3 M=42 ·\n * v4 M=62 · v10 M=213 · v40 = L2953/M2331/Q1663/H1273.\n */\nconst BYTE_CAPACITY: readonly (readonly [number, number, number, number])[] = [\n /* v1 */ [17, 14, 11, 7],\n /* v2 */ [32, 26, 20, 14],\n /* v3 */ [53, 42, 32, 24],\n /* v4 */ [78, 62, 46, 34],\n /* v5 */ [106, 84, 60, 44],\n /* v6 */ [134, 106, 74, 58],\n /* v7 */ [154, 122, 86, 64],\n /* v8 */ [192, 152, 108, 84],\n /* v9 */ [230, 180, 130, 98],\n /* v10 */ [271, 213, 151, 119],\n /* v11 */ [321, 251, 177, 137],\n /* v12 */ [367, 287, 203, 155],\n /* v13 */ [425, 331, 241, 177],\n /* v14 */ [458, 362, 258, 194],\n /* v15 */ [520, 412, 292, 220],\n /* v16 */ [586, 450, 322, 250],\n /* v17 */ [644, 504, 364, 280],\n /* v18 */ [718, 560, 394, 310],\n /* v19 */ [792, 624, 442, 338],\n /* v20 */ [858, 666, 482, 382],\n /* v21 */ [929, 711, 509, 403],\n /* v22 */ [1003, 779, 565, 439],\n /* v23 */ [1091, 857, 611, 461],\n /* v24 */ [1171, 911, 661, 511],\n /* v25 */ [1273, 997, 715, 535],\n /* v26 */ [1367, 1059, 751, 593],\n /* v27 */ [1465, 1125, 805, 625],\n /* v28 */ [1528, 1190, 868, 658],\n /* v29 */ [1628, 1264, 908, 698],\n /* v30 */ [1732, 1370, 982, 742],\n /* v31 */ [1840, 1452, 1030, 790],\n /* v32 */ [1952, 1538, 1112, 842],\n /* v33 */ [2068, 1628, 1168, 898],\n /* v34 */ [2188, 1722, 1228, 958],\n /* v35 */ [2303, 1809, 1283, 983],\n /* v36 */ [2431, 1911, 1351, 1051],\n /* v37 */ [2563, 1989, 1423, 1093],\n /* v38 */ [2699, 2099, 1499, 1139],\n /* v39 */ [2809, 2213, 1579, 1219],\n /* v40 */ [2953, 2331, 1663, 1273],\n];\n\nconst EC_COLUMN: Readonly<Record<QrEcLevel, 0 | 1 | 2 | 3>> = { L: 0, M: 1, Q: 2, H: 3 };\n\nfunction assertEcLevel(ecLevel: QrEcLevel): 0 | 1 | 2 | 3 {\n const column = EC_COLUMN[ecLevel];\n if (column === undefined) {\n throw new Error(`Unknown QR error correction level \"${String(ecLevel)}\". Expected one of \"L\", \"M\", \"Q\", \"H\".`);\n }\n return column;\n}\n\nfunction assertVersion(version: number): number {\n if (typeof version !== \"number\" || !Number.isInteger(version) || version < QR_MIN_VERSION || version > QR_MAX_VERSION) {\n throw new Error(\n `QR version must be an integer between ${QR_MIN_VERSION} and ${QR_MAX_VERSION}, got ${String(version)}.`,\n );\n }\n return version;\n}\n\n/**\n * BYTE-mode capacity in bytes for a given version/level.\n * Throws on an out-of-range version or unknown level.\n */\nexport function qrByteCapacity(version: number, ecLevel: QrEcLevel): number {\n const column = assertEcLevel(ecLevel);\n const row = BYTE_CAPACITY[assertVersion(version) - 1];\n if (row === undefined) {\n throw new Error(`No QR capacity row for version ${version}.`);\n }\n return row[column];\n}\n\n/** Modules per side for a QR version: 17 + 4*version. */\nexport function qrModuleCount(version: number): number {\n return 17 + 4 * assertVersion(version);\n}\n\n/**\n * Smallest QR version (1-40) whose BYTE-mode capacity at `ecLevel` fits\n * `payloadByteLen`. Throws a clear Error when nothing fits.\n *\n * NOTE: this is the *byte* length, not the character length — callers holding\n * a JS string should measure it with `new TextEncoder().encode(s).length`, not\n * `s.length`, or every non-ASCII character will silently under-count.\n */\nexport function minQrVersion(payloadByteLen: number, ecLevel: QrEcLevel): number {\n const column = assertEcLevel(ecLevel);\n if (typeof payloadByteLen !== \"number\" || !Number.isInteger(payloadByteLen) || payloadByteLen < 0) {\n throw new Error(`QR payload byte length must be a non-negative integer, got ${String(payloadByteLen)}.`);\n }\n for (let version = QR_MIN_VERSION; version <= QR_MAX_VERSION; version += 1) {\n const row = BYTE_CAPACITY[version - 1];\n if (row !== undefined && row[column] >= payloadByteLen) {\n return version;\n }\n }\n const max = qrByteCapacity(QR_MAX_VERSION, ecLevel);\n throw new Error(\n `QR payload of ${payloadByteLen} bytes does not fit any QR version at error correction level \"${ecLevel}\" ` +\n `(version ${QR_MAX_VERSION} holds ${max} bytes). Shorten the payload — a short resolver URL is the intended ` +\n `fix — or, only for a large clean code, drop to a lower error correction level.`,\n );\n}\n\n// ─── Rendering ───────────────────────────────────────────────────────────────\n\nfunction resolveEcLevel(ecLevel: QrEcLevel | undefined): QrEcLevel {\n if (ecLevel === undefined) return DEFAULT_QR_EC_LEVEL;\n assertEcLevel(ecLevel);\n return ecLevel;\n}\n\n/**\n * `margin` in the `qrcode` library IS the quiet zone measured in modules.\n * Anything below the ISO minimum is silently raised — a caller asking for 0 is\n * asking for a label that will not scan, and we would rather print a slightly\n * smaller symbol than an unreadable one.\n */\nfunction resolveQuietZone(quietZoneModules: number | undefined): number {\n if (typeof quietZoneModules !== \"number\" || !Number.isFinite(quietZoneModules)) {\n return QR_MIN_QUIET_ZONE_MODULES;\n }\n return Math.max(QR_MIN_QUIET_ZONE_MODULES, Math.floor(quietZoneModules));\n}\n\nfunction resolveWidthPx(widthPx: number | undefined): number | undefined {\n if (widthPx === undefined) return undefined;\n if (typeof widthPx !== \"number\" || !Number.isFinite(widthPx) || widthPx <= 0) {\n throw new Error(`QR widthPx must be a positive number, got ${String(widthPx)}.`);\n }\n return Math.round(widthPx);\n}\n\nfunction assertValue(value: string): string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(\"QR value must be a non-empty string.\");\n }\n return value;\n}\n\n/** PNG data URL. Lazy-imports \"qrcode\" at call time (import-inert module). */\nexport async function generateQrDataUrl(value: string, opts?: QrOptions): Promise<string> {\n assertValue(value);\n const width = resolveWidthPx(opts?.widthPx);\n const options: QRCodeToDataURLOptions = {\n errorCorrectionLevel: resolveEcLevel(opts?.ecLevel),\n margin: resolveQuietZone(opts?.quietZoneModules),\n };\n // exactOptionalPropertyTypes: omit the key entirely rather than assigning undefined.\n if (width !== undefined) options.width = width;\n\n const QRCode = await import(\"qrcode\");\n return QRCode.toDataURL(value, options);\n}\n\n/** SVG markup string. Same lazy import. */\nexport async function generateQrSvg(value: string, opts?: QrOptions): Promise<string> {\n assertValue(value);\n const width = resolveWidthPx(opts?.widthPx);\n const options: QRCodeToStringOptions = {\n type: \"svg\",\n errorCorrectionLevel: resolveEcLevel(opts?.ecLevel),\n margin: resolveQuietZone(opts?.quietZoneModules),\n };\n if (width !== undefined) options.width = width;\n\n const QRCode = await import(\"qrcode\");\n return QRCode.toString(value, options);\n}\n\n// ─── Payload builders ────────────────────────────────────────────────────────\n\n/**\n * Validate + normalize an origin for a payload builder.\n * Accepts `https://x.com`, `https://x.com/`, `https://x.com/go` — returns it\n * with every trailing slash stripped so callers can always concatenate a\n * leading-slash path without producing `//`.\n */\nfunction normalizeOrigin(origin: string): string {\n if (typeof origin !== \"string\" || origin.trim() === \"\") {\n throw new Error('QR origin is required — pass something like \"https://example.com\".');\n }\n const trimmed = origin.trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new Error(`QR origin \"${origin}\" is not a valid absolute URL. Expected something like \"https://example.com\".`);\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(`QR origin \"${origin}\" must use http or https, got \"${parsed.protocol}\".`);\n }\n if (parsed.search !== \"\" || parsed.hash !== \"\") {\n throw new Error(`QR origin \"${origin}\" must not carry a query string or fragment.`);\n }\n return trimmed.replace(/\\/+$/, \"\");\n}\n\n/**\n * Short resolver URL — the platform's default e-commerce payload.\n *\n * The CODE VALUE IS AN OPAQUE INPUT: minting and uniqueness are server-side,\n * never generated here. This function's whole job is to put a code the server\n * already owns behind a URL short enough to stay in a low QR version.\n *\n * Result looks like `https://x.com/l/AB12cd`.\n */\nexport function buildShortResolverUrl(opts: { origin: string; code: string; path?: string }): string {\n const origin = normalizeOrigin(opts.origin);\n\n const code = opts.code;\n if (typeof code !== \"string\" || code.trim() === \"\") {\n throw new Error(\"QR resolver code is required — it is minted server-side and passed in here as an opaque value.\");\n }\n\n const rawPath = opts.path === undefined ? \"/l\" : opts.path;\n if (typeof rawPath !== \"string\") {\n throw new Error(`QR resolver path must be a string, got ${String(rawPath)}.`);\n }\n if (rawPath.includes(\"?\") || rawPath.includes(\"#\")) {\n throw new Error(`QR resolver path \"${rawPath}\" must not contain a query string or fragment.`);\n }\n const cleanedPath = rawPath.trim().replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n const prefix = cleanedPath === \"\" ? \"\" : `/${cleanedPath}`;\n\n return `${origin}${prefix}/${encodeURIComponent(code)}`;\n}\n\n/**\n * GS1 mod-10 check digit over the payload (i.e. the digits WITHOUT the check\n * digit): weights 3 and 1 alternating from the right. Length-independent, so a\n * GTIN-12 and its zero-padded GTIN-14 form produce the same digit — which is\n * exactly why we validate BEFORE padding.\n */\nfunction gs1CheckDigit(payloadDigits: string): number {\n let sum = 0;\n let weight = 3;\n for (let i = payloadDigits.length - 1; i >= 0; i -= 1) {\n sum += (payloadDigits.charCodeAt(i) - 48) * weight;\n weight = weight === 3 ? 1 : 3;\n }\n return (10 - (sum % 10)) % 10;\n}\n\n/** Validate a GTIN-8/12/13/14 (own check digit) and return it zero-padded to GTIN-14. */\nfunction normalizeGtin(gtin: string): string {\n if (typeof gtin !== \"string\" || gtin.trim() === \"\") {\n throw new Error(\"GTIN is required for a GS1 Digital Link.\");\n }\n const raw = gtin.trim();\n if (!/^[0-9]+$/.test(raw)) {\n throw new Error(`GTIN \"${gtin}\" must contain digits only.`);\n }\n if (raw.length !== 8 && raw.length !== 12 && raw.length !== 13 && raw.length !== 14) {\n throw new Error(`GTIN \"${gtin}\" must be 8, 12, 13, or 14 digits, got ${raw.length}.`);\n }\n const expected = gs1CheckDigit(raw.slice(0, -1));\n const actual = raw.charCodeAt(raw.length - 1) - 48;\n if (expected !== actual) {\n throw new Error(`GTIN \"${raw}\" has an invalid GS1 check digit: expected ${expected}, got ${actual}.`);\n }\n return raw.padStart(14, \"0\");\n}\n\nfunction daysInMonth(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\n/**\n * AI 17 expiration date — STRICT \"YYMMDD\", 6 digits, nothing else.\n * `DD = 00` is honoured: GS1 defines it as \"last day of the month\".\n */\nfunction normalizeExpiry(expiry: string): string {\n if (typeof expiry !== \"string\" || !/^[0-9]{6}$/.test(expiry.trim())) {\n throw new Error(`GS1 expiry (AI 17) must be exactly 6 digits in YYMMDD form, got \"${String(expiry)}\".`);\n }\n const raw = expiry.trim();\n const year = 2000 + Number(raw.slice(0, 2));\n const month = Number(raw.slice(2, 4));\n const day = Number(raw.slice(4, 6));\n if (month < 1 || month > 12) {\n throw new Error(`GS1 expiry (AI 17) \"${raw}\" has an invalid month \"${raw.slice(2, 4)}\" — expected 01-12.`);\n }\n const max = daysInMonth(year, month);\n // DD=00 is the GS1 \"end of month\" convention and stays legal.\n if (day > max) {\n throw new Error(\n `GS1 expiry (AI 17) \"${raw}\" has an invalid day \"${raw.slice(4, 6)}\" — ${year}-${raw.slice(2, 4)} has ${max} days ` +\n `(00 is legal and means end of month).`,\n );\n }\n return raw;\n}\n\nfunction assertQualifier(value: string, ai: string, name: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new Error(`GS1 ${name} (AI ${ai}) must be a non-empty string when provided.`);\n }\n return value;\n}\n\n/** GS1 Digital Link builder for supply-chain interoperability. */\nexport function buildGs1DigitalLink(opts: {\n origin: string;\n gtin: string;\n lot?: string;\n serial?: string;\n expiry?: string;\n}): string {\n const origin = normalizeOrigin(opts.origin);\n const gtin14 = normalizeGtin(opts.gtin);\n\n // Path order is fixed by the GS1 Digital Link grammar:\n // /01/{gtin14} then the optional qualifiers /10/{lot} then /21/{serial}.\n let url = `${origin}/01/${gtin14}`;\n if (opts.lot !== undefined) {\n url += `/10/${encodeURIComponent(assertQualifier(opts.lot, \"10\", \"lot\"))}`;\n }\n if (opts.serial !== undefined) {\n url += `/21/${encodeURIComponent(assertQualifier(opts.serial, \"21\", \"serial\"))}`;\n }\n // AI 17 is a DATA ATTRIBUTE, not a path qualifier — it belongs in the query\n // string, never as a /17/ path segment.\n if (opts.expiry !== undefined) {\n url += `?17=${normalizeExpiry(opts.expiry)}`;\n }\n return url;\n}\n","/**\n * @ai-matrx/print/zpl — RAW ZPL II FOR ZEBRA-CLASS THERMAL PRINTERS.\n *\n * THE OPT-IN POWER LANE. Every other printer in this package composes an HTML\n * document and hands it to the OS print dialog, which rasterizes it. That is\n * the right default: it works on every printer with a plain driver, needs no\n * vendor SDK, and previews honestly on screen.\n *\n * It is also the wrong tool for one specific job. A browser rasterizes a QR at\n * whatever DPI the driver negotiates and then the printer re-samples it, so on\n * 203-dpi roll stock a small symbol's modules land on fractional dots and blur.\n * A ZPL `^BQ` command instead tells the printer \"draw this symbol at exactly N\n * dots per module\" — the modules are dot-aligned by construction, which is why\n * a raw-ZPL label out-scans a rasterized one at the dock door every time.\n *\n * SCOPE — deliberately narrow:\n * - ROLL STOCK ONLY. ZPL's page model is one continuous label; a 30-up Avery\n * sheet is a laser-printer artifact with no ZPL equivalent. Passing a sheet\n * template throws and names `printQrLabelSheet` instead.\n * - PURE STRING CODEGEN. Zero new dependencies, zero I/O. This module builds\n * the ZPL text; sending it to a printer (Browser Print, a raw TCP 9100\n * socket, a print server) is the host's job and stays out of the package.\n * - DETERMINISTIC. The same template + label + options always produce a\n * byte-identical string. Nothing here reads a clock, a random source, or the\n * DOM, and the module is import-inert with no mutable state.\n *\n * GEOMETRY comes from the SAME brain the HTML lane uses: `computeCellLayout`\n * from `./labels`, so the QR box, padding and badge zone are identical across\n * the two lanes and a design verified on screen prints the same on the Zebra.\n * Inches become dots by `round(inches * dpi)` — 203 dpi (8 dots/mm) is the\n * QL/ZD-class norm and the default; 300 dpi (12 dots/mm) is the other common\n * head. There is no 600-dpi option because no roll printer we recommend has one.\n *\n * Every command emitted below carries a one-line comment. For many readers of\n * this repo this file is their first ZPL, and an uncommented `^FO12,34^A0N,28,0`\n * is indistinguishable from line noise.\n *\n * Docs: FEATURE.md, NOTES-CHANGELOG.md, and\n * common-docs/projects/npm-package-extraction/PRINT-PACKAGE-DESIGN.md.\n */\n\nimport {\n computeCellLayout,\n MIN_SCANNABLE_MODULE_MM,\n type LabelTemplate,\n type QrLabel,\n type ScannabilityReport,\n} from \"./labels\";\nimport { minQrVersion, qrModuleCount, type QrEcLevel } from \"./qr\";\n\n// ─── Contract ────────────────────────────────────────────────────────────────\n\n/**\n * Printhead resolution in dots per inch.\n *\n * 203 = 8 dots/mm — the Zebra ZD/GK/GX and Brother QL class; the default.\n * 300 = 12 dots/mm — the higher-resolution ZD621/ZT heads.\n */\nexport type ZplDpi = 203 | 300;\n\nexport interface ZplOptions {\n /** Printhead resolution. Default 203 — the QL/ZD-class norm. */\n dpi?: ZplDpi;\n /**\n * Burn temperature offset, emitted as `^MD`. Range -30..30, where 0 is the\n * printer's configured darkness. OMITTED ENTIRELY when unset, so the\n * printer keeps whatever the operator calibrated — we never silently\n * override a tuned head.\n */\n darkness?: number;\n /** Labels printed per block, emitted as `^PQ`. Default 1 (no `^PQ` line). */\n copies?: number;\n /**\n * QR error correction. Default \"M\" — the platform default from `./qr`,\n * NEVER \"L\": these labels get scuffed and read at an angle.\n *\n * (Additive to the wave-3 spec's three fields: the HTML lane exposes error\n * correction as a first-class setting, and a ZPL lane that could not match\n * it would silently print a different symbol from the same design.)\n */\n ecLevel?: QrEcLevel;\n}\n\n/** ZPL's `^BQ` magnification is dots-per-module and is bounded 1..10 by the firmware. */\nexport const ZPL_MIN_MAGNIFICATION = 1;\nexport const ZPL_MAX_MAGNIFICATION = 10;\n\n/**\n * ISO/IEC 18004 §6.3.8 quiet zone, in modules.\n *\n * `^BQ` draws the SYMBOL ONLY — unlike the `qrcode` data URIs the HTML lane\n * inlines, the printer adds no margin of its own. The quiet zone is therefore\n * ours to reserve: we size the symbol against `modules + 8` and inset the field\n * origin by 4 magnified modules on each side.\n */\nexport const ZPL_QUIET_ZONE_MODULES = 4;\n\nexport const DEFAULT_ZPL_DPI: ZplDpi = 203;\nconst DEFAULT_ZPL_EC_LEVEL: QrEcLevel = \"M\";\n\nconst MM_PER_IN = 25.4;\n\n/** The exact guidance a sheet template earns. Exported so tests and hosts share one string. */\nexport const ZPL_SHEET_STOCK_MESSAGE =\n \"ZPL is for roll stock — use printQrLabelSheet for sheets\";\n\n// ─── Option resolution ───────────────────────────────────────────────────────\n\ninterface ResolvedZplOptions {\n dpi: ZplDpi;\n darkness: number | undefined;\n copies: number;\n ecLevel: QrEcLevel;\n}\n\nfunction resolveOptions(opts: ZplOptions | undefined): ResolvedZplOptions {\n const dpi = opts?.dpi ?? DEFAULT_ZPL_DPI;\n if (dpi !== 203 && dpi !== 300) {\n throw new Error(\n `ZPL dpi must be 203 (8 dots/mm) or 300 (12 dots/mm), got ${String(dpi)}.`,\n );\n }\n\n const copies = opts?.copies ?? 1;\n if (!Number.isInteger(copies) || copies < 1 || copies > 9999) {\n throw new Error(`ZPL copies must be an integer between 1 and 9999, got ${String(copies)}.`);\n }\n\n const rawDarkness = opts?.darkness;\n let darkness: number | undefined;\n if (rawDarkness !== undefined) {\n if (!Number.isInteger(rawDarkness) || rawDarkness < -30 || rawDarkness > 30) {\n throw new Error(\n `ZPL darkness (^MD) must be an integer between -30 and 30, got ${String(rawDarkness)}.`,\n );\n }\n darkness = rawDarkness;\n }\n\n const ecLevel = opts?.ecLevel ?? DEFAULT_ZPL_EC_LEVEL;\n if (ecLevel !== \"L\" && ecLevel !== \"M\" && ecLevel !== \"Q\" && ecLevel !== \"H\") {\n throw new Error(\n `ZPL QR error correction must be \"L\", \"M\", \"Q\" or \"H\", got ${String(ecLevel)}.`,\n );\n }\n\n // exactOptionalPropertyTypes: `darkness` is `number | undefined` by type,\n // and the emitter checks for undefined — no key is ever set to undefined.\n return { dpi, darkness, copies, ecLevel };\n}\n\nfunction assertRollStock(template: LabelTemplate): void {\n if (template.kind !== \"roll\") {\n throw new Error(\n `${ZPL_SHEET_STOCK_MESSAGE} (template \"${template.id}\" — ${template.stockCode} — is ` +\n `${template.cols}×${template.rows} sheet stock, which has no ZPL page model).`,\n );\n }\n}\n\n// ─── Field data escaping (^FH) ───────────────────────────────────────────────\n\nconst HEX = \"0123456789ABCDEF\";\n\n/**\n * Escape arbitrary text for a `^FD` field.\n *\n * THE CHOICE: we ALWAYS pair `^FD` with `^FH` (hex escape) and encode anything\n * that is not plain, safe printable ASCII as `_xx` UTF-8 bytes. The alternative\n * — swapping ZPL's control/format/delimiter characters with `^CC`/`^CT`/`^CD`\n * — mutates printer state that outlives the label, so a caption containing a\n * caret could change how every SUBSEQUENT label parses. Hex escaping is local\n * to the one field and cannot leak.\n *\n * Escaped, and why:\n * - `^` (0x5E) — the ZPL format-command prefix; unescaped it starts a command.\n * - `~` (0x7E) — the ZPL control-command prefix; same failure, immediately.\n * - `_` (0x5F) — the `^FH` escape character itself, so literal underscores in a\n * SKU survive instead of eating the next two characters.\n * - Anything outside 0x20..0x7E — control characters (a stray newline or NUL\n * would truncate the field) and every non-ASCII byte. `^CI28` puts the\n * printer in UTF-8, so an \"é\" is emitted as its two UTF-8 bytes `_C3_A9`.\n *\n * Everything else passes through verbatim, which keeps a normal SKU readable in\n * the generated ZPL — an operator debugging a label can still find the string.\n */\nexport function zplFieldData(text: string): string {\n if (typeof text !== \"string\") {\n throw new Error(`ZPL field data must be a string, got ${String(text)}.`);\n }\n if (text === \"\") return \"\";\n\n const bytes = new TextEncoder().encode(text);\n let out = \"\";\n for (const byte of bytes) {\n const safe =\n byte >= 0x20 &&\n byte <= 0x7e &&\n byte !== 0x5e /* ^ */ &&\n byte !== 0x7e /* ~ — the last printable byte, and ZPL's control prefix */ &&\n byte !== 0x5f; /* _ */\n if (safe) {\n out += String.fromCharCode(byte);\n } else {\n // `^FH`'s default escape character is `_`, followed by two hex digits.\n out += `_${HEX[(byte >> 4) & 0xf] ?? \"0\"}${HEX[byte & 0xf] ?? \"0\"}`;\n }\n }\n return out;\n}\n\n// ─── Geometry ────────────────────────────────────────────────────────────────\n\n/** Inches → printer dots. The one conversion; every coordinate below goes through it. */\nfunction dots(inches: number, dpi: ZplDpi): number {\n return Math.round(inches * dpi);\n}\n\nfunction clampMagnification(value: number): number {\n if (!Number.isFinite(value)) return ZPL_MIN_MAGNIFICATION;\n return Math.min(ZPL_MAX_MAGNIFICATION, Math.max(ZPL_MIN_MAGNIFICATION, Math.floor(value)));\n}\n\nfunction payloadByteLength(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\n/**\n * Dots per QR module for a payload inside a box of `qrBoxDots`.\n *\n * The box holds the symbol AND both quiet zones, so the divisor is\n * `qrModuleCount(version) + 2 * 4`. `floor` (never round) keeps the drawn\n * symbol inside its box; the clamp is the firmware's own 1..10 limit, which\n * means a box too small for even one dot per module still emits a valid label —\n * an unscannable one, which is exactly what `assertZplScannable` reports.\n */\nfunction magnificationFor(qrBoxDots: number, payloadBytes: number, ecLevel: QrEcLevel): QrFit {\n const version = minQrVersion(payloadBytes, ecLevel);\n const modulesPerSide = qrModuleCount(version) + ZPL_QUIET_ZONE_MODULES * 2;\n const magnification = clampMagnification(qrBoxDots / modulesPerSide);\n return { version, magnification, drawnDots: modulesPerSide * magnification };\n}\n\ninterface QrFit {\n version: number;\n magnification: number;\n /**\n * What the printer ACTUALLY draws, quiet zone included: `modules × mag`.\n *\n * This is ≤ the layout box, sometimes well below it — `mag` is a whole\n * number of dots and caps at 10, so a short payload on large stock cannot\n * fill its box. Layout uses THIS, never the box: centring a 290-dot symbol\n * inside a 455-dot box leaves it visibly off-centre with a phantom gap\n * underneath (confirmed on a Labelary render during development).\n */\n drawnDots: number;\n}\n\n/** The caption zone height, mirroring the HTML lane's two branches exactly. */\nfunction captionZoneIn(t: LabelTemplate, wide: boolean): number {\n return wide ? t.labelHIn * 0.3 : Math.min(0.28, t.labelHIn * 0.16);\n}\n\n// ─── Command emitters (one comment per command) ──────────────────────────────\n\n/**\n * A scalable-font text field.\n *\n * `^FO x,y` — field origin, top-left of the field, in dots from ^LH.\n * `^A0N,h,0` — font 0 (the built-in scalable), N = normal rotation,\n * h dots tall, width 0 = keep the font's own aspect ratio.\n * `^FB w,1,0,j` — field block: wrap/justify inside w dots, at most 1 line\n * (overflow is dropped, never spilled onto the neighbour),\n * 0 extra line spacing, j = L(eft) or C(enter).\n * `^FH` — the following ^FD uses `_xx` hex escapes.\n * `^FD…^FS` — the field data, terminated.\n */\nfunction textField(\n x: number,\n y: number,\n widthDots: number,\n fontHeightDots: number,\n justify: \"L\" | \"C\",\n text: string,\n): string {\n return (\n `^FO${x},${y}` +\n `^A0N,${fontHeightDots},0` +\n `^FB${widthDots},1,0,${justify}` +\n `^FH^FD${zplFieldData(text)}^FS`\n );\n}\n\n/**\n * A QR symbol.\n *\n * `^FO x,y` — origin of the SYMBOL, already inset by the quiet zone.\n * `^BQN,2,mag` — QR barcode: N = normal rotation, model 2 (the enhanced\n * model every current scanner reads), mag dots per module.\n * Error correction is NOT passed here: the `d` parameter is\n * firmware-dependent, while the ^FD prefix below is honoured\n * by every ZPL II printer.\n * `^FH^FD<ec>A,…` — ^FD for a QR carries switches before the data: the error\n * correction letter (H/Q/M/L), then `A` for AUTOMATIC input\n * mode (the printer picks numeric/alphanumeric/byte itself),\n * then a comma, then the payload. ^FH hex-escapes the\n * payload only — the switches are plain ASCII.\n */\nfunction qrField(x: number, y: number, magnification: number, ecLevel: QrEcLevel, value: string): string {\n return `^FO${x},${y}^BQN,2,${magnification}^FH^FD${ecLevel}A,${zplFieldData(value)}^FS`;\n}\n\n// ─── The one-label renderer ──────────────────────────────────────────────────\n\nfunction cleanLines(lines: readonly string[] | undefined): string[] {\n if (lines === undefined) return [];\n return lines.map((l) => l.trim()).filter((l) => l !== \"\");\n}\n\n/**\n * ONE roll label → a complete `^XA…^XZ` block.\n *\n * The block always opens with the same four lines so a label never inherits\n * state from whatever the printer last received:\n * `^XA` — start of label format.\n * `^CI28` — character set 28 = UTF-8, so `^FH` hex bytes decode as UTF-8.\n * `^LH0,0` — label home back to the physical origin (a previous format\n * may have shifted it; this is the classic \"everything is\n * offset by 30 dots\" bug).\n * `^PW` / `^LL` — print width and label length in dots, from the template's\n * inch fields. Roll stock IS the label, so these are the\n * die-cut dimensions.\n *\n * Zone order mirrors the HTML lane: QR, then the large badge, then the bold\n * caption, then the small detail lines — stacked and centred on tall stock,\n * QR-left/text-right on stock at least 1.8× as wide as it is tall.\n *\n * @throws if `template.kind !== \"roll\"` — see ZPL_SHEET_STOCK_MESSAGE.\n */\nexport function labelToZpl(template: LabelTemplate, label: QrLabel, opts?: ZplOptions): string {\n assertRollStock(template);\n const o = resolveOptions(opts);\n\n const captionText = (label.caption ?? label.qrValue).trim();\n const badgeText = (label.badge ?? \"\").trim();\n const lines = cleanLines(label.lines);\n const hasQr = typeof label.qrValue === \"string\" && label.qrValue !== \"\";\n\n // The SAME layout brain the HTML/PDF lanes use — never a forked copy.\n const layout = computeCellLayout(template, captionText !== \"\", badgeText !== \"\");\n\n const labelWDots = dots(template.labelWIn, o.dpi);\n const labelHDots = dots(template.labelHIn, o.dpi);\n const padDots = dots(layout.padIn, o.dpi);\n const qrBoxDots = hasQr ? dots(layout.qrIn, o.dpi) : 0;\n const badgeZoneDots = badgeText !== \"\" ? dots(layout.badgeZoneIn, o.dpi) : 0;\n const captionZoneDots =\n captionText !== \"\" ? dots(captionZoneIn(template, layout.wide), o.dpi) : 0;\n\n // Detail lines are a fixed 8pt, matching the HTML lane's `font-size:8pt`.\n const lineFontDots = Math.max(8, Math.round((o.dpi * 8) / 72));\n const lineStepDots = Math.round(lineFontDots * 1.25);\n const linesHDots = lines.length * lineStepDots;\n\n // Text is 80% of the badge zone and 72% of the caption zone; the remainder\n // is the leading that keeps a descender off the next zone.\n const badgeFontDots = Math.max(10, Math.round(badgeZoneDots * 0.8));\n const captionFontDots = Math.max(8, Math.round(captionZoneDots * 0.72));\n\n const commands: string[] = [];\n commands.push(\"^XA\"); // start of label format\n commands.push(\"^CI28\"); // UTF-8 input encoding\n commands.push(\"^LH0,0\"); // reset label home to the physical origin\n commands.push(`^PW${dots(template.labelWIn, o.dpi)}`); // print width, dots\n commands.push(`^LL${dots(template.labelHIn, o.dpi)}`); // label length, dots\n if (o.darkness !== undefined) {\n commands.push(`^MD${o.darkness}`); // burn-temperature offset, -30..30\n }\n\n const fit: QrFit = hasQr\n ? magnificationFor(qrBoxDots, payloadByteLength(label.qrValue), o.ecLevel)\n : { version: 0, magnification: 0, drawnDots: 0 };\n // The quiet zone is drawn by us, not by ^BQ: inset the symbol by 4 modules.\n const quietDots = fit.magnification * ZPL_QUIET_ZONE_MODULES;\n\n if (layout.wide) {\n // WIDE: QR on the left, vertically centred; text column to its right.\n const qrY = Math.max(padDots, Math.round((labelHDots - fit.drawnDots) / 2));\n if (hasQr) {\n commands.push(\n qrField(padDots + quietDots, qrY + quietDots, fit.magnification, o.ecLevel, label.qrValue),\n );\n }\n const textX = padDots + fit.drawnDots + padDots;\n const textW = Math.max(1, labelWDots - textX - padDots);\n let y = padDots;\n if (badgeText !== \"\") {\n commands.push(textField(textX, y, textW, badgeFontDots, \"L\", badgeText));\n y += badgeZoneDots;\n }\n if (captionText !== \"\") {\n commands.push(textField(textX, y, textW, captionFontDots, \"L\", captionText));\n y += captionZoneDots;\n }\n for (const line of lines) {\n commands.push(textField(textX, y, textW, lineFontDots, \"L\", line));\n y += lineStepDots;\n }\n } else {\n // STACKED: QR on top, everything centred, the whole block centred\n // vertically the way the HTML lane's flex column does.\n const contentH = fit.drawnDots + badgeZoneDots + captionZoneDots + linesHDots;\n const textW = Math.max(1, labelWDots - padDots * 2);\n let y = Math.max(padDots, Math.round((labelHDots - contentH) / 2));\n if (hasQr) {\n const qrX = Math.max(0, Math.round((labelWDots - fit.drawnDots) / 2));\n commands.push(\n qrField(qrX + quietDots, y + quietDots, fit.magnification, o.ecLevel, label.qrValue),\n );\n y += fit.drawnDots;\n }\n if (badgeText !== \"\") {\n commands.push(textField(padDots, y, textW, badgeFontDots, \"C\", badgeText));\n y += badgeZoneDots;\n }\n if (captionText !== \"\") {\n commands.push(textField(padDots, y, textW, captionFontDots, \"C\", captionText));\n y += captionZoneDots;\n }\n for (const line of lines) {\n commands.push(textField(padDots, y, textW, lineFontDots, \"C\", line));\n y += lineStepDots;\n }\n }\n\n if (o.copies > 1) {\n // ^PQ q,0,0,N — print quantity q, no pause/cut group, no cut-on-error.\n commands.push(`^PQ${o.copies},0,0,N`);\n }\n commands.push(\"^XZ\"); // end of label format — the printer prints here\n\n return commands.join(\"\\n\");\n}\n\n/**\n * BATCH: one ZPL document, one `^XA…^XZ` block per label.\n *\n * There is no shared preamble between blocks on purpose — each block is\n * self-sufficient (`^CI28`, `^LH`, `^PW`, `^LL`), so a stalled or re-sent block\n * still prints correctly and a host can split the document anywhere.\n *\n * An empty list returns an empty string: nothing to print is not an error.\n */\nexport function labelsToZpl(\n template: LabelTemplate,\n labels: readonly QrLabel[],\n opts?: ZplOptions,\n): string {\n assertRollStock(template);\n resolveOptions(opts); // validate once, up front, even for an empty batch\n if (labels.length === 0) return \"\";\n return labels.map((label) => labelToZpl(template, label, opts)).join(\"\\n\");\n}\n\n// ─── Scannability (the ZPL twin of labels' assertScannable) ──────────────────\n\n/**\n * Will a payload of `payloadByteLen` bytes actually scan off this roll stock in\n * the ZPL lane?\n *\n * Same verdict shape and same 0.4mm floor as `assertScannable` in `./labels`,\n * but measured through ZPL's own quantization: the printed module size is\n * `magnification × (25.4 / dpi)` mm, because `^BQ` draws a module as a whole\n * number of dots. That quantization is the entire point of this lane — and it\n * is also why a box that would have been fine as a rasterized image can round\n * DOWN to an unscannable magnification here.\n *\n * Never throws. A sheet template, an over-capacity payload, and a too-dense\n * combination all come back as `ok: false` with the reason in `message`.\n */\nexport function assertZplScannable(\n template: LabelTemplate,\n payloadByteLen: number,\n opts?: ZplOptions,\n): ScannabilityReport {\n const fail = (message: string, version = 0, moduleSizeMm = 0): ScannabilityReport => ({\n ok: false,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n message,\n });\n\n if (template.kind !== \"roll\") {\n return fail(\n `${ZPL_SHEET_STOCK_MESSAGE} (template \"${template.id}\" — ${template.stockCode} — is sheet stock).`,\n );\n }\n\n let o: ResolvedZplOptions;\n try {\n o = resolveOptions(opts);\n } catch (err) {\n return fail(err instanceof Error ? err.message : String(err));\n }\n\n // Conservative on purpose, exactly like the HTML lane: evaluate with the\n // caption ON, which is the SMALLER QR box.\n const layout = computeCellLayout(template, true);\n const qrBoxDots = dots(layout.qrIn, o.dpi);\n\n let fit: QrFit;\n try {\n fit = magnificationFor(qrBoxDots, payloadByteLen, o.ecLevel);\n } catch (err) {\n // An over-capacity payload (no QR version holds it) reports, never throws.\n return fail(err instanceof Error ? err.message : String(err));\n }\n\n const { version, magnification } = fit;\n // ONE magnified module is `mag` whole dots wide — that is the printed size.\n const moduleSizeMm = (magnification * MM_PER_IN) / o.dpi;\n\n if (moduleSizeMm >= MIN_SCANNABLE_MODULE_MM) {\n return {\n ok: true,\n version,\n moduleSizeMm,\n minModuleSizeMm: MIN_SCANNABLE_MODULE_MM,\n };\n }\n return fail(\n `A ${payloadByteLen}-byte payload needs a version ${version} QR, which ZPL prints at ` +\n `magnification ${magnification} (${moduleSizeMm.toFixed(3)}mm per module) on ` +\n `${template.stockCode} at ${o.dpi}dpi — below the ${MIN_SCANNABLE_MODULE_MM}mm floor a ` +\n `scanner can resolve. Shorten the payload (a short resolver URL is the intended fix), ` +\n `move to larger roll stock, or print on a 300dpi head.`,\n version,\n moduleSizeMm,\n );\n}\n\nexport type { ScannabilityReport } from \"./labels\";\n"],"mappings":"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","names":["escapeHtml","escapeHtml","caption","badge","MM_PER_IN","version","moduleSizeMm"]}