@signiphi/pdf-compose 0.1.0-beta.1 → 0.1.0-beta.11

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.
Files changed (106) hide show
  1. package/README.md +265 -0
  2. package/dist/components/DocumentGenerator.d.ts +3 -0
  3. package/dist/components/DocumentGenerator.d.ts.map +1 -0
  4. package/dist/components/EditorPanel.d.ts +15 -0
  5. package/dist/components/EditorPanel.d.ts.map +1 -0
  6. package/dist/components/EditorToolbar.d.ts +11 -0
  7. package/dist/components/EditorToolbar.d.ts.map +1 -0
  8. package/dist/components/ErrorBoundary.d.ts +18 -0
  9. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  10. package/dist/components/FieldEditPopover.d.ts +12 -0
  11. package/dist/components/FieldEditPopover.d.ts.map +1 -0
  12. package/dist/components/FieldInsertPopover.d.ts +12 -0
  13. package/dist/components/FieldInsertPopover.d.ts.map +1 -0
  14. package/dist/components/FieldNodeView.d.ts +3 -0
  15. package/dist/components/FieldNodeView.d.ts.map +1 -0
  16. package/dist/components/FieldOverlay.d.ts +9 -0
  17. package/dist/components/FieldOverlay.d.ts.map +1 -0
  18. package/dist/components/GeneratePanel.d.ts +15 -0
  19. package/dist/components/GeneratePanel.d.ts.map +1 -0
  20. package/dist/components/ImageNodeView.d.ts +11 -0
  21. package/dist/components/ImageNodeView.d.ts.map +1 -0
  22. package/dist/components/PreviewPanel.d.ts +12 -0
  23. package/dist/components/PreviewPanel.d.ts.map +1 -0
  24. package/dist/components/VariableEditPopover.d.ts +12 -0
  25. package/dist/components/VariableEditPopover.d.ts.map +1 -0
  26. package/dist/components/VariableInsertPopover.d.ts +13 -0
  27. package/dist/components/VariableInsertPopover.d.ts.map +1 -0
  28. package/dist/components/VariableNodeView.d.ts +3 -0
  29. package/dist/components/VariableNodeView.d.ts.map +1 -0
  30. package/dist/components/WatermarkNodeView.d.ts +3 -0
  31. package/dist/components/WatermarkNodeView.d.ts.map +1 -0
  32. package/dist/contexts/ThemeContext.d.ts +19 -0
  33. package/dist/contexts/ThemeContext.d.ts.map +1 -0
  34. package/dist/extensions/columns-node.d.ts +4 -0
  35. package/dist/extensions/columns-node.d.ts.map +1 -0
  36. package/dist/extensions/field-node.d.ts +31 -0
  37. package/dist/extensions/field-node.d.ts.map +1 -0
  38. package/dist/extensions/image-node.d.ts +10 -0
  39. package/dist/extensions/image-node.d.ts.map +1 -0
  40. package/dist/extensions/panel-node.d.ts +3 -0
  41. package/dist/extensions/panel-node.d.ts.map +1 -0
  42. package/dist/extensions/repeat-node.d.ts +3 -0
  43. package/dist/extensions/repeat-node.d.ts.map +1 -0
  44. package/dist/extensions/subtotals-node.d.ts +3 -0
  45. package/dist/extensions/subtotals-node.d.ts.map +1 -0
  46. package/dist/extensions/variable-node.d.ts +18 -0
  47. package/dist/extensions/variable-node.d.ts.map +1 -0
  48. package/dist/extensions/watermark-node.d.ts +3 -0
  49. package/dist/extensions/watermark-node.d.ts.map +1 -0
  50. package/dist/hooks/useDocumentGenerator.d.ts +46 -0
  51. package/dist/hooks/useDocumentGenerator.d.ts.map +1 -0
  52. package/dist/index.d.ts +20 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +3108 -509
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +3094 -512
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/lib/ui/button.d.ts +12 -0
  59. package/dist/lib/ui/button.d.ts.map +1 -0
  60. package/dist/lib/ui/index.d.ts +10 -0
  61. package/dist/lib/ui/index.d.ts.map +1 -0
  62. package/dist/lib/ui/input.d.ts +6 -0
  63. package/dist/lib/ui/input.d.ts.map +1 -0
  64. package/dist/lib/ui/label.d.ts +8 -0
  65. package/dist/lib/ui/label.d.ts.map +1 -0
  66. package/dist/lib/ui/popover.d.ts +8 -0
  67. package/dist/lib/ui/popover.d.ts.map +1 -0
  68. package/dist/lib/ui/select.d.ts +14 -0
  69. package/dist/lib/ui/select.d.ts.map +1 -0
  70. package/dist/lib/ui/toggle-group.d.ts +13 -0
  71. package/dist/lib/ui/toggle-group.d.ts.map +1 -0
  72. package/dist/lib/ui/tooltip.d.ts +8 -0
  73. package/dist/lib/ui/tooltip.d.ts.map +1 -0
  74. package/dist/lib/utils.d.ts +3 -0
  75. package/dist/lib/utils.d.ts.map +1 -0
  76. package/dist/styles/index.css +374 -194
  77. package/dist/styles/index.d.ts +1 -0
  78. package/dist/types/index.d.ts +204 -0
  79. package/dist/types/index.d.ts.map +1 -0
  80. package/dist/utils/error-helpers.d.ts +27 -0
  81. package/dist/utils/error-helpers.d.ts.map +1 -0
  82. package/dist/utils/field-helpers.d.ts +31 -0
  83. package/dist/utils/field-helpers.d.ts.map +1 -0
  84. package/dist/utils/markdown-parser.d.ts +6 -0
  85. package/dist/utils/markdown-parser.d.ts.map +1 -0
  86. package/dist/utils/markdown-validator.d.ts +13 -0
  87. package/dist/utils/markdown-validator.d.ts.map +1 -0
  88. package/dist/utils/markdown-writer.d.ts +6 -0
  89. package/dist/utils/markdown-writer.d.ts.map +1 -0
  90. package/dist/utils/pdf-generator.d.ts +77 -0
  91. package/dist/utils/pdf-generator.d.ts.map +1 -0
  92. package/dist/utils/pdf-metadata.d.ts +58 -0
  93. package/dist/utils/pdf-metadata.d.ts.map +1 -0
  94. package/dist/utils/pdf-preview.d.ts +7 -0
  95. package/dist/utils/pdf-preview.d.ts.map +1 -0
  96. package/dist/utils/template-pipeline.d.ts +39 -0
  97. package/dist/utils/template-pipeline.d.ts.map +1 -0
  98. package/dist/utils/theme-helpers.d.ts +21 -0
  99. package/dist/utils/theme-helpers.d.ts.map +1 -0
  100. package/dist/utils/variable-helpers.d.ts +74 -0
  101. package/dist/utils/variable-helpers.d.ts.map +1 -0
  102. package/dist/utils/xml-template-parser.d.ts +9 -0
  103. package/dist/utils/xml-template-parser.d.ts.map +1 -0
  104. package/package.json +16 -6
  105. package/src/styles/index.css +140 -68
  106. package/src/styles/index.d.ts +1 -0
package/dist/index.mjs CHANGED
@@ -1,17 +1,21 @@
1
1
  import * as React12 from 'react';
2
2
  import React12__default, { createContext, useCallback, useState, useRef, useEffect, useMemo, useContext } from 'react';
3
- import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, Maximize2, Loader2, Plus, PanelLeftOpen, CheckCircle2, FileDown, PanelLeftClose, Undo, Redo, Bold, Italic, Underline as Underline$1, Code, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Minus, AlignLeft, AlignCenter, AlignRight, Trash2, X, PenLine, Upload, WrapText, Download } from 'lucide-react';
3
+ import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, Droplets, Image, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, Maximize2, Loader2, Plus, Save, FileDown, CheckCircle2, PanelLeftOpen, PanelLeftClose, Undo, Redo, Bold, Italic, Underline as Underline$1, Code, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Minus, AlignLeft, AlignCenter, AlignRight, Trash2, X, PenLine, Upload, Search, WrapText, Download } from 'lucide-react';
4
4
  import { ReactNodeViewRenderer, NodeViewWrapper, useEditor, EditorContent } from '@tiptap/react';
5
5
  import StarterKit from '@tiptap/starter-kit';
6
6
  import Underline from '@tiptap/extension-underline';
7
7
  import TextAlign from '@tiptap/extension-text-align';
8
8
  import Placeholder from '@tiptap/extension-placeholder';
9
+ import Table from '@tiptap/extension-table';
10
+ import TableRow from '@tiptap/extension-table-row';
11
+ import TableCell from '@tiptap/extension-table-cell';
12
+ import TableHeader from '@tiptap/extension-table-header';
9
13
  import { Node, mergeAttributes } from '@tiptap/core';
10
14
  import { clsx } from 'clsx';
11
15
  import { twMerge } from 'tailwind-merge';
12
16
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
17
  import { v4 } from 'uuid';
14
- import { PDFDocument, StandardFonts, rgb, PDFName as PDFName$1, PDFString as PDFString$1 } from 'pdf-lib';
18
+ import { PDFDocument, StandardFonts, rgb, degrees, PDFName as PDFName$1, PDFString as PDFString$1 } from 'pdf-lib';
15
19
  import { NodeSelection } from '@tiptap/pm/state';
16
20
  import { Slot } from '@radix-ui/react-slot';
17
21
  import { cva } from 'class-variance-authority';
@@ -21,7 +25,7 @@ import * as LabelPrimitive from '@radix-ui/react-label';
21
25
 
22
26
  var __defProp = Object.defineProperty;
23
27
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
24
- var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
28
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
25
29
  function cn(...inputs) {
26
30
  return twMerge(clsx(inputs));
27
31
  }
@@ -213,7 +217,10 @@ var VariableNode = Node.create({
213
217
  return {
214
218
  varName: { default: "" },
215
219
  varLabel: { default: "" },
216
- varDefault: { default: "" }
220
+ varDefault: { default: "" },
221
+ format: { default: "" },
222
+ suppressZero: { default: "" },
223
+ block: { default: "" }
217
224
  };
218
225
  },
219
226
  parseHTML() {
@@ -279,6 +286,235 @@ var VariableNode = Node.create({
279
286
  };
280
287
  }
281
288
  });
289
+ var PanelNode = Node.create({
290
+ name: "panel",
291
+ group: "block",
292
+ content: "block+",
293
+ defining: true,
294
+ addAttributes() {
295
+ return {
296
+ title: { default: "" },
297
+ border: { default: "solid" },
298
+ headerStyle: { default: "" }
299
+ };
300
+ },
301
+ parseHTML() {
302
+ return [{
303
+ tag: "div[data-panel-node]",
304
+ contentElement: "div.panel-content"
305
+ }];
306
+ },
307
+ renderHTML({ node, HTMLAttributes }) {
308
+ const { title, border, headerStyle } = node.attrs;
309
+ const classes = ["panel-node"];
310
+ if (border === "dashed") classes.push("panel-border-dashed");
311
+ else if (border === "none") classes.push("panel-border-none");
312
+ else classes.push("panel-border-solid");
313
+ if (headerStyle === "dark") classes.push("panel-dark");
314
+ const attrs = mergeAttributes(
315
+ { "data-panel-node": "", class: classes.join(" ") },
316
+ HTMLAttributes
317
+ );
318
+ if (title) {
319
+ return [
320
+ "div",
321
+ attrs,
322
+ ["div", { class: "panel-title" }, title],
323
+ ["div", { class: "panel-content" }, 0]
324
+ ];
325
+ }
326
+ return ["div", attrs, ["div", { class: "panel-content" }, 0]];
327
+ }
328
+ });
329
+ var ColumnsNode = Node.create({
330
+ name: "columns",
331
+ group: "block",
332
+ content: "column+",
333
+ defining: true,
334
+ addAttributes() {
335
+ return {
336
+ split: { default: "50" },
337
+ padX: { default: "0" }
338
+ };
339
+ },
340
+ parseHTML() {
341
+ return [{ tag: "div[data-columns-node]" }];
342
+ },
343
+ renderHTML({ node, HTMLAttributes }) {
344
+ const split = node.attrs.split || "50";
345
+ return ["div", mergeAttributes(
346
+ {
347
+ "data-columns-node": "",
348
+ class: "columns-node",
349
+ style: `--xpc-col-split: ${split}`
350
+ },
351
+ HTMLAttributes
352
+ ), 0];
353
+ }
354
+ });
355
+ var ColumnNode = Node.create({
356
+ name: "column",
357
+ group: "",
358
+ content: "block+",
359
+ defining: true,
360
+ addAttributes() {
361
+ return {
362
+ padTop: { default: 0 }
363
+ };
364
+ },
365
+ parseHTML() {
366
+ return [{ tag: "div[data-column-node]" }];
367
+ },
368
+ renderHTML({ HTMLAttributes }) {
369
+ return ["div", mergeAttributes(
370
+ { "data-column-node": "", class: "column-node" },
371
+ HTMLAttributes
372
+ ), 0];
373
+ }
374
+ });
375
+ function WatermarkNodeView({ node, selected }) {
376
+ const { text } = node.attrs;
377
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsxs(
378
+ "div",
379
+ {
380
+ "data-watermark-node": "",
381
+ className: cn(
382
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
383
+ "bg-blue-50 text-blue-600 border-blue-200",
384
+ selected && "ring-2 ring-primary ring-offset-1"
385
+ ),
386
+ children: [
387
+ /* @__PURE__ */ jsx(Droplets, { size: 14 }),
388
+ /* @__PURE__ */ jsxs("span", { children: [
389
+ "Watermark: ",
390
+ text || "(empty)"
391
+ ] })
392
+ ]
393
+ }
394
+ ) });
395
+ }
396
+
397
+ // src/extensions/watermark-node.ts
398
+ var WatermarkNode = Node.create({
399
+ name: "watermark",
400
+ group: "block",
401
+ atom: true,
402
+ selectable: true,
403
+ draggable: false,
404
+ addAttributes() {
405
+ return {
406
+ text: { default: "" },
407
+ opacity: { default: "0.15" },
408
+ angle: { default: "-45" }
409
+ };
410
+ },
411
+ parseHTML() {
412
+ return [{ tag: "div[data-watermark-node]" }];
413
+ },
414
+ renderHTML({ HTMLAttributes }) {
415
+ return ["div", mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
416
+ },
417
+ addNodeView() {
418
+ return ReactNodeViewRenderer(WatermarkNodeView);
419
+ }
420
+ });
421
+ function ImageNodeView({ node, selected }) {
422
+ const { src, varName, alt, width, height, align } = node.attrs;
423
+ const hasRenderableSrc = !!src && /^(https?:|data:)/i.test(src);
424
+ const wrapperJustify = align === "center" ? "justify-center" : align === "right" ? "justify-end" : "justify-start";
425
+ const toCssSize = (v) => /^\d+(\.\d+)?$/.test(v) ? `${v}px` : v;
426
+ const sizeStyle = {};
427
+ if (width) sizeStyle.width = toCssSize(width);
428
+ if (height) sizeStyle.height = toCssSize(height);
429
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx("div", { className: cn("flex my-1", wrapperJustify), "data-image-node": "", children: hasRenderableSrc ? /* @__PURE__ */ jsx(
430
+ "img",
431
+ {
432
+ src,
433
+ alt: alt || varName || "",
434
+ style: sizeStyle,
435
+ className: cn(
436
+ "max-w-full rounded-sm object-contain",
437
+ selected && "ring-2 ring-primary ring-offset-1"
438
+ ),
439
+ draggable: false
440
+ }
441
+ ) : /* @__PURE__ */ jsxs(
442
+ "div",
443
+ {
444
+ "data-image-placeholder": "",
445
+ className: cn(
446
+ "inline-flex items-center gap-2 px-3 py-2 rounded-md border border-dashed text-sm",
447
+ "bg-slate-50 text-slate-500 border-slate-300",
448
+ selected && "ring-2 ring-primary ring-offset-1"
449
+ ),
450
+ style: height ? { minHeight: toCssSize(height) } : void 0,
451
+ title: varName ? `Image variable: ${varName}` : "Image",
452
+ children: [
453
+ /* @__PURE__ */ jsx(Image, { size: 16 }),
454
+ /* @__PURE__ */ jsx("span", { children: varName ? `Image: ${varName}` : alt || "Image" })
455
+ ]
456
+ }
457
+ ) }) });
458
+ }
459
+
460
+ // src/extensions/image-node.ts
461
+ var ImageNode = Node.create({
462
+ name: "imageNode",
463
+ group: "block",
464
+ atom: true,
465
+ selectable: true,
466
+ draggable: false,
467
+ addAttributes() {
468
+ return {
469
+ src: { default: "" },
470
+ varName: { default: "" },
471
+ alt: { default: "" },
472
+ width: { default: "" },
473
+ height: { default: "" },
474
+ align: { default: "" }
475
+ };
476
+ },
477
+ parseHTML() {
478
+ return [{ tag: "div[data-image-node]" }];
479
+ },
480
+ renderHTML({ HTMLAttributes }) {
481
+ return ["div", mergeAttributes({ "data-image-node": "" }, HTMLAttributes)];
482
+ },
483
+ addNodeView() {
484
+ return ReactNodeViewRenderer(ImageNodeView);
485
+ }
486
+ });
487
+ var RepeatNode = Node.create({
488
+ name: "repeatBlock",
489
+ group: "block",
490
+ content: "block+",
491
+ addAttributes() {
492
+ return {
493
+ data: { default: "" }
494
+ };
495
+ },
496
+ parseHTML() {
497
+ return [{ tag: "div[data-repeat-node]" }];
498
+ },
499
+ renderHTML({ HTMLAttributes }) {
500
+ return ["div", mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
501
+ }
502
+ });
503
+ var SubtotalsNode = Node.create({
504
+ name: "subtotalsBlock",
505
+ group: "block",
506
+ content: "block+",
507
+ defining: true,
508
+ parseHTML() {
509
+ return [{ tag: "div[data-subtotals-node]" }];
510
+ },
511
+ renderHTML({ HTMLAttributes }) {
512
+ return ["div", mergeAttributes(
513
+ { "data-subtotals-node": "", class: "subtotals-block" },
514
+ HTMLAttributes
515
+ ), 0];
516
+ }
517
+ });
282
518
 
283
519
  // src/types/index.ts
284
520
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
@@ -357,158 +593,23 @@ function extractFieldsFromContent(content) {
357
593
  }
358
594
  return fields;
359
595
  }
