@signiphi/pdf-compose 0.1.0-beta.0 → 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 +3083 -564
  51. package/dist/index.js.map +1 -1
  52. package/dist/index.mjs +3070 -567
  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.mjs CHANGED
@@ -1,17 +1,21 @@
1
1
  import * as React12 from 'react';
2
2
  import React12__default, { createContext, useCallback, useState, useRef, useEffect, useMemo, useContext } from 'react';
3
- import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, Maximize2, Loader2, Plus, PanelLeftOpen, CheckCircle2, FileDown, PanelLeftClose, Undo, Redo, Bold, Italic, Underline as Underline$1, Code, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Minus, AlignLeft, AlignCenter, AlignRight, Trash2, X, PenLine, Upload, WrapText, Download } from 'lucide-react';
3
+ import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, Droplets, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, Maximize2, Loader2, Plus, Save, FileDown, CheckCircle2, PanelLeftOpen, PanelLeftClose, Undo, Redo, Bold, Italic, Underline as Underline$1, Code, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Minus, AlignLeft, AlignCenter, AlignRight, Trash2, X, PenLine, Upload, Search, WrapText, Download } from 'lucide-react';
4
4
  import { ReactNodeViewRenderer, NodeViewWrapper, useEditor, EditorContent } from '@tiptap/react';
5
5
  import StarterKit from '@tiptap/starter-kit';
6
6
  import Underline from '@tiptap/extension-underline';
7
7
  import TextAlign from '@tiptap/extension-text-align';
8
8
  import Placeholder from '@tiptap/extension-placeholder';
9
+ import Table from '@tiptap/extension-table';
10
+ import TableRow from '@tiptap/extension-table-row';
11
+ import TableCell from '@tiptap/extension-table-cell';
12
+ import TableHeader from '@tiptap/extension-table-header';
9
13
  import { Node, mergeAttributes } from '@tiptap/core';
10
14
  import { clsx } from 'clsx';
11
15
  import { twMerge } from 'tailwind-merge';
12
16
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
17
  import { v4 } from 'uuid';
14
- import { PDFDocument, StandardFonts, rgb, PDFName as PDFName$1, PDFString as PDFString$1 } from 'pdf-lib';
18
+ import { PDFDocument, StandardFonts, rgb, degrees, PDFName as PDFName$1, PDFString as PDFString$1 } from 'pdf-lib';
15
19
  import { NodeSelection } from '@tiptap/pm/state';
16
20
  import { Slot } from '@radix-ui/react-slot';
17
21
  import { cva } from 'class-variance-authority';
@@ -21,7 +25,7 @@ import * as LabelPrimitive from '@radix-ui/react-label';
21
25
 
22
26
  var __defProp = Object.defineProperty;
23
27
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
24
- var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
28
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
25
29
  function cn(...inputs) {
26
30
  return twMerge(clsx(inputs));
27
31
  }
@@ -213,7 +217,10 @@ var VariableNode = Node.create({
213
217
  return {
214
218
  varName: { default: "" },
215
219
  varLabel: { default: "" },
216
- varDefault: { default: "" }
220
+ varDefault: { default: "" },
221
+ format: { default: "" },
222
+ suppressZero: { default: "" },
223
+ block: { default: "" }
217
224
  };
218
225
  },
219
226
  parseHTML() {
@@ -279,6 +286,169 @@ var VariableNode = Node.create({
279
286
  };
280
287
  }
281
288
  });
289
+ var PanelNode = Node.create({
290
+ name: "panel",
291
+ group: "block",
292
+ content: "block+",
293
+ defining: true,
294
+ addAttributes() {
295
+ return {
296
+ title: { default: "" },
297
+ border: { default: "solid" },
298
+ headerStyle: { default: "" }
299
+ };
300
+ },
301
+ parseHTML() {
302
+ return [{
303
+ tag: "div[data-panel-node]",
304
+ contentElement: "div.panel-content"
305
+ }];
306
+ },
307
+ renderHTML({ node, HTMLAttributes }) {
308
+ const { title, border, headerStyle } = node.attrs;
309
+ const classes = ["panel-node"];
310
+ if (border === "dashed") classes.push("panel-border-dashed");
311
+ else if (border === "none") classes.push("panel-border-none");
312
+ else classes.push("panel-border-solid");
313
+ if (headerStyle === "dark") classes.push("panel-dark");
314
+ const attrs = mergeAttributes(
315
+ { "data-panel-node": "", class: classes.join(" ") },
316
+ HTMLAttributes
317
+ );
318
+ if (title) {
319
+ return [
320
+ "div",
321
+ attrs,
322
+ ["div", { class: "panel-title" }, title],
323
+ ["div", { class: "panel-content" }, 0]
324
+ ];
325
+ }
326
+ return ["div", attrs, ["div", { class: "panel-content" }, 0]];
327
+ }
328
+ });
329
+ var ColumnsNode = Node.create({
330
+ name: "columns",
331
+ group: "block",
332
+ content: "column+",
333
+ defining: true,
334
+ addAttributes() {
335
+ return {
336
+ split: { default: "50" },
337
+ padX: { default: "0" }
338
+ };
339
+ },
340
+ parseHTML() {
341
+ return [{ tag: "div[data-columns-node]" }];
342
+ },
343
+ renderHTML({ node, HTMLAttributes }) {
344
+ const split = node.attrs.split || "50";
345
+ return ["div", mergeAttributes(
346
+ {
347
+ "data-columns-node": "",
348
+ class: "columns-node",
349
+ style: `--xpc-col-split: ${split}`
350
+ },
351
+ HTMLAttributes
352
+ ), 0];
353
+ }
354
+ });
355
+ var ColumnNode = Node.create({
356
+ name: "column",
357
+ group: "",
358
+ content: "block+",
359
+ defining: true,
360
+ addAttributes() {
361
+ return {
362
+ padTop: { default: 0 }
363
+ };
364
+ },
365
+ parseHTML() {
366
+ return [{ tag: "div[data-column-node]" }];
367
+ },
368
+ renderHTML({ HTMLAttributes }) {
369
+ return ["div", mergeAttributes(
370
+ { "data-column-node": "", class: "column-node" },
371
+ HTMLAttributes
372
+ ), 0];
373
+ }
374
+ });
375
+ function WatermarkNodeView({ node, selected }) {
376
+ const { text } = node.attrs;
377
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsxs(
378
+ "div",
379
+ {
380
+ "data-watermark-node": "",
381
+ className: cn(
382
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
383
+ "bg-blue-50 text-blue-600 border-blue-200",
384
+ selected && "ring-2 ring-primary ring-offset-1"
385
+ ),
386
+ children: [
387
+ /* @__PURE__ */ jsx(Droplets, { size: 14 }),
388
+ /* @__PURE__ */ jsxs("span", { children: [
389
+ "Watermark: ",
390
+ text || "(empty)"
391
+ ] })
392
+ ]
393
+ }
394
+ ) });
395
+ }
396
+
397
+ // src/extensions/watermark-node.ts
398
+ var WatermarkNode = Node.create({
399
+ name: "watermark",
400
+ group: "block",
401
+ atom: true,
402
+ selectable: true,
403
+ draggable: false,
404
+ addAttributes() {
405
+ return {
406
+ text: { default: "" },
407
+ opacity: { default: "0.15" },
408
+ angle: { default: "-45" }
409
+ };
410
+ },
411
+ parseHTML() {
412
+ return [{ tag: "div[data-watermark-node]" }];
413
+ },
414
+ renderHTML({ HTMLAttributes }) {
415
+ return ["div", mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
416
+ },
417
+ addNodeView() {
418
+ return ReactNodeViewRenderer(WatermarkNodeView);
419
+ }
420
+ });
421
+ var RepeatNode = Node.create({
422
+ name: "repeatBlock",
423
+ group: "block",
424
+ content: "block+",
425
+ addAttributes() {
426
+ return {
427
+ data: { default: "" }
428
+ };
429
+ },
430
+ parseHTML() {
431
+ return [{ tag: "div[data-repeat-node]" }];
432
+ },
433
+ renderHTML({ HTMLAttributes }) {
434
+ return ["div", mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
435
+ }
436
+ });
437
+ var SubtotalsNode = Node.create({
438
+ name: "subtotalsBlock",
439
+ group: "block",
440
+ content: "block+",
441
+ defining: true,
442
+ parseHTML() {
443
+ return [{ tag: "div[data-subtotals-node]" }];
444
+ },
445
+ renderHTML({ HTMLAttributes }) {
446
+ return ["div", mergeAttributes(
447
+ { "data-subtotals-node": "", class: "subtotals-block" },
448
+ HTMLAttributes
449
+ ), 0];
450
+ }
451
+ });
282
452
 
283
453
  // src/types/index.ts
284
454
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
@@ -357,158 +527,23 @@ function extractFieldsFromContent(content) {
357
527
  }
358
528
  return fields;
359
529
  }
360
-
361
- // src/utils/variable-helpers.ts
362
- function extractVariablesFromContent(content) {
363
- const seen = /* @__PURE__ */ new Map();
364
- function walk(node) {
365
- if (node.type === "variableNode" && node.attrs) {
366
- const varName = node.attrs.varName;
367
- if (varName && !seen.has(varName)) {
368
- seen.set(varName, {
369
- varName,
370
- varLabel: node.attrs.varLabel || varName,
371
- varDefault: node.attrs.varDefault || ""
372
- });
373
- }
374
- }
375
- if (node.content) {
376
- node.content.forEach(walk);
377
- }
378
- }
379
- if (content.content) {
380
- content.content.forEach(walk);
381
- }
382
- return Array.from(seen.values());
383
- }
384
- function replaceVariablesInContent(content, values) {
385
- function walkNode(node) {
386
- if (node.type === "variableNode" && node.attrs) {
387
- const varName = node.attrs.varName;
388
- const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || varName}]`;
389
- const textNode = { type: "text", text: replacement };
390
- if (node.marks && node.marks.length > 0) {
391
- textNode.marks = node.marks;
392
- }
393
- return textNode;
394
- }
395
- if (node.content) {
396
- return { ...node, content: node.content.map(walkNode) };
397
- }
398
- return node;
399
- }
400
- return walkNode(content);
401
- }
402
-
403
- // src/utils/markdown-writer.ts
404
- function serializeFieldToken(attrs) {
405
- const parts = ["field"];
406
- if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
407
- if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
408
- if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
409
- if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
410
- if (attrs.required) parts.push(`required:true`);
411
- if (attrs.options) parts.push(`options:${attrs.options}`);
412
- if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
413
- if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
414
- if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
415
- if (attrs.multiline) parts.push(`multiline:true`);
416
- if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
417
- if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
418
- return `{{${parts.join("|")}}}`;
419
- }
420
- function serializeVariableToken(attrs) {
421
- const parts = ["var"];
422
- if (attrs.varName) parts.push(`name:${attrs.varName}`);
423
- if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
424
- if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
425
- return `{{${parts.join("|")}}}`;
426
- }
427
- function serializeInline(content) {
428
- if (!content) return "";
429
- let result = "";
430
- for (const node of content) {
431
- if (node.type === "fieldNode") {
432
- result += serializeFieldToken(node.attrs || {});
433
- continue;
434
- }
435
- if (node.type === "variableNode") {
436
- let token = serializeVariableToken(node.attrs || {});
437
- const varMarks = node.marks || [];
438
- const isBold = varMarks.some((m) => m.type === "bold");
439
- const isItalic = varMarks.some((m) => m.type === "italic");
440
- const isUnderline = varMarks.some((m) => m.type === "underline");
441
- if (isBold && isItalic) token = `***${token}***`;
442
- else if (isBold) token = `**${token}**`;
443
- else if (isItalic) token = `*${token}*`;
444
- if (isUnderline) token = `__${token}__`;
445
- result += token;
446
- continue;
447
- }
448
- if (node.type === "text") {
449
- let text = node.text || "";
450
- const marks = node.marks || [];
451
- for (const mark of marks) {
452
- if (mark.type === "bold") text = `**${text}**`;
453
- else if (mark.type === "italic") text = `*${text}*`;
454
- else if (mark.type === "underline") text = `__${text}__`;
455
- else if (mark.type === "code") text = `\`${text}\``;
456
- }
457
- result += text;
458
- }
459
- if (node.type === "hardBreak") {
460
- result += " \n";
461
- }
462
- }
463
- return result;
464
- }
465
- function serializeBlock(node) {
466
- switch (node.type) {
467
- case "heading": {
468
- const level = node.attrs?.level || 1;
469
- const prefix = "#".repeat(level);
470
- return `${prefix} ${serializeInline(node.content)}`;
471
- }
472
- case "paragraph": {
473
- const inline = serializeInline(node.content);
474
- return inline;
475
- }
476
- case "bulletList": {
477
- return (node.content || []).map((item) => {
478
- const inner = (item.content || []).map(serializeBlock).join("\n");
479
- return `- ${inner}`;
480
- }).join("\n");
481
- }
482
- case "orderedList": {
483
- return (node.content || []).map((item, i) => {
484
- const inner = (item.content || []).map(serializeBlock).join("\n");
485
- return `${i + 1}. ${inner}`;
486
- }).join("\n");
487
- }
488
- case "listItem": {
489
- return (node.content || []).map(serializeBlock).join("\n");
490
- }
491
- case "blockquote": {
492
- return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
493
- }
494
- case "codeBlock": {
495
- const text = serializeInline(node.content);
496
- return `\`\`\`
497
- ${text}
498
- \`\`\``;
499
- }
500
- case "horizontalRule": {
501
- return "---";
502
- }
503
- default:
504
- return serializeInline(node.content);
530
+ var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
531
+ var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
532
+ var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
533
+ var DIRECTIVE_CLOSE = /^:::$/;
534
+ function parseDirectiveAttrs(str) {
535
+ const attrs = {};
536
+ if (!str) return attrs;
537
+ const pairs = str.split("|");
538
+ for (const pair of pairs) {
539
+ const colonIdx = pair.indexOf(":");
540
+ if (colonIdx === -1) continue;
541
+ const key = pair.slice(0, colonIdx).trim();
542
+ const value = pair.slice(colonIdx + 1).trim();
543
+ if (key) attrs[key] = value;
505
544
  }
545
+ return attrs;
506
546
  }
507
- function tiptapToMarkdown(doc) {
508
- if (!doc.content) return "";
509
- return doc.content.map(serializeBlock).join("\n\n");
510
- }
511
- var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
512
547
  function injectMarks(token, marks) {
513
548
  const existingMatch = token.match(/\|_marks:([^}|]+)/);
514
549
  if (existingMatch) {
@@ -625,6 +660,15 @@ function parseVariableToken(tokenBody) {
625
660
  case "default":
626
661
  attrs.varDefault = value;
627
662
  break;
663
+ case "suppress":
664
+ attrs.suppressZero = value === "zero" ? "true" : "";
665
+ break;
666
+ case "format":
667
+ attrs.format = value;
668
+ break;
669
+ case "block":
670
+ attrs.block = value === "true" ? "true" : "";
671
+ break;
628
672
  case "_marks":
629
673
  attrs._marks = value;
630
674
  break;
@@ -632,6 +676,44 @@ function parseVariableToken(tokenBody) {
632
676
  }
633
677
  return attrs;
634
678
  }
679
+ function parseImageToken(tokenBody) {
680
+ const attrs = {
681
+ src: "",
682
+ varName: "",
683
+ alt: "",
684
+ width: "",
685
+ height: "",
686
+ align: ""
687
+ };
688
+ const pairs = tokenBody.split("|");
689
+ for (const pair of pairs) {
690
+ const colonIdx = pair.indexOf(":");
691
+ if (colonIdx === -1) continue;
692
+ const key = pair.slice(0, colonIdx).trim();
693
+ const value = pair.slice(colonIdx + 1).trim();
694
+ switch (key) {
695
+ case "src":
696
+ attrs.src = value;
697
+ break;
698
+ case "var":
699
+ attrs.varName = value;
700
+ break;
701
+ case "alt":
702
+ attrs.alt = value;
703
+ break;
704
+ case "width":
705
+ attrs.width = value;
706
+ break;
707
+ case "height":
708
+ attrs.height = value;
709
+ break;
710
+ case "align":
711
+ attrs.align = value;
712
+ break;
713
+ }
714
+ }
715
+ return attrs;
716
+ }
635
717
  function parseInline(text) {
636
718
  text = preprocessMarkedTokens(text);
637
719
  const nodes = [];
@@ -655,6 +737,8 @@ function parseInline(text) {
655
737
  const node = { type: "variableNode", attrs };
656
738
  if (marks.length > 0) node.marks = marks;
657
739
  nodes.push(node);
740
+ } else if (match[1] === "image") {
741
+ nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
658
742
  }
659
743
  lastIndex = match.index + match[0].length;
660
744
  }
@@ -666,45 +750,63 @@ function parseInline(text) {
666
750
  function parseFormattedText(text) {
667
751
  if (!text) return [];
668
752
  const nodes = [];
669
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
753
+ const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
670
754
  for (const part of parts) {
671
755
  if (!part) continue;
672
- if (part.startsWith("**") && part.endsWith("**")) {
673
- nodes.push({
674
- type: "text",
675
- text: part.slice(2, -2),
676
- marks: [{ type: "bold" }]
677
- });
756
+ if (part.startsWith("***") && part.endsWith("***")) {
757
+ const inner = part.slice(3, -3);
758
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
759
+ } else if (part.startsWith("**") && part.endsWith("**")) {
760
+ const inner = part.slice(2, -2);
761
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
678
762
  } else if (part.startsWith("*") && part.endsWith("*")) {
679
- nodes.push({
680
- type: "text",
681
- text: part.slice(1, -1),
682
- marks: [{ type: "italic" }]
683
- });
763
+ const inner = part.slice(1, -1);
764
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
684
765
  } else if (part.startsWith("__") && part.endsWith("__")) {
685
- nodes.push({
686
- type: "text",
687
- text: part.slice(2, -2),
688
- marks: [{ type: "underline" }]
689
- });
766
+ const inner = part.slice(2, -2);
767
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
690
768
  } else if (part.startsWith("`") && part.endsWith("`")) {
691
- nodes.push({
692
- type: "text",
693
- text: part.slice(1, -1),
694
- marks: [{ type: "code" }]
695
- });
769
+ const inner = part.slice(1, -1);
770
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
696
771
  } else {
697
772
  nodes.push({ type: "text", text: part });
698
773
  }
699
774
  }
700
775
  return nodes;
701
776
  }
702
- function markdownToTiptap(markdown) {
703
- const lines = markdown.split("\n");
704
- const content = [];
705
- let i = 0;
706
- while (i < lines.length) {
707
- const line = lines[i];
777
+ function splitTableCells(row) {
778
+ const cells = [];
779
+ let current = "";
780
+ let depth = 0;
781
+ for (let i = 0; i < row.length; i++) {
782
+ if (row[i] === "{" && row[i + 1] === "{") {
783
+ depth++;
784
+ current += "{{";
785
+ i++;
786
+ } else if (row[i] === "}" && row[i + 1] === "}") {
787
+ depth = Math.max(0, depth - 1);
788
+ current += "}}";
789
+ i++;
790
+ } else if (row[i] === "|" && depth === 0) {
791
+ cells.push(current.trim());
792
+ current = "";
793
+ } else {
794
+ current += row[i];
795
+ }
796
+ }
797
+ if (current.trim()) {
798
+ cells.push(current.trim());
799
+ }
800
+ return cells;
801
+ }
802
+ function parseBlocks(lines, startIdx, stopCondition) {
803
+ const content = [];
804
+ let i = startIdx;
805
+ while (i < lines.length) {
806
+ const line = lines[i];
807
+ if (stopCondition && stopCondition(line)) {
808
+ break;
809
+ }
708
810
  if (line.trim() === "") {
709
811
  i++;
710
812
  continue;
@@ -778,13 +880,431 @@ function markdownToTiptap(markdown) {
778
880
  });
779
881
  continue;
780
882
  }
781
- content.push({
782
- type: "paragraph",
783
- content: parseInline(line)
784
- });
883
+ if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
884
+ const tableLines = [];
885
+ while (i < lines.length) {
886
+ const tl = lines[i];
887
+ if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
888
+ tableLines.push(tl);
889
+ i++;
890
+ } else {
891
+ break;
892
+ }
893
+ }
894
+ const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
895
+ const dataLines = tableLines.filter(
896
+ (tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
897
+ );
898
+ const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
899
+ const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
900
+ const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
901
+ const tableRows = [];
902
+ let isFirstDataRow = true;
903
+ for (const tl of tableLines) {
904
+ if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
905
+ const cells = splitTableCells(tl).filter((c) => c !== "");
906
+ if (isBorderless && isFirstDataRow) {
907
+ isFirstDataRow = false;
908
+ continue;
909
+ }
910
+ const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
911
+ const rowContent = cells.map((cellText) => ({
912
+ type: cellType,
913
+ content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
914
+ }));
915
+ tableRows.push({ type: "tableRow", content: rowContent });
916
+ isFirstDataRow = false;
917
+ }
918
+ if (tableRows.length > 0) {
919
+ const tableNode = { type: "table", content: tableRows };
920
+ if (isBorderless) {
921
+ tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
922
+ }
923
+ content.push(tableNode);
924
+ }
925
+ continue;
926
+ }
927
+ const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
928
+ if (directiveMatch) {
929
+ const directiveType = directiveMatch[1];
930
+ const rawAttrs = directiveMatch[2] || "";
931
+ const attrs = parseDirectiveAttrs(rawAttrs);
932
+ i++;
933
+ if (directiveType === "watermark") {
934
+ content.push({
935
+ type: "watermark",
936
+ attrs: {
937
+ text: attrs.text || "",
938
+ opacity: attrs.opacity || "0.15",
939
+ angle: attrs.angle || "-45"
940
+ }
941
+ });
942
+ continue;
943
+ }
944
+ if (directiveType === "panel") {
945
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
946
+ i = inner.nextIdx;
947
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
948
+ i++;
949
+ }
950
+ content.push({
951
+ type: "panel",
952
+ attrs: {
953
+ title: attrs.title || "",
954
+ border: attrs.border || "solid",
955
+ headerStyle: attrs.headerStyle || ""
956
+ },
957
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
958
+ });
959
+ continue;
960
+ }
961
+ if (directiveType === "repeat") {
962
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
963
+ i = inner.nextIdx;
964
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
965
+ i++;
966
+ }
967
+ content.push({
968
+ type: "repeatBlock",
969
+ attrs: { data: attrs.data || "" },
970
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
971
+ });
972
+ continue;
973
+ }
974
+ if (directiveType === "subtotals") {
975
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
976
+ i = inner.nextIdx;
977
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
978
+ i++;
979
+ }
980
+ content.push({
981
+ type: "subtotalsBlock",
982
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
983
+ });
984
+ continue;
985
+ }
986
+ if (directiveType === "columns") {
987
+ const splitVal = attrs.split || "50";
988
+ const columns = [];
989
+ while (i < lines.length) {
990
+ const colLine = lines[i].trim();
991
+ if (DIRECTIVE_CLOSE.test(colLine)) {
992
+ if (columns.length > 0) {
993
+ i++;
994
+ break;
995
+ }
996
+ i++;
997
+ break;
998
+ }
999
+ const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
1000
+ if (colMatch) {
1001
+ i++;
1002
+ const colInner = parseBlocks(lines, i, (l) => {
1003
+ const trimmed = l.trim();
1004
+ return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
1005
+ });
1006
+ i = colInner.nextIdx;
1007
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1008
+ i++;
1009
+ }
1010
+ const colAttrsRaw = colMatch[1] || "";
1011
+ const colAttrs = parseDirectiveAttrs(colAttrsRaw);
1012
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
1013
+ columns.push({
1014
+ type: "column",
1015
+ attrs: padTop ? { padTop } : void 0,
1016
+ content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
1017
+ });
1018
+ continue;
1019
+ }
1020
+ if (colLine === "") {
1021
+ i++;
1022
+ continue;
1023
+ }
1024
+ break;
1025
+ }
1026
+ const columnsAttrs = { split: splitVal };
1027
+ if (attrs.padX) columnsAttrs.padX = attrs.padX;
1028
+ content.push({
1029
+ type: "columns",
1030
+ attrs: columnsAttrs,
1031
+ content: columns.length > 0 ? columns : [
1032
+ { type: "column", content: [{ type: "paragraph" }] },
1033
+ { type: "column", content: [{ type: "paragraph" }] }
1034
+ ]
1035
+ });
1036
+ continue;
1037
+ }
1038
+ if (directiveType === "col") {
1039
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1040
+ i = inner.nextIdx;
1041
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1042
+ i++;
1043
+ }
1044
+ for (const block of inner.blocks) {
1045
+ content.push(block);
1046
+ }
1047
+ continue;
1048
+ }
1049
+ continue;
1050
+ }
1051
+ if (DIRECTIVE_CLOSE.test(line.trim())) {
1052
+ i++;
1053
+ continue;
1054
+ }
1055
+ const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
1056
+ if (imageMatch) {
1057
+ content.push({
1058
+ type: "imageNode",
1059
+ attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
1060
+ });
1061
+ i++;
1062
+ continue;
1063
+ }
1064
+ const inlineNodes = parseInline(line);
1065
+ const nonWhitespace = inlineNodes.filter(
1066
+ (n) => !(n.type === "text" && !(n.text || "").trim())
1067
+ );
1068
+ if (nonWhitespace.length === 1 && nonWhitespace[0].type === "imageNode") {
1069
+ content.push(nonWhitespace[0]);
1070
+ } else {
1071
+ content.push({ type: "paragraph", content: inlineNodes });
1072
+ }
785
1073
  i++;
786
1074
  }
787
- return { type: "doc", content };
1075
+ return { blocks: content, nextIdx: i };
1076
+ }
1077
+ function markdownToTiptap(markdown) {
1078
+ const lines = markdown.split("\n");
1079
+ const { blocks } = parseBlocks(lines, 0);
1080
+ return { type: "doc", content: blocks };
1081
+ }
1082
+
1083
+ // src/utils/variable-helpers.ts
1084
+ function extractVariablesFromContent(content) {
1085
+ const seen = /* @__PURE__ */ new Map();
1086
+ function walk(node) {
1087
+ if (node.type === "variableNode" && node.attrs) {
1088
+ const varName = node.attrs.varName;
1089
+ if (varName && !seen.has(varName)) {
1090
+ seen.set(varName, {
1091
+ varName,
1092
+ varLabel: node.attrs.varLabel || varName,
1093
+ varDefault: node.attrs.varDefault || ""
1094
+ });
1095
+ }
1096
+ }
1097
+ if (node.content) {
1098
+ node.content.forEach(walk);
1099
+ }
1100
+ }
1101
+ if (content.content) {
1102
+ content.content.forEach(walk);
1103
+ }
1104
+ return Array.from(seen.values());
1105
+ }
1106
+ function applyFormat(value, format) {
1107
+ if (format === "phone") {
1108
+ const digits = value.replace(/\D/g, "");
1109
+ if (digits.length === 10) {
1110
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
1111
+ }
1112
+ }
1113
+ return value;
1114
+ }
1115
+ function replaceVariablesInContent(content, values) {
1116
+ function walkNode(node) {
1117
+ if (node.type === "variableNode" && node.attrs) {
1118
+ const varName = node.attrs.varName;
1119
+ let replacement = values[varName] || node.attrs.varDefault || "";
1120
+ const format = node.attrs.format;
1121
+ if (format && replacement) {
1122
+ replacement = applyFormat(replacement, format);
1123
+ }
1124
+ const textNode = { type: "text", text: replacement };
1125
+ if (node.marks && node.marks.length > 0) {
1126
+ textNode.marks = node.marks;
1127
+ }
1128
+ return textNode;
1129
+ }
1130
+ if (node.type === "imageNode" && node.attrs?.varName) {
1131
+ const varName = node.attrs.varName;
1132
+ const resolved = values[varName] || node.attrs.src || "";
1133
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
1134
+ }
1135
+ if (node.content) {
1136
+ return { ...node, content: node.content.map(walkNode) };
1137
+ }
1138
+ return node;
1139
+ }
1140
+ return walkNode(content);
1141
+ }
1142
+ function expandBlockVariables(content, values) {
1143
+ function soleVariableOf(node) {
1144
+ if (node.type !== "paragraph" || !node.content) return null;
1145
+ const meaningful = node.content.filter(
1146
+ (c) => !(c.type === "text" && !(c.text || "").trim())
1147
+ );
1148
+ if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
1149
+ return meaningful[0];
1150
+ }
1151
+ return null;
1152
+ }
1153
+ function stripUnsafeNodes(node) {
1154
+ if (!node.content) return node;
1155
+ return {
1156
+ ...node,
1157
+ content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
1158
+ };
1159
+ }
1160
+ function isCellSafe(blocks) {
1161
+ return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
1162
+ }
1163
+ function walkNode(node, inCell) {
1164
+ if (!node.content) return node;
1165
+ const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
1166
+ const newContent = [];
1167
+ for (const child of node.content) {
1168
+ const varNode = soleVariableOf(child);
1169
+ if (varNode?.attrs) {
1170
+ const varName = varNode.attrs.varName;
1171
+ const value = values[varName] || varNode.attrs.varDefault || "";
1172
+ const isBlock = varNode.attrs.block === "true" || value.includes("\n");
1173
+ if (isBlock) {
1174
+ const parsed = stripUnsafeNodes(markdownToTiptap(value));
1175
+ const blocks = parsed.content || [];
1176
+ if (!childInCell || isCellSafe(blocks)) {
1177
+ newContent.push(...blocks);
1178
+ continue;
1179
+ }
1180
+ }
1181
+ }
1182
+ newContent.push(walkNode(child, childInCell));
1183
+ }
1184
+ return { ...node, content: newContent };
1185
+ }
1186
+ return walkNode(content, false);
1187
+ }
1188
+ function labelToVarName(label) {
1189
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1190
+ }
1191
+ function isZeroLike(value) {
1192
+ if (!value || value.trim() === "") return true;
1193
+ const cleaned = value.replace(/[$,\s]/g, "");
1194
+ return /^-?0+(\.0+)?$/.test(cleaned);
1195
+ }
1196
+ function suppressZeroContent(content, values) {
1197
+ function hasSuppressedVar(node) {
1198
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
1199
+ const varName = node.attrs.varName;
1200
+ const val = values[varName] || node.attrs.varDefault || "";
1201
+ return isZeroLike(val);
1202
+ }
1203
+ return (node.content || []).some(hasSuppressedVar);
1204
+ }
1205
+ function walkNode(node) {
1206
+ if (node.type === "table" && node.content) {
1207
+ const filtered = node.content.filter((row) => {
1208
+ if (row.type !== "tableRow") return true;
1209
+ const isHeader = row.content?.[0]?.type === "tableHeader";
1210
+ if (isHeader) return true;
1211
+ return !hasSuppressedVar(row);
1212
+ });
1213
+ const bodyRows = filtered.filter(
1214
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
1215
+ );
1216
+ if (bodyRows.length === 0) return null;
1217
+ return { ...node, content: filtered };
1218
+ }
1219
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
1220
+ return null;
1221
+ }
1222
+ if (node.content) {
1223
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
1224
+ return { ...node, content: filtered };
1225
+ }
1226
+ return node;
1227
+ }
1228
+ return walkNode(content) || content;
1229
+ }
1230
+ function expandRepeatContent(content, values) {
1231
+ const enrichedValues = { ...values };
1232
+ function cloneNode(node, dataKey, index) {
1233
+ if (node.type === "variableNode" && node.attrs?.varName) {
1234
+ const oldName = node.attrs.varName;
1235
+ const bareName = oldName.replace(/^operation_/, "");
1236
+ const newName = `${dataKey}_${index}_${bareName}`;
1237
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
1238
+ }
1239
+ if (node.content) {
1240
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
1241
+ }
1242
+ return { ...node };
1243
+ }
1244
+ function walkNode(node) {
1245
+ if (node.type === "repeatBlock" && node.attrs?.data) {
1246
+ const dataKey = node.attrs.data;
1247
+ const arrayJson = values[dataKey];
1248
+ if (!arrayJson) return [];
1249
+ let items;
1250
+ try {
1251
+ items = JSON.parse(arrayJson);
1252
+ } catch {
1253
+ return [];
1254
+ }
1255
+ const sums = /* @__PURE__ */ new Map();
1256
+ for (const item of items) {
1257
+ for (const [key, val] of Object.entries(item)) {
1258
+ const num = parseFloat(val);
1259
+ if (!isNaN(num)) {
1260
+ sums.set(key, (sums.get(key) || 0) + num);
1261
+ }
1262
+ }
1263
+ }
1264
+ const woKeysByNorm = /* @__PURE__ */ new Map();
1265
+ for (const k of Object.keys(enrichedValues)) {
1266
+ if (k.startsWith("workorder_")) {
1267
+ const norm = k.replace(/_/g, "");
1268
+ woKeysByNorm.set(norm, k);
1269
+ }
1270
+ }
1271
+ for (const [field, total] of sums) {
1272
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
1273
+ const directKey = `workorder_${field}`;
1274
+ enrichedValues[directKey] = formatted;
1275
+ const norm = `workorder${field}`.replace(/_/g, "");
1276
+ const existing = woKeysByNorm.get(norm);
1277
+ if (existing && existing !== directKey) {
1278
+ enrichedValues[existing] = formatted;
1279
+ }
1280
+ }
1281
+ const expanded = [];
1282
+ for (let i = 0; i < items.length; i++) {
1283
+ const item = items[i];
1284
+ for (const [key, val] of Object.entries(item)) {
1285
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
1286
+ }
1287
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
1288
+ expanded.push(...cloned);
1289
+ }
1290
+ return expanded;
1291
+ }
1292
+ if (node.content) {
1293
+ const newContent = [];
1294
+ for (const child of node.content) {
1295
+ const result2 = walkNode(child);
1296
+ if (Array.isArray(result2)) newContent.push(...result2);
1297
+ else newContent.push(result2);
1298
+ }
1299
+ return { ...node, content: newContent };
1300
+ }
1301
+ return node;
1302
+ }
1303
+ const result = walkNode(content);
1304
+ return {
1305
+ content: Array.isArray(result) ? { ...content, content: result } : result,
1306
+ values: enrichedValues
1307
+ };
788
1308
  }
