@ai-matrx/print 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiIA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuErB,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAKO,SAAS,mBACd,UACA,QAAQ,eACR,cAAc,IACN;AACR,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,WAAW,KAAK,CAAC;AAAA;AAAA,EAE1B,YAAY;AAAA,EACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,QAAQ;AAAA;AAAA;AAAA;AAIV;AAMO,SAAS,gBACd,cACA,WAAW,SACL;AACN,QAAM,MAAM,OAAO,KAAK,IAAI,UAAU,qCAAqC;AAC3E,MAAI,CAAC,KAAK;AAER,UAAM,OAAO,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACzE,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW,GAAG,SAAS,QAAQ,QAAQ,GAAG,EAAE,YAAY,CAAC;AAC3D,aAAS,KAAK,YAAY,CAAC;AAC3B,MAAE,MAAM;AACR,aAAS,KAAK,YAAY,CAAC;AAC3B,QAAI,gBAAgB,GAAG;AACvB;AAAA,EACF;AACA,MAAI,SAAS,KAAK;AAClB,MAAI,SAAS,MAAM,YAAY;AAC/B,MAAI,SAAS,MAAM;AACrB;AAKO,SAAS,iBACd,UACA,QAAQ,eACR,cAAc,IACR;AACN,QAAM,MAAM,mBAAmB,UAAU,OAAO,WAAW;AAC3D,kBAAgB,KAAK,KAAK;AAC5B;AAYO,SAAS,mBAAmB,WAAsC;AACvE,QAAM,gBAAgB,KAAK,UAAU,CAAC,GAAG,SAAS,CAAC;AACnD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAiBa,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBnC;","names":[]}
1
+ {"version":3,"sources":["../src/core.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 * MAY resolve to a PrintOutcome (\"opened\" | \"downloaded\") so the host can\n * tell the user when the popup-blocked download fallback fired; `void`\n * stays legal for printers that don't go through a print window.\n */\n print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | PrintOutcome | Promise<void | PrintOutcome>;\n}\n\n// ─── Shared Utilities ─────────────────────────────────────────────────────────\n\nconst PRINT_STYLES = `\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n font-size: 11pt;\n line-height: 1.6;\n color: #1a1a1a;\n background: #fff;\n max-width: 860px;\n margin: 0 auto;\n padding: 28px 36px;\n }\n\n .print-actions {\n display: flex;\n gap: 10px;\n margin-bottom: 24px;\n padding-bottom: 18px;\n border-bottom: 2px solid #e5e7eb;\n }\n\n .print-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n border: none;\n font-weight: 600;\n }\n\n .print-btn-primary { background: #2563eb; color: #fff; }\n .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }\n\n h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }\n h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }\n h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }\n p { margin-bottom: 0.7em; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n font-size: 10pt;\n }\n th {\n background: #1e293b;\n color: #f8fafc;\n font-weight: 600;\n text-align: left;\n padding: 7px 12px;\n border: 1px solid #334155;\n }\n td {\n padding: 6px 12px;\n border: 1px solid #e2e8f0;\n vertical-align: top;\n }\n tr:nth-child(even) td { background: #f8fafc; }\n\n @media print {\n body { max-width: 100%; padding: 0; }\n .print-actions { display: none !important; }\n th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n }\n`;\n\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 * How a print request actually reached the user.\n *\n * \"opened\" — a print window/tab was opened and the document written into it.\n * \"downloaded\" — the popup was blocked, so the document was downloaded as an\n * .html file instead (the contract's fallback). Hosts SHOULD\n * surface this to the user (e.g. a toast saying the print file\n * was downloaded) — a blocked popup plus a silent download is\n * indistinguishable from \"nothing happened\", which is exactly\n * how QA filed it (2026-08-30, F5/F6/F8).\n */\nexport type PrintOutcome = \"opened\" | \"downloaded\";\n\n/**\n * Open a new browser window and write a complete HTML document into it.\n * Falls back to downloading an .html file if the popup is blocked.\n *\n * Returns which path was taken so callers can tell the user when the\n * fallback fired. `windowFeatures` customizes the popup geometry (the\n * flashcards fixed-geometry variants pass a wider window).\n */\nexport function openPrintWindow(\n htmlDocument: string,\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PrintOutcome {\n const win = window.open(\"\", \"_blank\", windowFeatures);\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 // Revoke on a macrotask so the browser has started the blob fetch —\n // a synchronous revoke races the download in some engines.\n setTimeout(() => URL.revokeObjectURL(url), 0);\n return \"downloaded\";\n }\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n * Returns the same PrintOutcome as openPrintWindow.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): PrintOutcome {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n return openPrintWindow(doc, title);\n}\n\n/**\n * Build the auto-shrink script that runs INSIDE a printed window: for every\n * element matching each selector, its font-size is reduced (0.5pt steps, 6pt\n * floor) until it fits its parent element's box. This is the flashcards\n * FIT_TEXT behavior verbatim, parameterized by selector so every lane\n * (flashcards card text, Avery text, label captions) shares the ONE loop.\n *\n * Pass the same selectors the original used to get byte-identical behavior:\n * flashcards passes `[\".card-text .card-text-inner\", \".avery-text-wrap .avery-text\"]`.\n */\nexport function buildFitTextScript(selectors: readonly string[]): string {\n const selectorsJson = JSON.stringify([...selectors]);\n return `\n<script>\n(function() {\n function fitText(el, container) {\n // container is the bounding box we must fit inside\n var maxW = container.clientWidth;\n var maxH = container.clientHeight;\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var minSize = 6;\n // Give the element a chance to reflow at current size before measuring\n el.style.fontSize = size + \"pt\";\n while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"pt\";\n }\n }\n function runAll() {\n var selectors = ${selectorsJson};\n for (var s = 0; s < selectors.length; s++) {\n var els = document.querySelectorAll(selectors[s]);\n for (var i = 0; i < els.length; i++) {\n fitText(els[i], els[i].parentElement);\n }\n }\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", runAll);\n } else {\n // document.write pages may already be loaded\n setTimeout(runAll, 0);\n }\n})();\n</script>`;\n}\n\nexport { escapeHtml };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqIA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuErB,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAKO,SAAS,mBACd,UACA,QAAQ,eACR,cAAc,IACN;AACR,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,WAAW,KAAK,CAAC;AAAA;AAAA,EAE1B,YAAY;AAAA,EACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,QAAQ;AAAA;AAAA;AAAA;AAIV;AAuBO,SAAS,gBACd,cACA,WAAW,SACX,iBAAiB,uCACH;AACd,QAAM,MAAM,OAAO,KAAK,IAAI,UAAU,cAAc;AACpD,MAAI,CAAC,KAAK;AAER,UAAM,OAAO,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACzE,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW,GAAG,SAAS,QAAQ,QAAQ,GAAG,EAAE,YAAY,CAAC;AAC3D,aAAS,KAAK,YAAY,CAAC;AAC3B,MAAE,MAAM;AACR,aAAS,KAAK,YAAY,CAAC;AAG3B,eAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,CAAC;AAC5C,WAAO;AAAA,EACT;AACA,MAAI,SAAS,KAAK;AAClB,MAAI,SAAS,MAAM,YAAY;AAC/B,MAAI,SAAS,MAAM;AACnB,SAAO;AACT;AAMO,SAAS,iBACd,UACA,QAAQ,eACR,cAAc,IACA;AACd,QAAM,MAAM,mBAAmB,UAAU,OAAO,WAAW;AAC3D,SAAO,gBAAgB,KAAK,KAAK;AACnC;AAYO,SAAS,mBAAmB,WAAsC;AACvE,QAAM,gBAAgB,KAAK,UAAU,CAAC,GAAG,SAAS,CAAC;AACnD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAiBa,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBnC;","names":[]}
package/dist/core.d.cts CHANGED
@@ -107,23 +107,44 @@ interface BlockPrinter {
107
107
  * data — the block's structured data (serverData or parsed content)
108
108
  * variantId — which variant was chosen (undefined = default / no variants)
109
109
  * settings — resolved setting values chosen by the user
110
+ *
111
+ * MAY resolve to a PrintOutcome ("opened" | "downloaded") so the host can
112
+ * tell the user when the popup-blocked download fallback fired; `void`
113
+ * stays legal for printers that don't go through a print window.
110
114
  */
111
- print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | Promise<void>;
115
+ print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | PrintOutcome | Promise<void | PrintOutcome>;
112
116
  }
113
117
  declare function escapeHtml(str: string): string;
114
118
  /**
115
119
  * Wrap body HTML in a complete, styled print document.
116
120
  */
117
121
  declare function buildPrintDocument(bodyHtml: string, title?: string, extraStyles?: string): string;
122
+ /**
123
+ * How a print request actually reached the user.
124
+ *
125
+ * "opened" — a print window/tab was opened and the document written into it.
126
+ * "downloaded" — the popup was blocked, so the document was downloaded as an
127
+ * .html file instead (the contract's fallback). Hosts SHOULD
128
+ * surface this to the user (e.g. a toast saying the print file
129
+ * was downloaded) — a blocked popup plus a silent download is
130
+ * indistinguishable from "nothing happened", which is exactly
131
+ * how QA filed it (2026-08-30, F5/F6/F8).
132
+ */
133
+ type PrintOutcome = "opened" | "downloaded";
118
134
  /**
119
135
  * Open a new browser window and write a complete HTML document into it.
120
136
  * Falls back to downloading an .html file if the popup is blocked.
137
+ *
138
+ * Returns which path was taken so callers can tell the user when the
139
+ * fallback fired. `windowFeatures` customizes the popup geometry (the
140
+ * flashcards fixed-geometry variants pass a wider window).
121
141
  */
122
- declare function openPrintWindow(htmlDocument: string, filename?: string): void;
142
+ declare function openPrintWindow(htmlDocument: string, filename?: string, windowFeatures?: string): PrintOutcome;
123
143
  /**
124
144
  * Shorthand: build the document and open it in one call.
145
+ * Returns the same PrintOutcome as openPrintWindow.
125
146
  */
126
- declare function printHtmlContent(bodyHtml: string, title?: string, extraStyles?: string): void;
147
+ declare function printHtmlContent(bodyHtml: string, title?: string, extraStyles?: string): PrintOutcome;
127
148
  /**
128
149
  * Build the auto-shrink script that runs INSIDE a printed window: for every
129
150
  * element matching each selector, its font-size is reduced (0.5pt steps, 6pt
@@ -136,4 +157,4 @@ declare function printHtmlContent(bodyHtml: string, title?: string, extraStyles?
136
157
  */
137
158
  declare function buildFitTextScript(selectors: readonly string[]): string;
138
159
 
139
- export { type BlockPrinter, type PrintSetting, type PrintSettings, type PrintVariant, buildFitTextScript, buildPrintDocument, escapeHtml, openPrintWindow, printHtmlContent };
160
+ export { type BlockPrinter, type PrintOutcome, type PrintSetting, type PrintSettings, type PrintVariant, buildFitTextScript, buildPrintDocument, escapeHtml, openPrintWindow, printHtmlContent };
package/dist/core.d.ts CHANGED
@@ -107,23 +107,44 @@ interface BlockPrinter {
107
107
  * data — the block's structured data (serverData or parsed content)
108
108
  * variantId — which variant was chosen (undefined = default / no variants)
109
109
  * settings — resolved setting values chosen by the user
110
+ *
111
+ * MAY resolve to a PrintOutcome ("opened" | "downloaded") so the host can
112
+ * tell the user when the popup-blocked download fallback fired; `void`
113
+ * stays legal for printers that don't go through a print window.
110
114
  */
111
- print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | Promise<void>;
115
+ print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | PrintOutcome | Promise<void | PrintOutcome>;
112
116
  }
113
117
  declare function escapeHtml(str: string): string;
114
118
  /**
115
119
  * Wrap body HTML in a complete, styled print document.
116
120
  */
117
121
  declare function buildPrintDocument(bodyHtml: string, title?: string, extraStyles?: string): string;
122
+ /**
123
+ * How a print request actually reached the user.
124
+ *
125
+ * "opened" — a print window/tab was opened and the document written into it.
126
+ * "downloaded" — the popup was blocked, so the document was downloaded as an
127
+ * .html file instead (the contract's fallback). Hosts SHOULD
128
+ * surface this to the user (e.g. a toast saying the print file
129
+ * was downloaded) — a blocked popup plus a silent download is
130
+ * indistinguishable from "nothing happened", which is exactly
131
+ * how QA filed it (2026-08-30, F5/F6/F8).
132
+ */
133
+ type PrintOutcome = "opened" | "downloaded";
118
134
  /**
119
135
  * Open a new browser window and write a complete HTML document into it.
120
136
  * Falls back to downloading an .html file if the popup is blocked.
137
+ *
138
+ * Returns which path was taken so callers can tell the user when the
139
+ * fallback fired. `windowFeatures` customizes the popup geometry (the
140
+ * flashcards fixed-geometry variants pass a wider window).
121
141
  */
122
- declare function openPrintWindow(htmlDocument: string, filename?: string): void;
142
+ declare function openPrintWindow(htmlDocument: string, filename?: string, windowFeatures?: string): PrintOutcome;
123
143
  /**
124
144
  * Shorthand: build the document and open it in one call.
145
+ * Returns the same PrintOutcome as openPrintWindow.
125
146
  */
126
- declare function printHtmlContent(bodyHtml: string, title?: string, extraStyles?: string): void;
147
+ declare function printHtmlContent(bodyHtml: string, title?: string, extraStyles?: string): PrintOutcome;
127
148
  /**
128
149
  * Build the auto-shrink script that runs INSIDE a printed window: for every
129
150
  * element matching each selector, its font-size is reduced (0.5pt steps, 6pt
@@ -136,4 +157,4 @@ declare function printHtmlContent(bodyHtml: string, title?: string, extraStyles?
136
157
  */
137
158
  declare function buildFitTextScript(selectors: readonly string[]): string;
138
159
 
139
- export { type BlockPrinter, type PrintSetting, type PrintSettings, type PrintVariant, buildFitTextScript, buildPrintDocument, escapeHtml, openPrintWindow, printHtmlContent };
160
+ export { type BlockPrinter, type PrintOutcome, type PrintSetting, type PrintSettings, type PrintVariant, buildFitTextScript, buildPrintDocument, escapeHtml, openPrintWindow, printHtmlContent };
package/dist/core.js CHANGED
@@ -95,8 +95,8 @@ ${bodyHtml}
95
95
  </body>
96
96
  </html>`;
97
97
  }
98
- function openPrintWindow(htmlDocument, filename = "print") {
99
- const win = window.open("", "_blank", "width=920,height=720,scrollbars=yes");
98
+ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
99
+ const win = window.open("", "_blank", windowFeatures);
100
100
  if (!win) {
101
101
  const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
102
102
  const url = URL.createObjectURL(blob);
@@ -106,16 +106,17 @@ function openPrintWindow(htmlDocument, filename = "print") {
106
106
  document.body.appendChild(a);
107
107
  a.click();
108
108
  document.body.removeChild(a);
109
- URL.revokeObjectURL(url);
110
- return;
109
+ setTimeout(() => URL.revokeObjectURL(url), 0);
110
+ return "downloaded";
111
111
  }
112
112
  win.document.open();
113
113
  win.document.write(htmlDocument);
114
114
  win.document.close();
115
+ return "opened";
115
116
  }
116
117
  function printHtmlContent(bodyHtml, title = "AI Response", extraStyles = "") {
117
118
  const doc = buildPrintDocument(bodyHtml, title, extraStyles);
118
- openPrintWindow(doc, title);
119
+ return openPrintWindow(doc, title);
119
120
  }
120
121
  function buildFitTextScript(selectors) {
121
122
  const selectorsJson = JSON.stringify([...selectors]);
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.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"],"mappings":";AAiIA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuErB,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAKO,SAAS,mBACd,UACA,QAAQ,eACR,cAAc,IACN;AACR,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,WAAW,KAAK,CAAC;AAAA;AAAA,EAE1B,YAAY;AAAA,EACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,QAAQ;AAAA;AAAA;AAAA;AAIV;AAMO,SAAS,gBACd,cACA,WAAW,SACL;AACN,QAAM,MAAM,OAAO,KAAK,IAAI,UAAU,qCAAqC;AAC3E,MAAI,CAAC,KAAK;AAER,UAAM,OAAO,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACzE,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW,GAAG,SAAS,QAAQ,QAAQ,GAAG,EAAE,YAAY,CAAC;AAC3D,aAAS,KAAK,YAAY,CAAC;AAC3B,MAAE,MAAM;AACR,aAAS,KAAK,YAAY,CAAC;AAC3B,QAAI,gBAAgB,GAAG;AACvB;AAAA,EACF;AACA,MAAI,SAAS,KAAK;AAClB,MAAI,SAAS,MAAM,YAAY;AAC/B,MAAI,SAAS,MAAM;AACrB;AAKO,SAAS,iBACd,UACA,QAAQ,eACR,cAAc,IACR;AACN,QAAM,MAAM,mBAAmB,UAAU,OAAO,WAAW;AAC3D,kBAAgB,KAAK,KAAK;AAC5B;AAYO,SAAS,mBAAmB,WAAsC;AACvE,QAAM,gBAAgB,KAAK,UAAU,CAAC,GAAG,SAAS,CAAC;AACnD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAiBa,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBnC;","names":[]}
1
+ {"version":3,"sources":["../src/core.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 * MAY resolve to a PrintOutcome (\"opened\" | \"downloaded\") so the host can\n * tell the user when the popup-blocked download fallback fired; `void`\n * stays legal for printers that don't go through a print window.\n */\n print: (\n data: unknown,\n variantId?: string,\n settings?: PrintSettings,\n ) => void | PrintOutcome | Promise<void | PrintOutcome>;\n}\n\n// ─── Shared Utilities ─────────────────────────────────────────────────────────\n\nconst PRINT_STYLES = `\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n font-size: 11pt;\n line-height: 1.6;\n color: #1a1a1a;\n background: #fff;\n max-width: 860px;\n margin: 0 auto;\n padding: 28px 36px;\n }\n\n .print-actions {\n display: flex;\n gap: 10px;\n margin-bottom: 24px;\n padding-bottom: 18px;\n border-bottom: 2px solid #e5e7eb;\n }\n\n .print-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n border: none;\n font-weight: 600;\n }\n\n .print-btn-primary { background: #2563eb; color: #fff; }\n .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }\n\n h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }\n h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }\n h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }\n p { margin-bottom: 0.7em; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n font-size: 10pt;\n }\n th {\n background: #1e293b;\n color: #f8fafc;\n font-weight: 600;\n text-align: left;\n padding: 7px 12px;\n border: 1px solid #334155;\n }\n td {\n padding: 6px 12px;\n border: 1px solid #e2e8f0;\n vertical-align: top;\n }\n tr:nth-child(even) td { background: #f8fafc; }\n\n @media print {\n body { max-width: 100%; padding: 0; }\n .print-actions { display: none !important; }\n th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }\n }\n`;\n\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 * How a print request actually reached the user.\n *\n * \"opened\" — a print window/tab was opened and the document written into it.\n * \"downloaded\" — the popup was blocked, so the document was downloaded as an\n * .html file instead (the contract's fallback). Hosts SHOULD\n * surface this to the user (e.g. a toast saying the print file\n * was downloaded) — a blocked popup plus a silent download is\n * indistinguishable from \"nothing happened\", which is exactly\n * how QA filed it (2026-08-30, F5/F6/F8).\n */\nexport type PrintOutcome = \"opened\" | \"downloaded\";\n\n/**\n * Open a new browser window and write a complete HTML document into it.\n * Falls back to downloading an .html file if the popup is blocked.\n *\n * Returns which path was taken so callers can tell the user when the\n * fallback fired. `windowFeatures` customizes the popup geometry (the\n * flashcards fixed-geometry variants pass a wider window).\n */\nexport function openPrintWindow(\n htmlDocument: string,\n filename = \"print\",\n windowFeatures = \"width=920,height=720,scrollbars=yes\",\n): PrintOutcome {\n const win = window.open(\"\", \"_blank\", windowFeatures);\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 // Revoke on a macrotask so the browser has started the blob fetch —\n // a synchronous revoke races the download in some engines.\n setTimeout(() => URL.revokeObjectURL(url), 0);\n return \"downloaded\";\n }\n win.document.open();\n win.document.write(htmlDocument);\n win.document.close();\n return \"opened\";\n}\n\n/**\n * Shorthand: build the document and open it in one call.\n * Returns the same PrintOutcome as openPrintWindow.\n */\nexport function printHtmlContent(\n bodyHtml: string,\n title = \"AI Response\",\n extraStyles = \"\",\n): PrintOutcome {\n const doc = buildPrintDocument(bodyHtml, title, extraStyles);\n return openPrintWindow(doc, title);\n}\n\n/**\n * Build the auto-shrink script that runs INSIDE a printed window: for every\n * element matching each selector, its font-size is reduced (0.5pt steps, 6pt\n * floor) until it fits its parent element's box. This is the flashcards\n * FIT_TEXT behavior verbatim, parameterized by selector so every lane\n * (flashcards card text, Avery text, label captions) shares the ONE loop.\n *\n * Pass the same selectors the original used to get byte-identical behavior:\n * flashcards passes `[\".card-text .card-text-inner\", \".avery-text-wrap .avery-text\"]`.\n */\nexport function buildFitTextScript(selectors: readonly string[]): string {\n const selectorsJson = JSON.stringify([...selectors]);\n return `\n<script>\n(function() {\n function fitText(el, container) {\n // container is the bounding box we must fit inside\n var maxW = container.clientWidth;\n var maxH = container.clientHeight;\n var size = parseFloat(window.getComputedStyle(el).fontSize);\n var minSize = 6;\n // Give the element a chance to reflow at current size before measuring\n el.style.fontSize = size + \"pt\";\n while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {\n size -= 0.5;\n el.style.fontSize = size + \"pt\";\n }\n }\n function runAll() {\n var selectors = ${selectorsJson};\n for (var s = 0; s < selectors.length; s++) {\n var els = document.querySelectorAll(selectors[s]);\n for (var i = 0; i < els.length; i++) {\n fitText(els[i], els[i].parentElement);\n }\n }\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", runAll);\n } else {\n // document.write pages may already be loaded\n setTimeout(runAll, 0);\n }\n})();\n</script>`;\n}\n\nexport { escapeHtml };\n"],"mappings":";AAqIA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuErB,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAKO,SAAS,mBACd,UACA,QAAQ,eACR,cAAc,IACN;AACR,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,WAAW,KAAK,CAAC;AAAA;AAAA,EAE1B,YAAY;AAAA,EACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,QAAQ;AAAA;AAAA;AAAA;AAIV;AAuBO,SAAS,gBACd,cACA,WAAW,SACX,iBAAiB,uCACH;AACd,QAAM,MAAM,OAAO,KAAK,IAAI,UAAU,cAAc;AACpD,MAAI,CAAC,KAAK;AAER,UAAM,OAAO,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACzE,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW,GAAG,SAAS,QAAQ,QAAQ,GAAG,EAAE,YAAY,CAAC;AAC3D,aAAS,KAAK,YAAY,CAAC;AAC3B,MAAE,MAAM;AACR,aAAS,KAAK,YAAY,CAAC;AAG3B,eAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,CAAC;AAC5C,WAAO;AAAA,EACT;AACA,MAAI,SAAS,KAAK;AAClB,MAAI,SAAS,MAAM,YAAY;AAC/B,MAAI,SAAS,MAAM;AACnB,SAAO;AACT;AAMO,SAAS,iBACd,UACA,QAAQ,eACR,cAAc,IACA;AACd,QAAM,MAAM,mBAAmB,UAAU,OAAO,WAAW;AAC3D,SAAO,gBAAgB,KAAK,KAAK;AACnC;AAYO,SAAS,mBAAmB,WAAsC;AACvE,QAAM,gBAAgB,KAAK,UAAU,CAAC,GAAG,SAAS,CAAC;AACnD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAiBa,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBnC;","names":[]}
@@ -121,8 +121,8 @@ ${bodyHtml}
121
121
  </body>
122
122
  </html>`;
123
123
  }
124
- function openPrintWindow(htmlDocument, filename = "print") {
125
- const win = window.open("", "_blank", "width=920,height=720,scrollbars=yes");
124
+ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
125
+ const win = window.open("", "_blank", windowFeatures);
126
126
  if (!win) {
127
127
  const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
128
128
  const url = URL.createObjectURL(blob);
@@ -132,12 +132,13 @@ function openPrintWindow(htmlDocument, filename = "print") {
132
132
  document.body.appendChild(a);
133
133
  a.click();
134
134
  document.body.removeChild(a);
135
- URL.revokeObjectURL(url);
136
- return;
135
+ setTimeout(() => URL.revokeObjectURL(url), 0);
136
+ return "downloaded";
137
137
  }
138
138
  win.document.open();
139
139
  win.document.write(htmlDocument);
140
140
  win.document.close();
141
+ return "opened";
141
142
  }
142
143
  function buildFitTextScript(selectors) {
143
144
  const selectorsJson = JSON.stringify([...selectors]);
@@ -973,22 +974,7 @@ ${bodyHtml}
973
974
  ${FIT_TEXT_SCRIPT}
974
975
  </body>
975
976
  </html>`;
976
- const win = window.open("", "_blank", "width=1100,height=720,scrollbars=yes");
977
- if (!win) {
978
- const blob = new Blob([html], { type: "text/html;charset=utf-8" });
979
- const url = URL.createObjectURL(blob);
980
- const a = document.createElement("a");
981
- a.href = url;
982
- a.download = `${title.replace(/\s+/g, "-").toLowerCase()}-flashcards.html`;
983
- document.body.appendChild(a);
984
- a.click();
985
- document.body.removeChild(a);
986
- URL.revokeObjectURL(url);
987
- return;
988
- }
989
- win.document.open();
990
- win.document.write(html);
991
- win.document.close();
977
+ return openPrintWindow(html, `${title}-flashcards`, "width=1100,height=720,scrollbars=yes");
992
978
  }