360
-
361
- // src/utils/variable-helpers.ts
362
- function extractVariablesFromContent(content) {
363
- const seen = /* @__PURE__ */ new Map();
364
- function walk(node) {
365
- if (node.type === "variableNode" && node.attrs) {
366
- const varName = node.attrs.varName;
367
- if (varName && !seen.has(varName)) {
368
- seen.set(varName, {
369
- varName,
370
- varLabel: node.attrs.varLabel || varName,
371
- varDefault: node.attrs.varDefault || ""
372
- });
373
- }
374
- }
375
- if (node.content) {
376
- node.content.forEach(walk);
377
- }
378
- }
379
- if (content.content) {
380
- content.content.forEach(walk);
381
- }
382
- return Array.from(seen.values());
383
- }
384
- function replaceVariablesInContent(content, values) {
385
- function walkNode(node) {
386
- if (node.type === "variableNode" && node.attrs) {
387
- const varName = node.attrs.varName;
388
- const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || varName}]`;
389
- const textNode = { type: "text", text: replacement };
390
- if (node.marks && node.marks.length > 0) {
391
- textNode.marks = node.marks;
392
- }
393
- return textNode;
394
- }
395
- if (node.content) {
396
- return { ...node, content: node.content.map(walkNode) };
397
- }
398
- return node;
399
- }
400
- return walkNode(content);
401
- }
402
-
403
- // src/utils/markdown-writer.ts
404
- function serializeFieldToken(attrs) {
405
- const parts = ["field"];
406
- if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
407
- if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
408
- if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
409
- if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
410
- if (attrs.required) parts.push(`required:true`);
411
- if (attrs.options) parts.push(`options:${attrs.options}`);
412
- if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
413
- if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
414
- if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
415
- if (attrs.multiline) parts.push(`multiline:true`);
416
- if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
417
- if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
418
- return `{{${parts.join("|")}}}`;
419
- }
420
- function serializeVariableToken(attrs) {
421
- const parts = ["var"];
422
- if (attrs.varName) parts.push(`name:${attrs.varName}`);
423
- if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
424
- if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
425
- return `{{${parts.join("|")}}}`;
426
- }
427
- function serializeInline(content) {
428
- if (!content) return "";
429
- let result = "";
430
- for (const node of content) {
431
- if (node.type === "fieldNode") {
432
- result += serializeFieldToken(node.attrs || {});
433
- continue;
434
- }
435
- if (node.type === "variableNode") {
436
- let token = serializeVariableToken(node.attrs || {});
437
- const varMarks = node.marks || [];
438
- const isBold = varMarks.some((m) => m.type === "bold");
439
- const isItalic = varMarks.some((m) => m.type === "italic");
440
- const isUnderline = varMarks.some((m) => m.type === "underline");
441
- if (isBold && isItalic) token = `***${token}***`;
442
- else if (isBold) token = `**${token}**`;
443
- else if (isItalic) token = `*${token}*`;
444
- if (isUnderline) token = `__${token}__`;
445
- result += token;
446
- continue;
447
- }
448
- if (node.type === "text") {
449
- let text = node.text || "";
450
- const marks = node.marks || [];
451
- for (const mark of marks) {
452
- if (mark.type === "bold") text = `**${text}**`;
453
- else if (mark.type === "italic") text = `*${text}*`;
454
- else if (mark.type === "underline") text = `__${text}__`;
455
- else if (mark.type === "code") text = `\`${text}\``;
456
- }
457
- result += text;
458
- }
459
- if (node.type === "hardBreak") {
460
- result += " \n";
461
- }
462
- }
463
- return result;
464
- }
465
- function serializeBlock(node) {
466
- switch (node.type) {
467
- case "heading": {
468
- const level = node.attrs?.level || 1;
469
- const prefix = "#".repeat(level);
470
- return `${prefix} ${serializeInline(node.content)}`;
471
- }
472
- case "paragraph": {
473
- const inline = serializeInline(node.content);
474
- return inline;
475
- }
476
- case "bulletList": {
477
- return (node.content || []).map((item) => {
478
- const inner = (item.content || []).map(serializeBlock).join("\n");
479
- return `- ${inner}`;
480
- }).join("\n");
481
- }
482
- case "orderedList": {
483
- return (node.content || []).map((item, i) => {
484
- const inner = (item.content || []).map(serializeBlock).join("\n");
485
- return `${i + 1}. ${inner}`;
486
- }).join("\n");
487
- }
488
- case "listItem": {
489
- return (node.content || []).map(serializeBlock).join("\n");
490
- }
491
- case "blockquote": {
492
- return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
493
- }
494
- case "codeBlock": {
495
- const text = serializeInline(node.content);
496
- return `\`\`\`
497
- ${text}
498
- \`\`\``;
499
- }
500
- case "horizontalRule": {
501
- return "---";
502
- }
503
- default:
504
- return serializeInline(node.content);
596
+ var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
597
+ var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
598
+ var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
599
+ var DIRECTIVE_CLOSE = /^:::$/;
600
+ function parseDirectiveAttrs(str) {
601
+ const attrs = {};
602
+ if (!str) return attrs;
603
+ const pairs = str.split("|");
604
+ for (const pair of pairs) {
605
+ const colonIdx = pair.indexOf(":");
606
+ if (colonIdx === -1) continue;
607
+ const key = pair.slice(0, colonIdx).trim();
608
+ const value = pair.slice(colonIdx + 1).trim();
609
+ if (key) attrs[key] = value;
505
610
  }
611
+ return attrs;
506
612
  }
507
- function tiptapToMarkdown(doc) {
508
- if (!doc.content) return "";
509
- return doc.content.map(serializeBlock).join("\n\n");
510
- }
511
- var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
512
613
  function injectMarks(token, marks) {
513
614
  const existingMatch = token.match(/\|_marks:([^}|]+)/);
514
615
  if (existingMatch) {
@@ -625,6 +726,15 @@ function parseVariableToken(tokenBody) {
625
726
  case "default":
626
727
  attrs.varDefault = value;
627
728
  break;
729
+ case "suppress":
730
+ attrs.suppressZero = value === "zero" ? "true" : "";
731
+ break;
732
+ case "format":
733
+ attrs.format = value;
734
+ break;
735
+ case "block":
736
+ attrs.block = value === "true" ? "true" : "";
737
+ break;
628
738
  case "_marks":
629
739
  attrs._marks = value;
630
740
  break;
@@ -632,6 +742,44 @@ function parseVariableToken(tokenBody) {
632
742
  }
633
743
  return attrs;
634
744
  }
745
+ function parseImageToken(tokenBody) {
746
+ const attrs = {
747
+ src: "",
748
+ varName: "",
749
+ alt: "",
750
+ width: "",
751
+ height: "",
752
+ align: ""
753
+ };
754
+ const pairs = tokenBody.split("|");
755
+ for (const pair of pairs) {
756
+ const colonIdx = pair.indexOf(":");
757
+ if (colonIdx === -1) continue;
758
+ const key = pair.slice(0, colonIdx).trim();
759
+ const value = pair.slice(colonIdx + 1).trim();
760
+ switch (key) {
761
+ case "src":
762
+ attrs.src = value;
763
+ break;
764
+ case "var":
765
+ attrs.varName = value;
766
+ break;
767
+ case "alt":
768
+ attrs.alt = value;
769
+ break;
770
+ case "width":
771
+ attrs.width = value;
772
+ break;
773
+ case "height":
774
+ attrs.height = value;
775
+ break;
776
+ case "align":
777
+ attrs.align = value;
778
+ break;
779
+ }
780
+ }
781
+ return attrs;
782
+ }
635
783
  function parseInline(text) {
636
784
  text = preprocessMarkedTokens(text);
637
785
  const nodes = [];
@@ -655,6 +803,8 @@ function parseInline(text) {
655
803
  const node = { type: "variableNode", attrs };
656
804
  if (marks.length > 0) node.marks = marks;
657
805
  nodes.push(node);
806
+ } else if (match[1] === "image") {
807
+ nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
658
808
  }
659
809
  lastIndex = match.index + match[0].length;
660
810
  }
@@ -666,45 +816,63 @@ function parseInline(text) {
666
816
  function parseFormattedText(text) {
667
817
  if (!text) return [];
668
818
  const nodes = [];
669
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
819
+ const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
670
820
  for (const part of parts) {
671
821
  if (!part) continue;
672
- if (part.startsWith("**") && part.endsWith("**")) {
673
- nodes.push({
674
- type: "text",
675
- text: part.slice(2, -2),
676
- marks: [{ type: "bold" }]
677
- });
822
+ if (part.startsWith("***") && part.endsWith("***")) {
823
+ const inner = part.slice(3, -3);
824
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
825
+ } else if (part.startsWith("**") && part.endsWith("**")) {
826
+ const inner = part.slice(2, -2);
827
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
678
828
  } else if (part.startsWith("*") && part.endsWith("*")) {
679
- nodes.push({
680
- type: "text",
681
- text: part.slice(1, -1),
682
- marks: [{ type: "italic" }]
683
- });
829
+ const inner = part.slice(1, -1);
830
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
684
831
  } else if (part.startsWith("__") && part.endsWith("__")) {
685
- nodes.push({
686
- type: "text",
687
- text: part.slice(2, -2),
688
- marks: [{ type: "underline" }]
689
- });
832
+ const inner = part.slice(2, -2);
833
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
690
834
  } else if (part.startsWith("`") && part.endsWith("`")) {
691
- nodes.push({
692
- type: "text",
693
- text: part.slice(1, -1),
694
- marks: [{ type: "code" }]
695
- });
835
+ const inner = part.slice(1, -1);
836
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
696
837
  } else {
697
838
  nodes.push({ type: "text", text: part });
698
839
  }
699
840
  }
700
841
  return nodes;
701
842
  }
702
- function markdownToTiptap(markdown) {
703
- const lines = markdown.split("\n");
843
+ function splitTableCells(row) {
844
+ const cells = [];
845
+ let current = "";
846
+ let depth = 0;
847
+ for (let i = 0; i < row.length; i++) {
848
+ if (row[i] === "{" && row[i + 1] === "{") {
849
+ depth++;
850
+ current += "{{";
851
+ i++;
852
+ } else if (row[i] === "}" && row[i + 1] === "}") {
853
+ depth = Math.max(0, depth - 1);
854
+ current += "}}";
855
+ i++;
856
+ } else if (row[i] === "|" && depth === 0) {
857
+ cells.push(current.trim());
858
+ current = "";
859
+ } else {
860
+ current += row[i];
861
+ }
862
+ }
863
+ if (current.trim()) {
864
+ cells.push(current.trim());
865
+ }
866
+ return cells;
867
+ }
868
+ function parseBlocks(lines, startIdx, stopCondition) {
704
869
  const content = [];
705
- let i = 0;
870
+ let i = startIdx;
706
871
  while (i < lines.length) {
707
872
  const line = lines[i];
873
+ if (stopCondition && stopCondition(line)) {
874
+ break;
875
+ }
708
876
  if (line.trim() === "") {
709
877
  i++;
710
878
  continue;
@@ -778,13 +946,443 @@ function markdownToTiptap(markdown) {
778
946
  });
779
947
  continue;
780
948
  }
781
- content.push({
782
- type: "paragraph",
783
- content: parseInline(line)
784
- });
949
+ if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
950
+ const tableLines = [];
951
+ while (i < lines.length) {
952
+ const tl = lines[i];
953
+ if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
954
+ tableLines.push(tl);
955
+ i++;
956
+ } else {
957
+ break;
958
+ }
959
+ }
960
+ const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
961
+ const dataLines = tableLines.filter(
962
+ (tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
963
+ );
964
+ const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
965
+ const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
966
+ const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
967
+ const tableRows = [];
968
+ let isFirstDataRow = true;
969
+ for (const tl of tableLines) {
970
+ if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
971
+ const cells = splitTableCells(tl).filter((c) => c !== "");
972
+ if (isBorderless && isFirstDataRow) {
973
+ isFirstDataRow = false;
974
+ continue;
975
+ }
976
+ const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
977
+ const rowContent = cells.map((cellText) => ({
978
+ type: cellType,
979
+ content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
980
+ }));
981
+ tableRows.push({ type: "tableRow", content: rowContent });
982
+ isFirstDataRow = false;
983
+ }
984
+ if (tableRows.length > 0) {
985
+ const tableNode = { type: "table", content: tableRows };
986
+ if (isBorderless) {
987
+ tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
988
+ }
989
+ content.push(tableNode);
990
+ }
991
+ continue;
992
+ }
993
+ const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
994
+ if (directiveMatch) {
995
+ const directiveType = directiveMatch[1];
996
+ const rawAttrs = directiveMatch[2] || "";
997
+ const attrs = parseDirectiveAttrs(rawAttrs);
998
+ i++;
999
+ if (directiveType === "watermark") {
1000
+ content.push({
1001
+ type: "watermark",
1002
+ attrs: {
1003
+ text: attrs.text || "",
1004
+ opacity: attrs.opacity || "0.15",
1005
+ angle: attrs.angle || "-45"
1006
+ }
1007
+ });
1008
+ continue;
1009
+ }
1010
+ if (directiveType === "panel") {
1011
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1012
+ i = inner.nextIdx;
1013
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1014
+ i++;
1015
+ }
1016
+ content.push({
1017
+ type: "panel",
1018
+ attrs: {
1019
+ title: attrs.title || "",
1020
+ border: attrs.border || "solid",
1021
+ headerStyle: attrs.headerStyle || ""
1022
+ },
1023
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1024
+ });
1025
+ continue;
1026
+ }
1027
+ if (directiveType === "repeat") {
1028
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1029
+ i = inner.nextIdx;
1030
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1031
+ i++;
1032
+ }
1033
+ content.push({
1034
+ type: "repeatBlock",
1035
+ attrs: { data: attrs.data || "" },
1036
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1037
+ });
1038
+ continue;
1039
+ }
1040
+ if (directiveType === "subtotals") {
1041
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1042
+ i = inner.nextIdx;
1043
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1044
+ i++;
1045
+ }
1046
+ content.push({
1047
+ type: "subtotalsBlock",
1048
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1049
+ });
1050
+ continue;
1051
+ }
1052
+ if (directiveType === "columns") {
1053
+ const splitVal = attrs.split || "50";
1054
+ const columns = [];
1055
+ while (i < lines.length) {
1056
+ const colLine = lines[i].trim();
1057
+ if (DIRECTIVE_CLOSE.test(colLine)) {
1058
+ if (columns.length > 0) {
1059
+ i++;
1060
+ break;
1061
+ }
1062
+ i++;
1063
+ break;
1064
+ }
1065
+ const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
1066
+ if (colMatch) {
1067
+ i++;
1068
+ const colInner = parseBlocks(lines, i, (l) => {
1069
+ const trimmed = l.trim();
1070
+ return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
1071
+ });
1072
+ i = colInner.nextIdx;
1073
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1074
+ i++;
1075
+ }
1076
+ const colAttrsRaw = colMatch[1] || "";
1077
+ const colAttrs = parseDirectiveAttrs(colAttrsRaw);
1078
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
1079
+ columns.push({
1080
+ type: "column",
1081
+ attrs: padTop ? { padTop } : void 0,
1082
+ content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
1083
+ });
1084
+ continue;
1085
+ }
1086
+ if (colLine === "") {
1087
+ i++;
1088
+ continue;
1089
+ }
1090
+ break;
1091
+ }
1092
+ const columnsAttrs = { split: splitVal };
1093
+ if (attrs.padX) columnsAttrs.padX = attrs.padX;
1094
+ content.push({
1095
+ type: "columns",
1096
+ attrs: columnsAttrs,
1097
+ content: columns.length > 0 ? columns : [
1098
+ { type: "column", content: [{ type: "paragraph" }] },
1099
+ { type: "column", content: [{ type: "paragraph" }] }
1100
+ ]
1101
+ });
1102
+ continue;
1103
+ }
1104
+ if (directiveType === "col") {
1105
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1106
+ i = inner.nextIdx;
1107
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1108
+ i++;
1109
+ }
1110
+ for (const block of inner.blocks) {
1111
+ content.push(block);
1112
+ }
1113
+ continue;
1114
+ }
1115
+ continue;
1116
+ }
1117
+ if (DIRECTIVE_CLOSE.test(line.trim())) {
1118
+ i++;
1119
+ continue;
1120
+ }
1121
+ const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
1122
+ if (imageMatch) {
1123
+ content.push({
1124
+ type: "imageNode",
1125
+ attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
1126
+ });
1127
+ i++;
1128
+ continue;
1129
+ }
1130
+ const inlineNodes = parseInline(line);
1131
+ if (inlineNodes.some((n) => n.type === "imageNode")) {
1132
+ let run = [];
1133
+ const flushRun = () => {
1134
+ if (run.some((n) => !(n.type === "text" && !(n.text || "").trim()))) {
1135
+ content.push({ type: "paragraph", content: run });
1136
+ }
1137
+ run = [];
1138
+ };
1139
+ for (const n of inlineNodes) {
1140
+ if (n.type === "imageNode") {
1141
+ flushRun();
1142
+ content.push(n);
1143
+ } else {
1144
+ run.push(n);
1145
+ }
1146
+ }
1147
+ flushRun();
1148
+ } else {
1149
+ content.push({ type: "paragraph", content: inlineNodes });
1150
+ }
785
1151
  i++;
786
1152
  }
