@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.js CHANGED
@@ -7,6 +7,10 @@ var StarterKit = require('@tiptap/starter-kit');
7
7
  var Underline = require('@tiptap/extension-underline');
8
8
  var TextAlign = require('@tiptap/extension-text-align');
9
9
  var Placeholder = require('@tiptap/extension-placeholder');
10
+ var Table = require('@tiptap/extension-table');
11
+ var TableRow = require('@tiptap/extension-table-row');
12
+ var TableCell = require('@tiptap/extension-table-cell');
13
+ var TableHeader = require('@tiptap/extension-table-header');
10
14
  var core = require('@tiptap/core');
11
15
  var clsx = require('clsx');
12
16
  var tailwindMerge = require('tailwind-merge');
@@ -45,13 +49,17 @@ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
45
49
  var Underline__default = /*#__PURE__*/_interopDefault(Underline);
46
50
  var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
47
51
  var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
52
+ var Table__default = /*#__PURE__*/_interopDefault(Table);
53
+ var TableRow__default = /*#__PURE__*/_interopDefault(TableRow);
54
+ var TableCell__default = /*#__PURE__*/_interopDefault(TableCell);
55
+ var TableHeader__default = /*#__PURE__*/_interopDefault(TableHeader);
48
56
  var TooltipPrimitive__namespace = /*#__PURE__*/_interopNamespace(TooltipPrimitive);
49
57
  var PopoverPrimitive__namespace = /*#__PURE__*/_interopNamespace(PopoverPrimitive);
50
58
  var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
51
59
 
52
60
  var __defProp = Object.defineProperty;
53
61
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
54
- var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
62
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
55
63
  function cn(...inputs) {
56
64
  return tailwindMerge.twMerge(clsx.clsx(inputs));
57
65
  }
@@ -243,7 +251,10 @@ var VariableNode = core.Node.create({
243
251
  return {
244
252
  varName: { default: "" },
245
253
  varLabel: { default: "" },
246
- varDefault: { default: "" }
254
+ varDefault: { default: "" },
255
+ format: { default: "" },
256
+ suppressZero: { default: "" },
257
+ block: { default: "" }
247
258
  };
248
259
  },
249
260
  parseHTML() {
@@ -309,6 +320,235 @@ var VariableNode = core.Node.create({
309
320
  };
310
321
  }
311
322
  });
323
+ var PanelNode = core.Node.create({
324
+ name: "panel",
325
+ group: "block",
326
+ content: "block+",
327
+ defining: true,
328
+ addAttributes() {
329
+ return {
330
+ title: { default: "" },
331
+ border: { default: "solid" },
332
+ headerStyle: { default: "" }
333
+ };
334
+ },
335
+ parseHTML() {
336
+ return [{
337
+ tag: "div[data-panel-node]",
338
+ contentElement: "div.panel-content"
339
+ }];
340
+ },
341
+ renderHTML({ node, HTMLAttributes }) {
342
+ const { title, border, headerStyle } = node.attrs;
343
+ const classes = ["panel-node"];
344
+ if (border === "dashed") classes.push("panel-border-dashed");
345
+ else if (border === "none") classes.push("panel-border-none");
346
+ else classes.push("panel-border-solid");
347
+ if (headerStyle === "dark") classes.push("panel-dark");
348
+ const attrs = core.mergeAttributes(
349
+ { "data-panel-node": "", class: classes.join(" ") },
350
+ HTMLAttributes
351
+ );
352
+ if (title) {
353
+ return [
354
+ "div",
355
+ attrs,
356
+ ["div", { class: "panel-title" }, title],
357
+ ["div", { class: "panel-content" }, 0]
358
+ ];
359
+ }
360
+ return ["div", attrs, ["div", { class: "panel-content" }, 0]];
361
+ }
362
+ });
363
+ var ColumnsNode = core.Node.create({
364
+ name: "columns",
365
+ group: "block",
366
+ content: "column+",
367
+ defining: true,
368
+ addAttributes() {
369
+ return {
370
+ split: { default: "50" },
371
+ padX: { default: "0" }
372
+ };
373
+ },
374
+ parseHTML() {
375
+ return [{ tag: "div[data-columns-node]" }];
376
+ },
377
+ renderHTML({ node, HTMLAttributes }) {
378
+ const split = node.attrs.split || "50";
379
+ return ["div", core.mergeAttributes(
380
+ {
381
+ "data-columns-node": "",
382
+ class: "columns-node",
383
+ style: `--xpc-col-split: ${split}`
384
+ },
385
+ HTMLAttributes
386
+ ), 0];
387
+ }
388
+ });
389
+ var ColumnNode = core.Node.create({
390
+ name: "column",
391
+ group: "",
392
+ content: "block+",
393
+ defining: true,
394
+ addAttributes() {
395
+ return {
396
+ padTop: { default: 0 }
397
+ };
398
+ },
399
+ parseHTML() {
400
+ return [{ tag: "div[data-column-node]" }];
401
+ },
402
+ renderHTML({ HTMLAttributes }) {
403
+ return ["div", core.mergeAttributes(
404
+ { "data-column-node": "", class: "column-node" },
405
+ HTMLAttributes
406
+ ), 0];
407
+ }
408
+ });
409
+ function WatermarkNodeView({ node, selected }) {
410
+ const { text } = node.attrs;
411
+ return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { children: /* @__PURE__ */ jsxRuntime.jsxs(
412
+ "div",
413
+ {
414
+ "data-watermark-node": "",
415
+ className: cn(
416
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
417
+ "bg-blue-50 text-blue-600 border-blue-200",
418
+ selected && "ring-2 ring-primary ring-offset-1"
419
+ ),
420
+ children: [
421
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Droplets, { size: 14 }),
422
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
423
+ "Watermark: ",
424
+ text || "(empty)"
425
+ ] })
426
+ ]
427
+ }
428
+ ) });
429
+ }
430
+
431
+ // src/extensions/watermark-node.ts
432
+ var WatermarkNode = core.Node.create({
433
+ name: "watermark",
434
+ group: "block",
435
+ atom: true,
436
+ selectable: true,
437
+ draggable: false,
438
+ addAttributes() {
439
+ return {
440
+ text: { default: "" },
441
+ opacity: { default: "0.15" },
442
+ angle: { default: "-45" }
443
+ };
444
+ },
445
+ parseHTML() {
446
+ return [{ tag: "div[data-watermark-node]" }];
447
+ },
448
+ renderHTML({ HTMLAttributes }) {
449
+ return ["div", core.mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
450
+ },
451
+ addNodeView() {
452
+ return react.ReactNodeViewRenderer(WatermarkNodeView);
453
+ }
454
+ });
455
+ function ImageNodeView({ node, selected }) {
456
+ const { src, varName, alt, width, height, align } = node.attrs;
457
+ const hasRenderableSrc = !!src && /^(https?:|data:)/i.test(src);
458
+ const wrapperJustify = align === "center" ? "justify-center" : align === "right" ? "justify-end" : "justify-start";
459
+ const toCssSize = (v) => /^\d+(\.\d+)?$/.test(v) ? `${v}px` : v;
460
+ const sizeStyle = {};
461
+ if (width) sizeStyle.width = toCssSize(width);
462
+ if (height) sizeStyle.height = toCssSize(height);
463
+ return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex my-1", wrapperJustify), "data-image-node": "", children: hasRenderableSrc ? /* @__PURE__ */ jsxRuntime.jsx(
464
+ "img",
465
+ {
466
+ src,
467
+ alt: alt || varName || "",
468
+ style: sizeStyle,
469
+ className: cn(
470
+ "max-w-full rounded-sm object-contain",
471
+ selected && "ring-2 ring-primary ring-offset-1"
472
+ ),
473
+ draggable: false
474
+ }
475
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
476
+ "div",
477
+ {
478
+ "data-image-placeholder": "",
479
+ className: cn(
480
+ "inline-flex items-center gap-2 px-3 py-2 rounded-md border border-dashed text-sm",
481
+ "bg-slate-50 text-slate-500 border-slate-300",
482
+ selected && "ring-2 ring-primary ring-offset-1"
483
+ ),
484
+ style: height ? { minHeight: toCssSize(height) } : void 0,
485
+ title: varName ? `Image variable: ${varName}` : "Image",
486
+ children: [
487
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { size: 16 }),
488
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: varName ? `Image: ${varName}` : alt || "Image" })
489
+ ]
490
+ }
491
+ ) }) });
492
+ }
493
+
494
+ // src/extensions/image-node.ts
495
+ var ImageNode = core.Node.create({
496
+ name: "imageNode",
497
+ group: "block",
498
+ atom: true,
499
+ selectable: true,
500
+ draggable: false,
501
+ addAttributes() {
502
+ return {
503
+ src: { default: "" },
504
+ varName: { default: "" },
505
+ alt: { default: "" },
506
+ width: { default: "" },
507
+ height: { default: "" },
508
+ align: { default: "" }
509
+ };
510
+ },
511
+ parseHTML() {
512
+ return [{ tag: "div[data-image-node]" }];
513
+ },
514
+ renderHTML({ HTMLAttributes }) {
515
+ return ["div", core.mergeAttributes({ "data-image-node": "" }, HTMLAttributes)];
516
+ },
517
+ addNodeView() {
518
+ return react.ReactNodeViewRenderer(ImageNodeView);
519
+ }
520
+ });
521
+ var RepeatNode = core.Node.create({
522
+ name: "repeatBlock",
523
+ group: "block",
524
+ content: "block+",
525
+ addAttributes() {
526
+ return {
527
+ data: { default: "" }
528
+ };
529
+ },
530
+ parseHTML() {
531
+ return [{ tag: "div[data-repeat-node]" }];
532
+ },
533
+ renderHTML({ HTMLAttributes }) {
534
+ return ["div", core.mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
535
+ }
536
+ });
537
+ var SubtotalsNode = core.Node.create({
538
+ name: "subtotalsBlock",
539
+ group: "block",
540
+ content: "block+",
541
+ defining: true,
542
+ parseHTML() {
543
+ return [{ tag: "div[data-subtotals-node]" }];
544
+ },
545
+ renderHTML({ HTMLAttributes }) {
546
+ return ["div", core.mergeAttributes(
547
+ { "data-subtotals-node": "", class: "subtotals-block" },
548
+ HTMLAttributes
549
+ ), 0];
550
+ }
551
+ });
312
552
 
313
553
  // src/types/index.ts
314
554
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
@@ -387,158 +627,23 @@ function extractFieldsFromContent(content) {
387
627
  }
388
628
  return fields;
389
629
  }
