@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/dist/react.d.cts CHANGED
@@ -109,9 +109,25 @@ interface BlockPrinter {
109
109
  * data — the block's structured data (serverData or parsed content)
110
110
  * variantId — which variant was chosen (undefined = default / no variants)
111
111
  * settings — resolved setting values chosen by the user
112
+ *
113
+ * MAY resolve to a PrintOutcome ("opened" | "downloaded") so the host can
114
+ * tell the user when the popup-blocked download fallback fired; `void`
115
+ * stays legal for printers that don't go through a print window.
112
116
  */
113
- print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | Promise<void>;
117
+ print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | PrintOutcome | Promise<void | PrintOutcome>;
114
118
  }
119
+ /**
120
+ * How a print request actually reached the user.
121
+ *
122
+ * "opened" — a print window/tab was opened and the document written into it.
123
+ * "downloaded" — the popup was blocked, so the document was downloaded as an
124
+ * .html file instead (the contract's fallback). Hosts SHOULD
125
+ * surface this to the user (e.g. a toast saying the print file
126
+ * was downloaded) — a blocked popup plus a silent download is
127
+ * indistinguishable from "nothing happened", which is exactly
128
+ * how QA filed it (2026-08-30, F5/F6/F8).
129
+ */
130
+ type PrintOutcome = "opened" | "downloaded";
115
131
 
116
132
  /**
117
133
  * @ai-matrx/print/labels — the label-sheet engine.
@@ -258,18 +274,25 @@ interface PrintOptionsDialogProps {
258
274
  data: unknown;
259
275
  open: boolean;
260
276
  onOpenChange: (open: boolean) => void;
277
+ /**
278
+ * Fired after printer.print() settles, with the PrintOutcome when the
279
+ * printer reports one. "downloaded" means the popup was blocked and the
280
+ * print document was saved as an .html download instead — hosts should
281
+ * toast that, or the fallback is indistinguishable from nothing happening.
282
+ */
283
+ onPrinted?: (outcome: void | PrintOutcome) => void;
261
284
  }
262
285
  /**
263
286
  * Shared pre-print options UI — shown when a BlockPrinter has variants or settings.
264
287
  * Desktop: Dialog. Mobile: Drawer (bottom sheet).
265
288
  */
266
- declare function PrintOptionsDialog({ printer, data, open, onOpenChange, }: PrintOptionsDialogProps): React.JSX.Element;
289
+ declare function PrintOptionsDialog({ printer, data, open, onOpenChange, onPrinted, }: PrintOptionsDialogProps): React.JSX.Element;
267
290
  /**
268
291
  * Manages open state for PrintOptionsDialog.
269
292
  * If the printer has variants or settings, shows the dialog.
270
293
  * If it has neither, calls printer.print() immediately with no settings.
271
294
  */