787
- return { type: "doc", content };
1153
+ return { blocks: content, nextIdx: i };
1154
+ }
1155
+ function markdownToTiptap(markdown) {
1156
+ const lines = markdown.split("\n");
1157
+ const { blocks } = parseBlocks(lines, 0);
1158
+ return { type: "doc", content: blocks };
1159
+ }
1160
+
1161
+ // src/utils/variable-helpers.ts
1162
+ function extractVariablesFromContent(content) {
1163
+ const seen = /* @__PURE__ */ new Map();
1164
+ function walk(node) {
1165
+ if (node.type === "variableNode" && node.attrs) {
1166
+ const varName = node.attrs.varName;
1167
+ if (varName && !seen.has(varName)) {
1168
+ seen.set(varName, {
1169
+ varName,
1170
+ varLabel: node.attrs.varLabel || varName,
1171
+ varDefault: node.attrs.varDefault || ""
1172
+ });
1173
+ }
1174
+ }
1175
+ if (node.content) {
1176
+ node.content.forEach(walk);
1177
+ }
1178
+ }
1179
+ if (content.content) {
1180
+ content.content.forEach(walk);
1181
+ }
1182
+ return Array.from(seen.values());
1183
+ }
1184
+ function applyFormat(value, format) {
1185
+ if (format === "phone") {
1186
+ const digits = value.replace(/\D/g, "");
1187
+ if (digits.length === 10) {
1188
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
1189
+ }
1190
+ }
1191
+ return value;
1192
+ }
1193
+ function replaceVariablesInContent(content, values) {
1194
+ function walkNode(node) {
1195
+ if (node.type === "variableNode" && node.attrs) {
1196
+ const varName = node.attrs.varName;
1197
+ let replacement = values[varName] || node.attrs.varDefault || "";
1198
+ const format = node.attrs.format;
1199
+ if (format && replacement) {
1200
+ replacement = applyFormat(replacement, format);
1201
+ }
1202
+ const textNode = { type: "text", text: replacement };
1203
+ if (node.marks && node.marks.length > 0) {
1204
+ textNode.marks = node.marks;
1205
+ }
1206
+ return textNode;
1207
+ }
1208
+ if (node.type === "imageNode" && node.attrs?.varName) {
1209
+ const varName = node.attrs.varName;
1210
+ const resolved = values[varName] || node.attrs.src || "";
1211
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
1212
+ }
1213
+ if (node.content) {
1214
+ return { ...node, content: node.content.map(walkNode) };
1215
+ }
1216
+ return node;
1217
+ }
1218
+ return walkNode(content);
1219
+ }
1220
+ function expandBlockVariables(content, values) {
1221
+ function soleVariableOf(node) {
1222
+ if (node.type !== "paragraph" || !node.content) return null;
1223
+ const meaningful = node.content.filter(
1224
+ (c) => !(c.type === "text" && !(c.text || "").trim())
1225
+ );
1226
+ if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
1227
+ return meaningful[0];
1228
+ }
1229
+ return null;
1230
+ }
1231
+ function stripUnsafeNodes(node) {
1232
+ if (!node.content) return node;
1233
+ return {
1234
+ ...node,
1235
+ content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
1236
+ };
1237
+ }
1238
+ function isCellSafe(blocks) {
1239
+ return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
1240
+ }
1241
+ function walkNode(node, inCell) {
1242
+ if (!node.content) return node;
1243
+ const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
1244
+ const newContent = [];
1245
+ for (const child of node.content) {
1246
+ const varNode = soleVariableOf(child);
1247
+ if (varNode?.attrs) {
1248
+ const varName = varNode.attrs.varName;
1249
+ const value = values[varName] || varNode.attrs.varDefault || "";
1250
+ const isBlock = varNode.attrs.block === "true" || value.includes("\n");
1251
+ if (isBlock) {
1252
+ const parsed = stripUnsafeNodes(markdownToTiptap(value));
1253
+ const blocks = parsed.content || [];
1254
+ if (!childInCell || isCellSafe(blocks)) {
1255
+ newContent.push(...blocks);
1256
+ continue;
1257
+ }
1258
+ }
1259
+ }
1260
+ newContent.push(walkNode(child, childInCell));
1261
+ }
1262
+ return { ...node, content: newContent };
1263
+ }
1264
+ return walkNode(content, false);
1265
+ }
1266
+ function labelToVarName(label) {
1267
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1268
+ }
1269
+ function isZeroLike(value) {
1270
+ if (!value || value.trim() === "") return true;
1271
+ const cleaned = value.replace(/[$,\s]/g, "");
1272
+ return /^-?0+(\.0+)?$/.test(cleaned);
1273
+ }
1274
+ function suppressZeroContent(content, values) {
1275
+ function hasSuppressedVar(node) {
1276
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
1277
+ const varName = node.attrs.varName;
1278
+ const val = values[varName] || node.attrs.varDefault || "";
1279
+ return isZeroLike(val);
1280
+ }
1281
+ return (node.content || []).some(hasSuppressedVar);
1282
+ }
1283
+ function walkNode(node) {
1284
+ if (node.type === "table" && node.content) {
1285
+ const filtered = node.content.filter((row) => {
1286
+ if (row.type !== "tableRow") return true;
1287
+ const isHeader = row.content?.[0]?.type === "tableHeader";
1288
+ if (isHeader) return true;
1289
+ return !hasSuppressedVar(row);
1290
+ });
1291
+ const bodyRows = filtered.filter(
1292
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
1293
+ );
1294
+ if (bodyRows.length === 0) return null;
1295
+ return { ...node, content: filtered };
1296
+ }
1297
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
1298
+ return null;
1299
+ }
1300
+ if (node.content) {
1301
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
1302
+ return { ...node, content: filtered };
1303
+ }
1304
+ return node;
1305
+ }
1306
+ return walkNode(content) || content;
1307
+ }
1308
+ function expandRepeatContent(content, values) {
1309
+ const enrichedValues = { ...values };
1310
+ function cloneNode(node, dataKey, index) {
1311
+ if (node.type === "variableNode" && node.attrs?.varName) {
1312
+ const oldName = node.attrs.varName;
1313
+ const bareName = oldName.replace(/^operation_/, "");
1314
+ const newName = `${dataKey}_${index}_${bareName}`;
1315
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
1316
+ }
1317
+ if (node.content) {
1318
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
1319
+ }
1320
+ return { ...node };
1321
+ }
1322
+ function walkNode(node) {
1323
+ if (node.type === "repeatBlock" && node.attrs?.data) {
1324
+ const dataKey = node.attrs.data;
1325
+ const arrayJson = values[dataKey];
1326
+ if (!arrayJson) return [];
1327
+ let items;
1328
+ try {
1329
+ items = JSON.parse(arrayJson);
1330
+ } catch {
1331
+ return [];
1332
+ }
1333
+ const sums = /* @__PURE__ */ new Map();
1334
+ for (const item of items) {
1335
+ for (const [key, val] of Object.entries(item)) {
1336
+ const num = parseFloat(val);
1337
+ if (!isNaN(num)) {
1338
+ sums.set(key, (sums.get(key) || 0) + num);
1339
+ }
1340
+ }
1341
+ }
1342
+ const woKeysByNorm = /* @__PURE__ */ new Map();
1343
+ for (const k of Object.keys(enrichedValues)) {
1344
+ if (k.startsWith("workorder_")) {
1345
+ const norm = k.replace(/_/g, "");
1346
+ woKeysByNorm.set(norm, k);
1347
+ }
1348
+ }
1349
+ for (const [field, total] of sums) {
1350
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
1351
+ const directKey = `workorder_${field}`;
1352
+ enrichedValues[directKey] = formatted;
1353
+ const norm = `workorder${field}`.replace(/_/g, "");
1354
+ const existing = woKeysByNorm.get(norm);
1355
+ if (existing && existing !== directKey) {
1356
+ enrichedValues[existing] = formatted;
1357
+ }
1358
+ }
1359
+ const expanded = [];
1360
+ for (let i = 0; i < items.length; i++) {
1361
+ const item = items[i];
1362
+ for (const [key, val] of Object.entries(item)) {
1363
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
1364
+ }
1365
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
1366
+ expanded.push(...cloned);
1367
+ }
1368
+ return expanded;
1369
+ }
1370
+ if (node.content) {
1371
+ const newContent = [];
1372
+ for (const child of node.content) {
1373
+ const result2 = walkNode(child);
1374
+ if (Array.isArray(result2)) newContent.push(...result2);
1375
+ else newContent.push(result2);
1376
+ }
1377
+ return { ...node, content: newContent };
1378
+ }
1379
+ return node;
1380
+ }
1381
+ const result = walkNode(content);
1382
+ return {
1383
+ content: Array.isArray(result) ? { ...content, content: result } : result,
1384
+ values: enrichedValues
1385
+ };
788
1386
  }
789
1387
 
790
1388
  // src/utils/error-helpers.ts