390
-
391
- // src/utils/variable-helpers.ts
392
- function extractVariablesFromContent(content) {
393
- const seen = /* @__PURE__ */ new Map();
394
- function walk(node) {
395
- if (node.type === "variableNode" && node.attrs) {
396
- const varName = node.attrs.varName;
397
- if (varName && !seen.has(varName)) {
398
- seen.set(varName, {
399
- varName,
400
- varLabel: node.attrs.varLabel || varName,
401
- varDefault: node.attrs.varDefault || ""
402
- });
403
- }
404
- }
405
- if (node.content) {
406
- node.content.forEach(walk);
407
- }
408
- }
409
- if (content.content) {
410
- content.content.forEach(walk);
411
- }
412
- return Array.from(seen.values());
413
- }
414
- function replaceVariablesInContent(content, values) {
415
- function walkNode(node) {
416
- if (node.type === "variableNode" && node.attrs) {
417
- const varName = node.attrs.varName;
418
- const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || varName}]`;
419
- const textNode = { type: "text", text: replacement };
420
- if (node.marks && node.marks.length > 0) {
421
- textNode.marks = node.marks;
422
- }
423
- return textNode;
424
- }
425
- if (node.content) {
426
- return { ...node, content: node.content.map(walkNode) };
427
- }
428
- return node;
429
- }
430
- return walkNode(content);
431
- }
432
-
433
- // src/utils/markdown-writer.ts
434
- function serializeFieldToken(attrs) {
435
- const parts = ["field"];
436
- if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
437
- if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
438
- if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
439
- if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
440
- if (attrs.required) parts.push(`required:true`);
441
- if (attrs.options) parts.push(`options:${attrs.options}`);
442
- if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
443
- if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
444
- if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
445
- if (attrs.multiline) parts.push(`multiline:true`);
446
- if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
447
- if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
448
- return `{{${parts.join("|")}}}`;
449
- }
450
- function serializeVariableToken(attrs) {
451
- const parts = ["var"];
452
- if (attrs.varName) parts.push(`name:${attrs.varName}`);
453
- if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
454
- if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
455
- return `{{${parts.join("|")}}}`;
456
- }
457
- function serializeInline(content) {
458
- if (!content) return "";
459
- let result = "";
460
- for (const node of content) {
461
- if (node.type === "fieldNode") {
462
- result += serializeFieldToken(node.attrs || {});
463
- continue;
464
- }
465
- if (node.type === "variableNode") {
466
- let token = serializeVariableToken(node.attrs || {});
467
- const varMarks = node.marks || [];
468
- const isBold = varMarks.some((m) => m.type === "bold");
469
- const isItalic = varMarks.some((m) => m.type === "italic");
470
- const isUnderline = varMarks.some((m) => m.type === "underline");
471
- if (isBold && isItalic) token = `***${token}***`;
472
- else if (isBold) token = `**${token}**`;
473
- else if (isItalic) token = `*${token}*`;
474
- if (isUnderline) token = `__${token}__`;
475
- result += token;
476
- continue;
477
- }
478
- if (node.type === "text") {
479
- let text = node.text || "";
480
- const marks = node.marks || [];
481
- for (const mark of marks) {
482
- if (mark.type === "bold") text = `**${text}**`;
483
- else if (mark.type === "italic") text = `*${text}*`;
484
- else if (mark.type === "underline") text = `__${text}__`;
485
- else if (mark.type === "code") text = `\`${text}\``;
486
- }
487
- result += text;
488
- }
489
- if (node.type === "hardBreak") {
490
- result += " \n";
491
- }
492
- }
493
- return result;
494
- }
495
- function serializeBlock(node) {
496
- switch (node.type) {
497
- case "heading": {
498
- const level = node.attrs?.level || 1;
499
- const prefix = "#".repeat(level);
500
- return `${prefix} ${serializeInline(node.content)}`;
501
- }
502
- case "paragraph": {
503
- const inline = serializeInline(node.content);
504
- return inline;
505
- }
506
- case "bulletList": {
507
- return (node.content || []).map((item) => {
508
- const inner = (item.content || []).map(serializeBlock).join("\n");
509
- return `- ${inner}`;
510
- }).join("\n");
511
- }
512
- case "orderedList": {
513
- return (node.content || []).map((item, i) => {
514
- const inner = (item.content || []).map(serializeBlock).join("\n");
515
- return `${i + 1}. ${inner}`;
516
- }).join("\n");
517
- }
518
- case "listItem": {
519
- return (node.content || []).map(serializeBlock).join("\n");
520
- }
521
- case "blockquote": {
522
- return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
523
- }
524
- case "codeBlock": {
525
- const text = serializeInline(node.content);
526
- return `\`\`\`
527
- ${text}
528
- \`\`\``;
529
- }
530
- case "horizontalRule": {
531
- return "---";
532
- }
533
- default:
534
- return serializeInline(node.content);
630
+ var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
631
+ var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
632
+ var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
633
+ var DIRECTIVE_CLOSE = /^:::$/;
634
+ function parseDirectiveAttrs(str) {
635
+ const attrs = {};
636
+ if (!str) return attrs;
637
+ const pairs = str.split("|");
638
+ for (const pair of pairs) {
639
+ const colonIdx = pair.indexOf(":");
640
+ if (colonIdx === -1) continue;
641
+ const key = pair.slice(0, colonIdx).trim();
642
+ const value = pair.slice(colonIdx + 1).trim();
643
+ if (key) attrs[key] = value;
535
644
  }
645
+ return attrs;
536
646
  }
537
- function tiptapToMarkdown(doc) {
538
- if (!doc.content) return "";
539
- return doc.content.map(serializeBlock).join("\n\n");
540
- }
541
- var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
542
647
  function injectMarks(token, marks) {
543
648
  const existingMatch = token.match(/\|_marks:([^}|]+)/);
544
649
  if (existingMatch) {
@@ -655,6 +760,15 @@ function parseVariableToken(tokenBody) {
655
760
  case "default":
656
761
  attrs.varDefault = value;
657
762
  break;
763
+ case "suppress":
764
+ attrs.suppressZero = value === "zero" ? "true" : "";
765
+ break;
766
+ case "format":
767
+ attrs.format = value;
768
+ break;
769
+ case "block":
770
+ attrs.block = value === "true" ? "true" : "";
771
+ break;
658
772
  case "_marks":
659
773
  attrs._marks = value;
660
774
  break;
@@ -662,6 +776,44 @@ function parseVariableToken(tokenBody) {
662
776
  }
663
777
  return attrs;
664
778
  }
779
+ function parseImageToken(tokenBody) {
780
+ const attrs = {
781
+ src: "",
782
+ varName: "",
783
+ alt: "",
784
+ width: "",
785
+ height: "",
786
+ align: ""
787
+ };
788
+ const pairs = tokenBody.split("|");
789
+ for (const pair of pairs) {
790
+ const colonIdx = pair.indexOf(":");
791
+ if (colonIdx === -1) continue;
792
+ const key = pair.slice(0, colonIdx).trim();
793
+ const value = pair.slice(colonIdx + 1).trim();
794
+ switch (key) {
795
+ case "src":
796
+ attrs.src = value;
797
+ break;
798
+ case "var":
799
+ attrs.varName = value;
800
+ break;
801
+ case "alt":
802
+ attrs.alt = value;
803
+ break;
804
+ case "width":
805
+ attrs.width = value;
806
+ break;
807
+ case "height":
808
+ attrs.height = value;
809
+ break;
810
+ case "align":
811
+ attrs.align = value;
812
+ break;
813
+ }
814
+ }
815
+ return attrs;
816
+ }
665
817
  function parseInline(text) {
666
818
  text = preprocessMarkedTokens(text);
667
819
  const nodes = [];
@@ -685,6 +837,8 @@ function parseInline(text) {
685
837
  const node = { type: "variableNode", attrs };
686
838
  if (marks.length > 0) node.marks = marks;
687
839
  nodes.push(node);
840
+ } else if (match[1] === "image") {
841
+ nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
688
842
  }
689
843
  lastIndex = match.index + match[0].length;
690
844
  }
@@ -696,45 +850,63 @@ function parseInline(text) {
696
850
  function parseFormattedText(text) {
697
851
  if (!text) return [];
698
852
  const nodes = [];
699
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
853
+ const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
700
854
  for (const part of parts) {
701
855
  if (!part) continue;
702
- if (part.startsWith("**") && part.endsWith("**")) {
703
- nodes.push({
704
- type: "text",
705
- text: part.slice(2, -2),
706
- marks: [{ type: "bold" }]
707
- });
856
+ if (part.startsWith("***") && part.endsWith("***")) {
857
+ const inner = part.slice(3, -3);
858
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
859
+ } else if (part.startsWith("**") && part.endsWith("**")) {
860
+ const inner = part.slice(2, -2);
861
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
708
862
  } else if (part.startsWith("*") && part.endsWith("*")) {
709
- nodes.push({
710
- type: "text",
711
- text: part.slice(1, -1),
712
- marks: [{ type: "italic" }]
713
- });
863
+ const inner = part.slice(1, -1);
864
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
714
865
  } else if (part.startsWith("__") && part.endsWith("__")) {
715
- nodes.push({
716
- type: "text",
717
- text: part.slice(2, -2),
718
- marks: [{ type: "underline" }]
719
- });
866
+ const inner = part.slice(2, -2);
867
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
720
868
  } else if (part.startsWith("`") && part.endsWith("`")) {
721
- nodes.push({
722
- type: "text",
723
- text: part.slice(1, -1),
724
- marks: [{ type: "code" }]
725
- });
869
+ const inner = part.slice(1, -1);
870
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
726
871
  } else {
727
872
  nodes.push({ type: "text", text: part });
728
873
  }
729
874
  }
730
875
  return nodes;
731
876
  }
732
- function markdownToTiptap(markdown) {
733
- const lines = markdown.split("\n");
877
+ function splitTableCells(row) {
878
+ const cells = [];
879
+ let current = "";
880
+ let depth = 0;
881
+ for (let i = 0; i < row.length; i++) {
882
+ if (row[i] === "{" && row[i + 1] === "{") {
883
+ depth++;
884
+ current += "{{";
885
+ i++;
886
+ } else if (row[i] === "}" && row[i + 1] === "}") {
887
+ depth = Math.max(0, depth - 1);
888
+ current += "}}";
889
+ i++;
890
+ } else if (row[i] === "|" && depth === 0) {
891
+ cells.push(current.trim());
892
+ current = "";
893
+ } else {
894
+ current += row[i];
895
+ }
896
+ }
897
+ if (current.trim()) {
898
+ cells.push(current.trim());
899
+ }
900
+ return cells;
901
+ }
902
+ function parseBlocks(lines, startIdx, stopCondition) {
734
903
  const content = [];
735
- let i = 0;
904
+ let i = startIdx;
736
905
  while (i < lines.length) {
737
906
  const line = lines[i];
907
+ if (stopCondition && stopCondition(line)) {
908
+ break;
909
+ }
738
910
  if (line.trim() === "") {
739
911
  i++;
740
912
  continue;
@@ -808,13 +980,443 @@ function markdownToTiptap(markdown) {
808
980
  });
809
981
  continue;
810
982
  }
811
- content.push({
812
- type: "paragraph",
813
- content: parseInline(line)
814
- });
983
+ if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
984
+ const tableLines = [];
985
+ while (i < lines.length) {
986
+ const tl = lines[i];
987
+ if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
988
+ tableLines.push(tl);
989
+ i++;
990
+ } else {
991
+ break;
992
+ }
993
+ }
994
+ const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
995
+ const dataLines = tableLines.filter(
996
+ (tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
997
+ );
998
+ const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
999
+ const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
1000
+ const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
1001
+ const tableRows = [];
1002
+ let isFirstDataRow = true;
1003
+ for (const tl of tableLines) {
1004
+ if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
1005
+ const cells = splitTableCells(tl).filter((c) => c !== "");
1006
+ if (isBorderless && isFirstDataRow) {
1007
+ isFirstDataRow = false;
1008
+ continue;
1009
+ }
1010
+ const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
1011
+ const rowContent = cells.map((cellText) => ({
1012
+ type: cellType,
1013
+ content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
1014
+ }));
1015
+ tableRows.push({ type: "tableRow", content: rowContent });
1016
+ isFirstDataRow = false;
1017
+ }
1018
+ if (tableRows.length > 0) {
1019
+ const tableNode = { type: "table", content: tableRows };
1020
+ if (isBorderless) {
1021
+ tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
1022
+ }
1023
+ content.push(tableNode);
1024
+ }
1025
+ continue;
1026
+ }
1027
+ const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
1028
+ if (directiveMatch) {
1029
+ const directiveType = directiveMatch[1];
1030
+ const rawAttrs = directiveMatch[2] || "";
1031
+ const attrs = parseDirectiveAttrs(rawAttrs);
1032
+ i++;
1033
+ if (directiveType === "watermark") {
1034
+ content.push({
1035
+ type: "watermark",
1036
+ attrs: {
1037
+ text: attrs.text || "",
1038
+ opacity: attrs.opacity || "0.15",
1039
+ angle: attrs.angle || "-45"
1040
+ }
1041
+ });
1042
+ continue;
1043
+ }
1044
+ if (directiveType === "panel") {
1045
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1046
+ i = inner.nextIdx;
1047
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1048
+ i++;
1049
+ }
1050
+ content.push({
1051
+ type: "panel",
1052
+ attrs: {
1053
+ title: attrs.title || "",
1054
+ border: attrs.border || "solid",
1055
+ headerStyle: attrs.headerStyle || ""
1056
+ },
1057
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1058
+ });
1059
+ continue;
1060
+ }
1061
+ if (directiveType === "repeat") {
1062
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1063
+ i = inner.nextIdx;
1064
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1065
+ i++;
1066
+ }
1067
+ content.push({
1068
+ type: "repeatBlock",
1069
+ attrs: { data: attrs.data || "" },
1070
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1071
+ });
1072
+ continue;
1073
+ }
1074
+ if (directiveType === "subtotals") {
1075
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1076
+ i = inner.nextIdx;
1077
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1078
+ i++;
1079
+ }
1080
+ content.push({
1081
+ type: "subtotalsBlock",
1082
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1083
+ });
1084
+ continue;
1085
+ }
1086
+ if (directiveType === "columns") {
1087
+ const splitVal = attrs.split || "50";
1088
+ const columns = [];
1089
+ while (i < lines.length) {
1090
+ const colLine = lines[i].trim();
1091
+ if (DIRECTIVE_CLOSE.test(colLine)) {
1092
+ if (columns.length > 0) {
1093
+ i++;
1094
+ break;
1095
+ }
1096
+ i++;
1097
+ break;
1098
+ }
1099
+ const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
1100
+ if (colMatch) {
1101
+ i++;
1102
+ const colInner = parseBlocks(lines, i, (l) => {
1103
+ const trimmed = l.trim();
1104
+ return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
1105
+ });
1106
+ i = colInner.nextIdx;
1107
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1108
+ i++;
1109
+ }
1110
+ const colAttrsRaw = colMatch[1] || "";
1111
+ const colAttrs = parseDirectiveAttrs(colAttrsRaw);
1112
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
1113
+ columns.push({
1114
+ type: "column",
1115
+ attrs: padTop ? { padTop } : void 0,
1116
+ content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
1117
+ });
1118
+ continue;
1119
+ }
1120
+ if (colLine === "") {
1121
+ i++;
1122
+ continue;
1123
+ }
1124
+ break;
1125
+ }
1126
+ const columnsAttrs = { split: splitVal };
1127
+ if (attrs.padX) columnsAttrs.padX = attrs.padX;
1128
+ content.push({
1129
+ type: "columns",
1130
+ attrs: columnsAttrs,
1131
+ content: columns.length > 0 ? columns : [
1132
+ { type: "column", content: [{ type: "paragraph" }] },
1133
+ { type: "column", content: [{ type: "paragraph" }] }
1134
+ ]
1135
+ });
1136
+ continue;
1137
+ }
1138
+ if (directiveType === "col") {
1139
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1140
+ i = inner.nextIdx;
1141
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1142
+ i++;
1143
+ }
1144
+ for (const block of inner.blocks) {
1145
+ content.push(block);
1146
+ }
1147
+ continue;
1148
+ }
1149
+ continue;
1150
+ }
1151
+ if (DIRECTIVE_CLOSE.test(line.trim())) {
1152
+ i++;
1153
+ continue;
1154
+ }
1155
+ const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
1156
+ if (imageMatch) {
1157
+ content.push({
1158
+ type: "imageNode",
1159
+ attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
1160
+ });
1161
+ i++;
1162
+ continue;
1163
+ }
1164
+ const inlineNodes = parseInline(line);
1165
+ if (inlineNodes.some((n) => n.type === "imageNode")) {
1166
+ let run = [];
1167
+ const flushRun = () => {
1168
+ if (run.some((n) => !(n.type === "text" && !(n.text || "").trim()))) {
1169
+ content.push({ type: "paragraph", content: run });
1170
+ }
1171
+ run = [];
1172
+ };
1173
+ for (const n of inlineNodes) {
1174
+ if (n.type === "imageNode") {
1175
+ flushRun();
1176
+ content.push(n);
1177
+ } else {
1178
+ run.push(n);
1179
+ }
1180
+ }
1181
+ flushRun();
1182
+ } else {
1183
+ content.push({ type: "paragraph", content: inlineNodes });
1184
+ }
815
1185
  i++;
816
1186
  }
817
- return { type: "doc", content };
1187
+ return { blocks: content, nextIdx: i };
1188
+ }
1189
+ function markdownToTiptap(markdown) {
1190
+ const lines = markdown.split("\n");
1191
+ const { blocks } = parseBlocks(lines, 0);
1192
+ return { type: "doc", content: blocks };
1193
+ }
1194
+
1195
+ // src/utils/variable-helpers.ts
1196
+ function extractVariablesFromContent(content) {
1197
+ const seen = /* @__PURE__ */ new Map();
1198
+ function walk(node) {
1199
+ if (node.type === "variableNode" && node.attrs) {
1200
+ const varName = node.attrs.varName;
1201
+ if (varName && !seen.has(varName)) {
1202
+ seen.set(varName, {
1203
+ varName,
1204
+ varLabel: node.attrs.varLabel || varName,
1205
+ varDefault: node.attrs.varDefault || ""
1206
+ });
1207
+ }
1208
+ }
1209
+ if (node.content) {
1210
+ node.content.forEach(walk);
1211
+ }
1212
+ }
1213
+ if (content.content) {
1214
+ content.content.forEach(walk);
1215
+ }
1216
+ return Array.from(seen.values());
1217
+ }
1218
+ function applyFormat(value, format) {
1219
+ if (format === "phone") {
1220
+ const digits = value.replace(/\D/g, "");
1221
+ if (digits.length === 10) {
1222
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
1223
+ }
1224
+ }
1225
+ return value;
1226
+ }
1227
+ function replaceVariablesInContent(content, values) {
1228
+ function walkNode(node) {
1229
+ if (node.type === "variableNode" && node.attrs) {
1230
+ const varName = node.attrs.varName;
1231
+ let replacement = values[varName] || node.attrs.varDefault || "";
1232
+ const format = node.attrs.format;
1233
+ if (format && replacement) {
1234
+ replacement = applyFormat(replacement, format);
1235
+ }
1236
+ const textNode = { type: "text", text: replacement };
1237
+ if (node.marks && node.marks.length > 0) {
1238
+ textNode.marks = node.marks;
1239
+ }
1240
+ return textNode;
1241
+ }
1242
+ if (node.type === "imageNode" && node.attrs?.varName) {
1243
+ const varName = node.attrs.varName;
1244
+ const resolved = values[varName] || node.attrs.src || "";
1245
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
1246
+ }
1247
+ if (node.content) {
1248
+ return { ...node, content: node.content.map(walkNode) };
1249
+ }
1250
+ return node;
1251
+ }
1252
+ return walkNode(content);
1253
+ }
1254
+ function expandBlockVariables(content, values) {
1255
+ function soleVariableOf(node) {
1256
+ if (node.type !== "paragraph" || !node.content) return null;
1257
+ const meaningful = node.content.filter(
1258
+ (c) => !(c.type === "text" && !(c.text || "").trim())
1259
+ );
1260
+ if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
1261
+ return meaningful[0];
1262
+ }
1263
+ return null;
1264
+ }
1265
+ function stripUnsafeNodes(node) {
1266
+ if (!node.content) return node;
1267
+ return {
1268
+ ...node,
1269
+ content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
1270
+ };
1271
+ }
1272
+ function isCellSafe(blocks) {
1273
+ return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
1274
+ }
1275
+ function walkNode(node, inCell) {
1276
+ if (!node.content) return node;
1277
+ const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
1278
+ const newContent = [];
1279
+ for (const child of node.content) {
1280
+ const varNode = soleVariableOf(child);
1281
+ if (varNode?.attrs) {
1282
+ const varName = varNode.attrs.varName;
1283
+ const value = values[varName] || varNode.attrs.varDefault || "";
1284
+ const isBlock = varNode.attrs.block === "true" || value.includes("\n");
1285
+ if (isBlock) {
1286
+ const parsed = stripUnsafeNodes(markdownToTiptap(value));
1287
+ const blocks = parsed.content || [];
1288
+ if (!childInCell || isCellSafe(blocks)) {
1289
+ newContent.push(...blocks);
1290
+ continue;
1291
+ }
1292
+ }
1293
+ }
1294
+ newContent.push(walkNode(child, childInCell));
1295
+ }
1296
+ return { ...node, content: newContent };
1297
+ }
1298
+ return walkNode(content, false);
1299
+ }
1300
+ function labelToVarName(label) {
1301
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1302
+ }
1303
+ function isZeroLike(value) {
1304
+ if (!value || value.trim() === "") return true;
1305
+ const cleaned = value.replace(/[$,\s]/g, "");
1306
+ return /^-?0+(\.0+)?$/.test(cleaned);
1307
+ }
1308
+ function suppressZeroContent(content, values) {
1309
+ function hasSuppressedVar(node) {
1310
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
1311
+ const varName = node.attrs.varName;
1312
+ const val = values[varName] || node.attrs.varDefault || "";
1313
+ return isZeroLike(val);
1314
+ }
1315
+ return (node.content || []).some(hasSuppressedVar);
1316
+ }
1317
+ function walkNode(node) {
1318
+ if (node.type === "table" && node.content) {
1319
+ const filtered = node.content.filter((row) => {
1320
+ if (row.type !== "tableRow") return true;
1321
+ const isHeader = row.content?.[0]?.type === "tableHeader";
1322
+ if (isHeader) return true;
1323
+ return !hasSuppressedVar(row);
1324
+ });
1325
+ const bodyRows = filtered.filter(
1326
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
1327
+ );
1328
+ if (bodyRows.length === 0) return null;
1329
+ return { ...node, content: filtered };
1330
+ }
1331
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
1332
+ return null;
1333
+ }
1334
+ if (node.content) {
1335
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
1336
+ return { ...node, content: filtered };
1337
+ }
1338
+ return node;
1339
+ }
1340
+ return walkNode(content) || content;
1341
+ }
1342
+ function expandRepeatContent(content, values) {
1343
+ const enrichedValues = { ...values };
1344
+ function cloneNode(node, dataKey, index) {
1345
+ if (node.type === "variableNode" && node.attrs?.varName) {
1346
+ const oldName = node.attrs.varName;
1347
+ const bareName = oldName.replace(/^operation_/, "");
1348
+ const newName = `${dataKey}_${index}_${bareName}`;
1349
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
1350
+ }
1351
+ if (node.content) {
1352
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
1353
+ }
1354
+ return { ...node };
1355
+ }
1356
+ function walkNode(node) {
1357
+ if (node.type === "repeatBlock" && node.attrs?.data) {
1358
+ const dataKey = node.attrs.data;
1359
+ const arrayJson = values[dataKey];
1360
+ if (!arrayJson) return [];
1361
+ let items;
1362
+ try {
1363
+ items = JSON.parse(arrayJson);
1364
+ } catch {
1365
+ return [];
1366
+ }
1367
+ const sums = /* @__PURE__ */ new Map();
1368
+ for (const item of items) {
1369
+ for (const [key, val] of Object.entries(item)) {
1370
+ const num = parseFloat(val);
1371
+ if (!isNaN(num)) {
1372
+ sums.set(key, (sums.get(key) || 0) + num);
1373
+ }
1374
+ }
1375
+ }
1376
+ const woKeysByNorm = /* @__PURE__ */ new Map();
1377
+ for (const k of Object.keys(enrichedValues)) {
1378
+ if (k.startsWith("workorder_")) {
1379
+ const norm = k.replace(/_/g, "");
1380
+ woKeysByNorm.set(norm, k);
1381
+ }
1382
+ }
1383
+ for (const [field, total] of sums) {
1384
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
1385
+ const directKey = `workorder_${field}`;
1386
+ enrichedValues[directKey] = formatted;
1387
+ const norm = `workorder${field}`.replace(/_/g, "");
1388
+ const existing = woKeysByNorm.get(norm);
1389
+ if (existing && existing !== directKey) {
1390
+ enrichedValues[existing] = formatted;
1391
+ }
1392
+ }
1393
+ const expanded = [];
1394
+ for (let i = 0; i < items.length; i++) {
1395
+ const item = items[i];
1396
+ for (const [key, val] of Object.entries(item)) {
1397
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
1398
+ }
1399
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
1400
+ expanded.push(...cloned);
1401
+ }
1402
+ return expanded;
1403
+ }
1404
+ if (node.content) {
1405
+ const newContent = [];
1406
+ for (const child of node.content) {
1407
+ const result2 = walkNode(child);
1408
+ if (Array.isArray(result2)) newContent.push(...result2);
1409
+ else newContent.push(result2);
1410
+ }
1411
+ return { ...node, content: newContent };
1412
+ }
1413
+ return node;
1414
+ }
1415
+ const result = walkNode(content);
1416
+ return {
1417
+ content: Array.isArray(result) ? { ...content, content: result } : result,
1418
+ values: enrichedValues
1419
+ };
818
1420
  }
819
1421
 
820
1422
  // src/utils/error-helpers.ts