272
- declare function usePrintOptions(printer: BlockPrinter | null, data: unknown): {
295
+ declare function usePrintOptions(printer: BlockPrinter | null, data: unknown, onPrinted?: (outcome: void | PrintOutcome) => void): {
273
296
  open: boolean;
274
297
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
275
298
  triggerPrint: () => Promise<void>;
package/dist/react.d.ts CHANGED
@@ -109,9 +109,25 @@ interface BlockPrinter {
109
109
  * data — the block's structured data (serverData or parsed content)
110
110
  * variantId — which variant was chosen (undefined = default / no variants)
111
111
  * settings — resolved setting values chosen by the user
112
+ *
113
+ * MAY resolve to a PrintOutcome ("opened" | "downloaded") so the host can
114
+ * tell the user when the popup-blocked download fallback fired; `void`
115
+ * stays legal for printers that don't go through a print window.
112
116
  */
113
- print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | Promise<void>;
117
+ print: (data: unknown, variantId?: string, settings?: PrintSettings) => void | PrintOutcome | Promise<void | PrintOutcome>;
114
118
  }
119
+ /**
120
+ * How a print request actually reached the user.
121
+ *
122
+ * "opened" — a print window/tab was opened and the document written into it.
123
+ * "downloaded" — the popup was blocked, so the document was downloaded as an
124
+ * .html file instead (the contract's fallback). Hosts SHOULD
125
+ * surface this to the user (e.g. a toast saying the print file
126
+ * was downloaded) — a blocked popup plus a silent download is
127
+ * indistinguishable from "nothing happened", which is exactly
128
+ * how QA filed it (2026-08-30, F5/F6/F8).
129
+ */
130
+ type PrintOutcome = "opened" | "downloaded";
115
131
 
116
132
  /**
117
133
  * @ai-matrx/print/labels — the label-sheet engine.
@@ -258,18 +274,25 @@ interface PrintOptionsDialogProps {
258
274
  data: unknown;
259
275
  open: boolean;
260
276
  onOpenChange: (open: boolean) => void;
277
+ /**
278
+ * Fired after printer.print() settles, with the PrintOutcome when the
279
+ * printer reports one. "downloaded" means the popup was blocked and the
280
+ * print document was saved as an .html download instead — hosts should
281
+ * toast that, or the fallback is indistinguishable from nothing happening.
282
+ */
283
+ onPrinted?: (outcome: void | PrintOutcome) => void;
261
284
  }
262
285
  /**
263
286
  * Shared pre-print options UI — shown when a BlockPrinter has variants or settings.
264
287
  * Desktop: Dialog. Mobile: Drawer (bottom sheet).
265
288
  */
266
- declare function PrintOptionsDialog({ printer, data, open, onOpenChange, }: PrintOptionsDialogProps): React.JSX.Element;
289
+ declare function PrintOptionsDialog({ printer, data, open, onOpenChange, onPrinted, }: PrintOptionsDialogProps): React.JSX.Element;
267
290
  /**
268
291
  * Manages open state for PrintOptionsDialog.
269
292
  * If the printer has variants or settings, shows the dialog.
270
293
  * If it has neither, calls printer.print() immediately with no settings.
271
294
  */
272
- declare function usePrintOptions(printer: BlockPrinter | null, data: unknown): {
295
+ declare function usePrintOptions(printer: BlockPrinter | null, data: unknown, onPrinted?: (outcome: void | PrintOutcome) => void): {
273
296
  open: boolean;
274
297
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
275
298
  triggerPrint: () => Promise<void>;
package/dist/react.js CHANGED
@@ -11,8 +11,8 @@ import { twMerge } from "tailwind-merge";
11
11
  function escapeHtml(str) {
12
12
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13
13
  }
14
- function openPrintWindow(htmlDocument, filename = "print") {
15
- const win = window.open("", "_blank", "width=920,height=720,scrollbars=yes");
14
+ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
15
+ const win = window.open("", "_blank", windowFeatures);
16
16
  if (!win) {
17
17
  const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
18
18
  const url = URL.createObjectURL(blob);
@@ -22,12 +22,13 @@ function openPrintWindow(htmlDocument, filename = "print") {
22
22
  document.body.appendChild(a);
23
23
  a.click();
24
24
  document.body.removeChild(a);
25
- URL.revokeObjectURL(url);
26
- return;
25
+ setTimeout(() => URL.revokeObjectURL(url), 0);
26
+ return "downloaded";
27
27
  }
28
28
  win.document.open();
29
29
  win.document.write(htmlDocument);
30
30
  win.document.close();
31
+ return "opened";
31
32
  }
32
33
 
33
34
  // src/qr.ts
@@ -555,7 +556,7 @@ ${bodyHtml}
555
556
  ${withFitScript ? LP_FIT_SCRIPT : ""}
556
557
  </body>
557
558
  </html>`;
558
- openPrintWindow(html, title);
559
+ return openPrintWindow(html, title);
559
560
  }
560
561
  function expandLabels(labels, s) {
561
562
  const from = s.rangeFrom > 0 ? s.rangeFrom : 1;
@@ -593,7 +594,7 @@ function buildCalibrationBody(t) {
593
594
  return `<div class="lp-page">${banner}${marks}${cells.join("\n")}</div>`;
594
595
  }
595
596
  function printCalibrationSheet(template) {
596
- openSheetWindow(
597
+ return openSheetWindow(
597
598
  template,
598
599
  buildCalibrationBody(template),
599
600
  "Label calibration",
@@ -660,18 +661,16 @@ async function printQrLabelSheet(data, variantId, rawSettings) {
660
661
  const t = resolveTemplate(data, variantId);
661
662
  const s = resolveSettings(rawSettings);
662
663
  if (s.calibrationPage) {
663
- printCalibrationSheet(t);
664
- return;
664
+ return printCalibrationSheet(t);
665
665
  }
666
666
  const labels = expandLabels(data.labels ?? [], s);
667
667
  if (!labels.length) {
668
- openSheetWindow(
668
+ return openSheetWindow(
669
669
  t,
670
670
  `<div class="lp-page"><div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:11pt;">No labels to print.</div></div>`,
671
671
  "QR labels",
672
672
  false
673
673
  );
674
- return;
675
674
  }
676
675
  const hasBadge = labels.some((l) => (l.badge ?? "").trim() !== "");
677
676
  const layout = computeCellLayout(t, s.showCaption, hasBadge);
@@ -701,7 +700,7 @@ async function printQrLabelSheet(data, variantId, rawSettings) {
701
700
  const tail = cells.length % perPage;
702
701
  if (tail > 0)
703
702
  for (let i = 0; i < perPage - tail; i++) cells.push({ html: "" });
704
- openSheetWindow(
703
+ return openSheetWindow(
705
704
  t,
706
705
  renderPages(t, cells),
707
706
  "QR labels",
@@ -1101,7 +1100,8 @@ function visibleSettings(settings, variantId) {
1101
1100
  function PrintOptionsContent({
1102
1101
  printer,
1103
1102
  data,
1104
- onClose
1103
+ onClose,
1104
+ onPrinted
1105
1105
  }) {
1106
1106
  const [selectedVariant, setSelectedVariant] = useState(
1107
1107
  printer.variants[0]?.id ?? ""
@@ -1125,7 +1125,8 @@ function PrintOptionsContent({
1125
1125
  setIsPrinting(true);
1126
1126
  try {
1127
1127
  const settings = useDefaults ? buildDefaultSettings(allSettings) : settingValues;
1128
- await printer.print(data, variantId, settings);
1128
+ const outcome = await printer.print(data, variantId, settings);
1129
+ onPrinted?.(outcome);
1129
1130
  } finally {
1130
1131
  setIsPrinting(false);
1131
1132
  onClose();
@@ -1353,7 +1354,8 @@ function PrintOptionsDialog({
1353
1354
  printer,
1354
1355
  data,
1355
1356
  open,
1356
- onOpenChange
1357
+ onOpenChange,
1358
+ onPrinted
1357
1359
  }) {
1358
1360
  const isMobile = useIsMobile();
1359
1361
  const handleClose = () => onOpenChange(false);
@@ -1380,7 +1382,8 @@ function PrintOptionsDialog({
1380
1382
  {
1381
1383
  printer,
1382
1384
  data,
1383
- onClose: handleClose
1385
+ onClose: handleClose,
1386
+ onPrinted
1384
1387
  }
1385
1388
  ) })
1386
1389
  ] }) });
@@ -1398,18 +1401,20 @@ function PrintOptionsDialog({
1398
1401
  {
1399
1402
  printer,
1400
1403
  data,
1401
- onClose: handleClose
1404
+ onClose: handleClose,
1405
+ onPrinted
1402
1406
  }
1403
1407
  ) })
1404
1408
  ] }) });
1405
1409
  }
1406
- function usePrintOptions(printer, data) {
1410
+ function usePrintOptions(printer, data, onPrinted) {
1407
1411
  const [open, setOpen] = useState(false);
1408
1412
  const triggerPrint = async () => {
1409
1413
  if (!printer) return;
1410
1414
  const hasOptions = printer.variants.length > 0 || (printer.settings?.length ?? 0) > 0;
1411
1415
  if (!hasOptions) {
1412
- await printer.print(data);
1416
+ const outcome = await printer.print(data);
1417
+ onPrinted?.(outcome);
1413
1418
  } else {
1414
1419
  setOpen(true);
1415
1420
  }