@@ -922,10 +1520,40 @@ function buildMetadataObject(fields, actualFieldNames) {
922
1520
  // src/utils/pdf-generator.ts
923
1521
  var PAGE_WIDTH = 595.28;
924
1522
  var PAGE_HEIGHT = 841.89;
925
- var MARGIN = 72;
1523
+ var MARGIN = 40;
926
1524
  var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
927
1525
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
928
1526
  var LINE_HEIGHT_FACTOR = 1.4;
1527
+ var BODY_FONT_SIZE = 10;
1528
+ function hexToRgbColor(hex) {
1529
+ if (!hex) return null;
1530
+ const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
1531
+ if (!m) return null;
1532
+ const n = parseInt(m[1], 16);
1533
+ return rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
1534
+ }
1535
+ function contrastTextColor(fill) {
1536
+ const c = fill;
1537
+ const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
1538
+ return luminance > 0.6 ? rgb(0, 0, 0) : rgb(1, 1, 1);
1539
+ }
1540
+ function resolveTheme(theme) {
1541
+ const primary = hexToRgbColor(theme?.primaryColor);
1542
+ const accent = hexToRgbColor(theme?.accentColor);
1543
+ const border = hexToRgbColor(theme?.borderColor);
1544
+ return {
1545
+ tableHeaderBg: primary ?? rgb(0.3, 0.3, 0.3),
1546
+ tableHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
1547
+ tableBorder: border ?? rgb(0.75, 0.75, 0.75),
1548
+ panelDarkHeaderBg: primary ?? rgb(0.25, 0.25, 0.25),
1549
+ panelDarkHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
1550
+ panelBorder: border ?? rgb(0.6, 0.6, 0.6),
1551
+ rule: primary ?? rgb(0, 0, 0),
1552
+ pageHeaderText: primary ?? rgb(0, 0, 0),
1553
+ pageHeaderRule: accent ?? primary ?? rgb(0.75, 0.75, 0.75)
1554
+ };
1555
+ }
1556
+ var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
929
1557
  var FIELD_DISPLAY = {
930
1558
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
931
1559
  ["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
@@ -956,17 +1584,65 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
956
1584
  const height = textLineHeight + 4;
957
1585
  return { width, height };
958
1586
  }
1587
+ async function loadImageBytes(src) {
1588
+ if (src.startsWith("data:")) {
1589
+ const comma = src.indexOf(",");
1590
+ if (comma === -1) throw new Error("malformed data URL");
1591
+ const meta = src.slice(0, comma);
1592
+ if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
1593
+ const bin = atob(src.slice(comma + 1));
1594
+ const bytes = new Uint8Array(bin.length);
1595
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1596
+ return bytes;
1597
+ }
1598
+ const res = await fetch(src);
1599
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1600
+ return new Uint8Array(await res.arrayBuffer());
1601
+ }
1602
+ function describeImageSrc(src) {
1603
+ return src.length > 80 ? `${src.slice(0, 77)}...` : src;
1604
+ }
1605
+ async function preloadImages(pdfDoc, content) {
1606
+ const srcs = /* @__PURE__ */ new Set();
1607
+ (function walk(node) {
1608
+ if (node.type === "imageNode") {
1609
+ const src = node.attrs?.src || "";
1610
+ if (src) srcs.add(src);
1611
+ }
1612
+ for (const child of node.content || []) walk(child);
1613
+ })(content);
1614
+ const images = /* @__PURE__ */ new Map();
1615
+ const warnings = [];
1616
+ for (const src of srcs) {
1617
+ try {
1618
+ const bytes = await loadImageBytes(src);
1619
+ const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
1620
+ const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
1621
+ if (isPng) {
1622
+ images.set(src, await pdfDoc.embedPng(bytes));
1623
+ } else if (isJpeg) {
1624
+ images.set(src, await pdfDoc.embedJpg(bytes));
1625
+ } else {
1626
+ warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
1627
+ }
1628
+ } catch (err) {
1629
+ warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
1630
+ }
1631
+ }
1632
+ return { images, warnings };
1633
+ }
959
1634
  function ensureSpace(state, neededHeight) {
960
1635
  if (state.y + neededHeight > CONTENT_HEIGHT) {
961
1636
  const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
962
1637
  state.currentPage = newPage;
963
1638
  state.pageIndex++;
964
1639
  state.y = 0;
1640
+ renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
965
1641
  }
966
1642
  }
967
- function drawText(state, text, font, fontSize, indent = 0) {
1643
+ function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
968
1644
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
969
- const maxWidth = CONTENT_WIDTH - indent;
1645
+ const maxWidth = region.width - indent;
970
1646
  const words = text.split(/\s+/);
971
1647
  let line = "";
972
1648
  let totalAdvance = 0;
@@ -977,7 +1653,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
977
1653
  ensureSpace(state, lineHeight);
978
1654
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
979
1655
  state.currentPage.drawText(line, {
980
- x: MARGIN + indent,
1656
+ x: region.leftX + indent,
981
1657
  y: pdfY,
982
1658
  size: fontSize,
983
1659
  font,
@@ -994,7 +1670,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
994
1670
  ensureSpace(state, lineHeight);
995
1671
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
996
1672
  state.currentPage.drawText(line, {
997
- x: MARGIN + indent,
1673
+ x: region.leftX + indent,
998
1674
  y: pdfY,
999
1675
  size: fontSize,
1000
1676
  font,
@@ -1038,10 +1714,10 @@ function collectInlineSegments(content, fonts, fontSize) {
1038
1714
  }
1039
1715
  return segments;
1040
1716
  }
1041
- function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1717
+ function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
1042
1718
  const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
1043
- const maxX = MARGIN + CONTENT_WIDTH;
1044
- const startX = MARGIN + indent;
1719
+ const maxX = region.leftX + region.width;
1720
+ const startX = region.leftX + indent;
1045
1721
  let currentX = startX;
1046
1722
  let currentLineHeight = 0;
1047
1723
  let lineHasContent = false;
@@ -1146,13 +1822,318 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1146
1822
  }
1147
1823
  flushLine();
1148
1824
  }
1149
- function processInlineContent(state, content, fontSize, indent = 0) {
1825
+ function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
1150
1826
  if (!content) return;
1151
1827
  const segments = collectInlineSegments(content, state.fonts, fontSize);
1152
1828
  if (segments.length === 0) return;
1153
- layoutInlineSegments(state, segments, fontSize, indent);
1829
+ layoutInlineSegments(state, segments, fontSize, indent, region);
1830
+ }
1831
+ function extractCellText(cell) {
1832
+ const parts = [];
1833
+ for (const child of cell.content || []) {
1834
+ if (child.type === "paragraph" || child.type === "heading") {
1835
+ for (const inline of child.content || []) {
1836
+ if (inline.type === "text") {
1837
+ parts.push(inline.text || "");
1838
+ } else if (inline.type === "variableNode") {
1839
+ const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
1840
+ parts.push(`[${label}]`);
1841
+ } else if (inline.type === "fieldNode") {
1842
+ const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
1843
+ parts.push(`[ ${label} ]`);
1844
+ }
1845
+ }
1846
+ }
1847
+ }
1848
+ return parts.join("");
1154
1849
  }
1155
- function processBlock(state, node) {
1850
+ function isNumericText(text) {
1851
+ return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
1852
+ }
1853
+ function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
1854
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1855
+ const font = isHeader ? fonts.bold : fonts.regular;
1856
+ let totalHeight = 0;
1857
+ for (const child of cell.content || []) {
1858
+ if (child.type === "paragraph" || child.type === "heading") {
1859
+ const text = extractCellText({ content: [child] });
1860
+ if (!text) {
1861
+ totalHeight += lineHeight;
1862
+ continue;
1863
+ }
1864
+ const words = text.split(/\s+/);
1865
+ let line = "";
1866
+ let lineCount = 0;
1867
+ for (const word of words) {
1868
+ const testLine = line ? `${line} ${word}` : word;
1869
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
1870
+ if (testWidth > maxWidth && line) {
1871
+ lineCount++;
1872
+ line = word;
1873
+ } else {
1874
+ line = testLine;
1875
+ }
1876
+ }
1877
+ if (line) lineCount++;
1878
+ totalHeight += lineCount * lineHeight;
1879
+ }
1880
+ }
1881
+ return Math.max(totalHeight, lineHeight);
1882
+ }
1883
+ function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = rgb(0, 0, 0)) {
1884
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1885
+ const cellPadding = 4;
1886
+ const maxTextWidth = cellWidth - cellPadding * 2;
1887
+ let textX = cellX + cellPadding;
1888
+ let textY = cellY;
1889
+ for (const child of cell.content || []) {
1890
+ if (child.type === "paragraph" || child.type === "heading") {
1891
+ const segments = collectInlineSegments(
1892
+ child.content || [],
1893
+ isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
1894
+ fontSize
1895
+ );
1896
+ if (rightAlign && segments.length > 0) {
1897
+ const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
1898
+ const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
1899
+ if (textWidth <= maxTextWidth) {
1900
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1901
+ state.currentPage.drawText(fullText, {
1902
+ x: cellX + cellWidth - cellPadding - textWidth,
1903
+ y: pdfY,
1904
+ size: fontSize,
1905
+ font: isHeader ? fonts.bold : fonts.regular,
1906
+ color: textColor
1907
+ });
1908
+ return;
1909
+ }
1910
+ }
1911
+ let currentX = textX;
1912
+ for (const seg of segments) {
1913
+ if (seg.kind === "break") {
1914
+ textY += lineHeight;
1915
+ currentX = textX;
1916
+ continue;
1917
+ }
1918
+ if (seg.kind === "text") {
1919
+ const words = seg.text.split(/(\s+)/);
1920
+ const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
1921
+ for (const token of words) {
1922
+ if (!token) continue;
1923
+ if (/^\s+$/.test(token)) {
1924
+ currentX += spaceWidth;
1925
+ continue;
1926
+ }
1927
+ const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
1928
+ if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
1929
+ textY += lineHeight;
1930
+ currentX = textX;
1931
+ }
1932
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1933
+ state.currentPage.drawText(token, {
1934
+ x: currentX,
1935
+ y: pdfY,
1936
+ size: fontSize,
1937
+ font: seg.font,
1938
+ color: textColor
1939
+ });
1940
+ currentX += wordWidth;
1941
+ }
1942
+ } else if (seg.kind === "field") {
1943
+ const pdfY = PAGE_HEIGHT - textY - seg.height;
1944
+ if (state.drawFieldPlaceholders) {
1945
+ state.currentPage.drawRectangle({
1946
+ x: currentX,
1947
+ y: pdfY,
1948
+ width: seg.width,
1949
+ height: seg.height,
1950
+ borderColor: rgb(0.5, 0.5, 0.7),
1951
+ borderWidth: 0.5,
1952
+ color: rgb(0.95, 0.95, 1),
1953
+ borderDashArray: [4, 2]
1954
+ });
1955
+ }
1956
+ const fieldId = seg.attrs.fieldId;
1957
+ if (fieldId) {
1958
+ state.fieldPositions.set(fieldId, {
1959
+ x: currentX,
1960
+ y: pdfY,
1961
+ width: seg.width,
1962
+ height: seg.height,
1963
+ page: state.pageIndex + 1
1964
+ });
1965
+ }
1966
+ currentX += seg.width + 4;
1967
+ }
1968
+ }
1969
+ }
1970
+ }
1971
+ }
1972
+ function renderTable(state, node, region = DEFAULT_REGION) {
1973
+ const tableRows = node.content || [];
1974
+ if (tableRows.length === 0) return;
1975
+ const borderless = node.attrs?.borderless === true;
1976
+ const subtotals = node.attrs?.subtotals === true;
1977
+ const fontSize = 10;
1978
+ const cellPadding = borderless ? 2 : 4;
1979
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1980
+ const rowPadding = cellPadding * 2;
1981
+ const borderColor = state.theme.tableBorder;
1982
+ const headerBg = state.theme.tableHeaderBg;
1983
+ const headerTextColor = state.theme.tableHeaderText;
1984
+ let colCount = 0;
1985
+ for (const row of tableRows) {
1986
+ colCount = Math.max(colCount, row.content?.length || 0);
1987
+ }
1988
+ if (colCount === 0) return;
1989
+ const colMaxWidths = new Array(colCount).fill(0);
1990
+ for (const row of tableRows) {
1991
+ const cells = row.content || [];
1992
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1993
+ const cell = cells[c];
1994
+ const isHeader = cell.type === "tableHeader";
1995
+ const text = extractCellText(cell);
1996
+ const font = isHeader ? state.fonts.bold : state.fonts.regular;
1997
+ const textWidth = font.widthOfTextAtSize(text, fontSize);
1998
+ colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
1999
+ }
2000
+ }
2001
+ const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
2002
+ const minColWidth = 40;
2003
+ let colWidths;
2004
+ if (totalMeasured <= region.width) {
2005
+ const scale = region.width / totalMeasured;
2006
+ colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
2007
+ } else {
2008
+ colWidths = colMaxWidths.map(
2009
+ (w) => Math.max(w / totalMeasured * region.width, minColWidth)
2010
+ );
2011
+ }
2012
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
2013
+ if (colSum > 0) {
2014
+ const normFactor = region.width / colSum;
2015
+ colWidths = colWidths.map((w) => w * normFactor);
2016
+ }
2017
+ const headerRow = tableRows[0];
2018
+ const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
2019
+ function renderRow(row, isHeaderRow) {
2020
+ const cells = row.content || [];
2021
+ const isHeader = isHeaderRow && hasHeader;
2022
+ let rowHeight = lineHeight;
2023
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2024
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
2025
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
2026
+ rowHeight = Math.max(rowHeight, cellH);
2027
+ }
2028
+ rowHeight += rowPadding;
2029
+ ensureSpace(state, rowHeight);
2030
+ const rowTopY = state.y;
2031
+ let cellX = region.leftX;
2032
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2033
+ const cellW = colWidths[c];
2034
+ const cell = cells[c];
2035
+ const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
2036
+ const pdfCellBottom = pdfCellTop - rowHeight;
2037
+ if (isHeader && !borderless) {
2038
+ state.currentPage.drawRectangle({
2039
+ x: cellX,
2040
+ y: pdfCellBottom,
2041
+ width: cellW,
2042
+ height: rowHeight,
2043
+ color: headerBg
2044
+ });
2045
+ }
2046
+ if (!borderless) {
2047
+ state.currentPage.drawRectangle({
2048
+ x: cellX,
2049
+ y: pdfCellBottom,
2050
+ width: cellW,
2051
+ height: rowHeight,
2052
+ borderColor,
2053
+ borderWidth: 0.5
2054
+ });
2055
+ }
2056
+ const cellText = extractCellText(cell);
2057
+ const rightAlign = !isHeader && isNumericText(cellText);
2058
+ const contentY = MARGIN + rowTopY + cellPadding;
2059
+ const effectiveHeader = isHeader && !borderless;
2060
+ renderCellContent(
2061
+ state,
2062
+ cell,
2063
+ cellX,
2064
+ contentY,
2065
+ cellW,
2066
+ state.fonts,
2067
+ fontSize,
2068
+ effectiveHeader,
2069
+ rightAlign,
2070
+ effectiveHeader ? headerTextColor : rgb(0, 0, 0)
2071
+ );
2072
+ cellX += cellW;
2073
+ }
2074
+ state.y += rowHeight;
2075
+ }
2076
+ if (hasHeader && tableRows.length > 0) {
2077
+ renderRow(tableRows[0], true);
2078
+ }
2079
+ let lastPageIndex = state.pageIndex;
2080
+ const startIdx = hasHeader ? 1 : 0;
2081
+ const bodyRowCount = tableRows.length - startIdx;
2082
+ for (let r = startIdx; r < tableRows.length; r++) {
2083
+ const cells = tableRows[r].content || [];
2084
+ let estRowHeight = lineHeight;
2085
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2086
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
2087
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
2088
+ estRowHeight = Math.max(estRowHeight, cellH);
2089
+ }
2090
+ estRowHeight += rowPadding;
2091
+ if (state.y + estRowHeight > CONTENT_HEIGHT) {
2092
+ ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
2093
+ if (state.pageIndex !== lastPageIndex && hasHeader) {
2094
+ renderRow(headerRow, true);
2095
+ lastPageIndex = state.pageIndex;
2096
+ }
2097
+ }
2098
+ const rowTopYBeforeRender = state.y;
2099
+ renderRow(tableRows[r], false);
2100
+ lastPageIndex = state.pageIndex;
2101
+ if (subtotals && colCount >= 2) {
2102
+ const bodyIdx = r - startIdx;
2103
+ const isFirstBody = bodyIdx === 0;
2104
+ const isLastBody = bodyIdx === bodyRowCount - 1;
2105
+ if (isFirstBody || isLastBody) {
2106
+ const valueStartX = region.leftX + colWidths[0];
2107
+ const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
2108
+ const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
2109
+ const overlineColor = rgb(0, 0, 0);
2110
+ if (isFirstBody) {
2111
+ state.currentPage.drawLine({
2112
+ start: { x: valueStartX, y: pdfRowTopY },
2113
+ end: { x: valueEndX, y: pdfRowTopY },
2114
+ thickness: 0.5,
2115
+ color: overlineColor
2116
+ });
2117
+ } else if (isLastBody) {
2118
+ state.currentPage.drawLine({
2119
+ start: { x: valueStartX, y: pdfRowTopY + 2 },
2120
+ end: { x: valueEndX, y: pdfRowTopY + 2 },
2121
+ thickness: 0.5,
2122
+ color: overlineColor
2123
+ });
2124
+ state.currentPage.drawLine({
2125
+ start: { x: valueStartX, y: pdfRowTopY },
2126
+ end: { x: valueEndX, y: pdfRowTopY },
2127
+ thickness: 0.5,
2128
+ color: overlineColor
2129
+ });
2130
+ }
2131
+ }
2132
+ }
2133
+ }
2134
+ state.y += 3;
2135
+ }
2136
+ function processBlock(state, node, region = DEFAULT_REGION) {
1156
2137
  switch (node.type) {
1157
2138
  case "heading": {
1158
2139
  const level = node.attrs?.level || 1;
@@ -1169,34 +2150,34 @@ function processBlock(state, node) {
1169
2150
  boldItalic: state.fonts.boldItalic
1170
2151
  };
1171
2152
  const segments = collectInlineSegments(node.content, headingFonts, fontSize);
1172
- layoutInlineSegments(state, segments, fontSize);
2153
+ layoutInlineSegments(state, segments, fontSize, 0, region);
1173
2154
  state.y += spacing / 2;
1174
2155
  break;
1175
2156
  }
1176
2157
  case "paragraph": {
1177
- const fontSize = 12;
2158
+ const fontSize = BODY_FONT_SIZE;
1178
2159
  if (!node.content || node.content.length === 0) {
1179
- state.y += fontSize * LINE_HEIGHT_FACTOR;
2160
+ state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
1180
2161
  break;
1181
2162
  }
1182
- processInlineContent(state, node.content, fontSize);
1183
- state.y += 4;
2163
+ processInlineContent(state, node.content, fontSize, 0, region);
2164
+ state.y += 2;
1184
2165
  break;
1185
2166
  }
1186
2167
  case "bulletList": {
1187
2168
  for (const item of node.content || []) {
1188
2169
  for (const child of item.content || []) {
1189
- const fontSize = 12;
2170
+ const fontSize = BODY_FONT_SIZE;
1190
2171
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1191
2172
  const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1192
2173
  state.currentPage.drawText("\u2022", {
1193
- x: MARGIN + 8,
2174
+ x: region.leftX + 8,
1194
2175
  y: bulletPdfY,
1195
2176
  size: fontSize,
1196
2177
  font: state.fonts.regular,
1197
2178
  color: rgb(0, 0, 0)
1198
2179
  });
1199
- processInlineContent(state, child.content, fontSize, 24);
2180
+ processInlineContent(state, child.content, fontSize, 24, region);
1200
2181
  }
1201
2182
  }
1202
2183
  state.y += 4;
@@ -1206,17 +2187,17 @@ function processBlock(state, node) {
1206
2187
  let num = 1;
1207
2188
  for (const item of node.content || []) {
1208
2189
  for (const child of item.content || []) {
1209
- const fontSize = 12;
2190
+ const fontSize = BODY_FONT_SIZE;
1210
2191
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1211
2192
  const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1212
2193
  state.currentPage.drawText(`${num}.`, {
1213
- x: MARGIN + 4,
2194
+ x: region.leftX + 4,
1214
2195
  y: numPdfY,
1215
2196
  size: fontSize,
1216
2197
  font: state.fonts.regular,
1217
2198
  color: rgb(0, 0, 0)
1218
2199
  });
1219
- processInlineContent(state, child.content, fontSize, 24);
2200
+ processInlineContent(state, child.content, fontSize, 24, region);
1220
2201
  }
1221
2202
  num++;
1222
2203
  }
@@ -1226,10 +2207,10 @@ function processBlock(state, node) {
1226
2207
  case "blockquote": {
1227
2208
  const startY = state.y;
1228
2209
  for (const child of node.content || []) {
1229
- processInlineContent(state, child.content, 12, 16);
2210
+ processInlineContent(state, child.content, 12, 16, region);
1230
2211
  }
1231
2212
  const endY = state.y;
1232
- const barX = MARGIN + 4;
2213
+ const barX = region.leftX + 4;
1233
2214
  const barTop = PAGE_HEIGHT - MARGIN - startY;
1234
2215
  const barBottom = PAGE_HEIGHT - MARGIN - endY;
1235
2216
  state.currentPage.drawLine({
@@ -1242,16 +2223,16 @@ function processBlock(state, node) {
1242
2223
  break;
1243
2224
  }
1244
2225
  case "horizontalRule": {
1245
- ensureSpace(state, 16);
1246
- state.y += 8;
2226
+ ensureSpace(state, 10);
2227
+ state.y += 4;
1247
2228
  const ruleY = PAGE_HEIGHT - MARGIN - state.y;
1248
2229
  state.currentPage.drawLine({
1249
- start: { x: MARGIN, y: ruleY },
1250
- end: { x: PAGE_WIDTH - MARGIN, y: ruleY },
1251
- thickness: 1,
1252
- color: rgb(0.7, 0.7, 0.7)
2230
+ start: { x: region.leftX, y: ruleY },
2231
+ end: { x: region.leftX + region.width, y: ruleY },
2232
+ thickness: 2.5,
2233
+ color: state.theme.rule
1253
2234
  });
1254
- state.y += 8;
2235
+ state.y += 4;
1255
2236
  break;
1256
2237
  }
1257
2238
  case "codeBlock": {
@@ -1263,9 +2244,9 @@ function processBlock(state, node) {
1263
2244
  ensureSpace(state, blockHeight);
1264
2245
  const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
1265
2246
  state.currentPage.drawRectangle({
1266
- x: MARGIN,
2247
+ x: region.leftX,
1267
2248
  y: boxY,
1268
- width: CONTENT_WIDTH,
2249
+ width: region.width,
1269
2250
  height: blockHeight,
1270
2251
  color: rgb(0.95, 0.95, 0.95),
1271
2252
  borderColor: rgb(0.85, 0.85, 0.85),
@@ -1273,18 +2254,325 @@ function processBlock(state, node) {
1273
2254
  });
1274
2255
  state.y += 8;
1275
2256
  for (const line of lines) {
1276
- drawText(state, line || " ", state.fonts.regular, fontSize, 8);
2257
+ drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
1277
2258
  }
1278
2259
  state.y += 8;
1279
2260
  break;
1280
2261
  }
2262
+ case "table": {
2263
+ renderTable(state, node, region);
2264
+ break;
2265
+ }
2266
+ case "imageNode": {
2267
+ const src = node.attrs?.src || "";
2268
+ const image = src ? state.images.get(src) : void 0;
2269
+ if (!image) break;
2270
+ const natW = image.width;
2271
+ const natH = image.height;
2272
+ let w = parseFloat(node.attrs?.width || "") || 0;
2273
+ let h = parseFloat(node.attrs?.height || "") || 0;
2274
+ if (w && !h) h = natH / natW * w;
2275
+ else if (h && !w) w = natW / natH * h;
2276
+ else if (!w && !h) {
2277
+ w = natW;
2278
+ h = natH;
2279
+ }
2280
+ if (w > region.width) {
2281
+ h = h * (region.width / w);
2282
+ w = region.width;
2283
+ }
2284
+ const maxH = CONTENT_HEIGHT * 0.6;
2285
+ if (h > maxH) {
2286
+ w = w * (maxH / h);
2287
+ h = maxH;
2288
+ }
2289
+ ensureSpace(state, h + 4);
2290
+ const align = node.attrs?.align || "left";
2291
+ let x = region.leftX;
2292
+ if (align === "center") x = region.leftX + (region.width - w) / 2;
2293
+ else if (align === "right") x = region.leftX + region.width - w;
2294
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
2295
+ state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
2296
+ state.y += h + 4;
2297
+ break;
2298
+ }
2299
+ // ─── Layout directives ─────────────────────────────────────────
2300
+ case "panel": {
2301
+ const title = node.attrs?.title || "";
2302
+ const border = node.attrs?.border || "solid";
2303
+ const headerStyle = node.attrs?.headerStyle || "";
2304
+ const padding = 8;
2305
+ const titleFontSize = 10;
2306
+ const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
2307
+ const panelStartY = state.y;
2308
+ const panelStartPage = state.pageIndex;
2309
+ if (title) {
2310
+ state.y += titleHeight;
2311
+ }
2312
+ const isHeaderOnly = border === "none";
2313
+ if (!isHeaderOnly) {
2314
+ state.y += padding;
2315
+ }
2316
+ const innerRegion = {
2317
+ leftX: region.leftX + padding,
2318
+ width: region.width - padding * 2
2319
+ };
2320
+ if (!isHeaderOnly) {
2321
+ for (const child of node.content || []) {
2322
+ processBlock(state, child, innerRegion);
2323
+ }
2324
+ state.y += padding;
2325
+ }
2326
+ const startPage = state.pdfDoc.getPages()[panelStartPage];
2327
+ if (title) {
2328
+ const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
2329
+ const isDarkHeader = headerStyle === "dark";
2330
+ startPage.drawRectangle({
2331
+ x: region.leftX,
2332
+ y: titleBarY,
2333
+ width: region.width,
2334
+ height: titleHeight,
2335
+ color: isDarkHeader ? state.theme.panelDarkHeaderBg : rgb(0.93, 0.93, 0.93),
2336
+ borderColor: state.theme.panelBorder,
2337
+ borderWidth: 0.75
2338
+ });
2339
+ startPage.drawText(title, {
2340
+ x: region.leftX + padding,
2341
+ y: titleBarY + 4,
2342
+ size: titleFontSize,
2343
+ font: state.fonts.bold,
2344
+ color: isDarkHeader ? state.theme.panelDarkHeaderText : rgb(0, 0, 0)
2345
+ });
2346
+ }
2347
+ if (border !== "none") {
2348
+ const borderDashArray = border === "dashed" ? [4, 2] : void 0;
2349
+ if (panelStartPage === state.pageIndex) {
2350
+ const borderY = PAGE_HEIGHT - MARGIN - state.y;
2351
+ const panelHeight = state.y - panelStartY;
2352
+ startPage.drawRectangle({
2353
+ x: region.leftX,
2354
+ y: borderY,
2355
+ width: region.width,
2356
+ height: panelHeight,
2357
+ borderColor: state.theme.panelBorder,
2358
+ borderWidth: 0.75,
2359
+ borderDashArray
2360
+ });
2361
+ } else {
2362
+ const startBorderBottom = MARGIN;
2363
+ const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
2364
+ startPage.drawRectangle({
2365
+ x: region.leftX,
2366
+ y: startBorderBottom,
2367
+ width: region.width,
2368
+ height: startPanelHeight,
2369
+ borderColor: state.theme.panelBorder,
2370
+ borderWidth: 0.75,
2371
+ borderDashArray
2372
+ });
2373
+ const contTop = PAGE_HEIGHT - MARGIN;
2374
+ const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
2375
+ const contHeight = contTop - contBorderY;
2376
+ state.currentPage.drawRectangle({
2377
+ x: region.leftX,
2378
+ y: contBorderY,
2379
+ width: region.width,
2380
+ height: contHeight,
2381
+ borderColor: state.theme.panelBorder,
2382
+ borderWidth: 0.75,
2383
+ borderDashArray
2384
+ });
2385
+ }
2386
+ }
2387
+ state.y += 4;
2388
+ break;
2389
+ }
2390
+ case "columns": {
2391
+ const split = parseInt(node.attrs?.split || "50", 10);
2392
+ const padX = parseFloat(node.attrs?.padX || "0") || 0;
2393
+ const columns = (node.content || []).filter((c) => c.type === "column");
2394
+ if (columns.length === 0) break;
2395
+ const colRegionLeftX = region.leftX + padX;
2396
+ const colRegionWidth = region.width - 2 * padX;
2397
+ const gap = 12;
2398
+ const columnsStartY = state.y;
2399
+ const columnsStartPage = state.pageIndex;
2400
+ let colWidths;
2401
+ if (columns.length === 2) {
2402
+ const firstWidth = (colRegionWidth - gap) * (split / 100);
2403
+ const secondWidth = colRegionWidth - gap - firstWidth;
2404
+ colWidths = [firstWidth, secondWidth];
2405
+ } else {
2406
+ const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
2407
+ colWidths = columns.map(() => eachWidth);
2408
+ }
2409
+ let maxEndY = columnsStartY;
2410
+ let colX = colRegionLeftX;
2411
+ for (let ci = 0; ci < columns.length; ci++) {
2412
+ const col = columns[ci];
2413
+ const colWidth = colWidths[ci];
2414
+ const colRegion = { leftX: colX, width: colWidth };
2415
+ state.y = columnsStartY;
2416
+ state.pageIndex = columnsStartPage;
2417
+ state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
2418
+ const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
2419
+ if (padTop > 0) {
2420
+ state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
2421
+ }
2422
+ for (const child of col.content || []) {
2423
+ processBlock(state, child, colRegion);
2424
+ }
2425
+ maxEndY = Math.max(maxEndY, state.y);
2426
+ colX += colWidth + gap;
2427
+ }
2428
+ state.y = maxEndY;
2429
+ state.y += 4;
2430
+ break;
2431
+ }
2432
+ case "column": {
2433
+ for (const child of node.content || []) {
2434
+ processBlock(state, child, region);
2435
+ }
2436
+ break;
2437
+ }
2438
+ case "watermark": {
2439
+ break;
2440
+ }
2441
+ case "subtotalsBlock": {
2442
+ const stFontSize = BODY_FONT_SIZE;
2443
+ const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
2444
+ const valueRightX = region.leftX + region.width;
2445
+ const rows = (node.content || []).filter(
2446
+ (c) => c.type === "paragraph" && c.content?.length
2447
+ );
2448
+ const parsed = [];
2449
+ for (const row of rows) {
2450
+ let label = "";
2451
+ let value = "";
2452
+ for (const seg of row.content || []) {
2453
+ if (seg.type === "text") {
2454
+ const isBold = seg.marks?.some((m) => m.type === "bold");
2455
+ if (isBold) {
2456
+ label += seg.text || "";
2457
+ } else {
2458
+ const t = (seg.text || "").trim();
2459
+ if (t) value += (value ? " " : "") + t;
2460
+ }
2461
+ } else if (seg.type === "variableNode") {
2462
+ const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
2463
+ const isBoldVar = seg.marks?.some((m) => m.type === "bold");
2464
+ if (isBoldVar) {
2465
+ label += varLabel;
2466
+ } else {
2467
+ value += (value ? " " : "") + `[${varLabel}]`;
2468
+ }
2469
+ }
2470
+ }
2471
+ parsed.push({ label: label.trim(), value: value.trim() });
2472
+ }
2473
+ let maxLabelWidth = 0;
2474
+ for (const { label } of parsed) {
2475
+ if (label) {
2476
+ const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
2477
+ if (w > maxLabelWidth) maxLabelWidth = w;
2478
+ }
2479
+ }
2480
+ const gap = 12;
2481
+ const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
2482
+ const valueColStartX = labelX + maxLabelWidth + gap;
2483
+ const isLastRowTotal = parsed.length > 1;
2484
+ for (let ri = 0; ri < parsed.length; ri++) {
2485
+ const isTotal = isLastRowTotal && ri === parsed.length - 1;
2486
+ if (isTotal) {
2487
+ state.y += stLineHeight * 0.6;
2488
+ }
2489
+ ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
2490
+ const { label, value } = parsed[ri];
2491
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
2492
+ if (isTotal) {
2493
+ state.currentPage.drawLine({
2494
+ start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
2495
+ end: { x: valueRightX, y: pdfY + stFontSize + 6 },
2496
+ thickness: 0.75,
2497
+ color: rgb(0, 0, 0)
2498
+ });
2499
+ state.currentPage.drawLine({
2500
+ start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
2501
+ end: { x: valueRightX, y: pdfY + stFontSize + 3 },
2502
+ thickness: 0.75,
2503
+ color: rgb(0, 0, 0)
2504
+ });
2505
+ }
2506
+ if (label) {
2507
+ state.currentPage.drawText(label, {
2508
+ x: labelX,
2509
+ y: pdfY,
2510
+ size: stFontSize,
2511
+ font: state.fonts.bold,
2512
+ color: rgb(0, 0, 0)
2513
+ });
2514
+ }
2515
+ if (value) {
2516
+ const valueFont = state.fonts.bold;
2517
+ const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
2518
+ state.currentPage.drawText(value, {
2519
+ x: valueRightX - valueWidth,
2520
+ y: pdfY,
2521
+ size: stFontSize,
2522
+ font: valueFont,
2523
+ color: rgb(0, 0, 0)
2524
+ });
2525
+ }
2526
+ state.y += stLineHeight;
2527
+ }
2528
+ state.y += 4;
2529
+ break;
2530
+ }
2531
+ case "repeatBlock":
2532
+ if (node.content) {
2533
+ for (const child of node.content) {
2534
+ processBlock(state, child, region);
2535
+ }
2536
+ }
2537
+ break;
1281
2538
  default:
1282
2539
  if (node.content) {
1283
- processInlineContent(state, node.content, 12);
2540
+ processInlineContent(state, node.content, 12, 0, region);
1284
2541
  }
1285
2542
  break;
1286
2543
  }
1287
2544
  }
2545
+ function renderWatermarksOnPage(page, fonts, watermarkNodes) {
2546
+ for (const wmNode of watermarkNodes) {
2547
+ const text = wmNode.attrs?.text || "";
2548
+ if (!text) continue;
2549
+ const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
2550
+ const angle = parseFloat(wmNode.attrs?.angle || "-45");
2551
+ const fontSize = 100;
2552
+ const { width, height } = page.getSize();
2553
+ const centerX = width / 2;
2554
+ const centerY = height / 2;
2555
+ const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
2556
+ page.drawText(text, {
2557
+ x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
2558
+ y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
2559
+ size: fontSize,
2560
+ font: fonts.bold,
2561
+ color: rgb(0.5, 0.5, 0.5),
2562
+ opacity,
2563
+ rotate: degrees(angle)
2564
+ });
2565
+ }
2566
+ }
2567
+ function collectWatermarkNodes(content) {
2568
+ const nodes = [];
2569
+ for (const block of content.content || []) {
2570
+ if (block.type === "watermark") {
2571
+ nodes.push(block);
2572
+ }
2573
+ }
2574
+ return nodes;
2575
+ }
1288
2576
  function ensureFieldSuffix(fieldName, suffix) {
1289
2577
  const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
1290
2578
  return `${cleanName}${suffix}`;
@@ -1394,6 +2682,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1394
2682
  PDFString$1.of(field.label)
1395
2683
  );
1396
2684
  }
2685
+ const initialsFontSize = Math.min(
2686
+ 72,
2687
+ Math.max(8, Math.round(position.height * 0.55))
2688
+ );
2689
+ try {
2690
+ initialsField.setFontSize(initialsFontSize);
2691
+ const acro = initialsField;
2692
+ const daRef = acro.acroField.dict.get(PDFName$1.of("DA"));
2693
+ if (daRef) {
2694
+ const daStr = daRef.value;
2695
+ if (typeof daStr === "string") {
2696
+ const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
2697
+ acro.acroField.dict.set(PDFName$1.of("DA"), PDFString$1.of(newDa));
2698
+ }
2699
+ }
2700
+ } catch (fontError) {
2701
+ console.warn(`Failed to set initials font:`, fontError);
2702
+ }
1397
2703
  initialsField.enableReadOnly();
1398
2704
  if (field.required) initialsField.enableRequired();
1399
2705
  break;
@@ -1497,47 +2803,367 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1497
2803
  } catch (err) {
1498
2804
  warnings.push(`Failed to create ${field.type} field "${field.name}": ${getErrorMessage(err)}`);
1499
2805
  }
1500
- }
1501
- initPdfMetadata({ PDFName: PDFName$1, PDFString: PDFString$1 });
1502
- const metadata = buildMetadataObject(fields, actualFieldNames);
1503
- setSigniphiMetadata(pdfDoc, metadata);
1504
- return warnings;
1505
- }
1506
- async function generatePdfFromContent(content, options = {}) {
1507
- const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
1508
- const pdfDoc = await PDFDocument.create();
1509
- const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
1510
- const fonts = {
1511
- regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
1512
- bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
1513
- italic: await pdfDoc.embedFont(StandardFonts.HelveticaOblique),
1514
- boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
1515
- };
1516
- const state = {
1517
- currentPage: firstPage,
1518
- pageIndex: 0,
1519
- y: 0,
1520
- fonts,
1521
- pdfDoc,
1522
- fieldPositions: /* @__PURE__ */ new Map(),
1523
- drawFieldPlaceholders
1524
- };
1525
- if (content.content) {
1526
- for (const block of content.content) {
1527
- processBlock(state, block);
2806
+ }
2807
+ initPdfMetadata({ PDFName: PDFName$1, PDFString: PDFString$1 });
2808
+ const metadata = buildMetadataObject(fields, actualFieldNames);
2809
+ setSigniphiMetadata(pdfDoc, metadata);
2810
+ return warnings;
2811
+ }
2812
+ async function generatePdfFromContent(content, options = {}) {
2813
+ const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
2814
+ const theme = resolveTheme(options.theme);
2815
+ const pdfDoc = await PDFDocument.create();
2816
+ const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
2817
+ const fonts = {
2818
+ regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
2819
+ bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
2820
+ italic: await pdfDoc.embedFont(StandardFonts.HelveticaOblique),
2821
+ boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
2822
+ };
2823
+ const watermarkNodes = collectWatermarkNodes(content);
2824
+ const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
2825
+ const state = {
2826
+ currentPage: firstPage,
2827
+ pageIndex: 0,
2828
+ y: 0,
2829
+ fonts,
2830
+ pdfDoc,
2831
+ fieldPositions: /* @__PURE__ */ new Map(),
2832
+ drawFieldPlaceholders,
2833
+ watermarkNodes,
2834
+ theme,
2835
+ images
2836
+ };
2837
+ renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
2838
+ if (content.content) {
2839
+ for (const block of content.content) {
2840
+ processBlock(state, block);
2841
+ }
2842
+ }
2843
+ const allPages = pdfDoc.getPages();
2844
+ const totalPages = allPages.length;
2845
+ const pageNumFontSize = 9;
2846
+ const hfFontSize = 8;
2847
+ const hfColor = rgb(0.45, 0.45, 0.45);
2848
+ for (let i = 0; i < totalPages; i++) {
2849
+ const page = allPages[i];
2850
+ const label = `Page ${i + 1} of ${totalPages}`;
2851
+ const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
2852
+ page.drawText(label, {
2853
+ x: PAGE_WIDTH - MARGIN - labelWidth,
2854
+ y: MARGIN / 2 - pageNumFontSize / 2,
2855
+ size: pageNumFontSize,
2856
+ font: fonts.bold,
2857
+ color: rgb(0, 0, 0)
2858
+ });
2859
+ if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
2860
+ const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
2861
+ if (header.left) {
2862
+ page.drawText(header.left, {
2863
+ x: MARGIN,
2864
+ y: headerBaseY,
2865
+ size: pageNumFontSize,
2866
+ font: fonts.bold,
2867
+ color: theme.pageHeaderText
2868
+ });
2869
+ }
2870
+ if (header.right) {
2871
+ const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
2872
+ page.drawText(header.right, {
2873
+ x: PAGE_WIDTH - MARGIN - rightWidth,
2874
+ y: headerBaseY,
2875
+ size: hfFontSize,
2876
+ font: fonts.regular,
2877
+ color: hfColor
2878
+ });
2879
+ }
2880
+ page.drawLine({
2881
+ start: { x: MARGIN, y: headerBaseY - 6 },
2882
+ end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
2883
+ thickness: 0.75,
2884
+ color: theme.pageHeaderRule
2885
+ });
2886
+ }
2887
+ if (footer?.left) {
2888
+ page.drawText(footer.left, {
2889
+ x: MARGIN,
2890
+ y: MARGIN / 2 - pageNumFontSize / 2,
2891
+ size: hfFontSize,
2892
+ font: fonts.regular,
2893
+ color: hfColor
2894
+ });
2895
+ }
2896
+ }
2897
+ let fieldWarnings;
2898
+ if (embedFormFields && fields.length > 0) {
2899
+ const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
2900
+ if (warnings.length > 0) fieldWarnings = warnings;
2901
+ }
2902
+ const pdfBytes = new Uint8Array(await pdfDoc.save({ useObjectStreams: false }));
2903
+ return {
2904
+ pdfBytes,
2905
+ fieldPositions: state.fieldPositions,
2906
+ fieldWarnings,
2907
+ imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
2908
+ };
2909
+ }
2910
+
2911
+ // src/utils/template-pipeline.ts
2912
+ async function generatePdfFromTiptap(content, values, options = {}) {
2913
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2914
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
2915
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
2916
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2917
+ const fields = extractFieldsFromContent(replaced);
2918
+ const result = await generatePdfFromContent(replaced, {
2919
+ embedFormFields: fields.length > 0,
2920
+ fields,
2921
+ theme: options.theme,
2922
+ header: options.header,
2923
+ footer: options.footer,
2924
+ drawFieldPlaceholders: options.drawFieldPlaceholders
2925
+ });
2926
+ return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
2927
+ }
2928
+ async function generatePdfFromMarkdown(markdown, values, options = {}) {
2929
+ const content = markdownToTiptap(markdown);
2930
+ return generatePdfFromTiptap(content, values, options);
2931
+ }
2932
+
2933
+ // src/utils/markdown-writer.ts
2934
+ function serializeFieldToken(attrs) {
2935
+ const parts = ["field"];
2936
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2937
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2938
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2939
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2940
+ if (attrs.required) parts.push(`required:true`);
2941
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2942
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2943
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2944
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2945
+ if (attrs.multiline) parts.push(`multiline:true`);
2946
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2947
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2948
+ return `{{${parts.join("|")}}}`;
2949
+ }
2950
+ function serializeImageNode(attrs) {
2951
+ const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
2952
+ if (!hasExtras && attrs.src) {
2953
+ return `![${attrs.alt || ""}](${attrs.src})`;
2954
+ }
2955
+ const parts = ["image"];
2956
+ if (attrs.varName) parts.push(`var:${attrs.varName}`);
2957
+ else if (attrs.src) parts.push(`src:${attrs.src}`);
2958
+ if (attrs.alt) parts.push(`alt:${attrs.alt}`);
2959
+ if (attrs.width) parts.push(`width:${attrs.width}`);
2960
+ if (attrs.height) parts.push(`height:${attrs.height}`);
2961
+ if (attrs.align) parts.push(`align:${attrs.align}`);
2962
+ return `{{${parts.join("|")}}}`;
2963
+ }
2964
+ function serializeVariableToken(attrs) {
2965
+ const parts = ["var"];
2966
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2967
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2968
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2969
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2970
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2971
+ if (attrs.block === "true") parts.push(`block:true`);
2972
+ return `{{${parts.join("|")}}}`;
2973
+ }
2974
+ function serializeInline(content) {
2975
+ if (!content) return "";
2976
+ let result = "";
2977
+ for (const node of content) {
2978
+ if (node.type === "fieldNode") {
2979
+ result += serializeFieldToken(node.attrs || {});
2980
+ continue;
2981
+ }
2982
+ if (node.type === "imageNode") {
2983
+ result += serializeImageNode(node.attrs || {});
2984
+ continue;
2985
+ }
2986
+ if (node.type === "variableNode") {
2987
+ let token = serializeVariableToken(node.attrs || {});
2988
+ const varMarks = node.marks || [];
2989
+ const isBold = varMarks.some((m) => m.type === "bold");
2990
+ const isItalic = varMarks.some((m) => m.type === "italic");
2991
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2992
+ if (isBold && isItalic) token = `***${token}***`;
2993
+ else if (isBold) token = `**${token}**`;
2994
+ else if (isItalic) token = `*${token}*`;
2995
+ if (isUnderline) token = `__${token}__`;
2996
+ result += token;
2997
+ continue;
2998
+ }
2999
+ if (node.type === "text") {
3000
+ let text = node.text || "";
3001
+ const marks = node.marks || [];
3002
+ for (const mark of marks) {
3003
+ if (mark.type === "bold") text = `**${text}**`;
3004
+ else if (mark.type === "italic") text = `*${text}*`;
3005
+ else if (mark.type === "underline") text = `__${text}__`;
3006
+ else if (mark.type === "code") text = `\`${text}\``;
3007
+ }
3008
+ result += text;
3009
+ }
3010
+ if (node.type === "hardBreak") {
3011
+ result += " \n";
3012
+ }
3013
+ }
3014
+ return result;
3015
+ }
3016
+ function serializeBlock(node) {
3017
+ switch (node.type) {
3018
+ case "heading": {
3019
+ const level = node.attrs?.level || 1;
3020
+ const prefix = "#".repeat(level);
3021
+ return `${prefix} ${serializeInline(node.content)}`;
3022
+ }
3023
+ case "paragraph": {
3024
+ const inline = serializeInline(node.content);
3025
+ return inline;
3026
+ }
3027
+ case "bulletList": {
3028
+ return (node.content || []).map((item) => {
3029
+ const inner = (item.content || []).map(serializeBlock).join("\n");
3030
+ return `- ${inner}`;
3031
+ }).join("\n");
3032
+ }
3033
+ case "orderedList": {
3034
+ return (node.content || []).map((item, i) => {
3035
+ const inner = (item.content || []).map(serializeBlock).join("\n");
3036
+ return `${i + 1}. ${inner}`;
3037
+ }).join("\n");
3038
+ }
3039
+ case "listItem": {
3040
+ return (node.content || []).map(serializeBlock).join("\n");
3041
+ }
3042
+ case "blockquote": {
3043
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
3044
+ }
3045
+ case "codeBlock": {
3046
+ const text = serializeInline(node.content);
3047
+ return `\`\`\`
3048
+ ${text}
3049
+ \`\`\``;
3050
+ }
3051
+ case "horizontalRule": {
3052
+ return "---";
3053
+ }
3054
+ case "table": {
3055
+ const rows = node.content || [];
3056
+ const isBorderless = node.attrs?.borderless === true;
3057
+ const isSubtotals = node.attrs?.subtotals === true;
3058
+ const serializedRows = [];
3059
+ if (isBorderless && rows.length > 0) {
3060
+ const colCount = rows[0].content?.length || 0;
3061
+ const markerCell = isSubtotals ? "_" : "";
3062
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
3063
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
3064
+ serializedRows.push(markerRow);
3065
+ serializedRows.push(sep);
3066
+ }
3067
+ for (let ri = 0; ri < rows.length; ri++) {
3068
+ const row = rows[ri];
3069
+ const cells = (row.content || []).map((cell) => {
3070
+ const inner = (cell.content || []).map(serializeBlock).join("");
3071
+ return inner;
3072
+ });
3073
+ serializedRows.push(`| ${cells.join(" | ")} |`);
3074
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
3075
+ const sep = cells.map(() => "------").join("|");
3076
+ serializedRows.push(`|${sep}|`);
3077
+ }
3078
+ }
3079
+ return serializedRows.join("\n");
3080
+ }
3081
+ case "tableRow": {
3082
+ const cells = (node.content || []).map((cell) => {
3083
+ const inner = (cell.content || []).map(serializeBlock).join("");
3084
+ return inner;
3085
+ });
3086
+ return `| ${cells.join(" | ")} |`;
3087
+ }
3088
+ case "tableHeader":
3089
+ case "tableCell": {
3090
+ return (node.content || []).map(serializeBlock).join("");
3091
+ }
3092
+ case "imageNode": {
3093
+ return serializeImageNode(node.attrs || {});
3094
+ }
3095
+ case "watermark": {
3096
+ const attrs = node.attrs || {};
3097
+ const parts = [];
3098
+ if (attrs.text) parts.push(`text:${attrs.text}`);
3099
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
3100
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
3101
+ return `:::watermark{${parts.join("|")}}`;
3102
+ }
3103
+ case "panel": {
3104
+ const attrs = node.attrs || {};
3105
+ const parts = [];
3106
+ if (attrs.title) parts.push(`title:${attrs.title}`);
3107
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
3108
+ else if (attrs.border === "solid") parts.push(`border:solid`);
3109
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
3110
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3111
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3112
+ return `:::panel${attrStr}
3113
+ ${children}
3114
+ :::`;
3115
+ }
3116
+ case "repeatBlock": {
3117
+ const attrs = node.attrs || {};
3118
+ const parts = [];
3119
+ if (attrs.data) parts.push(`data:${attrs.data}`);
3120
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3121
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3122
+ return `:::repeat${attrStr}
3123
+ ${children}
3124
+ :::`;
3125
+ }
3126
+ case "subtotalsBlock": {
3127
+ const children = (node.content || []).map(serializeBlock).join("\n");
3128
+ return `:::subtotals
3129
+ ${children}
3130
+ :::`;
3131
+ }
3132
+ case "columns": {
3133
+ const attrs = node.attrs || {};
3134
+ const splitVal = attrs.split || "50";
3135
+ const cols = (node.content || []).filter((c) => c.type === "column");
3136
+ const colBlocks = cols.map((col) => {
3137
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
3138
+ const colAttrs = col.attrs || {};
3139
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3140
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3141
+ return `${colTag}
3142
+ ${children}
3143
+ :::`;
3144
+ });
3145
+ const padX = parseFloat(attrs.padX || "0") || 0;
3146
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
3147
+ return `:::columns{${colsAttrs}}
3148
+ ${colBlocks.join("\n")}
3149
+ :::`;
3150
+ }
3151
+ case "column": {
3152
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3153
+ const colAttrs = node.attrs || {};
3154
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3155
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3156
+ return `${colTag}
3157
+ ${children}
3158
+ :::`;
1528
3159
  }
3160
+ default:
3161
+ return serializeInline(node.content);
1529
3162
  }
1530
- let fieldWarnings;
1531
- if (embedFormFields && fields.length > 0) {
1532
- const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
1533
- if (warnings.length > 0) fieldWarnings = warnings;
1534
- }
1535
- const pdfBytes = await pdfDoc.save({ useObjectStreams: false });
1536
- return {
1537
- pdfBytes,
1538
- fieldPositions: state.fieldPositions,
1539
- fieldWarnings
1540
- };
3163
+ }
3164
+ function tiptapToMarkdown(doc) {
3165
+ if (!doc.content) return "";
3166
+ return doc.content.map(serializeBlock).join("\n\n");
1541
3167
  }
1542
3168
 
1543
3169
  // src/utils/pdf-preview.ts
@@ -1626,8 +3252,27 @@ function useDocumentGenerator(options = {}) {
1626
3252
  Placeholder.configure({
1627
3253
  placeholder: options.placeholder || "Start writing your document..."
1628
3254
  }),
3255
+ Table.extend({
3256
+ addAttributes() {
3257
+ return {
3258
+ ...this.parent?.(),
3259
+ borderless: { default: false },
3260
+ subtotals: { default: false }
3261
+ };
3262
+ }
3263
+ }).configure({ resizable: false }),
3264
+ TableRow,
3265
+ TableCell,
3266
+ TableHeader,
1629
3267
  FieldNode,
1630
- VariableNode
3268
+ VariableNode,
3269
+ PanelNode,
3270
+ ColumnsNode,
3271
+ ColumnNode,
3272
+ WatermarkNode,
3273
+ ImageNode,
3274
+ RepeatNode,
3275
+ SubtotalsNode
1631
3276
  ],
1632
3277
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1633
3278
  onUpdate: ({ editor: ed }) => {
@@ -1731,10 +3376,9 @@ function useDocumentGenerator(options = {}) {
1731
3376
  async (values) => {
1732
3377
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1733
3378
  const content = editor.getJSON();
1734
- const replacedContent = replaceVariablesInContent(content, values);
1735
- const result = await generatePdfFromContent(replacedContent);
1736
- const pages = await pdfToImages(result.pdfBytes);
1737
- return { pdfBytes: result.pdfBytes, pdfPages: pages };
3379
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
3380
+ const pdfPages2 = await pdfToImages(pdfBytes2);
3381
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1738
3382
  },
1739
3383
  [editor]
1740
3384
  );
@@ -2888,15 +4532,47 @@ function FieldEditPopover({
2888
4532
  ] }) })
2889
4533
  ] });