@@ -952,10 +1554,40 @@ function buildMetadataObject(fields, actualFieldNames) {
952
1554
  // src/utils/pdf-generator.ts
953
1555
  var PAGE_WIDTH = 595.28;
954
1556
  var PAGE_HEIGHT = 841.89;
955
- var MARGIN = 72;
1557
+ var MARGIN = 40;
956
1558
  var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
957
1559
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
958
1560
  var LINE_HEIGHT_FACTOR = 1.4;
1561
+ var BODY_FONT_SIZE = 10;
1562
+ function hexToRgbColor(hex) {
1563
+ if (!hex) return null;
1564
+ const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
1565
+ if (!m) return null;
1566
+ const n = parseInt(m[1], 16);
1567
+ return pdfLib.rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
1568
+ }
1569
+ function contrastTextColor(fill) {
1570
+ const c = fill;
1571
+ const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
1572
+ return luminance > 0.6 ? pdfLib.rgb(0, 0, 0) : pdfLib.rgb(1, 1, 1);
1573
+ }
1574
+ function resolveTheme(theme) {
1575
+ const primary = hexToRgbColor(theme?.primaryColor);
1576
+ const accent = hexToRgbColor(theme?.accentColor);
1577
+ const border = hexToRgbColor(theme?.borderColor);
1578
+ return {
1579
+ tableHeaderBg: primary ?? pdfLib.rgb(0.3, 0.3, 0.3),
1580
+ tableHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
1581
+ tableBorder: border ?? pdfLib.rgb(0.75, 0.75, 0.75),
1582
+ panelDarkHeaderBg: primary ?? pdfLib.rgb(0.25, 0.25, 0.25),
1583
+ panelDarkHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
1584
+ panelBorder: border ?? pdfLib.rgb(0.6, 0.6, 0.6),
1585
+ rule: primary ?? pdfLib.rgb(0, 0, 0),
1586
+ pageHeaderText: primary ?? pdfLib.rgb(0, 0, 0),
1587
+ pageHeaderRule: accent ?? primary ?? pdfLib.rgb(0.75, 0.75, 0.75)
1588
+ };
1589
+ }
1590
+ var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
959
1591
  var FIELD_DISPLAY = {
960
1592
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
961
1593
  ["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
@@ -986,17 +1618,65 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
986
1618
  const height = textLineHeight + 4;
987
1619
  return { width, height };
988
1620
  }
1621
+ async function loadImageBytes(src) {
1622
+ if (src.startsWith("data:")) {
1623
+ const comma = src.indexOf(",");
1624
+ if (comma === -1) throw new Error("malformed data URL");
1625
+ const meta = src.slice(0, comma);
1626
+ if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
1627
+ const bin = atob(src.slice(comma + 1));
1628
+ const bytes = new Uint8Array(bin.length);
1629
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1630
+ return bytes;
1631
+ }
1632
+ const res = await fetch(src);
1633
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1634
+ return new Uint8Array(await res.arrayBuffer());
1635
+ }
1636
+ function describeImageSrc(src) {
1637
+ return src.length > 80 ? `${src.slice(0, 77)}...` : src;
1638
+ }
1639
+ async function preloadImages(pdfDoc, content) {
1640
+ const srcs = /* @__PURE__ */ new Set();
1641
+ (function walk(node) {
1642
+ if (node.type === "imageNode") {
1643
+ const src = node.attrs?.src || "";
1644
+ if (src) srcs.add(src);
1645
+ }
1646
+ for (const child of node.content || []) walk(child);
1647
+ })(content);
1648
+ const images = /* @__PURE__ */ new Map();
1649
+ const warnings = [];
1650
+ for (const src of srcs) {
1651
+ try {
1652
+ const bytes = await loadImageBytes(src);
1653
+ const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
1654
+ const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
1655
+ if (isPng) {
1656
+ images.set(src, await pdfDoc.embedPng(bytes));
1657
+ } else if (isJpeg) {
1658
+ images.set(src, await pdfDoc.embedJpg(bytes));
1659
+ } else {
1660
+ warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
1661
+ }
1662
+ } catch (err) {
1663
+ warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
1664
+ }
1665
+ }
1666
+ return { images, warnings };
1667
+ }
989
1668
  function ensureSpace(state, neededHeight) {
990
1669
  if (state.y + neededHeight > CONTENT_HEIGHT) {
991
1670
  const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
992
1671
  state.currentPage = newPage;
993
1672
  state.pageIndex++;
994
1673
  state.y = 0;
1674
+ renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
995
1675
  }
996
1676
  }
997
- function drawText(state, text, font, fontSize, indent = 0) {
1677
+ function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
998
1678
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
999
- const maxWidth = CONTENT_WIDTH - indent;
1679
+ const maxWidth = region.width - indent;
1000
1680
  const words = text.split(/\s+/);
1001
1681
  let line = "";
1002
1682
  let totalAdvance = 0;
@@ -1007,7 +1687,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
1007
1687
  ensureSpace(state, lineHeight);
1008
1688
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1009
1689
  state.currentPage.drawText(line, {
1010
- x: MARGIN + indent,
1690
+ x: region.leftX + indent,
1011
1691
  y: pdfY,
1012
1692
  size: fontSize,
1013
1693
  font,
@@ -1024,7 +1704,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
1024
1704
  ensureSpace(state, lineHeight);
1025
1705
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1026
1706
  state.currentPage.drawText(line, {
1027
- x: MARGIN + indent,
1707
+ x: region.leftX + indent,
1028
1708
  y: pdfY,
1029
1709
  size: fontSize,
1030
1710
  font,
@@ -1068,10 +1748,10 @@ function collectInlineSegments(content, fonts, fontSize) {
1068
1748
  }
1069
1749
  return segments;
1070
1750
  }
1071
- function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1751
+ function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
1072
1752
  const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
1073
- const maxX = MARGIN + CONTENT_WIDTH;
1074
- const startX = MARGIN + indent;
1753
+ const maxX = region.leftX + region.width;
1754
+ const startX = region.leftX + indent;
1075
1755
  let currentX = startX;
1076
1756
  let currentLineHeight = 0;
1077
1757
  let lineHasContent = false;
@@ -1176,13 +1856,318 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1176
1856
  }
1177
1857
  flushLine();
1178
1858
  }
1179
- function processInlineContent(state, content, fontSize, indent = 0) {
1859
+ function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
1180
1860
  if (!content) return;
1181
1861
  const segments = collectInlineSegments(content, state.fonts, fontSize);
1182
1862
  if (segments.length === 0) return;
1183
- layoutInlineSegments(state, segments, fontSize, indent);
1863
+ layoutInlineSegments(state, segments, fontSize, indent, region);
1864
+ }
1865
+ function extractCellText(cell) {
1866
+ const parts = [];
1867
+ for (const child of cell.content || []) {
1868
+ if (child.type === "paragraph" || child.type === "heading") {
1869
+ for (const inline of child.content || []) {
1870
+ if (inline.type === "text") {
1871
+ parts.push(inline.text || "");
1872
+ } else if (inline.type === "variableNode") {
1873
+ const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
1874
+ parts.push(`[${label}]`);
1875
+ } else if (inline.type === "fieldNode") {
1876
+ const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
1877
+ parts.push(`[ ${label} ]`);
1878
+ }
1879
+ }
1880
+ }
1881
+ }
1882
+ return parts.join("");
1184
1883
  }
1185
- function processBlock(state, node) {
1884
+ function isNumericText(text) {
1885
+ return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
1886
+ }
1887
+ function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
1888
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1889
+ const font = isHeader ? fonts.bold : fonts.regular;
1890
+ let totalHeight = 0;
1891
+ for (const child of cell.content || []) {
1892
+ if (child.type === "paragraph" || child.type === "heading") {
1893
+ const text = extractCellText({ content: [child] });
1894
+ if (!text) {
1895
+ totalHeight += lineHeight;
1896
+ continue;
1897
+ }
1898
+ const words = text.split(/\s+/);
1899
+ let line = "";
1900
+ let lineCount = 0;
1901
+ for (const word of words) {
1902
+ const testLine = line ? `${line} ${word}` : word;
1903
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
1904
+ if (testWidth > maxWidth && line) {
1905
+ lineCount++;
1906
+ line = word;
1907
+ } else {
1908
+ line = testLine;
1909
+ }
1910
+ }
1911
+ if (line) lineCount++;
1912
+ totalHeight += lineCount * lineHeight;
1913
+ }
1914
+ }
1915
+ return Math.max(totalHeight, lineHeight);
1916
+ }
1917
+ function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = pdfLib.rgb(0, 0, 0)) {
1918
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1919
+ const cellPadding = 4;
1920
+ const maxTextWidth = cellWidth - cellPadding * 2;
1921
+ let textX = cellX + cellPadding;
1922
+ let textY = cellY;
1923
+ for (const child of cell.content || []) {
1924
+ if (child.type === "paragraph" || child.type === "heading") {
1925
+ const segments = collectInlineSegments(
1926
+ child.content || [],
1927
+ isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
1928
+ fontSize
1929
+ );
1930
+ if (rightAlign && segments.length > 0) {
1931
+ const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
1932
+ const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
1933
+ if (textWidth <= maxTextWidth) {
1934
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1935
+ state.currentPage.drawText(fullText, {
1936
+ x: cellX + cellWidth - cellPadding - textWidth,
1937
+ y: pdfY,
1938
+ size: fontSize,
1939
+ font: isHeader ? fonts.bold : fonts.regular,
1940
+ color: textColor
1941
+ });
1942
+ return;
1943
+ }
1944
+ }
1945
+ let currentX = textX;
1946
+ for (const seg of segments) {
1947
+ if (seg.kind === "break") {
1948
+ textY += lineHeight;
1949
+ currentX = textX;
1950
+ continue;
1951
+ }
1952
+ if (seg.kind === "text") {
1953
+ const words = seg.text.split(/(\s+)/);
1954
+ const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
1955
+ for (const token of words) {
1956
+ if (!token) continue;
1957
+ if (/^\s+$/.test(token)) {
1958
+ currentX += spaceWidth;
1959
+ continue;
1960
+ }
1961
+ const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
1962
+ if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
1963
+ textY += lineHeight;
1964
+ currentX = textX;
1965
+ }
1966
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1967
+ state.currentPage.drawText(token, {
1968
+ x: currentX,
1969
+ y: pdfY,
1970
+ size: fontSize,
1971
+ font: seg.font,
1972
+ color: textColor
1973
+ });
1974
+ currentX += wordWidth;
1975
+ }
1976
+ } else if (seg.kind === "field") {
1977
+ const pdfY = PAGE_HEIGHT - textY - seg.height;
1978
+ if (state.drawFieldPlaceholders) {
1979
+ state.currentPage.drawRectangle({
1980
+ x: currentX,
1981
+ y: pdfY,
1982
+ width: seg.width,
1983
+ height: seg.height,
1984
+ borderColor: pdfLib.rgb(0.5, 0.5, 0.7),
1985
+ borderWidth: 0.5,
1986
+ color: pdfLib.rgb(0.95, 0.95, 1),
1987
+ borderDashArray: [4, 2]
1988
+ });
1989
+ }
1990
+ const fieldId = seg.attrs.fieldId;
1991
+ if (fieldId) {
1992
+ state.fieldPositions.set(fieldId, {
1993
+ x: currentX,
1994
+ y: pdfY,
1995
+ width: seg.width,
1996
+ height: seg.height,
1997
+ page: state.pageIndex + 1
1998
+ });
1999
+ }
2000
+ currentX += seg.width + 4;
2001
+ }
2002
+ }
2003
+ }
2004
+ }
2005
+ }
2006
+ function renderTable(state, node, region = DEFAULT_REGION) {
2007
+ const tableRows = node.content || [];
2008
+ if (tableRows.length === 0) return;
2009
+ const borderless = node.attrs?.borderless === true;
2010
+ const subtotals = node.attrs?.subtotals === true;
2011
+ const fontSize = 10;
2012
+ const cellPadding = borderless ? 2 : 4;
2013
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
2014
+ const rowPadding = cellPadding * 2;
2015
+ const borderColor = state.theme.tableBorder;
2016
+ const headerBg = state.theme.tableHeaderBg;
2017
+ const headerTextColor = state.theme.tableHeaderText;
2018
+ let colCount = 0;
2019
+ for (const row of tableRows) {
2020
+ colCount = Math.max(colCount, row.content?.length || 0);
2021
+ }
2022
+ if (colCount === 0) return;
2023
+ const colMaxWidths = new Array(colCount).fill(0);
2024
+ for (const row of tableRows) {
2025
+ const cells = row.content || [];
2026
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2027
+ const cell = cells[c];
2028
+ const isHeader = cell.type === "tableHeader";
2029
+ const text = extractCellText(cell);
2030
+ const font = isHeader ? state.fonts.bold : state.fonts.regular;
2031
+ const textWidth = font.widthOfTextAtSize(text, fontSize);
2032
+ colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
2033
+ }
2034
+ }
2035
+ const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
2036
+ const minColWidth = 40;
2037
+ let colWidths;
2038
+ if (totalMeasured <= region.width) {
2039
+ const scale = region.width / totalMeasured;
2040
+ colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
2041
+ } else {
2042
+ colWidths = colMaxWidths.map(
2043
+ (w) => Math.max(w / totalMeasured * region.width, minColWidth)
2044
+ );
2045
+ }
2046
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
2047
+ if (colSum > 0) {
2048
+ const normFactor = region.width / colSum;
2049
+ colWidths = colWidths.map((w) => w * normFactor);
2050
+ }
2051
+ const headerRow = tableRows[0];
2052
+ const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
2053
+ function renderRow(row, isHeaderRow) {
2054
+ const cells = row.content || [];
2055
+ const isHeader = isHeaderRow && hasHeader;
2056
+ let rowHeight = lineHeight;
2057
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2058
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
2059
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
2060
+ rowHeight = Math.max(rowHeight, cellH);
2061
+ }
2062
+ rowHeight += rowPadding;
2063
+ ensureSpace(state, rowHeight);
2064
+ const rowTopY = state.y;
2065
+ let cellX = region.leftX;
2066
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2067
+ const cellW = colWidths[c];
2068
+ const cell = cells[c];
2069
+ const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
2070
+ const pdfCellBottom = pdfCellTop - rowHeight;
2071
+ if (isHeader && !borderless) {
2072
+ state.currentPage.drawRectangle({
2073
+ x: cellX,
2074
+ y: pdfCellBottom,
2075
+ width: cellW,
2076
+ height: rowHeight,
2077
+ color: headerBg
2078
+ });
2079
+ }
2080
+ if (!borderless) {
2081
+ state.currentPage.drawRectangle({
2082
+ x: cellX,
2083
+ y: pdfCellBottom,
2084
+ width: cellW,
2085
+ height: rowHeight,
2086
+ borderColor,
2087
+ borderWidth: 0.5
2088
+ });
2089
+ }
2090
+ const cellText = extractCellText(cell);
2091
+ const rightAlign = !isHeader && isNumericText(cellText);
2092
+ const contentY = MARGIN + rowTopY + cellPadding;
2093
+ const effectiveHeader = isHeader && !borderless;
2094
+ renderCellContent(
2095
+ state,
2096
+ cell,
2097
+ cellX,
2098
+ contentY,
2099
+ cellW,
2100
+ state.fonts,
2101
+ fontSize,
2102
+ effectiveHeader,
2103
+ rightAlign,
2104
+ effectiveHeader ? headerTextColor : pdfLib.rgb(0, 0, 0)
2105
+ );
2106
+ cellX += cellW;
2107
+ }
2108
+ state.y += rowHeight;
2109
+ }
2110
+ if (hasHeader && tableRows.length > 0) {
2111
+ renderRow(tableRows[0], true);
2112
+ }
2113
+ let lastPageIndex = state.pageIndex;
2114
+ const startIdx = hasHeader ? 1 : 0;
2115
+ const bodyRowCount = tableRows.length - startIdx;
2116
+ for (let r = startIdx; r < tableRows.length; r++) {
2117
+ const cells = tableRows[r].content || [];
2118
+ let estRowHeight = lineHeight;
2119
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2120
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
2121
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
2122
+ estRowHeight = Math.max(estRowHeight, cellH);
2123
+ }
2124
+ estRowHeight += rowPadding;
2125
+ if (state.y + estRowHeight > CONTENT_HEIGHT) {
2126
+ ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
2127
+ if (state.pageIndex !== lastPageIndex && hasHeader) {
2128
+ renderRow(headerRow, true);
2129
+ lastPageIndex = state.pageIndex;
2130
+ }
2131
+ }
2132
+ const rowTopYBeforeRender = state.y;
2133
+ renderRow(tableRows[r], false);
2134
+ lastPageIndex = state.pageIndex;
2135
+ if (subtotals && colCount >= 2) {
2136
+ const bodyIdx = r - startIdx;
2137
+ const isFirstBody = bodyIdx === 0;
2138
+ const isLastBody = bodyIdx === bodyRowCount - 1;
2139
+ if (isFirstBody || isLastBody) {
2140
+ const valueStartX = region.leftX + colWidths[0];
2141
+ const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
2142
+ const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
2143
+ const overlineColor = pdfLib.rgb(0, 0, 0);
2144
+ if (isFirstBody) {
2145
+ state.currentPage.drawLine({
2146
+ start: { x: valueStartX, y: pdfRowTopY },
2147
+ end: { x: valueEndX, y: pdfRowTopY },
2148
+ thickness: 0.5,
2149
+ color: overlineColor
2150
+ });
2151
+ } else if (isLastBody) {
2152
+ state.currentPage.drawLine({
2153
+ start: { x: valueStartX, y: pdfRowTopY + 2 },
2154
+ end: { x: valueEndX, y: pdfRowTopY + 2 },
2155
+ thickness: 0.5,
2156
+ color: overlineColor
2157
+ });
2158
+ state.currentPage.drawLine({
2159
+ start: { x: valueStartX, y: pdfRowTopY },
2160
+ end: { x: valueEndX, y: pdfRowTopY },
2161
+ thickness: 0.5,
2162
+ color: overlineColor
2163
+ });
2164
+ }
2165
+ }
2166
+ }
2167
+ }
2168
+ state.y += 3;
2169
+ }
2170
+ function processBlock(state, node, region = DEFAULT_REGION) {
1186
2171
  switch (node.type) {
1187
2172
  case "heading": {
1188
2173
  const level = node.attrs?.level || 1;
@@ -1199,34 +2184,34 @@ function processBlock(state, node) {
1199
2184
  boldItalic: state.fonts.boldItalic
1200
2185
  };
1201
2186
  const segments = collectInlineSegments(node.content, headingFonts, fontSize);
1202
- layoutInlineSegments(state, segments, fontSize);
2187
+ layoutInlineSegments(state, segments, fontSize, 0, region);
1203
2188
  state.y += spacing / 2;
1204
2189
  break;
1205
2190
  }
1206
2191
  case "paragraph": {
1207
- const fontSize = 12;
2192
+ const fontSize = BODY_FONT_SIZE;
1208
2193
  if (!node.content || node.content.length === 0) {
1209
- state.y += fontSize * LINE_HEIGHT_FACTOR;
2194
+ state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
1210
2195
  break;
1211
2196
  }
1212
- processInlineContent(state, node.content, fontSize);
1213
- state.y += 4;
2197
+ processInlineContent(state, node.content, fontSize, 0, region);
2198
+ state.y += 2;
1214
2199
  break;
1215
2200
  }
1216
2201
  case "bulletList": {
1217
2202
  for (const item of node.content || []) {
1218
2203
  for (const child of item.content || []) {
1219
- const fontSize = 12;
2204
+ const fontSize = BODY_FONT_SIZE;
1220
2205
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1221
2206
  const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1222
2207
  state.currentPage.drawText("\u2022", {
1223
- x: MARGIN + 8,
2208
+ x: region.leftX + 8,
1224
2209
  y: bulletPdfY,
1225
2210
  size: fontSize,
1226
2211
  font: state.fonts.regular,
1227
2212
  color: pdfLib.rgb(0, 0, 0)
1228
2213
  });
1229
- processInlineContent(state, child.content, fontSize, 24);
2214
+ processInlineContent(state, child.content, fontSize, 24, region);
1230
2215
  }
1231
2216
  }
1232
2217
  state.y += 4;
@@ -1236,17 +2221,17 @@ function processBlock(state, node) {
1236
2221
  let num = 1;
1237
2222
  for (const item of node.content || []) {
1238
2223
  for (const child of item.content || []) {
1239
- const fontSize = 12;
2224
+ const fontSize = BODY_FONT_SIZE;
1240
2225
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1241
2226
  const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1242
2227
  state.currentPage.drawText(`${num}.`, {
1243
- x: MARGIN + 4,
2228
+ x: region.leftX + 4,
1244
2229
  y: numPdfY,
1245
2230
  size: fontSize,
1246
2231
  font: state.fonts.regular,
1247
2232
  color: pdfLib.rgb(0, 0, 0)
1248
2233
  });
1249
- processInlineContent(state, child.content, fontSize, 24);
2234
+ processInlineContent(state, child.content, fontSize, 24, region);
1250
2235
  }
1251
2236
  num++;
1252
2237
  }
@@ -1256,10 +2241,10 @@ function processBlock(state, node) {
1256
2241
  case "blockquote": {
1257
2242
  const startY = state.y;
1258
2243
  for (const child of node.content || []) {
1259
- processInlineContent(state, child.content, 12, 16);
2244
+ processInlineContent(state, child.content, 12, 16, region);
1260
2245
  }
1261
2246
  const endY = state.y;
1262
- const barX = MARGIN + 4;
2247
+ const barX = region.leftX + 4;
1263
2248
  const barTop = PAGE_HEIGHT - MARGIN - startY;
1264
2249
  const barBottom = PAGE_HEIGHT - MARGIN - endY;
1265
2250
  state.currentPage.drawLine({
@@ -1272,16 +2257,16 @@ function processBlock(state, node) {
1272
2257
  break;
1273
2258
  }
1274
2259
  case "horizontalRule": {
1275
- ensureSpace(state, 16);
1276
- state.y += 8;
2260
+ ensureSpace(state, 10);
2261
+ state.y += 4;
1277
2262
  const ruleY = PAGE_HEIGHT - MARGIN - state.y;
1278
2263
  state.currentPage.drawLine({
1279
- start: { x: MARGIN, y: ruleY },
1280
- end: { x: PAGE_WIDTH - MARGIN, y: ruleY },
1281
- thickness: 1,
1282
- color: pdfLib.rgb(0.7, 0.7, 0.7)
2264
+ start: { x: region.leftX, y: ruleY },
2265
+ end: { x: region.leftX + region.width, y: ruleY },
2266
+ thickness: 2.5,
2267
+ color: state.theme.rule
1283
2268
  });
1284
- state.y += 8;
2269
+ state.y += 4;
1285
2270
  break;
1286
2271
  }
1287
2272
  case "codeBlock": {
@@ -1293,9 +2278,9 @@ function processBlock(state, node) {
1293
2278
  ensureSpace(state, blockHeight);
1294
2279
  const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
1295
2280
  state.currentPage.drawRectangle({
1296
- x: MARGIN,
2281
+ x: region.leftX,
1297
2282
  y: boxY,
1298
- width: CONTENT_WIDTH,
2283
+ width: region.width,
1299
2284
  height: blockHeight,
1300
2285
  color: pdfLib.rgb(0.95, 0.95, 0.95),
1301
2286
  borderColor: pdfLib.rgb(0.85, 0.85, 0.85),
@@ -1303,18 +2288,325 @@ function processBlock(state, node) {
1303
2288
  });
1304
2289
  state.y += 8;
1305
2290
  for (const line of lines) {
1306
- drawText(state, line || " ", state.fonts.regular, fontSize, 8);
2291
+ drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
1307
2292
  }
1308
2293
  state.y += 8;
1309
2294
  break;
1310
2295
  }
2296
+ case "table": {
2297
+ renderTable(state, node, region);
2298
+ break;
2299
+ }
2300
+ case "imageNode": {
2301
+ const src = node.attrs?.src || "";
2302
+ const image = src ? state.images.get(src) : void 0;
2303
+ if (!image) break;
2304
+ const natW = image.width;
2305
+ const natH = image.height;
2306
+ let w = parseFloat(node.attrs?.width || "") || 0;
2307
+ let h = parseFloat(node.attrs?.height || "") || 0;
2308
+ if (w && !h) h = natH / natW * w;
2309
+ else if (h && !w) w = natW / natH * h;
2310
+ else if (!w && !h) {
2311
+ w = natW;
2312
+ h = natH;
2313
+ }
2314
+ if (w > region.width) {
2315
+ h = h * (region.width / w);
2316
+ w = region.width;
2317
+ }
2318
+ const maxH = CONTENT_HEIGHT * 0.6;
2319
+ if (h > maxH) {
2320
+ w = w * (maxH / h);
2321
+ h = maxH;
2322
+ }
2323
+ ensureSpace(state, h + 4);
2324
+ const align = node.attrs?.align || "left";
2325
+ let x = region.leftX;
2326
+ if (align === "center") x = region.leftX + (region.width - w) / 2;
2327
+ else if (align === "right") x = region.leftX + region.width - w;
2328
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
2329
+ state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
2330
+ state.y += h + 4;
2331
+ break;
2332
+ }
2333
+ // ─── Layout directives ─────────────────────────────────────────
2334
+ case "panel": {
2335
+ const title = node.attrs?.title || "";
2336
+ const border = node.attrs?.border || "solid";
2337
+ const headerStyle = node.attrs?.headerStyle || "";
2338
+ const padding = 8;
2339
+ const titleFontSize = 10;
2340
+ const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
2341
+ const panelStartY = state.y;
2342
+ const panelStartPage = state.pageIndex;
2343
+ if (title) {
2344
+ state.y += titleHeight;
2345
+ }
2346
+ const isHeaderOnly = border === "none";
2347
+ if (!isHeaderOnly) {
2348
+ state.y += padding;
2349
+ }
2350
+ const innerRegion = {
2351
+ leftX: region.leftX + padding,
2352
+ width: region.width - padding * 2
2353
+ };
2354
+ if (!isHeaderOnly) {
2355
+ for (const child of node.content || []) {
2356
+ processBlock(state, child, innerRegion);
2357
+ }
2358
+ state.y += padding;
2359
+ }
2360
+ const startPage = state.pdfDoc.getPages()[panelStartPage];
2361
+ if (title) {
2362
+ const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
2363
+ const isDarkHeader = headerStyle === "dark";
2364
+ startPage.drawRectangle({
2365
+ x: region.leftX,
2366
+ y: titleBarY,
2367
+ width: region.width,
2368
+ height: titleHeight,
2369
+ color: isDarkHeader ? state.theme.panelDarkHeaderBg : pdfLib.rgb(0.93, 0.93, 0.93),
2370
+ borderColor: state.theme.panelBorder,
2371
+ borderWidth: 0.75
2372
+ });
2373
+ startPage.drawText(title, {
2374
+ x: region.leftX + padding,
2375
+ y: titleBarY + 4,
2376
+ size: titleFontSize,
2377
+ font: state.fonts.bold,
2378
+ color: isDarkHeader ? state.theme.panelDarkHeaderText : pdfLib.rgb(0, 0, 0)
2379
+ });
2380
+ }
2381
+ if (border !== "none") {
2382
+ const borderDashArray = border === "dashed" ? [4, 2] : void 0;
2383
+ if (panelStartPage === state.pageIndex) {
2384
+ const borderY = PAGE_HEIGHT - MARGIN - state.y;
2385
+ const panelHeight = state.y - panelStartY;
2386
+ startPage.drawRectangle({
2387
+ x: region.leftX,
2388
+ y: borderY,
2389
+ width: region.width,
2390
+ height: panelHeight,
2391
+ borderColor: state.theme.panelBorder,
2392
+ borderWidth: 0.75,
2393
+ borderDashArray
2394
+ });
2395
+ } else {
2396
+ const startBorderBottom = MARGIN;
2397
+ const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
2398
+ startPage.drawRectangle({
2399
+ x: region.leftX,
2400
+ y: startBorderBottom,
2401
+ width: region.width,
2402
+ height: startPanelHeight,
2403
+ borderColor: state.theme.panelBorder,
2404
+ borderWidth: 0.75,
2405
+ borderDashArray
2406
+ });
2407
+ const contTop = PAGE_HEIGHT - MARGIN;
2408
+ const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
2409
+ const contHeight = contTop - contBorderY;
2410
+ state.currentPage.drawRectangle({
2411
+ x: region.leftX,
2412
+ y: contBorderY,
2413
+ width: region.width,
2414
+ height: contHeight,
2415
+ borderColor: state.theme.panelBorder,
2416
+ borderWidth: 0.75,
2417
+ borderDashArray
2418
+ });
2419
+ }
2420
+ }
2421
+ state.y += 4;
2422
+ break;
2423
+ }
2424
+ case "columns": {
2425
+ const split = parseInt(node.attrs?.split || "50", 10);
2426
+ const padX = parseFloat(node.attrs?.padX || "0") || 0;
2427
+ const columns = (node.content || []).filter((c) => c.type === "column");
2428
+ if (columns.length === 0) break;
2429
+ const colRegionLeftX = region.leftX + padX;
2430
+ const colRegionWidth = region.width - 2 * padX;
2431
+ const gap = 12;
2432
+ const columnsStartY = state.y;
2433
+ const columnsStartPage = state.pageIndex;
2434
+ let colWidths;
2435
+ if (columns.length === 2) {
2436
+ const firstWidth = (colRegionWidth - gap) * (split / 100);
2437
+ const secondWidth = colRegionWidth - gap - firstWidth;
2438
+ colWidths = [firstWidth, secondWidth];
2439
+ } else {
2440
+ const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
2441
+ colWidths = columns.map(() => eachWidth);
2442
+ }
2443
+ let maxEndY = columnsStartY;
2444
+ let colX = colRegionLeftX;
2445
+ for (let ci = 0; ci < columns.length; ci++) {
2446
+ const col = columns[ci];
2447
+ const colWidth = colWidths[ci];
2448
+ const colRegion = { leftX: colX, width: colWidth };
2449
+ state.y = columnsStartY;
2450
+ state.pageIndex = columnsStartPage;
2451
+ state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
2452
+ const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
2453
+ if (padTop > 0) {
2454
+ state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
2455
+ }
2456
+ for (const child of col.content || []) {
2457
+ processBlock(state, child, colRegion);
2458
+ }
2459
+ maxEndY = Math.max(maxEndY, state.y);
2460
+ colX += colWidth + gap;
2461
+ }
2462
+ state.y = maxEndY;
2463
+ state.y += 4;
2464
+ break;
2465
+ }
2466
+ case "column": {
2467
+ for (const child of node.content || []) {
2468
+ processBlock(state, child, region);
2469
+ }
2470
+ break;
2471
+ }
2472
+ case "watermark": {
2473
+ break;
2474
+ }
2475
+ case "subtotalsBlock": {
2476
+ const stFontSize = BODY_FONT_SIZE;
2477
+ const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
2478
+ const valueRightX = region.leftX + region.width;
2479
+ const rows = (node.content || []).filter(
2480
+ (c) => c.type === "paragraph" && c.content?.length
2481
+ );
2482
+ const parsed = [];
2483
+ for (const row of rows) {
2484
+ let label = "";
2485
+ let value = "";
2486
+ for (const seg of row.content || []) {
2487
+ if (seg.type === "text") {
2488
+ const isBold = seg.marks?.some((m) => m.type === "bold");
2489
+ if (isBold) {
2490
+ label += seg.text || "";
2491
+ } else {
2492
+ const t = (seg.text || "").trim();
2493
+ if (t) value += (value ? " " : "") + t;
2494
+ }
2495
+ } else if (seg.type === "variableNode") {
2496
+ const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
2497
+ const isBoldVar = seg.marks?.some((m) => m.type === "bold");
2498
+ if (isBoldVar) {
2499
+ label += varLabel;
2500
+ } else {
2501
+ value += (value ? " " : "") + `[${varLabel}]`;
2502
+ }
2503
+ }
2504
+ }
2505
+ parsed.push({ label: label.trim(), value: value.trim() });
2506
+ }
2507
+ let maxLabelWidth = 0;
2508
+ for (const { label } of parsed) {
2509
+ if (label) {
2510
+ const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
2511
+ if (w > maxLabelWidth) maxLabelWidth = w;
2512
+ }
2513
+ }
2514
+ const gap = 12;
2515
+ const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
2516
+ const valueColStartX = labelX + maxLabelWidth + gap;
2517
+ const isLastRowTotal = parsed.length > 1;
2518
+ for (let ri = 0; ri < parsed.length; ri++) {
2519
+ const isTotal = isLastRowTotal && ri === parsed.length - 1;
2520
+ if (isTotal) {
2521
+ state.y += stLineHeight * 0.6;
2522
+ }
2523
+ ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
2524
+ const { label, value } = parsed[ri];
2525
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
2526
+ if (isTotal) {
2527
+ state.currentPage.drawLine({
2528
+ start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
2529
+ end: { x: valueRightX, y: pdfY + stFontSize + 6 },
2530
+ thickness: 0.75,
2531
+ color: pdfLib.rgb(0, 0, 0)
2532
+ });
2533
+ state.currentPage.drawLine({
2534
+ start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
2535
+ end: { x: valueRightX, y: pdfY + stFontSize + 3 },
2536
+ thickness: 0.75,
2537
+ color: pdfLib.rgb(0, 0, 0)
2538
+ });
2539
+ }
2540
+ if (label) {
2541
+ state.currentPage.drawText(label, {
2542
+ x: labelX,
2543
+ y: pdfY,
2544
+ size: stFontSize,
2545
+ font: state.fonts.bold,
2546
+ color: pdfLib.rgb(0, 0, 0)
2547
+ });
2548
+ }
2549
+ if (value) {
2550
+ const valueFont = state.fonts.bold;
2551
+ const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
2552
+ state.currentPage.drawText(value, {
2553
+ x: valueRightX - valueWidth,
2554
+ y: pdfY,
2555
+ size: stFontSize,
2556
+ font: valueFont,
2557
+ color: pdfLib.rgb(0, 0, 0)
2558
+ });
2559
+ }
2560
+ state.y += stLineHeight;
2561
+ }
2562
+ state.y += 4;
2563
+ break;
2564
+ }
2565
+ case "repeatBlock":
2566
+ if (node.content) {
2567
+ for (const child of node.content) {
2568
+ processBlock(state, child, region);
2569
+ }
2570
+ }
2571
+ break;
1311
2572
  default:
1312
2573
  if (node.content) {
1313
- processInlineContent(state, node.content, 12);
2574
+ processInlineContent(state, node.content, 12, 0, region);
1314
2575
  }
1315
2576
  break;
1316
2577
  }
1317
2578
  }
2579
+ function renderWatermarksOnPage(page, fonts, watermarkNodes) {
2580
+ for (const wmNode of watermarkNodes) {
2581
+ const text = wmNode.attrs?.text || "";
2582
+ if (!text) continue;
2583
+ const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
2584
+ const angle = parseFloat(wmNode.attrs?.angle || "-45");
2585
+ const fontSize = 100;
2586
+ const { width, height } = page.getSize();
2587
+ const centerX = width / 2;
2588
+ const centerY = height / 2;
2589
+ const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
2590
+ page.drawText(text, {
2591
+ x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
2592
+ y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
2593
+ size: fontSize,
2594
+ font: fonts.bold,
2595
+ color: pdfLib.rgb(0.5, 0.5, 0.5),
2596
+ opacity,
2597
+ rotate: pdfLib.degrees(angle)
2598
+ });
2599
+ }
2600
+ }
2601
+ function collectWatermarkNodes(content) {
2602
+ const nodes = [];
2603
+ for (const block of content.content || []) {
2604
+ if (block.type === "watermark") {
2605
+ nodes.push(block);
2606
+ }
2607
+ }
2608
+ return nodes;
2609
+ }
1318
2610
  function ensureFieldSuffix(fieldName, suffix) {
1319
2611
  const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
1320
2612
  return `${cleanName}${suffix}`;
@@ -1424,6 +2716,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1424
2716
  pdfLib.PDFString.of(field.label)
1425
2717
  );
1426
2718
  }
2719
+ const initialsFontSize = Math.min(
2720
+ 72,
2721
+ Math.max(8, Math.round(position.height * 0.55))
2722
+ );
2723
+ try {
2724
+ initialsField.setFontSize(initialsFontSize);
2725
+ const acro = initialsField;
2726
+ const daRef = acro.acroField.dict.get(pdfLib.PDFName.of("DA"));
2727
+ if (daRef) {
2728
+ const daStr = daRef.value;
2729
+ if (typeof daStr === "string") {
2730
+ const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
2731
+ acro.acroField.dict.set(pdfLib.PDFName.of("DA"), pdfLib.PDFString.of(newDa));
2732
+ }
2733
+ }
2734
+ } catch (fontError) {
2735
+ console.warn(`Failed to set initials font:`, fontError);
2736
+ }
1427
2737
  initialsField.enableReadOnly();
1428
2738
  if (field.required) initialsField.enableRequired();
1429
2739
  break;
@@ -1527,47 +2837,367 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1527
2837
  } catch (err) {
1528
2838
  warnings.push(`Failed to create ${field.type} field "${field.name}": ${getErrorMessage(err)}`);
1529
2839
  }
1530
- }
1531
- initPdfMetadata({ PDFName: pdfLib.PDFName, PDFString: pdfLib.PDFString });
1532
- const metadata = buildMetadataObject(fields, actualFieldNames);
1533
- setSigniphiMetadata(pdfDoc, metadata);
1534
- return warnings;
1535
- }
1536
- async function generatePdfFromContent(content, options = {}) {
1537
- const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
1538
- const pdfDoc = await pdfLib.PDFDocument.create();
1539
- const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
1540
- const fonts = {
1541
- regular: await pdfDoc.embedFont(pdfLib.StandardFonts.Helvetica),
1542
- bold: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBold),
1543
- italic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaOblique),
1544
- boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
1545
- };
1546
- const state = {
1547
- currentPage: firstPage,
1548
- pageIndex: 0,
1549
- y: 0,
1550
- fonts,
1551
- pdfDoc,
1552
- fieldPositions: /* @__PURE__ */ new Map(),
1553
- drawFieldPlaceholders
1554
- };
1555
- if (content.content) {
1556
- for (const block of content.content) {
1557
- processBlock(state, block);
2840
+ }
2841
+ initPdfMetadata({ PDFName: pdfLib.PDFName, PDFString: pdfLib.PDFString });
2842
+ const metadata = buildMetadataObject(fields, actualFieldNames);
2843
+ setSigniphiMetadata(pdfDoc, metadata);
2844
+ return warnings;
2845
+ }
2846
+ async function generatePdfFromContent(content, options = {}) {
2847
+ const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
2848
+ const theme = resolveTheme(options.theme);
2849
+ const pdfDoc = await pdfLib.PDFDocument.create();
2850
+ const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
2851
+ const fonts = {
2852
+ regular: await pdfDoc.embedFont(pdfLib.StandardFonts.Helvetica),
2853
+ bold: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBold),
2854
+ italic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaOblique),
2855
+ boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
2856
+ };
2857
+ const watermarkNodes = collectWatermarkNodes(content);
2858
+ const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
2859
+ const state = {
2860
+ currentPage: firstPage,
2861
+ pageIndex: 0,
2862
+ y: 0,
2863
+ fonts,
2864
+ pdfDoc,
2865
+ fieldPositions: /* @__PURE__ */ new Map(),
2866
+ drawFieldPlaceholders,
2867
+ watermarkNodes,
2868
+ theme,
2869
+ images
2870
+ };
2871
+ renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
2872
+ if (content.content) {
2873
+ for (const block of content.content) {
2874
+ processBlock(state, block);
2875
+ }
2876
+ }
2877
+ const allPages = pdfDoc.getPages();
2878
+ const totalPages = allPages.length;
2879
+ const pageNumFontSize = 9;
2880
+ const hfFontSize = 8;
2881
+ const hfColor = pdfLib.rgb(0.45, 0.45, 0.45);
2882
+ for (let i = 0; i < totalPages; i++) {
2883
+ const page = allPages[i];
2884
+ const label = `Page ${i + 1} of ${totalPages}`;
2885
+ const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
2886
+ page.drawText(label, {
2887
+ x: PAGE_WIDTH - MARGIN - labelWidth,
2888
+ y: MARGIN / 2 - pageNumFontSize / 2,
2889
+ size: pageNumFontSize,
2890
+ font: fonts.bold,
2891
+ color: pdfLib.rgb(0, 0, 0)
2892
+ });
2893
+ if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
2894
+ const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
2895
+ if (header.left) {
2896
+ page.drawText(header.left, {
2897
+ x: MARGIN,
2898
+ y: headerBaseY,
2899
+ size: pageNumFontSize,
2900
+ font: fonts.bold,
2901
+ color: theme.pageHeaderText
2902
+ });
2903
+ }
2904
+ if (header.right) {
2905
+ const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
2906
+ page.drawText(header.right, {
2907
+ x: PAGE_WIDTH - MARGIN - rightWidth,
2908
+ y: headerBaseY,
2909
+ size: hfFontSize,
2910
+ font: fonts.regular,
2911
+ color: hfColor
2912
+ });
2913
+ }
2914
+ page.drawLine({
2915
+ start: { x: MARGIN, y: headerBaseY - 6 },
2916
+ end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
2917
+ thickness: 0.75,
2918
+ color: theme.pageHeaderRule
2919
+ });
2920
+ }
2921
+ if (footer?.left) {
2922
+ page.drawText(footer.left, {
2923
+ x: MARGIN,
2924
+ y: MARGIN / 2 - pageNumFontSize / 2,
2925
+ size: hfFontSize,
2926
+ font: fonts.regular,
2927
+ color: hfColor
2928
+ });
2929
+ }
2930
+ }
2931
+ let fieldWarnings;
2932
+ if (embedFormFields && fields.length > 0) {
2933
+ const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
2934
+ if (warnings.length > 0) fieldWarnings = warnings;
2935
+ }
2936
+ const pdfBytes = new Uint8Array(await pdfDoc.save({ useObjectStreams: false }));
2937
+ return {
2938
+ pdfBytes,
2939
+ fieldPositions: state.fieldPositions,
2940
+ fieldWarnings,
2941
+ imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
2942
+ };
2943
+ }
2944
+
2945
+ // src/utils/template-pipeline.ts
2946
+ async function generatePdfFromTiptap(content, values, options = {}) {
2947
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2948
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
2949
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
2950
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2951
+ const fields = extractFieldsFromContent(replaced);
2952
+ const result = await generatePdfFromContent(replaced, {
2953
+ embedFormFields: fields.length > 0,
2954
+ fields,
2955
+ theme: options.theme,
2956
+ header: options.header,
2957
+ footer: options.footer,
2958
+ drawFieldPlaceholders: options.drawFieldPlaceholders
2959
+ });
2960
+ return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
2961
+ }
2962
+ async function generatePdfFromMarkdown(markdown, values, options = {}) {
2963
+ const content = markdownToTiptap(markdown);
2964
+ return generatePdfFromTiptap(content, values, options);
2965
+ }
2966
+
2967
+ // src/utils/markdown-writer.ts
2968
+ function serializeFieldToken(attrs) {
2969
+ const parts = ["field"];
2970
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2971
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2972
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2973
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2974
+ if (attrs.required) parts.push(`required:true`);
2975
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2976
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2977
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2978
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2979
+ if (attrs.multiline) parts.push(`multiline:true`);
2980
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2981
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2982
+ return `{{${parts.join("|")}}}`;
2983
+ }
2984
+ function serializeImageNode(attrs) {
2985
+ const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
2986
+ if (!hasExtras && attrs.src) {
2987
+ return `![${attrs.alt || ""}](${attrs.src})`;
2988
+ }
2989
+ const parts = ["image"];
2990
+ if (attrs.varName) parts.push(`var:${attrs.varName}`);
2991
+ else if (attrs.src) parts.push(`src:${attrs.src}`);
2992
+ if (attrs.alt) parts.push(`alt:${attrs.alt}`);
2993
+ if (attrs.width) parts.push(`width:${attrs.width}`);
2994
+ if (attrs.height) parts.push(`height:${attrs.height}`);
2995
+ if (attrs.align) parts.push(`align:${attrs.align}`);
2996
+ return `{{${parts.join("|")}}}`;
2997
+ }
2998
+ function serializeVariableToken(attrs) {
2999
+ const parts = ["var"];
3000
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
3001
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
3002
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
3003
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
3004
+ if (attrs.format) parts.push(`format:${attrs.format}`);
3005
+ if (attrs.block === "true") parts.push(`block:true`);
3006
+ return `{{${parts.join("|")}}}`;
3007
+ }
3008
+ function serializeInline(content) {
3009
+ if (!content) return "";
3010
+ let result = "";
3011
+ for (const node of content) {
3012
+ if (node.type === "fieldNode") {
3013
+ result += serializeFieldToken(node.attrs || {});
3014
+ continue;
3015
+ }
3016
+ if (node.type === "imageNode") {
3017
+ result += serializeImageNode(node.attrs || {});
3018
+ continue;
3019
+ }
3020
+ if (node.type === "variableNode") {
3021
+ let token = serializeVariableToken(node.attrs || {});
3022
+ const varMarks = node.marks || [];
3023
+ const isBold = varMarks.some((m) => m.type === "bold");
3024
+ const isItalic = varMarks.some((m) => m.type === "italic");
3025
+ const isUnderline = varMarks.some((m) => m.type === "underline");
3026
+ if (isBold && isItalic) token = `***${token}***`;
3027
+ else if (isBold) token = `**${token}**`;
3028
+ else if (isItalic) token = `*${token}*`;
3029
+ if (isUnderline) token = `__${token}__`;
3030
+ result += token;
3031
+ continue;
3032
+ }
3033
+ if (node.type === "text") {
3034
+ let text = node.text || "";
3035
+ const marks = node.marks || [];
3036
+ for (const mark of marks) {
3037
+ if (mark.type === "bold") text = `**${text}**`;
3038
+ else if (mark.type === "italic") text = `*${text}*`;
3039
+ else if (mark.type === "underline") text = `__${text}__`;
3040
+ else if (mark.type === "code") text = `\`${text}\``;
3041
+ }
3042
+ result += text;
3043
+ }
3044
+ if (node.type === "hardBreak") {
3045
+ result += " \n";
3046
+ }
3047
+ }
3048
+ return result;
3049
+ }
3050
+ function serializeBlock(node) {
3051
+ switch (node.type) {
3052
+ case "heading": {
3053
+ const level = node.attrs?.level || 1;
3054
+ const prefix = "#".repeat(level);
3055
+ return `${prefix} ${serializeInline(node.content)}`;
3056
+ }
3057
+ case "paragraph": {
3058
+ const inline = serializeInline(node.content);
3059
+ return inline;
3060
+ }
3061
+ case "bulletList": {
3062
+ return (node.content || []).map((item) => {
3063
+ const inner = (item.content || []).map(serializeBlock).join("\n");
3064
+ return `- ${inner}`;
3065
+ }).join("\n");
3066
+ }
3067
+ case "orderedList": {
3068
+ return (node.content || []).map((item, i) => {
3069
+ const inner = (item.content || []).map(serializeBlock).join("\n");
3070
+ return `${i + 1}. ${inner}`;
3071
+ }).join("\n");
3072
+ }
3073
+ case "listItem": {
3074
+ return (node.content || []).map(serializeBlock).join("\n");
3075
+ }
3076
+ case "blockquote": {
3077
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
3078
+ }
3079
+ case "codeBlock": {
3080
+ const text = serializeInline(node.content);
3081
+ return `\`\`\`
3082
+ ${text}
3083
+ \`\`\``;
3084
+ }
3085
+ case "horizontalRule": {
3086
+ return "---";
3087
+ }
3088
+ case "table": {
3089
+ const rows = node.content || [];
3090
+ const isBorderless = node.attrs?.borderless === true;
3091
+ const isSubtotals = node.attrs?.subtotals === true;
3092
+ const serializedRows = [];
3093
+ if (isBorderless && rows.length > 0) {
3094
+ const colCount = rows[0].content?.length || 0;
3095
+ const markerCell = isSubtotals ? "_" : "";
3096
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
3097
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
3098
+ serializedRows.push(markerRow);
3099
+ serializedRows.push(sep);
3100
+ }
3101
+ for (let ri = 0; ri < rows.length; ri++) {
3102
+ const row = rows[ri];
3103
+ const cells = (row.content || []).map((cell) => {
3104
+ const inner = (cell.content || []).map(serializeBlock).join("");
3105
+ return inner;
3106
+ });
3107
+ serializedRows.push(`| ${cells.join(" | ")} |`);
3108
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
3109
+ const sep = cells.map(() => "------").join("|");
3110
+ serializedRows.push(`|${sep}|`);
3111
+ }
3112
+ }
3113
+ return serializedRows.join("\n");
3114
+ }
3115
+ case "tableRow": {
3116
+ const cells = (node.content || []).map((cell) => {
3117
+ const inner = (cell.content || []).map(serializeBlock).join("");
3118
+ return inner;
3119
+ });
3120
+ return `| ${cells.join(" | ")} |`;
3121
+ }
3122
+ case "tableHeader":
3123
+ case "tableCell": {
3124
+ return (node.content || []).map(serializeBlock).join("");
3125
+ }
3126
+ case "imageNode": {
3127
+ return serializeImageNode(node.attrs || {});
3128
+ }
3129
+ case "watermark": {
3130
+ const attrs = node.attrs || {};
3131
+ const parts = [];
3132
+ if (attrs.text) parts.push(`text:${attrs.text}`);
3133
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
3134
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
3135
+ return `:::watermark{${parts.join("|")}}`;
3136
+ }
3137
+ case "panel": {
3138
+ const attrs = node.attrs || {};
3139
+ const parts = [];
3140
+ if (attrs.title) parts.push(`title:${attrs.title}`);
3141
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
3142
+ else if (attrs.border === "solid") parts.push(`border:solid`);
3143
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
3144
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3145
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3146
+ return `:::panel${attrStr}
3147
+ ${children}
3148
+ :::`;
3149
+ }
3150
+ case "repeatBlock": {
3151
+ const attrs = node.attrs || {};
3152
+ const parts = [];
3153
+ if (attrs.data) parts.push(`data:${attrs.data}`);
3154
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3155
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3156
+ return `:::repeat${attrStr}
3157
+ ${children}
3158
+ :::`;
3159
+ }
3160
+ case "subtotalsBlock": {
3161
+ const children = (node.content || []).map(serializeBlock).join("\n");
3162
+ return `:::subtotals
3163
+ ${children}
3164
+ :::`;
3165
+ }
3166
+ case "columns": {
3167
+ const attrs = node.attrs || {};
3168
+ const splitVal = attrs.split || "50";
3169
+ const cols = (node.content || []).filter((c) => c.type === "column");
3170
+ const colBlocks = cols.map((col) => {
3171
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
3172
+ const colAttrs = col.attrs || {};
3173
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3174
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3175
+ return `${colTag}
3176
+ ${children}
3177
+ :::`;
3178
+ });
3179
+ const padX = parseFloat(attrs.padX || "0") || 0;
3180
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
3181
+ return `:::columns{${colsAttrs}}
3182
+ ${colBlocks.join("\n")}
3183
+ :::`;
3184
+ }
3185
+ case "column": {
3186
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3187
+ const colAttrs = node.attrs || {};
3188
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3189
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3190
+ return `${colTag}
3191
+ ${children}
3192
+ :::`;
1558
3193
  }
3194
+ default:
3195
+ return serializeInline(node.content);
1559
3196
  }
