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

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 (102) 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/PreviewPanel.d.ts +12 -0
  21. package/dist/components/PreviewPanel.d.ts.map +1 -0
  22. package/dist/components/VariableEditPopover.d.ts +12 -0
  23. package/dist/components/VariableEditPopover.d.ts.map +1 -0
  24. package/dist/components/VariableInsertPopover.d.ts +13 -0
  25. package/dist/components/VariableInsertPopover.d.ts.map +1 -0
  26. package/dist/components/VariableNodeView.d.ts +3 -0
  27. package/dist/components/VariableNodeView.d.ts.map +1 -0
  28. package/dist/components/WatermarkNodeView.d.ts +3 -0
  29. package/dist/components/WatermarkNodeView.d.ts.map +1 -0
  30. package/dist/contexts/ThemeContext.d.ts +19 -0
  31. package/dist/contexts/ThemeContext.d.ts.map +1 -0
  32. package/dist/extensions/columns-node.d.ts +4 -0
  33. package/dist/extensions/columns-node.d.ts.map +1 -0
  34. package/dist/extensions/field-node.d.ts +31 -0
  35. package/dist/extensions/field-node.d.ts.map +1 -0
  36. package/dist/extensions/panel-node.d.ts +3 -0
  37. package/dist/extensions/panel-node.d.ts.map +1 -0
  38. package/dist/extensions/repeat-node.d.ts +3 -0
  39. package/dist/extensions/repeat-node.d.ts.map +1 -0
  40. package/dist/extensions/subtotals-node.d.ts +3 -0
  41. package/dist/extensions/subtotals-node.d.ts.map +1 -0
  42. package/dist/extensions/variable-node.d.ts +18 -0
  43. package/dist/extensions/variable-node.d.ts.map +1 -0
  44. package/dist/extensions/watermark-node.d.ts +3 -0
  45. package/dist/extensions/watermark-node.d.ts.map +1 -0
  46. package/dist/hooks/useDocumentGenerator.d.ts +46 -0
  47. package/dist/hooks/useDocumentGenerator.d.ts.map +1 -0
  48. package/dist/index.d.ts +19 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +3081 -562
  51. package/dist/index.js.map +1 -1
  52. package/dist/index.mjs +3068 -565
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/lib/ui/button.d.ts +12 -0
  55. package/dist/lib/ui/button.d.ts.map +1 -0
  56. package/dist/lib/ui/index.d.ts +10 -0
  57. package/dist/lib/ui/index.d.ts.map +1 -0
  58. package/dist/lib/ui/input.d.ts +6 -0
  59. package/dist/lib/ui/input.d.ts.map +1 -0
  60. package/dist/lib/ui/label.d.ts +8 -0
  61. package/dist/lib/ui/label.d.ts.map +1 -0
  62. package/dist/lib/ui/popover.d.ts +8 -0
  63. package/dist/lib/ui/popover.d.ts.map +1 -0
  64. package/dist/lib/ui/select.d.ts +14 -0
  65. package/dist/lib/ui/select.d.ts.map +1 -0
  66. package/dist/lib/ui/toggle-group.d.ts +13 -0
  67. package/dist/lib/ui/toggle-group.d.ts.map +1 -0
  68. package/dist/lib/ui/tooltip.d.ts +8 -0
  69. package/dist/lib/ui/tooltip.d.ts.map +1 -0
  70. package/dist/lib/utils.d.ts +3 -0
  71. package/dist/lib/utils.d.ts.map +1 -0
  72. package/dist/styles/index.css +349 -194
  73. package/dist/styles/index.d.ts +1 -0
  74. package/dist/types/index.d.ts +204 -0
  75. package/dist/types/index.d.ts.map +1 -0
  76. package/dist/utils/error-helpers.d.ts +27 -0
  77. package/dist/utils/error-helpers.d.ts.map +1 -0
  78. package/dist/utils/field-helpers.d.ts +31 -0
  79. package/dist/utils/field-helpers.d.ts.map +1 -0
  80. package/dist/utils/markdown-parser.d.ts +6 -0
  81. package/dist/utils/markdown-parser.d.ts.map +1 -0
  82. package/dist/utils/markdown-validator.d.ts +13 -0
  83. package/dist/utils/markdown-validator.d.ts.map +1 -0
  84. package/dist/utils/markdown-writer.d.ts +6 -0
  85. package/dist/utils/markdown-writer.d.ts.map +1 -0
  86. package/dist/utils/pdf-generator.d.ts +77 -0
  87. package/dist/utils/pdf-generator.d.ts.map +1 -0
  88. package/dist/utils/pdf-metadata.d.ts +58 -0
  89. package/dist/utils/pdf-metadata.d.ts.map +1 -0
  90. package/dist/utils/pdf-preview.d.ts +7 -0
  91. package/dist/utils/pdf-preview.d.ts.map +1 -0
  92. package/dist/utils/template-pipeline.d.ts +39 -0
  93. package/dist/utils/template-pipeline.d.ts.map +1 -0
  94. package/dist/utils/theme-helpers.d.ts +21 -0
  95. package/dist/utils/theme-helpers.d.ts.map +1 -0
  96. package/dist/utils/variable-helpers.d.ts +74 -0
  97. package/dist/utils/variable-helpers.d.ts.map +1 -0
  98. package/dist/utils/xml-template-parser.d.ts +9 -0
  99. package/dist/utils/xml-template-parser.d.ts.map +1 -0
  100. package/package.json +16 -6
  101. package/src/styles/index.css +140 -68
  102. 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,169 @@ 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
+ var RepeatNode = core.Node.create({
456
+ name: "repeatBlock",
457
+ group: "block",
458
+ content: "block+",
459
+ addAttributes() {
460
+ return {
461
+ data: { default: "" }
462
+ };
463
+ },
464
+ parseHTML() {
465
+ return [{ tag: "div[data-repeat-node]" }];
466
+ },
467
+ renderHTML({ HTMLAttributes }) {
468
+ return ["div", core.mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
469
+ }
470
+ });
471
+ var SubtotalsNode = core.Node.create({
472
+ name: "subtotalsBlock",
473
+ group: "block",
474
+ content: "block+",
475
+ defining: true,
476
+ parseHTML() {
477
+ return [{ tag: "div[data-subtotals-node]" }];
478
+ },
479
+ renderHTML({ HTMLAttributes }) {
480
+ return ["div", core.mergeAttributes(
481
+ { "data-subtotals-node": "", class: "subtotals-block" },
482
+ HTMLAttributes
483
+ ), 0];
484
+ }
485
+ });
312
486
 
313
487
  // src/types/index.ts
314
488
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
@@ -387,158 +561,23 @@ function extractFieldsFromContent(content) {
387
561
  }
388
562
  return fields;
389
563
  }
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);
564
+ var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
565
+ var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
566
+ var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
567
+ var DIRECTIVE_CLOSE = /^:::$/;
568
+ function parseDirectiveAttrs(str) {
569
+ const attrs = {};
570
+ if (!str) return attrs;
571
+ const pairs = str.split("|");
572
+ for (const pair of pairs) {
573
+ const colonIdx = pair.indexOf(":");
574
+ if (colonIdx === -1) continue;
575
+ const key = pair.slice(0, colonIdx).trim();
576
+ const value = pair.slice(colonIdx + 1).trim();
577
+ if (key) attrs[key] = value;
535
578
  }
579
+ return attrs;
536
580
  }
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
581
  function injectMarks(token, marks) {
543
582
  const existingMatch = token.match(/\|_marks:([^}|]+)/);
544
583
  if (existingMatch) {
@@ -655,6 +694,15 @@ function parseVariableToken(tokenBody) {
655
694
  case "default":
656
695
  attrs.varDefault = value;
657
696
  break;
697
+ case "suppress":
698
+ attrs.suppressZero = value === "zero" ? "true" : "";
699
+ break;
700
+ case "format":
701
+ attrs.format = value;
702
+ break;
703
+ case "block":
704
+ attrs.block = value === "true" ? "true" : "";
705
+ break;
658
706
  case "_marks":
659
707
  attrs._marks = value;
660
708
  break;
@@ -662,6 +710,44 @@ function parseVariableToken(tokenBody) {
662
710
  }
663
711
  return attrs;
664
712
  }
713
+ function parseImageToken(tokenBody) {
714
+ const attrs = {
715
+ src: "",
716
+ varName: "",
717
+ alt: "",
718
+ width: "",
719
+ height: "",
720
+ align: ""
721
+ };
722
+ const pairs = tokenBody.split("|");
723
+ for (const pair of pairs) {
724
+ const colonIdx = pair.indexOf(":");
725
+ if (colonIdx === -1) continue;
726
+ const key = pair.slice(0, colonIdx).trim();
727
+ const value = pair.slice(colonIdx + 1).trim();
728
+ switch (key) {
729
+ case "src":
730
+ attrs.src = value;
731
+ break;
732
+ case "var":
733
+ attrs.varName = value;
734
+ break;
735
+ case "alt":
736
+ attrs.alt = value;
737
+ break;
738
+ case "width":
739
+ attrs.width = value;
740
+ break;
741
+ case "height":
742
+ attrs.height = value;
743
+ break;
744
+ case "align":
745
+ attrs.align = value;
746
+ break;
747
+ }
748
+ }
749
+ return attrs;
750
+ }
665
751
  function parseInline(text) {
666
752
  text = preprocessMarkedTokens(text);
667
753
  const nodes = [];
@@ -685,6 +771,8 @@ function parseInline(text) {
685
771
  const node = { type: "variableNode", attrs };
686
772
  if (marks.length > 0) node.marks = marks;
687
773
  nodes.push(node);
774
+ } else if (match[1] === "image") {
775
+ nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
688
776
  }
689
777
  lastIndex = match.index + match[0].length;
690
778
  }
@@ -696,45 +784,63 @@ function parseInline(text) {
696
784
  function parseFormattedText(text) {
697
785
  if (!text) return [];
698
786
  const nodes = [];
699
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
787
+ const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
700
788
  for (const part of parts) {
701
789
  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
- });
790
+ if (part.startsWith("***") && part.endsWith("***")) {
791
+ const inner = part.slice(3, -3);
792
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
793
+ } else if (part.startsWith("**") && part.endsWith("**")) {
794
+ const inner = part.slice(2, -2);
795
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
708
796
  } else if (part.startsWith("*") && part.endsWith("*")) {
709
- nodes.push({
710
- type: "text",
711
- text: part.slice(1, -1),
712
- marks: [{ type: "italic" }]
713
- });
797
+ const inner = part.slice(1, -1);
798
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
714
799
  } else if (part.startsWith("__") && part.endsWith("__")) {
715
- nodes.push({
716
- type: "text",
717
- text: part.slice(2, -2),
718
- marks: [{ type: "underline" }]
719
- });
800
+ const inner = part.slice(2, -2);
801
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
720
802
  } else if (part.startsWith("`") && part.endsWith("`")) {
721
- nodes.push({
722
- type: "text",
723
- text: part.slice(1, -1),
724
- marks: [{ type: "code" }]
725
- });
803
+ const inner = part.slice(1, -1);
804
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
726
805
  } else {
727
806
  nodes.push({ type: "text", text: part });
728
807
  }
729
808
  }
730
809
  return nodes;
731
810
  }
732
- function markdownToTiptap(markdown) {
733
- const lines = markdown.split("\n");
734
- const content = [];
735
- let i = 0;
736
- while (i < lines.length) {
737
- const line = lines[i];
811
+ function splitTableCells(row) {
812
+ const cells = [];
813
+ let current = "";
814
+ let depth = 0;
815
+ for (let i = 0; i < row.length; i++) {
816
+ if (row[i] === "{" && row[i + 1] === "{") {
817
+ depth++;
818
+ current += "{{";
819
+ i++;
820
+ } else if (row[i] === "}" && row[i + 1] === "}") {
821
+ depth = Math.max(0, depth - 1);
822
+ current += "}}";
823
+ i++;
824
+ } else if (row[i] === "|" && depth === 0) {
825
+ cells.push(current.trim());
826
+ current = "";
827
+ } else {
828
+ current += row[i];
829
+ }
830
+ }
831
+ if (current.trim()) {
832
+ cells.push(current.trim());
833
+ }
834
+ return cells;
835
+ }
836
+ function parseBlocks(lines, startIdx, stopCondition) {
837
+ const content = [];
838
+ let i = startIdx;
839
+ while (i < lines.length) {
840
+ const line = lines[i];
841
+ if (stopCondition && stopCondition(line)) {
842
+ break;
843
+ }
738
844
  if (line.trim() === "") {
739
845
  i++;
740
846
  continue;
@@ -808,13 +914,431 @@ function markdownToTiptap(markdown) {
808
914
  });
809
915
  continue;
810
916
  }
811
- content.push({
812
- type: "paragraph",
813
- content: parseInline(line)
814
- });
917
+ if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
918
+ const tableLines = [];
919
+ while (i < lines.length) {
920
+ const tl = lines[i];
921
+ if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
922
+ tableLines.push(tl);
923
+ i++;
924
+ } else {
925
+ break;
926
+ }
927
+ }
928
+ const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
929
+ const dataLines = tableLines.filter(
930
+ (tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
931
+ );
932
+ const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
933
+ const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
934
+ const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
935
+ const tableRows = [];
936
+ let isFirstDataRow = true;
937
+ for (const tl of tableLines) {
938
+ if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
939
+ const cells = splitTableCells(tl).filter((c) => c !== "");
940
+ if (isBorderless && isFirstDataRow) {
941
+ isFirstDataRow = false;
942
+ continue;
943
+ }
944
+ const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
945
+ const rowContent = cells.map((cellText) => ({
946
+ type: cellType,
947
+ content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
948
+ }));
949
+ tableRows.push({ type: "tableRow", content: rowContent });
950
+ isFirstDataRow = false;
951
+ }
952
+ if (tableRows.length > 0) {
953
+ const tableNode = { type: "table", content: tableRows };
954
+ if (isBorderless) {
955
+ tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
956
+ }
957
+ content.push(tableNode);
958
+ }
959
+ continue;
960
+ }
961
+ const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
962
+ if (directiveMatch) {
963
+ const directiveType = directiveMatch[1];
964
+ const rawAttrs = directiveMatch[2] || "";
965
+ const attrs = parseDirectiveAttrs(rawAttrs);
966
+ i++;
967
+ if (directiveType === "watermark") {
968
+ content.push({
969
+ type: "watermark",
970
+ attrs: {
971
+ text: attrs.text || "",
972
+ opacity: attrs.opacity || "0.15",
973
+ angle: attrs.angle || "-45"
974
+ }
975
+ });
976
+ continue;
977
+ }
978
+ if (directiveType === "panel") {
979
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
980
+ i = inner.nextIdx;
981
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
982
+ i++;
983
+ }
984
+ content.push({
985
+ type: "panel",
986
+ attrs: {
987
+ title: attrs.title || "",
988
+ border: attrs.border || "solid",
989
+ headerStyle: attrs.headerStyle || ""
990
+ },
991
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
992
+ });
993
+ continue;
994
+ }
995
+ if (directiveType === "repeat") {
996
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
997
+ i = inner.nextIdx;
998
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
999
+ i++;
1000
+ }
1001
+ content.push({
1002
+ type: "repeatBlock",
1003
+ attrs: { data: attrs.data || "" },
1004
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1005
+ });
1006
+ continue;
1007
+ }
1008
+ if (directiveType === "subtotals") {
1009
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1010
+ i = inner.nextIdx;
1011
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1012
+ i++;
1013
+ }
1014
+ content.push({
1015
+ type: "subtotalsBlock",
1016
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1017
+ });
1018
+ continue;
1019
+ }
1020
+ if (directiveType === "columns") {
1021
+ const splitVal = attrs.split || "50";
1022
+ const columns = [];
1023
+ while (i < lines.length) {
1024
+ const colLine = lines[i].trim();
1025
+ if (DIRECTIVE_CLOSE.test(colLine)) {
1026
+ if (columns.length > 0) {
1027
+ i++;
1028
+ break;
1029
+ }
1030
+ i++;
1031
+ break;
1032
+ }
1033
+ const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
1034
+ if (colMatch) {
1035
+ i++;
1036
+ const colInner = parseBlocks(lines, i, (l) => {
1037
+ const trimmed = l.trim();
1038
+ return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
1039
+ });
1040
+ i = colInner.nextIdx;
1041
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1042
+ i++;
1043
+ }
1044
+ const colAttrsRaw = colMatch[1] || "";
1045
+ const colAttrs = parseDirectiveAttrs(colAttrsRaw);
1046
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
1047
+ columns.push({
1048
+ type: "column",
1049
+ attrs: padTop ? { padTop } : void 0,
1050
+ content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
1051
+ });
1052
+ continue;
1053
+ }
1054
+ if (colLine === "") {
1055
+ i++;
1056
+ continue;
1057
+ }
1058
+ break;
1059
+ }
1060
+ const columnsAttrs = { split: splitVal };
1061
+ if (attrs.padX) columnsAttrs.padX = attrs.padX;
1062
+ content.push({
1063
+ type: "columns",
1064
+ attrs: columnsAttrs,
1065
+ content: columns.length > 0 ? columns : [
1066
+ { type: "column", content: [{ type: "paragraph" }] },
1067
+ { type: "column", content: [{ type: "paragraph" }] }
1068
+ ]
1069
+ });
1070
+ continue;
1071
+ }
1072
+ if (directiveType === "col") {
1073
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1074
+ i = inner.nextIdx;
1075
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1076
+ i++;
1077
+ }
1078
+ for (const block of inner.blocks) {
1079
+ content.push(block);
1080
+ }
1081
+ continue;
1082
+ }
1083
+ continue;
1084
+ }
1085
+ if (DIRECTIVE_CLOSE.test(line.trim())) {
1086
+ i++;
1087
+ continue;
1088
+ }
1089
+ const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
1090
+ if (imageMatch) {
1091
+ content.push({
1092
+ type: "imageNode",
1093
+ attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
1094
+ });
1095
+ i++;
1096
+ continue;
1097
+ }
1098
+ const inlineNodes = parseInline(line);
1099
+ const nonWhitespace = inlineNodes.filter(
1100
+ (n) => !(n.type === "text" && !(n.text || "").trim())
1101
+ );
1102
+ if (nonWhitespace.length === 1 && nonWhitespace[0].type === "imageNode") {
1103
+ content.push(nonWhitespace[0]);
1104
+ } else {
1105
+ content.push({ type: "paragraph", content: inlineNodes });
1106
+ }
815
1107
  i++;