2890
4534
  }
2891
- function labelToVarName(label) {
4535
+ var sizeClasses = {
4536
+ md: "px-3 py-1.5 text-xs font-medium",
4537
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4538
+ };
4539
+ function ToggleGroup({
4540
+ value,
4541
+ onChange,
4542
+ options,
4543
+ size = "md",
4544
+ className
4545
+ }) {
4546
+ return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsx(
4547
+ "button",
4548
+ {
4549
+ className: cn(
4550
+ "rounded-md transition-colors",
4551
+ sizeClasses[size],
4552
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4553
+ ),
4554
+ onClick: () => onChange(opt.value),
4555
+ children: opt.label
4556
+ },
4557
+ opt.value
4558
+ )) });
4559
+ }
4560
+ function labelToVarName2(label) {
2892
4561
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2893
4562
  }
4563
+ function varNameToLabel(name) {
4564
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4565
+ }
2894
4566
  function VariableInsertPopover({
2895
4567
  open,
2896
4568
  onOpenChange,
2897
4569
  onInsert,
4570
+ predefinedVariables,
2898
4571
  children
2899
4572
  }) {
4573
+ const hasPredefined = !!predefinedVariables?.length;
4574
+ const [mode, setMode] = useState(hasPredefined ? "existing" : "new");
4575
+ const [search, setSearch] = useState("");
2900
4576
  const [varLabel, setVarLabel] = useState("");
2901
4577
  const [varName, setVarName] = useState("");
2902
4578
  const [varDefault, setVarDefault] = useState("");
@@ -2906,7 +4582,9 @@ function VariableInsertPopover({
2906
4582
  setVarName("");
2907
4583
  setVarDefault("");
2908
4584
  setNameManuallyEdited(false);
2909
- }, []);
4585
+ setSearch("");
4586
+ setMode(hasPredefined ? "existing" : "new");
4587
+ }, [hasPredefined]);
2910
4588
  const handleOpenChange = useCallback(
2911
4589
  (nextOpen) => {
2912
4590
  if (!nextOpen) reset();
@@ -2916,7 +4594,7 @@ function VariableInsertPopover({
2916
4594
  );
2917
4595
  useEffect(() => {
2918
4596
  if (!nameManuallyEdited) {
2919
- setVarName(labelToVarName(varLabel));
4597
+ setVarName(labelToVarName2(varLabel));
2920
4598
  }
2921
4599
  }, [varLabel, nameManuallyEdited]);
2922
4600
  const handleInsert = useCallback(() => {
@@ -2924,10 +4602,32 @@ function VariableInsertPopover({
2924
4602
  onInsert({
2925
4603
  varName: varName.trim(),
2926
4604
  varLabel: varLabel.trim(),
2927
- varDefault: varDefault.trim()
4605
+ varDefault: varDefault.trim(),
4606
+ format: ""
2928
4607
  });
2929
4608
  handleOpenChange(false);
2930
4609
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4610
+ const handlePickPredefined = useCallback(
4611
+ (pv) => {
4612
+ onInsert({
4613
+ varName: pv.varName,
4614
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4615
+ varDefault: "",
4616
+ format: ""
4617
+ });
4618
+ handleOpenChange(false);
4619
+ },
4620
+ [onInsert, handleOpenChange]
4621
+ );
4622
+ const filteredVars = useMemo(() => {
4623
+ if (!predefinedVariables?.length) return [];
4624
+ const q = search.toLowerCase();
4625
+ if (!q) return predefinedVariables;
4626
+ return predefinedVariables.filter((pv) => {
4627
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4628
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4629
+ });
4630
+ }, [predefinedVariables, search]);
2931
4631
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2932
4632
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
2933
4633
  /* @__PURE__ */ jsx(PopoverContent, { className: "w-72 p-0", align: "start", sideOffset: 8, children: /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
@@ -2935,72 +4635,114 @@ function VariableInsertPopover({
2935
4635
  /* @__PURE__ */ jsx(Braces, { size: 14 }),
2936
4636
  /* @__PURE__ */ jsx("span", { children: "Insert Variable" })
2937
4637
  ] }),
2938
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2939
- /* @__PURE__ */ jsxs("div", { children: [
2940
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4638
+ hasPredefined && /* @__PURE__ */ jsx(
4639
+ ToggleGroup,
4640
+ {
4641
+ value: mode,
4642
+ onChange: setMode,
4643
+ options: [
4644
+ { value: "existing", label: "Existing" },
4645
+ { value: "new", label: "New" }
4646
+ ],
4647
+ size: "sm"
4648
+ }
4649
+ ),
4650
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4651
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
4652
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2941
4653
  /* @__PURE__ */ jsx(
2942
4654
  Input,
2943
4655
  {
2944
- id: "var-label",
2945
- value: varLabel,
2946
- onChange: (e) => setVarLabel(e.target.value),
2947
- placeholder: "e.g. Company Name",
2948
- className: "h-8 text-xs"
4656
+ value: search,
4657
+ onChange: (e) => setSearch(e.target.value),
4658
+ placeholder: "Search variables...",
4659
+ className: "h-8 text-xs pl-7"
2949
4660
  }
2950
4661
  )
2951
4662
  ] }),
2952
- /* @__PURE__ */ jsxs("div", { children: [
2953
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4663
+ /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto -mx-1", children: filteredVars.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground px-2 py-3 text-center", children: "No variables found" }) : filteredVars.map((pv) => {
4664
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4665
+ return /* @__PURE__ */ jsxs(
4666
+ "button",
4667
+ {
4668
+ className: "w-full text-left px-2 py-1.5 rounded-md text-xs hover:bg-accent transition-colors flex flex-col gap-0.5",
4669
+ onClick: () => handlePickPredefined(pv),
4670
+ children: [
4671
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
4672
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4673
+ ]
4674
+ },
4675
+ pv.varName
4676
+ );
4677
+ }) })
4678
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4679
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4680
+ /* @__PURE__ */ jsxs("div", { children: [
4681
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4682
+ /* @__PURE__ */ jsx(
4683
+ Input,
4684
+ {
4685
+ id: "var-label",
4686
+ value: varLabel,
4687
+ onChange: (e) => setVarLabel(e.target.value),
4688
+ placeholder: "e.g. Company Name",
4689
+ className: "h-8 text-xs"
4690
+ }
4691
+ )
4692
+ ] }),
4693
+ /* @__PURE__ */ jsxs("div", { children: [
4694
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4695
+ /* @__PURE__ */ jsx(
4696
+ Input,
4697
+ {
4698
+ id: "var-name",
4699
+ value: varName,
4700
+ onChange: (e) => {
4701
+ setVarName(e.target.value);
4702
+ setNameManuallyEdited(true);
4703
+ },
4704
+ placeholder: "e.g. company_name",
4705
+ className: "h-8 text-xs font-mono"
4706
+ }
4707
+ ),
4708
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4709
+ ] }),
4710
+ /* @__PURE__ */ jsxs("div", { children: [
4711
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4712
+ /* @__PURE__ */ jsx(
4713
+ Input,
4714
+ {
4715
+ id: "var-default",
4716
+ value: varDefault,
4717
+ onChange: (e) => setVarDefault(e.target.value),
4718
+ placeholder: "Optional",
4719
+ className: "h-8 text-xs"
4720
+ }
4721
+ )
4722
+ ] })
4723
+ ] }),
4724
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2954
4725
  /* @__PURE__ */ jsx(
2955
- Input,
4726
+ Button,
2956
4727
  {
2957
- id: "var-name",
2958
- value: varName,
2959
- onChange: (e) => {
2960
- setVarName(e.target.value);
2961
- setNameManuallyEdited(true);
2962
- },
2963
- placeholder: "e.g. company_name",
2964
- className: "h-8 text-xs font-mono"
4728
+ size: "sm",
4729
+ className: "h-8 flex-1 text-xs",
4730
+ onClick: handleInsert,
4731
+ disabled: !varLabel.trim() || !varName.trim(),
4732
+ children: "Insert"
2965
4733
  }
2966
4734
  ),
2967
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
2968
- ] }),
2969
- /* @__PURE__ */ jsxs("div", { children: [
2970
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
2971
4735
  /* @__PURE__ */ jsx(
2972
- Input,
4736
+ Button,
2973
4737
  {
2974
- id: "var-default",
2975
- value: varDefault,
2976
- onChange: (e) => setVarDefault(e.target.value),
2977
- placeholder: "Optional",
2978
- className: "h-8 text-xs"
4738
+ variant: "outline",
4739
+ size: "sm",
4740
+ className: "h-8 text-xs",
4741
+ onClick: () => handleOpenChange(false),
4742
+ children: "Cancel"
2979
4743
  }
2980
4744
  )
2981
4745
  ] })
2982
- ] }),
2983
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
- /* @__PURE__ */ jsx(
2985
- Button,
2986
- {
2987
- size: "sm",
2988
- className: "h-8 flex-1 text-xs",
2989
- onClick: handleInsert,
2990
- disabled: !varLabel.trim() || !varName.trim(),
2991
- children: "Insert"
2992
- }
2993
- ),
2994
- /* @__PURE__ */ jsx(
2995
- Button,
2996
- {
2997
- variant: "outline",
2998
- size: "sm",
2999
- className: "h-8 text-xs",
3000
- onClick: () => handleOpenChange(false),
3001
- children: "Cancel"
3002
- }
3003
- )
3004
4746
  ] })