1560
- let fieldWarnings;
1561
- if (embedFormFields && fields.length > 0) {
1562
- const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
1563
- if (warnings.length > 0) fieldWarnings = warnings;
1564
- }
1565
- const pdfBytes = await pdfDoc.save({ useObjectStreams: false });
1566
- return {
1567
- pdfBytes,
1568
- fieldPositions: state.fieldPositions,
1569
- fieldWarnings
1570
- };
3197
+ }
3198
+ function tiptapToMarkdown(doc) {
3199
+ if (!doc.content) return "";
3200
+ return doc.content.map(serializeBlock).join("\n\n");
1571
3201
  }
1572
3202
 
1573
3203
  // src/utils/pdf-preview.ts
@@ -1656,8 +3286,27 @@ function useDocumentGenerator(options = {}) {
1656
3286
  Placeholder__default.default.configure({
1657
3287
  placeholder: options.placeholder || "Start writing your document..."
1658
3288
  }),
3289
+ Table__default.default.extend({
3290
+ addAttributes() {
3291
+ return {
3292
+ ...this.parent?.(),
3293
+ borderless: { default: false },
3294
+ subtotals: { default: false }
3295
+ };
3296
+ }
3297
+ }).configure({ resizable: false }),
3298
+ TableRow__default.default,
3299
+ TableCell__default.default,
3300
+ TableHeader__default.default,
1659
3301
  FieldNode,
1660
- VariableNode
3302
+ VariableNode,
3303
+ PanelNode,
3304
+ ColumnsNode,
3305
+ ColumnNode,
3306
+ WatermarkNode,
3307
+ ImageNode,
3308
+ RepeatNode,
3309
+ SubtotalsNode
1661
3310
  ],