816
1108
  }
817
- return { type: "doc", content };
1109
+ return { blocks: content, nextIdx: i };
1110
+ }
1111
+ function markdownToTiptap(markdown) {
1112
+ const lines = markdown.split("\n");
1113
+ const { blocks } = parseBlocks(lines, 0);
1114
+ return { type: "doc", content: blocks };
1115
+ }
1116
+
1117
+ // src/utils/variable-helpers.ts
1118
+ function extractVariablesFromContent(content) {
1119
+ const seen = /* @__PURE__ */ new Map();
1120
+ function walk(node) {
1121
+ if (node.type === "variableNode" && node.attrs) {
1122
+ const varName = node.attrs.varName;
1123
+ if (varName && !seen.has(varName)) {
1124
+ seen.set(varName, {
1125
+ varName,
1126
+ varLabel: node.attrs.varLabel || varName,
1127
+ varDefault: node.attrs.varDefault || ""
1128
+ });
1129
+ }
1130
+ }
1131
+ if (node.content) {
1132
+ node.content.forEach(walk);
1133
+ }
1134
+ }
1135
+ if (content.content) {
1136
+ content.content.forEach(walk);
1137
+ }
1138
+ return Array.from(seen.values());
1139
+ }
1140
+ function applyFormat(value, format) {
1141
+ if (format === "phone") {
1142
+ const digits = value.replace(/\D/g, "");
1143
+ if (digits.length === 10) {
1144
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
1145
+ }
1146
+ }
1147
+ return value;
1148
+ }
1149
+ function replaceVariablesInContent(content, values) {
1150
+ function walkNode(node) {
1151
+ if (node.type === "variableNode" && node.attrs) {
1152
+ const varName = node.attrs.varName;
1153
+ let replacement = values[varName] || node.attrs.varDefault || "";
1154
+ const format = node.attrs.format;
1155
+ if (format && replacement) {
1156
+ replacement = applyFormat(replacement, format);
1157
+ }
1158
+ const textNode = { type: "text", text: replacement };
1159
+ if (node.marks && node.marks.length > 0) {
1160
+ textNode.marks = node.marks;
1161
+ }
1162
+ return textNode;
1163
+ }
1164
+ if (node.type === "imageNode" && node.attrs?.varName) {
1165
+ const varName = node.attrs.varName;
1166
+ const resolved = values[varName] || node.attrs.src || "";
1167
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
1168
+ }
1169
+ if (node.content) {
1170
+ return { ...node, content: node.content.map(walkNode) };
1171
+ }
1172
+ return node;
1173
+ }
1174
+ return walkNode(content);
1175
+ }
1176
+ function expandBlockVariables(content, values) {
1177
+ function soleVariableOf(node) {
1178
+ if (node.type !== "paragraph" || !node.content) return null;
1179
+ const meaningful = node.content.filter(
1180
+ (c) => !(c.type === "text" && !(c.text || "").trim())
1181
+ );
1182
+ if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
1183
+ return meaningful[0];
1184
+ }
1185
+ return null;
1186
+ }
1187
+ function stripUnsafeNodes(node) {
1188
+ if (!node.content) return node;
1189
+ return {
1190
+ ...node,
1191
+ content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
1192
+ };
1193
+ }
1194
+ function isCellSafe(blocks) {
1195
+ return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
1196
+ }
1197
+ function walkNode(node, inCell) {
1198
+ if (!node.content) return node;
1199
+ const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
1200
+ const newContent = [];
1201
+ for (const child of node.content) {
1202
+ const varNode = soleVariableOf(child);
1203
+ if (varNode?.attrs) {
1204
+ const varName = varNode.attrs.varName;
1205
+ const value = values[varName] || varNode.attrs.varDefault || "";
1206
+ const isBlock = varNode.attrs.block === "true" || value.includes("\n");
1207
+ if (isBlock) {
1208
+ const parsed = stripUnsafeNodes(markdownToTiptap(value));
1209
+ const blocks = parsed.content || [];
1210
+ if (!childInCell || isCellSafe(blocks)) {
1211
+ newContent.push(...blocks);
1212
+ continue;
1213
+ }
1214
+ }
1215
+ }
1216
+ newContent.push(walkNode(child, childInCell));
1217
+ }
1218
+ return { ...node, content: newContent };
1219
+ }
1220
+ return walkNode(content, false);
1221
+ }
1222
+ function labelToVarName(label) {
1223
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1224
+ }
1225
+ function isZeroLike(value) {
1226
+ if (!value || value.trim() === "") return true;
1227
+ const cleaned = value.replace(/[$,\s]/g, "");
1228
+ return /^-?0+(\.0+)?$/.test(cleaned);
1229
+ }
1230
+ function suppressZeroContent(content, values) {
1231
+ function hasSuppressedVar(node) {
1232
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
1233
+ const varName = node.attrs.varName;
1234
+ const val = values[varName] || node.attrs.varDefault || "";
1235
+ return isZeroLike(val);
1236
+ }
1237
+ return (node.content || []).some(hasSuppressedVar);
1238
+ }
1239
+ function walkNode(node) {
1240
+ if (node.type === "table" && node.content) {
1241
+ const filtered = node.content.filter((row) => {
1242
+ if (row.type !== "tableRow") return true;
1243
+ const isHeader = row.content?.[0]?.type === "tableHeader";
1244
+ if (isHeader) return true;
1245
+ return !hasSuppressedVar(row);
1246
+ });
1247
+ const bodyRows = filtered.filter(
1248
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
1249
+ );
1250
+ if (bodyRows.length === 0) return null;
1251
+ return { ...node, content: filtered };
1252
+ }
1253
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
1254
+ return null;
1255
+ }
1256
+ if (node.content) {
1257
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
1258
+ return { ...node, content: filtered };
1259
+ }
1260
+ return node;
1261
+ }
1262
+ return walkNode(content) || content;
1263
+ }
1264
+ function expandRepeatContent(content, values) {
1265
+ const enrichedValues = { ...values };
1266
+ function cloneNode(node, dataKey, index) {
1267
+ if (node.type === "variableNode" && node.attrs?.varName) {
1268
+ const oldName = node.attrs.varName;
1269
+ const bareName = oldName.replace(/^operation_/, "");
1270
+ const newName = `${dataKey}_${index}_${bareName}`;
1271
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
1272
+ }
1273
+ if (node.content) {
1274
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
1275
+ }
1276
+ return { ...node };
1277
+ }
1278
+ function walkNode(node) {
1279
+ if (node.type === "repeatBlock" && node.attrs?.data) {
1280
+ const dataKey = node.attrs.data;
1281
+ const arrayJson = values[dataKey];
1282
+ if (!arrayJson) return [];
1283
+ let items;
1284
+ try {
1285
+ items = JSON.parse(arrayJson);
1286
+ } catch {
1287
+ return [];
1288
+ }
1289
+ const sums = /* @__PURE__ */ new Map();
1290
+ for (const item of items) {
1291
+ for (const [key, val] of Object.entries(item)) {
1292
+ const num = parseFloat(val);
1293
+ if (!isNaN(num)) {
1294
+ sums.set(key, (sums.get(key) || 0) + num);
1295
+ }
1296
+ }
1297
+ }
1298
+ const woKeysByNorm = /* @__PURE__ */ new Map();
1299
+ for (const k of Object.keys(enrichedValues)) {
1300
+ if (k.startsWith("workorder_")) {
1301
+ const norm = k.replace(/_/g, "");
1302
+ woKeysByNorm.set(norm, k);
1303
+ }
1304
+ }
1305
+ for (const [field, total] of sums) {
1306
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
1307
+ const directKey = `workorder_${field}`;
1308
+ enrichedValues[directKey] = formatted;
1309
+ const norm = `workorder${field}`.replace(/_/g, "");
1310
+ const existing = woKeysByNorm.get(norm);
1311
+ if (existing && existing !== directKey) {
1312
+ enrichedValues[existing] = formatted;
1313
+ }
1314
+ }
1315
+ const expanded = [];
1316
+ for (let i = 0; i < items.length; i++) {
1317
+ const item = items[i];
1318
+ for (const [key, val] of Object.entries(item)) {
1319
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
1320
+ }
1321
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
1322
+ expanded.push(...cloned);
1323
+ }
1324
+ return expanded;
1325
+ }
1326
+ if (node.content) {
1327
+ const newContent = [];
1328
+ for (const child of node.content) {
1329
+ const result2 = walkNode(child);
1330
+ if (Array.isArray(result2)) newContent.push(...result2);
1331
+ else newContent.push(result2);
1332
+ }
1333
+ return { ...node, content: newContent };
1334
+ }
1335
+ return node;
1336
+ }
1337
+ const result = walkNode(content);
1338
+ return {
1339
+ content: Array.isArray(result) ? { ...content, content: result } : result,
1340
+ values: enrichedValues
1341
+ };
818
1342
  }
819
1343
 
820
1344
  // src/utils/error-helpers.ts