3005
4747
  ] }) })
3006
4748
  ] });
@@ -3222,7 +4964,6 @@ function PreviewPanel({
3222
4964
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3223
4965
  );
3224
4966
  }, [positionedFields, currentPage, currentPageIndex]);
3225
- const showScrollbars = zoomLevel > 1;
3226
4967
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
3227
4968
  /* @__PURE__ */ jsx("div", { className: "border-b border-border px-2 py-1.5 flex-shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
3228
4969
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3338,12 +5079,13 @@ function PreviewPanel({
3338
5079
  "div",
3339
5080
  {
3340
5081
  className: cn(
3341
- "border border-border rounded-lg bg-muted/30 shrink-0",
3342
- showScrollbars ? "overflow-auto" : "overflow-hidden"
5082
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3343
5083
  ),
3344
5084
  style: {
3345
5085
  width: pageDisplaySize.viewportWidth,
3346
- height: pageDisplaySize.viewportHeight
5086
+ height: pageDisplaySize.viewportHeight,
5087
+ maxWidth: "100%",
5088
+ maxHeight: "100%"
3347
5089
  },
3348
5090
  children: /* @__PURE__ */ jsxs(
3349
5091
  "div",
@@ -3414,8 +5156,8 @@ function validateMarkdown(markdown) {
3414
5156
  errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
3415
5157
  } else {
3416
5158
  const tokenType = tokenContent.slice(0, pipeIdx);
3417
- if (tokenType !== "field" && tokenType !== "var") {
3418
- errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "var")`);
5159
+ if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
5160
+ errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
3419
5161
  } else {
3420
5162
  const body = tokenContent.slice(pipeIdx + 1);
3421
5163
  const pairs = body.split("|");
@@ -3429,12 +5171,48 @@ function validateMarkdown(markdown) {
3429
5171
  if (!hasName) {
3430
5172
  errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
3431
5173
  }
5174
+ } else if (tokenType === "image") {
5175
+ const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
5176
+ if (!hasSource) {
5177
+ errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
5178
+ }
3432
5179
  }
3433
5180
  }
3434
5181
  }
3435
5182
  searchFrom = closeIdx + 2;
3436
5183
  }
3437
5184
  }
5185
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
5186
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
5187
+ const directiveStack = [];
5188
+ for (let i = 0; i < lines.length; i++) {
5189
+ const trimmed = lines[i].trim();
5190
+ const lineNum = i + 1;
5191
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
5192
+ if (openMatch) {
5193
+ const dtype = openMatch[1];
5194
+ if (dtype !== "watermark") {
5195
+ if (dtype === "col") {
5196
+ const parent = directiveStack[directiveStack.length - 1];
5197
+ if (!parent || parent.type !== "columns") {
5198
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
5199
+ }
5200
+ }
5201
+ directiveStack.push({ type: dtype, line: lineNum });
5202
+ }
5203
+ continue;
5204
+ }
5205
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
5206
+ if (directiveStack.length === 0) {
5207
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
5208
+ } else {
5209
+ directiveStack.pop();
5210
+ }
5211
+ }
5212
+ }
5213
+ for (const open of directiveStack) {
5214
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
5215
+ }
3438
5216
  let fields = [];
3439
5217
  let variables = [];
3440
5218
  try {
@@ -3468,31 +5246,6 @@ function validateMarkdown(markdown) {
3468
5246
  fields
3469
5247
  };
3470
5248
  }
3471
- var sizeClasses = {
3472
- md: "px-3 py-1.5 text-xs font-medium",
3473
- sm: "px-2 py-0.5 text-[10px] font-medium"
3474
- };
3475
- function ToggleGroup({
3476
- value,
3477
- onChange,
3478
- options,
3479
- size = "md",
3480
- className
3481
- }) {
3482
- return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsx(
3483
- "button",
3484
- {
3485
- className: cn(
3486
- "rounded-md transition-colors",
3487
- sizeClasses[size],
3488
- value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
3489
- ),
3490
- onClick: () => onChange(opt.value),
3491
- children: opt.label
3492
- },
3493
- opt.value
3494
- )) });
3495
- }
3496
5249
  function parseCsv(text) {
3497
5250
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3498
5251
  if (lines.length < 2) return [];
@@ -3542,7 +5295,10 @@ function GeneratePanel({
3542
5295
  editorContent,
3543
5296
  editorVariables,
3544
5297
  onGeneratePdf,
3545
- initialBulkData
5298
+ initialBulkData,
5299
+ initialVariableValues,
5300
+ exportFileName,
5301
+ templateMode
3546
5302
  }) {
3547
5303
  const [templateSource, setTemplateSource] = useState("editor");
3548
5304
  const [importedMarkdown, setImportedMarkdown] = useState(null);
@@ -3550,6 +5306,7 @@ function GeneratePanel({
3550
5306
  const [validationResult, setValidationResult] = useState(null);
3551
5307
  const [mode, setMode] = useState("single");
3552
5308
  const [variableValues, setVariableValues] = useState({});
5309
+ const [variableSearch, setVariableSearch] = useState("");
3553
5310
  const [bulkInputFormat, setBulkInputFormat] = useState("json");
3554
5311
  const [bulkInput, setBulkInput] = useState("");
3555
5312
  const [bulkData, setBulkData] = useState(null);
@@ -3576,11 +5333,22 @@ function GeneratePanel({
3576
5333
  const col = pos - textBefore.lastIndexOf("\n");
3577
5334
  setJsonCursor({ line, col });
3578
5335
  }, []);
5336
+ const lockedVarNames = React12__default.useMemo(
5337
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
5338
+ [templateMode, initialVariableValues]
5339
+ );
3579
5340
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3580
5341
  const hasVariables = activeVariables.length > 0;
3581
5342
  const hasEditorContent = editorMarkdown.trim().length > 0;
3582
5343
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3583
5344
  const hasContent = hasImportedContent || hasEditorContent;
5345
+ const filteredVariables = React12__default.useMemo(() => {
5346
+ if (!variableSearch.trim()) return activeVariables;
5347
+ const q = variableSearch.toLowerCase();
5348
+ return activeVariables.filter(
5349
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
5350
+ );
5351
+ }, [activeVariables, variableSearch]);
3584
5352
  const getActiveContent = useCallback(() => {
3585
5353
  if (templateSource === "imported" && importedMarkdown) {
3586
5354
  return markdownToTiptap(importedMarkdown);
@@ -3748,7 +5516,10 @@ function GeneratePanel({
3748
5516
  setPreviewError(null);
3749
5517
  try {
3750
5518
  const content = getActiveContent();
3751
- const replaced = replaceVariablesInContent(content, variableValues);
5519
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5520
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5521
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5522
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3752
5523
  const fields = extractFieldsFromContent(replaced);
3753
5524
  const result = await generatePdfFromContent(replaced);
3754
5525
  const pages = await pdfToImages(result.pdfBytes);
@@ -3769,7 +5540,10 @@ function GeneratePanel({
3769
5540
  setExportSuccess(null);
3770
5541
  try {
3771
5542
  const content = getActiveContent();
3772
- const replaced = replaceVariablesInContent(content, variableValues);
5543
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5544
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5545
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5546
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3773
5547
  const fields = extractFieldsFromContent(replaced);
3774
5548
  const result = await generatePdfFromContent(replaced, {
3775
5549
  drawFieldPlaceholders: false,
@@ -3777,7 +5551,7 @@ function GeneratePanel({
3777
5551
  fields
3778
5552
  });
3779
5553
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3780
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5554
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3781
5555
  if (onGeneratePdf) {
3782
5556
  onGeneratePdf(blob, fileName);
3783
5557
  } else {
@@ -3849,7 +5623,10 @@ function GeneratePanel({
3849
5623
  const allWarnings = [];
3850
5624
  for (let i = 0; i < bulkData.length; i++) {
3851
5625
  const values = bulkData[i];
3852
- const replaced = replaceVariablesInContent(content, values);
5626
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5627
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5628
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5629
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3853
5630
  const fields = extractFieldsFromContent(replaced);
3854
5631
  const result = await generatePdfFromContent(replaced, {
3855
5632
  drawFieldPlaceholders: false,
@@ -3904,13 +5681,15 @@ ${allWarnings.join("\n")}`);
3904
5681
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3905
5682
  React12__default.useEffect(() => {
3906
5683
  if (templateSource === "editor") {
3907
- const defaults = {};
5684
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3908
5685
  for (const v of editorVariables) {
3909
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5686
+ if (!defaults[v.varName]) {
5687
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5688
+ }
3910
5689
  }
3911
5690
  setVariableValues(defaults);
3912
5691
  }
3913
- }, [editorVariables, templateSource]);
5692
+ }, [editorVariables, templateSource, initialVariableValues]);
3914
5693
  useEffect(() => {
3915
5694
  setPreviewFresh(false);
3916
5695
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3922,8 +5701,8 @@ ${allWarnings.join("\n")}`);
3922
5701
  }, [exportSuccess]);
3923
5702
  return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3924
5703
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
3925
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
3926
- /* @__PURE__ */ jsxs("div", { children: [
5704
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5705
+ !templateMode && /* @__PURE__ */ jsxs("div", { children: [
3927
5706
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3928
5707
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3929
5708
  templateSource === "imported" && /* @__PURE__ */ jsxs(
@@ -4016,10 +5795,10 @@ ${allWarnings.join("\n")}`);
4016
5795
  !hasContent && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4017
5796
  /* @__PURE__ */ jsx(Braces, { size: 32, className: "mb-2 opacity-50" }),
4018
5797
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "No template content" }),
4019
- /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Import a .md template above or add content in the Editor tab." })
5798
+ /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: templateMode ? "No template content was provided." : "Import a .md template above or add content in the Editor tab." })
4020
5799
  ] }),
4021
5800
  hasVariables && hasContent && /* @__PURE__ */ jsxs("div", { className: "border border-border rounded-lg overflow-hidden", children: [
4022
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsx(
5801
+ !templateMode && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsx(
4023
5802
  ToggleGroup,
4024
5803
  {
4025
5804
  value: mode,
@@ -4031,20 +5810,53 @@ ${allWarnings.join("\n")}`);
4031
5810
  }
4032
5811
  ) }),
4033
5812
  mode === "single" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4034
- /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
4035
- activeVariables.map((v) => /* @__PURE__ */ jsxs("div", { children: [
4036
- /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5813
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
5814
+ /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5815
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5816
+ ] }),
5817
+ activeVariables.length > 5 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
5818
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4037
5819
  /* @__PURE__ */ jsx(
4038
5820
  Input,
4039
5821
  {
4040
- id: `gen-${v.varName}`,
4041
- value: variableValues[v.varName] || "",
4042
- onChange: (e) => updateVariableValue(v.varName, e.target.value),
4043
- placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
4044
- className: "h-8 text-xs"
5822
+ value: variableSearch,
5823
+ onChange: (e) => setVariableSearch(e.target.value),
5824
+ placeholder: "Search variables...",
5825
+ className: "h-8 text-xs pl-8"
5826
+ }
5827
+ ),
5828
+ variableSearch && /* @__PURE__ */ jsx(
5829
+ "button",
5830
+ {
5831
+ type: "button",
5832
+ onClick: () => setVariableSearch(""),
5833
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5834
+ children: /* @__PURE__ */ jsx(X, { size: 12 })
4045
5835
  }
4046
5836
  )
4047
- ] }, v.varName))
5837
+ ] }),
5838
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5839
+ "No variables match \u201C",
5840
+ variableSearch,
5841
+ "\u201D"
5842
+ ] }),
5843
+ filteredVariables.map((v) => {
5844
+ const isLocked = lockedVarNames.has(v.varName);
5845
+ return /* @__PURE__ */ jsxs("div", { children: [
5846
+ /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5847
+ /* @__PURE__ */ jsx(
5848
+ Input,
5849
+ {
5850
+ id: `gen-${v.varName}`,
5851
+ value: variableValues[v.varName] || "",
5852
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5853
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5854
+ disabled: isLocked,
5855
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5856
+ }
5857
+ )
5858
+ ] }, v.varName);
5859
+ })
4048
5860
  ] }),
4049
5861
  mode === "bulk" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4050
5862
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4233,56 +6045,60 @@ ${allWarnings.join("\n")}`);
4233
6045
  ] }),
4234
6046
  !hasVariables && hasContent && /* @__PURE__ */ jsx("div", { className: "p-3 rounded-md bg-muted/30 border border-border", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: 'This template has no variables. You can still preview and export the PDF as-is, or add variables in the Editor tab using the "Insert Variable" button.' }) })