1662
3311
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1663
3312
  onUpdate: ({ editor: ed }) => {
@@ -1761,10 +3410,9 @@ function useDocumentGenerator(options = {}) {
1761
3410
  async (values) => {
1762
3411
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1763
3412
  const content = editor.getJSON();
1764
- const replacedContent = replaceVariablesInContent(content, values);
1765
- const result = await generatePdfFromContent(replacedContent);
1766
- const pages = await pdfToImages(result.pdfBytes);
1767
- return { pdfBytes: result.pdfBytes, pdfPages: pages };
3413
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
3414
+ const pdfPages2 = await pdfToImages(pdfBytes2);
3415
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1768
3416
  },
1769
3417
  [editor]
1770
3418
  );
@@ -2918,15 +4566,47 @@ function FieldEditPopover({
2918
4566
  ] }) })
2919
4567
  ] });
2920
4568
  }
2921
- function labelToVarName(label) {
4569
+ var sizeClasses = {
4570
+ md: "px-3 py-1.5 text-xs font-medium",
4571
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4572
+ };
4573
+ function ToggleGroup({
4574
+ value,
4575
+ onChange,
4576
+ options,
4577
+ size = "md",
4578
+ className
4579
+ }) {
4580
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
4581
+ "button",
4582
+ {
4583
+ className: cn(
4584
+ "rounded-md transition-colors",
4585
+ sizeClasses[size],
4586
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4587
+ ),
4588
+ onClick: () => onChange(opt.value),
4589
+ children: opt.label
4590
+ },
4591
+ opt.value
4592
+ )) });
4593
+ }
4594
+ function labelToVarName2(label) {
2922
4595
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2923
4596
  }
4597
+ function varNameToLabel(name) {
4598
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4599
+ }
2924
4600
  function VariableInsertPopover({
2925
4601
  open,
2926
4602
  onOpenChange,
2927
4603
  onInsert,
4604
+ predefinedVariables,
2928
4605
  children
2929
4606
  }) {
4607
+ const hasPredefined = !!predefinedVariables?.length;
4608
+ const [mode, setMode] = React12.useState(hasPredefined ? "existing" : "new");
4609
+ const [search, setSearch] = React12.useState("");
2930
4610
  const [varLabel, setVarLabel] = React12.useState("");
2931
4611
  const [varName, setVarName] = React12.useState("");
2932
4612
  const [varDefault, setVarDefault] = React12.useState("");
@@ -2936,7 +4616,9 @@ function VariableInsertPopover({
2936
4616
  setVarName("");
2937
4617
  setVarDefault("");
2938
4618
  setNameManuallyEdited(false);
2939
- }, []);
4619
+ setSearch("");
4620
+ setMode(hasPredefined ? "existing" : "new");
4621
+ }, [hasPredefined]);
2940
4622
  const handleOpenChange = React12.useCallback(
2941
4623
  (nextOpen) => {
2942
4624
  if (!nextOpen) reset();
@@ -2946,7 +4628,7 @@ function VariableInsertPopover({
2946
4628
  );
2947
4629
  React12.useEffect(() => {
2948
4630
  if (!nameManuallyEdited) {
2949
- setVarName(labelToVarName(varLabel));
4631
+ setVarName(labelToVarName2(varLabel));
2950
4632
  }
2951
4633
  }, [varLabel, nameManuallyEdited]);
2952
4634
  const handleInsert = React12.useCallback(() => {
@@ -2954,10 +4636,32 @@ function VariableInsertPopover({
2954
4636
  onInsert({
2955
4637
  varName: varName.trim(),
2956
4638
  varLabel: varLabel.trim(),
2957
- varDefault: varDefault.trim()
4639
+ varDefault: varDefault.trim(),
4640
+ format: ""
2958
4641
  });
2959
4642
  handleOpenChange(false);
2960
4643
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4644
+ const handlePickPredefined = React12.useCallback(
4645
+ (pv) => {
4646
+ onInsert({
4647
+ varName: pv.varName,
4648
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4649
+ varDefault: "",
4650
+ format: ""
4651
+ });
4652
+ handleOpenChange(false);
4653
+ },
4654
+ [onInsert, handleOpenChange]
4655
+ );
4656
+ const filteredVars = React12.useMemo(() => {
4657
+ if (!predefinedVariables?.length) return [];
4658
+ const q = search.toLowerCase();
4659
+ if (!q) return predefinedVariables;
4660
+ return predefinedVariables.filter((pv) => {
4661
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4662
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4663
+ });
4664
+ }, [predefinedVariables, search]);
2961
4665
  return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2962
4666
  /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children }),
2963
4667
  /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-72 p-0", align: "start", sideOffset: 8, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
@@ -2965,72 +4669,114 @@ function VariableInsertPopover({
2965
4669
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 14 }),
2966
4670
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Insert Variable" })
2967
4671
  ] }),
2968
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2969
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2970
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4672
+ hasPredefined && /* @__PURE__ */ jsxRuntime.jsx(
4673
+ ToggleGroup,
4674
+ {
4675
+ value: mode,
4676
+ onChange: setMode,
4677
+ options: [
4678
+ { value: "existing", label: "Existing" },
4679
+ { value: "new", label: "New" }
4680
+ ],
4681
+ size: "sm"
4682
+ }
4683
+ ),
4684
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4685
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4686
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2971
4687
  /* @__PURE__ */ jsxRuntime.jsx(
2972
4688
  Input,
2973
4689
  {
2974
- id: "var-label",
2975
- value: varLabel,
2976
- onChange: (e) => setVarLabel(e.target.value),
2977
- placeholder: "e.g. Company Name",
2978
- className: "h-8 text-xs"
4690
+ value: search,
4691
+ onChange: (e) => setSearch(e.target.value),
4692
+ placeholder: "Search variables...",
4693
+ className: "h-8 text-xs pl-7"
2979
4694
  }
2980
4695
  )
2981
4696
  ] }),