@@ -952,10 +1476,40 @@ function buildMetadataObject(fields, actualFieldNames) {
952
1476
  // src/utils/pdf-generator.ts
953
1477
  var PAGE_WIDTH = 595.28;
954
1478
  var PAGE_HEIGHT = 841.89;
955
- var MARGIN = 72;
1479
+ var MARGIN = 40;
956
1480
  var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
957
1481
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
958
1482
  var LINE_HEIGHT_FACTOR = 1.4;
1483
+ var BODY_FONT_SIZE = 10;
1484
+ function hexToRgbColor(hex) {
1485
+ if (!hex) return null;
1486
+ const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
1487
+ if (!m) return null;
1488
+ const n = parseInt(m[1], 16);
1489
+ return pdfLib.rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
1490
+ }
1491
+ function contrastTextColor(fill) {
1492
+ const c = fill;
1493
+ const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
1494
+ return luminance > 0.6 ? pdfLib.rgb(0, 0, 0) : pdfLib.rgb(1, 1, 1);
1495
+ }
1496
+ function resolveTheme(theme) {
1497
+ const primary = hexToRgbColor(theme?.primaryColor);
1498
+ const accent = hexToRgbColor(theme?.accentColor);
1499
+ const border = hexToRgbColor(theme?.borderColor);
1500
+ return {
1501
+ tableHeaderBg: primary ?? pdfLib.rgb(0.3, 0.3, 0.3),
1502
+ tableHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
1503
+ tableBorder: border ?? pdfLib.rgb(0.75, 0.75, 0.75),
1504
+ panelDarkHeaderBg: primary ?? pdfLib.rgb(0.25, 0.25, 0.25),
1505
+ panelDarkHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
1506
+ panelBorder: border ?? pdfLib.rgb(0.6, 0.6, 0.6),
1507
+ rule: primary ?? pdfLib.rgb(0, 0, 0),
1508
+ pageHeaderText: primary ?? pdfLib.rgb(0, 0, 0),
1509
+ pageHeaderRule: accent ?? primary ?? pdfLib.rgb(0.75, 0.75, 0.75)
1510
+ };
1511
+ }
1512
+ var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
959
1513
  var FIELD_DISPLAY = {
960
1514
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
961
1515
  ["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
@@ -986,17 +1540,65 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
986
1540
  const height = textLineHeight + 4;
987
1541
  return { width, height };
988
1542
  }
1543
+ async function loadImageBytes(src) {
1544
+ if (src.startsWith("data:")) {
1545
+ const comma = src.indexOf(",");
1546
+ if (comma === -1) throw new Error("malformed data URL");
1547
+ const meta = src.slice(0, comma);
1548
+ if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
1549
+ const bin = atob(src.slice(comma + 1));
1550
+ const bytes = new Uint8Array(bin.length);
1551
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1552
+ return bytes;
1553
+ }
1554
+ const res = await fetch(src);
1555
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1556
+ return new Uint8Array(await res.arrayBuffer());
1557
+ }
1558
+ function describeImageSrc(src) {
1559
+ return src.length > 80 ? `${src.slice(0, 77)}...` : src;
1560
+ }
1561
+ async function preloadImages(pdfDoc, content) {
1562
+ const srcs = /* @__PURE__ */ new Set();
1563
+ (function walk(node) {
1564
+ if (node.type === "imageNode") {
1565
+ const src = node.attrs?.src || "";
1566
+ if (src) srcs.add(src);
1567
+ }
1568
+ for (const child of node.content || []) walk(child);
1569
+ })(content);
1570
+ const images = /* @__PURE__ */ new Map();
1571
+ const warnings = [];
1572
+ for (const src of srcs) {
1573
+ try {
1574
+ const bytes = await loadImageBytes(src);
1575
+ const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
1576
+ const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
1577
+ if (isPng) {
1578
+ images.set(src, await pdfDoc.embedPng(bytes));
1579
+ } else if (isJpeg) {
1580
+ images.set(src, await pdfDoc.embedJpg(bytes));
1581
+ } else {
1582
+ warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
1583
+ }
1584
+ } catch (err) {
1585
+ warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
1586
+ }
1587
+ }
1588
+ return { images, warnings };
1589
+ }
989
1590
  function ensureSpace(state, neededHeight) {
990
1591
  if (state.y + neededHeight > CONTENT_HEIGHT) {
991
1592
  const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
992
1593
  state.currentPage = newPage;
993
1594
  state.pageIndex++;
994
1595
  state.y = 0;
1596
+ renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
995
1597
  }
996
1598
  }
997
- function drawText(state, text, font, fontSize, indent = 0) {
1599
+ function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
998
1600
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
999
- const maxWidth = CONTENT_WIDTH - indent;
1601
+ const maxWidth = region.width - indent;
1000
1602
  const words = text.split(/\s+/);
1001
1603
  let line = "";
1002
1604
  let totalAdvance = 0;
@@ -1007,7 +1609,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
1007
1609
  ensureSpace(state, lineHeight);
1008
1610
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1009
1611
  state.currentPage.drawText(line, {
1010
- x: MARGIN + indent,
1612
+ x: region.leftX + indent,
1011
1613
  y: pdfY,
1012
1614
  size: fontSize,
1013
1615
  font,
@@ -1024,7 +1626,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
1024
1626
  ensureSpace(state, lineHeight);
1025
1627
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1026
1628
  state.currentPage.drawText(line, {
1027
- x: MARGIN + indent,
1629
+ x: region.leftX + indent,
1028
1630
  y: pdfY,
1029
1631
  size: fontSize,
1030
1632
  font,
@@ -1068,10 +1670,10 @@ function collectInlineSegments(content, fonts, fontSize) {
1068
1670
  }
1069
1671
  return segments;
1070
1672
  }
1071
- function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1673
+ function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
1072
1674
  const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
1073
- const maxX = MARGIN + CONTENT_WIDTH;
1074
- const startX = MARGIN + indent;
1675
+ const maxX = region.leftX + region.width;
1676
+ const startX = region.leftX + indent;
1075
1677
  let currentX = startX;
1076
1678
  let currentLineHeight = 0;
1077
1679
  let lineHasContent = false;
@@ -1176,90 +1778,395 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1176
1778
  }
1177
1779
  flushLine();
1178
1780
  }
1179
- function processInlineContent(state, content, fontSize, indent = 0) {
1781
+ function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
1180
1782
  if (!content) return;
1181
1783
  const segments = collectInlineSegments(content, state.fonts, fontSize);
1182
1784
  if (segments.length === 0) return;
1183
- layoutInlineSegments(state, segments, fontSize, indent);
1785
+ layoutInlineSegments(state, segments, fontSize, indent, region);
1184
1786
  }
1185
- function processBlock(state, node) {
1186
- switch (node.type) {
1187
- case "heading": {
1188
- const level = node.attrs?.level || 1;
1189
- const fontSizes = { 1: 24, 2: 20, 3: 16 };
1190
- const fontSize = fontSizes[level] || 16;
1191
- const spacing = fontSize * 0.6;
1192
- ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR + spacing);
1193
- state.y += spacing / 2;
1194
- if (!node.content) break;
1195
- const headingFonts = {
1196
- regular: state.fonts.bold,
1197
- bold: state.fonts.bold,
1198
- italic: state.fonts.boldItalic,
1199
- boldItalic: state.fonts.boldItalic
1200
- };
1201
- const segments = collectInlineSegments(node.content, headingFonts, fontSize);
1202
- layoutInlineSegments(state, segments, fontSize);
1203
- state.y += spacing / 2;
1204
- break;
1205
- }
1206
- case "paragraph": {
1207
- const fontSize = 12;
1208
- if (!node.content || node.content.length === 0) {
1209
- state.y += fontSize * LINE_HEIGHT_FACTOR;
1210
- break;
1787
+ function extractCellText(cell) {
1788
+ const parts = [];
1789
+ for (const child of cell.content || []) {
1790
+ if (child.type === "paragraph" || child.type === "heading") {
1791
+ for (const inline of child.content || []) {
1792
+ if (inline.type === "text") {
1793
+ parts.push(inline.text || "");
1794
+ } else if (inline.type === "variableNode") {
1795
+ const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
1796
+ parts.push(`[${label}]`);
1797
+ } else if (inline.type === "fieldNode") {
1798
+ const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
1799
+ parts.push(`[ ${label} ]`);
1800
+ }
1211
1801
  }
1212
- processInlineContent(state, node.content, fontSize);
1213
- state.y += 4;
1214
- break;
1215
1802
  }
1216
- case "bulletList": {
1217
- for (const item of node.content || []) {
1218
- for (const child of item.content || []) {
1219
- const fontSize = 12;
1220
- ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1221
- const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1222
- state.currentPage.drawText("\u2022", {
1223
- x: MARGIN + 8,
1224
- y: bulletPdfY,
1225
- size: fontSize,
1226
- font: state.fonts.regular,
1227
- color: pdfLib.rgb(0, 0, 0)
1228
- });
1229
- processInlineContent(state, child.content, fontSize, 24);
1803
+ }
1804
+ return parts.join("");
1805
+ }
1806
+ function isNumericText(text) {
1807
+ return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
1808
+ }
1809
+ function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
1810
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1811
+ const font = isHeader ? fonts.bold : fonts.regular;
1812
+ let totalHeight = 0;
1813
+ for (const child of cell.content || []) {
1814
+ if (child.type === "paragraph" || child.type === "heading") {
1815
+ const text = extractCellText({ content: [child] });
1816
+ if (!text) {
1817
+ totalHeight += lineHeight;
1818
+ continue;
1819
+ }
1820
+ const words = text.split(/\s+/);
1821
+ let line = "";
1822
+ let lineCount = 0;
1823
+ for (const word of words) {
1824
+ const testLine = line ? `${line} ${word}` : word;
1825
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
1826
+ if (testWidth > maxWidth && line) {
1827
+ lineCount++;
1828
+ line = word;
1829
+ } else {
1830
+ line = testLine;
1230
1831
  }
1231
1832
  }
1232
- state.y += 4;
1233
- break;
1833
+ if (line) lineCount++;
1834
+ totalHeight += lineCount * lineHeight;
1234
1835
  }
1235
- case "orderedList": {
1236
- let num = 1;
1237
- for (const item of node.content || []) {
1238
- for (const child of item.content || []) {
1239
- const fontSize = 12;
1240
- ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1241
- const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1242
- state.currentPage.drawText(`${num}.`, {
1243
- x: MARGIN + 4,
1244
- y: numPdfY,
1836
+ }
1837
+ return Math.max(totalHeight, lineHeight);
1838
+ }
1839
+ function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = pdfLib.rgb(0, 0, 0)) {
1840
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1841
+ const cellPadding = 4;
1842
+ const maxTextWidth = cellWidth - cellPadding * 2;
1843
+ let textX = cellX + cellPadding;
1844
+ let textY = cellY;
1845
+ for (const child of cell.content || []) {
1846
+ if (child.type === "paragraph" || child.type === "heading") {
1847
+ const segments = collectInlineSegments(
1848
+ child.content || [],
1849
+ isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
1850
+ fontSize
1851
+ );
1852
+ if (rightAlign && segments.length > 0) {
1853
+ const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
1854
+ const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
1855
+ if (textWidth <= maxTextWidth) {
1856
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1857
+ state.currentPage.drawText(fullText, {
1858
+ x: cellX + cellWidth - cellPadding - textWidth,
1859
+ y: pdfY,
1245
1860
  size: fontSize,
1246
- font: state.fonts.regular,
1247
- color: pdfLib.rgb(0, 0, 0)
1861
+ font: isHeader ? fonts.bold : fonts.regular,
1862
+ color: textColor
1248
1863
  });
1249
- processInlineContent(state, child.content, fontSize, 24);
1864
+ return;
1250
1865
  }
1251
- num++;
1252
1866
  }
1253
- state.y += 4;
1254
- break;
1255
- }
1867
+ let currentX = textX;
1868
+ for (const seg of segments) {
1869
+ if (seg.kind === "break") {
1870
+ textY += lineHeight;
1871
+ currentX = textX;
1872
+ continue;
1873
+ }
1874
+ if (seg.kind === "text") {
1875
+ const words = seg.text.split(/(\s+)/);
1876
+ const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
1877
+ for (const token of words) {
1878
+ if (!token) continue;
1879
+ if (/^\s+$/.test(token)) {
1880
+ currentX += spaceWidth;
1881
+ continue;
1882
+ }
1883
+ const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
1884
+ if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
1885
+ textY += lineHeight;
1886
+ currentX = textX;
1887
+ }
1888
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1889
+ state.currentPage.drawText(token, {
1890
+ x: currentX,
1891
+ y: pdfY,
1892
+ size: fontSize,
1893
+ font: seg.font,
1894
+ color: textColor
1895
+ });
1896
+ currentX += wordWidth;
1897
+ }
1898
+ } else if (seg.kind === "field") {
1899
+ const pdfY = PAGE_HEIGHT - textY - seg.height;
1900
+ if (state.drawFieldPlaceholders) {
1901
+ state.currentPage.drawRectangle({
1902
+ x: currentX,
1903
+ y: pdfY,
1904
+ width: seg.width,
1905
+ height: seg.height,
1906
+ borderColor: pdfLib.rgb(0.5, 0.5, 0.7),
1907
+ borderWidth: 0.5,
1908
+ color: pdfLib.rgb(0.95, 0.95, 1),
1909
+ borderDashArray: [4, 2]
1910
+ });
1911
+ }
1912
+ const fieldId = seg.attrs.fieldId;
1913
+ if (fieldId) {
1914
+ state.fieldPositions.set(fieldId, {
1915
+ x: currentX,
1916
+ y: pdfY,
1917
+ width: seg.width,
1918
+ height: seg.height,
1919
+ page: state.pageIndex + 1
1920
+ });
1921
+ }
1922
+ currentX += seg.width + 4;
1923
+ }
1924
+ }
1925
+ }
1926
+ }
1927
+ }
1928
+ function renderTable(state, node, region = DEFAULT_REGION) {
1929
+ const tableRows = node.content || [];
1930
+ if (tableRows.length === 0) return;
1931
+ const borderless = node.attrs?.borderless === true;
1932
+ const subtotals = node.attrs?.subtotals === true;
1933
+ const fontSize = 10;
1934
+ const cellPadding = borderless ? 2 : 4;
1935
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1936
+ const rowPadding = cellPadding * 2;
1937
+ const borderColor = state.theme.tableBorder;
1938
+ const headerBg = state.theme.tableHeaderBg;
1939
+ const headerTextColor = state.theme.tableHeaderText;
1940
+ let colCount = 0;
1941
+ for (const row of tableRows) {
1942
+ colCount = Math.max(colCount, row.content?.length || 0);
1943
+ }
1944
+ if (colCount === 0) return;
1945
+ const colMaxWidths = new Array(colCount).fill(0);
1946
+ for (const row of tableRows) {
1947
+ const cells = row.content || [];
1948
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1949
+ const cell = cells[c];
1950
+ const isHeader = cell.type === "tableHeader";
1951
+ const text = extractCellText(cell);
1952
+ const font = isHeader ? state.fonts.bold : state.fonts.regular;
1953
+ const textWidth = font.widthOfTextAtSize(text, fontSize);
1954
+ colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
1955
+ }
1956
+ }
1957
+ const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
1958
+ const minColWidth = 40;
1959
+ let colWidths;
1960
+ if (totalMeasured <= region.width) {
1961
+ const scale = region.width / totalMeasured;
1962
+ colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
1963
+ } else {
1964
+ colWidths = colMaxWidths.map(
1965
+ (w) => Math.max(w / totalMeasured * region.width, minColWidth)
1966
+ );
1967
+ }
1968
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
1969
+ if (colSum > 0) {
1970
+ const normFactor = region.width / colSum;
1971
+ colWidths = colWidths.map((w) => w * normFactor);
1972
+ }
1973
+ const headerRow = tableRows[0];
1974
+ const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
1975
+ function renderRow(row, isHeaderRow) {
1976
+ const cells = row.content || [];
1977
+ const isHeader = isHeaderRow && hasHeader;
1978
+ let rowHeight = lineHeight;
1979
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1980
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
1981
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
1982
+ rowHeight = Math.max(rowHeight, cellH);
1983
+ }
1984
+ rowHeight += rowPadding;
1985
+ ensureSpace(state, rowHeight);
1986
+ const rowTopY = state.y;
1987
+ let cellX = region.leftX;
1988
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1989
+ const cellW = colWidths[c];
1990
+ const cell = cells[c];
1991
+ const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
1992
+ const pdfCellBottom = pdfCellTop - rowHeight;
1993
+ if (isHeader && !borderless) {
1994
+ state.currentPage.drawRectangle({
1995
+ x: cellX,
1996
+ y: pdfCellBottom,
1997
+ width: cellW,
1998
+ height: rowHeight,
1999
+ color: headerBg
2000
+ });
2001
+ }
2002
+ if (!borderless) {
2003
+ state.currentPage.drawRectangle({
2004
+ x: cellX,
2005
+ y: pdfCellBottom,
2006
+ width: cellW,
2007
+ height: rowHeight,
2008
+ borderColor,
2009
+ borderWidth: 0.5
2010
+ });
2011
+ }
2012
+ const cellText = extractCellText(cell);
2013
+ const rightAlign = !isHeader && isNumericText(cellText);
2014
+ const contentY = MARGIN + rowTopY + cellPadding;
2015
+ const effectiveHeader = isHeader && !borderless;
2016
+ renderCellContent(
2017
+ state,
2018
+ cell,
2019
+ cellX,
2020
+ contentY,
2021
+ cellW,
2022
+ state.fonts,
2023
+ fontSize,
2024
+ effectiveHeader,
2025
+ rightAlign,
2026
+ effectiveHeader ? headerTextColor : pdfLib.rgb(0, 0, 0)
2027
+ );
2028
+ cellX += cellW;
2029
+ }
2030
+ state.y += rowHeight;
2031
+ }
2032
+ if (hasHeader && tableRows.length > 0) {
2033
+ renderRow(tableRows[0], true);
2034
+ }
2035
+ let lastPageIndex = state.pageIndex;
2036
+ const startIdx = hasHeader ? 1 : 0;
2037
+ const bodyRowCount = tableRows.length - startIdx;
2038
+ for (let r = startIdx; r < tableRows.length; r++) {
2039
+ const cells = tableRows[r].content || [];
2040
+ let estRowHeight = lineHeight;
2041
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2042
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
2043
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
2044
+ estRowHeight = Math.max(estRowHeight, cellH);
2045
+ }
2046
+ estRowHeight += rowPadding;
2047
+ if (state.y + estRowHeight > CONTENT_HEIGHT) {
2048
+ ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
2049
+ if (state.pageIndex !== lastPageIndex && hasHeader) {
2050
+ renderRow(headerRow, true);
2051
+ lastPageIndex = state.pageIndex;
2052
+ }
2053
+ }
2054
+ const rowTopYBeforeRender = state.y;
2055
+ renderRow(tableRows[r], false);
2056
+ lastPageIndex = state.pageIndex;
2057
+ if (subtotals && colCount >= 2) {
2058
+ const bodyIdx = r - startIdx;
2059
+ const isFirstBody = bodyIdx === 0;
2060
+ const isLastBody = bodyIdx === bodyRowCount - 1;
2061
+ if (isFirstBody || isLastBody) {
2062
+ const valueStartX = region.leftX + colWidths[0];
2063
+ const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
2064
+ const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
2065
+ const overlineColor = pdfLib.rgb(0, 0, 0);
2066
+ if (isFirstBody) {
2067
+ state.currentPage.drawLine({
2068
+ start: { x: valueStartX, y: pdfRowTopY },
2069
+ end: { x: valueEndX, y: pdfRowTopY },
2070
+ thickness: 0.5,
2071
+ color: overlineColor
2072
+ });
2073
+ } else if (isLastBody) {
2074
+ state.currentPage.drawLine({
2075
+ start: { x: valueStartX, y: pdfRowTopY + 2 },
2076
+ end: { x: valueEndX, y: pdfRowTopY + 2 },
2077
+ thickness: 0.5,
2078
+ color: overlineColor
2079
+ });
2080
+ state.currentPage.drawLine({
2081
+ start: { x: valueStartX, y: pdfRowTopY },
2082
+ end: { x: valueEndX, y: pdfRowTopY },
2083
+ thickness: 0.5,
2084
+ color: overlineColor
2085
+ });
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ state.y += 3;
2091
+ }
2092
+ function processBlock(state, node, region = DEFAULT_REGION) {
2093
+ switch (node.type) {
2094
+ case "heading": {
2095
+ const level = node.attrs?.level || 1;
2096
+ const fontSizes = { 1: 24, 2: 20, 3: 16 };
2097
+ const fontSize = fontSizes[level] || 16;
2098
+ const spacing = fontSize * 0.6;
2099
+ ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR + spacing);
2100
+ state.y += spacing / 2;
2101
+ if (!node.content) break;
2102
+ const headingFonts = {
2103
+ regular: state.fonts.bold,
2104
+ bold: state.fonts.bold,
2105
+ italic: state.fonts.boldItalic,
2106
+ boldItalic: state.fonts.boldItalic
2107
+ };
2108
+ const segments = collectInlineSegments(node.content, headingFonts, fontSize);
2109
+ layoutInlineSegments(state, segments, fontSize, 0, region);
2110
+ state.y += spacing / 2;
2111
+ break;
2112
+ }
2113
+ case "paragraph": {
2114
+ const fontSize = BODY_FONT_SIZE;
2115
+ if (!node.content || node.content.length === 0) {
2116
+ state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
2117
+ break;
2118
+ }
2119
+ processInlineContent(state, node.content, fontSize, 0, region);
2120
+ state.y += 2;
2121
+ break;
2122
+ }
2123
+ case "bulletList": {
2124
+ for (const item of node.content || []) {
2125
+ for (const child of item.content || []) {
2126
+ const fontSize = BODY_FONT_SIZE;
2127
+ ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
2128
+ const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
2129
+ state.currentPage.drawText("\u2022", {
2130
+ x: region.leftX + 8,
2131
+ y: bulletPdfY,
2132
+ size: fontSize,
2133
+ font: state.fonts.regular,
2134
+ color: pdfLib.rgb(0, 0, 0)
2135
+ });
2136
+ processInlineContent(state, child.content, fontSize, 24, region);
2137
+ }
2138
+ }
2139
+ state.y += 4;
2140
+ break;
2141
+ }
2142
+ case "orderedList": {
2143
+ let num = 1;
2144
+ for (const item of node.content || []) {
2145
+ for (const child of item.content || []) {
2146
+ const fontSize = BODY_FONT_SIZE;
2147
+ ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
2148
+ const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
2149
+ state.currentPage.drawText(`${num}.`, {
2150
+ x: region.leftX + 4,
2151
+ y: numPdfY,
2152
+ size: fontSize,
2153
+ font: state.fonts.regular,
2154
+ color: pdfLib.rgb(0, 0, 0)
2155
+ });
2156
+ processInlineContent(state, child.content, fontSize, 24, region);
2157
+ }
2158
+ num++;
2159
+ }
2160
+ state.y += 4;
2161
+ break;
2162
+ }
1256
2163
  case "blockquote": {
1257
2164
  const startY = state.y;
1258
2165
  for (const child of node.content || []) {
1259
- processInlineContent(state, child.content, 12, 16);
2166
+ processInlineContent(state, child.content, 12, 16, region);
1260
2167
  }
1261
2168
  const endY = state.y;
1262
- const barX = MARGIN + 4;
2169
+ const barX = region.leftX + 4;
1263
2170
  const barTop = PAGE_HEIGHT - MARGIN - startY;
1264
2171
  const barBottom = PAGE_HEIGHT - MARGIN - endY;
1265
2172
  state.currentPage.drawLine({
@@ -1272,16 +2179,16 @@ function processBlock(state, node) {
1272
2179
  break;
1273
2180
  }
1274
2181
  case "horizontalRule": {
1275
- ensureSpace(state, 16);
1276
- state.y += 8;
2182
+ ensureSpace(state, 10);
2183
+ state.y += 4;
1277
2184
  const ruleY = PAGE_HEIGHT - MARGIN - state.y;
1278
2185
  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)
2186
+ start: { x: region.leftX, y: ruleY },
2187
+ end: { x: region.leftX + region.width, y: ruleY },
2188
+ thickness: 2.5,
2189
+ color: state.theme.rule
1283
2190
  });
1284
- state.y += 8;
2191
+ state.y += 4;
1285
2192
  break;
1286
2193
  }
1287
2194
  case "codeBlock": {
@@ -1293,9 +2200,9 @@ function processBlock(state, node) {
1293
2200
  ensureSpace(state, blockHeight);
1294
2201
  const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
1295
2202
  state.currentPage.drawRectangle({
1296
- x: MARGIN,
2203
+ x: region.leftX,
1297
2204
  y: boxY,
1298
- width: CONTENT_WIDTH,
2205
+ width: region.width,
1299
2206
  height: blockHeight,
1300
2207
  color: pdfLib.rgb(0.95, 0.95, 0.95),
1301
2208
  borderColor: pdfLib.rgb(0.85, 0.85, 0.85),
@@ -1303,18 +2210,325 @@ function processBlock(state, node) {
1303
2210
  });
1304
2211
  state.y += 8;
1305
2212
  for (const line of lines) {
1306
- drawText(state, line || " ", state.fonts.regular, fontSize, 8);
2213
+ drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
1307
2214
  }
1308
2215
  state.y += 8;
1309
2216
  break;
1310
2217
  }
2218
+ case "table": {
2219
+ renderTable(state, node, region);
2220
+ break;
2221
+ }
2222
+ case "imageNode": {
2223
+ const src = node.attrs?.src || "";
2224
+ const image = src ? state.images.get(src) : void 0;
2225
+ if (!image) break;
2226
+ const natW = image.width;
2227
+ const natH = image.height;
2228
+ let w = parseFloat(node.attrs?.width || "") || 0;
2229
+ let h = parseFloat(node.attrs?.height || "") || 0;
2230
+ if (w && !h) h = natH / natW * w;
2231
+ else if (h && !w) w = natW / natH * h;
2232
+ else if (!w && !h) {
2233
+ w = natW;
2234
+ h = natH;
2235
+ }
2236
+ if (w > region.width) {
2237
+ h = h * (region.width / w);
2238
+ w = region.width;
2239
+ }
2240
+ const maxH = CONTENT_HEIGHT * 0.6;
2241
+ if (h > maxH) {
2242
+ w = w * (maxH / h);
2243
+ h = maxH;
2244
+ }
2245
+ ensureSpace(state, h + 4);
2246
+ const align = node.attrs?.align || "left";
2247
+ let x = region.leftX;
2248
+ if (align === "center") x = region.leftX + (region.width - w) / 2;
2249
+ else if (align === "right") x = region.leftX + region.width - w;
2250
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
2251
+ state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
2252
+ state.y += h + 4;
2253
+ break;
2254
+ }
2255
+ // ─── Layout directives ─────────────────────────────────────────
2256
+ case "panel": {
2257
+ const title = node.attrs?.title || "";
2258
+ const border = node.attrs?.border || "solid";
2259
+ const headerStyle = node.attrs?.headerStyle || "";
2260
+ const padding = 8;
2261
+ const titleFontSize = 10;
2262
+ const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
2263
+ const panelStartY = state.y;
2264
+ const panelStartPage = state.pageIndex;
2265
+ if (title) {
2266
+ state.y += titleHeight;
2267
+ }
2268
+ const isHeaderOnly = border === "none";
2269
+ if (!isHeaderOnly) {
2270
+ state.y += padding;
2271
+ }
2272
+ const innerRegion = {
2273
+ leftX: region.leftX + padding,
2274
+ width: region.width - padding * 2
2275
+ };
2276
+ if (!isHeaderOnly) {
2277
+ for (const child of node.content || []) {
2278
+ processBlock(state, child, innerRegion);
2279
+ }
2280
+ state.y += padding;
2281
+ }
2282
+ const startPage = state.pdfDoc.getPages()[panelStartPage];
2283
+ if (title) {
2284
+ const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
2285
+ const isDarkHeader = headerStyle === "dark";
2286
+ startPage.drawRectangle({
2287
+ x: region.leftX,
2288
+ y: titleBarY,
2289
+ width: region.width,
2290
+ height: titleHeight,
2291
+ color: isDarkHeader ? state.theme.panelDarkHeaderBg : pdfLib.rgb(0.93, 0.93, 0.93),
2292
+ borderColor: state.theme.panelBorder,
2293
+ borderWidth: 0.75
2294
+ });
2295
+ startPage.drawText(title, {
2296
+ x: region.leftX + padding,
2297
+ y: titleBarY + 4,
2298
+ size: titleFontSize,
2299
+ font: state.fonts.bold,
2300
+ color: isDarkHeader ? state.theme.panelDarkHeaderText : pdfLib.rgb(0, 0, 0)
2301
+ });
2302
+ }
2303
+ if (border !== "none") {
2304
+ const borderDashArray = border === "dashed" ? [4, 2] : void 0;
2305
+ if (panelStartPage === state.pageIndex) {
2306
+ const borderY = PAGE_HEIGHT - MARGIN - state.y;
2307
+ const panelHeight = state.y - panelStartY;
2308
+ startPage.drawRectangle({
2309
+ x: region.leftX,
2310
+ y: borderY,
2311
+ width: region.width,
2312
+ height: panelHeight,
2313
+ borderColor: state.theme.panelBorder,
2314
+ borderWidth: 0.75,
2315
+ borderDashArray
2316
+ });
2317
+ } else {
2318
+ const startBorderBottom = MARGIN;
2319
+ const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
2320
+ startPage.drawRectangle({
2321
+ x: region.leftX,
2322
+ y: startBorderBottom,
2323
+ width: region.width,
2324
+ height: startPanelHeight,
2325
+ borderColor: state.theme.panelBorder,
2326
+ borderWidth: 0.75,
2327
+ borderDashArray
2328
+ });
2329
+ const contTop = PAGE_HEIGHT - MARGIN;
2330
+ const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
2331
+ const contHeight = contTop - contBorderY;
2332
+ state.currentPage.drawRectangle({
2333
+ x: region.leftX,
2334
+ y: contBorderY,
2335
+ width: region.width,
2336
+ height: contHeight,
2337
+ borderColor: state.theme.panelBorder,
2338
+ borderWidth: 0.75,
2339
+ borderDashArray
2340
+ });
2341
+ }
2342
+ }
2343
+ state.y += 4;
2344
+ break;
2345
+ }
2346
+ case "columns": {
2347
+ const split = parseInt(node.attrs?.split || "50", 10);
2348
+ const padX = parseFloat(node.attrs?.padX || "0") || 0;
2349
+ const columns = (node.content || []).filter((c) => c.type === "column");
2350
+ if (columns.length === 0) break;
2351
+ const colRegionLeftX = region.leftX + padX;
2352
+ const colRegionWidth = region.width - 2 * padX;
2353
+ const gap = 12;
2354
+ const columnsStartY = state.y;
2355
+ const columnsStartPage = state.pageIndex;
2356
+ let colWidths;
2357
+ if (columns.length === 2) {
2358
+ const firstWidth = (colRegionWidth - gap) * (split / 100);
2359
+ const secondWidth = colRegionWidth - gap - firstWidth;
2360
+ colWidths = [firstWidth, secondWidth];
2361
+ } else {
2362
+ const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
2363
+ colWidths = columns.map(() => eachWidth);
2364
+ }
2365
+ let maxEndY = columnsStartY;
2366
+ let colX = colRegionLeftX;
2367
+ for (let ci = 0; ci < columns.length; ci++) {
2368
+ const col = columns[ci];
2369
+ const colWidth = colWidths[ci];
2370
+ const colRegion = { leftX: colX, width: colWidth };
2371
+ state.y = columnsStartY;
2372
+ state.pageIndex = columnsStartPage;
2373
+ state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
2374
+ const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
2375
+ if (padTop > 0) {
2376
+ state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
2377
+ }
2378
+ for (const child of col.content || []) {
2379
+ processBlock(state, child, colRegion);
2380
+ }
2381
+ maxEndY = Math.max(maxEndY, state.y);
2382
+ colX += colWidth + gap;
2383
+ }
2384
+ state.y = maxEndY;
2385
+ state.y += 4;
2386
+ break;
2387
+ }
2388
+ case "column": {
2389
+ for (const child of node.content || []) {
2390
+ processBlock(state, child, region);
2391
+ }
2392
+ break;
2393
+ }
2394
+ case "watermark": {
2395
+ break;
2396
+ }
2397
+ case "subtotalsBlock": {
2398
+ const stFontSize = BODY_FONT_SIZE;
2399
+ const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
2400
+ const valueRightX = region.leftX + region.width;
2401
+ const rows = (node.content || []).filter(
2402
+ (c) => c.type === "paragraph" && c.content?.length
2403
+ );
2404
+ const parsed = [];
2405
+ for (const row of rows) {
2406
+ let label = "";
2407
+ let value = "";
2408
+ for (const seg of row.content || []) {
2409
+ if (seg.type === "text") {
2410
+ const isBold = seg.marks?.some((m) => m.type === "bold");
2411
+ if (isBold) {
2412
+ label += seg.text || "";
2413
+ } else {
2414
+ const t = (seg.text || "").trim();
2415
+ if (t) value += (value ? " " : "") + t;
2416
+ }
2417
+ } else if (seg.type === "variableNode") {
2418
+ const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
2419
+ const isBoldVar = seg.marks?.some((m) => m.type === "bold");
2420
+ if (isBoldVar) {
2421
+ label += varLabel;
2422
+ } else {
2423
+ value += (value ? " " : "") + `[${varLabel}]`;
2424
+ }
2425
+ }
2426
+ }
2427
+ parsed.push({ label: label.trim(), value: value.trim() });
2428
+ }
2429
+ let maxLabelWidth = 0;
2430
+ for (const { label } of parsed) {
2431
+ if (label) {
2432
+ const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
2433
+ if (w > maxLabelWidth) maxLabelWidth = w;
2434
+ }
2435
+ }
2436
+ const gap = 12;
2437
+ const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
2438
+ const valueColStartX = labelX + maxLabelWidth + gap;
2439
+ const isLastRowTotal = parsed.length > 1;
2440
+ for (let ri = 0; ri < parsed.length; ri++) {
2441
+ const isTotal = isLastRowTotal && ri === parsed.length - 1;
2442
+ if (isTotal) {
2443
+ state.y += stLineHeight * 0.6;
2444
+ }
2445
+ ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
2446
+ const { label, value } = parsed[ri];
2447
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
2448
+ if (isTotal) {
2449
+ state.currentPage.drawLine({
2450
+ start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
2451
+ end: { x: valueRightX, y: pdfY + stFontSize + 6 },
2452
+ thickness: 0.75,
2453
+ color: pdfLib.rgb(0, 0, 0)
2454
+ });
2455
+ state.currentPage.drawLine({
2456
+ start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
2457
+ end: { x: valueRightX, y: pdfY + stFontSize + 3 },
2458
+ thickness: 0.75,
2459
+ color: pdfLib.rgb(0, 0, 0)
2460
+ });
2461
+ }
2462
+ if (label) {
2463
+ state.currentPage.drawText(label, {
2464
+ x: labelX,
2465
+ y: pdfY,
2466
+ size: stFontSize,
2467
+ font: state.fonts.bold,
2468
+ color: pdfLib.rgb(0, 0, 0)
2469
+ });
2470
+ }
2471
+ if (value) {
2472
+ const valueFont = state.fonts.bold;
2473
+ const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
2474
+ state.currentPage.drawText(value, {
2475
+ x: valueRightX - valueWidth,
2476
+ y: pdfY,
2477
+ size: stFontSize,
2478
+ font: valueFont,
2479
+ color: pdfLib.rgb(0, 0, 0)
2480
+ });
2481
+ }
2482
+ state.y += stLineHeight;
2483
+ }
2484
+ state.y += 4;
2485
+ break;
2486
+ }
2487
+ case "repeatBlock":
2488
+ if (node.content) {
2489
+ for (const child of node.content) {
2490
+ processBlock(state, child, region);
2491
+ }
2492
+ }
2493
+ break;
1311
2494
  default:
1312
2495
  if (node.content) {
1313
- processInlineContent(state, node.content, 12);
2496
+ processInlineContent(state, node.content, 12, 0, region);
1314
2497
  }
1315
2498
  break;
1316
2499
  }
1317
2500
  }
2501
+ function renderWatermarksOnPage(page, fonts, watermarkNodes) {
2502
+ for (const wmNode of watermarkNodes) {
2503
+ const text = wmNode.attrs?.text || "";
2504
+ if (!text) continue;
2505
+ const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
2506
+ const angle = parseFloat(wmNode.attrs?.angle || "-45");
2507
+ const fontSize = 100;
2508
+ const { width, height } = page.getSize();
2509
+ const centerX = width / 2;
2510
+ const centerY = height / 2;
2511
+ const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
2512
+ page.drawText(text, {
2513
+ x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
2514
+ y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
2515
+ size: fontSize,
2516
+ font: fonts.bold,
2517
+ color: pdfLib.rgb(0.5, 0.5, 0.5),
2518
+ opacity,
2519
+ rotate: pdfLib.degrees(angle)
2520
+ });
2521
+ }
2522
+ }
2523
+ function collectWatermarkNodes(content) {
2524
+ const nodes = [];
2525
+ for (const block of content.content || []) {
2526
+ if (block.type === "watermark") {
2527
+ nodes.push(block);
2528
+ }
2529
+ }
2530
+ return nodes;
2531
+ }
1318
2532
  function ensureFieldSuffix(fieldName, suffix) {
1319
2533
  const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
1320
2534
  return `${cleanName}${suffix}`;
@@ -1424,6 +2638,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1424
2638
  pdfLib.PDFString.of(field.label)
1425
2639
  );
1426
2640
  }
2641
+ const initialsFontSize = Math.min(
2642
+ 72,
2643
+ Math.max(8, Math.round(position.height * 0.55))
2644
+ );
2645
+ try {
2646
+ initialsField.setFontSize(initialsFontSize);
2647
+ const acro = initialsField;
2648
+ const daRef = acro.acroField.dict.get(pdfLib.PDFName.of("DA"));
2649
+ if (daRef) {
2650
+ const daStr = daRef.value;
2651
+ if (typeof daStr === "string") {
2652
+ const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
2653
+ acro.acroField.dict.set(pdfLib.PDFName.of("DA"), pdfLib.PDFString.of(newDa));
2654
+ }
2655
+ }
2656
+ } catch (fontError) {
2657
+ console.warn(`Failed to set initials font:`, fontError);
2658
+ }
1427
2659
  initialsField.enableReadOnly();
1428
2660
  if (field.required) initialsField.enableRequired();
1429
2661
  break;
@@ -1527,47 +2759,367 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1527
2759
  } catch (err) {
1528
2760
  warnings.push(`Failed to create ${field.type} field "${field.name}": ${getErrorMessage(err)}`);
1529
2761
  }
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);
2762
+ }
2763
+ initPdfMetadata({ PDFName: pdfLib.PDFName, PDFString: pdfLib.PDFString });
2764
+ const metadata = buildMetadataObject(fields, actualFieldNames);
2765
+ setSigniphiMetadata(pdfDoc, metadata);
2766
+ return warnings;
2767
+ }
2768
+ async function generatePdfFromContent(content, options = {}) {
2769
+ const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
2770
+ const theme = resolveTheme(options.theme);
2771
+ const pdfDoc = await pdfLib.PDFDocument.create();
2772
+ const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
2773
+ const fonts = {
2774
+ regular: await pdfDoc.embedFont(pdfLib.StandardFonts.Helvetica),
2775
+ bold: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBold),
2776
+ italic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaOblique),
2777
+ boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
2778
+ };
2779
+ const watermarkNodes = collectWatermarkNodes(content);
2780
+ const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
2781
+ const state = {
2782
+ currentPage: firstPage,
2783
+ pageIndex: 0,
2784
+ y: 0,
2785
+ fonts,
2786
+ pdfDoc,
2787
+ fieldPositions: /* @__PURE__ */ new Map(),
2788
+ drawFieldPlaceholders,
2789
+ watermarkNodes,
2790
+ theme,
2791
+ images
2792
+ };
2793
+ renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
2794
+ if (content.content) {
2795
+ for (const block of content.content) {
2796
+ processBlock(state, block);
2797
+ }
2798
+ }
2799
+ const allPages = pdfDoc.getPages();
2800
+ const totalPages = allPages.length;
2801
+ const pageNumFontSize = 9;
2802
+ const hfFontSize = 8;
2803
+ const hfColor = pdfLib.rgb(0.45, 0.45, 0.45);
2804
+ for (let i = 0; i < totalPages; i++) {
2805
+ const page = allPages[i];
2806
+ const label = `Page ${i + 1} of ${totalPages}`;
2807
+ const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
2808
+ page.drawText(label, {
2809
+ x: PAGE_WIDTH - MARGIN - labelWidth,
2810
+ y: MARGIN / 2 - pageNumFontSize / 2,
2811
+ size: pageNumFontSize,
2812
+ font: fonts.bold,
2813
+ color: pdfLib.rgb(0, 0, 0)
2814
+ });
2815
+ if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
2816
+ const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
2817
+ if (header.left) {
2818
+ page.drawText(header.left, {
2819
+ x: MARGIN,
2820
+ y: headerBaseY,
2821
+ size: pageNumFontSize,
2822
+ font: fonts.bold,
2823
+ color: theme.pageHeaderText
2824
+ });
2825
+ }
2826
+ if (header.right) {
2827
+ const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
2828
+ page.drawText(header.right, {
2829
+ x: PAGE_WIDTH - MARGIN - rightWidth,
2830
+ y: headerBaseY,
2831
+ size: hfFontSize,
2832
+ font: fonts.regular,
2833
+ color: hfColor
2834
+ });
2835
+ }
2836
+ page.drawLine({
2837
+ start: { x: MARGIN, y: headerBaseY - 6 },
2838
+ end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
2839
+ thickness: 0.75,
2840
+ color: theme.pageHeaderRule
2841
+ });
2842
+ }
2843
+ if (footer?.left) {
2844
+ page.drawText(footer.left, {
2845
+ x: MARGIN,
2846
+ y: MARGIN / 2 - pageNumFontSize / 2,
2847
+ size: hfFontSize,
2848
+ font: fonts.regular,
2849
+ color: hfColor
2850
+ });
2851
+ }
2852
+ }
2853
+ let fieldWarnings;
2854
+ if (embedFormFields && fields.length > 0) {
2855
+ const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
2856
+ if (warnings.length > 0) fieldWarnings = warnings;
2857
+ }
2858
+ const pdfBytes = new Uint8Array(await pdfDoc.save({ useObjectStreams: false }));
2859
+ return {
2860
+ pdfBytes,
2861
+ fieldPositions: state.fieldPositions,
2862
+ fieldWarnings,
2863
+ imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
2864
+ };
2865
+ }
2866
+
2867
+ // src/utils/template-pipeline.ts
2868
+ async function generatePdfFromTiptap(content, values, options = {}) {
2869
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2870
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
2871
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
2872
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2873
+ const fields = extractFieldsFromContent(replaced);
2874
+ const result = await generatePdfFromContent(replaced, {
2875
+ embedFormFields: fields.length > 0,
2876
+ fields,
2877
+ theme: options.theme,
2878
+ header: options.header,
2879
+ footer: options.footer,
2880
+ drawFieldPlaceholders: options.drawFieldPlaceholders
2881
+ });
2882
+ return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
2883
+ }
2884
+ async function generatePdfFromMarkdown(markdown, values, options = {}) {
2885
+ const content = markdownToTiptap(markdown);
2886
+ return generatePdfFromTiptap(content, values, options);
2887
+ }
2888
+
2889
+ // src/utils/markdown-writer.ts
2890
+ function serializeFieldToken(attrs) {
2891
+ const parts = ["field"];
2892
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2893
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2894
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2895
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2896
+ if (attrs.required) parts.push(`required:true`);
2897
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2898
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2899
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2900
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2901
+ if (attrs.multiline) parts.push(`multiline:true`);
2902
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2903
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2904
+ return `{{${parts.join("|")}}}`;
2905
+ }
2906
+ function serializeImageNode(attrs) {
2907
+ const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
2908
+ if (!hasExtras && attrs.src) {
2909
+ return `![${attrs.alt || ""}](${attrs.src})`;
2910
+ }
2911
+ const parts = ["image"];
2912
+ if (attrs.varName) parts.push(`var:${attrs.varName}`);
2913
+ else if (attrs.src) parts.push(`src:${attrs.src}`);
2914
+ if (attrs.alt) parts.push(`alt:${attrs.alt}`);
2915
+ if (attrs.width) parts.push(`width:${attrs.width}`);
2916
+ if (attrs.height) parts.push(`height:${attrs.height}`);
2917
+ if (attrs.align) parts.push(`align:${attrs.align}`);
2918
+ return `{{${parts.join("|")}}}`;
2919
+ }
2920
+ function serializeVariableToken(attrs) {
2921
+ const parts = ["var"];
2922
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2923
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2924
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2925
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2926
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2927
+ if (attrs.block === "true") parts.push(`block:true`);
2928
+ return `{{${parts.join("|")}}}`;
2929
+ }
2930
+ function serializeInline(content) {
2931
+ if (!content) return "";
2932
+ let result = "";
2933
+ for (const node of content) {
2934
+ if (node.type === "fieldNode") {
2935
+ result += serializeFieldToken(node.attrs || {});
2936
+ continue;
2937
+ }
2938
+ if (node.type === "imageNode") {
2939
+ result += serializeImageNode(node.attrs || {});
2940
+ continue;
2941
+ }
2942
+ if (node.type === "variableNode") {
2943
+ let token = serializeVariableToken(node.attrs || {});
2944
+ const varMarks = node.marks || [];
2945
+ const isBold = varMarks.some((m) => m.type === "bold");
2946
+ const isItalic = varMarks.some((m) => m.type === "italic");
2947
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2948
+ if (isBold && isItalic) token = `***${token}***`;
2949
+ else if (isBold) token = `**${token}**`;
2950
+ else if (isItalic) token = `*${token}*`;
2951
+ if (isUnderline) token = `__${token}__`;
2952
+ result += token;
2953
+ continue;
2954
+ }
2955
+ if (node.type === "text") {
2956
+ let text = node.text || "";
2957
+ const marks = node.marks || [];
2958
+ for (const mark of marks) {
2959
+ if (mark.type === "bold") text = `**${text}**`;
2960
+ else if (mark.type === "italic") text = `*${text}*`;
2961
+ else if (mark.type === "underline") text = `__${text}__`;
2962
+ else if (mark.type === "code") text = `\`${text}\``;
2963
+ }
2964
+ result += text;
2965
+ }
2966
+ if (node.type === "hardBreak") {
2967
+ result += " \n";
2968
+ }
2969
+ }
2970
+ return result;
2971
+ }
2972
+ function serializeBlock(node) {
2973
+ switch (node.type) {
2974
+ case "heading": {
2975
+ const level = node.attrs?.level || 1;
2976
+ const prefix = "#".repeat(level);
2977
+ return `${prefix} ${serializeInline(node.content)}`;
2978
+ }
2979
+ case "paragraph": {
2980
+ const inline = serializeInline(node.content);
2981
+ return inline;
2982
+ }
2983
+ case "bulletList": {
2984
+ return (node.content || []).map((item) => {
2985
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2986
+ return `- ${inner}`;
2987
+ }).join("\n");
2988
+ }
2989
+ case "orderedList": {
2990
+ return (node.content || []).map((item, i) => {
2991
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2992
+ return `${i + 1}. ${inner}`;
2993
+ }).join("\n");
2994
+ }
2995
+ case "listItem": {
2996
+ return (node.content || []).map(serializeBlock).join("\n");
2997
+ }
2998
+ case "blockquote": {
2999
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
3000
+ }
3001
+ case "codeBlock": {
3002
+ const text = serializeInline(node.content);
3003
+ return `\`\`\`
3004
+ ${text}
3005
+ \`\`\``;
3006
+ }
3007
+ case "horizontalRule": {
3008
+ return "---";
3009
+ }
3010
+ case "table": {
3011
+ const rows = node.content || [];
3012
+ const isBorderless = node.attrs?.borderless === true;
3013
+ const isSubtotals = node.attrs?.subtotals === true;
3014
+ const serializedRows = [];
3015
+ if (isBorderless && rows.length > 0) {
3016
+ const colCount = rows[0].content?.length || 0;
3017
+ const markerCell = isSubtotals ? "_" : "";
3018
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
3019
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
3020
+ serializedRows.push(markerRow);
3021
+ serializedRows.push(sep);
3022
+ }
3023
+ for (let ri = 0; ri < rows.length; ri++) {
3024
+ const row = rows[ri];
3025
+ const cells = (row.content || []).map((cell) => {
3026
+ const inner = (cell.content || []).map(serializeBlock).join("");
3027
+ return inner;
3028
+ });
3029
+ serializedRows.push(`| ${cells.join(" | ")} |`);
3030
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
3031
+ const sep = cells.map(() => "------").join("|");
3032
+ serializedRows.push(`|${sep}|`);
3033
+ }
3034
+ }
3035
+ return serializedRows.join("\n");
3036
+ }
3037
+ case "tableRow": {
3038
+ const cells = (node.content || []).map((cell) => {
3039
+ const inner = (cell.content || []).map(serializeBlock).join("");
3040
+ return inner;
3041
+ });
3042
+ return `| ${cells.join(" | ")} |`;
3043
+ }
3044
+ case "tableHeader":
3045
+ case "tableCell": {
3046
+ return (node.content || []).map(serializeBlock).join("");
3047
+ }
3048
+ case "imageNode": {
3049
+ return serializeImageNode(node.attrs || {});
3050
+ }
3051
+ case "watermark": {
3052
+ const attrs = node.attrs || {};
3053
+ const parts = [];
3054
+ if (attrs.text) parts.push(`text:${attrs.text}`);
3055
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
3056
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
3057
+ return `:::watermark{${parts.join("|")}}`;
3058
+ }
3059
+ case "panel": {
3060
+ const attrs = node.attrs || {};
3061
+ const parts = [];
3062
+ if (attrs.title) parts.push(`title:${attrs.title}`);
3063
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
3064
+ else if (attrs.border === "solid") parts.push(`border:solid`);
3065
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
3066
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3067
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3068
+ return `:::panel${attrStr}
3069
+ ${children}
3070
+ :::`;
3071
+ }
3072
+ case "repeatBlock": {
3073
+ const attrs = node.attrs || {};
3074
+ const parts = [];
3075
+ if (attrs.data) parts.push(`data:${attrs.data}`);
3076
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3077
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3078
+ return `:::repeat${attrStr}
3079
+ ${children}
3080
+ :::`;
3081
+ }
3082
+ case "subtotalsBlock": {
3083
+ const children = (node.content || []).map(serializeBlock).join("\n");
3084
+ return `:::subtotals
3085
+ ${children}
3086
+ :::`;
3087
+ }
3088
+ case "columns": {
3089
+ const attrs = node.attrs || {};
3090
+ const splitVal = attrs.split || "50";
3091
+ const cols = (node.content || []).filter((c) => c.type === "column");
3092
+ const colBlocks = cols.map((col) => {
3093
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
3094
+ const colAttrs = col.attrs || {};
3095
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3096
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3097
+ return `${colTag}
3098
+ ${children}
3099
+ :::`;
3100
+ });
3101
+ const padX = parseFloat(attrs.padX || "0") || 0;
3102
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
3103
+ return `:::columns{${colsAttrs}}
3104
+ ${colBlocks.join("\n")}
3105
+ :::`;
3106
+ }
3107
+ case "column": {
3108
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3109
+ const colAttrs = node.attrs || {};
3110
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3111
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3112
+ return `${colTag}
3113
+ ${children}
3114
+ :::`;
1558
3115
  }
3116
+ default:
3117
+ return serializeInline(node.content);
1559
3118
  }
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
- };
3119
+ }
3120
+ function tiptapToMarkdown(doc) {
3121
+ if (!doc.content) return "";
3122
+ return doc.content.map(serializeBlock).join("\n\n");
1571
3123
  }
1572
3124
 
1573
3125
  // src/utils/pdf-preview.ts
@@ -1656,8 +3208,26 @@ function useDocumentGenerator(options = {}) {
1656
3208
  Placeholder__default.default.configure({
1657
3209
  placeholder: options.placeholder || "Start writing your document..."
1658
3210
  }),
3211
+ Table__default.default.extend({
3212
+ addAttributes() {
3213
+ return {
3214
+ ...this.parent?.(),
3215
+ borderless: { default: false },
3216
+ subtotals: { default: false }
3217
+ };
3218
+ }
3219
+ }).configure({ resizable: false }),
3220
+ TableRow__default.default,
3221
+ TableCell__default.default,
3222
+ TableHeader__default.default,
1659
3223
  FieldNode,
1660
- VariableNode
3224
+ VariableNode,
3225
+ PanelNode,
3226
+ ColumnsNode,
3227
+ ColumnNode,
3228
+ WatermarkNode,
3229
+ RepeatNode,
3230
+ SubtotalsNode
1661
3231
  ],
1662
3232
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1663
3233
  onUpdate: ({ editor: ed }) => {
@@ -1761,10 +3331,9 @@ function useDocumentGenerator(options = {}) {
1761
3331
  async (values) => {
1762
3332
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1763
3333
  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 };
3334
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
3335
+ const pdfPages2 = await pdfToImages(pdfBytes2);
3336
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1768
3337
  },
1769
3338
  [editor]
1770
3339
  );
@@ -2918,15 +4487,47 @@ function FieldEditPopover({
2918
4487
  ] }) })