4235
6047
  ] }),
4236
- previewError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
4237
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4238
- /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: previewError })
4239
- ] }) }) }),
4240
- exportError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
4241
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4242
- /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: exportError })
4243
- ] }) }) }),
4244
- exportSuccess && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-emerald-500/10 border border-emerald-500/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
4245
- /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "text-emerald-600 shrink-0" }),
4246
- /* @__PURE__ */ jsx("p", { className: "text-xs text-emerald-700", children: exportSuccess })
4247
- ] }) }) }),
4248
- hasContent && /* @__PURE__ */ jsx("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
4249
- /* @__PURE__ */ jsxs(
4250
- Button,
4251
- {
4252
- variant: "secondary",
4253
- onClick: handlePreview,
4254
- disabled: isGenerating,
4255
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4256
- children: [
4257
- isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
4258
- isGenerating ? "Generating..." : "Generate PDF"
4259
- ]
4260
- }
4261
- ),
4262
- /* @__PURE__ */ jsxs(
6048
+ hasContent && /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: [
6049
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
6050
+ /* @__PURE__ */ jsxs(
6051
+ Button,
6052
+ {
6053
+ variant: "secondary",
6054
+ onClick: handlePreview,
6055
+ disabled: isGenerating,
6056
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6057
+ children: [
6058
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
6059
+ isGenerating ? "Generating..." : "Generate PDF"
6060
+ ]
6061
+ }
6062
+ ),
6063
+ /* @__PURE__ */ jsxs(
6064
+ Button,
6065
+ {
6066
+ onClick: handleExportSingle,
6067
+ disabled: !previewFresh || isExporting,
6068
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6069
+ children: [
6070
+ isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
6071
+ isExporting ? "Exporting..." : "Export PDF"
6072
+ ]
6073
+ }
6074
+ )
6075
+ ] }) : /* @__PURE__ */ jsxs(
4263
6076
  Button,
4264
6077
  {
4265
- onClick: handleExportSingle,
4266
- disabled: !previewFresh || isExporting,
4267
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6078
+ onClick: handleBulkGenerate,
6079
+ disabled: isExporting || !bulkData || bulkData.length === 0,
6080
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
4268
6081
  children: [
4269
6082
  isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4270
- isExporting ? "Exporting..." : "Export PDF"
6083
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4271
6084
  ]
4272
6085
  }
4273
- )
4274
- ] }) : /* @__PURE__ */ jsxs(
4275
- Button,
4276
- {
4277
- onClick: handleBulkGenerate,
4278
- disabled: isExporting || !bulkData || bulkData.length === 0,
4279
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
4280
- children: [
4281
- isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4282
- isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4283
- ]
4284
- }
4285
- ) })
6086
+ ),
6087
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6088
+ previewError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6089
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6090
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: previewError })
6091
+ ] }),
6092
+ exportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6093
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6094
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportError })
6095
+ ] }),
6096
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6097
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
6098
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportSuccess })
6099
+ ] })
6100
+ ] })
6101
+ ] })
4286
6102
  ] }),
4287
6103
  /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4288
6104
  /* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsx(
@@ -4460,7 +6276,13 @@ function DocumentGeneratorInner({
4460
6276
  showToolbar = true,
4461
6277
  showGenerateTab = true,
4462
6278
  onGeneratePdf,
4463
- initialBulkData
6279
+ initialBulkData,
6280
+ defaultTab = "editor",
6281
+ initialVariableValues,
6282
+ predefinedVariables,
6283
+ exportFileName,
6284
+ templateMode,
6285
+ onSaveTemplate
4464
6286
  }) {
4465
6287
  const {
4466
6288
  editor,
@@ -4487,7 +6309,7 @@ function DocumentGeneratorInner({
4487
6309
  placeholder,
4488
6310
  onChange
4489
6311
  });
4490
- const [activeTab, setActiveTab] = useState("editor");
6312
+ const [activeTab, setActiveTab] = useState(defaultTab);
4491
6313
  const [insertPopoverOpen, setInsertPopoverOpen] = useState(false);
4492
6314
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = useState(false);
4493
6315
  const [editFieldId, setEditFieldId] = useState(null);
@@ -4499,6 +6321,7 @@ function DocumentGeneratorInner({
4499
6321
  const editorWrapperRef = useRef(null);
4500
6322
  const [editorCollapsed, setEditorCollapsed] = useState(false);
4501
6323
  const [mdExportError, setMdExportError] = useState(null);
6324
+ const [isSaving, setIsSaving] = useState(false);
4502
6325
  const [exportSuccess, setExportSuccess] = useState(null);
4503
6326
  useEffect(() => {
4504
6327
  const handler = (e) => {
@@ -4569,7 +6392,28 @@ function DocumentGeneratorInner({
4569
6392
  onExport(result);
4570
6393
  setExportSuccess("Document exported successfully");
4571
6394
  }
4572
- }, [exportDocument, onExport]);
6395
+ }, [exportDocument, onExport]);
6396
+ const handleSaveTemplate = useCallback(async () => {
6397
+ const md = exportMarkdown();
6398
+ if (!md.trim()) {
6399
+ setMdExportError("Editor content is empty");
6400
+ return;
6401
+ }
6402
+ const validation = validateMarkdown(md);
6403
+ if (!validation.valid) {
6404
+ setMdExportError(validation.errors.join("; "));
6405
+ return;
6406
+ }
6407
+ setMdExportError(null);
6408
+ setIsSaving(true);
6409
+ try {
6410
+ await onSaveTemplate?.(md);
6411
+ setExportSuccess("Template saved");
6412
+ } catch {
6413
+ } finally {
6414
+ setIsSaving(false);
6415
+ }
6416
+ }, [exportMarkdown, onSaveTemplate]);
4573
6417
  const handleExportMarkdown = useCallback(() => {
4574
6418
  const md = exportMarkdown();
4575
6419
  if (!md.trim()) {
@@ -4586,10 +6430,10 @@ function DocumentGeneratorInner({
4586
6430
  const url = URL.createObjectURL(blob);
4587
6431
  const a = document.createElement("a");
4588
6432
  a.href = url;
4589
- a.download = "template.md";
6433
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4590
6434
  a.click();
4591
6435
  URL.revokeObjectURL(url);
4592
- }, [exportMarkdown]);
6436
+ }, [exportMarkdown, exportFileName]);
4593
6437
  useEffect(() => {
4594
6438
  if (mdExportError) setMdExportError(null);
4595
6439
  }, [markdown]);
@@ -4625,6 +6469,7 @@ function DocumentGeneratorInner({
4625
6469
  open: insertVarPopoverOpen,
4626
6470
  onOpenChange: setInsertVarPopoverOpen,
4627
6471
  onInsert: handleInsertVariable,
6472
+ predefinedVariables,
4628
6473
  children: /* @__PURE__ */ jsxs(
4629
6474
  Button,
4630
6475
  {
@@ -4665,118 +6510,141 @@ function DocumentGeneratorInner({
4665
6510
  ] })
4666
6511
  ] })
4667
6512
  ] }),
4668
- /* @__PURE__ */ jsxs("div", { className: cn(
6513
+ /* @__PURE__ */ jsx("div", { className: cn(
4669
6514
  "flex flex-col flex-1 min-h-0",
4670
6515
  activeTab !== "editor" && "hidden"
6516
+ ), children: /* @__PURE__ */ jsxs("div", { className: cn(
6517
+ "flex-1 min-h-0",
6518
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4671
6519
  ), children: [
4672
- /* @__PURE__ */ jsxs("div", { className: cn(
4673
- "flex-1 min-h-0",
4674
- showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4675
- ), children: [
4676
- !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
4677
- showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
4678
- EditorToolbar,
4679
- {
4680
- editor,
4681
- insertFieldButton,
4682
- insertVariableButton,
4683
- onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4684
- }
4685
- ) }),
4686
- /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsx(
4687
- EditorContent,
4688
- {
4689
- editor,
4690
- className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4691
- }
4692
- ) })
4693
- ] }),
4694
- showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4695
- showPreview && /* @__PURE__ */ jsx(
4696
- PreviewPanel,
6520
+ !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6521
+ showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
6522
+ EditorToolbar,
4697
6523
  {
4698
- pages: pdfPages,
4699
- isGenerating,
4700
- positionedFields,
4701
- headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
4702
- Button,
4703
- {
4704
- variant: "ghost",
4705
- size: "sm",
4706
- className: "h-8 w-8 p-0",
4707
- onClick: () => setEditorCollapsed(false),
4708
- children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
4709
- }
4710
- ) : void 0,
4711
- className: "flex-1 border-0 rounded-none"
6524
+ editor,
6525
+ insertFieldButton,
6526
+ insertVariableButton,
6527
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4712
6528
  }
4713
- )
4714
- ] }),
4715
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
4716
- /* @__PURE__ */ jsxs(
4717
- Button,
6529
+ ) }),
6530
+ /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsx(
6531
+ EditorContent,
4718
6532
  {
4719
- variant: "secondary",
4720
- onClick: generatePdf,
4721
- disabled: isGenerating || !editor,
4722
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4723
- children: [
4724
- isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
4725
- isGenerating ? "Generating..." : "Generate PDF"
4726
- ]
6533
+ editor,
6534
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4727
6535
  }
4728
- ),
4729
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
4730
- mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4731
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4732
- /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
4733
- ] }),
4734
- generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4735
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4736
- /* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
4737
- ] }),
4738
- fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
4739
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4740
- /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
4741
- fieldWarnings.length,
4742
- " field",
4743
- fieldWarnings.length !== 1 ? "s" : "",
4744
- " had warnings during generation"
4745
- ] })
4746
- ] }),
4747
- exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
4748
- /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
4749
- /* @__PURE__ */ jsx("span", { children: exportSuccess })
4750
- ] })
4751
- ] }),
4752
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6536
+ ) }),
6537
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
4753
6538
  /* @__PURE__ */ jsxs(
4754
6539
  Button,
4755
6540
  {
4756
- onClick: handleExportMarkdown,
4757
- disabled: !editor || !markdown.trim(),
4758
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6541
+ variant: "secondary",
6542
+ onClick: generatePdf,
6543
+ disabled: isGenerating || !editor,
6544
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4759
6545
  children: [
4760
- /* @__PURE__ */ jsx(FileText, { size: 16 }),
4761
- "Export MD"
6546
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
6547
+ isGenerating ? "Generating..." : "Generate PDF"
4762
6548
  ]
4763
6549
  }
4764
6550
  ),
4765
- /* @__PURE__ */ jsxs(
6551
+ onSaveTemplate ? /* @__PURE__ */ jsxs(
4766
6552
  Button,
4767
6553
  {
4768
- onClick: handleExport,
4769
- disabled: isGenerating || !editor || pdfPages.length === 0,
6554
+ onClick: handleSaveTemplate,
6555
+ disabled: isSaving || !editor || !markdown.trim(),
4770
6556
  className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4771
6557
  children: [
4772
- /* @__PURE__ */ jsx(FileDown, { size: 16 }),
4773
- exportButtonText
6558
+ isSaving ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Save, { size: 16 }),
6559
+ isSaving ? "Saving..." : exportButtonText || "Save Template"
4774
6560
  ]
4775
6561
  }
4776
- )
6562
+ ) : /* @__PURE__ */ jsxs(Popover, { children: [
6563
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
6564
+ Button,
6565
+ {
6566
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6567
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6568
+ children: [
6569
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6570
+ "Export",
6571
+ /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
6572
+ ]
6573
+ }
6574
+ ) }),
6575
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6576
+ /* @__PURE__ */ jsxs(
6577
+ "button",
6578
+ {
6579
+ onClick: handleExportMarkdown,
6580
+ disabled: !editor || !markdown.trim(),
6581
+ className: "flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
6582
+ children: [
6583
+ /* @__PURE__ */ jsx(FileText, { size: 16 }),
6584
+ "Export as MD"
6585
+ ]
6586
+ }
6587
+ ),
6588
+ /* @__PURE__ */ jsxs(
6589
+ "button",
6590
+ {
6591
+ onClick: handleExport,
6592
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6593
+ className: "flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
6594
+ children: [
6595
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6596
+ "Export as PDF"
6597
+ ]
6598
+ }
6599
+ )
6600
+ ] })
6601
+ ] }),
6602
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6603
+ mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6604
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6605
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
6606
+ ] }),
6607
+ generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6608
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6609
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
6610
+ ] }),
6611
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6612
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6613
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
6614
+ fieldWarnings.length,
6615
+ " field",
6616
+ fieldWarnings.length !== 1 ? "s" : "",
6617
+ " had warnings during generation"
6618
+ ] })
6619
+ ] }),
6620
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6621
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
6622
+ /* @__PURE__ */ jsx("span", { children: exportSuccess })
6623
+ ] })
6624
+ ] })
4777
6625
  ] })