2982
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2983
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4697
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-48 overflow-y-auto -mx-1", children: filteredVars.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground px-2 py-3 text-center", children: "No variables found" }) : filteredVars.map((pv) => {
4698
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4699
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4700
+ "button",
4701
+ {
4702
+ 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",
4703
+ onClick: () => handlePickPredefined(pv),
4704
+ children: [
4705
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
4706
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4707
+ ]
4708
+ },
4709
+ pv.varName
4710
+ );
4711
+ }) })
4712
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4713
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4714
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4715
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4716
+ /* @__PURE__ */ jsxRuntime.jsx(
4717
+ Input,
4718
+ {
4719
+ id: "var-label",
4720
+ value: varLabel,
4721
+ onChange: (e) => setVarLabel(e.target.value),
4722
+ placeholder: "e.g. Company Name",
4723
+ className: "h-8 text-xs"
4724
+ }
4725
+ )
4726
+ ] }),
4727
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4728
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4729
+ /* @__PURE__ */ jsxRuntime.jsx(
4730
+ Input,
4731
+ {
4732
+ id: "var-name",
4733
+ value: varName,
4734
+ onChange: (e) => {
4735
+ setVarName(e.target.value);
4736
+ setNameManuallyEdited(true);
4737
+ },
4738
+ placeholder: "e.g. company_name",
4739
+ className: "h-8 text-xs font-mono"
4740
+ }
4741
+ ),
4742
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4743
+ ] }),
4744
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4745
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4746
+ /* @__PURE__ */ jsxRuntime.jsx(
4747
+ Input,
4748
+ {
4749
+ id: "var-default",
4750
+ value: varDefault,
4751
+ onChange: (e) => setVarDefault(e.target.value),
4752
+ placeholder: "Optional",
4753
+ className: "h-8 text-xs"
4754
+ }
4755
+ )
4756
+ ] })
4757
+ ] }),
4758
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
4759
  /* @__PURE__ */ jsxRuntime.jsx(
2985
- Input,
4760
+ Button,
2986
4761
  {
2987
- id: "var-name",
2988
- value: varName,
2989
- onChange: (e) => {
2990
- setVarName(e.target.value);
2991
- setNameManuallyEdited(true);
2992
- },
2993
- placeholder: "e.g. company_name",
2994
- className: "h-8 text-xs font-mono"
4762
+ size: "sm",
4763
+ className: "h-8 flex-1 text-xs",
4764
+ onClick: handleInsert,
4765
+ disabled: !varLabel.trim() || !varName.trim(),
4766
+ children: "Insert"
2995
4767
  }
2996
4768
  ),
2997
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
2998
- ] }),
2999
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3000
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
3001
4769
  /* @__PURE__ */ jsxRuntime.jsx(
3002
- Input,
4770
+ Button,
3003
4771
  {
3004
- id: "var-default",
3005
- value: varDefault,
3006
- onChange: (e) => setVarDefault(e.target.value),
3007
- placeholder: "Optional",
3008
- className: "h-8 text-xs"
4772
+ variant: "outline",
4773
+ size: "sm",
4774
+ className: "h-8 text-xs",
4775
+ onClick: () => handleOpenChange(false),
4776
+ children: "Cancel"
3009
4777
  }
3010
4778
  )
3011
4779
  ] })
3012
- ] }),
3013
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
3014
- /* @__PURE__ */ jsxRuntime.jsx(
3015
- Button,
3016
- {
3017
- size: "sm",
3018
- className: "h-8 flex-1 text-xs",
3019
- onClick: handleInsert,
3020
- disabled: !varLabel.trim() || !varName.trim(),
3021
- children: "Insert"
3022
- }
3023
- ),
3024
- /* @__PURE__ */ jsxRuntime.jsx(
3025
- Button,
3026
- {
3027
- variant: "outline",
3028
- size: "sm",
3029
- className: "h-8 text-xs",
3030
- onClick: () => handleOpenChange(false),
3031
- children: "Cancel"
3032
- }
3033
- )
3034
4780
  ] })
3035
4781
  ] }) })
3036
4782
  ] });
@@ -3252,7 +4998,6 @@ function PreviewPanel({
3252
4998
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3253
4999
  );
3254
5000
  }, [positionedFields, currentPage, currentPageIndex]);
3255
- const showScrollbars = zoomLevel > 1;
3256
5001
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
3257
5002
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border px-2 py-1.5 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
3258
5003
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3368,12 +5113,13 @@ function PreviewPanel({
3368
5113
  "div",
3369
5114
  {
3370
5115
  className: cn(
3371
- "border border-border rounded-lg bg-muted/30 shrink-0",
3372
- showScrollbars ? "overflow-auto" : "overflow-hidden"
5116
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3373
5117
  ),
3374
5118
  style: {
3375
5119
  width: pageDisplaySize.viewportWidth,
3376
- height: pageDisplaySize.viewportHeight
5120
+ height: pageDisplaySize.viewportHeight,
5121
+ maxWidth: "100%",
5122
+ maxHeight: "100%"
3377
5123
  },
3378
5124
  children: /* @__PURE__ */ jsxRuntime.jsxs(
3379
5125
  "div",
@@ -3444,8 +5190,8 @@ function validateMarkdown(markdown) {
3444
5190
  errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
3445
5191
  } else {
3446
5192
  const tokenType = tokenContent.slice(0, pipeIdx);
3447
- if (tokenType !== "field" && tokenType !== "var") {
3448
- errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "var")`);
5193
+ if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
5194
+ errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
3449
5195
  } else {
3450
5196
  const body = tokenContent.slice(pipeIdx + 1);
3451
5197
  const pairs = body.split("|");
@@ -3459,12 +5205,48 @@ function validateMarkdown(markdown) {
3459
5205
  if (!hasName) {
3460
5206
  errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
3461
5207
  }
5208
+ } else if (tokenType === "image") {
5209
+ const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
5210
+ if (!hasSource) {
5211
+ errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
5212
+ }
3462
5213
  }
3463
5214
  }
3464
5215
  }
3465
5216
  searchFrom = closeIdx + 2;
3466
5217
  }
3467
5218
  }
5219
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
5220
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
5221
+ const directiveStack = [];
5222
+ for (let i = 0; i < lines.length; i++) {
5223
+ const trimmed = lines[i].trim();
5224
+ const lineNum = i + 1;
5225
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
5226
+ if (openMatch) {
5227
+ const dtype = openMatch[1];
5228
+ if (dtype !== "watermark") {
5229
+ if (dtype === "col") {
5230
+ const parent = directiveStack[directiveStack.length - 1];
5231
+ if (!parent || parent.type !== "columns") {
5232
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
5233
+ }
5234
+ }
5235
+ directiveStack.push({ type: dtype, line: lineNum });
5236
+ }
5237
+ continue;
5238
+ }
5239
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
5240
+ if (directiveStack.length === 0) {
5241
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
5242
+ } else {
5243
+ directiveStack.pop();
5244
+ }
5245
+ }
5246
+ }
5247
+ for (const open of directiveStack) {
5248
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
5249
+ }
3468
5250
  let fields = [];
3469
5251
  let variables = [];
3470
5252
  try {
@@ -3498,31 +5280,6 @@ function validateMarkdown(markdown) {
3498
5280
  fields
3499
5281
  };
3500
5282
  }
3501
- var sizeClasses = {
3502
- md: "px-3 py-1.5 text-xs font-medium",
3503
- sm: "px-2 py-0.5 text-[10px] font-medium"
3504
- };
3505
- function ToggleGroup({
3506
- value,
3507
- onChange,
3508
- options,
3509
- size = "md",
3510
- className
3511
- }) {
3512
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
3513
- "button",
3514
- {
3515
- className: cn(
3516
- "rounded-md transition-colors",
3517
- sizeClasses[size],
3518
- value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
3519
- ),
3520
- onClick: () => onChange(opt.value),
3521
- children: opt.label
3522
- },
3523
- opt.value
3524
- )) });
3525
- }
3526
5283
  function parseCsv(text) {
3527
5284
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3528
5285
  if (lines.length < 2) return [];
@@ -3572,7 +5329,10 @@ function GeneratePanel({
3572
5329
  editorContent,
3573
5330
  editorVariables,
3574
5331
  onGeneratePdf,
3575
- initialBulkData
5332
+ initialBulkData,
5333
+ initialVariableValues,
5334
+ exportFileName,
5335
+ templateMode
3576
5336
  }) {
3577
5337
  const [templateSource, setTemplateSource] = React12.useState("editor");
3578
5338
  const [importedMarkdown, setImportedMarkdown] = React12.useState(null);
@@ -3580,6 +5340,7 @@ function GeneratePanel({
3580
5340
  const [validationResult, setValidationResult] = React12.useState(null);
3581
5341
  const [mode, setMode] = React12.useState("single");
3582
5342
  const [variableValues, setVariableValues] = React12.useState({});
5343
+ const [variableSearch, setVariableSearch] = React12.useState("");
3583
5344
  const [bulkInputFormat, setBulkInputFormat] = React12.useState("json");
3584
5345
  const [bulkInput, setBulkInput] = React12.useState("");
3585
5346
  const [bulkData, setBulkData] = React12.useState(null);
@@ -3606,11 +5367,22 @@ function GeneratePanel({
3606
5367
  const col = pos - textBefore.lastIndexOf("\n");
3607
5368
  setJsonCursor({ line, col });
3608
5369
  }, []);
5370
+ const lockedVarNames = React12__namespace.default.useMemo(
5371
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
5372
+ [templateMode, initialVariableValues]
5373
+ );
3609
5374
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3610
5375
  const hasVariables = activeVariables.length > 0;
3611
5376
  const hasEditorContent = editorMarkdown.trim().length > 0;
3612
5377
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3613
5378
  const hasContent = hasImportedContent || hasEditorContent;
5379
+ const filteredVariables = React12__namespace.default.useMemo(() => {
5380
+ if (!variableSearch.trim()) return activeVariables;
5381
+ const q = variableSearch.toLowerCase();
5382
+ return activeVariables.filter(
5383
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
5384
+ );
5385
+ }, [activeVariables, variableSearch]);
3614
5386
  const getActiveContent = React12.useCallback(() => {
3615
5387
  if (templateSource === "imported" && importedMarkdown) {
3616
5388
  return markdownToTiptap(importedMarkdown);
@@ -3778,7 +5550,10 @@ function GeneratePanel({
3778
5550
  setPreviewError(null);
3779
5551
  try {
3780
5552
  const content = getActiveContent();
3781
- const replaced = replaceVariablesInContent(content, variableValues);
5553
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5554
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5555
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5556
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3782
5557
  const fields = extractFieldsFromContent(replaced);
3783
5558
  const result = await generatePdfFromContent(replaced);
3784
5559
  const pages = await pdfToImages(result.pdfBytes);
@@ -3799,7 +5574,10 @@ function GeneratePanel({
3799
5574
  setExportSuccess(null);
3800
5575
  try {
3801
5576
  const content = getActiveContent();
3802
- const replaced = replaceVariablesInContent(content, variableValues);
5577
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5578
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5579
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5580
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3803
5581
  const fields = extractFieldsFromContent(replaced);
3804
5582
  const result = await generatePdfFromContent(replaced, {
3805
5583
  drawFieldPlaceholders: false,
@@ -3807,7 +5585,7 @@ function GeneratePanel({
3807
5585
  fields
3808
5586
  });
3809
5587
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3810
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5588
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3811
5589
  if (onGeneratePdf) {
3812
5590
  onGeneratePdf(blob, fileName);
3813
5591
  } else {
@@ -3879,7 +5657,10 @@ function GeneratePanel({
3879
5657
  const allWarnings = [];
3880
5658
  for (let i = 0; i < bulkData.length; i++) {
3881
5659
  const values = bulkData[i];
3882
- const replaced = replaceVariablesInContent(content, values);
5660
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5661
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5662
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5663
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3883
5664
  const fields = extractFieldsFromContent(replaced);
3884
5665
  const result = await generatePdfFromContent(replaced, {
3885
5666
  drawFieldPlaceholders: false,
@@ -3934,13 +5715,15 @@ ${allWarnings.join("\n")}`);
3934
5715
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3935
5716
  React12__namespace.default.useEffect(() => {
3936
5717
  if (templateSource === "editor") {
3937
- const defaults = {};
5718
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3938
5719
  for (const v of editorVariables) {
3939
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5720
+ if (!defaults[v.varName]) {
5721
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5722
+ }
3940
5723
  }
3941
5724
  setVariableValues(defaults);
3942
5725
  }
3943
- }, [editorVariables, templateSource]);
5726
+ }, [editorVariables, templateSource, initialVariableValues]);
3944
5727
  React12.useEffect(() => {
3945
5728
  setPreviewFresh(false);
3946
5729
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3952,8 +5735,8 @@ ${allWarnings.join("\n")}`);
3952
5735
  }, [exportSuccess]);
3953
5736
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3954
5737
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
3955
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
3956
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5738
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5739
+ !templateMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3957
5740
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3958
5741
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3959
5742
  templateSource === "imported" && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4046,10 +5829,10 @@ ${allWarnings.join("\n")}`);
4046
5829
  !hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4047
5830
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 32, className: "mb-2 opacity-50" }),
4048
5831
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "No template content" }),
4049
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs mt-1", children: "Import a .md template above or add content in the Editor tab." })
5832
+ /* @__PURE__ */ jsxRuntime.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." })
4050
5833
  ] }),
4051
5834
  hasVariables && hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-border rounded-lg overflow-hidden", children: [
4052
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
5835
+ !templateMode && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
4053
5836
  ToggleGroup,
4054
5837
  {
4055
5838
  value: mode,
@@ -4061,20 +5844,53 @@ ${allWarnings.join("\n")}`);
4061
5844
  }
4062
5845
  ) }),
4063
5846
  mode === "single" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4064
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
4065
- activeVariables.map((v) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4066
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5847
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
5848
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5849
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5850
+ ] }),
5851
+ activeVariables.length > 5 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
5852
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4067
5853
  /* @__PURE__ */ jsxRuntime.jsx(
4068
5854
  Input,
4069
5855
  {
4070
- id: `gen-${v.varName}`,
4071
- value: variableValues[v.varName] || "",
4072
- onChange: (e) => updateVariableValue(v.varName, e.target.value),
4073
- placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
4074
- className: "h-8 text-xs"
5856
+ value: variableSearch,
5857
+ onChange: (e) => setVariableSearch(e.target.value),
5858
+ placeholder: "Search variables...",
5859
+ className: "h-8 text-xs pl-8"
5860
+ }
5861
+ ),
5862
+ variableSearch && /* @__PURE__ */ jsxRuntime.jsx(
5863
+ "button",
5864
+ {
5865
+ type: "button",
5866
+ onClick: () => setVariableSearch(""),
5867
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5868
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
4075
5869
  }
4076
5870
  )
4077
- ] }, v.varName))
5871
+ ] }),
5872
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5873
+ "No variables match \u201C",
5874
+ variableSearch,
5875
+ "\u201D"
5876
+ ] }),
5877
+ filteredVariables.map((v) => {
5878
+ const isLocked = lockedVarNames.has(v.varName);
5879
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5880
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5881
+ /* @__PURE__ */ jsxRuntime.jsx(
5882
+ Input,
5883
+ {
5884
+ id: `gen-${v.varName}`,
5885
+ value: variableValues[v.varName] || "",
5886
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5887
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5888
+ disabled: isLocked,
5889
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5890
+ }
5891
+ )
5892
+ ] }, v.varName);
5893
+ })
4078
5894
  ] }),
4079
5895
  mode === "bulk" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4080
5896
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4263,56 +6079,60 @@ ${allWarnings.join("\n")}`);
4263
6079
  ] }),
4264
6080
  !hasVariables && hasContent && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-3 rounded-md bg-muted/30 border border-border", children: /* @__PURE__ */ jsxRuntime.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.' }) })
4265
6081
  ] }),
4266
- previewError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5", children: [
4267
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4268
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: previewError })
4269
- ] }) }) }),
4270
- exportError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5", children: [
4271
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4272
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: exportError })
4273
- ] }) }) }),
4274
- exportSuccess && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 rounded-md bg-emerald-500/10 border border-emerald-500/20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4275
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "text-emerald-600 shrink-0" }),
4276
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-emerald-700", children: exportSuccess })
4277
- ] }) }) }),
4278
- hasContent && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4279
- /* @__PURE__ */ jsxRuntime.jsxs(
4280
- Button,
4281
- {
4282
- variant: "secondary",
4283
- onClick: handlePreview,
4284
- disabled: isGenerating,
4285
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4286
- children: [
4287
- isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
4288
- isGenerating ? "Generating..." : "Generate PDF"
4289
- ]
4290
- }
4291
- ),
4292
- /* @__PURE__ */ jsxRuntime.jsxs(
6082
+ hasContent && /* @__PURE__ */ jsxRuntime.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: [
6083
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6084
+ /* @__PURE__ */ jsxRuntime.jsxs(
6085
+ Button,
6086
+ {
6087
+ variant: "secondary",
6088
+ onClick: handlePreview,
6089
+ disabled: isGenerating,
6090
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6091
+ children: [
6092
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
6093
+ isGenerating ? "Generating..." : "Generate PDF"
6094
+ ]
6095
+ }
6096
+ ),
6097
+ /* @__PURE__ */ jsxRuntime.jsxs(
6098
+ Button,
6099
+ {
6100
+ onClick: handleExportSingle,
6101
+ disabled: !previewFresh || isExporting,
6102
+ 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",
6103
+ children: [
6104
+ isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
6105
+ isExporting ? "Exporting..." : "Export PDF"
6106
+ ]
6107
+ }
6108
+ )
6109
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4293
6110
  Button,
4294
6111
  {
4295
- onClick: handleExportSingle,
4296
- disabled: !previewFresh || isExporting,
4297
- 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",
6112
+ onClick: handleBulkGenerate,
6113
+ disabled: isExporting || !bulkData || bulkData.length === 0,
6114
+ 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",
4298
6115
  children: [
4299
6116
  isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
4300
- isExporting ? "Exporting..." : "Export PDF"
6117
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4301
6118
  ]
4302
6119
  }
4303
- )
4304
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4305
- Button,
4306
- {
4307
- onClick: handleBulkGenerate,
4308
- disabled: isExporting || !bulkData || bulkData.length === 0,
4309
- 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",
4310
- children: [
4311
- isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
4312
- isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4313
- ]
4314
- }
4315
- ) })
6120
+ ),
6121
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6122
+ previewError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6123
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6124
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: previewError })
6125
+ ] }),
6126
+ exportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6127
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6128
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportError })
6129
+ ] }),
6130
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6131
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
6132
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportSuccess })
6133
+ ] })
6134
+ ] })
6135
+ ] })
4316
6136
  ] }),