789
1309
 
790
1310
  // src/utils/error-helpers.ts
@@ -922,10 +1442,40 @@ function buildMetadataObject(fields, actualFieldNames) {
922
1442
  // src/utils/pdf-generator.ts
923
1443
  var PAGE_WIDTH = 595.28;
924
1444
  var PAGE_HEIGHT = 841.89;
925
- var MARGIN = 72;
1445
+ var MARGIN = 40;
926
1446
  var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
927
1447
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
928
1448
  var LINE_HEIGHT_FACTOR = 1.4;
1449
+ var BODY_FONT_SIZE = 10;
1450
+ function hexToRgbColor(hex) {
1451
+ if (!hex) return null;
1452
+ const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
1453
+ if (!m) return null;
1454
+ const n = parseInt(m[1], 16);
1455
+ return rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
1456
+ }
1457
+ function contrastTextColor(fill) {
1458
+ const c = fill;
1459
+ const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
1460
+ return luminance > 0.6 ? rgb(0, 0, 0) : rgb(1, 1, 1);
1461
+ }
1462
+ function resolveTheme(theme) {
1463
+ const primary = hexToRgbColor(theme?.primaryColor);
1464
+ const accent = hexToRgbColor(theme?.accentColor);
1465
+ const border = hexToRgbColor(theme?.borderColor);
1466
+ return {
1467
+ tableHeaderBg: primary ?? rgb(0.3, 0.3, 0.3),
1468
+ tableHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
1469
+ tableBorder: border ?? rgb(0.75, 0.75, 0.75),
1470
+ panelDarkHeaderBg: primary ?? rgb(0.25, 0.25, 0.25),
1471
+ panelDarkHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
1472
+ panelBorder: border ?? rgb(0.6, 0.6, 0.6),
1473
+ rule: primary ?? rgb(0, 0, 0),
1474
+ pageHeaderText: primary ?? rgb(0, 0, 0),
1475
+ pageHeaderRule: accent ?? primary ?? rgb(0.75, 0.75, 0.75)
1476
+ };
1477
+ }
1478
+ var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
929
1479
  var FIELD_DISPLAY = {
930
1480
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
931
1481
  ["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
@@ -956,17 +1506,65 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
956
1506
  const height = textLineHeight + 4;
957
1507
  return { width, height };
958
1508
  }
1509
+ async function loadImageBytes(src) {
1510
+ if (src.startsWith("data:")) {
1511
+ const comma = src.indexOf(",");
1512
+ if (comma === -1) throw new Error("malformed data URL");
1513
+ const meta = src.slice(0, comma);
1514
+ if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
1515
+ const bin = atob(src.slice(comma + 1));
1516
+ const bytes = new Uint8Array(bin.length);
1517
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1518
+ return bytes;
1519
+ }
1520
+ const res = await fetch(src);
1521
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1522
+ return new Uint8Array(await res.arrayBuffer());
1523
+ }
1524
+ function describeImageSrc(src) {
1525
+ return src.length > 80 ? `${src.slice(0, 77)}...` : src;
1526
+ }
1527
+ async function preloadImages(pdfDoc, content) {
1528
+ const srcs = /* @__PURE__ */ new Set();
1529
+ (function walk(node) {
1530
+ if (node.type === "imageNode") {
1531
+ const src = node.attrs?.src || "";
1532
+ if (src) srcs.add(src);
1533
+ }
1534
+ for (const child of node.content || []) walk(child);
1535
+ })(content);
1536
+ const images = /* @__PURE__ */ new Map();
1537
+ const warnings = [];
1538
+ for (const src of srcs) {
1539
+ try {
1540
+ const bytes = await loadImageBytes(src);
1541
+ const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
1542
+ const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
1543
+ if (isPng) {
1544
+ images.set(src, await pdfDoc.embedPng(bytes));
1545
+ } else if (isJpeg) {
1546
+ images.set(src, await pdfDoc.embedJpg(bytes));
1547
+ } else {
1548
+ warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
1549
+ }
1550
+ } catch (err) {
1551
+ warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
1552
+ }
1553
+ }
1554
+ return { images, warnings };
1555
+ }
959
1556
  function ensureSpace(state, neededHeight) {
960
1557
  if (state.y + neededHeight > CONTENT_HEIGHT) {
961
1558
  const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
962
1559
  state.currentPage = newPage;
963
1560
  state.pageIndex++;
964
1561
  state.y = 0;
1562
+ renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
965
1563
  }
966
1564
  }
967
- function drawText(state, text, font, fontSize, indent = 0) {
1565
+ function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
968
1566
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
969
- const maxWidth = CONTENT_WIDTH - indent;
1567
+ const maxWidth = region.width - indent;
970
1568
  const words = text.split(/\s+/);
971
1569
  let line = "";
972
1570
  let totalAdvance = 0;
@@ -977,7 +1575,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
977
1575
  ensureSpace(state, lineHeight);
978
1576
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
979
1577
  state.currentPage.drawText(line, {
980
- x: MARGIN + indent,
1578
+ x: region.leftX + indent,
981
1579
  y: pdfY,
982
1580
  size: fontSize,
983
1581
  font,
@@ -994,7 +1592,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
994
1592
  ensureSpace(state, lineHeight);
995
1593
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
996
1594
  state.currentPage.drawText(line, {
997
- x: MARGIN + indent,
1595
+ x: region.leftX + indent,
998
1596
  y: pdfY,
999
1597
  size: fontSize,
1000
1598
  font,
@@ -1038,10 +1636,10 @@ function collectInlineSegments(content, fonts, fontSize) {
1038
1636
  }
1039
1637
  return segments;
1040
1638
  }
1041
- function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1639
+ function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
1042
1640
  const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
1043
- const maxX = MARGIN + CONTENT_WIDTH;
1044
- const startX = MARGIN + indent;
1641
+ const maxX = region.leftX + region.width;
1642
+ const startX = region.leftX + indent;
1045
1643
  let currentX = startX;
1046
1644
  let currentLineHeight = 0;
1047
1645
  let lineHasContent = false;
@@ -1146,90 +1744,395 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1146
1744
  }
1147
1745
  flushLine();
1148
1746
  }
1149
- function processInlineContent(state, content, fontSize, indent = 0) {
1747
+ function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
1150
1748
  if (!content) return;
1151
1749
  const segments = collectInlineSegments(content, state.fonts, fontSize);
1152
1750
  if (segments.length === 0) return;
1153
- layoutInlineSegments(state, segments, fontSize, indent);
1751
+ layoutInlineSegments(state, segments, fontSize, indent, region);
1154
1752
  }
1155
- function processBlock(state, node) {
1156
- switch (node.type) {
1157
- case "heading": {
1158
- const level = node.attrs?.level || 1;
1159
- const fontSizes = { 1: 24, 2: 20, 3: 16 };
1160
- const fontSize = fontSizes[level] || 16;
1161
- const spacing = fontSize * 0.6;
1162
- ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR + spacing);
1163
- state.y += spacing / 2;
1164
- if (!node.content) break;
1165
- const headingFonts = {
1166
- regular: state.fonts.bold,
1167
- bold: state.fonts.bold,
1168
- italic: state.fonts.boldItalic,
1169
- boldItalic: state.fonts.boldItalic
1170
- };
1171
- const segments = collectInlineSegments(node.content, headingFonts, fontSize);
1172
- layoutInlineSegments(state, segments, fontSize);
1173
- state.y += spacing / 2;
1174
- break;
1175
- }
1176
- case "paragraph": {
1177
- const fontSize = 12;
1178
- if (!node.content || node.content.length === 0) {
1179
- state.y += fontSize * LINE_HEIGHT_FACTOR;
1180
- break;
1753
+ function extractCellText(cell) {
1754
+ const parts = [];
1755
+ for (const child of cell.content || []) {
1756
+ if (child.type === "paragraph" || child.type === "heading") {
1757
+ for (const inline of child.content || []) {
1758
+ if (inline.type === "text") {
1759
+ parts.push(inline.text || "");
1760
+ } else if (inline.type === "variableNode") {
1761
+ const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
1762
+ parts.push(`[${label}]`);
1763
+ } else if (inline.type === "fieldNode") {
1764
+ const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
1765
+ parts.push(`[ ${label} ]`);
1766
+ }
1181
1767
  }
1182
- processInlineContent(state, node.content, fontSize);
1183
- state.y += 4;
1184
- break;
1185
1768
  }
1186
- case "bulletList": {
1187
- for (const item of node.content || []) {
1188
- for (const child of item.content || []) {
1189
- const fontSize = 12;
1190
- ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1191
- const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1192
- state.currentPage.drawText("\u2022", {
1193
- x: MARGIN + 8,
1194
- y: bulletPdfY,
1195
- size: fontSize,
1196
- font: state.fonts.regular,
1197
- color: rgb(0, 0, 0)
1198
- });
1199
- processInlineContent(state, child.content, fontSize, 24);
1769
+ }
1770
+ return parts.join("");
1771
+ }
1772
+ function isNumericText(text) {
1773
+ return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
1774
+ }
1775
+ function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
1776
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1777
+ const font = isHeader ? fonts.bold : fonts.regular;
1778
+ let totalHeight = 0;
1779
+ for (const child of cell.content || []) {
1780
+ if (child.type === "paragraph" || child.type === "heading") {
1781
+ const text = extractCellText({ content: [child] });
1782
+ if (!text) {
1783
+ totalHeight += lineHeight;
1784
+ continue;
1785
+ }
1786
+ const words = text.split(/\s+/);
1787
+ let line = "";
1788
+ let lineCount = 0;
1789
+ for (const word of words) {
1790
+ const testLine = line ? `${line} ${word}` : word;
1791
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
1792
+ if (testWidth > maxWidth && line) {
1793
+ lineCount++;
1794
+ line = word;
1795
+ } else {
1796
+ line = testLine;
1200
1797
  }
1201
1798
  }
1202
- state.y += 4;
1203
- break;
1799
+ if (line) lineCount++;
1800
+ totalHeight += lineCount * lineHeight;
1204
1801
  }
1205
- case "orderedList": {
1206
- let num = 1;
1207
- for (const item of node.content || []) {
1208
- for (const child of item.content || []) {
1209
- const fontSize = 12;
1210
- ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1211
- const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1212
- state.currentPage.drawText(`${num}.`, {
1213
- x: MARGIN + 4,
1214
- y: numPdfY,
1802
+ }
1803
+ return Math.max(totalHeight, lineHeight);
1804
+ }
1805
+ function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = rgb(0, 0, 0)) {
1806
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1807
+ const cellPadding = 4;
1808
+ const maxTextWidth = cellWidth - cellPadding * 2;
1809
+ let textX = cellX + cellPadding;
1810
+ let textY = cellY;
1811
+ for (const child of cell.content || []) {
1812
+ if (child.type === "paragraph" || child.type === "heading") {
1813
+ const segments = collectInlineSegments(
1814
+ child.content || [],
1815
+ isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
1816
+ fontSize
1817
+ );
1818
+ if (rightAlign && segments.length > 0) {
1819
+ const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
1820
+ const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
1821
+ if (textWidth <= maxTextWidth) {
1822
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1823
+ state.currentPage.drawText(fullText, {
1824
+ x: cellX + cellWidth - cellPadding - textWidth,
1825
+ y: pdfY,
1215
1826
  size: fontSize,
1216
- font: state.fonts.regular,
1217
- color: rgb(0, 0, 0)
1827
+ font: isHeader ? fonts.bold : fonts.regular,
1828
+ color: textColor
1218
1829
  });
1219
- processInlineContent(state, child.content, fontSize, 24);
1830
+ return;
1220
1831
  }
1221
- num++;
1222
1832
  }
1223
- state.y += 4;
1224
- break;
1225
- }
1833
+ let currentX = textX;
1834
+ for (const seg of segments) {
1835
+ if (seg.kind === "break") {
1836
+ textY += lineHeight;
1837
+ currentX = textX;
1838
+ continue;
1839
+ }
1840
+ if (seg.kind === "text") {
1841
+ const words = seg.text.split(/(\s+)/);
1842
+ const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
1843
+ for (const token of words) {
1844
+ if (!token) continue;
1845
+ if (/^\s+$/.test(token)) {
1846
+ currentX += spaceWidth;
1847
+ continue;
1848
+ }
1849
+ const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
1850
+ if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
1851
+ textY += lineHeight;
1852
+ currentX = textX;
1853
+ }
1854
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1855
+ state.currentPage.drawText(token, {
1856
+ x: currentX,
1857
+ y: pdfY,
1858
+ size: fontSize,
1859
+ font: seg.font,
1860
+ color: textColor
1861
+ });
1862
+ currentX += wordWidth;
1863
+ }
1864
+ } else if (seg.kind === "field") {
1865
+ const pdfY = PAGE_HEIGHT - textY - seg.height;
1866
+ if (state.drawFieldPlaceholders) {
1867
+ state.currentPage.drawRectangle({
1868
+ x: currentX,
1869
+ y: pdfY,
1870
+ width: seg.width,
1871
+ height: seg.height,
1872
+ borderColor: rgb(0.5, 0.5, 0.7),
1873
+ borderWidth: 0.5,
1874
+ color: rgb(0.95, 0.95, 1),
1875
+ borderDashArray: [4, 2]
1876
+ });
1877
+ }
1878
+ const fieldId = seg.attrs.fieldId;
1879
+ if (fieldId) {
1880
+ state.fieldPositions.set(fieldId, {
1881
+ x: currentX,
1882
+ y: pdfY,
1883
+ width: seg.width,
1884
+ height: seg.height,
1885
+ page: state.pageIndex + 1
1886
+ });
1887
+ }
1888
+ currentX += seg.width + 4;
1889
+ }
1890
+ }
1891
+ }
1892
+ }
1893
+ }
1894
+ function renderTable(state, node, region = DEFAULT_REGION) {
1895
+ const tableRows = node.content || [];
1896
+ if (tableRows.length === 0) return;
1897
+ const borderless = node.attrs?.borderless === true;
1898
+ const subtotals = node.attrs?.subtotals === true;
1899
+ const fontSize = 10;
1900
+ const cellPadding = borderless ? 2 : 4;
1901
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1902
+ const rowPadding = cellPadding * 2;
1903
+ const borderColor = state.theme.tableBorder;
1904
+ const headerBg = state.theme.tableHeaderBg;
1905
+ const headerTextColor = state.theme.tableHeaderText;
1906
+ let colCount = 0;
1907
+ for (const row of tableRows) {
1908
+ colCount = Math.max(colCount, row.content?.length || 0);
1909
+ }
1910
+ if (colCount === 0) return;
1911
+ const colMaxWidths = new Array(colCount).fill(0);
1912
+ for (const row of tableRows) {
1913
+ const cells = row.content || [];
1914
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1915
+ const cell = cells[c];
1916
+ const isHeader = cell.type === "tableHeader";
1917
+ const text = extractCellText(cell);
1918
+ const font = isHeader ? state.fonts.bold : state.fonts.regular;
1919
+ const textWidth = font.widthOfTextAtSize(text, fontSize);
1920
+ colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
1921
+ }
1922
+ }
1923
+ const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
1924
+ const minColWidth = 40;
1925
+ let colWidths;
1926
+ if (totalMeasured <= region.width) {
1927
+ const scale = region.width / totalMeasured;
1928
+ colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
1929
+ } else {
1930
+ colWidths = colMaxWidths.map(
1931
+ (w) => Math.max(w / totalMeasured * region.width, minColWidth)
1932
+ );
1933
+ }
1934
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
1935
+ if (colSum > 0) {
1936
+ const normFactor = region.width / colSum;
1937
+ colWidths = colWidths.map((w) => w * normFactor);
1938
+ }
1939
+ const headerRow = tableRows[0];
1940
+ const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
1941
+ function renderRow(row, isHeaderRow) {
1942
+ const cells = row.content || [];
1943
+ const isHeader = isHeaderRow && hasHeader;
1944
+ let rowHeight = lineHeight;
1945
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1946
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
1947
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
1948
+ rowHeight = Math.max(rowHeight, cellH);
1949
+ }
1950
+ rowHeight += rowPadding;
1951
+ ensureSpace(state, rowHeight);
1952
+ const rowTopY = state.y;
1953
+ let cellX = region.leftX;
1954
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1955
+ const cellW = colWidths[c];
1956
+ const cell = cells[c];
1957
+ const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
1958
+ const pdfCellBottom = pdfCellTop - rowHeight;
1959
+ if (isHeader && !borderless) {
1960
+ state.currentPage.drawRectangle({
1961
+ x: cellX,
1962
+ y: pdfCellBottom,
1963
+ width: cellW,
1964
+ height: rowHeight,
1965
+ color: headerBg
1966
+ });
1967
+ }
1968
+ if (!borderless) {
1969
+ state.currentPage.drawRectangle({
1970
+ x: cellX,
1971
+ y: pdfCellBottom,
1972
+ width: cellW,
1973
+ height: rowHeight,
1974
+ borderColor,
1975
+ borderWidth: 0.5
1976
+ });
1977
+ }
1978
+ const cellText = extractCellText(cell);
1979
+ const rightAlign = !isHeader && isNumericText(cellText);
1980
+ const contentY = MARGIN + rowTopY + cellPadding;
1981
+ const effectiveHeader = isHeader && !borderless;
1982
+ renderCellContent(
1983
+ state,
1984
+ cell,
1985
+ cellX,
1986
+ contentY,
1987
+ cellW,
1988
+ state.fonts,
1989
+ fontSize,
1990
+ effectiveHeader,
1991
+ rightAlign,
1992
+ effectiveHeader ? headerTextColor : rgb(0, 0, 0)
1993
+ );
1994
+ cellX += cellW;
1995
+ }
1996
+ state.y += rowHeight;
1997
+ }
1998
+ if (hasHeader && tableRows.length > 0) {
1999
+ renderRow(tableRows[0], true);
2000
+ }
2001
+ let lastPageIndex = state.pageIndex;
2002
+ const startIdx = hasHeader ? 1 : 0;
2003
+ const bodyRowCount = tableRows.length - startIdx;
2004
+ for (let r = startIdx; r < tableRows.length; r++) {
2005
+ const cells = tableRows[r].content || [];
2006
+ let estRowHeight = lineHeight;
2007
+ for (let c = 0; c < cells.length && c < colCount; c++) {
2008
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
2009
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
2010
+ estRowHeight = Math.max(estRowHeight, cellH);
2011
+ }
2012
+ estRowHeight += rowPadding;
2013
+ if (state.y + estRowHeight > CONTENT_HEIGHT) {
2014
+ ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
2015
+ if (state.pageIndex !== lastPageIndex && hasHeader) {
2016
+ renderRow(headerRow, true);
2017
+ lastPageIndex = state.pageIndex;
2018
+ }
2019
+ }
2020
+ const rowTopYBeforeRender = state.y;
2021
+ renderRow(tableRows[r], false);
2022
+ lastPageIndex = state.pageIndex;
2023
+ if (subtotals && colCount >= 2) {
2024
+ const bodyIdx = r - startIdx;
2025
+ const isFirstBody = bodyIdx === 0;
2026
+ const isLastBody = bodyIdx === bodyRowCount - 1;
2027
+ if (isFirstBody || isLastBody) {
2028
+ const valueStartX = region.leftX + colWidths[0];
2029
+ const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
2030
+ const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
2031
+ const overlineColor = rgb(0, 0, 0);
2032
+ if (isFirstBody) {
2033
+ state.currentPage.drawLine({
2034
+ start: { x: valueStartX, y: pdfRowTopY },
2035
+ end: { x: valueEndX, y: pdfRowTopY },
2036
+ thickness: 0.5,
2037
+ color: overlineColor
2038
+ });
2039
+ } else if (isLastBody) {
2040
+ state.currentPage.drawLine({
2041
+ start: { x: valueStartX, y: pdfRowTopY + 2 },
2042
+ end: { x: valueEndX, y: pdfRowTopY + 2 },
2043
+ thickness: 0.5,
2044
+ color: overlineColor
2045
+ });
2046
+ state.currentPage.drawLine({
2047
+ start: { x: valueStartX, y: pdfRowTopY },
2048
+ end: { x: valueEndX, y: pdfRowTopY },
2049
+ thickness: 0.5,
2050
+ color: overlineColor
2051
+ });
2052
+ }
2053
+ }
2054
+ }
2055
+ }
2056
+ state.y += 3;
2057
+ }
2058
+ function processBlock(state, node, region = DEFAULT_REGION) {
2059
+ switch (node.type) {
2060
+ case "heading": {
2061
+ const level = node.attrs?.level || 1;
2062
+ const fontSizes = { 1: 24, 2: 20, 3: 16 };
2063
+ const fontSize = fontSizes[level] || 16;
2064
+ const spacing = fontSize * 0.6;
2065
+ ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR + spacing);
2066
+ state.y += spacing / 2;
2067
+ if (!node.content) break;
2068
+ const headingFonts = {
2069
+ regular: state.fonts.bold,
2070
+ bold: state.fonts.bold,
2071
+ italic: state.fonts.boldItalic,
2072
+ boldItalic: state.fonts.boldItalic
2073
+ };
2074
+ const segments = collectInlineSegments(node.content, headingFonts, fontSize);
2075
+ layoutInlineSegments(state, segments, fontSize, 0, region);
2076
+ state.y += spacing / 2;
2077
+ break;
2078
+ }
2079
+ case "paragraph": {
2080
+ const fontSize = BODY_FONT_SIZE;
2081
+ if (!node.content || node.content.length === 0) {
2082
+ state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
2083
+ break;
2084
+ }
2085
+ processInlineContent(state, node.content, fontSize, 0, region);
2086
+ state.y += 2;
2087
+ break;
2088
+ }
2089
+ case "bulletList": {
2090
+ for (const item of node.content || []) {
2091
+ for (const child of item.content || []) {
2092
+ const fontSize = BODY_FONT_SIZE;
2093
+ ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
2094
+ const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
2095
+ state.currentPage.drawText("\u2022", {
2096
+ x: region.leftX + 8,
2097
+ y: bulletPdfY,
2098
+ size: fontSize,
2099
+ font: state.fonts.regular,
2100
+ color: rgb(0, 0, 0)
2101
+ });
2102
+ processInlineContent(state, child.content, fontSize, 24, region);
2103
+ }
2104
+ }
2105
+ state.y += 4;
2106
+ break;
2107
+ }
2108
+ case "orderedList": {
2109
+ let num = 1;
2110
+ for (const item of node.content || []) {
2111
+ for (const child of item.content || []) {
2112
+ const fontSize = BODY_FONT_SIZE;
2113
+ ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
2114
+ const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
2115
+ state.currentPage.drawText(`${num}.`, {
2116
+ x: region.leftX + 4,
2117
+ y: numPdfY,
2118
+ size: fontSize,
2119
+ font: state.fonts.regular,
2120
+ color: rgb(0, 0, 0)
2121
+ });
2122
+ processInlineContent(state, child.content, fontSize, 24, region);
2123
+ }
2124
+ num++;
2125
+ }
2126
+ state.y += 4;
2127
+ break;
2128
+ }
1226
2129
  case "blockquote": {
1227
2130
  const startY = state.y;
1228
2131
  for (const child of node.content || []) {
1229
- processInlineContent(state, child.content, 12, 16);
2132
+ processInlineContent(state, child.content, 12, 16, region);
1230
2133
  }
1231
2134
  const endY = state.y;
1232
- const barX = MARGIN + 4;
2135
+ const barX = region.leftX + 4;
1233
2136
  const barTop = PAGE_HEIGHT - MARGIN - startY;
1234
2137
  const barBottom = PAGE_HEIGHT - MARGIN - endY;
1235
2138
  state.currentPage.drawLine({
@@ -1242,16 +2145,16 @@ function processBlock(state, node) {
1242
2145
  break;
1243
2146
  }
1244
2147
  case "horizontalRule": {
1245
- ensureSpace(state, 16);
1246
- state.y += 8;
2148
+ ensureSpace(state, 10);
2149
+ state.y += 4;
1247
2150
  const ruleY = PAGE_HEIGHT - MARGIN - state.y;
1248
2151
  state.currentPage.drawLine({
1249
- start: { x: MARGIN, y: ruleY },
1250
- end: { x: PAGE_WIDTH - MARGIN, y: ruleY },
1251
- thickness: 1,
1252
- color: rgb(0.7, 0.7, 0.7)
2152
+ start: { x: region.leftX, y: ruleY },
2153
+ end: { x: region.leftX + region.width, y: ruleY },
2154
+ thickness: 2.5,
2155
+ color: state.theme.rule
1253
2156
  });
1254
- state.y += 8;
2157
+ state.y += 4;
1255
2158
  break;
1256
2159
  }
1257
2160
  case "codeBlock": {
@@ -1263,9 +2166,9 @@ function processBlock(state, node) {
1263
2166
  ensureSpace(state, blockHeight);
1264
2167
  const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
1265
2168
  state.currentPage.drawRectangle({
1266
- x: MARGIN,
2169
+ x: region.leftX,
1267
2170
  y: boxY,
1268
- width: CONTENT_WIDTH,
2171
+ width: region.width,
1269
2172
  height: blockHeight,
1270
2173
  color: rgb(0.95, 0.95, 0.95),
1271
2174
  borderColor: rgb(0.85, 0.85, 0.85),
@@ -1273,18 +2176,325 @@ function processBlock(state, node) {
1273
2176
  });
1274
2177
  state.y += 8;
1275
2178
  for (const line of lines) {
1276
- drawText(state, line || " ", state.fonts.regular, fontSize, 8);
2179
+ drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
1277
2180
  }
1278
2181
  state.y += 8;
1279
2182
  break;
1280
2183
  }
2184
+ case "table": {
2185
+ renderTable(state, node, region);
2186
+ break;
2187
+ }
2188
+ case "imageNode": {
2189
+ const src = node.attrs?.src || "";
2190
+ const image = src ? state.images.get(src) : void 0;
2191
+ if (!image) break;
2192
+ const natW = image.width;
2193
+ const natH = image.height;
2194
+ let w = parseFloat(node.attrs?.width || "") || 0;
2195
+ let h = parseFloat(node.attrs?.height || "") || 0;
2196
+ if (w && !h) h = natH / natW * w;
2197
+ else if (h && !w) w = natW / natH * h;
2198
+ else if (!w && !h) {
2199
+ w = natW;
2200
+ h = natH;
2201
+ }
2202
+ if (w > region.width) {
2203
+ h = h * (region.width / w);
2204
+ w = region.width;
2205
+ }
2206
+ const maxH = CONTENT_HEIGHT * 0.6;
2207
+ if (h > maxH) {
2208
+ w = w * (maxH / h);
2209
+ h = maxH;
2210
+ }
2211
+ ensureSpace(state, h + 4);
2212
+ const align = node.attrs?.align || "left";
2213
+ let x = region.leftX;
2214
+ if (align === "center") x = region.leftX + (region.width - w) / 2;
2215
+ else if (align === "right") x = region.leftX + region.width - w;
2216
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
2217
+ state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
2218
+ state.y += h + 4;
2219
+ break;
2220
+ }
2221
+ // ─── Layout directives ─────────────────────────────────────────
2222
+ case "panel": {
2223
+ const title = node.attrs?.title || "";
2224
+ const border = node.attrs?.border || "solid";
2225
+ const headerStyle = node.attrs?.headerStyle || "";
2226
+ const padding = 8;
2227
+ const titleFontSize = 10;
2228
+ const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
2229
+ const panelStartY = state.y;
2230
+ const panelStartPage = state.pageIndex;
2231
+ if (title) {
2232
+ state.y += titleHeight;
2233
+ }
2234
+ const isHeaderOnly = border === "none";
2235
+ if (!isHeaderOnly) {
2236
+ state.y += padding;
2237
+ }
2238
+ const innerRegion = {
2239
+ leftX: region.leftX + padding,
2240
+ width: region.width - padding * 2
2241
+ };
2242
+ if (!isHeaderOnly) {
2243
+ for (const child of node.content || []) {
2244
+ processBlock(state, child, innerRegion);
2245
+ }
2246
+ state.y += padding;
2247
+ }
2248
+ const startPage = state.pdfDoc.getPages()[panelStartPage];
2249
+ if (title) {
2250
+ const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
2251
+ const isDarkHeader = headerStyle === "dark";
2252
+ startPage.drawRectangle({
2253
+ x: region.leftX,
2254
+ y: titleBarY,
2255
+ width: region.width,
2256
+ height: titleHeight,
2257
+ color: isDarkHeader ? state.theme.panelDarkHeaderBg : rgb(0.93, 0.93, 0.93),
2258
+ borderColor: state.theme.panelBorder,
2259
+ borderWidth: 0.75
2260
+ });
2261
+ startPage.drawText(title, {
2262
+ x: region.leftX + padding,
2263
+ y: titleBarY + 4,
2264
+ size: titleFontSize,
2265
+ font: state.fonts.bold,
2266
+ color: isDarkHeader ? state.theme.panelDarkHeaderText : rgb(0, 0, 0)
2267
+ });
2268
+ }
2269
+ if (border !== "none") {
2270
+ const borderDashArray = border === "dashed" ? [4, 2] : void 0;
2271
+ if (panelStartPage === state.pageIndex) {
2272
+ const borderY = PAGE_HEIGHT - MARGIN - state.y;
2273
+ const panelHeight = state.y - panelStartY;
2274
+ startPage.drawRectangle({
2275
+ x: region.leftX,
2276
+ y: borderY,
2277
+ width: region.width,
2278
+ height: panelHeight,
2279
+ borderColor: state.theme.panelBorder,
2280
+ borderWidth: 0.75,
2281
+ borderDashArray
2282
+ });
2283
+ } else {
2284
+ const startBorderBottom = MARGIN;
2285
+ const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
2286
+ startPage.drawRectangle({
2287
+ x: region.leftX,
2288
+ y: startBorderBottom,
2289
+ width: region.width,
2290
+ height: startPanelHeight,
2291
+ borderColor: state.theme.panelBorder,
2292
+ borderWidth: 0.75,
2293
+ borderDashArray
2294
+ });
2295
+ const contTop = PAGE_HEIGHT - MARGIN;
2296
+ const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
2297
+ const contHeight = contTop - contBorderY;
2298
+ state.currentPage.drawRectangle({
2299
+ x: region.leftX,
2300
+ y: contBorderY,
2301
+ width: region.width,
2302
+ height: contHeight,
2303
+ borderColor: state.theme.panelBorder,
2304
+ borderWidth: 0.75,
2305
+ borderDashArray
2306
+ });
2307
+ }
2308
+ }
2309
+ state.y += 4;
2310
+ break;
2311
+ }
2312
+ case "columns": {
2313
+ const split = parseInt(node.attrs?.split || "50", 10);
2314
+ const padX = parseFloat(node.attrs?.padX || "0") || 0;
2315
+ const columns = (node.content || []).filter((c) => c.type === "column");
2316
+ if (columns.length === 0) break;
2317
+ const colRegionLeftX = region.leftX + padX;
2318
+ const colRegionWidth = region.width - 2 * padX;
2319
+ const gap = 12;
2320
+ const columnsStartY = state.y;
2321
+ const columnsStartPage = state.pageIndex;
2322
+ let colWidths;
2323
+ if (columns.length === 2) {
2324
+ const firstWidth = (colRegionWidth - gap) * (split / 100);
2325
+ const secondWidth = colRegionWidth - gap - firstWidth;
2326
+ colWidths = [firstWidth, secondWidth];
2327
+ } else {
2328
+ const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
2329
+ colWidths = columns.map(() => eachWidth);
2330
+ }
2331
+ let maxEndY = columnsStartY;
2332
+ let colX = colRegionLeftX;
2333
+ for (let ci = 0; ci < columns.length; ci++) {
2334
+ const col = columns[ci];
2335
+ const colWidth = colWidths[ci];
2336
+ const colRegion = { leftX: colX, width: colWidth };
2337
+ state.y = columnsStartY;
2338
+ state.pageIndex = columnsStartPage;
2339
+ state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
2340
+ const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
2341
+ if (padTop > 0) {
2342
+ state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
2343
+ }
2344
+ for (const child of col.content || []) {
2345
+ processBlock(state, child, colRegion);
2346
+ }
2347
+ maxEndY = Math.max(maxEndY, state.y);
2348
+ colX += colWidth + gap;
2349
+ }
2350
+ state.y = maxEndY;
2351
+ state.y += 4;
2352
+ break;
2353
+ }
2354
+ case "column": {
2355
+ for (const child of node.content || []) {
2356
+ processBlock(state, child, region);
2357
+ }
2358
+ break;
2359
+ }
2360
+ case "watermark": {
2361
+ break;
2362
+ }
2363
+ case "subtotalsBlock": {
2364
+ const stFontSize = BODY_FONT_SIZE;
2365
+ const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
2366
+ const valueRightX = region.leftX + region.width;
2367
+ const rows = (node.content || []).filter(
2368
+ (c) => c.type === "paragraph" && c.content?.length
2369
+ );
2370
+ const parsed = [];
2371
+ for (const row of rows) {
2372
+ let label = "";
2373
+ let value = "";
2374
+ for (const seg of row.content || []) {
2375
+ if (seg.type === "text") {
2376
+ const isBold = seg.marks?.some((m) => m.type === "bold");
2377
+ if (isBold) {
2378
+ label += seg.text || "";
2379
+ } else {
2380
+ const t = (seg.text || "").trim();
2381
+ if (t) value += (value ? " " : "") + t;
2382
+ }
2383
+ } else if (seg.type === "variableNode") {
2384
+ const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
2385
+ const isBoldVar = seg.marks?.some((m) => m.type === "bold");
2386
+ if (isBoldVar) {
2387
+ label += varLabel;
2388
+ } else {
2389
+ value += (value ? " " : "") + `[${varLabel}]`;
2390
+ }
2391
+ }
2392
+ }
2393
+ parsed.push({ label: label.trim(), value: value.trim() });
2394
+ }
2395
+ let maxLabelWidth = 0;
2396
+ for (const { label } of parsed) {
2397
+ if (label) {
2398
+ const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
2399
+ if (w > maxLabelWidth) maxLabelWidth = w;
2400
+ }
2401
+ }
2402
+ const gap = 12;
2403
+ const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
2404
+ const valueColStartX = labelX + maxLabelWidth + gap;
2405
+ const isLastRowTotal = parsed.length > 1;
2406
+ for (let ri = 0; ri < parsed.length; ri++) {
2407
+ const isTotal = isLastRowTotal && ri === parsed.length - 1;
2408
+ if (isTotal) {
2409
+ state.y += stLineHeight * 0.6;
2410
+ }
2411
+ ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
2412
+ const { label, value } = parsed[ri];
2413
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
2414
+ if (isTotal) {
2415
+ state.currentPage.drawLine({
2416
+ start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
2417
+ end: { x: valueRightX, y: pdfY + stFontSize + 6 },
2418
+ thickness: 0.75,
2419
+ color: rgb(0, 0, 0)
2420
+ });
2421
+ state.currentPage.drawLine({
2422
+ start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
2423
+ end: { x: valueRightX, y: pdfY + stFontSize + 3 },
2424
+ thickness: 0.75,
2425
+ color: rgb(0, 0, 0)
2426
+ });
2427
+ }
2428
+ if (label) {
2429
+ state.currentPage.drawText(label, {
2430
+ x: labelX,
2431
+ y: pdfY,
2432
+ size: stFontSize,
2433
+ font: state.fonts.bold,
2434
+ color: rgb(0, 0, 0)
2435
+ });
2436
+ }
2437
+ if (value) {
2438
+ const valueFont = state.fonts.bold;
2439
+ const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
2440
+ state.currentPage.drawText(value, {
2441
+ x: valueRightX - valueWidth,
2442
+ y: pdfY,
2443
+ size: stFontSize,
2444
+ font: valueFont,
2445
+ color: rgb(0, 0, 0)
2446
+ });
2447
+ }
2448
+ state.y += stLineHeight;
2449
+ }
2450
+ state.y += 4;
2451
+ break;
2452
+ }
2453
+ case "repeatBlock":
2454
+ if (node.content) {
2455
+ for (const child of node.content) {
2456
+ processBlock(state, child, region);
2457
+ }
2458
+ }
2459
+ break;
1281
2460
  default:
1282
2461
  if (node.content) {
1283
- processInlineContent(state, node.content, 12);
2462
+ processInlineContent(state, node.content, 12, 0, region);
1284
2463
  }
1285
2464
  break;
1286
2465
  }
1287
2466
  }
2467
+ function renderWatermarksOnPage(page, fonts, watermarkNodes) {
2468
+ for (const wmNode of watermarkNodes) {
2469
+ const text = wmNode.attrs?.text || "";
2470
+ if (!text) continue;
2471
+ const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
2472
+ const angle = parseFloat(wmNode.attrs?.angle || "-45");
2473
+ const fontSize = 100;
2474
+ const { width, height } = page.getSize();
2475
+ const centerX = width / 2;
2476
+ const centerY = height / 2;
2477
+ const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
2478
+ page.drawText(text, {
2479
+ x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
2480
+ y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
2481
+ size: fontSize,
2482
+ font: fonts.bold,
2483
+ color: rgb(0.5, 0.5, 0.5),
2484
+ opacity,
2485
+ rotate: degrees(angle)
2486
+ });
2487
+ }
2488
+ }
2489
+ function collectWatermarkNodes(content) {
2490
+ const nodes = [];
2491
+ for (const block of content.content || []) {
2492
+ if (block.type === "watermark") {
2493
+ nodes.push(block);
2494
+ }
2495
+ }
2496
+ return nodes;
2497
+ }
1288
2498
  function ensureFieldSuffix(fieldName, suffix) {
1289
2499
  const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
1290
2500
  return `${cleanName}${suffix}`;
@@ -1394,6 +2604,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1394
2604
  PDFString$1.of(field.label)
1395
2605
  );
1396
2606
  }
2607
+ const initialsFontSize = Math.min(
2608
+ 72,
2609
+ Math.max(8, Math.round(position.height * 0.55))
2610
+ );
2611
+ try {
2612
+ initialsField.setFontSize(initialsFontSize);
2613
+ const acro = initialsField;
2614
+ const daRef = acro.acroField.dict.get(PDFName$1.of("DA"));
2615
+ if (daRef) {
2616
+ const daStr = daRef.value;
2617
+ if (typeof daStr === "string") {
2618
+ const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
2619
+ acro.acroField.dict.set(PDFName$1.of("DA"), PDFString$1.of(newDa));
2620
+ }
2621
+ }
2622
+ } catch (fontError) {
2623
+ console.warn(`Failed to set initials font:`, fontError);
2624
+ }
1397
2625
  initialsField.enableReadOnly();
1398
2626
  if (field.required) initialsField.enableRequired();
1399
2627
  break;
@@ -1497,47 +2725,367 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
1497
2725
  } catch (err) {
1498
2726
  warnings.push(`Failed to create ${field.type} field "${field.name}": ${getErrorMessage(err)}`);
1499
2727
  }
1500
- }
1501
- initPdfMetadata({ PDFName: PDFName$1, PDFString: PDFString$1 });
1502
- const metadata = buildMetadataObject(fields, actualFieldNames);
1503
- setSigniphiMetadata(pdfDoc, metadata);
1504
- return warnings;
1505
- }
1506
- async function generatePdfFromContent(content, options = {}) {
1507
- const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
1508
- const pdfDoc = await PDFDocument.create();
1509
- const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
1510
- const fonts = {
1511
- regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
1512
- bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
1513
- italic: await pdfDoc.embedFont(StandardFonts.HelveticaOblique),
1514
- boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
1515
- };
1516
- const state = {
1517
- currentPage: firstPage,
1518
- pageIndex: 0,
1519
- y: 0,
1520
- fonts,
1521
- pdfDoc,
1522
- fieldPositions: /* @__PURE__ */ new Map(),
1523
- drawFieldPlaceholders
1524
- };
1525
- if (content.content) {
1526
- for (const block of content.content) {
1527
- processBlock(state, block);
2728
+ }
2729
+ initPdfMetadata({ PDFName: PDFName$1, PDFString: PDFString$1 });
2730
+ const metadata = buildMetadataObject(fields, actualFieldNames);
2731
+ setSigniphiMetadata(pdfDoc, metadata);
2732
+ return warnings;
2733
+ }
2734
+ async function generatePdfFromContent(content, options = {}) {
2735
+ const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
2736
+ const theme = resolveTheme(options.theme);
2737
+ const pdfDoc = await PDFDocument.create();
2738
+ const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
2739
+ const fonts = {
2740
+ regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
2741
+ bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
2742
+ italic: await pdfDoc.embedFont(StandardFonts.HelveticaOblique),
2743
+ boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
2744
+ };
2745
+ const watermarkNodes = collectWatermarkNodes(content);
2746
+ const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
2747
+ const state = {
2748
+ currentPage: firstPage,
2749
+ pageIndex: 0,
2750
+ y: 0,
2751
+ fonts,
2752
+ pdfDoc,
2753
+ fieldPositions: /* @__PURE__ */ new Map(),
2754
+ drawFieldPlaceholders,
2755
+ watermarkNodes,
2756
+ theme,
2757
+ images
2758
+ };
2759
+ renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
2760
+ if (content.content) {
2761
+ for (const block of content.content) {
2762
+ processBlock(state, block);
2763
+ }
2764
+ }
2765
+ const allPages = pdfDoc.getPages();
2766
+ const totalPages = allPages.length;
2767
+ const pageNumFontSize = 9;
2768
+ const hfFontSize = 8;
2769
+ const hfColor = rgb(0.45, 0.45, 0.45);
2770
+ for (let i = 0; i < totalPages; i++) {
2771
+ const page = allPages[i];
2772
+ const label = `Page ${i + 1} of ${totalPages}`;
2773
+ const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
2774
+ page.drawText(label, {
2775
+ x: PAGE_WIDTH - MARGIN - labelWidth,
2776
+ y: MARGIN / 2 - pageNumFontSize / 2,
2777
+ size: pageNumFontSize,
2778
+ font: fonts.bold,
2779
+ color: rgb(0, 0, 0)
2780
+ });
2781
+ if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
2782
+ const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
2783
+ if (header.left) {
2784
+ page.drawText(header.left, {
2785
+ x: MARGIN,
2786
+ y: headerBaseY,
2787
+ size: pageNumFontSize,
2788
+ font: fonts.bold,
2789
+ color: theme.pageHeaderText
2790
+ });
2791
+ }
2792
+ if (header.right) {
2793
+ const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
2794
+ page.drawText(header.right, {
2795
+ x: PAGE_WIDTH - MARGIN - rightWidth,
2796
+ y: headerBaseY,
2797
+ size: hfFontSize,
2798
+ font: fonts.regular,
2799
+ color: hfColor
2800
+ });
2801
+ }
2802
+ page.drawLine({
2803
+ start: { x: MARGIN, y: headerBaseY - 6 },
2804
+ end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
2805
+ thickness: 0.75,
2806
+ color: theme.pageHeaderRule
2807
+ });
2808
+ }
2809
+ if (footer?.left) {
2810
+ page.drawText(footer.left, {
2811
+ x: MARGIN,
2812
+ y: MARGIN / 2 - pageNumFontSize / 2,
2813
+ size: hfFontSize,
2814
+ font: fonts.regular,
2815
+ color: hfColor
2816
+ });
2817
+ }
2818
+ }
2819
+ let fieldWarnings;
2820
+ if (embedFormFields && fields.length > 0) {
2821
+ const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
2822
+ if (warnings.length > 0) fieldWarnings = warnings;
2823
+ }
2824
+ const pdfBytes = new Uint8Array(await pdfDoc.save({ useObjectStreams: false }));
2825
+ return {
2826
+ pdfBytes,
2827
+ fieldPositions: state.fieldPositions,
2828
+ fieldWarnings,
2829
+ imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
2830
+ };
2831
+ }
2832
+
2833
+ // src/utils/template-pipeline.ts
2834
+ async function generatePdfFromTiptap(content, values, options = {}) {
2835
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2836
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
2837
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
2838
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2839
+ const fields = extractFieldsFromContent(replaced);
2840
+ const result = await generatePdfFromContent(replaced, {
2841
+ embedFormFields: fields.length > 0,
2842
+ fields,
2843
+ theme: options.theme,
2844
+ header: options.header,
2845
+ footer: options.footer,
2846
+ drawFieldPlaceholders: options.drawFieldPlaceholders
2847
+ });
2848
+ return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
2849
+ }
2850
+ async function generatePdfFromMarkdown(markdown, values, options = {}) {
2851
+ const content = markdownToTiptap(markdown);
2852
+ return generatePdfFromTiptap(content, values, options);
2853
+ }
2854
+
2855
+ // src/utils/markdown-writer.ts
2856
+ function serializeFieldToken(attrs) {
2857
+ const parts = ["field"];
2858
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2859
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2860
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2861
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2862
+ if (attrs.required) parts.push(`required:true`);
2863
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2864
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2865
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2866
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2867
+ if (attrs.multiline) parts.push(`multiline:true`);
2868
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2869
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2870
+ return `{{${parts.join("|")}}}`;
2871
+ }
2872
+ function serializeImageNode(attrs) {
2873
+ const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
2874
+ if (!hasExtras && attrs.src) {
2875
+ return `![${attrs.alt || ""}](${attrs.src})`;
2876
+ }
2877
+ const parts = ["image"];
2878
+ if (attrs.varName) parts.push(`var:${attrs.varName}`);
2879
+ else if (attrs.src) parts.push(`src:${attrs.src}`);
2880
+ if (attrs.alt) parts.push(`alt:${attrs.alt}`);
2881
+ if (attrs.width) parts.push(`width:${attrs.width}`);
2882
+ if (attrs.height) parts.push(`height:${attrs.height}`);
2883
+ if (attrs.align) parts.push(`align:${attrs.align}`);
2884
+ return `{{${parts.join("|")}}}`;
2885
+ }
2886
+ function serializeVariableToken(attrs) {
2887
+ const parts = ["var"];
2888
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2889
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2890
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2891
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2892
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2893
+ if (attrs.block === "true") parts.push(`block:true`);
2894
+ return `{{${parts.join("|")}}}`;
2895
+ }
2896
+ function serializeInline(content) {
2897
+ if (!content) return "";
2898
+ let result = "";
2899
+ for (const node of content) {
2900
+ if (node.type === "fieldNode") {
2901
+ result += serializeFieldToken(node.attrs || {});
2902
+ continue;
2903
+ }
2904
+ if (node.type === "imageNode") {
2905
+ result += serializeImageNode(node.attrs || {});
2906
+ continue;
2907
+ }
2908
+ if (node.type === "variableNode") {
2909
+ let token = serializeVariableToken(node.attrs || {});
2910
+ const varMarks = node.marks || [];
2911
+ const isBold = varMarks.some((m) => m.type === "bold");
2912
+ const isItalic = varMarks.some((m) => m.type === "italic");
2913
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2914
+ if (isBold && isItalic) token = `***${token}***`;
2915
+ else if (isBold) token = `**${token}**`;
2916
+ else if (isItalic) token = `*${token}*`;
2917
+ if (isUnderline) token = `__${token}__`;
2918
+ result += token;
2919
+ continue;
2920
+ }
2921
+ if (node.type === "text") {
2922
+ let text = node.text || "";
2923
+ const marks = node.marks || [];
2924
+ for (const mark of marks) {
2925
+ if (mark.type === "bold") text = `**${text}**`;
2926
+ else if (mark.type === "italic") text = `*${text}*`;
2927
+ else if (mark.type === "underline") text = `__${text}__`;
2928
+ else if (mark.type === "code") text = `\`${text}\``;
2929
+ }
2930
+ result += text;
2931
+ }
2932
+ if (node.type === "hardBreak") {
2933
+ result += " \n";
2934
+ }
2935
+ }
2936
+ return result;
2937
+ }
2938
+ function serializeBlock(node) {
2939
+ switch (node.type) {
2940
+ case "heading": {
2941
+ const level = node.attrs?.level || 1;
2942
+ const prefix = "#".repeat(level);
2943
+ return `${prefix} ${serializeInline(node.content)}`;
2944
+ }
2945
+ case "paragraph": {
2946
+ const inline = serializeInline(node.content);
2947
+ return inline;
2948
+ }
2949
+ case "bulletList": {
2950
+ return (node.content || []).map((item) => {
2951
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2952
+ return `- ${inner}`;
2953
+ }).join("\n");
2954
+ }
2955
+ case "orderedList": {
2956
+ return (node.content || []).map((item, i) => {
2957
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2958
+ return `${i + 1}. ${inner}`;
2959
+ }).join("\n");
2960
+ }
2961
+ case "listItem": {
2962
+ return (node.content || []).map(serializeBlock).join("\n");
2963
+ }
2964
+ case "blockquote": {
2965
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
2966
+ }
2967
+ case "codeBlock": {
2968
+ const text = serializeInline(node.content);
2969
+ return `\`\`\`
2970
+ ${text}
2971
+ \`\`\``;
2972
+ }
2973
+ case "horizontalRule": {
2974
+ return "---";
2975
+ }
2976
+ case "table": {
2977
+ const rows = node.content || [];
2978
+ const isBorderless = node.attrs?.borderless === true;
2979
+ const isSubtotals = node.attrs?.subtotals === true;
2980
+ const serializedRows = [];
2981
+ if (isBorderless && rows.length > 0) {
2982
+ const colCount = rows[0].content?.length || 0;
2983
+ const markerCell = isSubtotals ? "_" : "";
2984
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
2985
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
2986
+ serializedRows.push(markerRow);
2987
+ serializedRows.push(sep);
2988
+ }
2989
+ for (let ri = 0; ri < rows.length; ri++) {
2990
+ const row = rows[ri];
2991
+ const cells = (row.content || []).map((cell) => {
2992
+ const inner = (cell.content || []).map(serializeBlock).join("");
2993
+ return inner;
2994
+ });
2995
+ serializedRows.push(`| ${cells.join(" | ")} |`);
2996
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
2997
+ const sep = cells.map(() => "------").join("|");
2998
+ serializedRows.push(`|${sep}|`);
2999
+ }
3000
+ }
3001
+ return serializedRows.join("\n");
3002
+ }
3003
+ case "tableRow": {
3004
+ const cells = (node.content || []).map((cell) => {
3005
+ const inner = (cell.content || []).map(serializeBlock).join("");
3006
+ return inner;
3007
+ });
3008
+ return `| ${cells.join(" | ")} |`;
3009
+ }
3010
+ case "tableHeader":
3011
+ case "tableCell": {
3012
+ return (node.content || []).map(serializeBlock).join("");
3013
+ }
3014
+ case "imageNode": {
3015
+ return serializeImageNode(node.attrs || {});
3016
+ }
3017
+ case "watermark": {
3018
+ const attrs = node.attrs || {};
3019
+ const parts = [];
3020
+ if (attrs.text) parts.push(`text:${attrs.text}`);
3021
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
3022
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
3023
+ return `:::watermark{${parts.join("|")}}`;
3024
+ }
3025
+ case "panel": {
3026
+ const attrs = node.attrs || {};
3027
+ const parts = [];
3028
+ if (attrs.title) parts.push(`title:${attrs.title}`);
3029
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
3030
+ else if (attrs.border === "solid") parts.push(`border:solid`);
3031
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
3032
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3033
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3034
+ return `:::panel${attrStr}
3035
+ ${children}
3036
+ :::`;
3037
+ }
3038
+ case "repeatBlock": {
3039
+ const attrs = node.attrs || {};
3040
+ const parts = [];
3041
+ if (attrs.data) parts.push(`data:${attrs.data}`);
3042
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
3043
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3044
+ return `:::repeat${attrStr}
3045
+ ${children}
3046
+ :::`;
3047
+ }
3048
+ case "subtotalsBlock": {
3049
+ const children = (node.content || []).map(serializeBlock).join("\n");
3050
+ return `:::subtotals
3051
+ ${children}
3052
+ :::`;
3053
+ }
3054
+ case "columns": {
3055
+ const attrs = node.attrs || {};
3056
+ const splitVal = attrs.split || "50";
3057
+ const cols = (node.content || []).filter((c) => c.type === "column");
3058
+ const colBlocks = cols.map((col) => {
3059
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
3060
+ const colAttrs = col.attrs || {};
3061
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3062
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3063
+ return `${colTag}
3064
+ ${children}
3065
+ :::`;
3066
+ });
3067
+ const padX = parseFloat(attrs.padX || "0") || 0;
3068
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
3069
+ return `:::columns{${colsAttrs}}
3070
+ ${colBlocks.join("\n")}
3071
+ :::`;
3072
+ }
3073
+ case "column": {
3074
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
3075
+ const colAttrs = node.attrs || {};
3076
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
3077
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
3078
+ return `${colTag}
3079
+ ${children}
3080
+ :::`;
1528
3081
  }
3082
+ default:
3083
+ return serializeInline(node.content);
1529
3084
  }
1530
- let fieldWarnings;
1531
- if (embedFormFields && fields.length > 0) {
1532
- const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
1533
- if (warnings.length > 0) fieldWarnings = warnings;
1534
- }
1535
- const pdfBytes = await pdfDoc.save({ useObjectStreams: false });
1536
- return {
1537
- pdfBytes,
1538
- fieldPositions: state.fieldPositions,
1539
- fieldWarnings
1540
- };
3085
+ }
3086
+ function tiptapToMarkdown(doc) {
3087
+ if (!doc.content) return "";
3088
+ return doc.content.map(serializeBlock).join("\n\n");
1541
3089
  }
1542
3090
 
1543
3091
  // src/utils/pdf-preview.ts
@@ -1626,8 +3174,26 @@ function useDocumentGenerator(options = {}) {
1626
3174
  Placeholder.configure({
1627
3175
  placeholder: options.placeholder || "Start writing your document..."
1628
3176
  }),
3177
+ Table.extend({
3178
+ addAttributes() {
3179
+ return {
3180
+ ...this.parent?.(),
3181
+ borderless: { default: false },
3182
+ subtotals: { default: false }
3183
+ };
3184
+ }
3185
+ }).configure({ resizable: false }),
3186
+ TableRow,
3187
+ TableCell,
3188
+ TableHeader,
1629
3189
  FieldNode,
1630
- VariableNode
3190
+ VariableNode,
3191
+ PanelNode,
3192
+ ColumnsNode,
3193
+ ColumnNode,
3194
+ WatermarkNode,
3195
+ RepeatNode,
3196
+ SubtotalsNode
1631
3197
  ],
1632
3198
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1633
3199
  onUpdate: ({ editor: ed }) => {
@@ -1731,10 +3297,9 @@ function useDocumentGenerator(options = {}) {
1731
3297
  async (values) => {
1732
3298
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1733
3299
  const content = editor.getJSON();
1734
- const replacedContent = replaceVariablesInContent(content, values);
1735
- const result = await generatePdfFromContent(replacedContent);
1736
- const pages = await pdfToImages(result.pdfBytes);
1737
- return { pdfBytes: result.pdfBytes, pdfPages: pages };
3300
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
3301
+ const pdfPages2 = await pdfToImages(pdfBytes2);
3302
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1738
3303
  },
1739
3304
  [editor]
1740
3305
  );
@@ -2888,15 +4453,47 @@ function FieldEditPopover({
2888
4453
  ] }) })
2889
4454
  ] });
2890
4455
  }
2891
- function labelToVarName(label) {
4456
+ var sizeClasses = {
4457
+ md: "px-3 py-1.5 text-xs font-medium",
4458
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4459
+ };
4460
+ function ToggleGroup({
4461
+ value,
4462
+ onChange,
4463
+ options,
4464
+ size = "md",
4465
+ className
4466
+ }) {
4467
+ return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsx(
4468
+ "button",
4469
+ {
4470
+ className: cn(
4471
+ "rounded-md transition-colors",
4472
+ sizeClasses[size],
4473
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4474
+ ),
4475
+ onClick: () => onChange(opt.value),
4476
+ children: opt.label
4477
+ },
4478
+ opt.value
4479
+ )) });
4480
+ }
4481
+ function labelToVarName2(label) {
2892
4482
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2893
4483
  }
