@ai-matrx/print 0.2.0 → 0.2.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/CHANGELOG.md +31 -0
- package/dist/booklet.cjs +6 -5
- package/dist/booklet.cjs.map +1 -1
- package/dist/booklet.d.cts +4 -1
- package/dist/booklet.d.ts +4 -1
- package/dist/booklet.js +6 -5
- package/dist/booklet.js.map +1 -1
- package/dist/core.cjs +6 -5
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +25 -4
- package/dist/core.d.ts +25 -4
- package/dist/core.js +6 -5
- package/dist/core.js.map +1 -1
- package/dist/flashcards.cjs +18 -65
- package/dist/flashcards.cjs.map +1 -1
- package/dist/flashcards.js +18 -65
- package/dist/flashcards.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/labels.cjs +10 -11
- package/dist/labels.cjs.map +1 -1
- package/dist/labels.d.cts +3 -3
- package/dist/labels.d.ts +3 -3
- package/dist/labels.js +10 -11
- package/dist/labels.js.map +1 -1
- package/dist/react.cjs +23 -18
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +26 -3
- package/dist/react.d.ts +26 -3
- package/dist/react.js +23 -18
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
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):
|
|
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):
|
|
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",
|
|
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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\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":[]}
|
package/dist/flashcards.cjs
CHANGED
|
@@ -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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1656
|
+
return openPrintWindow(
|
|
1657
|
+
buildPrintDocument(bodyHtml, title, styles),
|
|
1658
|
+
"flashcards"
|
|
1659
|
+
);
|
|
1707
1660
|
}
|
|
1708
1661
|
};
|
|
1709
1662
|
//# sourceMappingURL=flashcards.cjs.map
|