4317
6137
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
4318
6138
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -4490,7 +6310,13 @@ function DocumentGeneratorInner({
4490
6310
  showToolbar = true,
4491
6311
  showGenerateTab = true,
4492
6312
  onGeneratePdf,
4493
- initialBulkData
6313
+ initialBulkData,
6314
+ defaultTab = "editor",
6315
+ initialVariableValues,
6316
+ predefinedVariables,
6317
+ exportFileName,
6318
+ templateMode,
6319
+ onSaveTemplate
4494
6320
  }) {
4495
6321
  const {
4496
6322
  editor,
@@ -4517,7 +6343,7 @@ function DocumentGeneratorInner({
4517
6343
  placeholder,
4518
6344
  onChange
4519
6345
  });
4520
- const [activeTab, setActiveTab] = React12.useState("editor");
6346
+ const [activeTab, setActiveTab] = React12.useState(defaultTab);
4521
6347
  const [insertPopoverOpen, setInsertPopoverOpen] = React12.useState(false);
4522
6348
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = React12.useState(false);
4523
6349
  const [editFieldId, setEditFieldId] = React12.useState(null);
@@ -4529,6 +6355,7 @@ function DocumentGeneratorInner({
4529
6355
  const editorWrapperRef = React12.useRef(null);
4530
6356
  const [editorCollapsed, setEditorCollapsed] = React12.useState(false);
4531
6357
  const [mdExportError, setMdExportError] = React12.useState(null);
6358
+ const [isSaving, setIsSaving] = React12.useState(false);
4532
6359
  const [exportSuccess, setExportSuccess] = React12.useState(null);
4533
6360
  React12.useEffect(() => {
4534
6361
  const handler = (e) => {
@@ -4599,7 +6426,28 @@ function DocumentGeneratorInner({
4599
6426
  onExport(result);
4600
6427
  setExportSuccess("Document exported successfully");
4601
6428
  }
4602
- }, [exportDocument, onExport]);
6429
+ }, [exportDocument, onExport]);
6430
+ const handleSaveTemplate = React12.useCallback(async () => {
6431
+ const md = exportMarkdown();
6432
+ if (!md.trim()) {
6433
+ setMdExportError("Editor content is empty");
6434
+ return;
6435
+ }
6436
+ const validation = validateMarkdown(md);
6437
+ if (!validation.valid) {
6438
+ setMdExportError(validation.errors.join("; "));
6439
+ return;
6440
+ }
6441
+ setMdExportError(null);
6442
+ setIsSaving(true);
6443
+ try {
6444
+ await onSaveTemplate?.(md);
6445
+ setExportSuccess("Template saved");
6446
+ } catch {
6447
+ } finally {
6448
+ setIsSaving(false);
6449
+ }
6450
+ }, [exportMarkdown, onSaveTemplate]);
4603
6451
  const handleExportMarkdown = React12.useCallback(() => {
4604
6452
  const md = exportMarkdown();
4605
6453
  if (!md.trim()) {
@@ -4616,10 +6464,10 @@ function DocumentGeneratorInner({
4616
6464
  const url = URL.createObjectURL(blob);
4617
6465
  const a = document.createElement("a");
4618
6466
  a.href = url;
4619
- a.download = "template.md";
6467
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4620
6468
  a.click();
4621
6469
  URL.revokeObjectURL(url);
4622
- }, [exportMarkdown]);
6470
+ }, [exportMarkdown, exportFileName]);
4623
6471
  React12.useEffect(() => {
4624
6472
  if (mdExportError) setMdExportError(null);
4625
6473
  }, [markdown]);
@@ -4655,6 +6503,7 @@ function DocumentGeneratorInner({
4655
6503
  open: insertVarPopoverOpen,
4656
6504
  onOpenChange: setInsertVarPopoverOpen,
4657
6505
  onInsert: handleInsertVariable,
6506
+ predefinedVariables,
4658
6507
  children: /* @__PURE__ */ jsxRuntime.jsxs(
4659
6508
  Button,
4660
6509
  {
@@ -4695,118 +6544,141 @@ function DocumentGeneratorInner({
4695
6544
  ] })
4696
6545
  ] })
4697
6546
  ] }),
4698
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6547
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
4699
6548
  "flex flex-col flex-1 min-h-0",
4700
6549
  activeTab !== "editor" && "hidden"
6550
+ ), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6551
+ "flex-1 min-h-0",
6552
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4701
6553
  ), children: [
4702
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
4703
- "flex-1 min-h-0",
4704
- showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4705
- ), children: [
4706
- !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
4707
- showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
4708
- EditorToolbar,
4709
- {
4710
- editor,
4711
- insertFieldButton,
4712
- insertVariableButton,
4713
- onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4714
- }
4715
- ) }),
4716
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(
4717
- react.EditorContent,
4718
- {
4719
- editor,
4720
- className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4721
- }
4722
- ) })
4723
- ] }),
4724
- showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
4725
- showPreview && /* @__PURE__ */ jsxRuntime.jsx(
4726
- PreviewPanel,
6554
+ !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6555
+ showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
6556
+ EditorToolbar,
4727
6557
  {
4728
- pages: pdfPages,
4729
- isGenerating,
4730
- positionedFields,
4731
- headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
4732
- Button,
4733
- {
4734
- variant: "ghost",
4735
- size: "sm",
4736
- className: "h-8 w-8 p-0",
4737
- onClick: () => setEditorCollapsed(false),
4738
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
4739
- }
4740
- ) : void 0,
4741
- className: "flex-1 border-0 rounded-none"
6558
+ editor,
6559
+ insertFieldButton,
6560
+ insertVariableButton,
6561
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4742
6562
  }
4743
- )
4744
- ] }),
4745
- /* @__PURE__ */ jsxRuntime.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: [
4746
- /* @__PURE__ */ jsxRuntime.jsxs(
4747
- Button,
6563
+ ) }),
6564
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
6565
+ react.EditorContent,
4748
6566
  {
4749
- variant: "secondary",
4750
- onClick: generatePdf,
4751
- disabled: isGenerating || !editor,
4752
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4753
- children: [
4754
- isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
4755
- isGenerating ? "Generating..." : "Generate PDF"
4756
- ]
6567
+ editor,
6568
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4757
6569
  }
4758
- ),
4759
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
4760
- mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4761
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
4762
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
4763
- ] }),
4764
- generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4765
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
4766
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
4767
- ] }),
4768
- fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
4769
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
4770
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
4771
- fieldWarnings.length,
4772
- " field",
4773
- fieldWarnings.length !== 1 ? "s" : "",
4774
- " had warnings during generation"
4775
- ] })
4776
- ] }),
4777
- exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
4778
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
4779
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
4780
- ] })
4781
- ] }),
4782
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
6570
+ ) }),
6571
+ /* @__PURE__ */ jsxRuntime.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: [
4783
6572
  /* @__PURE__ */ jsxRuntime.jsxs(
4784
6573
  Button,
4785
6574
  {
4786
- onClick: handleExportMarkdown,
4787
- disabled: !editor || !markdown.trim(),
4788
- 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",
6575
+ variant: "secondary",
6576
+ onClick: generatePdf,
6577
+ disabled: isGenerating || !editor,
6578
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4789
6579
  children: [
4790
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
4791
- "Export MD"
6580
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
6581
+ isGenerating ? "Generating..." : "Generate PDF"
4792
6582
  ]
4793
6583
  }
4794
6584
  ),
4795
- /* @__PURE__ */ jsxRuntime.jsxs(
6585
+ onSaveTemplate ? /* @__PURE__ */ jsxRuntime.jsxs(
4796
6586
  Button,
4797
6587
  {
4798
- onClick: handleExport,
4799
- disabled: isGenerating || !editor || pdfPages.length === 0,
6588
+ onClick: handleSaveTemplate,
6589
+ disabled: isSaving || !editor || !markdown.trim(),
4800
6590
  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",
4801
6591
  children: [
4802
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
4803
- exportButtonText
6592
+ isSaving ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 16 }),
6593
+ isSaving ? "Saving..." : exportButtonText || "Save Template"
4804
6594
  ]
4805
6595
  }
4806
- )
6596
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
6597
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
6598
+ Button,
6599
+ {
6600
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6601
+ 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",
6602
+ children: [
6603
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6604
+ "Export",
6605
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14 })
6606
+ ]
6607
+ }
6608
+ ) }),
6609
+ /* @__PURE__ */ jsxRuntime.jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6610
+ /* @__PURE__ */ jsxRuntime.jsxs(
6611
+ "button",
6612
+ {
6613
+ onClick: handleExportMarkdown,
6614
+ disabled: !editor || !markdown.trim(),
6615
+ 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",
6616
+ children: [
6617
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
6618
+ "Export as MD"
6619
+ ]
6620
+ }
6621
+ ),
6622
+ /* @__PURE__ */ jsxRuntime.jsxs(
6623
+ "button",
6624
+ {
6625
+ onClick: handleExport,
6626
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6627
+ 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",
6628
+ children: [
6629
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6630
+ "Export as PDF"
6631
+ ]
6632
+ }
6633
+ )
6634
+ ] })
6635
+ ] }),
6636
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6637
+ mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6638
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6639
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
6640
+ ] }),
6641
+ generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6642
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6643
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
6644
+ ] }),
6645
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6646
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6647
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
6648
+ fieldWarnings.length,
6649
+ " field",
6650
+ fieldWarnings.length !== 1 ? "s" : "",
6651
+ " had warnings during generation"
6652
+ ] })
6653
+ ] }),
6654
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6655
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
6656
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
6657
+ ] })
6658
+ ] })
4807
6659
  ] })
4808
- ] })
4809
- ] }),
6660
+ ] }),
6661
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
6662
+ showPreview && /* @__PURE__ */ jsxRuntime.jsx(
6663
+ PreviewPanel,
6664
+ {
6665
+ pages: pdfPages,
6666
+ isGenerating,
6667
+ positionedFields,
6668
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
6669
+ Button,
6670
+ {
6671
+ variant: "ghost",
6672
+ size: "sm",
6673
+ className: "h-8 w-8 p-0",
6674
+ onClick: () => setEditorCollapsed(false),
6675
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
6676
+ }
6677
+ ) : void 0,
6678
+ className: "flex-1 border-0 rounded-none"
6679
+ }
6680
+ )
6681
+ ] }) }),
4810
6682
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsxRuntime.jsx(
4811
6683
  GeneratePanel,
4812
6684
  {
@@ -4814,7 +6686,10 @@ function DocumentGeneratorInner({
4814
6686
  editorContent: editor?.getJSON(),
4815
6687
  editorVariables: variables,
4816
6688
  onGeneratePdf,
4817
- initialBulkData
6689
+ initialBulkData,
6690
+ initialVariableValues,
6691
+ exportFileName,
6692
+ templateMode
4818
6693
  }
4819
6694
  ),
4820
6695
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4955,11 +6830,735 @@ function EditorPanel({
4955
6830
  ] });
4956
6831
  }
4957
6832
 