4484
+ function varNameToLabel(name) {
4485
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4486
+ }
2894
4487
  function VariableInsertPopover({
2895
4488
  open,
2896
4489
  onOpenChange,
2897
4490
  onInsert,
4491
+ predefinedVariables,
2898
4492
  children
2899
4493
  }) {
4494
+ const hasPredefined = !!predefinedVariables?.length;
4495
+ const [mode, setMode] = useState(hasPredefined ? "existing" : "new");
4496
+ const [search, setSearch] = useState("");
2900
4497
  const [varLabel, setVarLabel] = useState("");
2901
4498
  const [varName, setVarName] = useState("");
2902
4499
  const [varDefault, setVarDefault] = useState("");
@@ -2906,7 +4503,9 @@ function VariableInsertPopover({
2906
4503
  setVarName("");
2907
4504
  setVarDefault("");
2908
4505
  setNameManuallyEdited(false);
2909
- }, []);
4506
+ setSearch("");
4507
+ setMode(hasPredefined ? "existing" : "new");
4508
+ }, [hasPredefined]);
2910
4509
  const handleOpenChange = useCallback(
2911
4510
  (nextOpen) => {
2912
4511
  if (!nextOpen) reset();
@@ -2916,7 +4515,7 @@ function VariableInsertPopover({
2916
4515
  );
2917
4516
  useEffect(() => {
2918
4517
  if (!nameManuallyEdited) {
2919
- setVarName(labelToVarName(varLabel));
4518
+ setVarName(labelToVarName2(varLabel));
2920
4519
  }
2921
4520
  }, [varLabel, nameManuallyEdited]);
2922
4521
  const handleInsert = useCallback(() => {
@@ -2924,10 +4523,32 @@ function VariableInsertPopover({
2924
4523
  onInsert({
2925
4524
  varName: varName.trim(),
2926
4525
  varLabel: varLabel.trim(),
2927
- varDefault: varDefault.trim()
4526
+ varDefault: varDefault.trim(),
4527
+ format: ""
2928
4528
  });
2929
4529
  handleOpenChange(false);
2930
4530
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4531
+ const handlePickPredefined = useCallback(
4532
+ (pv) => {
4533
+ onInsert({
4534
+ varName: pv.varName,
4535
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4536
+ varDefault: "",
4537
+ format: ""
4538
+ });
4539
+ handleOpenChange(false);
4540
+ },
4541
+ [onInsert, handleOpenChange]
4542
+ );
4543
+ const filteredVars = useMemo(() => {
4544
+ if (!predefinedVariables?.length) return [];
4545
+ const q = search.toLowerCase();
4546
+ if (!q) return predefinedVariables;
4547
+ return predefinedVariables.filter((pv) => {
4548
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4549
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4550
+ });
4551
+ }, [predefinedVariables, search]);
2931
4552
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2932
4553
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
2933
4554
  /* @__PURE__ */ jsx(PopoverContent, { className: "w-72 p-0", align: "start", sideOffset: 8, children: /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
@@ -2935,72 +4556,114 @@ function VariableInsertPopover({
2935
4556
  /* @__PURE__ */ jsx(Braces, { size: 14 }),
2936
4557
  /* @__PURE__ */ jsx("span", { children: "Insert Variable" })
2937
4558
  ] }),
2938
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2939
- /* @__PURE__ */ jsxs("div", { children: [
2940
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4559
+ hasPredefined && /* @__PURE__ */ jsx(
4560
+ ToggleGroup,
4561
+ {
4562
+ value: mode,
4563
+ onChange: setMode,
4564
+ options: [
4565
+ { value: "existing", label: "Existing" },
4566
+ { value: "new", label: "New" }
4567
+ ],
4568
+ size: "sm"
4569
+ }
4570
+ ),
4571
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4572
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
4573
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2941
4574
  /* @__PURE__ */ jsx(
2942
4575
  Input,
2943
4576
  {
2944
- id: "var-label",
2945
- value: varLabel,
2946
- onChange: (e) => setVarLabel(e.target.value),
2947
- placeholder: "e.g. Company Name",
2948
- className: "h-8 text-xs"
4577
+ value: search,
4578
+ onChange: (e) => setSearch(e.target.value),
4579
+ placeholder: "Search variables...",
4580
+ className: "h-8 text-xs pl-7"
2949
4581
  }
2950
4582
  )
2951
4583
  ] }),
2952
- /* @__PURE__ */ jsxs("div", { children: [
2953
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4584
+ /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto -mx-1", children: filteredVars.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground px-2 py-3 text-center", children: "No variables found" }) : filteredVars.map((pv) => {
4585
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4586
+ return /* @__PURE__ */ jsxs(
4587
+ "button",
4588
+ {
4589
+ 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",
4590
+ onClick: () => handlePickPredefined(pv),
4591
+ children: [
4592
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
4593
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4594
+ ]
4595
+ },
4596
+ pv.varName
4597
+ );
4598
+ }) })
4599
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4600
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4601
+ /* @__PURE__ */ jsxs("div", { children: [
4602
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4603
+ /* @__PURE__ */ jsx(
4604
+ Input,
4605
+ {
4606
+ id: "var-label",
4607
+ value: varLabel,
4608
+ onChange: (e) => setVarLabel(e.target.value),
4609
+ placeholder: "e.g. Company Name",
4610
+ className: "h-8 text-xs"
4611
+ }
4612
+ )
4613
+ ] }),
4614
+ /* @__PURE__ */ jsxs("div", { children: [
4615
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4616
+ /* @__PURE__ */ jsx(
4617
+ Input,
4618
+ {
4619
+ id: "var-name",
4620
+ value: varName,
4621
+ onChange: (e) => {
4622
+ setVarName(e.target.value);
4623
+ setNameManuallyEdited(true);
4624
+ },
4625
+ placeholder: "e.g. company_name",
4626
+ className: "h-8 text-xs font-mono"
4627
+ }
4628
+ ),
4629
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4630
+ ] }),
4631
+ /* @__PURE__ */ jsxs("div", { children: [
4632
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4633
+ /* @__PURE__ */ jsx(
4634
+ Input,
4635
+ {
4636
+ id: "var-default",
4637
+ value: varDefault,
4638
+ onChange: (e) => setVarDefault(e.target.value),
4639
+ placeholder: "Optional",
4640
+ className: "h-8 text-xs"
4641
+ }
4642
+ )
4643
+ ] })
4644
+ ] }),
4645
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2954
4646
  /* @__PURE__ */ jsx(
2955
- Input,
4647
+ Button,
2956
4648
  {
2957
- id: "var-name",
2958
- value: varName,
2959
- onChange: (e) => {
2960
- setVarName(e.target.value);
2961
- setNameManuallyEdited(true);
2962
- },
2963
- placeholder: "e.g. company_name",
2964
- className: "h-8 text-xs font-mono"
4649
+ size: "sm",
4650
+ className: "h-8 flex-1 text-xs",
4651
+ onClick: handleInsert,
4652
+ disabled: !varLabel.trim() || !varName.trim(),
4653
+ children: "Insert"
2965
4654
  }
2966
4655
  ),
2967
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
2968
- ] }),
2969
- /* @__PURE__ */ jsxs("div", { children: [
2970
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
2971
4656
  /* @__PURE__ */ jsx(
2972
- Input,
4657
+ Button,
2973
4658
  {
2974
- id: "var-default",
2975
- value: varDefault,
2976
- onChange: (e) => setVarDefault(e.target.value),
2977
- placeholder: "Optional",
2978
- className: "h-8 text-xs"
4659
+ variant: "outline",
4660
+ size: "sm",
4661
+ className: "h-8 text-xs",
4662
+ onClick: () => handleOpenChange(false),
4663
+ children: "Cancel"
2979
4664
  }
2980
4665
  )
2981
4666
  ] })
2982
- ] }),
2983
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
- /* @__PURE__ */ jsx(
2985
- Button,
2986
- {
2987
- size: "sm",
2988
- className: "h-8 flex-1 text-xs",
2989
- onClick: handleInsert,
2990
- disabled: !varLabel.trim() || !varName.trim(),
2991
- children: "Insert"
2992
- }
2993
- ),
2994
- /* @__PURE__ */ jsx(
2995
- Button,
2996
- {
2997
- variant: "outline",
2998
- size: "sm",
2999
- className: "h-8 text-xs",
3000
- onClick: () => handleOpenChange(false),
3001
- children: "Cancel"
3002
- }
3003
- )
3004
4667
  ] })