2919
4488
  ] });
2920
4489
  }
2921
- function labelToVarName(label) {
4490
+ var sizeClasses = {
4491
+ md: "px-3 py-1.5 text-xs font-medium",
4492
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4493
+ };
4494
+ function ToggleGroup({
4495
+ value,
4496
+ onChange,
4497
+ options,
4498
+ size = "md",
4499
+ className
4500
+ }) {
4501
+ 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(
4502
+ "button",
4503
+ {
4504
+ className: cn(
4505
+ "rounded-md transition-colors",
4506
+ sizeClasses[size],
4507
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4508
+ ),
4509
+ onClick: () => onChange(opt.value),
4510
+ children: opt.label
4511
+ },
4512
+ opt.value
4513
+ )) });
4514
+ }
4515
+ function labelToVarName2(label) {
2922
4516
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2923
4517
  }
4518
+ function varNameToLabel(name) {
4519
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4520
+ }
2924
4521
  function VariableInsertPopover({
2925
4522
  open,
2926
4523
  onOpenChange,
2927
4524
  onInsert,
4525
+ predefinedVariables,
2928
4526
  children
2929
4527
  }) {
4528
+ const hasPredefined = !!predefinedVariables?.length;
4529
+ const [mode, setMode] = React12.useState(hasPredefined ? "existing" : "new");
4530
+ const [search, setSearch] = React12.useState("");
2930
4531
  const [varLabel, setVarLabel] = React12.useState("");
2931
4532
  const [varName, setVarName] = React12.useState("");
2932
4533
  const [varDefault, setVarDefault] = React12.useState("");
@@ -2936,7 +4537,9 @@ function VariableInsertPopover({
2936
4537
  setVarName("");
2937
4538
  setVarDefault("");
2938
4539
  setNameManuallyEdited(false);
2939
- }, []);
4540
+ setSearch("");
4541
+ setMode(hasPredefined ? "existing" : "new");
4542
+ }, [hasPredefined]);
2940
4543
  const handleOpenChange = React12.useCallback(
2941
4544
  (nextOpen) => {
2942
4545
  if (!nextOpen) reset();
@@ -2946,7 +4549,7 @@ function VariableInsertPopover({
2946
4549
  );
2947
4550
  React12.useEffect(() => {
2948
4551
  if (!nameManuallyEdited) {
2949
- setVarName(labelToVarName(varLabel));
4552
+ setVarName(labelToVarName2(varLabel));
2950
4553
  }
2951
4554
  }, [varLabel, nameManuallyEdited]);
2952
4555
  const handleInsert = React12.useCallback(() => {
@@ -2954,10 +4557,32 @@ function VariableInsertPopover({
2954
4557
  onInsert({
2955
4558
  varName: varName.trim(),
2956
4559
  varLabel: varLabel.trim(),
2957
- varDefault: varDefault.trim()
4560
+ varDefault: varDefault.trim(),
4561
+ format: ""
2958
4562
  });
2959
4563
  handleOpenChange(false);
2960
4564
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4565
+ const handlePickPredefined = React12.useCallback(
4566
+ (pv) => {
4567
+ onInsert({
4568
+ varName: pv.varName,
4569
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4570
+ varDefault: "",
4571
+ format: ""
4572
+ });
4573
+ handleOpenChange(false);
4574
+ },
4575
+ [onInsert, handleOpenChange]
4576
+ );
4577
+ const filteredVars = React12.useMemo(() => {
4578
+ if (!predefinedVariables?.length) return [];
4579
+ const q = search.toLowerCase();
4580
+ if (!q) return predefinedVariables;
4581
+ return predefinedVariables.filter((pv) => {
4582
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4583
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4584
+ });
4585
+ }, [predefinedVariables, search]);
2961
4586
  return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2962
4587
  /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children }),
2963
4588
  /* @__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 +4590,114 @@ function VariableInsertPopover({
2965
4590
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 14 }),
2966
4591
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Insert Variable" })
2967
4592
  ] }),
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" }),
4593
+ hasPredefined && /* @__PURE__ */ jsxRuntime.jsx(
4594
+ ToggleGroup,
4595
+ {
4596
+ value: mode,
4597
+ onChange: setMode,
4598
+ options: [
4599
+ { value: "existing", label: "Existing" },
4600
+ { value: "new", label: "New" }
4601
+ ],
4602
+ size: "sm"
4603
+ }
4604
+ ),
4605
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4606
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4607
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2971
4608
  /* @__PURE__ */ jsxRuntime.jsx(
2972
4609
  Input,
2973
4610
  {
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"
4611
+ value: search,
4612
+ onChange: (e) => setSearch(e.target.value),
4613
+ placeholder: "Search variables...",
4614
+ className: "h-8 text-xs pl-7"
2979
4615
  }
2980
4616
  )
2981
4617
  ] }),
2982
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2983
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4618
+ /* @__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) => {
4619
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4620
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4621
+ "button",
4622
+ {
4623
+ 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",
4624
+ onClick: () => handlePickPredefined(pv),
4625
+ children: [
4626
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
4627
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4628
+ ]
4629
+ },
4630
+ pv.varName
4631
+ );
4632
+ }) })
4633
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4634
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4635
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4636
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4637
+ /* @__PURE__ */ jsxRuntime.jsx(
4638
+ Input,
4639
+ {
4640
+ id: "var-label",
4641
+ value: varLabel,
4642
+ onChange: (e) => setVarLabel(e.target.value),
4643
+ placeholder: "e.g. Company Name",
4644
+ className: "h-8 text-xs"
4645
+ }
4646
+ )
4647
+ ] }),
4648
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4649
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4650
+ /* @__PURE__ */ jsxRuntime.jsx(
4651
+ Input,
4652
+ {
4653
+ id: "var-name",
4654
+ value: varName,
4655
+ onChange: (e) => {
4656
+ setVarName(e.target.value);
4657
+ setNameManuallyEdited(true);
4658
+ },
4659
+ placeholder: "e.g. company_name",
4660
+ className: "h-8 text-xs font-mono"
4661
+ }
4662
+ ),
4663
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4664
+ ] }),
4665
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4666
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4667
+ /* @__PURE__ */ jsxRuntime.jsx(
4668
+ Input,
4669
+ {
4670
+ id: "var-default",
4671
+ value: varDefault,
4672
+ onChange: (e) => setVarDefault(e.target.value),
4673
+ placeholder: "Optional",
4674
+ className: "h-8 text-xs"
4675
+ }
4676
+ )
4677
+ ] })
4678
+ ] }),
4679
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
4680
  /* @__PURE__ */ jsxRuntime.jsx(
2985
- Input,
4681
+ Button,
2986
4682
  {
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"
4683
+ size: "sm",
4684
+ className: "h-8 flex-1 text-xs",
4685
+ onClick: handleInsert,
4686
+ disabled: !varLabel.trim() || !varName.trim(),
4687
+ children: "Insert"
2995
4688
  }
2996
4689
  ),
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
4690
  /* @__PURE__ */ jsxRuntime.jsx(
3002
- Input,
4691
+ Button,
3003
4692
  {
3004
- id: "var-default",
3005
- value: varDefault,
3006
- onChange: (e) => setVarDefault(e.target.value),
3007
- placeholder: "Optional",
3008
- className: "h-8 text-xs"
4693
+ variant: "outline",
4694
+ size: "sm",
4695
+ className: "h-8 text-xs",
4696
+ onClick: () => handleOpenChange(false),
4697
+ children: "Cancel"
3009
4698
  }
3010
4699
  )