4778
- ] })
4779
- ] }),
6626
+ ] }),
6627
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
6628
+ showPreview && /* @__PURE__ */ jsx(
6629
+ PreviewPanel,
6630
+ {
6631
+ pages: pdfPages,
6632
+ isGenerating,
6633
+ positionedFields,
6634
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
6635
+ Button,
6636
+ {
6637
+ variant: "ghost",
6638
+ size: "sm",
6639
+ className: "h-8 w-8 p-0",
6640
+ onClick: () => setEditorCollapsed(false),
6641
+ children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
6642
+ }
6643
+ ) : void 0,
6644
+ className: "flex-1 border-0 rounded-none"
6645
+ }
6646
+ )
6647
+ ] }) }),
4780
6648
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsx(
4781
6649
  GeneratePanel,
4782
6650
  {
@@ -4784,7 +6652,10 @@ function DocumentGeneratorInner({
4784
6652
  editorContent: editor?.getJSON(),
4785
6653
  editorVariables: variables,
4786
6654
  onGeneratePdf,
4787
- initialBulkData
6655
+ initialBulkData,
6656
+ initialVariableValues,
6657
+ exportFileName,
6658
+ templateMode
4788
6659
  }
4789
6660
  ),
4790
6661
  /* @__PURE__ */ jsx(
@@ -4925,6 +6796,717 @@ function EditorPanel({
4925
6796
  ] });
4926
6797
  }
4927
6798
 
4928
- export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, ThemeProvider, useDocumentGenerator, useTheme };
6799
+ // src/utils/xml-template-parser.ts
6800
+ function extractFieldRefs(expr) {
6801
+ const refs = [];
6802
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6803
+ let m;
6804
+ while ((m = re.exec(expr)) !== null) {
6805
+ if (m[2]) {
6806
+ refs.push(`${m[1]}.${m[2]}`);
6807
+ } else {
6808
+ refs.push(m[1]);
6809
+ }
6810
+ }
6811
+ return refs;
6812
+ }
6813
+ function fieldRefToVarName(ref, bandContext) {
6814
+ if (ref.includes(".")) {
6815
+ return labelToVarName(ref.replace(/\./g, "_"));
6816
+ }
6817
+ if (bandContext) {
6818
+ return labelToVarName(bandContext + "_" + ref);
6819
+ }
6820
+ return labelToVarName(ref);
6821
+ }
6822
+ function fieldRefToLabel(ref) {
6823
+ const parts = ref.split(".");
6824
+ const raw = parts[parts.length - 1];
6825
+ return raw.replace(/_/g, " ");
6826
+ }
6827
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6828
+ const trimmed = expr.trim();
6829
+ const suppressZero = /<>\s*0/.test(trimmed);
6830
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6831
+ const aggMatch = trimmed.match(
6832
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6833
+ );
6834
+ if (aggMatch) {
6835
+ const fieldName = aggMatch[2];
6836
+ const varName = labelToVarName("workorder_" + fieldName);
6837
+ const label = fieldName.replace(/_/g, " ");
6838
+ return registry.register(varName, label, szOpts);
6839
+ }
6840
+ const concatSingle = trimmed.match(
6841
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6842
+ );
6843
+ if (concatSingle) {
6844
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6845
+ const varName = fieldRefToVarName(ref, bandContext);
6846
+ const label = fieldRefToLabel(ref);
6847
+ return registry.register(varName, label, szOpts);
6848
+ }
6849
+ const concatMulti = trimmed.match(
6850
+ /^concat\(/i
6851
+ );
6852
+ if (concatMulti) {
6853
+ const refs = extractFieldRefs(trimmed);
6854
+ if (refs.length > 0) {
6855
+ const uniqueRefs = [...new Set(refs)];
6856
+ const tokens = uniqueRefs.map((ref) => {
6857
+ const varName = fieldRefToVarName(ref, bandContext);
6858
+ const label = fieldRefToLabel(ref);
6859
+ return registry.register(varName, label, szOpts);
6860
+ });
6861
+ return tokens.join(" ");
6862
+ }
6863
+ }
6864
+ const toLongMatch = trimmed.match(
6865
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6866
+ );
6867
+ if (toLongMatch) {
6868
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6869
+ const varName = fieldRefToVarName(ref, bandContext);
6870
+ const label = fieldRefToLabel(ref);
6871
+ return registry.register(varName, label, szOpts);
6872
+ }
6873
+ const fmtMatch = trimmed.match(
6874
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6875
+ );
6876
+ if (fmtMatch) {
6877
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6878
+ }
6879
+ const replaceMatch = trimmed.match(
6880
+ /^Replace\(\s*\[([^\]]+)\]/i
6881
+ );
6882
+ if (replaceMatch) {
6883
+ const ref = replaceMatch[1];
6884
+ const varName = fieldRefToVarName(ref, bandContext);
6885
+ const label = fieldRefToLabel(ref);
6886
+ return registry.register(varName, label, szOpts);
6887
+ }
6888
+ const iifLabelMatch = trimmed.match(
6889
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6890
+ );
6891
+ if (iifLabelMatch) {
6892
+ return iifLabelMatch[1];
6893
+ }
6894
+ const iifLabelElse = trimmed.match(
6895
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6896
+ );
6897
+ if (iifLabelElse) {
6898
+ return iifLabelElse[1];
6899
+ }
6900
+ const iifEmptyElse = trimmed.match(
6901
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6902
+ );
6903
+ if (iifEmptyElse) {
6904
+ return iifEmptyElse[1];
6905
+ }
6906
+ const nestedIifStaticLabel = trimmed.match(
6907
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6908
+ );
6909
+ if (nestedIifStaticLabel) {
6910
+ return nestedIifStaticLabel[1];
6911
+ }
6912
+ const iifValueMatch = trimmed.match(
6913
+ /^iif\s*\(/i
6914
+ );
6915
+ if (iifValueMatch) {
6916
+ const refs = extractFieldRefs(trimmed);
6917
+ if (refs.length > 0) {
6918
+ const uniqueRefs = [...new Set(refs)];
6919
+ const innerAggMatch = trimmed.match(
6920
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6921
+ );
6922
+ if (innerAggMatch) {
6923
+ const fieldName = innerAggMatch[2];
6924
+ const varName2 = labelToVarName("workorder_" + fieldName);
6925
+ const label2 = fieldName.replace(/_/g, " ");
6926
+ return registry.register(varName2, label2, szOpts);
6927
+ }
6928
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6929
+ const varName = fieldRefToVarName(ref, bandContext);
6930
+ const label = fieldRefToLabel(ref);
6931
+ return registry.register(varName, label, szOpts);
6932
+ }
6933
+ const staticMatch = trimmed.match(/'([^']+)'/);
6934
+ if (staticMatch) return staticMatch[1];
6935
+ return "";
6936
+ }
6937
+ const arithmeticRefs = extractFieldRefs(trimmed);
6938
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6939
+ if (siblingLabel) {
6940
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6941
+ const varName = labelToVarName(
6942
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6943
+ );
6944
+ return registry.register(varName, cleanLabel, szOpts);
6945
+ }
6946
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6947
+ const tokens = uniqueRefs.map((ref) => {
6948
+ const varName = fieldRefToVarName(ref, bandContext);
6949
+ const label = fieldRefToLabel(ref);
6950
+ return registry.register(varName, label, szOpts);
6951
+ });
6952
+ return tokens.join(" ");
6953
+ }
6954
+ const simpleMatch = trimmed.match(
6955
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6956
+ );
6957
+ if (simpleMatch) {
6958
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6959
+ const varName = fieldRefToVarName(ref, bandContext);
6960
+ const label = fieldRefToLabel(ref);
6961
+ return registry.register(varName, label, szOpts);
6962
+ }
6963
+ if (arithmeticRefs.length >= 1) {
6964
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6965
+ const varName = fieldRefToVarName(ref, bandContext);
6966
+ const label = fieldRefToLabel(ref);
6967
+ return registry.register(varName, label, szOpts);
6968
+ }
6969
+ return "";
6970
+ }
6971
+ var VariableRegistry = class {
6972
+ constructor() {
6973
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6974
+ }
6975
+ register(varName, varLabel, options) {
6976
+ if (!this.map.has(varName)) {
6977
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6978
+ }
6979
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6980
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6981
+ }
6982
+ getAll() {
6983
+ return Array.from(this.map.values());
6984
+ }
6985
+ };
6986
+ function parseLocationFloat(s) {
6987
+ if (!s) return { x: 0, y: 0 };
6988
+ const parts = s.split(",");
6989
+ return {
6990
+ x: parseFloat(parts[0]) || 0,
6991
+ y: parseFloat(parts[1]) || 0
6992
+ };
6993
+ }
6994
+ function parseSizeF(s) {
6995
+ if (!s) return { w: 0, h: 0 };
6996
+ const parts = s.split(",");
6997
+ return {
6998
+ w: parseFloat(parts[0]) || 0,
6999
+ h: parseFloat(parts[1]) || 0
7000
+ };
7001
+ }
7002
+ function extractControls(bandEl) {
7003
+ const controlsEl = bandEl.querySelector(":scope > Controls");
7004
+ if (!controlsEl) return [];
7005
+ const items = [];
7006
+ for (const child of Array.from(controlsEl.children)) {
7007
+ items.push(extractControl(child));
7008
+ }
7009
+ return items;
7010
+ }
7011
+ function extractControl(el) {
7012
+ const ctrlType = el.getAttribute("ControlType") || "";
7013
+ const name = el.getAttribute("Name") || "";
7014
+ const text = el.getAttribute("Text") || "";
7015
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
7016
+ const size = parseSizeF(el.getAttribute("SizeF"));
7017
+ const formatString = el.getAttribute("TextFormatString") || null;
7018
+ let expression = null;
7019
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
7020
+ if (bindingsEl) {
7021
+ for (const item of Array.from(bindingsEl.children)) {
7022
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
7023
+ expression = item.getAttribute("Expression");
7024
+ break;
7025
+ }
7026
+ }
7027
+ }
7028
+ const children = [];
7029
+ if (ctrlType === "XRPanel") {
7030
+ const panelControls = extractControls(el);
7031
+ children.push(...panelControls);
7032
+ }
7033
+ return {
7034
+ name,
7035
+ type: ctrlType,
7036
+ text,
7037
+ x: loc.x,
7038
+ y: loc.y,
7039
+ width: size.w,
7040
+ expression,
7041
+ formatString,
7042
+ children
7043
+ };
7044
+ }
7045
+ function groupByRow(controls, threshold = 8) {
7046
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
7047
+ const rows = [];
7048
+ for (const ctrl of sorted) {
7049
+ const lastRow = rows[rows.length - 1];
7050
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
7051
+ lastRow.controls.push(ctrl);
7052
+ } else {
7053
+ rows.push({ y: ctrl.y, controls: [ctrl] });
7054
+ }
7055
+ }
7056
+ for (const row of rows) {
7057
+ row.controls.sort((a, b) => a.x - b.x);
7058
+ }
7059
+ return rows;
7060
+ }
7061
+ function isStaticLabel(ctrl) {
7062
+ return ctrl.type === "XRLabel" && !ctrl.expression;
7063
+ }
7064
+ function isBound(ctrl) {
7065
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
7066
+ }
7067
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
7068
+ const leftExtent = leftControls.reduce(
7069
+ (max, c) => Math.max(max, c.x + c.width),
7070
+ 0
7071
+ );
7072
+ const rightMinX = rightControls.reduce(
7073
+ (min, c) => Math.min(min, c.x),
7074
+ contentWidth
7075
+ );
7076
+ const rightMaxExtent = rightControls.reduce(
7077
+ (max, c) => Math.max(max, c.x + c.width),
7078
+ 0
7079
+ );
7080
+ const rightExtent = rightMaxExtent - rightMinX;
7081
+ if (leftExtent + rightExtent === 0) return 50;
7082
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
7083
+ }
7084
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
7085
+ const lines = [];
7086
+ const leftControls = controls.filter((c) => c.x < 437);
7087
+ const rightControls = controls.filter((c) => c.x >= 437);
7088
+ const leftEntries = [];
7089
+ if (leftControls.length > 0) {
7090
+ const leftRows = groupByRow(leftControls);
7091
+ for (const row of leftRows) {
7092
+ const parts = [];
7093
+ for (const ctrl of row.controls) {
7094
+ if (isBound(ctrl)) {
7095
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7096
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
7097
+ resolved = resolved.replace("}}", "|format:phone}}");
7098
+ }
7099
+ if (resolved) parts.push(resolved);
7100
+ } else if (isStaticLabel(ctrl)) {
7101
+ const text = ctrl.text.trim();
7102
+ if (text && !text.match(/^label\d+$/)) {
7103
+ parts.push(text);
7104
+ }
7105
+ }
7106
+ }
7107
+ if (parts.length > 0) {
7108
+ leftEntries.push(parts.join(" "));
7109
+ }
7110
+ }
7111
+ }
7112
+ const rightEntries = [];
7113
+ if (rightControls.length > 0) {
7114
+ const rightRows = groupByRow(rightControls);
7115
+ for (const row of rightRows) {
7116
+ let label = "";
7117
+ let value = "";
7118
+ for (const ctrl of row.controls) {
7119
+ if (isStaticLabel(ctrl)) {
7120
+ const text = ctrl.text.trim();
7121
+ if (text && !text.match(/^label\d+$/)) {
7122
+ label = text;
7123
+ }
7124
+ } else if (isBound(ctrl)) {
7125
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
7126
+ if (ctrl.expression) {
7127
+ const refs = extractFieldRefs(ctrl.expression);
7128
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
7129
+ value = value.replace("}}", "|suppress:zero}}");
7130
+ }
7131
+ }
7132
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7133
+ value = value.replace("}}", "|format:phone}}");
7134
+ }
7135
+ }
7136
+ }
7137
+ if (label || value) {
7138
+ rightEntries.push({ label, value, y: row.y });
7139
+ }
7140
+ }
7141
+ }
7142
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7143
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7144
+ lines.push(`:::columns{split:${outerSplit}}`);
7145
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
7146
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
7147
+ const xmlLineHeight = 17;
7148
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
7149
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
7150
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
7151
+ lines.push(leftColTag);
7152
+ for (const entry of leftEntries) {
7153
+ lines.push(entry);
7154
+ }
7155
+ lines.push(":::");
7156
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
7157
+ lines.push(rightColTag);
7158
+ if (rightEntries.length > 0) {
7159
+ lines.push("| | |");
7160
+ lines.push("|---|---|");
7161
+ let prevY = -Infinity;
7162
+ for (const entry of rightEntries) {
7163
+ if (prevY > -Infinity && entry.y - prevY > 25) {
7164
+ lines.push("| | |");
7165
+ }
7166
+ const label = entry.label || "";
7167
+ const colon = entry.label ? ":" : "";
7168
+ const value = entry.value ? ` ${entry.value}` : "";
7169
+ lines.push(`| ${label} | ${colon}${value} |`);
7170
+ prevY = entry.y;
7171
+ }
7172
+ }
7173
+ lines.push(":::");
7174
+ lines.push(":::");
7175
+ lines.push("");
7176
+ }
7177
+ return lines.join("\n");
7178
+ }
7179
+ function renderReportHeaderBand(controls, registry, contentWidth) {
7180
+ const lines = [];
7181
+ const leftControls = controls.filter((c) => c.x < 437);
7182
+ const rightControls = controls.filter((c) => c.x >= 437);
7183
+ const leftEntries = [];
7184
+ if (leftControls.length > 0) {
7185
+ const leftRows = groupByRow(leftControls);
7186
+ for (const row of leftRows) {
7187
+ const parts = [];
7188
+ for (const ctrl of row.controls) {
7189
+ if (isBound(ctrl)) {
7190
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7191
+ if (resolved) parts.push(resolved);
7192
+ } else if (isStaticLabel(ctrl)) {
7193
+ const text = ctrl.text.trim();
7194
+ if (text && !text.match(/^label\d+$/)) {
7195
+ parts.push(text);
7196
+ }
7197
+ }
7198
+ }
7199
+ if (parts.length > 0) {
7200
+ leftEntries.push(parts.join(" "));
7201
+ }
7202
+ }
7203
+ }
7204
+ const rightEntries = [];
7205
+ if (rightControls.length > 0) {
7206
+ const rightRows = groupByRow(rightControls);
7207
+ for (const row of rightRows) {
7208
+ let label = "";
7209
+ let value = "";
7210
+ for (const ctrl of row.controls) {
7211
+ if (isStaticLabel(ctrl)) {
7212
+ const text = ctrl.text.trim();
7213
+ if (text && !text.match(/^label\d+$/)) {
7214
+ label = text;
7215
+ }
7216
+ } else if (isBound(ctrl)) {
7217
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7218
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7219
+ value = value.replace("}}", "|format:phone}}");
7220
+ }
7221
+ }
7222
+ }
7223
+ if (label || value) {
7224
+ rightEntries.push({ label, value });
7225
+ }
7226
+ }
7227
+ }
7228
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7229
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7230
+ lines.push(`:::columns{split:${outerSplit}}`);
7231
+ lines.push(":::col");
7232
+ for (const entry of leftEntries) {
7233
+ lines.push(entry);
7234
+ }
7235
+ lines.push(":::");
7236
+ lines.push(":::col");
7237
+ lines.push("| | |");
7238
+ lines.push("|---|---|");
7239
+ for (const entry of rightEntries) {
7240
+ const label = entry.label || "";
7241
+ const value = entry.value ? ` ${entry.value}` : "";
7242
+ lines.push(`| ${label} |${value} |`);
7243
+ }
7244
+ lines.push(":::");
7245
+ lines.push(":::");
7246
+ lines.push("");
7247
+ }
7248
+ return lines.join("\n");
7249
+ }
7250
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
7251
+ const lines = [];
7252
+ const headerTexts = [];
7253
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
7254
+ if (isStaticLabel(ctrl)) {
7255
+ const text = ctrl.text.trim();
7256
+ if (text) headerTexts.push(text);
7257
+ }
7258
+ }
7259
+ const titleText = headerTexts.join(" ");
7260
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
7261
+ lines.push(":::");
7262
+ lines.push(":::repeat{data:operations}");
7263
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
7264
+ const detailParts = [];
7265
+ for (const ctrl of sortedDetail) {
7266
+ if (isBound(ctrl)) {
7267
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
7268
+ if (resolved) detailParts.push(resolved);
7269
+ }
7270
+ }
7271
+ if (detailParts.length > 0) {
7272
+ if (detailParts.length >= 2) {
7273
+ lines.push(":::columns{split:12}");
7274
+ lines.push(":::col");
7275
+ lines.push(`**${detailParts[0]}**`);
7276
+ lines.push(":::");
7277
+ lines.push(":::col");
7278
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
7279
+ lines.push(":::");
7280
+ lines.push(":::");
7281
+ } else {
7282
+ lines.push(`**${detailParts[0]}**`);
7283
+ }
7284
+ lines.push("");
7285
+ }
7286
+ if (groupFooterControls.length > 0) {
7287
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
7288
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
7289
+ }
7290
+ lines.push("---");
7291
+ lines.push(":::");
7292
+ lines.push("");
7293
+ return lines.join("\n");
7294
+ }
7295
+ function renderGroupFooterBand(controls, registry, _contentWidth) {
7296
+ const rows = groupByRow(controls);
7297
+ const entries = [];
7298
+ for (const row of rows) {
7299
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7300
+ if (labels.length === 0) continue;
7301
+ let label = "";
7302
+ let value = "";
7303
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7304
+ for (const ctrl of sorted) {
7305
+ if (isStaticLabel(ctrl)) {
7306
+ const text = ctrl.text.trim();
7307
+ if (text && !text.match(/^label\d+$/)) {
7308
+ label = text;
7309
+ }
7310
+ } else if (isBound(ctrl)) {
7311
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
7312
+ if (resolved) {
7313
+ if (!resolved.startsWith("{{var|")) {
7314
+ if (!label) label = resolved;
7315
+ } else {
7316
+ value = resolved;
7317
+ }
7318
+ }
7319
+ }
7320
+ }
7321
+ if (label && value) {
7322
+ entries.push({ label, value });
7323
+ }
7324
+ }
7325
+ if (entries.length === 0) return "";
7326
+ const lines = [];
7327
+ lines.push(":::subtotals");
7328
+ for (const entry of entries) {
7329
+ lines.push(`**${entry.label}** ${entry.value}`);
7330
+ }
7331
+ lines.push(":::");
7332
+ lines.push("");
7333
+ return lines.join("\n");
7334
+ }
7335
+ function renderReportFooterBand(controls, registry) {
7336
+ const lines = [];
7337
+ const panel = controls.find((c) => c.type === "XRPanel");
7338
+ const thankYouLabel = controls.find(
7339
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
7340
+ );
7341
+ const priceRows = [];
7342
+ if (panel && panel.children.length > 0) {
7343
+ const rows = groupByRow(panel.children, 15);
7344
+ for (const row of rows) {
7345
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7346
+ if (labels.length === 0) continue;
7347
+ let label = "";
7348
+ let value = "";
7349
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7350
+ for (const ctrl of sorted) {
7351
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
7352
+ continue;
7353
+ }
7354
+ const isLabelPos = ctrl.x < 150;
7355
+ if (isStaticLabel(ctrl)) {
7356
+ const text = ctrl.text.trim();
7357
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
7358
+ label = text;
7359
+ }
7360
+ } else if (isBound(ctrl)) {
7361
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
7362
+ if (resolved) {
7363
+ if (isLabelPos) {
7364
+ if (!label) label = resolved;
7365
+ } else {
7366
+ if (!value) value = resolved;
7367
+ }
7368
+ }
7369
+ }
7370
+ }
7371
+ if (label && value) {
7372
+ priceRows.push({ label, value });
7373
+ } else if (value) {
7374
+ priceRows.push({ label: "", value });
7375
+ }
7376
+ }
7377
+ }
7378
+ lines.push(":::columns{split:40|padX:20}");
7379
+ lines.push(":::col{padTop:1}");
7380
+ if (thankYouLabel) {
7381
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
7382
+ }
7383
+ lines.push(":::");
7384
+ lines.push(":::col{padTop:1}");
7385
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
7386
+ if (priceRows.length > 0) {
7387
+ lines.push(":::subtotals");
7388
+ for (const row of priceRows) {
7389
+ const label = row.label ? `**${row.label}**` : "";
7390
+ lines.push(`${label} ${row.value}`);
7391
+ }
7392
+ lines.push(":::");
7393
+ }
7394
+ lines.push(":::");
7395
+ lines.push(":::");
7396
+ lines.push(":::");
7397
+ lines.push("");
7398
+ return lines.join("\n");
7399
+ }
7400
+ function parseXmlTemplate(xmlString) {
7401
+ const warnings = [];
7402
+ const registry = new VariableRegistry();
7403
+ const parser = new DOMParser();
7404
+ const doc = parser.parseFromString(xmlString, "text/xml");
7405
+ const parseError = doc.querySelector("parsererror");
7406
+ if (parseError) {
7407
+ return {
7408
+ markdown: "",
7409
+ variables: [],
7410
+ reportName: "",
7411
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7412
+ };
7413
+ }
7414
+ const root = doc.documentElement;
7415
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7416
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7417
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7418
+ const marginParts = marginsStr.split(",").map(Number);
7419
+ const leftMargin = marginParts[0] || 50;
7420
+ const rightMargin = marginParts[1] || 50;
7421
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7422
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7423
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7424
+ if (calcFieldsEl) {
7425
+ for (const item of Array.from(calcFieldsEl.children)) {
7426
+ const name = item.getAttribute("Name");
7427
+ if (name) calculatedFieldNames.add(name);
7428
+ }
7429
+ }
7430
+ const markdownSections = [];
7431
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7432
+ if (watermarkEl) {
7433
+ const wmText = watermarkEl.getAttribute("Text");
7434
+ if (wmText) {
7435
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7436
+ markdownSections.push("");
7437
+ }
7438
+ }
7439
+ const bandsEl = root.querySelector(":scope > Bands");
7440
+ if (!bandsEl) {
7441
+ warnings.push("No <Bands> element found in the XML");
7442
+ return {
7443
+ markdown: "",
7444
+ variables: registry.getAll(),
7445
+ reportName,
7446
+ warnings
7447
+ };
7448
+ }
7449
+ let topMarginControls = [];
7450
+ let reportHeaderControls = [];
7451
+ let pageHeaderControls = [];
7452
+ let detailControls = [];
7453
+ let groupFooterControls = [];
7454
+ let reportFooterControls = [];
7455
+ for (const bandItem of Array.from(bandsEl.children)) {
7456
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7457
+ if (ctrlType === "TopMarginBand") {
7458
+ topMarginControls = extractControls(bandItem);
7459
+ } else if (ctrlType === "ReportHeaderBand") {
7460
+ reportHeaderControls = extractControls(bandItem);
7461
+ } else if (ctrlType === "PageHeaderBand") {
7462
+ pageHeaderControls = extractControls(bandItem);
7463
+ } else if (ctrlType === "DetailReportBand") {
7464
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7465
+ if (nestedBands) {
7466
+ for (const nested of Array.from(nestedBands.children)) {
7467
+ const nestedType = nested.getAttribute("ControlType") || "";
7468
+ if (nestedType === "DetailBand") {
7469
+ detailControls = extractControls(nested);
7470
+ } else if (nestedType === "GroupFooterBand") {
7471
+ groupFooterControls = extractControls(nested);
7472
+ }
7473
+ }
7474
+ }
7475
+ } else if (ctrlType === "ReportFooterBand") {
7476
+ reportFooterControls = extractControls(bandItem);
7477
+ }
7478
+ }
7479
+ if (topMarginControls.length > 0) {
7480
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7481
+ }
7482
+ if (reportHeaderControls.length > 0) {
7483
+ markdownSections.push(
7484
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7485
+ );
7486
+ }
7487
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7488
+ markdownSections.push(
7489
+ renderOperationsSection(
7490
+ pageHeaderControls,
7491
+ detailControls,
7492
+ groupFooterControls,
7493
+ registry)
7494
+ );
7495
+ }
7496
+ if (reportFooterControls.length > 0) {
7497
+ markdownSections.push(
7498
+ renderReportFooterBand(reportFooterControls, registry)
7499
+ );
7500
+ }
7501
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7502
+ return {
7503
+ markdown,
7504
+ variables: registry.getAll(),
7505
+ reportName,
7506
+ warnings
7507
+ };
7508
+ }
7509
+
7510
+ export { DocumentGenerator, EditorPanel, FormFieldType, ImageNode, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandBlockVariables, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
4929
7511
  //# sourceMappingURL=index.mjs.map
4930
7512
  //# sourceMappingURL=index.mjs.map