3005
4668
  ] }) })
3006
4669
  ] });
@@ -3222,7 +4885,6 @@ function PreviewPanel({
3222
4885
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3223
4886
  );
3224
4887
  }, [positionedFields, currentPage, currentPageIndex]);
3225
- const showScrollbars = zoomLevel > 1;
3226
4888
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
3227
4889
  /* @__PURE__ */ jsx("div", { className: "border-b border-border px-2 py-1.5 flex-shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
3228
4890
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3338,12 +5000,13 @@ function PreviewPanel({
3338
5000
  "div",
3339
5001
  {
3340
5002
  className: cn(
3341
- "border border-border rounded-lg bg-muted/30 shrink-0",
3342
- showScrollbars ? "overflow-auto" : "overflow-hidden"
5003
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3343
5004
  ),
3344
5005
  style: {
3345
5006
  width: pageDisplaySize.viewportWidth,
3346
- height: pageDisplaySize.viewportHeight
5007
+ height: pageDisplaySize.viewportHeight,
5008
+ maxWidth: "100%",
5009
+ maxHeight: "100%"
3347
5010
  },
3348
5011
  children: /* @__PURE__ */ jsxs(
3349
5012
  "div",
@@ -3414,8 +5077,8 @@ function validateMarkdown(markdown) {
3414
5077
  errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
3415
5078
  } else {
3416
5079
  const tokenType = tokenContent.slice(0, pipeIdx);
3417
- if (tokenType !== "field" && tokenType !== "var") {
3418
- errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "var")`);
5080
+ if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
5081
+ errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
3419
5082
  } else {
3420
5083
  const body = tokenContent.slice(pipeIdx + 1);
3421
5084
  const pairs = body.split("|");
@@ -3429,12 +5092,48 @@ function validateMarkdown(markdown) {
3429
5092
  if (!hasName) {
3430
5093
  errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
3431
5094
  }
5095
+ } else if (tokenType === "image") {
5096
+ const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
5097
+ if (!hasSource) {
5098
+ errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
5099
+ }
3432
5100
  }
3433
5101
  }
3434
5102
  }
3435
5103
  searchFrom = closeIdx + 2;
3436
5104
  }
3437
5105
  }
5106
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
5107
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
5108
+ const directiveStack = [];
5109
+ for (let i = 0; i < lines.length; i++) {
5110
+ const trimmed = lines[i].trim();
5111
+ const lineNum = i + 1;
5112
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
5113
+ if (openMatch) {
5114
+ const dtype = openMatch[1];
5115
+ if (dtype !== "watermark") {
5116
+ if (dtype === "col") {
5117
+ const parent = directiveStack[directiveStack.length - 1];
5118
+ if (!parent || parent.type !== "columns") {
5119
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
5120
+ }
5121
+ }
5122
+ directiveStack.push({ type: dtype, line: lineNum });
5123
+ }
5124
+ continue;
5125
+ }
5126
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
5127
+ if (directiveStack.length === 0) {
5128
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
5129
+ } else {
5130
+ directiveStack.pop();
5131
+ }
5132
+ }
5133
+ }
5134
+ for (const open of directiveStack) {
5135
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
5136
+ }
3438
5137
  let fields = [];
3439
5138
  let variables = [];
3440
5139
  try {
@@ -3468,31 +5167,6 @@ function validateMarkdown(markdown) {
3468
5167
  fields
3469
5168
  };
3470
5169
  }
3471
- var sizeClasses = {
3472
- md: "px-3 py-1.5 text-xs font-medium",
3473
- sm: "px-2 py-0.5 text-[10px] font-medium"
3474
- };
3475
- function ToggleGroup({
3476
- value,
3477
- onChange,
3478
- options,
3479
- size = "md",
3480
- className
3481
- }) {
3482
- return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsx(
3483
- "button",
3484
- {
3485
- className: cn(
3486
- "rounded-md transition-colors",
3487
- sizeClasses[size],
3488
- value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
3489
- ),
3490
- onClick: () => onChange(opt.value),
3491
- children: opt.label
3492
- },
3493
- opt.value
3494
- )) });
3495
- }
3496
5170
  function parseCsv(text) {
3497
5171
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3498
5172
  if (lines.length < 2) return [];
@@ -3542,7 +5216,10 @@ function GeneratePanel({
3542
5216
  editorContent,
3543
5217
  editorVariables,
3544
5218
  onGeneratePdf,
3545
- initialBulkData
5219
+ initialBulkData,
5220
+ initialVariableValues,
5221
+ exportFileName,
5222
+ templateMode
3546
5223
  }) {
3547
5224
  const [templateSource, setTemplateSource] = useState("editor");
3548
5225
  const [importedMarkdown, setImportedMarkdown] = useState(null);
@@ -3550,6 +5227,7 @@ function GeneratePanel({
3550
5227
  const [validationResult, setValidationResult] = useState(null);
3551
5228
  const [mode, setMode] = useState("single");
3552
5229
  const [variableValues, setVariableValues] = useState({});
5230
+ const [variableSearch, setVariableSearch] = useState("");
3553
5231
  const [bulkInputFormat, setBulkInputFormat] = useState("json");
3554
5232
  const [bulkInput, setBulkInput] = useState("");
3555
5233
  const [bulkData, setBulkData] = useState(null);
@@ -3576,11 +5254,22 @@ function GeneratePanel({
3576
5254
  const col = pos - textBefore.lastIndexOf("\n");
3577
5255
  setJsonCursor({ line, col });
3578
5256
  }, []);
5257
+ const lockedVarNames = React12__default.useMemo(
5258
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
5259
+ [templateMode, initialVariableValues]
5260
+ );
3579
5261
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3580
5262
  const hasVariables = activeVariables.length > 0;
3581
5263
  const hasEditorContent = editorMarkdown.trim().length > 0;
3582
5264
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3583
5265
  const hasContent = hasImportedContent || hasEditorContent;
5266
+ const filteredVariables = React12__default.useMemo(() => {
5267
+ if (!variableSearch.trim()) return activeVariables;
5268
+ const q = variableSearch.toLowerCase();
5269
+ return activeVariables.filter(
5270
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
5271
+ );
5272
+ }, [activeVariables, variableSearch]);
3584
5273
  const getActiveContent = useCallback(() => {
3585
5274
  if (templateSource === "imported" && importedMarkdown) {
3586
5275
  return markdownToTiptap(importedMarkdown);
@@ -3748,7 +5437,10 @@ function GeneratePanel({
3748
5437
  setPreviewError(null);
3749
5438
  try {
3750
5439
  const content = getActiveContent();
3751
- const replaced = replaceVariablesInContent(content, variableValues);
5440
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5441
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5442
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5443
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3752
5444
  const fields = extractFieldsFromContent(replaced);
3753
5445
  const result = await generatePdfFromContent(replaced);
3754
5446
  const pages = await pdfToImages(result.pdfBytes);
@@ -3769,7 +5461,10 @@ function GeneratePanel({
3769
5461
  setExportSuccess(null);
3770
5462
  try {
3771
5463
  const content = getActiveContent();
3772
- const replaced = replaceVariablesInContent(content, variableValues);
5464
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5465
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5466
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5467
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3773
5468
  const fields = extractFieldsFromContent(replaced);
3774
5469
  const result = await generatePdfFromContent(replaced, {
3775
5470
  drawFieldPlaceholders: false,
@@ -3777,7 +5472,7 @@ function GeneratePanel({
3777
5472
  fields
3778
5473
  });
3779
5474
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3780
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5475
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3781
5476
  if (onGeneratePdf) {
3782
5477
  onGeneratePdf(blob, fileName);
3783
5478
  } else {
@@ -3849,7 +5544,10 @@ function GeneratePanel({
3849
5544
  const allWarnings = [];
3850
5545
  for (let i = 0; i < bulkData.length; i++) {
3851
5546
  const values = bulkData[i];
3852
- const replaced = replaceVariablesInContent(content, values);
5547
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5548
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5549
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5550
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3853
5551
  const fields = extractFieldsFromContent(replaced);
3854
5552
  const result = await generatePdfFromContent(replaced, {
3855
5553
  drawFieldPlaceholders: false,
@@ -3904,13 +5602,15 @@ ${allWarnings.join("\n")}`);
3904
5602
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3905
5603
  React12__default.useEffect(() => {
3906
5604
  if (templateSource === "editor") {
3907
- const defaults = {};
5605
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3908
5606
  for (const v of editorVariables) {
3909
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5607
+ if (!defaults[v.varName]) {
5608
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5609
+ }
3910
5610
  }
3911
5611
  setVariableValues(defaults);
3912
5612
  }
3913
- }, [editorVariables, templateSource]);
5613
+ }, [editorVariables, templateSource, initialVariableValues]);
3914
5614
  useEffect(() => {
3915
5615
  setPreviewFresh(false);
3916
5616
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3922,8 +5622,8 @@ ${allWarnings.join("\n")}`);
3922
5622
  }, [exportSuccess]);
3923
5623
  return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3924
5624
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
3925
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
3926
- /* @__PURE__ */ jsxs("div", { children: [
5625
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5626
+ !templateMode && /* @__PURE__ */ jsxs("div", { children: [
3927
5627
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3928
5628
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3929
5629
  templateSource === "imported" && /* @__PURE__ */ jsxs(
@@ -4016,10 +5716,10 @@ ${allWarnings.join("\n")}`);
4016
5716
  !hasContent && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4017
5717
  /* @__PURE__ */ jsx(Braces, { size: 32, className: "mb-2 opacity-50" }),
4018
5718
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "No template content" }),
4019
- /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Import a .md template above or add content in the Editor tab." })
5719
+ /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: templateMode ? "No template content was provided." : "Import a .md template above or add content in the Editor tab." })
4020
5720
  ] }),