993
979
  var AVERY_5388_STYLES = `
994
980
  @page { size: portrait; margin: 0; }
@@ -1170,22 +1156,7 @@ ${bodyHtml}
1170
1156
  ${FIT_TEXT_SCRIPT}
1171
1157
  </body>
1172
1158
  </html>`;
1173
- const win = window.open("", "_blank", "width=900,height=720,scrollbars=yes");
1174
- if (!win) {
1175
- const blob = new Blob([html], { type: "text/html;charset=utf-8" });
1176
- const url = URL.createObjectURL(blob);
1177
- const a = document.createElement("a");
1178
- a.href = url;
1179
- a.download = `${title.replace(/\s+/g, "-").toLowerCase()}-avery5388.html`;
1180
- document.body.appendChild(a);
1181
- a.click();
1182
- document.body.removeChild(a);
1183
- URL.revokeObjectURL(url);
1184
- return;
1185
- }
1186
- win.document.open();
1187
- win.document.write(html);
1188
- win.document.close();
1159
+ return openPrintWindow(html, `${title}-avery5388`, "width=900,height=720,scrollbars=yes");
1189
1160
  }
1190
1161
  var PORTRAIT_6UP_STYLES = `
1191
1162
  @page { size: portrait; margin: 0; }
@@ -1435,22 +1406,7 @@ ${bodyHtml}
1435
1406
  ${FIT_TEXT_SCRIPT}
1436
1407
  </body>
1437
1408
  </html>`;