6833
+ // src/utils/xml-template-parser.ts
6834
+ function extractFieldRefs(expr) {
6835
+ const refs = [];
6836
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6837
+ let m;
6838
+ while ((m = re.exec(expr)) !== null) {
6839
+ if (m[2]) {
6840
+ refs.push(`${m[1]}.${m[2]}`);
6841
+ } else {
6842
+ refs.push(m[1]);
6843
+ }
6844
+ }
6845
+ return refs;
6846
+ }
6847
+ function fieldRefToVarName(ref, bandContext) {
6848
+ if (ref.includes(".")) {
6849
+ return labelToVarName(ref.replace(/\./g, "_"));
6850
+ }
6851
+ if (bandContext) {
6852
+ return labelToVarName(bandContext + "_" + ref);
6853
+ }
6854
+ return labelToVarName(ref);
6855
+ }
6856
+ function fieldRefToLabel(ref) {
6857
+ const parts = ref.split(".");
6858
+ const raw = parts[parts.length - 1];
6859
+ return raw.replace(/_/g, " ");
6860
+ }
6861
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6862
+ const trimmed = expr.trim();
6863
+ const suppressZero = /<>\s*0/.test(trimmed);
6864
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6865
+ const aggMatch = trimmed.match(
6866
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6867
+ );
6868
+ if (aggMatch) {
6869
+ const fieldName = aggMatch[2];
6870
+ const varName = labelToVarName("workorder_" + fieldName);
6871
+ const label = fieldName.replace(/_/g, " ");
6872
+ return registry.register(varName, label, szOpts);
6873
+ }
6874
+ const concatSingle = trimmed.match(
6875
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6876
+ );
6877
+ if (concatSingle) {
6878
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6879
+ const varName = fieldRefToVarName(ref, bandContext);
6880
+ const label = fieldRefToLabel(ref);
6881
+ return registry.register(varName, label, szOpts);
6882
+ }
6883
+ const concatMulti = trimmed.match(
6884
+ /^concat\(/i
6885
+ );
6886
+ if (concatMulti) {
6887
+ const refs = extractFieldRefs(trimmed);
6888
+ if (refs.length > 0) {
6889
+ const uniqueRefs = [...new Set(refs)];
6890
+ const tokens = uniqueRefs.map((ref) => {
6891
+ const varName = fieldRefToVarName(ref, bandContext);
6892
+ const label = fieldRefToLabel(ref);
6893
+ return registry.register(varName, label, szOpts);
6894
+ });
6895
+ return tokens.join(" ");
6896
+ }
6897
+ }
6898
+ const toLongMatch = trimmed.match(
6899
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6900
+ );
6901
+ if (toLongMatch) {
6902
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6903
+ const varName = fieldRefToVarName(ref, bandContext);
6904
+ const label = fieldRefToLabel(ref);
6905
+ return registry.register(varName, label, szOpts);
6906
+ }
6907
+ const fmtMatch = trimmed.match(
6908
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6909
+ );
6910
+ if (fmtMatch) {
6911
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6912
+ }
6913
+ const replaceMatch = trimmed.match(
6914
+ /^Replace\(\s*\[([^\]]+)\]/i
6915
+ );
6916
+ if (replaceMatch) {
6917
+ const ref = replaceMatch[1];
6918
+ const varName = fieldRefToVarName(ref, bandContext);
6919
+ const label = fieldRefToLabel(ref);
6920
+ return registry.register(varName, label, szOpts);
6921
+ }
6922
+ const iifLabelMatch = trimmed.match(
6923
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6924
+ );
6925
+ if (iifLabelMatch) {
6926
+ return iifLabelMatch[1];
6927
+ }
6928
+ const iifLabelElse = trimmed.match(
6929
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6930
+ );
6931
+ if (iifLabelElse) {
6932
+ return iifLabelElse[1];
6933
+ }
6934
+ const iifEmptyElse = trimmed.match(
6935
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6936
+ );
6937
+ if (iifEmptyElse) {
6938
+ return iifEmptyElse[1];
6939
+ }
6940
+ const nestedIifStaticLabel = trimmed.match(
6941
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6942
+ );
6943
+ if (nestedIifStaticLabel) {
6944
+ return nestedIifStaticLabel[1];
6945
+ }
6946
+ const iifValueMatch = trimmed.match(
6947
+ /^iif\s*\(/i
6948
+ );
6949
+ if (iifValueMatch) {
6950
+ const refs = extractFieldRefs(trimmed);
6951
+ if (refs.length > 0) {
6952
+ const uniqueRefs = [...new Set(refs)];
6953
+ const innerAggMatch = trimmed.match(
6954
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6955
+ );
6956
+ if (innerAggMatch) {
6957
+ const fieldName = innerAggMatch[2];
6958
+ const varName2 = labelToVarName("workorder_" + fieldName);
6959
+ const label2 = fieldName.replace(/_/g, " ");
6960
+ return registry.register(varName2, label2, szOpts);
6961
+ }
6962
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6963
+ const varName = fieldRefToVarName(ref, bandContext);
6964
+ const label = fieldRefToLabel(ref);
6965
+ return registry.register(varName, label, szOpts);
6966
+ }
6967
+ const staticMatch = trimmed.match(/'([^']+)'/);
6968
+ if (staticMatch) return staticMatch[1];
6969
+ return "";
6970
+ }
6971
+ const arithmeticRefs = extractFieldRefs(trimmed);
6972
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6973
+ if (siblingLabel) {
6974
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6975
+ const varName = labelToVarName(
6976
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6977
+ );
6978
+ return registry.register(varName, cleanLabel, szOpts);
6979
+ }
6980
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6981
+ const tokens = uniqueRefs.map((ref) => {
6982
+ const varName = fieldRefToVarName(ref, bandContext);
6983
+ const label = fieldRefToLabel(ref);
6984
+ return registry.register(varName, label, szOpts);
6985
+ });
6986
+ return tokens.join(" ");
6987
+ }
6988
+ const simpleMatch = trimmed.match(
6989
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6990
+ );
6991
+ if (simpleMatch) {
6992
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6993
+ const varName = fieldRefToVarName(ref, bandContext);
6994
+ const label = fieldRefToLabel(ref);
6995
+ return registry.register(varName, label, szOpts);
6996
+ }
6997
+ if (arithmeticRefs.length >= 1) {
6998
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6999
+ const varName = fieldRefToVarName(ref, bandContext);
7000
+ const label = fieldRefToLabel(ref);
7001
+ return registry.register(varName, label, szOpts);
7002
+ }
7003
+ return "";
7004
+ }
7005
+ var VariableRegistry = class {
7006
+ constructor() {
7007
+ __publicField(this, "map", /* @__PURE__ */ new Map());
7008
+ }
7009
+ register(varName, varLabel, options) {
7010
+ if (!this.map.has(varName)) {
7011
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
7012
+ }
7013
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
7014
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
7015
+ }
7016
+ getAll() {
7017
+ return Array.from(this.map.values());
7018
+ }
7019
+ };
7020
+ function parseLocationFloat(s) {
7021
+ if (!s) return { x: 0, y: 0 };
7022
+ const parts = s.split(",");
7023
+ return {
7024
+ x: parseFloat(parts[0]) || 0,
7025
+ y: parseFloat(parts[1]) || 0
7026
+ };
7027
+ }
7028
+ function parseSizeF(s) {
7029
+ if (!s) return { w: 0, h: 0 };
7030
+ const parts = s.split(",");
7031
+ return {
7032
+ w: parseFloat(parts[0]) || 0,
7033
+ h: parseFloat(parts[1]) || 0
7034
+ };
7035
+ }
7036
+ function extractControls(bandEl) {
7037
+ const controlsEl = bandEl.querySelector(":scope > Controls");
7038
+ if (!controlsEl) return [];
7039
+ const items = [];
7040
+ for (const child of Array.from(controlsEl.children)) {
7041
+ items.push(extractControl(child));
7042
+ }
7043
+ return items;
7044
+ }
7045
+ function extractControl(el) {
7046
+ const ctrlType = el.getAttribute("ControlType") || "";
7047
+ const name = el.getAttribute("Name") || "";
7048
+ const text = el.getAttribute("Text") || "";
7049
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
7050
+ const size = parseSizeF(el.getAttribute("SizeF"));
7051
+ const formatString = el.getAttribute("TextFormatString") || null;
7052
+ let expression = null;
7053
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
7054
+ if (bindingsEl) {
7055
+ for (const item of Array.from(bindingsEl.children)) {
7056
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
7057
+ expression = item.getAttribute("Expression");
7058
+ break;
7059
+ }
7060
+ }
7061
+ }
7062
+ const children = [];
7063
+ if (ctrlType === "XRPanel") {
7064
+ const panelControls = extractControls(el);
7065
+ children.push(...panelControls);
7066
+ }
7067
+ return {
7068
+ name,
7069
+ type: ctrlType,
7070
+ text,
7071
+ x: loc.x,
7072
+ y: loc.y,
7073
+ width: size.w,
7074
+ expression,
7075
+ formatString,
7076
+ children
7077
+ };
7078
+ }
7079
+ function groupByRow(controls, threshold = 8) {
7080
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
7081
+ const rows = [];
7082
+ for (const ctrl of sorted) {
7083
+ const lastRow = rows[rows.length - 1];
7084
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
7085
+ lastRow.controls.push(ctrl);
7086
+ } else {
7087
+ rows.push({ y: ctrl.y, controls: [ctrl] });
7088
+ }
7089
+ }
7090
+ for (const row of rows) {
7091
+ row.controls.sort((a, b) => a.x - b.x);
7092
+ }
7093
+ return rows;
7094
+ }
7095
+ function isStaticLabel(ctrl) {
7096
+ return ctrl.type === "XRLabel" && !ctrl.expression;
7097
+ }
7098
+ function isBound(ctrl) {
7099
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
7100
+ }
7101
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
7102
+ const leftExtent = leftControls.reduce(
7103
+ (max, c) => Math.max(max, c.x + c.width),
7104
+ 0
7105
+ );
7106
+ const rightMinX = rightControls.reduce(
7107
+ (min, c) => Math.min(min, c.x),
7108
+ contentWidth
7109
+ );
7110
+ const rightMaxExtent = rightControls.reduce(
7111
+ (max, c) => Math.max(max, c.x + c.width),
7112
+ 0
7113
+ );
7114
+ const rightExtent = rightMaxExtent - rightMinX;
7115
+ if (leftExtent + rightExtent === 0) return 50;
7116
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
7117
+ }
7118
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
7119
+ const lines = [];
7120
+ const leftControls = controls.filter((c) => c.x < 437);
7121
+ const rightControls = controls.filter((c) => c.x >= 437);
7122
+ const leftEntries = [];
7123
+ if (leftControls.length > 0) {
7124
+ const leftRows = groupByRow(leftControls);
7125
+ for (const row of leftRows) {
7126
+ const parts = [];
7127
+ for (const ctrl of row.controls) {
7128
+ if (isBound(ctrl)) {
7129
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7130
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
7131
+ resolved = resolved.replace("}}", "|format:phone}}");
7132
+ }
7133
+ if (resolved) parts.push(resolved);
7134
+ } else if (isStaticLabel(ctrl)) {
7135
+ const text = ctrl.text.trim();
7136
+ if (text && !text.match(/^label\d+$/)) {
7137
+ parts.push(text);
7138
+ }
7139
+ }
7140
+ }
7141
+ if (parts.length > 0) {
7142
+ leftEntries.push(parts.join(" "));
7143
+ }
7144
+ }
7145
+ }
7146
+ const rightEntries = [];
7147
+ if (rightControls.length > 0) {
7148
+ const rightRows = groupByRow(rightControls);
7149
+ for (const row of rightRows) {
7150
+ let label = "";
7151
+ let value = "";
7152
+ for (const ctrl of row.controls) {
7153
+ if (isStaticLabel(ctrl)) {
7154
+ const text = ctrl.text.trim();
7155
+ if (text && !text.match(/^label\d+$/)) {
7156
+ label = text;
7157
+ }
7158
+ } else if (isBound(ctrl)) {
7159
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
7160
+ if (ctrl.expression) {
7161
+ const refs = extractFieldRefs(ctrl.expression);
7162
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
7163
+ value = value.replace("}}", "|suppress:zero}}");
7164
+ }
7165
+ }
7166
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7167
+ value = value.replace("}}", "|format:phone}}");
7168
+ }
7169
+ }
7170
+ }
7171
+ if (label || value) {
7172
+ rightEntries.push({ label, value, y: row.y });
7173
+ }
7174
+ }
7175
+ }
7176
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7177
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7178
+ lines.push(`:::columns{split:${outerSplit}}`);
7179
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
7180
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
7181
+ const xmlLineHeight = 17;
7182
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
7183
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
7184
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
7185
+ lines.push(leftColTag);
7186
+ for (const entry of leftEntries) {
7187
+ lines.push(entry);
7188
+ }
7189
+ lines.push(":::");
7190
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
7191
+ lines.push(rightColTag);
7192
+ if (rightEntries.length > 0) {
7193
+ lines.push("| | |");
7194
+ lines.push("|---|---|");
7195
+ let prevY = -Infinity;
7196
+ for (const entry of rightEntries) {
7197
+ if (prevY > -Infinity && entry.y - prevY > 25) {
7198
+ lines.push("| | |");
7199
+ }
7200
+ const label = entry.label || "";
7201
+ const colon = entry.label ? ":" : "";
7202
+ const value = entry.value ? ` ${entry.value}` : "";
7203
+ lines.push(`| ${label} | ${colon}${value} |`);
7204
+ prevY = entry.y;
7205
+ }
7206
+ }
7207
+ lines.push(":::");
7208
+ lines.push(":::");
7209
+ lines.push("");
7210
+ }
7211
+ return lines.join("\n");
7212
+ }
7213
+ function renderReportHeaderBand(controls, registry, contentWidth) {
7214
+ const lines = [];
7215
+ const leftControls = controls.filter((c) => c.x < 437);
7216
+ const rightControls = controls.filter((c) => c.x >= 437);
7217
+ const leftEntries = [];
7218
+ if (leftControls.length > 0) {
7219
+ const leftRows = groupByRow(leftControls);
7220
+ for (const row of leftRows) {
7221
+ const parts = [];
7222
+ for (const ctrl of row.controls) {
7223
+ if (isBound(ctrl)) {
7224
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7225
+ if (resolved) parts.push(resolved);
7226
+ } else if (isStaticLabel(ctrl)) {
7227
+ const text = ctrl.text.trim();
7228
+ if (text && !text.match(/^label\d+$/)) {
7229
+ parts.push(text);
7230
+ }
7231
+ }
7232
+ }
7233
+ if (parts.length > 0) {
7234
+ leftEntries.push(parts.join(" "));
7235
+ }
7236
+ }
7237
+ }
7238
+ const rightEntries = [];
7239
+ if (rightControls.length > 0) {
7240
+ const rightRows = groupByRow(rightControls);
7241
+ for (const row of rightRows) {
7242
+ let label = "";
7243
+ let value = "";
7244
+ for (const ctrl of row.controls) {
7245
+ if (isStaticLabel(ctrl)) {
7246
+ const text = ctrl.text.trim();
7247
+ if (text && !text.match(/^label\d+$/)) {
7248
+ label = text;
7249
+ }
7250
+ } else if (isBound(ctrl)) {
7251
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7252
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7253
+ value = value.replace("}}", "|format:phone}}");
7254
+ }
7255
+ }
7256
+ }
7257
+ if (label || value) {
7258
+ rightEntries.push({ label, value });
7259
+ }
7260
+ }
7261
+ }
7262
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7263
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7264
+ lines.push(`:::columns{split:${outerSplit}}`);
7265
+ lines.push(":::col");
7266
+ for (const entry of leftEntries) {
7267
+ lines.push(entry);
7268
+ }
7269
+ lines.push(":::");
7270
+ lines.push(":::col");
7271
+ lines.push("| | |");
7272
+ lines.push("|---|---|");
7273
+ for (const entry of rightEntries) {
7274
+ const label = entry.label || "";
7275
+ const value = entry.value ? ` ${entry.value}` : "";
7276
+ lines.push(`| ${label} |${value} |`);
7277
+ }
7278
+ lines.push(":::");
7279
+ lines.push(":::");
7280
+ lines.push("");
7281
+ }
7282
+ return lines.join("\n");
7283
+ }
7284
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
7285
+ const lines = [];
7286
+ const headerTexts = [];
7287
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
7288
+ if (isStaticLabel(ctrl)) {
7289
+ const text = ctrl.text.trim();
7290
+ if (text) headerTexts.push(text);
7291
+ }
7292
+ }
7293
+ const titleText = headerTexts.join(" ");
7294
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
7295
+ lines.push(":::");
7296
+ lines.push(":::repeat{data:operations}");
7297
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
7298
+ const detailParts = [];
7299
+ for (const ctrl of sortedDetail) {
7300
+ if (isBound(ctrl)) {
7301
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
7302
+ if (resolved) detailParts.push(resolved);
7303
+ }
7304
+ }
7305
+ if (detailParts.length > 0) {
7306
+ if (detailParts.length >= 2) {
7307
+ lines.push(":::columns{split:12}");
7308
+ lines.push(":::col");
7309
+ lines.push(`**${detailParts[0]}**`);
7310
+ lines.push(":::");
7311
+ lines.push(":::col");
7312
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
7313
+ lines.push(":::");
7314
+ lines.push(":::");
7315
+ } else {
7316
+ lines.push(`**${detailParts[0]}**`);
7317
+ }
7318
+ lines.push("");
7319
+ }
7320
+ if (groupFooterControls.length > 0) {
7321
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
7322
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
7323
+ }
7324
+ lines.push("---");
7325
+ lines.push(":::");
7326
+ lines.push("");
7327
+ return lines.join("\n");
7328
+ }
7329
+ function renderGroupFooterBand(controls, registry, _contentWidth) {
7330
+ const rows = groupByRow(controls);
7331
+ const entries = [];
7332
+ for (const row of rows) {
7333
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7334
+ if (labels.length === 0) continue;
7335
+ let label = "";
7336
+ let value = "";
7337
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7338
+ for (const ctrl of sorted) {
7339
+ if (isStaticLabel(ctrl)) {
7340
+ const text = ctrl.text.trim();
7341
+ if (text && !text.match(/^label\d+$/)) {
7342
+ label = text;
7343
+ }
7344
+ } else if (isBound(ctrl)) {
7345
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
7346
+ if (resolved) {
7347
+ if (!resolved.startsWith("{{var|")) {
7348
+ if (!label) label = resolved;
7349
+ } else {
7350
+ value = resolved;
7351
+ }
7352
+ }
7353
+ }
7354
+ }
7355
+ if (label && value) {
7356
+ entries.push({ label, value });
7357
+ }
7358
+ }
7359
+ if (entries.length === 0) return "";
7360
+ const lines = [];
7361
+ lines.push(":::subtotals");
7362
+ for (const entry of entries) {
7363
+ lines.push(`**${entry.label}** ${entry.value}`);
7364
+ }
7365
+ lines.push(":::");
7366
+ lines.push("");
7367
+ return lines.join("\n");
7368
+ }
7369
+ function renderReportFooterBand(controls, registry) {
7370
+ const lines = [];
7371
+ const panel = controls.find((c) => c.type === "XRPanel");
7372
+ const thankYouLabel = controls.find(
7373
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
7374
+ );
7375
+ const priceRows = [];
7376
+ if (panel && panel.children.length > 0) {
7377
+ const rows = groupByRow(panel.children, 15);
7378
+ for (const row of rows) {
7379
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7380
+ if (labels.length === 0) continue;
7381
+ let label = "";
7382
+ let value = "";
7383
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7384
+ for (const ctrl of sorted) {
7385
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
7386
+ continue;
7387
+ }
7388
+ const isLabelPos = ctrl.x < 150;
7389
+ if (isStaticLabel(ctrl)) {
7390
+ const text = ctrl.text.trim();
7391
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
7392
+ label = text;
7393
+ }
7394
+ } else if (isBound(ctrl)) {
7395
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
7396
+ if (resolved) {
7397
+ if (isLabelPos) {
7398
+ if (!label) label = resolved;
7399
+ } else {
7400
+ if (!value) value = resolved;
7401
+ }
7402
+ }
7403
+ }
7404
+ }
7405
+ if (label && value) {
7406
+ priceRows.push({ label, value });
7407
+ } else if (value) {
7408
+ priceRows.push({ label: "", value });
7409
+ }
7410
+ }
7411
+ }
7412
+ lines.push(":::columns{split:40|padX:20}");
7413
+ lines.push(":::col{padTop:1}");
7414
+ if (thankYouLabel) {
7415
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
7416
+ }
7417
+ lines.push(":::");
7418
+ lines.push(":::col{padTop:1}");
7419
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
7420
+ if (priceRows.length > 0) {
7421
+ lines.push(":::subtotals");
7422
+ for (const row of priceRows) {
7423
+ const label = row.label ? `**${row.label}**` : "";
7424
+ lines.push(`${label} ${row.value}`);
7425
+ }
7426
+ lines.push(":::");
7427
+ }
7428
+ lines.push(":::");
7429
+ lines.push(":::");
7430
+ lines.push(":::");
7431
+ lines.push("");
7432
+ return lines.join("\n");
7433
+ }
7434
+ function parseXmlTemplate(xmlString) {
7435
+ const warnings = [];
7436
+ const registry = new VariableRegistry();
7437
+ const parser = new DOMParser();
7438
+ const doc = parser.parseFromString(xmlString, "text/xml");
7439
+ const parseError = doc.querySelector("parsererror");
7440
+ if (parseError) {
7441
+ return {
7442
+ markdown: "",
7443
+ variables: [],
7444
+ reportName: "",
7445
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7446
+ };
7447
+ }
7448
+ const root = doc.documentElement;
7449
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7450
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7451
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7452
+ const marginParts = marginsStr.split(",").map(Number);
7453
+ const leftMargin = marginParts[0] || 50;
7454
+ const rightMargin = marginParts[1] || 50;
7455
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7456
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7457
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7458
+ if (calcFieldsEl) {
7459
+ for (const item of Array.from(calcFieldsEl.children)) {
7460
+ const name = item.getAttribute("Name");
7461
+ if (name) calculatedFieldNames.add(name);
7462
+ }
7463
+ }
7464
+ const markdownSections = [];
7465
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7466
+ if (watermarkEl) {
7467
+ const wmText = watermarkEl.getAttribute("Text");
7468
+ if (wmText) {
7469
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7470
+ markdownSections.push("");
7471
+ }
7472
+ }
7473
+ const bandsEl = root.querySelector(":scope > Bands");
7474
+ if (!bandsEl) {
7475
+ warnings.push("No <Bands> element found in the XML");
7476
+ return {
7477
+ markdown: "",
7478
+ variables: registry.getAll(),
7479
+ reportName,
7480
+ warnings
7481
+ };
7482
+ }
7483
+ let topMarginControls = [];
7484
+ let reportHeaderControls = [];
7485
+ let pageHeaderControls = [];
7486
+ let detailControls = [];
7487
+ let groupFooterControls = [];
7488
+ let reportFooterControls = [];
7489
+ for (const bandItem of Array.from(bandsEl.children)) {
7490
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7491
+ if (ctrlType === "TopMarginBand") {
7492
+ topMarginControls = extractControls(bandItem);
7493
+ } else if (ctrlType === "ReportHeaderBand") {
7494
+ reportHeaderControls = extractControls(bandItem);
7495
+ } else if (ctrlType === "PageHeaderBand") {
7496
+ pageHeaderControls = extractControls(bandItem);
7497
+ } else if (ctrlType === "DetailReportBand") {
7498
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7499
+ if (nestedBands) {
7500
+ for (const nested of Array.from(nestedBands.children)) {
7501
+ const nestedType = nested.getAttribute("ControlType") || "";
7502
+ if (nestedType === "DetailBand") {
7503
+ detailControls = extractControls(nested);
7504
+ } else if (nestedType === "GroupFooterBand") {
7505
+ groupFooterControls = extractControls(nested);
7506
+ }
7507
+ }
7508
+ }
7509
+ } else if (ctrlType === "ReportFooterBand") {
7510
+ reportFooterControls = extractControls(bandItem);
7511
+ }
7512
+ }
7513
+ if (topMarginControls.length > 0) {
7514
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7515
+ }
7516
+ if (reportHeaderControls.length > 0) {
7517
+ markdownSections.push(
7518
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7519
+ );
7520
+ }
7521
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7522
+ markdownSections.push(
7523
+ renderOperationsSection(
7524
+ pageHeaderControls,
7525
+ detailControls,
7526
+ groupFooterControls,
7527
+ registry)
7528
+ );
7529
+ }
7530
+ if (reportFooterControls.length > 0) {
7531
+ markdownSections.push(
7532
+ renderReportFooterBand(reportFooterControls, registry)
7533
+ );
7534
+ }
7535
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7536
+ return {
7537
+ markdown,
7538
+ variables: registry.getAll(),
7539
+ reportName,
7540
+ warnings
7541
+ };
7542
+ }
7543
+
4958
7544
  exports.DocumentGenerator = DocumentGenerator;
4959
7545
  exports.EditorPanel = EditorPanel;
4960
7546
  exports.FormFieldType = FormFieldType;
7547
+ exports.ImageNode = ImageNode;
4961
7548
  exports.PreviewPanel = PreviewPanel;
7549
+ exports.RepeatNode = RepeatNode;
7550
+ exports.SubtotalsNode = SubtotalsNode;
4962
7551
  exports.ThemeProvider = ThemeProvider;
7552
+ exports.expandBlockVariables = expandBlockVariables;
7553
+ exports.expandRepeatContent = expandRepeatContent;
7554
+ exports.extractVariablesFromContent = extractVariablesFromContent;
7555
+ exports.generatePdfFromMarkdown = generatePdfFromMarkdown;
7556
+ exports.generatePdfFromTiptap = generatePdfFromTiptap;
7557
+ exports.isZeroLike = isZeroLike;
7558
+ exports.markdownToTiptap = markdownToTiptap;
7559
+ exports.parseXmlTemplate = parseXmlTemplate;
7560
+ exports.suppressZeroContent = suppressZeroContent;
7561
+ exports.tiptapToMarkdown = tiptapToMarkdown;
4963
7562
  exports.useDocumentGenerator = useDocumentGenerator;
4964
7563
  exports.useTheme = useTheme;
4965
7564
  //# sourceMappingURL=index.js.map