4021
5721
  hasVariables && hasContent && /* @__PURE__ */ jsxs("div", { className: "border border-border rounded-lg overflow-hidden", children: [
4022
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsx(
5722
+ !templateMode && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsx(
4023
5723
  ToggleGroup,
4024
5724
  {
4025
5725
  value: mode,
@@ -4031,20 +5731,53 @@ ${allWarnings.join("\n")}`);
4031
5731
  }
4032
5732
  ) }),
4033
5733
  mode === "single" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4034
- /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
4035
- activeVariables.map((v) => /* @__PURE__ */ jsxs("div", { children: [
4036
- /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5734
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
5735
+ /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5736
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5737
+ ] }),
5738
+ activeVariables.length > 5 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
5739
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4037
5740
  /* @__PURE__ */ jsx(
4038
5741
  Input,
4039
5742
  {
4040
- id: `gen-${v.varName}`,
4041
- value: variableValues[v.varName] || "",
4042
- onChange: (e) => updateVariableValue(v.varName, e.target.value),
4043
- placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
4044
- className: "h-8 text-xs"
5743
+ value: variableSearch,
5744
+ onChange: (e) => setVariableSearch(e.target.value),
5745
+ placeholder: "Search variables...",
5746
+ className: "h-8 text-xs pl-8"
5747
+ }
5748
+ ),
5749
+ variableSearch && /* @__PURE__ */ jsx(
5750
+ "button",
5751
+ {
5752
+ type: "button",
5753
+ onClick: () => setVariableSearch(""),
5754
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5755
+ children: /* @__PURE__ */ jsx(X, { size: 12 })
4045
5756
  }
4046
5757
  )
4047
- ] }, v.varName))
5758
+ ] }),
5759
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5760
+ "No variables match \u201C",
5761
+ variableSearch,
5762
+ "\u201D"
5763
+ ] }),
5764
+ filteredVariables.map((v) => {
5765
+ const isLocked = lockedVarNames.has(v.varName);
5766
+ return /* @__PURE__ */ jsxs("div", { children: [
5767
+ /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5768
+ /* @__PURE__ */ jsx(
5769
+ Input,
5770
+ {
5771
+ id: `gen-${v.varName}`,
5772
+ value: variableValues[v.varName] || "",
5773
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5774
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5775
+ disabled: isLocked,
5776
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5777
+ }
5778
+ )
5779
+ ] }, v.varName);
5780
+ })
4048
5781
  ] }),
4049
5782
  mode === "bulk" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4050
5783
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4233,56 +5966,60 @@ ${allWarnings.join("\n")}`);
4233
5966
  ] }),
4234
5967
  !hasVariables && hasContent && /* @__PURE__ */ jsx("div", { className: "p-3 rounded-md bg-muted/30 border border-border", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: 'This template has no variables. You can still preview and export the PDF as-is, or add variables in the Editor tab using the "Insert Variable" button.' }) })