3011
4700
  ] })
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
4701
  ] })
3035
4702
  ] }) })
3036
4703
  ] });
@@ -3252,7 +4919,6 @@ function PreviewPanel({
3252
4919
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3253
4920
  );
3254
4921
  }, [positionedFields, currentPage, currentPageIndex]);
3255
- const showScrollbars = zoomLevel > 1;
3256
4922
  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
4923
  /* @__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
4924
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3368,12 +5034,13 @@ function PreviewPanel({
3368
5034
  "div",
3369
5035
  {
3370
5036
  className: cn(
3371
- "border border-border rounded-lg bg-muted/30 shrink-0",
3372
- showScrollbars ? "overflow-auto" : "overflow-hidden"
5037
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3373
5038
  ),
3374
5039
  style: {
3375
5040
  width: pageDisplaySize.viewportWidth,
3376
- height: pageDisplaySize.viewportHeight
5041
+ height: pageDisplaySize.viewportHeight,
5042
+ maxWidth: "100%",
5043
+ maxHeight: "100%"
3377
5044
  },
3378
5045
  children: /* @__PURE__ */ jsxRuntime.jsxs(
3379
5046
  "div",
@@ -3444,8 +5111,8 @@ function validateMarkdown(markdown) {
3444
5111
  errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
3445
5112
  } else {
3446
5113
  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")`);
5114
+ if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
5115
+ errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
3449
5116
  } else {
3450
5117
  const body = tokenContent.slice(pipeIdx + 1);
3451
5118
  const pairs = body.split("|");
@@ -3459,12 +5126,48 @@ function validateMarkdown(markdown) {
3459
5126
  if (!hasName) {
3460
5127
  errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
3461
5128
  }
5129
+ } else if (tokenType === "image") {
5130
+ const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
5131
+ if (!hasSource) {
5132
+ errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
5133
+ }
3462
5134
  }
3463
5135
  }
3464
5136
  }