1438
- const win = window.open("", "_blank", "width=900,height=720,scrollbars=yes");
1439
- if (!win) {
1440
- const blob = new Blob([html], { type: "text/html;charset=utf-8" });
1441
- const url = URL.createObjectURL(blob);
1442
- const a = document.createElement("a");
1443
- a.href = url;
1444
- a.download = `${title.replace(/\s+/g, "-").toLowerCase()}-flashcards-6up.html`;
1445
- document.body.appendChild(a);
1446
- a.click();
1447
- document.body.removeChild(a);
1448
- URL.revokeObjectURL(url);
1449
- return;
1450
- }
1451
- win.document.open();
1452
- win.document.write(html);
1453
- win.document.close();
1409
+ return openPrintWindow(html, `${title}-flashcards-6up`, "width=900,height=720,scrollbars=yes");
1454
1410
  }
1455
1411
  function deriveTitle(data) {
1456
1412
  if (data && typeof data === "object" && "title" in data) {
@@ -1629,7 +1585,7 @@ var flashcardsPrinter = {
1629
1585
  const title = deriveTitle(data);
1630
1586
  const s = resolveSettings(rawSettings);
1631
1587
  if (allCards.length === 0) {
1632
- openPrintWindow(
1588
+ return openPrintWindow(
1633
1589
  buildPrintDocument(
1634
1590
  "<p>No flashcard data available to print.</p>",
1635
1591
  title,
@@ -1637,7 +1593,6 @@ var flashcardsPrinter = {
1637
1593
  ),
1638
1594
  "flashcards"
1639
1595
  );
1640
- return;
1641
1596
  }
1642
1597
  const rawFrom = typeof rawSettings?.fromCard === "number" ? rawSettings.fromCard : 0;
1643
1598
  const rawTo = typeof rawSettings?.toCard === "number" ? rawSettings.toCard : 0;
@@ -1647,36 +1602,31 @@ var flashcardsPrinter = {
1647
1602
  const startIndex = fromIdx - 1;
1648
1603
  const cards = allCards.slice(startIndex, toIdx);
1649
1604
  if (variantId === "landscape-duplex") {
1650
- openLandscapeWindow(
1605
+ return openLandscapeWindow(
1651
1606
  renderLandscape(cards, title, "duplex", s, startIndex),
1652
1607
  title
1653
1608
  );
1654
- return;
1655
1609
  }
1656
1610
  if (variantId === "landscape-stacked") {
1657
- openLandscapeWindow(
1611
+ return openLandscapeWindow(
1658
1612
  renderLandscape(cards, title, "stacked", s, startIndex),
1659
1613
  title
1660
1614
  );
1661
- return;
1662
1615
  }
1663
1616
  if (variantId === "portrait-6up-duplex") {
1664
- openPortrait6UpWindow(
1617
+ return openPortrait6UpWindow(
1665
1618
  renderPortrait6Up(cards, title, "duplex", s, startIndex),
1666
1619
  title
1667
1620
  );
1668
- return;
1669
1621
  }
1670
1622
  if (variantId === "portrait-6up-stacked") {
1671
- openPortrait6UpWindow(
1623
+ return openPortrait6UpWindow(
1672
1624
  renderPortrait6Up(cards, title, "stacked", s, startIndex),
1673
1625
  title
1674
1626
  );
1675
- return;
1676
1627
  }
1677
1628
  if (variantId === "avery-5388") {
1678
- openAveryWindow(renderAvery5388(cards, title, s, startIndex), title);
1679
- return;
1629
+ return openAveryWindow(renderAvery5388(cards, title, s, startIndex), title);
1680
1630
  }
1681
1631
  let bodyHtml;
1682
1632
  let styles;
@@ -1703,7 +1653,10 @@ var flashcardsPrinter = {
1703
1653
  styles = BOTH_SIDES_STYLES;
1704
1654
  break;
1705
1655
  }
1706
- openPrintWindow(buildPrintDocument(bodyHtml, title, styles), "flashcards");
1656
+ return openPrintWindow(
1657
+ buildPrintDocument(bodyHtml, title, styles),
1658
+ "flashcards"
1659
+ );
1707
1660
  }
1708
1661
  };
1709
1662
  //# sourceMappingURL=flashcards.cjs.map