4235
5968
  ] }),
4236
- previewError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
4237
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4238
- /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: previewError })
4239
- ] }) }) }),
4240
- exportError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
4241
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4242
- /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: exportError })
4243
- ] }) }) }),
4244
- exportSuccess && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-emerald-500/10 border border-emerald-500/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
4245
- /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "text-emerald-600 shrink-0" }),
4246
- /* @__PURE__ */ jsx("p", { className: "text-xs text-emerald-700", children: exportSuccess })
4247
- ] }) }) }),
4248
- hasContent && /* @__PURE__ */ jsx("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
4249
- /* @__PURE__ */ jsxs(
4250
- Button,
4251
- {
4252
- variant: "secondary",
4253
- onClick: handlePreview,
4254
- disabled: isGenerating,
4255
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4256
- children: [
4257
- isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
4258
- isGenerating ? "Generating..." : "Generate PDF"
4259
- ]
4260
- }
4261
- ),
4262
- /* @__PURE__ */ jsxs(
5969
+ hasContent && /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: [
5970
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
5971
+ /* @__PURE__ */ jsxs(
5972
+ Button,
5973
+ {
5974
+ variant: "secondary",
5975
+ onClick: handlePreview,
5976
+ disabled: isGenerating,
5977
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5978
+ children: [
5979
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
5980
+ isGenerating ? "Generating..." : "Generate PDF"
5981
+ ]
5982
+ }
5983
+ ),
5984
+ /* @__PURE__ */ jsxs(
5985
+ Button,
5986
+ {
5987
+ onClick: handleExportSingle,
5988
+ disabled: !previewFresh || isExporting,
5989
+ 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",
5990
+ children: [
5991
+ isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
5992
+ isExporting ? "Exporting..." : "Export PDF"
5993
+ ]
5994
+ }
5995
+ )
5996
+ ] }) : /* @__PURE__ */ jsxs(
4263
5997
  Button,
4264
5998
  {
4265
- onClick: handleExportSingle,
4266
- disabled: !previewFresh || isExporting,
4267
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5999
+ onClick: handleBulkGenerate,
6000
+ disabled: isExporting || !bulkData || bulkData.length === 0,
6001
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
4268
6002
  children: [
4269
6003
  isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4270
- isExporting ? "Exporting..." : "Export PDF"
6004
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4271
6005
  ]
4272
6006
  }
4273
- )
4274
- ] }) : /* @__PURE__ */ jsxs(
4275
- Button,
4276
- {
4277
- onClick: handleBulkGenerate,
4278
- disabled: isExporting || !bulkData || bulkData.length === 0,
4279
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
4280
- children: [
4281
- isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4282
- isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4283
- ]
4284
- }
4285
- ) })
6007
+ ),
6008
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6009
+ previewError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6010
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6011
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: previewError })
6012
+ ] }),
6013
+ exportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6014
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6015
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportError })
6016
+ ] }),
6017
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6018
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
6019
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportSuccess })
6020
+ ] })
6021
+ ] })
6022
+ ] })
4286
6023
  ] }),