3465
5137
  searchFrom = closeIdx + 2;
3466
5138
  }
3467
5139
  }
5140
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
5141
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
5142
+ const directiveStack = [];
5143
+ for (let i = 0; i < lines.length; i++) {
5144
+ const trimmed = lines[i].trim();
5145
+ const lineNum = i + 1;
5146
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
5147
+ if (openMatch) {
5148
+ const dtype = openMatch[1];
5149
+ if (dtype !== "watermark") {
5150
+ if (dtype === "col") {
5151
+ const parent = directiveStack[directiveStack.length - 1];
5152
+ if (!parent || parent.type !== "columns") {
5153
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
5154
+ }
5155
+ }
5156
+ directiveStack.push({ type: dtype, line: lineNum });
5157
+ }
5158
+ continue;
5159
+ }
5160
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
5161
+ if (directiveStack.length === 0) {
5162
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
5163
+ } else {
5164
+ directiveStack.pop();
5165
+ }
5166
+ }
5167
+ }
5168
+ for (const open of directiveStack) {
5169
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
5170
+ }
3468
5171
  let fields = [];
3469
5172
  let variables = [];
3470
5173
  try {
@@ -3498,31 +5201,6 @@ function validateMarkdown(markdown) {
3498
5201
  fields
3499
5202
  };
3500
5203
  }
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
5204
  function parseCsv(text) {
3527
5205
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3528
5206
  if (lines.length < 2) return [];
@@ -3572,7 +5250,10 @@ function GeneratePanel({
3572
5250
  editorContent,
3573
5251
  editorVariables,
3574
5252
  onGeneratePdf,
3575
- initialBulkData
5253
+ initialBulkData,
5254
+ initialVariableValues,
5255
+ exportFileName,
5256
+ templateMode
3576
5257
  }) {
3577
5258
  const [templateSource, setTemplateSource] = React12.useState("editor");
3578
5259
  const [importedMarkdown, setImportedMarkdown] = React12.useState(null);
@@ -3580,6 +5261,7 @@ function GeneratePanel({
3580
5261
  const [validationResult, setValidationResult] = React12.useState(null);
3581
5262
  const [mode, setMode] = React12.useState("single");
3582
5263
  const [variableValues, setVariableValues] = React12.useState({});
5264
+ const [variableSearch, setVariableSearch] = React12.useState("");
3583
5265
  const [bulkInputFormat, setBulkInputFormat] = React12.useState("json");
3584
5266
  const [bulkInput, setBulkInput] = React12.useState("");
3585
5267
  const [bulkData, setBulkData] = React12.useState(null);
@@ -3606,11 +5288,22 @@ function GeneratePanel({
3606
5288
  const col = pos - textBefore.lastIndexOf("\n");
3607
5289
  setJsonCursor({ line, col });
3608
5290
  }, []);
5291
+ const lockedVarNames = React12__namespace.default.useMemo(
5292
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
5293
+ [templateMode, initialVariableValues]
5294
+ );
3609
5295
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3610
5296
  const hasVariables = activeVariables.length > 0;
3611
5297
  const hasEditorContent = editorMarkdown.trim().length > 0;
3612
5298
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3613
5299
  const hasContent = hasImportedContent || hasEditorContent;
5300
+ const filteredVariables = React12__namespace.default.useMemo(() => {
5301
+ if (!variableSearch.trim()) return activeVariables;
5302
+ const q = variableSearch.toLowerCase();
5303
+ return activeVariables.filter(
5304
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
5305
+ );
5306
+ }, [activeVariables, variableSearch]);
3614
5307
  const getActiveContent = React12.useCallback(() => {
3615
5308
  if (templateSource === "imported" && importedMarkdown) {
3616
5309
  return markdownToTiptap(importedMarkdown);
@@ -3778,7 +5471,10 @@ function GeneratePanel({
3778
5471
  setPreviewError(null);
3779
5472
  try {
3780
5473
  const content = getActiveContent();
3781
- const replaced = replaceVariablesInContent(content, variableValues);
5474
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5475
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5476
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5477
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3782
5478
  const fields = extractFieldsFromContent(replaced);
3783
5479
  const result = await generatePdfFromContent(replaced);
3784
5480
  const pages = await pdfToImages(result.pdfBytes);
@@ -3799,7 +5495,10 @@ function GeneratePanel({
3799
5495
  setExportSuccess(null);
3800
5496
  try {
3801
5497
  const content = getActiveContent();
3802
- const replaced = replaceVariablesInContent(content, variableValues);
5498
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5499
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5500
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5501
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3803
5502
  const fields = extractFieldsFromContent(replaced);
3804
5503
  const result = await generatePdfFromContent(replaced, {
3805
5504
  drawFieldPlaceholders: false,
@@ -3807,7 +5506,7 @@ function GeneratePanel({
3807
5506
  fields
3808
5507
  });
3809
5508
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3810
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5509
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3811
5510
  if (onGeneratePdf) {
3812
5511
  onGeneratePdf(blob, fileName);
3813
5512
  } else {
@@ -3879,7 +5578,10 @@ function GeneratePanel({
3879
5578
  const allWarnings = [];
3880
5579
  for (let i = 0; i < bulkData.length; i++) {
3881
5580
  const values = bulkData[i];
3882
- const replaced = replaceVariablesInContent(content, values);
5581
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5582
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5583
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5584
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3883
5585
  const fields = extractFieldsFromContent(replaced);
3884
5586
  const result = await generatePdfFromContent(replaced, {
3885
5587
  drawFieldPlaceholders: false,
@@ -3934,13 +5636,15 @@ ${allWarnings.join("\n")}`);
3934
5636
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3935
5637
  React12__namespace.default.useEffect(() => {
3936
5638
  if (templateSource === "editor") {
3937
- const defaults = {};
5639
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3938
5640
  for (const v of editorVariables) {
3939
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5641
+ if (!defaults[v.varName]) {
5642
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5643
+ }
3940
5644
  }
3941
5645
  setVariableValues(defaults);
3942
5646
  }
3943
- }, [editorVariables, templateSource]);
5647
+ }, [editorVariables, templateSource, initialVariableValues]);
3944
5648
  React12.useEffect(() => {
3945
5649
  setPreviewFresh(false);
3946
5650
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3952,8 +5656,8 @@ ${allWarnings.join("\n")}`);
3952
5656
  }, [exportSuccess]);
3953
5657
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3954
5658
  /* @__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: [
5659
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5660
+ !templateMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3957
5661
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3958
5662
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3959
5663
  templateSource === "imported" && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4046,10 +5750,10 @@ ${allWarnings.join("\n")}`);
4046
5750
  !hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4047
5751
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 32, className: "mb-2 opacity-50" }),
4048
5752
  /* @__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." })
5753
+ /* @__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
5754
  ] }),
4051
5755
  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(
5756
+ !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
5757
  ToggleGroup,
4054
5758
  {
4055
5759
  value: mode,
@@ -4061,20 +5765,53 @@ ${allWarnings.join("\n")}`);
4061
5765
  }
4062
5766
  ) }),
4063
5767
  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 }),
5768
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
5769
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5770
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5771
+ ] }),
5772
+ activeVariables.length > 5 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
5773
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4067
5774
  /* @__PURE__ */ jsxRuntime.jsx(
4068
5775
  Input,
4069
5776
  {
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"
5777
+ value: variableSearch,
5778
+ onChange: (e) => setVariableSearch(e.target.value),
5779
+ placeholder: "Search variables...",
5780
+ className: "h-8 text-xs pl-8"
5781
+ }
5782
+ ),
5783
+ variableSearch && /* @__PURE__ */ jsxRuntime.jsx(
5784
+ "button",
5785
+ {
5786
+ type: "button",
5787
+ onClick: () => setVariableSearch(""),
5788
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5789
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
4075
5790
  }
4076
5791
  )
4077
- ] }, v.varName))
5792
+ ] }),
5793
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5794
+ "No variables match \u201C",
5795
+ variableSearch,
5796
+ "\u201D"
5797
+ ] }),
5798
+ filteredVariables.map((v) => {
5799
+ const isLocked = lockedVarNames.has(v.varName);
5800
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5801
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5802
+ /* @__PURE__ */ jsxRuntime.jsx(
5803
+ Input,
5804
+ {
5805
+ id: `gen-${v.varName}`,
5806
+ value: variableValues[v.varName] || "",
5807
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5808
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5809
+ disabled: isLocked,
5810
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5811
+ }
5812
+ )
5813
+ ] }, v.varName);
5814
+ })
4078
5815
  ] }),
4079
5816
  mode === "bulk" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4080
5817
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4263,56 +6000,60 @@ ${allWarnings.join("\n")}`);
4263
6000
  ] }),
4264
6001
  !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
6002
  ] }),
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(
6003
+ 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: [
6004
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6005
+ /* @__PURE__ */ jsxRuntime.jsxs(
6006
+ Button,
6007
+ {
6008
+ variant: "secondary",
6009
+ onClick: handlePreview,
6010
+ disabled: isGenerating,
6011
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6012
+ children: [
6013
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
6014
+ isGenerating ? "Generating..." : "Generate PDF"
6015
+ ]
6016
+ }
6017
+ ),
6018
+ /* @__PURE__ */ jsxRuntime.jsxs(
6019
+ Button,
6020
+ {
6021
+ onClick: handleExportSingle,
6022
+ disabled: !previewFresh || isExporting,
6023
+ 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",
6024
+ children: [
6025
+ isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
6026
+ isExporting ? "Exporting..." : "Export PDF"
6027
+ ]
6028
+ }
6029
+ )
6030
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4293
6031
  Button,
4294
6032
  {
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",
6033
+ onClick: handleBulkGenerate,
6034
+ disabled: isExporting || !bulkData || bulkData.length === 0,
6035
+ 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
6036
  children: [
4299
6037
  isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
4300
- isExporting ? "Exporting..." : "Export PDF"
6038
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4301
6039
  ]
4302
6040
  }
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
- ) })
6041
+ ),
6042
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6043
+ previewError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6044
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6045
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: previewError })
6046
+ ] }),
6047
+ exportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6048
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6049
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportError })
6050
+ ] }),
6051
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6052
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
6053
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportSuccess })
6054
+ ] })
6055
+ ] })
6056
+ ] })
4316
6057
  ] }),
4317
6058
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
4318
6059
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -4490,7 +6231,13 @@ function DocumentGeneratorInner({
4490
6231
  showToolbar = true,
4491
6232
  showGenerateTab = true,
4492
6233
  onGeneratePdf,
4493
- initialBulkData
6234
+ initialBulkData,
6235
+ defaultTab = "editor",
6236
+ initialVariableValues,
6237
+ predefinedVariables,
6238
+ exportFileName,
6239
+ templateMode,
6240
+ onSaveTemplate
4494
6241
  }) {
4495
6242
  const {
4496
6243
  editor,
@@ -4517,7 +6264,7 @@ function DocumentGeneratorInner({
4517
6264
  placeholder,
4518
6265
  onChange
4519
6266
  });
4520
- const [activeTab, setActiveTab] = React12.useState("editor");
6267
+ const [activeTab, setActiveTab] = React12.useState(defaultTab);
4521
6268
  const [insertPopoverOpen, setInsertPopoverOpen] = React12.useState(false);
4522
6269
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = React12.useState(false);
4523
6270
  const [editFieldId, setEditFieldId] = React12.useState(null);
@@ -4529,6 +6276,7 @@ function DocumentGeneratorInner({
4529
6276
  const editorWrapperRef = React12.useRef(null);
4530
6277
  const [editorCollapsed, setEditorCollapsed] = React12.useState(false);
4531
6278
  const [mdExportError, setMdExportError] = React12.useState(null);
6279
+ const [isSaving, setIsSaving] = React12.useState(false);
4532
6280
  const [exportSuccess, setExportSuccess] = React12.useState(null);
4533
6281
  React12.useEffect(() => {
4534
6282
  const handler = (e) => {
@@ -4599,7 +6347,28 @@ function DocumentGeneratorInner({
4599
6347
  onExport(result);
4600
6348
  setExportSuccess("Document exported successfully");
4601
6349
  }
4602
- }, [exportDocument, onExport]);
6350
+ }, [exportDocument, onExport]);
6351
+ const handleSaveTemplate = React12.useCallback(async () => {
6352
+ const md = exportMarkdown();
6353
+ if (!md.trim()) {
6354
+ setMdExportError("Editor content is empty");
6355
+ return;
6356
+ }
6357
+ const validation = validateMarkdown(md);
6358
+ if (!validation.valid) {
6359
+ setMdExportError(validation.errors.join("; "));
6360
+ return;
6361
+ }
6362
+ setMdExportError(null);
6363
+ setIsSaving(true);
6364
+ try {
6365
+ await onSaveTemplate?.(md);
6366
+ setExportSuccess("Template saved");
6367
+ } catch {
6368
+ } finally {
6369
+ setIsSaving(false);
6370
+ }
6371
+ }, [exportMarkdown, onSaveTemplate]);
4603
6372
  const handleExportMarkdown = React12.useCallback(() => {
4604
6373
  const md = exportMarkdown();
4605
6374
  if (!md.trim()) {
@@ -4616,10 +6385,10 @@ function DocumentGeneratorInner({
4616
6385
  const url = URL.createObjectURL(blob);
4617
6386
  const a = document.createElement("a");
4618
6387
  a.href = url;
4619
- a.download = "template.md";
6388
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4620
6389
  a.click();
4621
6390
  URL.revokeObjectURL(url);
4622
- }, [exportMarkdown]);
6391
+ }, [exportMarkdown, exportFileName]);
4623
6392
  React12.useEffect(() => {
4624
6393
  if (mdExportError) setMdExportError(null);
4625
6394
  }, [markdown]);
@@ -4655,6 +6424,7 @@ function DocumentGeneratorInner({
4655
6424
  open: insertVarPopoverOpen,
4656
6425
  onOpenChange: setInsertVarPopoverOpen,
4657
6426
  onInsert: handleInsertVariable,
6427
+ predefinedVariables,
4658
6428
  children: /* @__PURE__ */ jsxRuntime.jsxs(
4659
6429
  Button,
4660
6430
  {
@@ -4695,118 +6465,141 @@ function DocumentGeneratorInner({
4695
6465
  ] })
4696
6466
  ] })
4697
6467
  ] }),
4698
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6468
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
4699
6469
  "flex flex-col flex-1 min-h-0",
4700
6470
  activeTab !== "editor" && "hidden"
6471
+ ), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6472
+ "flex-1 min-h-0",
6473
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4701
6474
  ), 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,
6475
+ !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6476
+ showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
6477
+ EditorToolbar,
4727
6478
  {
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"
6479
+ editor,
6480
+ insertFieldButton,
6481
+ insertVariableButton,
6482
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4742
6483
  }
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,
6484
+ ) }),
6485
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
6486
+ react.EditorContent,
4748
6487
  {
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
- ]
6488
+ editor,
6489
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4757
6490
  }
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: [
6491
+ ) }),
6492
+ /* @__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
6493
  /* @__PURE__ */ jsxRuntime.jsxs(
4784
6494
  Button,
4785
6495
  {
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",
6496
+ variant: "secondary",
6497
+ onClick: generatePdf,
6498
+ disabled: isGenerating || !editor,
6499
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4789
6500
  children: [
4790
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
4791
- "Export MD"
6501
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
6502
+ isGenerating ? "Generating..." : "Generate PDF"
4792
6503
  ]
4793
6504
  }
4794
6505
  ),
4795
- /* @__PURE__ */ jsxRuntime.jsxs(
6506
+ onSaveTemplate ? /* @__PURE__ */ jsxRuntime.jsxs(
4796
6507
  Button,
4797
6508
  {
4798
- onClick: handleExport,
4799
- disabled: isGenerating || !editor || pdfPages.length === 0,
6509
+ onClick: handleSaveTemplate,
6510
+ disabled: isSaving || !editor || !markdown.trim(),
4800
6511
  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
6512
  children: [
4802
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
4803
- exportButtonText
6513
+ isSaving ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 16 }),
6514
+ isSaving ? "Saving..." : exportButtonText || "Save Template"
4804
6515
  ]
4805
6516
  }
4806
- )
6517
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
6518
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
6519
+ Button,
6520
+ {
6521
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6522
+ 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",
6523
+ children: [
6524
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6525
+ "Export",
6526
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14 })
6527
+ ]
6528
+ }
6529
+ ) }),
6530
+ /* @__PURE__ */ jsxRuntime.jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6531
+ /* @__PURE__ */ jsxRuntime.jsxs(
6532
+ "button",
6533
+ {
6534
+ onClick: handleExportMarkdown,
6535
+ disabled: !editor || !markdown.trim(),
6536
+ 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",
6537
+ children: [
6538
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
6539
+ "Export as MD"
6540
+ ]
6541
+ }
6542
+ ),
6543
+ /* @__PURE__ */ jsxRuntime.jsxs(
6544
+ "button",
6545
+ {
6546
+ onClick: handleExport,
6547
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6548
+ 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",
6549
+ children: [
6550
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6551
+ "Export as PDF"
6552
+ ]
6553
+ }
6554
+ )
6555
+ ] })
6556
+ ] }),
6557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6558
+ mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6559
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6560
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
6561
+ ] }),
6562
+ generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6563
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6564
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
6565
+ ] }),
6566
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6567
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6568
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
6569
+ fieldWarnings.length,
6570
+ " field",
6571
+ fieldWarnings.length !== 1 ? "s" : "",
6572
+ " had warnings during generation"
6573
+ ] })
6574
+ ] }),
6575
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6576
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
6577
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
6578
+ ] })
6579
+ ] })
4807
6580
  ] })
4808
- ] })
4809
- ] }),
6581
+ ] }),
6582
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
6583
+ showPreview && /* @__PURE__ */ jsxRuntime.jsx(
6584
+ PreviewPanel,
6585
+ {
6586
+ pages: pdfPages,
6587
+ isGenerating,
6588
+ positionedFields,
6589
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
6590
+ Button,
6591
+ {
6592
+ variant: "ghost",
6593
+ size: "sm",
6594
+ className: "h-8 w-8 p-0",
6595
+ onClick: () => setEditorCollapsed(false),
6596
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
6597
+ }
6598
+ ) : void 0,
6599
+ className: "flex-1 border-0 rounded-none"
6600
+ }
6601
+ )
6602
+ ] }) }),
4810
6603
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsxRuntime.jsx(
4811
6604
  GeneratePanel,
4812
6605
  {
@@ -4814,7 +6607,10 @@ function DocumentGeneratorInner({
4814
6607
  editorContent: editor?.getJSON(),
4815
6608
  editorVariables: variables,
4816
6609
  onGeneratePdf,
4817
- initialBulkData
6610
+ initialBulkData,
6611
+ initialVariableValues,
6612
+ exportFileName,
6613
+ templateMode
4818
6614
  }
4819
6615
  ),
4820
6616
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4955,11 +6751,734 @@ function EditorPanel({
4955
6751
  ] });
4956
6752
  }
4957
6753
 
6754
+ // src/utils/xml-template-parser.ts
6755
+ function extractFieldRefs(expr) {
6756
+ const refs = [];
6757
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6758
+ let m;
6759
+ while ((m = re.exec(expr)) !== null) {
6760
+ if (m[2]) {
6761
+ refs.push(`${m[1]}.${m[2]}`);
6762
+ } else {
6763
+ refs.push(m[1]);
6764
+ }
6765
+ }
6766
+ return refs;
6767
+ }
6768
+ function fieldRefToVarName(ref, bandContext) {
6769
+ if (ref.includes(".")) {
6770
+ return labelToVarName(ref.replace(/\./g, "_"));
6771
+ }
6772
+ if (bandContext) {
6773
+ return labelToVarName(bandContext + "_" + ref);
6774
+ }
6775
+ return labelToVarName(ref);
6776
+ }
6777
+ function fieldRefToLabel(ref) {
6778
+ const parts = ref.split(".");
6779
+ const raw = parts[parts.length - 1];
6780
+ return raw.replace(/_/g, " ");
6781
+ }
6782
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6783
+ const trimmed = expr.trim();
6784
+ const suppressZero = /<>\s*0/.test(trimmed);
6785
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6786
+ const aggMatch = trimmed.match(
6787
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6788
+ );
6789
+ if (aggMatch) {
6790
+ const fieldName = aggMatch[2];
6791
+ const varName = labelToVarName("workorder_" + fieldName);
6792
+ const label = fieldName.replace(/_/g, " ");
6793
+ return registry.register(varName, label, szOpts);
6794
+ }
6795
+ const concatSingle = trimmed.match(
6796
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6797
+ );
6798
+ if (concatSingle) {
6799
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6800
+ const varName = fieldRefToVarName(ref, bandContext);
6801
+ const label = fieldRefToLabel(ref);
6802
+ return registry.register(varName, label, szOpts);
6803
+ }
6804
+ const concatMulti = trimmed.match(
6805
+ /^concat\(/i
6806
+ );
6807
+ if (concatMulti) {
6808
+ const refs = extractFieldRefs(trimmed);
6809
+ if (refs.length > 0) {
6810
+ const uniqueRefs = [...new Set(refs)];
6811
+ const tokens = uniqueRefs.map((ref) => {
6812
+ const varName = fieldRefToVarName(ref, bandContext);
6813
+ const label = fieldRefToLabel(ref);
6814
+ return registry.register(varName, label, szOpts);
6815
+ });
6816
+ return tokens.join(" ");
6817
+ }
6818
+ }
6819
+ const toLongMatch = trimmed.match(
6820
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6821
+ );
6822
+ if (toLongMatch) {
6823
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6824
+ const varName = fieldRefToVarName(ref, bandContext);
6825
+ const label = fieldRefToLabel(ref);
6826
+ return registry.register(varName, label, szOpts);
6827
+ }
6828
+ const fmtMatch = trimmed.match(
6829
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6830
+ );
6831
+ if (fmtMatch) {
6832
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6833
+ }
6834
+ const replaceMatch = trimmed.match(
6835
+ /^Replace\(\s*\[([^\]]+)\]/i
6836
+ );
6837
+ if (replaceMatch) {
6838
+ const ref = replaceMatch[1];
6839
+ const varName = fieldRefToVarName(ref, bandContext);
6840
+ const label = fieldRefToLabel(ref);
6841
+ return registry.register(varName, label, szOpts);
6842
+ }
6843
+ const iifLabelMatch = trimmed.match(
6844
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6845
+ );
6846
+ if (iifLabelMatch) {
6847
+ return iifLabelMatch[1];
6848
+ }
6849
+ const iifLabelElse = trimmed.match(
6850
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6851
+ );
6852
+ if (iifLabelElse) {
6853
+ return iifLabelElse[1];
6854
+ }
6855
+ const iifEmptyElse = trimmed.match(
6856
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6857
+ );
6858
+ if (iifEmptyElse) {
6859
+ return iifEmptyElse[1];
6860
+ }
6861
+ const nestedIifStaticLabel = trimmed.match(
6862
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6863
+ );
6864
+ if (nestedIifStaticLabel) {
6865
+ return nestedIifStaticLabel[1];
6866
+ }
6867
+ const iifValueMatch = trimmed.match(
6868
+ /^iif\s*\(/i
6869
+ );
6870
+ if (iifValueMatch) {
6871
+ const refs = extractFieldRefs(trimmed);
6872
+ if (refs.length > 0) {
6873
+ const uniqueRefs = [...new Set(refs)];
6874
+ const innerAggMatch = trimmed.match(
6875
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6876
+ );
6877
+ if (innerAggMatch) {
6878
+ const fieldName = innerAggMatch[2];
6879
+ const varName2 = labelToVarName("workorder_" + fieldName);
6880
+ const label2 = fieldName.replace(/_/g, " ");
6881
+ return registry.register(varName2, label2, szOpts);
6882
+ }
6883
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6884
+ const varName = fieldRefToVarName(ref, bandContext);
6885
+ const label = fieldRefToLabel(ref);
6886
+ return registry.register(varName, label, szOpts);
6887
+ }
6888
+ const staticMatch = trimmed.match(/'([^']+)'/);
6889
+ if (staticMatch) return staticMatch[1];
6890
+ return "";
6891
+ }
6892
+ const arithmeticRefs = extractFieldRefs(trimmed);
6893
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6894
+ if (siblingLabel) {
6895
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6896
+ const varName = labelToVarName(
6897
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6898
+ );
6899
+ return registry.register(varName, cleanLabel, szOpts);
6900
+ }
6901
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6902
+ const tokens = uniqueRefs.map((ref) => {
6903
+ const varName = fieldRefToVarName(ref, bandContext);
6904
+ const label = fieldRefToLabel(ref);
6905
+ return registry.register(varName, label, szOpts);
6906
+ });
6907
+ return tokens.join(" ");
6908
+ }
6909
+ const simpleMatch = trimmed.match(
6910
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6911
+ );
6912
+ if (simpleMatch) {
6913
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6914
+ const varName = fieldRefToVarName(ref, bandContext);
6915
+ const label = fieldRefToLabel(ref);
6916
+ return registry.register(varName, label, szOpts);
6917
+ }
6918
+ if (arithmeticRefs.length >= 1) {
6919
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6920
+ const varName = fieldRefToVarName(ref, bandContext);
6921
+ const label = fieldRefToLabel(ref);
6922
+ return registry.register(varName, label, szOpts);
6923
+ }
6924
+ return "";
6925
+ }
6926
+ var VariableRegistry = class {
6927
+ constructor() {
6928
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6929
+ }
6930
+ register(varName, varLabel, options) {
6931
+ if (!this.map.has(varName)) {
6932
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6933
+ }
6934
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6935
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6936
+ }
6937
+ getAll() {
6938
+ return Array.from(this.map.values());
6939
+ }
6940
+ };
6941
+ function parseLocationFloat(s) {
6942
+ if (!s) return { x: 0, y: 0 };
6943
+ const parts = s.split(",");
6944
+ return {
6945
+ x: parseFloat(parts[0]) || 0,
6946
+ y: parseFloat(parts[1]) || 0
6947
+ };
6948
+ }
6949
+ function parseSizeF(s) {
6950
+ if (!s) return { w: 0, h: 0 };
6951
+ const parts = s.split(",");
6952
+ return {
6953
+ w: parseFloat(parts[0]) || 0,
6954
+ h: parseFloat(parts[1]) || 0
6955
+ };
6956
+ }
6957
+ function extractControls(bandEl) {
6958
+ const controlsEl = bandEl.querySelector(":scope > Controls");
6959
+ if (!controlsEl) return [];
6960
+ const items = [];
6961
+ for (const child of Array.from(controlsEl.children)) {
6962
+ items.push(extractControl(child));
6963
+ }
6964
+ return items;
6965
+ }
6966
+ function extractControl(el) {
6967
+ const ctrlType = el.getAttribute("ControlType") || "";
6968
+ const name = el.getAttribute("Name") || "";
6969
+ const text = el.getAttribute("Text") || "";
6970
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
6971
+ const size = parseSizeF(el.getAttribute("SizeF"));
6972
+ const formatString = el.getAttribute("TextFormatString") || null;
6973
+ let expression = null;
6974
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
6975
+ if (bindingsEl) {
6976
+ for (const item of Array.from(bindingsEl.children)) {
6977
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
6978
+ expression = item.getAttribute("Expression");
6979
+ break;
6980
+ }
6981
+ }
6982
+ }
6983
+ const children = [];
6984
+ if (ctrlType === "XRPanel") {
6985
+ const panelControls = extractControls(el);
6986
+ children.push(...panelControls);
6987
+ }
6988
+ return {
6989
+ name,
6990
+ type: ctrlType,
6991
+ text,
6992
+ x: loc.x,
6993
+ y: loc.y,
6994
+ width: size.w,
6995
+ expression,
6996
+ formatString,
6997
+ children
6998
+ };
6999
+ }
7000
+ function groupByRow(controls, threshold = 8) {
7001
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
7002
+ const rows = [];
7003
+ for (const ctrl of sorted) {
7004
+ const lastRow = rows[rows.length - 1];
7005
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
7006
+ lastRow.controls.push(ctrl);
7007
+ } else {
7008
+ rows.push({ y: ctrl.y, controls: [ctrl] });
7009
+ }
7010
+ }
7011
+ for (const row of rows) {
7012
+ row.controls.sort((a, b) => a.x - b.x);
7013
+ }
7014
+ return rows;
7015
+ }
7016
+ function isStaticLabel(ctrl) {
7017
+ return ctrl.type === "XRLabel" && !ctrl.expression;
7018
+ }
7019
+ function isBound(ctrl) {
7020
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
7021
+ }
7022
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
7023
+ const leftExtent = leftControls.reduce(
7024
+ (max, c) => Math.max(max, c.x + c.width),
7025
+ 0
7026
+ );
7027
+ const rightMinX = rightControls.reduce(
7028
+ (min, c) => Math.min(min, c.x),
7029
+ contentWidth
7030
+ );
7031
+ const rightMaxExtent = rightControls.reduce(
7032
+ (max, c) => Math.max(max, c.x + c.width),
7033
+ 0
7034
+ );
7035
+ const rightExtent = rightMaxExtent - rightMinX;
7036
+ if (leftExtent + rightExtent === 0) return 50;
7037
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
7038
+ }
7039
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
7040
+ const lines = [];
7041
+ const leftControls = controls.filter((c) => c.x < 437);
7042
+ const rightControls = controls.filter((c) => c.x >= 437);
7043
+ const leftEntries = [];
7044
+ if (leftControls.length > 0) {
7045
+ const leftRows = groupByRow(leftControls);
7046
+ for (const row of leftRows) {
7047
+ const parts = [];
7048
+ for (const ctrl of row.controls) {
7049
+ if (isBound(ctrl)) {
7050
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7051
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
7052
+ resolved = resolved.replace("}}", "|format:phone}}");
7053
+ }
7054
+ if (resolved) parts.push(resolved);
7055
+ } else if (isStaticLabel(ctrl)) {
7056
+ const text = ctrl.text.trim();
7057
+ if (text && !text.match(/^label\d+$/)) {
7058
+ parts.push(text);
7059
+ }
7060
+ }
7061
+ }
7062
+ if (parts.length > 0) {
7063
+ leftEntries.push(parts.join(" "));
7064
+ }
7065
+ }
7066
+ }
7067
+ const rightEntries = [];
7068
+ if (rightControls.length > 0) {
7069
+ const rightRows = groupByRow(rightControls);
7070
+ for (const row of rightRows) {
7071
+ let label = "";
7072
+ let value = "";
7073
+ for (const ctrl of row.controls) {
7074
+ if (isStaticLabel(ctrl)) {
7075
+ const text = ctrl.text.trim();
7076
+ if (text && !text.match(/^label\d+$/)) {
7077
+ label = text;
7078
+ }
7079
+ } else if (isBound(ctrl)) {
7080
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
7081
+ if (ctrl.expression) {
7082
+ const refs = extractFieldRefs(ctrl.expression);
7083
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
7084
+ value = value.replace("}}", "|suppress:zero}}");
7085
+ }
7086
+ }
7087
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7088
+ value = value.replace("}}", "|format:phone}}");
7089
+ }
7090
+ }
7091
+ }
7092
+ if (label || value) {
7093
+ rightEntries.push({ label, value, y: row.y });
7094
+ }
7095
+ }
7096
+ }
7097
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7098
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7099
+ lines.push(`:::columns{split:${outerSplit}}`);
7100
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
7101
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
7102
+ const xmlLineHeight = 17;
7103
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
7104
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
7105
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
7106
+ lines.push(leftColTag);
7107
+ for (const entry of leftEntries) {
7108
+ lines.push(entry);
7109
+ }
7110
+ lines.push(":::");
7111
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
7112
+ lines.push(rightColTag);
7113
+ if (rightEntries.length > 0) {
7114
+ lines.push("| | |");
7115
+ lines.push("|---|---|");
7116
+ let prevY = -Infinity;
7117
+ for (const entry of rightEntries) {
7118
+ if (prevY > -Infinity && entry.y - prevY > 25) {
7119
+ lines.push("| | |");
7120
+ }
7121
+ const label = entry.label || "";
7122
+ const colon = entry.label ? ":" : "";
7123
+ const value = entry.value ? ` ${entry.value}` : "";
7124
+ lines.push(`| ${label} | ${colon}${value} |`);
7125
+ prevY = entry.y;
7126
+ }
7127
+ }
7128
+ lines.push(":::");
7129
+ lines.push(":::");
7130
+ lines.push("");
7131
+ }
7132
+ return lines.join("\n");
7133
+ }
7134
+ function renderReportHeaderBand(controls, registry, contentWidth) {
7135
+ const lines = [];
7136
+ const leftControls = controls.filter((c) => c.x < 437);
7137
+ const rightControls = controls.filter((c) => c.x >= 437);
7138
+ const leftEntries = [];
7139
+ if (leftControls.length > 0) {
7140
+ const leftRows = groupByRow(leftControls);
7141
+ for (const row of leftRows) {
7142
+ const parts = [];
7143
+ for (const ctrl of row.controls) {
7144
+ if (isBound(ctrl)) {
7145
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7146
+ if (resolved) parts.push(resolved);
7147
+ } else if (isStaticLabel(ctrl)) {
7148
+ const text = ctrl.text.trim();
7149
+ if (text && !text.match(/^label\d+$/)) {
7150
+ parts.push(text);
7151
+ }
7152
+ }
7153
+ }
7154
+ if (parts.length > 0) {
7155
+ leftEntries.push(parts.join(" "));
7156
+ }
7157
+ }
7158
+ }
7159
+ const rightEntries = [];
7160
+ if (rightControls.length > 0) {
7161
+ const rightRows = groupByRow(rightControls);
7162
+ for (const row of rightRows) {
7163
+ let label = "";
7164
+ let value = "";
7165
+ for (const ctrl of row.controls) {
7166
+ if (isStaticLabel(ctrl)) {
7167
+ const text = ctrl.text.trim();
7168
+ if (text && !text.match(/^label\d+$/)) {
7169
+ label = text;
7170
+ }
7171
+ } else if (isBound(ctrl)) {
7172
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7173
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7174
+ value = value.replace("}}", "|format:phone}}");
7175
+ }
7176
+ }
7177
+ }
7178
+ if (label || value) {
7179
+ rightEntries.push({ label, value });
7180
+ }
7181
+ }
7182
+ }
7183
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7184
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7185
+ lines.push(`:::columns{split:${outerSplit}}`);
7186
+ lines.push(":::col");
7187
+ for (const entry of leftEntries) {
7188
+ lines.push(entry);
7189
+ }
7190
+ lines.push(":::");
7191
+ lines.push(":::col");
7192
+ lines.push("| | |");
7193
+ lines.push("|---|---|");
7194
+ for (const entry of rightEntries) {
7195
+ const label = entry.label || "";
7196
+ const value = entry.value ? ` ${entry.value}` : "";
7197
+ lines.push(`| ${label} |${value} |`);
7198
+ }
7199
+ lines.push(":::");
7200
+ lines.push(":::");
7201
+ lines.push("");
7202
+ }
7203
+ return lines.join("\n");
7204
+ }
7205
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
7206
+ const lines = [];
7207
+ const headerTexts = [];
7208
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
7209
+ if (isStaticLabel(ctrl)) {
7210
+ const text = ctrl.text.trim();
7211
+ if (text) headerTexts.push(text);
7212
+ }
7213
+ }
7214
+ const titleText = headerTexts.join(" ");
7215
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
7216
+ lines.push(":::");
7217
+ lines.push(":::repeat{data:operations}");
7218
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
7219
+ const detailParts = [];
7220
+ for (const ctrl of sortedDetail) {
7221
+ if (isBound(ctrl)) {
7222
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
7223
+ if (resolved) detailParts.push(resolved);
7224
+ }
7225
+ }
7226
+ if (detailParts.length > 0) {
7227
+ if (detailParts.length >= 2) {
7228
+ lines.push(":::columns{split:12}");
7229
+ lines.push(":::col");
7230
+ lines.push(`**${detailParts[0]}**`);
7231
+ lines.push(":::");
7232
+ lines.push(":::col");
7233
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
7234
+ lines.push(":::");
7235
+ lines.push(":::");
7236
+ } else {
7237
+ lines.push(`**${detailParts[0]}**`);
7238
+ }
7239
+ lines.push("");
7240
+ }
7241
+ if (groupFooterControls.length > 0) {
7242
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
7243
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
7244
+ }
7245
+ lines.push("---");
7246
+ lines.push(":::");
7247
+ lines.push("");
7248
+ return lines.join("\n");
7249
+ }
7250
+ function renderGroupFooterBand(controls, registry, _contentWidth) {
7251
+ const rows = groupByRow(controls);
7252
+ const entries = [];
7253
+ for (const row of rows) {
7254
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7255
+ if (labels.length === 0) continue;
7256
+ let label = "";
7257
+ let value = "";
7258
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7259
+ for (const ctrl of sorted) {
7260
+ if (isStaticLabel(ctrl)) {
7261
+ const text = ctrl.text.trim();
7262
+ if (text && !text.match(/^label\d+$/)) {
7263
+ label = text;
7264
+ }
7265
+ } else if (isBound(ctrl)) {
7266
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
7267
+ if (resolved) {
7268
+ if (!resolved.startsWith("{{var|")) {
7269
+ if (!label) label = resolved;
7270
+ } else {
7271
+ value = resolved;
7272
+ }
7273
+ }
7274
+ }
7275
+ }
7276
+ if (label && value) {
7277
+ entries.push({ label, value });
7278
+ }
7279
+ }
7280
+ if (entries.length === 0) return "";
7281
+ const lines = [];
7282
+ lines.push(":::subtotals");
7283
+ for (const entry of entries) {
7284
+ lines.push(`**${entry.label}** ${entry.value}`);
7285
+ }
7286
+ lines.push(":::");
7287
+ lines.push("");
7288
+ return lines.join("\n");
7289
+ }
7290
+ function renderReportFooterBand(controls, registry) {
7291
+ const lines = [];
7292
+ const panel = controls.find((c) => c.type === "XRPanel");
7293
+ const thankYouLabel = controls.find(
7294
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
7295
+ );
7296
+ const priceRows = [];
7297
+ if (panel && panel.children.length > 0) {
7298
+ const rows = groupByRow(panel.children, 15);
7299
+ for (const row of rows) {
7300
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7301
+ if (labels.length === 0) continue;
7302
+ let label = "";
7303
+ let value = "";
7304
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7305
+ for (const ctrl of sorted) {
7306
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
7307
+ continue;
7308
+ }
7309
+ const isLabelPos = ctrl.x < 150;
7310
+ if (isStaticLabel(ctrl)) {
7311
+ const text = ctrl.text.trim();
7312
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
7313
+ label = text;
7314
+ }
7315
+ } else if (isBound(ctrl)) {
7316
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
7317
+ if (resolved) {
7318
+ if (isLabelPos) {
7319
+ if (!label) label = resolved;
7320
+ } else {
7321
+ if (!value) value = resolved;
7322
+ }
7323
+ }
7324
+ }
7325
+ }
7326
+ if (label && value) {
7327
+ priceRows.push({ label, value });
7328
+ } else if (value) {
7329
+ priceRows.push({ label: "", value });
7330
+ }
7331
+ }
7332
+ }
7333
+ lines.push(":::columns{split:40|padX:20}");
7334
+ lines.push(":::col{padTop:1}");
7335
+ if (thankYouLabel) {
7336
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
7337
+ }
7338
+ lines.push(":::");
7339
+ lines.push(":::col{padTop:1}");
7340
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
7341
+ if (priceRows.length > 0) {
7342
+ lines.push(":::subtotals");
7343
+ for (const row of priceRows) {
7344
+ const label = row.label ? `**${row.label}**` : "";
7345
+ lines.push(`${label} ${row.value}`);
7346
+ }
7347
+ lines.push(":::");
7348
+ }
7349
+ lines.push(":::");
7350
+ lines.push(":::");
7351
+ lines.push(":::");
7352
+ lines.push("");
7353
+ return lines.join("\n");
7354
+ }
7355
+ function parseXmlTemplate(xmlString) {
7356
+ const warnings = [];
7357
+ const registry = new VariableRegistry();
7358
+ const parser = new DOMParser();
7359
+ const doc = parser.parseFromString(xmlString, "text/xml");
7360
+ const parseError = doc.querySelector("parsererror");
7361
+ if (parseError) {
7362
+ return {
7363
+ markdown: "",
7364
+ variables: [],
7365
+ reportName: "",
7366
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7367
+ };
7368
+ }
7369
+ const root = doc.documentElement;
7370
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7371
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7372
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7373
+ const marginParts = marginsStr.split(",").map(Number);
7374
+ const leftMargin = marginParts[0] || 50;
7375
+ const rightMargin = marginParts[1] || 50;
7376
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7377
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7378
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7379
+ if (calcFieldsEl) {
7380
+ for (const item of Array.from(calcFieldsEl.children)) {
7381
+ const name = item.getAttribute("Name");
7382
+ if (name) calculatedFieldNames.add(name);
7383
+ }
7384
+ }
7385
+ const markdownSections = [];
7386
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7387
+ if (watermarkEl) {
7388
+ const wmText = watermarkEl.getAttribute("Text");
7389
+ if (wmText) {
7390
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7391
+ markdownSections.push("");
7392
+ }
7393
+ }
7394
+ const bandsEl = root.querySelector(":scope > Bands");
7395
+ if (!bandsEl) {
7396
+ warnings.push("No <Bands> element found in the XML");
7397
+ return {
7398
+ markdown: "",
7399
+ variables: registry.getAll(),
7400
+ reportName,
7401
+ warnings
7402
+ };
7403
+ }
7404
+ let topMarginControls = [];
7405
+ let reportHeaderControls = [];
7406
+ let pageHeaderControls = [];
7407
+ let detailControls = [];
7408
+ let groupFooterControls = [];
7409
+ let reportFooterControls = [];
7410
+ for (const bandItem of Array.from(bandsEl.children)) {
7411
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7412
+ if (ctrlType === "TopMarginBand") {
7413
+ topMarginControls = extractControls(bandItem);
7414
+ } else if (ctrlType === "ReportHeaderBand") {
7415
+ reportHeaderControls = extractControls(bandItem);
7416
+ } else if (ctrlType === "PageHeaderBand") {
7417
+ pageHeaderControls = extractControls(bandItem);
7418
+ } else if (ctrlType === "DetailReportBand") {
7419
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7420
+ if (nestedBands) {
7421
+ for (const nested of Array.from(nestedBands.children)) {
7422
+ const nestedType = nested.getAttribute("ControlType") || "";
7423
+ if (nestedType === "DetailBand") {
7424
+ detailControls = extractControls(nested);
7425
+ } else if (nestedType === "GroupFooterBand") {
7426
+ groupFooterControls = extractControls(nested);
7427
+ }
7428
+ }
7429
+ }
7430
+ } else if (ctrlType === "ReportFooterBand") {
7431
+ reportFooterControls = extractControls(bandItem);
7432
+ }
7433
+ }
7434
+ if (topMarginControls.length > 0) {
7435
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7436
+ }
7437
+ if (reportHeaderControls.length > 0) {
7438
+ markdownSections.push(
7439
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7440
+ );
7441
+ }
7442
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7443
+ markdownSections.push(
7444
+ renderOperationsSection(
7445
+ pageHeaderControls,
7446
+ detailControls,
7447
+ groupFooterControls,
7448
+ registry)
7449
+ );
7450
+ }
7451
+ if (reportFooterControls.length > 0) {
7452
+ markdownSections.push(
7453
+ renderReportFooterBand(reportFooterControls, registry)
7454
+ );
7455
+ }
7456
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7457
+ return {
7458
+ markdown,
7459
+ variables: registry.getAll(),
7460
+ reportName,
7461
+ warnings
7462
+ };
7463
+ }
7464
+
4958
7465
  exports.DocumentGenerator = DocumentGenerator;
4959
7466
  exports.EditorPanel = EditorPanel;
4960
7467
  exports.FormFieldType = FormFieldType;
4961
7468
  exports.PreviewPanel = PreviewPanel;
7469
+ exports.RepeatNode = RepeatNode;
7470
+ exports.SubtotalsNode = SubtotalsNode;
4962
7471
  exports.ThemeProvider = ThemeProvider;
7472
+ exports.expandBlockVariables = expandBlockVariables;
7473
+ exports.expandRepeatContent = expandRepeatContent;
7474
+ exports.extractVariablesFromContent = extractVariablesFromContent;
7475
+ exports.generatePdfFromMarkdown = generatePdfFromMarkdown;
7476
+ exports.generatePdfFromTiptap = generatePdfFromTiptap;
7477
+ exports.isZeroLike = isZeroLike;
7478
+ exports.markdownToTiptap = markdownToTiptap;
7479
+ exports.parseXmlTemplate = parseXmlTemplate;
7480
+ exports.suppressZeroContent = suppressZeroContent;
7481
+ exports.tiptapToMarkdown = tiptapToMarkdown;
4963
7482
  exports.useDocumentGenerator = useDocumentGenerator;
4964
7483
  exports.useTheme = useTheme;
4965
7484
  //# sourceMappingURL=index.js.map