@ai-matrx/print 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts","../src/qr.ts","../src/labels.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// ─── 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 print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | Promise<void>;\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\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\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 * 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 */\nexport function openPrintWindow(\n htmlDocument: string,\n filename = \"print\",\n): void {\n const win = window.open(\"\", \"_blank\", \"width=920,height=720,scrollbars=yes\");\n if (!win) {\n // Popup blocked — fall back to file download\n const blob = new Blob([htmlDocument], { type: \"text/html;charset=utf-8\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `${filename.replace(/\\s+/g, \"-\").toLowerCase()}.html`;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n return;\n }\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): void {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n 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\nexport { escapeHtml };\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/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 escapeHtml,\n openPrintWindow,\n type BlockPrinter,\n type PrintSettings,\n} from \"./core\";\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): void {\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 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): void {\n 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<void> {\n const t = resolveTemplate(data, variantId);\n const s = resolveSettings(rawSettings);\n\n if (s.calibrationPage) {\n printCalibrationSheet(t);\n return;\n }\n\n const labels = expandLabels(data.labels ?? [], s);\n if (!labels.length) {\n 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 return;\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 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"],"mappings":"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","names":["caption","badge"]}
package/dist/pdf.cjs ADDED
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/pdf.ts
31
+ var pdf_exports = {};
32
+ __export(pdf_exports, {
33
+ captureBlockElement: () => captureBlockElement,
34
+ captureElementsToPDF: () => captureElementsToPDF,
35
+ captureElementsToPDFBlob: () => captureElementsToPDFBlob,
36
+ captureToClipboardImage: () => captureToClipboardImage,
37
+ captureToPDF: () => captureToPDF,
38
+ captureToPDFBlob: () => captureToPDFBlob,
39
+ createDomCapturePrinter: () => createDomCapturePrinter,
40
+ markdownToPdfBlob: () => markdownToPdfBlob,
41
+ sanitizeForCapture: () => sanitizeForCapture
42
+ });
43
+ module.exports = __toCommonJS(pdf_exports);
44
+ var PAGE_DIMS = {
45
+ a4: { w: 210, h: 297 },
46
+ letter: { w: 215.9, h: 279.4 }
47
+ };
48
+ var UNSAFE_COLOR_RE = /\b(oklch|oklab|lab|lch|color)\s*\(/i;
49
+ function isSafeColor(value) {
50
+ return !UNSAFE_COLOR_RE.test(value);
51
+ }
52
+ function safenColor(value, prop, isDark) {
53
+ if (isSafeColor(value)) return value;
54
+ const lp = prop.toLowerCase();
55
+ if (lp.includes("background")) return isDark ? "#1f2937" : "#ffffff";
56
+ if (lp === "color") return isDark ? "#f3f4f6" : "#111827";
57
+ if (lp.includes("border") || lp.includes("outline"))
58
+ return isDark ? "#4b5563" : "#d1d5db";
59
+ if (lp === "fill") return isDark ? "#9ca3af" : "#374151";
60
+ if (lp === "stroke") return isDark ? "#6b7280" : "#6b7280";
61
+ return isDark ? "#6b7280" : "#6b7280";
62
+ }
63
+ function patchGetComputedStyle(isDark) {
64
+ const original = window.getComputedStyle;
65
+ const callOriginal = original.bind(window);
66
+ window.getComputedStyle = function patchedGetComputedStyle(elt, pseudoElt) {
67
+ const computed = callOriginal(elt, pseudoElt);
68
+ return new Proxy(computed, {
69
+ get(target, prop) {
70
+ const value = Reflect.get(target, prop, target);
71
+ if (typeof prop === "string" && typeof value === "string" && value) {
72
+ if (!isSafeColor(value)) {
73
+ return safenColor(value, prop, isDark);
74
+ }
75
+ }
76
+ if (prop === "getPropertyValue") {
77
+ return (propName) => {
78
+ const raw = target.getPropertyValue(propName);
79
+ if (raw && !isSafeColor(raw)) {
80
+ return safenColor(raw, propName, isDark);
81
+ }
82
+ return raw;
83
+ };
84
+ }
85
+ if (typeof value === "function") {
86
+ const fn = value;
87
+ return fn.bind(target);
88
+ }
89
+ return value;
90
+ }
91
+ });
92
+ };
93
+ return () => {
94
+ window.getComputedStyle = original;
95
+ };
96
+ }
97
+ async function captureToPDF(element, options = {}) {
98
+ const pdf = await renderElementToPdf(element, options);
99
+ pdf.save(`${options.filename ?? "ai-response"}.pdf`);
100
+ }
101
+ async function captureToPDFBlob(element, options = {}) {
102
+ const pdf = await renderElementToPdf(element, options);
103
+ return pdf.output("blob");
104
+ }
105
+ async function captureElementsToPDF(elements, options = {}) {
106
+ const pdf = await renderElementsToPdf(elements, options);
107
+ pdf.save(`${options.filename ?? "ai-response"}.pdf`);
108
+ }
109
+ async function captureElementsToPDFBlob(elements, options = {}) {
110
+ const pdf = await renderElementsToPdf(elements, options);
111
+ return pdf.output("blob");
112
+ }
113
+ async function renderElementsToPdf(elements, options) {
114
+ if (elements.length === 0) {
115
+ throw new Error("No report pages are available to export.");
116
+ }
117
+ const {
118
+ paperSize = "a4",
119
+ orientation = "portrait",
120
+ scale = 2,
121
+ background = "#ffffff",
122
+ imageFormat = "png",
123
+ imageQuality = 0.92,
124
+ onProgress
125
+ } = options;
126
+ const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([
127
+ import("html2canvas"),
128
+ import("jspdf")
129
+ ]);
130
+ const dims = PAGE_DIMS[paperSize];
131
+ const pageW = orientation === "portrait" ? dims.w : dims.h;
132
+ const pageH = orientation === "portrait" ? dims.h : dims.w;
133
+ const pdf = new jsPDF({ orientation, unit: "mm", format: paperSize });
134
+ const restoreGetComputedStyle = patchGetComputedStyle(false);
135
+ try {
136
+ for (const [index, element] of elements.entries()) {
137
+ onProgress?.(index, elements.length);
138
+ const canvas = await html2canvas(element, {
139
+ scale,
140
+ useCORS: true,
141
+ backgroundColor: background,
142
+ logging: false,
143
+ windowWidth: element.scrollWidth,
144
+ windowHeight: element.scrollHeight
145
+ });
146
+ if (index > 0) pdf.addPage();
147
+ const isJpeg = imageFormat === "jpeg";
148
+ pdf.addImage(
149
+ canvas.toDataURL(
150
+ isJpeg ? "image/jpeg" : "image/png",
151
+ isJpeg ? imageQuality : void 0
152
+ ),
153
+ isJpeg ? "JPEG" : "PNG",
154
+ 0,
155
+ 0,
156
+ pageW,
157
+ pageH
158
+ );
159
+ onProgress?.(index + 1, elements.length);
160
+ }
161
+ } finally {
162
+ restoreGetComputedStyle();
163
+ }
164
+ return pdf;
165
+ }
166
+ async function renderElementToPdf(element, options = {}) {
167
+ const {
168
+ paperSize = "a4",
169
+ orientation = "portrait",
170
+ scale = 2,
171
+ background = "#ffffff",
172
+ imageFormat = "png",
173
+ imageQuality = 0.92,
174
+ onProgress
175
+ } = options;
176
+ const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([
177
+ import("html2canvas"),
178
+ import("jspdf")
179
+ ]);
180
+ const dims = PAGE_DIMS[paperSize];
181
+ const pageW = orientation === "portrait" ? dims.w : dims.h;
182
+ const pageH = orientation === "portrait" ? dims.h : dims.w;
183
+ onProgress?.(0, 1);
184
+ const isDark = options.theme === "light" ? false : document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
185
+ const bgColor = background === "#ffffff" && isDark ? "#111827" : background;
186
+ const restoreGetComputedStyle = patchGetComputedStyle(isDark);
187
+ let canvas;
188
+ try {
189
+ canvas = await html2canvas(element, {
190
+ scale,
191
+ useCORS: true,
192
+ backgroundColor: bgColor,
193
+ logging: false,
194
+ windowWidth: element.scrollWidth,
195
+ windowHeight: element.scrollHeight
196
+ });
197
+ } finally {
198
+ restoreGetComputedStyle();
199
+ }
200
+ const isJpeg = imageFormat === "jpeg";
201
+ const imgData = canvas.toDataURL(
202
+ isJpeg ? "image/jpeg" : "image/png",
203
+ isJpeg ? imageQuality : void 0
204
+ );
205
+ const imgFormat = isJpeg ? "JPEG" : "PNG";
206
+ const imgW = pageW;
207
+ const imgH = canvas.height * pageW / canvas.width;
208
+ const pdf = new jsPDF({ orientation, unit: "mm", format: paperSize });
209
+ let heightLeft = imgH;
210
+ let yOffset = 0;
211
+ let page = 0;
212
+ pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);
213
+ heightLeft -= pageH;
214
+ page++;
215
+ const totalPages = Math.ceil(imgH / pageH);
216
+ while (heightLeft > 0) {
217
+ yOffset -= pageH;
218
+ pdf.addPage();
219
+ pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);
220
+ heightLeft -= pageH;
221
+ page++;
222
+ onProgress?.(page, totalPages);
223
+ }
224
+ return pdf;
225
+ }
226
+ async function captureToClipboardImage(element) {
227
+ const { default: html2canvas } = await import("html2canvas");
228
+ const isDark = document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches;
229
+ const restoreGetComputedStyle = patchGetComputedStyle(isDark);
230
+ let canvas;
231
+ try {
232
+ canvas = await html2canvas(element, {
233
+ scale: 2,
234
+ useCORS: true,
235
+ backgroundColor: isDark ? "#111827" : "#ffffff",
236
+ logging: false
237
+ });
238
+ } finally {
239
+ restoreGetComputedStyle();
240
+ }
241
+ await new Promise((resolve, reject) => {
242
+ canvas.toBlob(async (blob) => {
243
+ if (!blob) {
244
+ resolve();
245
+ return;
246
+ }
247
+ try {
248
+ try {
249
+ await navigator.clipboard.write([
250
+ new ClipboardItem({ "image/png": blob })
251
+ ]);
252
+ } catch {
253
+ const url = URL.createObjectURL(blob);
254
+ const a = document.createElement("a");
255
+ a.href = url;
256
+ a.download = "capture.png";
257
+ document.body.appendChild(a);
258
+ a.click();
259
+ document.body.removeChild(a);
260
+ URL.revokeObjectURL(url);
261
+ }
262
+ resolve();
263
+ } catch (error) {
264
+ reject(error instanceof Error ? error : new Error(String(error)));
265
+ }
266
+ }, "image/png");
267
+ });
268
+ }
269
+ function createDomCapturePrinter(blockLabel) {
270
+ return {
271
+ label: `Print ${blockLabel}`,
272
+ variants: [],
273
+ // No-op: DOM capture blocks use captureElement() directly
274
+ print: () => {
275
+ }
276
+ };
277
+ }
278
+ async function captureBlockElement(element, filename, orientation = "landscape") {
279
+ await captureToPDF(element, {
280
+ filename,
281
+ scale: 2,
282
+ orientation
283
+ });
284
+ }
285
+ function sanitizeForCapture(html) {
286
+ const template = document.createElement("template");
287
+ template.innerHTML = html;
288
+ const fragment = template.content;
289
+ fragment.querySelectorAll("script, iframe, object, embed, link, meta").forEach((el) => el.remove());
290
+ fragment.querySelectorAll("*").forEach((el) => {
291
+ for (const attr of Array.from(el.attributes)) {
292
+ const name = attr.name.toLowerCase();
293
+ if (name.startsWith("on")) el.removeAttribute(attr.name);
294
+ if ((name === "href" || name === "src") && attr.value.trim().toLowerCase().startsWith("javascript:")) {
295
+ el.removeAttribute(attr.name);
296
+ }
297
+ }
298
+ });
299
+ return fragment;
300
+ }
301
+ async function markdownToPdfBlob(markdown, deps) {
302
+ if (!markdown.trim()) {
303
+ throw new Error("Nothing to convert \u2014 the content is empty.");
304
+ }
305
+ const bodyHtml = deps.convertToHtml(markdown);
306
+ const css = await deps.loadCss();
307
+ const host = document.createElement("div");
308
+ host.style.cssText = "position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;";
309
+ host.setAttribute("aria-hidden", "true");
310
+ const style = document.createElement("style");
311
+ style.textContent = css;
312
+ host.appendChild(style);
313
+ const contentEl = document.createElement("div");
314
+ contentEl.className = "matrx-content-container";
315
+ contentEl.style.cssText = "padding:32px;background:#ffffff;";
316
+ contentEl.appendChild(sanitizeForCapture(bodyHtml));
317
+ host.appendChild(contentEl);
318
+ document.body.appendChild(host);
319
+ try {
320
+ return await captureToPDFBlob(host, { background: "#fff", theme: "light" });
321
+ } finally {
322
+ host.remove();
323
+ }
324
+ }
325
+ //# sourceMappingURL=pdf.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pdf.ts"],"sourcesContent":["/**\n * DOM-Capture Print Utilities — Tier 2 Full-Message Print\n *\n * Captures a rendered DOM element using html2canvas, then pipes it into jsPDF.\n * This handles ALL block types because it screenshots the actual rendered output.\n *\n * Problem: Tailwind CSS 4 uses modern CSS color functions (oklch, lab, color(display-p3))\n * that html2canvas cannot parse. It calls console.error for each one, which gets\n * captured by AdminDebugContextCollector and shows up as false errors.\n *\n * Solution: Patch window.getComputedStyle during the capture so html2canvas always\n * receives safe hex fallbacks instead of unparseable color function strings.\n *\n * ---\n *\n * This module is the packaged union of three source files that always shipped\n * together:\n * • dom-capture-print-utils — the capture/PDF renderers (above),\n * • dom-capture-block-printer — the BlockPrinter factory + captureBlockElement,\n * • markdown-pdf — markdownToPdfBlob.\n *\n * Package-level seams (the only deliberate departures from the originals):\n * • `markdownToPdfBlob` takes its markdown→HTML converter and its stylesheet\n * loader INJECTED (`MarkdownPdfDeps`) instead of dynamically importing app\n * modules. The package cannot reach into an application's feature folders.\n * • `sanitizeForCapture` is a package-added export (it was module-private in\n * the original) so the sanitizer can be tested on its own.\n * • `captureBlockElement` calls `captureToPDF` directly rather than through a\n * dynamic import — html2canvas and jspdf are still lazily imported inside\n * the render functions, which is the laziness that actually matters.\n *\n * html2canvas and jspdf stay behind call-time dynamic imports: importing this\n * module pulls in neither, and the module touches `window`/`document` only when\n * one of its functions is called.\n */\n\nimport type { BlockPrinter } from \"./core\";\n\nexport interface DomCaptureOptions {\n /** Paper size — 'a4' (default) or 'letter' */\n paperSize?: \"a4\" | \"letter\";\n /** Orientation */\n orientation?: \"portrait\" | \"landscape\";\n /** Scale factor for html2canvas (higher = sharper, slower). Default: 2 */\n scale?: number;\n /** Background colour for the capture. Default: '#ffffff' */\n background?: string;\n /** Filename for the downloaded PDF (without extension). Default: 'ai-response' */\n filename?: string;\n /** Image encoding inside the PDF. PNG is lossless; JPEG is much smaller for reports. */\n imageFormat?: \"png\" | \"jpeg\";\n /** JPEG quality from 0 to 1. Ignored for PNG. Default: 0.92. */\n imageQuality?: number;\n /** Called after each page is captured, with current page / total estimate */\n onProgress?: (page: number, total: number) => void;\n /**\n * Color-fallback theme for unparseable (oklch/lab) computed colors.\n * \"auto\" (default) follows the app's dark class — right for capturing\n * on-screen UI. Pass \"light\" for always-light offscreen document renders\n * so dark-mode fallbacks never bleed into a white-background PDF.\n */\n theme?: \"auto\" | \"light\";\n}\n\nconst PAGE_DIMS = {\n a4: { w: 210, h: 297 },\n letter: { w: 215.9, h: 279.4 },\n} as const;\n\n// Matches any modern CSS color function html2canvas cannot parse\nconst UNSAFE_COLOR_RE = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction isSafeColor(value: string): boolean {\n return !UNSAFE_COLOR_RE.test(value);\n}\n\n/**\n * Replaces modern CSS color functions in a single value string with a\n * context-appropriate hex fallback.\n */\nfunction safenColor(value: string, prop: string, isDark: boolean): string {\n if (isSafeColor(value)) return value;\n const lp = prop.toLowerCase();\n if (lp.includes(\"background\")) return isDark ? \"#1f2937\" : \"#ffffff\";\n if (lp === \"color\") return isDark ? \"#f3f4f6\" : \"#111827\";\n if (lp.includes(\"border\") || lp.includes(\"outline\"))\n return isDark ? \"#4b5563\" : \"#d1d5db\";\n if (lp === \"fill\") return isDark ? \"#9ca3af\" : \"#374151\";\n if (lp === \"stroke\") return isDark ? \"#6b7280\" : \"#6b7280\";\n return isDark ? \"#6b7280\" : \"#6b7280\";\n}\n\n/**\n * Patch window.getComputedStyle so that any call during the capture returns\n * safe hex values instead of oklch/lab/etc.\n *\n * html2canvas calls getComputedStyle extensively — this is the authoritative\n * intercept point. Returns a restore function.\n */\nfunction patchGetComputedStyle(isDark: boolean): () => void {\n // Two references on purpose. `original` is the UNBOUND function that was\n // installed before us and is what the restore puts back — restoring a bound\n // wrapper instead would leave one more `.bind` layer installed after every\n // capture, so the real function identity would never come back. `callOriginal`\n // is the bound form we invoke, because getComputedStyle needs `window` as its\n // receiver.\n const original = window.getComputedStyle;\n const callOriginal = original.bind(window);\n\n window.getComputedStyle = function patchedGetComputedStyle(\n elt: Element,\n pseudoElt?: string | null,\n ): CSSStyleDeclaration {\n const computed = callOriginal(elt, pseudoElt);\n\n return new Proxy(computed, {\n get(target, prop) {\n // CSSStyleDeclaration accessors require the native object as their\n // receiver. Using the Proxy as the receiver throws \"Illegal\n // invocation\" while html2canvas clones inline styles.\n const value: unknown = Reflect.get(target, prop, target);\n\n // Intercept string property accesses (color values)\n if (typeof prop === \"string\" && typeof value === \"string\" && value) {\n if (!isSafeColor(value)) {\n return safenColor(value, prop, isDark);\n }\n }\n\n // Intercept getPropertyValue\n if (prop === \"getPropertyValue\") {\n return (propName: string) => {\n const raw = target.getPropertyValue(propName);\n if (raw && !isSafeColor(raw)) {\n return safenColor(raw, propName, isDark);\n }\n return raw;\n };\n }\n\n if (typeof value === \"function\") {\n const fn = value as (...args: unknown[]) => unknown;\n return fn.bind(target);\n }\n\n return value;\n },\n });\n };\n\n return () => {\n window.getComputedStyle = original;\n };\n}\n\n/**\n * Capture a DOM element and export it as a multi-page PDF download.\n */\nexport async function captureToPDF(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementToPdf(element, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/**\n * Capture a DOM element and return the multi-page PDF as a Blob — for\n * callers that upload/persist the PDF instead of downloading it (e.g. the\n * chat \"Save as → PDF Document\" action saving into the user's Files).\n */\nexport async function captureToPDFBlob(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementToPdf(element, options);\n return pdf.output(\"blob\");\n}\n\n/**\n * Capture deliberately paginated DOM elements as one PDF page each.\n *\n * Use this for designed reports whose page boundaries are part of the layout.\n * The ordinary capture helpers intentionally slice one continuous canvas; this\n * variant preserves explicit report pages and therefore cannot cut a section\n * in half at an arbitrary paper boundary.\n */\nexport async function captureElementsToPDF(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<void> {\n const pdf = await renderElementsToPdf(elements, options);\n pdf.save(`${options.filename ?? \"ai-response\"}.pdf`);\n}\n\n/** Blob form of captureElementsToPDF for durable file-system uploads. */\nexport async function captureElementsToPDFBlob(\n elements: HTMLElement[],\n options: DomCaptureOptions = {},\n): Promise<Blob> {\n const pdf = await renderElementsToPdf(elements, options);\n return pdf.output(\"blob\");\n}\n\nasync function renderElementsToPdf(\n elements: HTMLElement[],\n options: DomCaptureOptions,\n) {\n if (elements.length === 0) {\n throw new Error(\"No report pages are available to export.\");\n }\n\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n const restoreGetComputedStyle = patchGetComputedStyle(false);\n\n try {\n for (const [index, element] of elements.entries()) {\n onProgress?.(index, elements.length);\n const canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: background,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n });\n if (index > 0) pdf.addPage();\n const isJpeg = imageFormat === \"jpeg\";\n pdf.addImage(\n canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n ),\n isJpeg ? \"JPEG\" : \"PNG\",\n 0,\n 0,\n pageW,\n pageH,\n );\n onProgress?.(index + 1, elements.length);\n }\n } finally {\n restoreGetComputedStyle();\n }\n\n return pdf;\n}\n\nasync function renderElementToPdf(\n element: HTMLElement,\n options: DomCaptureOptions = {},\n) {\n const {\n paperSize = \"a4\",\n orientation = \"portrait\",\n scale = 2,\n background = \"#ffffff\",\n imageFormat = \"png\",\n imageQuality = 0.92,\n onProgress,\n } = options;\n\n const [{ default: html2canvas }, { default: jsPDF }] = await Promise.all([\n import(\"html2canvas\"),\n import(\"jspdf\"),\n ]);\n\n const dims = PAGE_DIMS[paperSize];\n const pageW = orientation === \"portrait\" ? dims.w : dims.h;\n const pageH = orientation === \"portrait\" ? dims.h : dims.w;\n\n onProgress?.(0, 1);\n\n const isDark =\n options.theme === \"light\"\n ? false\n : document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const bgColor = background === \"#ffffff\" && isDark ? \"#111827\" : background;\n\n const restoreGetComputedStyle = patchGetComputedStyle(isDark);\n\n let canvas: HTMLCanvasElement;\n try {\n canvas = await html2canvas(element, {\n scale,\n useCORS: true,\n backgroundColor: bgColor,\n logging: false,\n windowWidth: element.scrollWidth,\n windowHeight: element.scrollHeight,\n });\n } finally {\n restoreGetComputedStyle();\n }\n\n // imageFormat/imageQuality are honoured here too. They are documented on\n // DomCaptureOptions with no \"elements-only\" caveat, and the paginated\n // renderer above already obeys them; this path used to hardcode PNG and\n // silently drop whatever the caller asked for. PNG remains the default, so\n // callers that never set the option are unaffected.\n const isJpeg = imageFormat === \"jpeg\";\n const imgData = canvas.toDataURL(\n isJpeg ? \"image/jpeg\" : \"image/png\",\n isJpeg ? imageQuality : undefined,\n );\n const imgFormat = isJpeg ? \"JPEG\" : \"PNG\";\n const imgW = pageW;\n const imgH = (canvas.height * pageW) / canvas.width;\n\n const pdf = new jsPDF({ orientation, unit: \"mm\", format: paperSize });\n\n let heightLeft = imgH;\n let yOffset = 0;\n let page = 0;\n\n pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);\n heightLeft -= pageH;\n page++;\n\n const totalPages = Math.ceil(imgH / pageH);\n while (heightLeft > 0) {\n yOffset -= pageH;\n pdf.addPage();\n pdf.addImage(imgData, imgFormat, 0, yOffset, imgW, imgH);\n heightLeft -= pageH;\n page++;\n onProgress?.(page, totalPages);\n }\n\n return pdf;\n}\n\n/**\n * Capture a DOM element and copy it to clipboard (or download) as PNG.\n */\nexport async function captureToClipboardImage(\n element: HTMLElement,\n): Promise<void> {\n const { default: html2canvas } = await import(\"html2canvas\");\n\n const isDark =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n const restoreGetComputedStyle = patchGetComputedStyle(isDark);\n\n let canvas: HTMLCanvasElement;\n try {\n canvas = await html2canvas(element, {\n scale: 2,\n useCORS: true,\n backgroundColor: isDark ? \"#111827\" : \"#ffffff\",\n logging: false,\n });\n } finally {\n restoreGetComputedStyle();\n }\n\n // The returned promise covers the copy/download, not just the capture. The\n // original resolved the moment toBlob was *scheduled*, so `await\n // captureToClipboardImage(el)` followed by a \"Copied!\" toast fired before\n // anything had been copied, and a throw inside the fallback path escaped as\n // an unhandled rejection.\n await new Promise<void>((resolve, reject) => {\n canvas.toBlob(async (blob) => {\n if (!blob) {\n resolve();\n return;\n }\n try {\n try {\n await navigator.clipboard.write([\n new ClipboardItem({ \"image/png\": blob }),\n ]);\n } catch {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = \"capture.png\";\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n resolve();\n } catch (error) {\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n }, \"image/png\");\n });\n}\n\n/**\n * DOM-Capture BlockPrinter factory\n *\n * Creates a BlockPrinter that captures a provided DOM element and exports it as PDF.\n * Used by visual blocks (Timeline, ComparisonTable, DecisionTree, Diagram) where\n * the rendered DOM is the most faithful representation.\n *\n * Usage inside a block component:\n * const { captureRef, printAction } = useBlockDomCapture('Timeline');\n * // attach captureRef to the block's root element\n * // add printAction to customActions or controls\n */\n\n/**\n * Create a BlockPrinter backed by DOM capture.\n * The printer's `print(data)` call is a no-op — actual printing is triggered\n * imperatively via `captureElement(el)`.\n *\n * This exists so the block can register itself in the BlockPrinter contract\n * while keeping the DOM reference local to the component.\n */\nexport function createDomCapturePrinter(blockLabel: string): BlockPrinter {\n return {\n label: `Print ${blockLabel}`,\n variants: [],\n // No-op: DOM capture blocks use captureElement() directly\n print: () => {},\n };\n}\n\n/**\n * Capture a DOM element as PDF.\n * Called directly by block components when their print button is clicked.\n *\n * @param element - The DOM element to capture\n * @param filename - Output filename (without extension)\n * @param orientation - \"landscape\" (default) for wide visual blocks; \"portrait\" for tall/list content\n */\nexport async function captureBlockElement(\n element: HTMLElement,\n filename: string,\n orientation: \"landscape\" | \"portrait\" = \"landscape\",\n): Promise<void> {\n await captureToPDF(element, {\n filename,\n scale: 2,\n orientation,\n });\n}\n\n/**\n * markdownToPdfBlob — render a markdown string to a styled, multi-page PDF\n * Blob without any visible UI.\n *\n * Pipeline: markdown → WordPress-styled HTML (the same converter the\n * publish/copy-HTML flows use) → sanitized offscreen DOM host → html2canvas +\n * jsPDF via `captureToPDFBlob`. Everything heavy stays lazy-loaded behind\n * this module.\n *\n * Used by the chat \"Save as → PDF Document\" action, which uploads the blob\n * into the user's Files. Reuse this for any \"content → PDF file\" need — do\n * not fork another markdown-to-pdf path.\n */\n\n/**\n * The two application-owned pieces markdownToPdfBlob needs. The original\n * reached for them with dynamic imports of app feature modules; a package\n * takes them from its caller instead.\n */\nexport interface MarkdownPdfDeps {\n /** markdown → HTML body string (the same converter the publish/copy-HTML flows use) */\n convertToHtml: (markdown: string) => string;\n /** the class-scoped stylesheet text the rendered HTML expects */\n loadCss: () => Promise<string> | string;\n}\n\n/**\n * Markdown can carry raw HTML; we are about to attach it to the live\n * document, so strip anything executable. html2canvas ignores scripts, but\n * inline handlers (`onerror`) and javascript: URLs would fire on attach.\n */\nexport function sanitizeForCapture(html: string): DocumentFragment {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n const fragment = template.content;\n\n fragment\n .querySelectorAll(\"script, iframe, object, embed, link, meta\")\n .forEach((el) => el.remove());\n fragment.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n if (name.startsWith(\"on\")) el.removeAttribute(attr.name);\n if (\n (name === \"href\" || name === \"src\") &&\n attr.value.trim().toLowerCase().startsWith(\"javascript:\")\n ) {\n el.removeAttribute(attr.name);\n }\n }\n });\n return fragment;\n}\n\nexport async function markdownToPdfBlob(\n markdown: string,\n deps: MarkdownPdfDeps,\n): Promise<Blob> {\n if (!markdown.trim()) {\n throw new Error(\"Nothing to convert — the content is empty.\");\n }\n\n const bodyHtml = deps.convertToHtml(markdown);\n const css = await deps.loadCss();\n\n // Offscreen host — must be in the document for html2canvas to compute\n // styles, but never visible and never intercepting input.\n const host = document.createElement(\"div\");\n host.style.cssText =\n \"position:fixed;left:-10000px;top:0;width:800px;background:#ffffff;color:#111827;pointer-events:none;\";\n host.setAttribute(\"aria-hidden\", \"true\");\n\n const style = document.createElement(\"style\");\n style.textContent = css;\n host.appendChild(style);\n\n const contentEl = document.createElement(\"div\");\n // The WP stylesheet is class-scoped (.matrx-*) and expects this container.\n contentEl.className = \"matrx-content-container\";\n contentEl.style.cssText = \"padding:32px;background:#ffffff;\";\n contentEl.appendChild(sanitizeForCapture(bodyHtml));\n host.appendChild(contentEl);\n\n document.body.appendChild(host);\n try {\n // Always-light document render: \"#fff\" (not \"#ffffff\") bypasses the\n // dark-mode background flip, and theme:\"light\" keeps the oklch-fallback\n // patch from substituting dark text colors while the app is in dark mode.\n return await captureToPDFBlob(host, { background: \"#fff\", theme: \"light\" });\n } finally {\n host.remove();\n }\n}\n"],"mappings":"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","names":[]}