4287
6024
  /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4288
6025
  /* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsx(
@@ -4460,7 +6197,13 @@ function DocumentGeneratorInner({
4460
6197
  showToolbar = true,
4461
6198
  showGenerateTab = true,
4462
6199
  onGeneratePdf,
4463
- initialBulkData
6200
+ initialBulkData,
6201
+ defaultTab = "editor",
6202
+ initialVariableValues,
6203
+ predefinedVariables,
6204
+ exportFileName,
6205
+ templateMode,
6206
+ onSaveTemplate
4464
6207
  }) {
4465
6208
  const {
4466
6209
  editor,
@@ -4487,7 +6230,7 @@ function DocumentGeneratorInner({
4487
6230
  placeholder,
4488
6231
  onChange
4489
6232
  });
4490
- const [activeTab, setActiveTab] = useState("editor");
6233
+ const [activeTab, setActiveTab] = useState(defaultTab);
4491
6234
  const [insertPopoverOpen, setInsertPopoverOpen] = useState(false);
4492
6235
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = useState(false);
4493
6236
  const [editFieldId, setEditFieldId] = useState(null);
@@ -4499,6 +6242,7 @@ function DocumentGeneratorInner({
4499
6242
  const editorWrapperRef = useRef(null);
4500
6243
  const [editorCollapsed, setEditorCollapsed] = useState(false);
4501
6244
  const [mdExportError, setMdExportError] = useState(null);
6245
+ const [isSaving, setIsSaving] = useState(false);
4502
6246
  const [exportSuccess, setExportSuccess] = useState(null);
4503
6247
  useEffect(() => {
4504
6248
  const handler = (e) => {
@@ -4569,7 +6313,28 @@ function DocumentGeneratorInner({
4569
6313
  onExport(result);
4570
6314
  setExportSuccess("Document exported successfully");
4571
6315
  }
4572
- }, [exportDocument, onExport]);
6316
+ }, [exportDocument, onExport]);
6317
+ const handleSaveTemplate = useCallback(async () => {
6318
+ const md = exportMarkdown();
6319
+ if (!md.trim()) {
6320
+ setMdExportError("Editor content is empty");
6321
+ return;
6322
+ }
6323
+ const validation = validateMarkdown(md);
6324
+ if (!validation.valid) {
6325
+ setMdExportError(validation.errors.join("; "));
6326
+ return;
6327
+ }
6328
+ setMdExportError(null);
6329
+ setIsSaving(true);
6330
+ try {
6331
+ await onSaveTemplate?.(md);
6332
+ setExportSuccess("Template saved");
6333
+ } catch {
6334
+ } finally {
6335
+ setIsSaving(false);
6336
+ }
6337
+ }, [exportMarkdown, onSaveTemplate]);
4573
6338
  const handleExportMarkdown = useCallback(() => {
4574
6339
  const md = exportMarkdown();
4575
6340
  if (!md.trim()) {
@@ -4586,10 +6351,10 @@ function DocumentGeneratorInner({
4586
6351
  const url = URL.createObjectURL(blob);
4587
6352
  const a = document.createElement("a");
4588
6353
  a.href = url;
4589
- a.download = "template.md";
6354
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4590
6355
  a.click();
4591
6356
  URL.revokeObjectURL(url);
4592
- }, [exportMarkdown]);
6357
+ }, [exportMarkdown, exportFileName]);
4593
6358
  useEffect(() => {
4594
6359
  if (mdExportError) setMdExportError(null);
4595
6360
  }, [markdown]);
@@ -4625,6 +6390,7 @@ function DocumentGeneratorInner({
4625
6390
  open: insertVarPopoverOpen,
4626
6391
  onOpenChange: setInsertVarPopoverOpen,
4627
6392
  onInsert: handleInsertVariable,
6393
+ predefinedVariables,
4628
6394
  children: /* @__PURE__ */ jsxs(
4629
6395
  Button,
4630
6396
  {
@@ -4665,118 +6431,141 @@ function DocumentGeneratorInner({
4665
6431
  ] })
4666
6432
  ] })
4667
6433
  ] }),
4668
- /* @__PURE__ */ jsxs("div", { className: cn(
6434
+ /* @__PURE__ */ jsx("div", { className: cn(
4669
6435
  "flex flex-col flex-1 min-h-0",
4670
6436
  activeTab !== "editor" && "hidden"
6437
+ ), children: /* @__PURE__ */ jsxs("div", { className: cn(
6438
+ "flex-1 min-h-0",
6439
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4671
6440
  ), children: [
4672
- /* @__PURE__ */ jsxs("div", { className: cn(
4673
- "flex-1 min-h-0",
4674
- showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4675
- ), children: [
4676
- !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
4677
- showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
4678
- EditorToolbar,
4679
- {
4680
- editor,
4681
- insertFieldButton,
4682
- insertVariableButton,
4683
- onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4684
- }
4685
- ) }),
4686
- /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsx(
4687
- EditorContent,
4688
- {
4689
- editor,
4690
- className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4691
- }
4692
- ) })
4693
- ] }),
4694
- showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4695
- showPreview && /* @__PURE__ */ jsx(
4696
- PreviewPanel,
6441
+ !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6442
+ showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
6443
+ EditorToolbar,
4697
6444
  {
4698
- pages: pdfPages,
4699
- isGenerating,
4700
- positionedFields,
4701
- headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
4702
- Button,
4703
- {
4704
- variant: "ghost",
4705
- size: "sm",
4706
- className: "h-8 w-8 p-0",
4707
- onClick: () => setEditorCollapsed(false),
4708
- children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
4709
- }
4710
- ) : void 0,
4711
- className: "flex-1 border-0 rounded-none"
6445
+ editor,
6446
+ insertFieldButton,
6447
+ insertVariableButton,
6448
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4712
6449
  }
4713
- )
4714
- ] }),
4715
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
4716
- /* @__PURE__ */ jsxs(
4717
- Button,
6450
+ ) }),
6451
+ /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsx(
6452
+ EditorContent,
4718
6453
  {
4719
- variant: "secondary",
4720
- onClick: generatePdf,
4721
- disabled: isGenerating || !editor,
4722
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4723
- children: [
4724
- isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
4725
- isGenerating ? "Generating..." : "Generate PDF"
4726
- ]
6454
+ editor,
6455
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4727
6456
  }
4728
- ),
4729
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
4730
- mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4731
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4732
- /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
4733
- ] }),
4734
- generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4735
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4736
- /* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
4737
- ] }),
4738
- fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
4739
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4740
- /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
4741
- fieldWarnings.length,
4742
- " field",
4743
- fieldWarnings.length !== 1 ? "s" : "",
4744
- " had warnings during generation"
4745
- ] })
4746
- ] }),
4747
- exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
4748
- /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
4749
- /* @__PURE__ */ jsx("span", { children: exportSuccess })
4750
- ] })
4751
- ] }),
4752
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6457
+ ) }),
6458
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
4753
6459
  /* @__PURE__ */ jsxs(
4754
6460
  Button,
4755
6461
  {
4756
- onClick: handleExportMarkdown,
4757
- disabled: !editor || !markdown.trim(),
4758
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6462
+ variant: "secondary",
6463
+ onClick: generatePdf,
6464
+ disabled: isGenerating || !editor,
6465
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4759
6466
  children: [
4760
- /* @__PURE__ */ jsx(FileText, { size: 16 }),
4761
- "Export MD"
6467
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
6468
+ isGenerating ? "Generating..." : "Generate PDF"
4762
6469
  ]
4763
6470
  }
4764
6471
  ),
4765
- /* @__PURE__ */ jsxs(
6472
+ onSaveTemplate ? /* @__PURE__ */ jsxs(
4766
6473
  Button,
4767
6474
  {
4768
- onClick: handleExport,
4769
- disabled: isGenerating || !editor || pdfPages.length === 0,
6475
+ onClick: handleSaveTemplate,
6476
+ disabled: isSaving || !editor || !markdown.trim(),
4770
6477
  className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4771
6478
  children: [
4772
- /* @__PURE__ */ jsx(FileDown, { size: 16 }),
4773
- exportButtonText
6479
+ isSaving ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Save, { size: 16 }),
6480
+ isSaving ? "Saving..." : exportButtonText || "Save Template"
4774
6481
  ]
4775
6482
  }
4776
- )
6483
+ ) : /* @__PURE__ */ jsxs(Popover, { children: [
6484
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
6485
+ Button,
6486
+ {
6487
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6488
+ 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",
6489
+ children: [
6490
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6491
+ "Export",
6492
+ /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
6493
+ ]
6494
+ }
6495
+ ) }),
6496
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6497
+ /* @__PURE__ */ jsxs(
6498
+ "button",
6499
+ {
6500
+ onClick: handleExportMarkdown,
6501
+ disabled: !editor || !markdown.trim(),
6502
+ 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",
6503
+ children: [
6504
+ /* @__PURE__ */ jsx(FileText, { size: 16 }),
6505
+ "Export as MD"
6506
+ ]
6507
+ }
6508
+ ),
6509
+ /* @__PURE__ */ jsxs(
6510
+ "button",
6511
+ {
6512
+ onClick: handleExport,
6513
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6514
+ 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",
6515
+ children: [
6516
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6517
+ "Export as PDF"
6518
+ ]
6519
+ }
6520
+ )
6521
+ ] })
6522
+ ] }),
6523
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6524
+ mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6525
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6526
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
6527
+ ] }),
6528
+ generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6529
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6530
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
6531
+ ] }),
6532
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6533
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6534
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
6535
+ fieldWarnings.length,
6536
+ " field",
6537
+ fieldWarnings.length !== 1 ? "s" : "",
6538
+ " had warnings during generation"
6539
+ ] })
6540
+ ] }),
6541
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6542
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
6543
+ /* @__PURE__ */ jsx("span", { children: exportSuccess })
6544
+ ] })
6545
+ ] })
4777
6546
  ] })
4778
- ] })
4779
- ] }),
6547
+ ] }),
6548
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
6549
+ showPreview && /* @__PURE__ */ jsx(
6550
+ PreviewPanel,
6551
+ {
6552
+ pages: pdfPages,
6553
+ isGenerating,
6554
+ positionedFields,
6555
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
6556
+ Button,
6557
+ {
6558
+ variant: "ghost",
6559
+ size: "sm",
6560
+ className: "h-8 w-8 p-0",
6561
+ onClick: () => setEditorCollapsed(false),
6562
+ children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
6563
+ }
6564
+ ) : void 0,
6565
+ className: "flex-1 border-0 rounded-none"
6566
+ }
6567
+ )
6568
+ ] }) }),
4780
6569
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsx(
4781
6570
  GeneratePanel,
4782
6571
  {
@@ -4784,7 +6573,10 @@ function DocumentGeneratorInner({
4784
6573
  editorContent: editor?.getJSON(),
4785
6574
  editorVariables: variables,
4786
6575
  onGeneratePdf,
4787
- initialBulkData
6576
+ initialBulkData,
6577
+ initialVariableValues,
6578
+ exportFileName,
6579
+ templateMode
4788
6580
  }
4789
6581
  ),
4790
6582
  /* @__PURE__ */ jsx(
@@ -4797,7 +6589,7 @@ function DocumentGeneratorInner({
4797
6589
  onDelete: deleteField,
4798
6590
  onClose: handleCloseFieldEditPopover
4799
6591
  },
4800
- editFieldId
6592
+ editFieldId ?? "field-edit"
4801
6593
  ),
4802
6594
  /* @__PURE__ */ jsx(
4803
6595
  VariableEditPopover,
@@ -4809,7 +6601,7 @@ function DocumentGeneratorInner({
4809
6601
  onDelete: deleteVariable,
4810
6602
  onClose: handleCloseVarEditPopover
4811
6603
  },
4812
- editVarName
6604
+ editVarName ?? "var-edit"
4813
6605
  )
4814
6606
  ] });
4815
6607
  }
@@ -4925,6 +6717,717 @@ function EditorPanel({
4925
6717
  ] });
4926
6718
  }
4927
6719
 
4928
- export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, ThemeProvider, useDocumentGenerator, useTheme };
6720
+ // src/utils/xml-template-parser.ts
6721
+ function extractFieldRefs(expr) {
6722
+ const refs = [];
6723
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6724
+ let m;
6725
+ while ((m = re.exec(expr)) !== null) {
6726
+ if (m[2]) {
6727
+ refs.push(`${m[1]}.${m[2]}`);
6728
+ } else {
6729
+ refs.push(m[1]);
6730
+ }
6731
+ }
6732
+ return refs;
6733
+ }
6734
+ function fieldRefToVarName(ref, bandContext) {
6735
+ if (ref.includes(".")) {
6736
+ return labelToVarName(ref.replace(/\./g, "_"));
6737
+ }
6738
+ if (bandContext) {
6739
+ return labelToVarName(bandContext + "_" + ref);
6740
+ }
6741
+ return labelToVarName(ref);
6742
+ }
6743
+ function fieldRefToLabel(ref) {
6744
+ const parts = ref.split(".");
6745
+ const raw = parts[parts.length - 1];
6746
+ return raw.replace(/_/g, " ");
6747
+ }
6748
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6749
+ const trimmed = expr.trim();
6750
+ const suppressZero = /<>\s*0/.test(trimmed);
6751
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6752
+ const aggMatch = trimmed.match(
6753
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6754
+ );
6755
+ if (aggMatch) {
6756
+ const fieldName = aggMatch[2];
6757
+ const varName = labelToVarName("workorder_" + fieldName);
6758
+ const label = fieldName.replace(/_/g, " ");
6759
+ return registry.register(varName, label, szOpts);
6760
+ }
6761
+ const concatSingle = trimmed.match(
6762
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6763
+ );
6764
+ if (concatSingle) {
6765
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6766
+ const varName = fieldRefToVarName(ref, bandContext);
6767
+ const label = fieldRefToLabel(ref);
6768
+ return registry.register(varName, label, szOpts);
6769
+ }
6770
+ const concatMulti = trimmed.match(
6771
+ /^concat\(/i
6772
+ );
6773
+ if (concatMulti) {
6774
+ const refs = extractFieldRefs(trimmed);
6775
+ if (refs.length > 0) {
6776
+ const uniqueRefs = [...new Set(refs)];
6777
+ const tokens = uniqueRefs.map((ref) => {
6778
+ const varName = fieldRefToVarName(ref, bandContext);
6779
+ const label = fieldRefToLabel(ref);
6780
+ return registry.register(varName, label, szOpts);
6781
+ });
6782
+ return tokens.join(" ");
6783
+ }
6784
+ }
6785
+ const toLongMatch = trimmed.match(
6786
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6787
+ );
6788
+ if (toLongMatch) {
6789
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6790
+ const varName = fieldRefToVarName(ref, bandContext);
6791
+ const label = fieldRefToLabel(ref);
6792
+ return registry.register(varName, label, szOpts);
6793
+ }
6794
+ const fmtMatch = trimmed.match(
6795
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6796
+ );
6797
+ if (fmtMatch) {
6798
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6799
+ }
6800
+ const replaceMatch = trimmed.match(
6801
+ /^Replace\(\s*\[([^\]]+)\]/i
6802
+ );
6803
+ if (replaceMatch) {
6804
+ const ref = replaceMatch[1];
6805
+ const varName = fieldRefToVarName(ref, bandContext);
6806
+ const label = fieldRefToLabel(ref);
6807
+ return registry.register(varName, label, szOpts);
6808
+ }
6809
+ const iifLabelMatch = trimmed.match(
6810
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6811
+ );
6812
+ if (iifLabelMatch) {
6813
+ return iifLabelMatch[1];
6814
+ }
6815
+ const iifLabelElse = trimmed.match(
6816
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6817
+ );
6818
+ if (iifLabelElse) {
6819
+ return iifLabelElse[1];
6820
+ }
6821
+ const iifEmptyElse = trimmed.match(
6822
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6823
+ );
6824
+ if (iifEmptyElse) {
6825
+ return iifEmptyElse[1];
6826
+ }
6827
+ const nestedIifStaticLabel = trimmed.match(
6828
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6829
+ );
6830
+ if (nestedIifStaticLabel) {
6831
+ return nestedIifStaticLabel[1];
6832
+ }
6833
+ const iifValueMatch = trimmed.match(
6834
+ /^iif\s*\(/i
6835
+ );
6836
+ if (iifValueMatch) {
6837
+ const refs = extractFieldRefs(trimmed);
6838
+ if (refs.length > 0) {
6839
+ const uniqueRefs = [...new Set(refs)];
6840
+ const innerAggMatch = trimmed.match(
6841
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6842
+ );
6843
+ if (innerAggMatch) {
6844
+ const fieldName = innerAggMatch[2];
6845
+ const varName2 = labelToVarName("workorder_" + fieldName);
6846
+ const label2 = fieldName.replace(/_/g, " ");
6847
+ return registry.register(varName2, label2, szOpts);
6848
+ }
6849
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6850
+ const varName = fieldRefToVarName(ref, bandContext);
6851
+ const label = fieldRefToLabel(ref);
6852
+ return registry.register(varName, label, szOpts);
6853
+ }
6854
+ const staticMatch = trimmed.match(/'([^']+)'/);
6855
+ if (staticMatch) return staticMatch[1];
6856
+ return "";
6857
+ }
6858
+ const arithmeticRefs = extractFieldRefs(trimmed);
6859
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6860
+ if (siblingLabel) {
6861
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6862
+ const varName = labelToVarName(
6863
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6864
+ );
6865
+ return registry.register(varName, cleanLabel, szOpts);
6866
+ }
6867
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6868
+ const tokens = uniqueRefs.map((ref) => {
6869
+ const varName = fieldRefToVarName(ref, bandContext);
6870
+ const label = fieldRefToLabel(ref);
6871
+ return registry.register(varName, label, szOpts);
6872
+ });
6873
+ return tokens.join(" ");
6874
+ }
6875
+ const simpleMatch = trimmed.match(
6876
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6877
+ );
6878
+ if (simpleMatch) {
6879
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6880
+ const varName = fieldRefToVarName(ref, bandContext);
6881
+ const label = fieldRefToLabel(ref);
6882
+ return registry.register(varName, label, szOpts);
6883
+ }
6884
+ if (arithmeticRefs.length >= 1) {
6885
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6886
+ const varName = fieldRefToVarName(ref, bandContext);
6887
+ const label = fieldRefToLabel(ref);
6888
+ return registry.register(varName, label, szOpts);
6889
+ }
6890
+ return "";
6891
+ }
6892
+ var VariableRegistry = class {
6893
+ constructor() {
6894
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6895
+ }
6896
+ register(varName, varLabel, options) {
6897
+ if (!this.map.has(varName)) {
6898
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6899
+ }
6900
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6901
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6902
+ }
6903
+ getAll() {
6904
+ return Array.from(this.map.values());
6905
+ }
6906
+ };
6907
+ function parseLocationFloat(s) {
6908
+ if (!s) return { x: 0, y: 0 };
6909
+ const parts = s.split(",");
6910
+ return {
6911
+ x: parseFloat(parts[0]) || 0,
6912
+ y: parseFloat(parts[1]) || 0
6913
+ };
6914
+ }
6915
+ function parseSizeF(s) {
6916
+ if (!s) return { w: 0, h: 0 };
6917
+ const parts = s.split(",");
6918
+ return {
6919
+ w: parseFloat(parts[0]) || 0,
6920
+ h: parseFloat(parts[1]) || 0
6921
+ };
6922
+ }
6923
+ function extractControls(bandEl) {
6924
+ const controlsEl = bandEl.querySelector(":scope > Controls");
6925
+ if (!controlsEl) return [];
6926
+ const items = [];
6927
+ for (const child of Array.from(controlsEl.children)) {
6928
+ items.push(extractControl(child));
6929
+ }
6930
+ return items;
6931
+ }
6932
+ function extractControl(el) {
6933
+ const ctrlType = el.getAttribute("ControlType") || "";
6934
+ const name = el.getAttribute("Name") || "";
6935
+ const text = el.getAttribute("Text") || "";
6936
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
6937
+ const size = parseSizeF(el.getAttribute("SizeF"));
6938
+ const formatString = el.getAttribute("TextFormatString") || null;
6939
+ let expression = null;
6940
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
6941
+ if (bindingsEl) {
6942
+ for (const item of Array.from(bindingsEl.children)) {
6943
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
6944
+ expression = item.getAttribute("Expression");
6945
+ break;
6946
+ }
6947
+ }
6948
+ }
6949
+ const children = [];
6950
+ if (ctrlType === "XRPanel") {
6951
+ const panelControls = extractControls(el);
6952
+ children.push(...panelControls);
6953
+ }
6954
+ return {
6955
+ name,
6956
+ type: ctrlType,
6957
+ text,
6958
+ x: loc.x,
6959
+ y: loc.y,
6960
+ width: size.w,
6961
+ expression,
6962
+ formatString,
6963
+ children
6964
+ };
6965
+ }
6966
+ function groupByRow(controls, threshold = 8) {
6967
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
6968
+ const rows = [];
6969
+ for (const ctrl of sorted) {
6970
+ const lastRow = rows[rows.length - 1];
6971
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
6972
+ lastRow.controls.push(ctrl);
6973
+ } else {
6974
+ rows.push({ y: ctrl.y, controls: [ctrl] });
6975
+ }
6976
+ }
6977
+ for (const row of rows) {
6978
+ row.controls.sort((a, b) => a.x - b.x);
6979
+ }
6980
+ return rows;
6981
+ }
6982
+ function isStaticLabel(ctrl) {
6983
+ return ctrl.type === "XRLabel" && !ctrl.expression;
6984
+ }
6985
+ function isBound(ctrl) {
6986
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
6987
+ }
6988
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
6989
+ const leftExtent = leftControls.reduce(
6990
+ (max, c) => Math.max(max, c.x + c.width),
6991
+ 0
6992
+ );
6993
+ const rightMinX = rightControls.reduce(
6994
+ (min, c) => Math.min(min, c.x),
6995
+ contentWidth
6996
+ );
6997
+ const rightMaxExtent = rightControls.reduce(
6998
+ (max, c) => Math.max(max, c.x + c.width),
6999
+ 0
7000
+ );
7001
+ const rightExtent = rightMaxExtent - rightMinX;
7002
+ if (leftExtent + rightExtent === 0) return 50;
7003
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
7004
+ }
7005
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
7006
+ const lines = [];
7007
+ const leftControls = controls.filter((c) => c.x < 437);
7008
+ const rightControls = controls.filter((c) => c.x >= 437);
7009
+ const leftEntries = [];
7010
+ if (leftControls.length > 0) {
7011
+ const leftRows = groupByRow(leftControls);
7012
+ for (const row of leftRows) {
7013
+ const parts = [];
7014
+ for (const ctrl of row.controls) {
7015
+ if (isBound(ctrl)) {
7016
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7017
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
7018
+ resolved = resolved.replace("}}", "|format:phone}}");
7019
+ }
7020
+ if (resolved) parts.push(resolved);
7021
+ } else if (isStaticLabel(ctrl)) {
7022
+ const text = ctrl.text.trim();
7023
+ if (text && !text.match(/^label\d+$/)) {
7024
+ parts.push(text);
7025
+ }
7026
+ }
7027
+ }
7028
+ if (parts.length > 0) {
7029
+ leftEntries.push(parts.join(" "));
7030
+ }
7031
+ }
7032
+ }
7033
+ const rightEntries = [];
7034
+ if (rightControls.length > 0) {
7035
+ const rightRows = groupByRow(rightControls);
7036
+ for (const row of rightRows) {
7037
+ let label = "";
7038
+ let value = "";
7039
+ for (const ctrl of row.controls) {
7040
+ if (isStaticLabel(ctrl)) {
7041
+ const text = ctrl.text.trim();
7042
+ if (text && !text.match(/^label\d+$/)) {
7043
+ label = text;
7044
+ }
7045
+ } else if (isBound(ctrl)) {
7046
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
7047
+ if (ctrl.expression) {
7048
+ const refs = extractFieldRefs(ctrl.expression);
7049
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
7050
+ value = value.replace("}}", "|suppress:zero}}");
7051
+ }
7052
+ }
7053
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7054
+ value = value.replace("}}", "|format:phone}}");
7055
+ }
7056
+ }
7057
+ }
7058
+ if (label || value) {
7059
+ rightEntries.push({ label, value, y: row.y });
7060
+ }
7061
+ }
7062
+ }
7063
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7064
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7065
+ lines.push(`:::columns{split:${outerSplit}}`);
7066
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
7067
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
7068
+ const xmlLineHeight = 17;
7069
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
7070
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
7071
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
7072
+ lines.push(leftColTag);
7073
+ for (const entry of leftEntries) {
7074
+ lines.push(entry);
7075
+ }
7076
+ lines.push(":::");
7077
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
7078
+ lines.push(rightColTag);
7079
+ if (rightEntries.length > 0) {
7080
+ lines.push("| | |");
7081
+ lines.push("|---|---|");
7082
+ let prevY = -Infinity;
7083
+ for (const entry of rightEntries) {
7084
+ if (prevY > -Infinity && entry.y - prevY > 25) {
7085
+ lines.push("| | |");
7086
+ }
7087
+ const label = entry.label || "";
7088
+ const colon = entry.label ? ":" : "";
7089
+ const value = entry.value ? ` ${entry.value}` : "";
7090
+ lines.push(`| ${label} | ${colon}${value} |`);
7091
+ prevY = entry.y;
7092
+ }
7093
+ }
7094
+ lines.push(":::");
7095
+ lines.push(":::");
7096
+ lines.push("");
7097
+ }
7098
+ return lines.join("\n");
7099
+ }
7100
+ function renderReportHeaderBand(controls, registry, contentWidth) {
7101
+ const lines = [];
7102
+ const leftControls = controls.filter((c) => c.x < 437);
7103
+ const rightControls = controls.filter((c) => c.x >= 437);
7104
+ const leftEntries = [];
7105
+ if (leftControls.length > 0) {
7106
+ const leftRows = groupByRow(leftControls);
7107
+ for (const row of leftRows) {
7108
+ const parts = [];
7109
+ for (const ctrl of row.controls) {
7110
+ if (isBound(ctrl)) {
7111
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7112
+ if (resolved) parts.push(resolved);
7113
+ } else if (isStaticLabel(ctrl)) {
7114
+ const text = ctrl.text.trim();
7115
+ if (text && !text.match(/^label\d+$/)) {
7116
+ parts.push(text);
7117
+ }
7118
+ }
7119
+ }
7120
+ if (parts.length > 0) {
7121
+ leftEntries.push(parts.join(" "));
7122
+ }
7123
+ }
7124
+ }
7125
+ const rightEntries = [];
7126
+ if (rightControls.length > 0) {
7127
+ const rightRows = groupByRow(rightControls);
7128
+ for (const row of rightRows) {
7129
+ let label = "";
7130
+ let value = "";
7131
+ for (const ctrl of row.controls) {
7132
+ if (isStaticLabel(ctrl)) {
7133
+ const text = ctrl.text.trim();
7134
+ if (text && !text.match(/^label\d+$/)) {
7135
+ label = text;
7136
+ }
7137
+ } else if (isBound(ctrl)) {
7138
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
7139
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
7140
+ value = value.replace("}}", "|format:phone}}");
7141
+ }
7142
+ }
7143
+ }
7144
+ if (label || value) {
7145
+ rightEntries.push({ label, value });
7146
+ }
7147
+ }
7148
+ }
7149
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
7150
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
7151
+ lines.push(`:::columns{split:${outerSplit}}`);
7152
+ lines.push(":::col");
7153
+ for (const entry of leftEntries) {
7154
+ lines.push(entry);
7155
+ }
7156
+ lines.push(":::");
7157
+ lines.push(":::col");
7158
+ lines.push("| | |");
7159
+ lines.push("|---|---|");
7160
+ for (const entry of rightEntries) {
7161
+ const label = entry.label || "";
7162
+ const value = entry.value ? ` ${entry.value}` : "";
7163
+ lines.push(`| ${label} |${value} |`);
7164
+ }
7165
+ lines.push(":::");
7166
+ lines.push(":::");
7167
+ lines.push("");
7168
+ }
7169
+ return lines.join("\n");
7170
+ }
7171
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
7172
+ const lines = [];
7173
+ const headerTexts = [];
7174
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
7175
+ if (isStaticLabel(ctrl)) {
7176
+ const text = ctrl.text.trim();
7177
+ if (text) headerTexts.push(text);
7178
+ }
7179
+ }
7180
+ const titleText = headerTexts.join(" ");
7181
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
7182
+ lines.push(":::");
7183
+ lines.push(":::repeat{data:operations}");
7184
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
7185
+ const detailParts = [];
7186
+ for (const ctrl of sortedDetail) {
7187
+ if (isBound(ctrl)) {
7188
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
7189
+ if (resolved) detailParts.push(resolved);
7190
+ }
7191
+ }
7192
+ if (detailParts.length > 0) {
7193
+ if (detailParts.length >= 2) {
7194
+ lines.push(":::columns{split:12}");
7195
+ lines.push(":::col");
7196
+ lines.push(`**${detailParts[0]}**`);
7197
+ lines.push(":::");
7198
+ lines.push(":::col");
7199
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
7200
+ lines.push(":::");
7201
+ lines.push(":::");
7202
+ } else {
7203
+ lines.push(`**${detailParts[0]}**`);
7204
+ }
7205
+ lines.push("");
7206
+ }
7207
+ if (groupFooterControls.length > 0) {
7208
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
7209
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
7210
+ }
7211
+ lines.push("---");
7212
+ lines.push(":::");
7213
+ lines.push("");
7214
+ return lines.join("\n");
7215
+ }
7216
+ function renderGroupFooterBand(controls, registry, _contentWidth) {
7217
+ const rows = groupByRow(controls);
7218
+ const entries = [];
7219
+ for (const row of rows) {
7220
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7221
+ if (labels.length === 0) continue;
7222
+ let label = "";
7223
+ let value = "";
7224
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7225
+ for (const ctrl of sorted) {
7226
+ if (isStaticLabel(ctrl)) {
7227
+ const text = ctrl.text.trim();
7228
+ if (text && !text.match(/^label\d+$/)) {
7229
+ label = text;
7230
+ }
7231
+ } else if (isBound(ctrl)) {
7232
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
7233
+ if (resolved) {
7234
+ if (!resolved.startsWith("{{var|")) {
7235
+ if (!label) label = resolved;
7236
+ } else {
7237
+ value = resolved;
7238
+ }
7239
+ }
7240
+ }
7241
+ }
7242
+ if (label && value) {
7243
+ entries.push({ label, value });
7244
+ }
7245
+ }
7246
+ if (entries.length === 0) return "";
7247
+ const lines = [];
7248
+ lines.push(":::subtotals");
7249
+ for (const entry of entries) {
7250
+ lines.push(`**${entry.label}** ${entry.value}`);
7251
+ }
7252
+ lines.push(":::");
7253
+ lines.push("");
7254
+ return lines.join("\n");
7255
+ }
7256
+ function renderReportFooterBand(controls, registry) {
7257
+ const lines = [];
7258
+ const panel = controls.find((c) => c.type === "XRPanel");
7259
+ const thankYouLabel = controls.find(
7260
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
7261
+ );
7262
+ const priceRows = [];
7263
+ if (panel && panel.children.length > 0) {
7264
+ const rows = groupByRow(panel.children, 15);
7265
+ for (const row of rows) {
7266
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
7267
+ if (labels.length === 0) continue;
7268
+ let label = "";
7269
+ let value = "";
7270
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
7271
+ for (const ctrl of sorted) {
7272
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
7273
+ continue;
7274
+ }
7275
+ const isLabelPos = ctrl.x < 150;
7276
+ if (isStaticLabel(ctrl)) {
7277
+ const text = ctrl.text.trim();
7278
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
7279
+ label = text;
7280
+ }
7281
+ } else if (isBound(ctrl)) {
7282
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
7283
+ if (resolved) {
7284
+ if (isLabelPos) {
7285
+ if (!label) label = resolved;
7286
+ } else {
7287
+ if (!value) value = resolved;
7288
+ }
7289
+ }
7290
+ }
7291
+ }
7292
+ if (label && value) {
7293
+ priceRows.push({ label, value });
7294
+ } else if (value) {
7295
+ priceRows.push({ label: "", value });
7296
+ }
7297
+ }
7298
+ }
7299
+ lines.push(":::columns{split:40|padX:20}");
7300
+ lines.push(":::col{padTop:1}");
7301
+ if (thankYouLabel) {
7302
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
7303
+ }
7304
+ lines.push(":::");
7305
+ lines.push(":::col{padTop:1}");
7306
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
7307
+ if (priceRows.length > 0) {
7308
+ lines.push(":::subtotals");
7309
+ for (const row of priceRows) {
7310
+ const label = row.label ? `**${row.label}**` : "";
7311
+ lines.push(`${label} ${row.value}`);
7312
+ }
7313
+ lines.push(":::");
7314
+ }
7315
+ lines.push(":::");
7316
+ lines.push(":::");
7317
+ lines.push(":::");
7318
+ lines.push("");
7319
+ return lines.join("\n");
7320
+ }
7321
+ function parseXmlTemplate(xmlString) {
7322
+ const warnings = [];
7323
+ const registry = new VariableRegistry();
7324
+ const parser = new DOMParser();
7325
+ const doc = parser.parseFromString(xmlString, "text/xml");
7326
+ const parseError = doc.querySelector("parsererror");
7327
+ if (parseError) {
7328
+ return {
7329
+ markdown: "",
7330
+ variables: [],
7331
+ reportName: "",
7332
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7333
+ };
7334
+ }
7335
+ const root = doc.documentElement;
7336
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7337
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7338
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7339
+ const marginParts = marginsStr.split(",").map(Number);
7340
+ const leftMargin = marginParts[0] || 50;
7341
+ const rightMargin = marginParts[1] || 50;
7342
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7343
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7344
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7345
+ if (calcFieldsEl) {
7346
+ for (const item of Array.from(calcFieldsEl.children)) {
7347
+ const name = item.getAttribute("Name");
7348
+ if (name) calculatedFieldNames.add(name);
7349
+ }
7350
+ }
7351
+ const markdownSections = [];
7352
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7353
+ if (watermarkEl) {
7354
+ const wmText = watermarkEl.getAttribute("Text");
7355
+ if (wmText) {
7356
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7357
+ markdownSections.push("");
7358
+ }
7359
+ }
7360
+ const bandsEl = root.querySelector(":scope > Bands");
7361
+ if (!bandsEl) {
7362
+ warnings.push("No <Bands> element found in the XML");
7363
+ return {
7364
+ markdown: "",
7365
+ variables: registry.getAll(),
7366
+ reportName,
7367
+ warnings
7368
+ };
7369
+ }
7370
+ let topMarginControls = [];
7371
+ let reportHeaderControls = [];
7372
+ let pageHeaderControls = [];
7373
+ let detailControls = [];
7374
+ let groupFooterControls = [];
7375
+ let reportFooterControls = [];
7376
+ for (const bandItem of Array.from(bandsEl.children)) {
7377
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7378
+ if (ctrlType === "TopMarginBand") {
7379
+ topMarginControls = extractControls(bandItem);
7380
+ } else if (ctrlType === "ReportHeaderBand") {
7381
+ reportHeaderControls = extractControls(bandItem);
7382
+ } else if (ctrlType === "PageHeaderBand") {
7383
+ pageHeaderControls = extractControls(bandItem);
7384
+ } else if (ctrlType === "DetailReportBand") {
7385
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7386
+ if (nestedBands) {
7387
+ for (const nested of Array.from(nestedBands.children)) {
7388
+ const nestedType = nested.getAttribute("ControlType") || "";
7389
+ if (nestedType === "DetailBand") {
7390
+ detailControls = extractControls(nested);
7391
+ } else if (nestedType === "GroupFooterBand") {
7392
+ groupFooterControls = extractControls(nested);
7393
+ }
7394
+ }
7395
+ }
7396
+ } else if (ctrlType === "ReportFooterBand") {
7397
+ reportFooterControls = extractControls(bandItem);
7398
+ }
7399
+ }
7400
+ if (topMarginControls.length > 0) {
7401
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7402
+ }
7403
+ if (reportHeaderControls.length > 0) {
7404
+ markdownSections.push(
7405
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7406
+ );
7407
+ }
7408
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7409
+ markdownSections.push(
7410
+ renderOperationsSection(
7411
+ pageHeaderControls,
7412
+ detailControls,
7413
+ groupFooterControls,
7414
+ registry)
7415
+ );
7416
+ }
7417
+ if (reportFooterControls.length > 0) {
7418
+ markdownSections.push(
7419
+ renderReportFooterBand(reportFooterControls, registry)
7420
+ );
7421
+ }
7422
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7423
+ return {
7424
+ markdown,
7425
+ variables: registry.getAll(),
7426
+ reportName,
7427
+ warnings
7428
+ };
7429
+ }
7430
+
7431
+ export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandBlockVariables, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
4929
7432
  //# sourceMappingURL=index.mjs.map
4930
7433
  //# sourceMappingURL=index.mjs.map