@templatical/editor 0.24.1 → 0.25.0

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 (153) hide show
  1. package/dist/{AiChatSidebar-X4viKtvs.js → AiChatSidebar-CIkS8DsO.js} +3 -3
  2. package/dist/{AiFeatureMenu-DDAamRaA.js → AiFeatureMenu-Cji1LBJJ.js} +3 -3
  3. package/dist/{BlockIssueBadge-Qxrl7Ujl.js → BlockIssueBadge-BQiT0FML.js} +2 -2
  4. package/dist/{BlockPreviewCanvas-Cf-kxbEr.js → BlockPreviewCanvas-DAnwZofk.js} +10 -6
  5. package/dist/{CloudEditor-JIzhj1Xq.js → CloudEditor-CXaft6Ej.js} +60 -58
  6. package/dist/{CollaboratorBar-tC7AACTb.js → CollaboratorBar-BR-rL2wG.js} +2 -2
  7. package/dist/{ColorPicker--_2PbzFk.js → ColorPicker-B4ArSUhy.js} +3 -3
  8. package/dist/{CommentsSidebar-Ci2Ov8FG.js → CommentsSidebar-Bm5GLDts.js} +4 -4
  9. package/dist/{CountdownBlock-aZIKfBWY.js → CountdownBlock-CIVAQDVu.js} +1 -1
  10. package/dist/{CountdownToolbar-DHp6T0Q_.js → CountdownToolbar-DBmb3tlb.js} +3 -3
  11. package/dist/{DesignReferenceSidebar-DQsC-3hB.js → DesignReferenceSidebar-CnyJ8L-j.js} +3 -3
  12. package/dist/{IssuesPanel-CoOfib44.js → IssuesPanel-D3XgmYCt.js} +2 -2
  13. package/dist/{MergeTagModeToggle-CiFiSV0C.js → MergeTagModeToggle-BK0fI3P2.js} +3 -3
  14. package/dist/{NumberWithSuffix-CXFbUqsx.js → NumberWithSuffix-Cjj9UNIp.js} +1 -1
  15. package/dist/{ParagraphEditor-Cy-bpEjI.js → ParagraphEditor-jGnbJEA2.js} +8 -8
  16. package/dist/{RichTextEditorContent-CYJko6id.js → RichTextEditorContent-DaaKdzjb.js} +4 -4
  17. package/dist/{SaveBlockDialog-CNTSaMEa.js → SaveBlockDialog-B3ngEldp.js} +7 -7
  18. package/dist/{SavedBlocksBrowserModal-CvxS5-YX.js → SavedBlocksBrowserModal-DawN3XaB.js} +6 -6
  19. package/dist/{SavedBlocksPanels-CNdUxS89.js → SavedBlocksPanels-eDC9Y46N.js} +1 -1
  20. package/dist/{SavedBlocksPickBar-BvXKhblL.js → SavedBlocksPickBar-CQbbXmzj.js} +2 -2
  21. package/dist/{SnapshotHistory-BqPSIV9L.js → SnapshotHistory-COxKtHM-.js} +4 -4
  22. package/dist/{TemplateScoringPanel-CMEuz9yF.js → TemplateScoringPanel-bUuqOO1z.js} +3 -3
  23. package/dist/{TestEmailModal-BDhnnCBx.js → TestEmailModal-CytkbZVQ.js} +87 -56
  24. package/dist/{TestEmailPanel--RwnwLn2.js → TestEmailPanel-CHUY7VZn.js} +1 -1
  25. package/dist/{TitleEditor-F7E_pU9H.js → TitleEditor-aMR1bOyY.js} +7 -7
  26. package/dist/{TplModal-BIiS24Of.js → TplModal-fmYNYdyp.js} +3 -3
  27. package/dist/{blockTypeIcons-C6PnLMp7.js → blockTypeIcons-C6b9rg88.js} +1 -1
  28. package/dist/bundle-stats.json +6 -6
  29. package/dist/{cdn/chunks/ca-CO4nq6TX.js → ca-8Dvbwo50.js} +5 -2
  30. package/dist/cdn/chunks/{AiChatSidebar-UbF3ePzR.js → AiChatSidebar-bkBKpNgE.js} +4 -4
  31. package/dist/cdn/chunks/{AiChatSidebar-UbF3ePzR.js.map → AiChatSidebar-bkBKpNgE.js.map} +1 -1
  32. package/dist/cdn/chunks/{AiFeatureMenu-OIwoJzHG.js → AiFeatureMenu-CI4SYTq2.js} +4 -4
  33. package/dist/cdn/chunks/{AiFeatureMenu-OIwoJzHG.js.map → AiFeatureMenu-CI4SYTq2.js.map} +1 -1
  34. package/dist/cdn/chunks/{BlockIssueBadge-Bd2EuT1C.js → BlockIssueBadge-CRnx8Dv9.js} +3 -3
  35. package/dist/cdn/chunks/{BlockIssueBadge-Bd2EuT1C.js.map → BlockIssueBadge-CRnx8Dv9.js.map} +1 -1
  36. package/dist/cdn/chunks/{BlockPreviewCanvas-C-ut8GkJ.js → BlockPreviewCanvas-CXCWhqbp.js} +11 -7
  37. package/dist/cdn/chunks/BlockPreviewCanvas-CXCWhqbp.js.map +1 -0
  38. package/dist/cdn/chunks/{CloudEditor-D87qsqbZ.js → CloudEditor-D-t7RyDh.js} +62 -60
  39. package/dist/cdn/chunks/CloudEditor-D-t7RyDh.js.map +1 -0
  40. package/dist/cdn/chunks/{CollaboratorBar-Br4RvNrA.js → CollaboratorBar-DcXYo47v.js} +3 -3
  41. package/dist/cdn/chunks/{CollaboratorBar-Br4RvNrA.js.map → CollaboratorBar-DcXYo47v.js.map} +1 -1
  42. package/dist/cdn/chunks/{ColorPicker-B-ymPPtU.js → ColorPicker-_1O7YU2z.js} +4 -4
  43. package/dist/cdn/chunks/{ColorPicker-B-ymPPtU.js.map → ColorPicker-_1O7YU2z.js.map} +1 -1
  44. package/dist/cdn/chunks/{CommentsSidebar-Tv_WTcJv.js → CommentsSidebar-BEgLBYX-.js} +5 -5
  45. package/dist/cdn/chunks/{CommentsSidebar-Tv_WTcJv.js.map → CommentsSidebar-BEgLBYX-.js.map} +1 -1
  46. package/dist/cdn/chunks/{CountdownBlock-cEcXSK91.js → CountdownBlock-CPnQA44g.js} +2 -2
  47. package/dist/cdn/chunks/{CountdownBlock-cEcXSK91.js.map → CountdownBlock-CPnQA44g.js.map} +1 -1
  48. package/dist/cdn/chunks/{CountdownToolbar-CLp3_ebk.js → CountdownToolbar-Cdo-30U4.js} +4 -4
  49. package/dist/cdn/chunks/{CountdownToolbar-CLp3_ebk.js.map → CountdownToolbar-Cdo-30U4.js.map} +1 -1
  50. package/dist/cdn/chunks/{DesignReferenceSidebar-YeqxSsxO.js → DesignReferenceSidebar-DPqBr-ut.js} +4 -4
  51. package/dist/cdn/chunks/{DesignReferenceSidebar-YeqxSsxO.js.map → DesignReferenceSidebar-DPqBr-ut.js.map} +1 -1
  52. package/dist/cdn/chunks/{IssuesPanel-BhsvC3XQ.js → IssuesPanel-DoQWb23r.js} +3 -3
  53. package/dist/cdn/chunks/{IssuesPanel-BhsvC3XQ.js.map → IssuesPanel-DoQWb23r.js.map} +1 -1
  54. package/dist/cdn/chunks/{MergeTagModeToggle-DrdxQA0K.js → MergeTagModeToggle-CWuVC3MU.js} +4 -4
  55. package/dist/cdn/chunks/{MergeTagModeToggle-DrdxQA0K.js.map → MergeTagModeToggle-CWuVC3MU.js.map} +1 -1
  56. package/dist/cdn/chunks/{NumberWithSuffix-BK_Xh4w4.js → NumberWithSuffix-DQ4sgvt5.js} +2 -2
  57. package/dist/cdn/chunks/{NumberWithSuffix-BK_Xh4w4.js.map → NumberWithSuffix-DQ4sgvt5.js.map} +1 -1
  58. package/dist/cdn/chunks/{ParagraphEditor-DKz4vTWB.js → ParagraphEditor-Uv5LPF3W.js} +9 -9
  59. package/dist/cdn/chunks/{ParagraphEditor-DKz4vTWB.js.map → ParagraphEditor-Uv5LPF3W.js.map} +1 -1
  60. package/dist/cdn/chunks/{RichTextEditorContent-CJ2WVicQ.js → RichTextEditorContent-Bt3s09qY.js} +5 -5
  61. package/dist/cdn/chunks/{RichTextEditorContent-CJ2WVicQ.js.map → RichTextEditorContent-Bt3s09qY.js.map} +1 -1
  62. package/dist/cdn/chunks/{SaveBlockDialog-DTemkKPX.js → SaveBlockDialog-BAew6EFd.js} +8 -8
  63. package/dist/cdn/chunks/{SaveBlockDialog-DTemkKPX.js.map → SaveBlockDialog-BAew6EFd.js.map} +1 -1
  64. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DbMaIbeH.js → SavedBlocksBrowserModal-Dn4IJDJW.js} +6 -6
  65. package/dist/cdn/chunks/{SavedBlocksBrowserModal-DbMaIbeH.js.map → SavedBlocksBrowserModal-Dn4IJDJW.js.map} +1 -1
  66. package/dist/cdn/chunks/{SavedBlocksPanels-CUffuDAC.js → SavedBlocksPanels-B62U5DYW.js} +2 -2
  67. package/dist/cdn/chunks/{SavedBlocksPanels-CUffuDAC.js.map → SavedBlocksPanels-B62U5DYW.js.map} +1 -1
  68. package/dist/cdn/chunks/{SavedBlocksPickBar-D7SmNz2n.js → SavedBlocksPickBar-CyRKZg2T.js} +3 -3
  69. package/dist/cdn/chunks/{SavedBlocksPickBar-D7SmNz2n.js.map → SavedBlocksPickBar-CyRKZg2T.js.map} +1 -1
  70. package/dist/cdn/chunks/{SnapshotHistory-m5Z8bTej.js → SnapshotHistory-QQt0DKey.js} +4 -4
  71. package/dist/cdn/chunks/{SnapshotHistory-m5Z8bTej.js.map → SnapshotHistory-QQt0DKey.js.map} +1 -1
  72. package/dist/cdn/chunks/{TemplateScoringPanel-DgRU5fty.js → TemplateScoringPanel-BwyCMcit.js} +65 -65
  73. package/dist/cdn/chunks/{TemplateScoringPanel-DgRU5fty.js.map → TemplateScoringPanel-BwyCMcit.js.map} +1 -1
  74. package/dist/cdn/chunks/{TestEmailModal-Ce8-vzGh.js → TestEmailModal-B5UQkDJp.js} +89 -58
  75. package/dist/cdn/chunks/TestEmailModal-B5UQkDJp.js.map +1 -0
  76. package/dist/cdn/chunks/{TestEmailPanel-BlmIKKSL.js → TestEmailPanel-CvuBq4RA.js} +2 -2
  77. package/dist/cdn/chunks/{TestEmailPanel-BlmIKKSL.js.map → TestEmailPanel-CvuBq4RA.js.map} +1 -1
  78. package/dist/cdn/chunks/{TitleEditor-C6_bQxYR.js → TitleEditor-B9W954u9.js} +8 -8
  79. package/dist/cdn/chunks/{TitleEditor-C6_bQxYR.js.map → TitleEditor-B9W954u9.js.map} +1 -1
  80. package/dist/cdn/chunks/{TplModal-D6bFON2K.js → TplModal-B8UsvWc8.js} +4 -4
  81. package/dist/cdn/chunks/{TplModal-D6bFON2K.js.map → TplModal-B8UsvWc8.js.map} +1 -1
  82. package/dist/{ca-CO4nq6TX.js → cdn/chunks/ca-8Dvbwo50.js} +7 -0
  83. package/dist/cdn/chunks/{ca-CO4nq6TX.js.map → ca-8Dvbwo50.js.map} +1 -1
  84. package/dist/cdn/chunks/{cloud-CRlDc0Ev.js → cloud-DOaGUdR0.js} +2 -2
  85. package/dist/cdn/chunks/{cloud-CRlDc0Ev.js.map → cloud-DOaGUdR0.js.map} +1 -1
  86. package/dist/{de-CUeaFJph.js → cdn/chunks/de-CieSMCUp.js} +7 -0
  87. package/dist/cdn/chunks/{de-CUeaFJph.js.map → de-CieSMCUp.js.map} +1 -1
  88. package/dist/{en-zCYIPAL5.js → cdn/chunks/en-D0t_2Fq_.js} +7 -0
  89. package/dist/cdn/chunks/{en-zCYIPAL5.js.map → en-D0t_2Fq_.js.map} +1 -1
  90. package/dist/cdn/chunks/{es-BSiH7YkP.js → es-DwDoNuLM.js} +6 -1
  91. package/dist/cdn/chunks/{es-BSiH7YkP.js.map → es-DwDoNuLM.js.map} +1 -1
  92. package/dist/cdn/chunks/{extensions-CKMD_er_.js → extensions-5jgtarD-.js} +19 -19
  93. package/dist/cdn/chunks/{extensions-CKMD_er_.js.map → extensions-5jgtarD-.js.map} +1 -1
  94. package/dist/{fr-DTVJ3det.js → cdn/chunks/fr-CSEEzv1_.js} +7 -0
  95. package/dist/cdn/chunks/{fr-DTVJ3det.js.map → fr-CSEEzv1_.js.map} +1 -1
  96. package/dist/cdn/chunks/keys-BxPCfHBH.js +12 -0
  97. package/dist/cdn/chunks/keys-BxPCfHBH.js.map +1 -0
  98. package/dist/{nl-BqSquFS3.js → cdn/chunks/nl-xyk1T2mA.js} +7 -0
  99. package/dist/cdn/chunks/{nl-BqSquFS3.js.map → nl-xyk1T2mA.js.map} +1 -1
  100. package/dist/{pt-BR-CMljK37h.js → cdn/chunks/pt-BR-yr6uOJgn.js} +7 -0
  101. package/dist/cdn/chunks/{pt-BR-CMljK37h.js.map → pt-BR-yr6uOJgn.js.map} +1 -1
  102. package/dist/cdn/chunks/{src-B4UhYbI8.js → src-B8W42vT-.js} +152 -152
  103. package/dist/cdn/chunks/{src-B4UhYbI8.js.map → src-B8W42vT-.js.map} +1 -1
  104. package/dist/cdn/chunks/{src-B3HKHglu.js → src-C417zeUg.js} +20 -20
  105. package/dist/cdn/chunks/{src-B3HKHglu.js.map → src-C417zeUg.js.map} +1 -1
  106. package/dist/cdn/chunks/{src-D5mfLR4V.js → src-CZjSXPYq.js} +8 -3
  107. package/dist/cdn/chunks/src-CZjSXPYq.js.map +1 -0
  108. package/dist/cdn/chunks/{src-CumZZNXZ.js → src-Dpj0x27M.js} +2 -2
  109. package/dist/cdn/chunks/{src-CumZZNXZ.js.map → src-Dpj0x27M.js.map} +1 -1
  110. package/dist/cdn/chunks/{styles--m9OCb_Z.js → styles-DANwMzW2.js} +743 -724
  111. package/dist/cdn/chunks/styles-DANwMzW2.js.map +1 -0
  112. package/dist/cdn/chunks/{useCloudI18n-sx1pqEbY.js → useCloudI18n-CjVzv11v.js} +2 -2
  113. package/dist/cdn/chunks/{useCloudI18n-sx1pqEbY.js.map → useCloudI18n-CjVzv11v.js.map} +1 -1
  114. package/dist/cdn/chunks/{useEditorCore-ZqF7P99K.js → useEditorCore-DfW-thCr.js} +472 -401
  115. package/dist/cdn/chunks/useEditorCore-DfW-thCr.js.map +1 -0
  116. package/dist/cdn/chunks/{useI18n-vD57BfVR.js → useI18n-DniEFeIl.js} +2 -2
  117. package/dist/cdn/chunks/{useI18n-vD57BfVR.js.map → useI18n-DniEFeIl.js.map} +1 -1
  118. package/dist/cdn/chunks/{useMergeTag-HatzNGgc.js → useMergeTag-Bk9_f5cc.js} +6 -6
  119. package/dist/cdn/chunks/{useMergeTag-HatzNGgc.js.map → useMergeTag-Bk9_f5cc.js.map} +1 -1
  120. package/dist/cdn/chunks/{usePopoverPosition-BdQ3Sv4B.js → usePopoverPosition-C4xFJCJ2.js} +2 -2
  121. package/dist/cdn/chunks/{usePopoverPosition-BdQ3Sv4B.js.map → usePopoverPosition-C4xFJCJ2.js.map} +1 -1
  122. package/dist/cdn/editor.css +1 -1
  123. package/dist/cdn/editor.js +21 -19
  124. package/dist/cdn/editor.js.map +1 -1
  125. package/dist/{cloud-R8ci6yR1.js → cloud-BZn3jqoL.js} +1 -1
  126. package/dist/{cdn/chunks/de-CUeaFJph.js → de-CieSMCUp.js} +5 -2
  127. package/dist/{dist-Ba5Q-UX1.js → dist-CQ0fVBQ3.js} +7 -2
  128. package/dist/{cdn/chunks/en-zCYIPAL5.js → en-D0t_2Fq_.js} +5 -2
  129. package/dist/{es-BSiH7YkP.js → es-DwDoNuLM.js} +5 -0
  130. package/dist/{extensions-CGxj6-2o.js → extensions-BjvyU9Ea.js} +3 -3
  131. package/dist/{cdn/chunks/fr-DTVJ3det.js → fr-CSEEzv1_.js} +5 -2
  132. package/dist/index.d.ts +26 -0
  133. package/dist/keys-BFPiNAIn.js +10 -0
  134. package/dist/{cdn/chunks/nl-BqSquFS3.js → nl-xyk1T2mA.js} +5 -2
  135. package/dist/{cdn/chunks/pt-BR-CMljK37h.js → pt-BR-yr6uOJgn.js} +5 -2
  136. package/dist/style.css +1 -1
  137. package/dist/{styles-C9AMvkJk.js → styles-C9GM-bBC.js} +885 -866
  138. package/dist/templatical-editor.js +93 -91
  139. package/dist/{useCloudI18n-BC_QV2Z7.js → useCloudI18n-B2lItQ6O.js} +1 -1
  140. package/dist/{useEditorCore-Dojb-P2f.js → useEditorCore-Ck_hBiGA.js} +908 -837
  141. package/dist/{useI18n-DzOcl2wl.js → useI18n-C1KcdX_K.js} +1 -1
  142. package/dist/{useMergeTag-BFxXCv3p.js → useMergeTag-DHCwRMfH.js} +3 -3
  143. package/dist/{usePopoverPosition-Dtr1Mp1x.js → usePopoverPosition-ClnEh_Cp.js} +1 -1
  144. package/package.json +7 -7
  145. package/dist/cdn/chunks/BlockPreviewCanvas-C-ut8GkJ.js.map +0 -1
  146. package/dist/cdn/chunks/CloudEditor-D87qsqbZ.js.map +0 -1
  147. package/dist/cdn/chunks/TestEmailModal-Ce8-vzGh.js.map +0 -1
  148. package/dist/cdn/chunks/keys-CswSK7eb.js +0 -12
  149. package/dist/cdn/chunks/keys-CswSK7eb.js.map +0 -1
  150. package/dist/cdn/chunks/src-D5mfLR4V.js.map +0 -1
  151. package/dist/cdn/chunks/styles--m9OCb_Z.js.map +0 -1
  152. package/dist/cdn/chunks/useEditorCore-ZqF7P99K.js.map +0 -1
  153. package/dist/keys-uWpGv4Pq.js +0 -10
@@ -1 +0,0 @@
1
- {"version":3,"file":"TestEmailModal-Ce8-vzGh.js","names":[],"sources":["../../../src/utils/validateEmailShape.ts","../../../src/components/TestEmailModal.vue","../../../src/components/TestEmailModal.vue"],"sourcesContent":["/**\n * Characters that cannot appear in a domain but routinely arrive attached to one.\n *\n * A **denylist**, not an allowlist, so unicode domains keep working — an\n * allowlist of `[a-z0-9.-]` would be the stricter, more obviously \"correct\"\n * choice and would reject them. What this actually targets is the punctuation\n * that comes from pasting out of a list or a mail client: `user@example.com,`\n * and `<user@example.com>`.\n */\nconst IMPOSSIBLE_DOMAIN_CHARS = /[,;<>()[\\]\\\\\"'`|/?#@!$%^&*+=~{}]/;\n\n/**\n * Does this string look like an email address?\n *\n * Deliberately loose: non-empty, no whitespace, exactly one `@`, a non-empty\n * local part, and a domain containing a dot with something either side and no\n * impossible characters. That is enough to catch the mistakes a typo actually\n * produces — a missing `@`, a trailing comma, a half-typed domain — before the\n * user pays for a round-trip.\n *\n * **Do not \"improve\" this into an RFC-ish regex.** Those reliably reject valid\n * addresses (plus-addressing, long new gTLDs, unicode domains) and the resulting\n * bug reports cost far more than the malformed addresses they catch. Only the\n * receiving mail server can really tell, and the sending backend has to validate\n * anyway — this is a fast-feedback affordance, not a gate. `validateEmailShape`\n * has its own test file pinning the addresses that must keep passing.\n */\nexport function looksLikeEmail(value: string): boolean {\n const trimmed = value.trim();\n if (trimmed.length === 0) return false;\n if (/\\s/.test(trimmed)) return false;\n\n const parts = trimmed.split(\"@\");\n if (parts.length !== 2) return false;\n\n const [local, domain] = parts;\n if (local.length === 0) return false;\n if (IMPOSSIBLE_DOMAIN_CHARS.test(domain)) return false;\n\n // A dot with at least one character either side, so `a@b` and `a@b.` fail but\n // `a@b.c` and `a@sub.example.museum` pass.\n const dot = domain.indexOf(\".\");\n return dot > 0 && dot < domain.length - 1;\n}\n","<script setup lang=\"ts\">\n/**\n * Send-a-test dialog, shared by the OSS and Cloud editors.\n *\n * The recipient control has three shapes, driven entirely by whether the\n * provider configured an allowlist:\n *\n * - **no allowlist** — free text, validated for shape only. This branch exists\n * for BYO senders; Cloud never takes it, because its list comes from a signed\n * JWT.\n * - **exactly one** — a read-only field. Preserved verbatim from the Cloud-only\n * version; there is nothing to choose, so offering an editable field would\n * imply otherwise.\n * - **several** — a picker of exactly those addresses.\n *\n * An empty allowlist never reaches here: the feature reports itself unavailable,\n * so no trigger renders. (The Cloud-only version fell through to `v-else` and\n * rendered a select with no options and a permanently disabled Send.)\n */\nimport TplModal from \"./TplModal.vue\";\n// Statically imported: this modal is already lazy-loaded as a whole, so the\n// preview rides its chunk rather than costing a second round-trip and flashing\n// an empty frame. Still shared with the saved-blocks surfaces, which lazy-load\n// it themselves.\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\n// The editor header's own control, reused verbatim so the two viewport switches\n// are the same component rather than two things that resemble each other.\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./MergeTagModeToggle.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { looksLikeEmail } from \"../utils/validateEmailShape\";\nimport { Check, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { hasMergeTagSamples, type ViewportSize } from \"@templatical/types\";\nimport { EDITOR_KEY, MERGE_TAGS_KEY, MERGE_TAG_SAMPLE_MODE_KEY } from \"../keys\";\nimport type { TestEmailError } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n visible: boolean;\n /** Omitted / undefined means unrestricted, so the field accepts free text. */\n allowedRecipients?: string[];\n defaultRecipient?: string;\n isSending: boolean;\n justSent: boolean;\n error: TestEmailError | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send\", recipient: string): void;\n (e: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\nconst editor = inject(EDITOR_KEY, null);\n\nconst recipient = ref(\"\");\n\nconst previewViewport = ref<ViewportSize>(\"desktop\");\n\n/**\n * Shared with the editor's own preview, so switching here and switching there\n * are the same choice — it is a property of how the user wants to read a\n * preview, not of one dialog. Falls back to a local ref for headless mounts.\n */\nconst sharedSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Seeded from availability, matching how `useEditorCore` seeds the shared ref.\n// A blanket `true` would make the dialog claim tags \"show example values\" for a\n// consumer who configured none — false, and with no toggle rendered to correct\n// it, since the toggle hides itself when nothing has a sample.\nconst localSampleMode = ref(hasMergeTagSamples(inject(MERGE_TAGS_KEY, [])));\nconst sampleMode = computed({\n get: () => sharedSampleMode?.value ?? localSampleMode.value,\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\n/**\n * Read live from the editor rather than passed in, so a preview left open while\n * a Cloud collaborator edits stays current. Empty without an editor (headless\n * mounts), which simply renders an empty frame.\n */\nconst previewBlocks = computed(() => editor?.content.value.blocks ?? []);\n\nconst hasAllowlist = computed(\n () =>\n props.allowedRecipients !== undefined && props.allowedRecipients.length > 0,\n);\nconst isSingleRecipient = computed(() => props.allowedRecipients?.length === 1);\n\n// Mounted behind a `v-if` on the same state that drives `visible`, so it can\n// mount with `visible` already true — without `immediate` the field would never\n// be seeded.\nwatch(\n () => props.visible,\n (visible) => {\n if (!visible) return;\n recipient.value =\n props.defaultRecipient ?? props.allowedRecipients?.[0] ?? \"\";\n },\n { immediate: true },\n);\n\n/** Shape-check free text only; a picker can't produce a malformed value. */\nconst isRecipientValid = computed(() =>\n hasAllowlist.value\n ? recipient.value.length > 0\n : looksLikeEmail(recipient.value),\n);\n\nconst canSend = computed(\n () => isRecipientValid.value && !props.isSending && !props.justSent,\n);\n\n/** Free text that's been typed into but isn't yet a plausible address. */\nconst showShapeHint = computed(\n () =>\n !hasAllowlist.value &&\n recipient.value.trim().length > 0 &&\n !isRecipientValid.value,\n);\n\nconst errorText = computed(() => {\n if (!props.error) return null;\n return props.error.kind === \"provider\"\n ? props.error.message\n : t.testEmail.recipientNotAllowed;\n});\n\nfunction handleSend(): void {\n if (!canSend.value) return;\n emit(\"send\", recipient.value.trim());\n}\n\nfunction handleClose(): void {\n // Sending is the one state that blocks dismissal; the success state closes\n // immediately rather than making the user wait out the timer.\n if (props.isSending) return;\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Grows to `max-w-2xl` only while the preview is open, so a 600px email\n renders at roughly true size — the same reason SaveBlockDialog picked\n that width. Collapsed, it stays the compact form it was. `max-h-[90vh]`\n with a `min-h-0 overflow-y-auto` preview region is the pattern the\n saved-blocks dialogs use: `min-h-0` is what lets a flex child shrink\n below its content and actually scroll. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSending\"\n aria-labelledby=\"tpl-test-email-title\"\n class=\"tpl-scale-in tpl:mx-4 tpl:flex tpl:max-h-[90vh] tpl:w-full tpl:max-w-2xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-test-email-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.testEmail.title }}\n </h3>\n\n <!-- Recipient -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-test-email-recipient\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.recipientLabel }}\n </label>\n\n <!-- Exactly one allowed address: nothing to choose. -->\n <input\n v-if=\"isSingleRecipient\"\n id=\"tpl-test-email-recipient\"\n type=\"text\"\n :value=\"recipient\"\n disabled\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:opacity-70 tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <!-- Several allowed addresses: pick one. -->\n <select\n v-else-if=\"hasAllowlist\"\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"email in allowedRecipients\"\n :key=\"email\"\n :value=\"email\"\n >\n {{ email }}\n </option>\n </select>\n\n <!-- No allowlist: free text. -->\n <input\n v-else\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n type=\"email\"\n autocomplete=\"email\"\n spellcheck=\"false\"\n :placeholder=\"t.testEmail.recipientPlaceholder\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <p\n v-if=\"showShapeHint\"\n class=\"tpl:mt-1.5 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.testEmail.invalidAddress }}\n </p>\n </div>\n\n <!-- Preview header: a label plus the viewport switch. Always shown — the\n preview is part of the dialog rather than something to opt into. -->\n <div class=\"tpl:mb-2 tpl:shrink-0\">\n <!-- Two segmented controls: `flex-wrap` so they drop to their own row\n rather than crushing the label on a narrow viewport. -->\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.preview }}\n </span>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <MergeTagModeToggle\n :sample-mode=\"sampleMode\"\n @change=\"sampleMode = $event\"\n />\n\n <!-- Without this the preview would silently show only the desktop\n variant of any responsive block. -->\n <ViewportToggle\n :viewport=\"previewViewport\"\n @change=\"previewViewport = $event\"\n />\n </div>\n </div>\n\n <!-- The hint has to follow the mode: \"shown unresolved\" is simply false\n once every tag renders a realistic value. -->\n <p\n data-testid=\"test-email-preview-hint\"\n class=\"tpl:mt-1 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n sampleMode ? t.testEmail.previewHintSample : t.testEmail.previewHint\n }}\n </p>\n </div>\n\n <!-- The one region allowed to grow: `flex-1 min-h-0` so it absorbs the\n remaining height and scrolls, leaving the form and actions fixed. -->\n <div\n id=\"tpl-test-email-preview\"\n data-testid=\"test-email-preview\"\n class=\"tpl:mb-3 tpl:min-h-0 tpl:flex-1 tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius-sm)] tpl:p-3 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <BlockPreviewCanvas\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n />\n </div>\n\n <!-- Success. Announced politely; the dialog closes itself shortly after. -->\n <p\n v-if=\"justSent\"\n role=\"status\"\n data-testid=\"test-email-success\"\n class=\"tpl:mb-3 tpl:flex tpl:shrink-0 tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success,var(--tpl-primary))]\"\n >\n <Check :size=\"13\" :stroke-width=\"2.5\" />\n {{ t.testEmail.success }}\n </p>\n\n <!-- Failure. Stays open so the user can retry. -->\n <p\n v-else-if=\"errorText\"\n role=\"alert\"\n data-testid=\"test-email-error\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ errorText }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"test-email-cancel\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSending\"\n :class=\"{ 'tpl:cursor-not-allowed tpl:opacity-50': isSending }\"\n @click=\"handleClose\"\n >\n {{ t.testEmail.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"test-email-send\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!canSend\"\n @click=\"handleSend\"\n >\n <span v-if=\"isSending\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.testEmail.sending }}\n </span>\n <span v-else>\n {{ t.testEmail.send }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Send-a-test dialog, shared by the OSS and Cloud editors.\n *\n * The recipient control has three shapes, driven entirely by whether the\n * provider configured an allowlist:\n *\n * - **no allowlist** — free text, validated for shape only. This branch exists\n * for BYO senders; Cloud never takes it, because its list comes from a signed\n * JWT.\n * - **exactly one** — a read-only field. Preserved verbatim from the Cloud-only\n * version; there is nothing to choose, so offering an editable field would\n * imply otherwise.\n * - **several** — a picker of exactly those addresses.\n *\n * An empty allowlist never reaches here: the feature reports itself unavailable,\n * so no trigger renders. (The Cloud-only version fell through to `v-else` and\n * rendered a select with no options and a permanently disabled Send.)\n */\nimport TplModal from \"./TplModal.vue\";\n// Statically imported: this modal is already lazy-loaded as a whole, so the\n// preview rides its chunk rather than costing a second round-trip and flashing\n// an empty frame. Still shared with the saved-blocks surfaces, which lazy-load\n// it themselves.\nimport BlockPreviewCanvas from \"./BlockPreviewCanvas.vue\";\n// The editor header's own control, reused verbatim so the two viewport switches\n// are the same component rather than two things that resemble each other.\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport MergeTagModeToggle from \"./MergeTagModeToggle.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { looksLikeEmail } from \"../utils/validateEmailShape\";\nimport { Check, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { hasMergeTagSamples, type ViewportSize } from \"@templatical/types\";\nimport { EDITOR_KEY, MERGE_TAGS_KEY, MERGE_TAG_SAMPLE_MODE_KEY } from \"../keys\";\nimport type { TestEmailError } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n visible: boolean;\n /** Omitted / undefined means unrestricted, so the field accepts free text. */\n allowedRecipients?: string[];\n defaultRecipient?: string;\n isSending: boolean;\n justSent: boolean;\n error: TestEmailError | null;\n}>();\n\nconst emit = defineEmits<{\n (e: \"send\", recipient: string): void;\n (e: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\nconst editor = inject(EDITOR_KEY, null);\n\nconst recipient = ref(\"\");\n\nconst previewViewport = ref<ViewportSize>(\"desktop\");\n\n/**\n * Shared with the editor's own preview, so switching here and switching there\n * are the same choice — it is a property of how the user wants to read a\n * preview, not of one dialog. Falls back to a local ref for headless mounts.\n */\nconst sharedSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Seeded from availability, matching how `useEditorCore` seeds the shared ref.\n// A blanket `true` would make the dialog claim tags \"show example values\" for a\n// consumer who configured none — false, and with no toggle rendered to correct\n// it, since the toggle hides itself when nothing has a sample.\nconst localSampleMode = ref(hasMergeTagSamples(inject(MERGE_TAGS_KEY, [])));\nconst sampleMode = computed({\n get: () => sharedSampleMode?.value ?? localSampleMode.value,\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\n/**\n * Read live from the editor rather than passed in, so a preview left open while\n * a Cloud collaborator edits stays current. Empty without an editor (headless\n * mounts), which simply renders an empty frame.\n */\nconst previewBlocks = computed(() => editor?.content.value.blocks ?? []);\n\nconst hasAllowlist = computed(\n () =>\n props.allowedRecipients !== undefined && props.allowedRecipients.length > 0,\n);\nconst isSingleRecipient = computed(() => props.allowedRecipients?.length === 1);\n\n// Mounted behind a `v-if` on the same state that drives `visible`, so it can\n// mount with `visible` already true — without `immediate` the field would never\n// be seeded.\nwatch(\n () => props.visible,\n (visible) => {\n if (!visible) return;\n recipient.value =\n props.defaultRecipient ?? props.allowedRecipients?.[0] ?? \"\";\n },\n { immediate: true },\n);\n\n/** Shape-check free text only; a picker can't produce a malformed value. */\nconst isRecipientValid = computed(() =>\n hasAllowlist.value\n ? recipient.value.length > 0\n : looksLikeEmail(recipient.value),\n);\n\nconst canSend = computed(\n () => isRecipientValid.value && !props.isSending && !props.justSent,\n);\n\n/** Free text that's been typed into but isn't yet a plausible address. */\nconst showShapeHint = computed(\n () =>\n !hasAllowlist.value &&\n recipient.value.trim().length > 0 &&\n !isRecipientValid.value,\n);\n\nconst errorText = computed(() => {\n if (!props.error) return null;\n return props.error.kind === \"provider\"\n ? props.error.message\n : t.testEmail.recipientNotAllowed;\n});\n\nfunction handleSend(): void {\n if (!canSend.value) return;\n emit(\"send\", recipient.value.trim());\n}\n\nfunction handleClose(): void {\n // Sending is the one state that blocks dismissal; the success state closes\n // immediately rather than making the user wait out the timer.\n if (props.isSending) return;\n emit(\"close\");\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n handleSend();\n }\n if (event.key === \"Escape\") {\n handleClose();\n }\n}\n</script>\n\n<template>\n <TplModal :visible=\"visible\" @close=\"handleClose\" @keydown=\"handleKeydown\">\n <!-- Grows to `max-w-2xl` only while the preview is open, so a 600px email\n renders at roughly true size — the same reason SaveBlockDialog picked\n that width. Collapsed, it stays the compact form it was. `max-h-[90vh]`\n with a `min-h-0 overflow-y-auto` preview region is the pattern the\n saved-blocks dialogs use: `min-h-0` is what lets a flex child shrink\n below its content and actually scroll. -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-busy=\"isSending\"\n aria-labelledby=\"tpl-test-email-title\"\n class=\"tpl-scale-in tpl:mx-4 tpl:flex tpl:max-h-[90vh] tpl:w-full tpl:max-w-2xl tpl:flex-col tpl:rounded-[var(--tpl-radius-lg)] tpl:p-5\"\n style=\"\n background-color: var(--tpl-bg-elevated);\n box-shadow: var(--tpl-shadow-xl);\n \"\n >\n <h3\n id=\"tpl-test-email-title\"\n class=\"tpl:mb-4 tpl:shrink-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.testEmail.title }}\n </h3>\n\n <!-- Recipient -->\n <div class=\"tpl:mb-3 tpl:shrink-0\">\n <label\n for=\"tpl-test-email-recipient\"\n class=\"tpl:mb-1.5 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.recipientLabel }}\n </label>\n\n <!-- Exactly one allowed address: nothing to choose. -->\n <input\n v-if=\"isSingleRecipient\"\n id=\"tpl-test-email-recipient\"\n type=\"text\"\n :value=\"recipient\"\n disabled\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:opacity-70 tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <!-- Several allowed addresses: pick one. -->\n <select\n v-else-if=\"hasAllowlist\"\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n >\n <option\n v-for=\"email in allowedRecipients\"\n :key=\"email\"\n :value=\"email\"\n >\n {{ email }}\n </option>\n </select>\n\n <!-- No allowlist: free text. -->\n <input\n v-else\n id=\"tpl-test-email-recipient\"\n v-model=\"recipient\"\n type=\"email\"\n autocomplete=\"email\"\n spellcheck=\"false\"\n :placeholder=\"t.testEmail.recipientPlaceholder\"\n :disabled=\"isSending || justSent\"\n data-testid=\"test-email-recipient\"\n class=\"tpl:h-9 tpl:w-full tpl:rounded-md tpl:border tpl:px-3 tpl:py-1 tpl:text-sm tpl:shadow-xs tpl:outline-none tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]\"\n />\n\n <p\n v-if=\"showShapeHint\"\n class=\"tpl:mt-1.5 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.testEmail.invalidAddress }}\n </p>\n </div>\n\n <!-- Preview header: a label plus the viewport switch. Always shown — the\n preview is part of the dialog rather than something to opt into. -->\n <div class=\"tpl:mb-2 tpl:shrink-0\">\n <!-- Two segmented controls: `flex-wrap` so they drop to their own row\n rather than crushing the label on a narrow viewport. -->\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.testEmail.preview }}\n </span>\n\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <MergeTagModeToggle\n :sample-mode=\"sampleMode\"\n @change=\"sampleMode = $event\"\n />\n\n <!-- Without this the preview would silently show only the desktop\n variant of any responsive block. -->\n <ViewportToggle\n :viewport=\"previewViewport\"\n @change=\"previewViewport = $event\"\n />\n </div>\n </div>\n\n <!-- The hint has to follow the mode: \"shown unresolved\" is simply false\n once every tag renders a realistic value. -->\n <p\n data-testid=\"test-email-preview-hint\"\n class=\"tpl:mt-1 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n sampleMode ? t.testEmail.previewHintSample : t.testEmail.previewHint\n }}\n </p>\n </div>\n\n <!-- The one region allowed to grow: `flex-1 min-h-0` so it absorbs the\n remaining height and scrolls, leaving the form and actions fixed. -->\n <div\n id=\"tpl-test-email-preview\"\n data-testid=\"test-email-preview\"\n class=\"tpl:mb-3 tpl:min-h-0 tpl:flex-1 tpl:overflow-y-auto tpl:rounded-[var(--tpl-radius-sm)] tpl:p-3 tpl:bg-[var(--tpl-canvas-bg)]\"\n >\n <BlockPreviewCanvas\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n />\n </div>\n\n <!-- Success. Announced politely; the dialog closes itself shortly after. -->\n <p\n v-if=\"justSent\"\n role=\"status\"\n data-testid=\"test-email-success\"\n class=\"tpl:mb-3 tpl:flex tpl:shrink-0 tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success,var(--tpl-primary))]\"\n >\n <Check :size=\"13\" :stroke-width=\"2.5\" />\n {{ t.testEmail.success }}\n </p>\n\n <!-- Failure. Stays open so the user can retry. -->\n <p\n v-else-if=\"errorText\"\n role=\"alert\"\n data-testid=\"test-email-error\"\n class=\"tpl:mb-3 tpl:shrink-0 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n {{ errorText }}\n </p>\n\n <!-- Actions -->\n <div class=\"tpl:flex tpl:shrink-0 tpl:justify-end tpl:gap-2\">\n <button\n type=\"button\"\n data-testid=\"test-email-cancel\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)]\"\n :disabled=\"isSending\"\n :class=\"{ 'tpl:cursor-not-allowed tpl:opacity-50': isSending }\"\n @click=\"handleClose\"\n >\n {{ t.testEmail.cancel }}\n </button>\n <button\n type=\"button\"\n data-testid=\"test-email-send\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:px-3 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:shadow-xs tpl:transition-all tpl:duration-150 tpl:hover:opacity-90 tpl:disabled:cursor-not-allowed tpl:disabled:opacity-50 tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-bg)]\"\n :disabled=\"!canSend\"\n @click=\"handleSend\"\n >\n <span v-if=\"isSending\" class=\"tpl:flex tpl:items-center tpl:gap-1.5\">\n <LoaderCircle\n class=\"tpl:animate-spin\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n {{ t.testEmail.sending }}\n </span>\n <span v-else>\n {{ t.testEmail.send }}\n </span>\n </button>\n </div>\n </div>\n </TplModal>\n</template>\n"],"mappings":"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"}
@@ -1,12 +0,0 @@
1
- import { T as e } from "./draggable-JcMTNF3U.js";
2
- //#region src/keys.ts
3
- var t = Symbol("translations"), n = Symbol("editor"), r = Symbol("history"), i = Symbol("blockActions"), a = Symbol("conditionPreview"), o = Symbol("fontsManager"), s = Symbol("themeStyles"), c = Symbol("tplUiTheme"), l = Symbol("blockDefaults"), u = Symbol("blockRegistry"), d = Symbol("customBlockDefinitions"), f = Symbol("paletteBlocks"), p = Symbol("htmlBlockPreview"), m = Symbol("colors"), h = Symbol("customBlockStylesheets"), g = Symbol("mergeTags"), _ = Symbol("mergeTagSyntax"), v = Symbol("onRequestMergeTag"), y = Symbol("mergeTagAutocomplete"), b = Symbol("mergeTagSampleMode"), x = Symbol("useMergeTagSamples"), S = Symbol("mergeTagPicker"), C = Symbol("logicTags"), w = Symbol("logicPairs"), T = Symbol("logicPicker"), E = Symbol("onRequestLogic"), D = Symbol("onRequestMedia"), O = Symbol("imageUrlResolver"), k = Symbol("displayConditions"), A = Symbol("allowCustomConditions"), j = Symbol("capabilities"), M = Symbol("keyboardReorder"), N = Symbol("templateLint"), P = Symbol("editorRoot"), F = Symbol("popoverRoot"), I = Symbol("savedBlocks"), L = Symbol("authManager"), R = Symbol("aiConfig"), z = Symbol("comments"), B = Symbol("scoring"), V = Symbol("cloudTranslations");
4
- function H(t, n) {
5
- let r = e(t, null);
6
- if (r == null) throw Error(`${n} requires a provider for ${t.description ?? "unknown key"}. Ensure it is a descendant of Editor or CloudEditor.`);
7
- return r;
8
- }
9
- //#endregion
10
- export { E as A, c as B, C, S as D, y as E, I as F, H, B as I, N as L, v as M, f as N, b as O, F as P, s as R, w as S, g as T, x as V, o as _, l as a, O as b, V as c, a as d, d as f, P as g, n as h, i, D as j, _ as k, m as l, k as m, A as n, u as o, h as p, L as r, j as s, R as t, z as u, r as v, T as w, M as x, p as y, t as z };
11
-
12
- //# sourceMappingURL=keys-CswSK7eb.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"keys-CswSK7eb.js","names":[],"sources":["../../../src/keys.ts"],"sourcesContent":["import { inject, type InjectionKey, type ComputedRef, type Ref } from \"vue\";\nimport type {\n UseHistoryReturn,\n UseBlockActionsReturn,\n UseConditionPreviewReturn,\n} from \"@templatical/core\";\nimport type {\n BlockDefaults,\n CustomBlockDefinition,\n DisplayCondition,\n LogicPair,\n LogicTag,\n MergeTag,\n SyntaxPreset,\n} from \"@templatical/types\";\nimport type { Translations, CloudTranslations } from \"./i18n\";\nimport type {\n AuthManager,\n UseAiConfigReturn,\n UseCommentsReturn,\n UseTemplateScoringReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseSavedBlocksReturn } from \"@templatical/core\";\nimport type { BaseEditorReturn } from \"./composables/useEditorCore\";\nimport type { ImageUrlResolver } from \"./composables/useImageUrlResolver\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\nimport type { UseBlockRegistryReturn } from \"./composables/useBlockRegistry\";\nimport type { UseKeyboardReorderReturn } from \"./composables/useKeyboardReorder\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { OnRequestMedia } from \"./index\";\n\n// ---------------------------------------------------------------------------\n// Shared keys (provided by useEditorCore, consumed by all components)\n// ---------------------------------------------------------------------------\n\nexport const TRANSLATIONS_KEY: InjectionKey<Translations> =\n Symbol(\"translations\");\n\nexport const EDITOR_KEY: InjectionKey<BaseEditorReturn> = Symbol(\"editor\");\n\nexport const HISTORY_KEY: InjectionKey<UseHistoryReturn> = Symbol(\"history\");\n\nexport const BLOCK_ACTIONS_KEY: InjectionKey<UseBlockActionsReturn> =\n Symbol(\"blockActions\");\n\nexport const CONDITION_PREVIEW_KEY: InjectionKey<UseConditionPreviewReturn> =\n Symbol(\"conditionPreview\");\n\nexport const FONTS_MANAGER_KEY: InjectionKey<UseFontsReturn> =\n Symbol(\"fontsManager\");\n\nexport const THEME_STYLES_KEY: InjectionKey<\n ComputedRef<Record<string, string>>\n> = Symbol(\"themeStyles\");\n\nexport const UI_THEME_KEY: InjectionKey<ComputedRef<string>> =\n Symbol(\"tplUiTheme\");\n\nexport const BLOCK_DEFAULTS_KEY: InjectionKey<BlockDefaults | undefined> =\n Symbol(\"blockDefaults\");\n\nexport const BLOCK_REGISTRY_KEY: InjectionKey<UseBlockRegistryReturn> =\n Symbol(\"blockRegistry\");\n\nexport const CUSTOM_BLOCK_DEFINITIONS_KEY: InjectionKey<\n CustomBlockDefinition[]\n> = Symbol(\"customBlockDefinitions\");\n\n/**\n * Consumer-supplied block-palette allowlist + order (`config.paletteBlocks`).\n * `undefined` means the default full palette. Consumed by `Sidebar.vue`.\n */\nexport const PALETTE_BLOCKS_KEY: InjectionKey<string[] | undefined> =\n Symbol(\"paletteBlocks\");\n\n/**\n * Whether HTML blocks render a live sandboxed-iframe preview in the canvas\n * (`config.htmlBlockPreview`), normalized to a boolean. `false` (the default)\n * keeps the static placeholder card. Consumed by `HtmlBlock.vue`.\n */\nexport const HTML_BLOCK_PREVIEW_KEY: InjectionKey<boolean> =\n Symbol(\"htmlBlockPreview\");\n\n/**\n * Consumer-supplied color-picker palette (`config.colors`), normalized to\n * `{ presets, allowCustom, allowCustomIgnored }` by `resolveColorsConfig`.\n * Provided by `useEditorCore`, consumed by every `ColorPicker`. The inject\n * default (`DEFAULT_RESOLVED_COLORS` = `{ presets: [], allowCustom: true,\n * allowCustomIgnored: false }`) makes an unconfigured picker render exactly\n * as before.\n */\nexport const COLORS_KEY: InjectionKey<\n import(\"./utils/resolveColorsConfig\").ResolvedColors\n> = Symbol(\"colors\");\n\nexport const CUSTOM_BLOCK_STYLESHEETS_KEY: InjectionKey<ComputedRef<string[]>> =\n Symbol(\"customBlockStylesheets\");\n\nexport const MERGE_TAGS_KEY: InjectionKey<MergeTag[]> = Symbol(\"mergeTags\");\n\nexport const MERGE_TAG_SYNTAX_KEY: InjectionKey<SyntaxPreset> =\n Symbol(\"mergeTagSyntax\");\n\nexport const ON_REQUEST_MERGE_TAG_KEY: InjectionKey<\n (() => Promise<MergeTag | null>) | null\n> = Symbol(\"onRequestMergeTag\");\n\nexport const MERGE_TAG_AUTOCOMPLETE_KEY: InjectionKey<boolean> = Symbol(\n \"mergeTagAutocomplete\",\n);\n\n/**\n * The user's choice between the previews' two merge-tag views: `true` for\n * Sample (tags replaced by their `MergeTag.sample`, rendered as ordinary text)\n * and `false` for Label (tags shown as their label with the usual cue).\n *\n * Session state owned by `useEditorCore` and driven by a toggle that exists\n * only on preview surfaces. Defaults to Sample.\n */\nexport const MERGE_TAG_SAMPLE_MODE_KEY: InjectionKey<Ref<boolean>> =\n Symbol(\"mergeTagSampleMode\");\n\n/**\n * Whether **the current render surface** substitutes samples.\n *\n * Provided per surface rather than globally, and that is the whole point:\n * substitution must never happen while editing, so `Canvas.vue` folds in its\n * `previewMode` flag and provides `false` whenever the canvas is editable, no\n * matter what the mode ref says. Preview surfaces provide the mode directly.\n *\n * Block components inject **this** key. They must never read\n * `MERGE_TAG_SAMPLE_MODE_KEY`, which would bypass the editing guard.\n */\nexport const USE_MERGE_TAG_SAMPLES_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"useMergeTagSamples\");\n\n/**\n * Singleton state for the built-in merge tag picker modal. Provided by\n * `useEditorCore`, consumed by `useMergeTag.requestMergeTag()` (to open\n * the modal as a fall-through when only `mergeTags.tags` is configured)\n * and by `MergeTagPickerModal.vue` (to render and resolve).\n */\nexport const MERGE_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useMergeTagPicker\").UseMergeTagPickerReturn\n> = Symbol(\"mergeTagPicker\");\n\n// ---------------------------------------------------------------------------\n// Logic tags — a standalone feature, separate from merge tags. Native\n// highlighting (LogicMergeTagNode) is always on; these drive the dedicated\n// logic picker + insertion.\n// ---------------------------------------------------------------------------\n\nexport const LOGIC_TAGS_KEY: InjectionKey<LogicTag[]> = Symbol(\"logicTags\");\n\nexport const LOGIC_PAIRS_KEY: InjectionKey<LogicPair[]> = Symbol(\"logicPairs\");\n\n/**\n * Singleton state for the built-in logic picker modal. Provided by\n * `useEditorCore`, consumed by `useLogicTag.requestLogicTag()` (to open the modal)\n * and by `LogicTagPickerModal.vue` (to render and resolve).\n */\nexport const LOGIC_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useLogicTagPicker\").UseLogicTagPickerReturn\n> = Symbol(\"logicPicker\");\n\nexport const ON_REQUEST_LOGIC_TAG_KEY: InjectionKey<\n (() => Promise<LogicTag | LogicPair | null>) | null\n> = Symbol(\"onRequestLogic\");\n\nexport const ON_REQUEST_MEDIA_KEY: InjectionKey<OnRequestMedia | null> =\n Symbol(\"onRequestMedia\");\n\n/**\n * Per-editor display-only image URL resolver (`config.resolveImageUrl`),\n * wrapped with a per-src cache by `createImageUrlResolver`. `null` when the\n * host doesn't resolve — the canvas then displays canonical src values\n * verbatim. Consumed via `useResolvedImageSrc` in image-displaying blocks.\n */\nexport const IMAGE_URL_RESOLVER_KEY: InjectionKey<ImageUrlResolver | null> =\n Symbol(\"imageUrlResolver\");\n\nexport const DISPLAY_CONDITIONS_KEY: InjectionKey<DisplayCondition[]> =\n Symbol(\"displayConditions\");\n\nexport const ALLOW_CUSTOM_CONDITIONS_KEY: InjectionKey<boolean> = Symbol(\n \"allowCustomConditions\",\n);\n\nexport const CAPABILITIES_KEY: InjectionKey<EditorCapabilities> =\n Symbol(\"capabilities\");\n\nexport const KEYBOARD_REORDER_KEY: InjectionKey<UseKeyboardReorderReturn> =\n Symbol(\"keyboardReorder\");\n\nexport const TEMPLATE_LINT_KEY: InjectionKey<\n import(\"./composables/useTemplateLint\").UseTemplateLintReturn | null\n> = Symbol(\"templateLint\");\n\n/**\n * The editor's effective DOM root — `Document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true`. Composables that read `document.activeElement`,\n * popup mount targets, etc. should inject this and use the root-aware API\n * (both `Document` and `ShadowRoot` expose `activeElement` etc.).\n */\nexport const EDITOR_ROOT_KEY: InjectionKey<Document | ShadowRoot> =\n Symbol(\"editorRoot\");\n\n/**\n * Mount target for popovers, toolbars, and modal dialogs that previously\n * teleported to `document.body`. Provided by `useEditorCore` as a ref bound\n * to a `<div class=\"tpl-popover-root\" />` rendered at the top level of the\n * editor template. Teleports use `:to=\"popoverRoot\"` so popups land inside\n * the editor's effective root — `document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true` — instead of escaping the shadow boundary.\n *\n * Null until the editor's template mounts; consumers must guard with\n * `v-if=\"popoverRoot\"` before passing to `<Teleport>`.\n */\nexport const POPOVER_ROOT_KEY: InjectionKey<Ref<HTMLElement | null>> =\n Symbol(\"popoverRoot\");\n\n/**\n * Reactive saved-blocks state, provided by `useSavedBlocksFeature` when a\n * `SavedBlocksProvider` is configured. Shared by OSS and Cloud — the storage\n * transport lives behind the provider, so this key is not cloud-specific.\n */\nexport const SAVED_BLOCKS_KEY: InjectionKey<UseSavedBlocksReturn> =\n Symbol(\"savedBlocks\");\n\n// ---------------------------------------------------------------------------\n// Cloud-only keys (provided by CloudEditor, consumed by cloud components)\n// ---------------------------------------------------------------------------\n\nexport const AUTH_MANAGER_KEY: InjectionKey<AuthManager> =\n Symbol(\"authManager\");\n\nexport const AI_CONFIG_KEY: InjectionKey<UseAiConfigReturn> =\n Symbol(\"aiConfig\");\n\nexport const COMMENTS_KEY: InjectionKey<UseCommentsReturn> = Symbol(\"comments\");\n\nexport const SCORING_KEY: InjectionKey<UseTemplateScoringReturn> =\n Symbol(\"scoring\");\n\nexport const CLOUD_TRANSLATIONS_KEY: InjectionKey<CloudTranslations> =\n Symbol(\"cloudTranslations\");\n\n// ---------------------------------------------------------------------------\n// Helper for required injections with explicit null default + throw\n// ---------------------------------------------------------------------------\n\nexport function requireInject<T>(\n key: InjectionKey<T>,\n componentName: string,\n): T {\n const value = inject(key, null) as T | null;\n if (value === null || value === undefined) {\n throw new Error(\n `${componentName} requires a provider for ${key.description ?? \"unknown key\"}. Ensure it is a descendant of Editor or CloudEditor.`,\n );\n }\n return value;\n}\n"],"mappings":";;AAmCA,IAAa,IACX,OAAO,cAAc,GAEV,IAA6C,OAAO,QAAQ,GAE5D,IAA8C,OAAO,SAAS,GAE9D,IACX,OAAO,cAAc,GAEV,IACX,OAAO,kBAAkB,GAEd,IACX,OAAO,cAAc,GAEV,IAET,OAAO,aAAa,GAEX,IACX,OAAO,YAAY,GAER,IACX,OAAO,eAAe,GAEX,IACX,OAAO,eAAe,GAEX,IAET,OAAO,wBAAwB,GAMtB,IACX,OAAO,eAAe,GAOX,IACX,OAAO,kBAAkB,GAUd,IAET,OAAO,QAAQ,GAEN,IACX,OAAO,wBAAwB,GAEpB,IAA2C,OAAO,WAAW,GAE7D,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAEjB,IAAoD,OAC/D,sBACF,GAUa,IACX,OAAO,oBAAoB,GAahB,IACX,OAAO,oBAAoB,GAQhB,IAET,OAAO,gBAAgB,GAQd,IAA2C,OAAO,WAAW,GAE7D,IAA6C,OAAO,YAAY,GAOhE,IAET,OAAO,aAAa,GAEX,IAET,OAAO,gBAAgB,GAEd,IACX,OAAO,gBAAgB,GAQZ,IACX,OAAO,kBAAkB,GAEd,IACX,OAAO,mBAAmB,GAEf,IAAqD,OAChE,uBACF,GAEa,IACX,OAAO,cAAc,GAEV,IACX,OAAO,iBAAiB,GAEb,IAET,OAAO,cAAc,GAQZ,IACX,OAAO,YAAY,GAaR,IACX,OAAO,aAAa,GAOT,IACX,OAAO,aAAa,GAMT,IACX,OAAO,aAAa,GAET,IACX,OAAO,UAAU,GAEN,IAAgD,OAAO,UAAU,GAEjE,IACX,OAAO,SAAS,GAEL,IACX,OAAO,mBAAmB;AAM5B,SAAgB,EACd,GACA,GACG;CACH,IAAM,IAAQ,EAAO,GAAK,IAAI;CAC9B,IAAI,KAAU,MACZ,MAAU,MACR,GAAG,EAAc,2BAA2B,EAAI,eAAe,cAAc,sDAC/E;CAEF,OAAO;AACT"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"src-D5mfLR4V.js","names":[],"sources":["../../../../types/src/blocks.ts","../../../../types/src/social.ts","../../../../types/src/guards.ts","../../../../types/src/defaults.ts","../../../../types/src/template.ts","../../../../types/src/factory.ts","../../../../types/src/clone.ts","../../../../types/src/merge-tags.ts","../../../../types/src/config.ts"],"sourcesContent":["export interface SpacingValue {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nexport interface BlockStyles {\n padding: SpacingValue;\n backgroundColor?: string;\n}\n\nexport interface BlockVisibility {\n desktop: boolean;\n mobile: boolean;\n}\n\nexport interface BaseBlock {\n id: string;\n type: string;\n styles: BlockStyles;\n visibility?: BlockVisibility;\n displayCondition?: {\n label: string;\n before: string;\n after: string;\n group?: string;\n description?: string;\n };\n}\n\nexport type ColumnLayout = \"1\" | \"2\" | \"3\" | \"2-1\" | \"1-2\";\n\n/**\n * Optional outer frame for a section. When present, the section is rendered\n * inside an `mj-wrapper` — a full-width band (its own background + padding)\n * that frames the section, e.g. a white card sitting on a colored band.\n */\nexport interface SectionWrapper {\n backgroundColor?: string;\n padding?: SpacingValue;\n /** Corner radius in px for the outer frame. Omitted/0 = square corners. */\n borderRadius?: number;\n}\n\nexport interface SectionBlock extends BaseBlock {\n type: \"section\";\n columns: ColumnLayout;\n children: Block[][];\n /**\n * Whether columns stack vertically on mobile. Absent or `true` keeps MJML's\n * default responsive behavior (columns stack below 480px). `false` renders\n * the columns inside an `<mj-group>` so they stay side-by-side on mobile,\n * proportionally shrunk to fit.\n */\n stackOnMobile?: boolean;\n /** Corner radius in px. Omitted/0 = square corners. */\n borderRadius?: number;\n /** Optional outer frame (rendered as an `mj-wrapper` around the section). */\n wrapper?: SectionWrapper;\n}\n\nexport type HeadingLevel = 1 | 2 | 3 | 4;\n\nexport const HEADING_LEVEL_FONT_SIZE: Record<HeadingLevel, number> = {\n 1: 36,\n 2: 28,\n 3: 22,\n 4: 18,\n};\n\nexport interface TitleBlock extends BaseBlock {\n type: \"title\";\n content: string;\n level: HeadingLevel;\n /** Text color. Unset = inherit the document-level `textColor`. */\n color?: string;\n textAlign: \"left\" | \"center\" | \"right\";\n fontFamily?: string;\n}\n\nexport interface ParagraphBlock extends BaseBlock {\n type: \"paragraph\";\n content: string;\n}\n\nexport interface ImageBlock extends BaseBlock {\n type: \"image\";\n src: string;\n alt: string;\n width: number | \"full\";\n align: \"left\" | \"center\" | \"right\";\n linkUrl?: string;\n linkOpenInNewTab?: boolean;\n placeholderUrl?: string;\n decorative?: boolean;\n}\n\nexport interface ButtonBlock extends BaseBlock {\n type: \"button\";\n text: string;\n url: string;\n openInNewTab?: boolean;\n backgroundColor: string;\n textColor: string;\n borderRadius: number;\n fontSize: number;\n buttonPadding: SpacingValue;\n fontFamily?: string;\n width?: number | \"full\";\n}\n\nexport interface DividerBlock extends BaseBlock {\n type: \"divider\";\n lineStyle: \"solid\" | \"dashed\" | \"dotted\";\n color: string;\n thickness: number;\n width: number | \"full\";\n}\n\nexport interface VideoBlock extends BaseBlock {\n type: \"video\";\n url: string;\n openInNewTab?: boolean;\n thumbnailUrl: string;\n alt: string;\n width: number | \"full\";\n align: \"left\" | \"center\" | \"right\";\n placeholderUrl?: string;\n}\n\nexport type SocialPlatform =\n | \"facebook\"\n | \"twitter\"\n | \"instagram\"\n | \"linkedin\"\n | \"youtube\"\n | \"tiktok\"\n | \"pinterest\"\n | \"email\"\n | \"whatsapp\"\n | \"telegram\"\n | \"discord\"\n | \"snapchat\"\n | \"reddit\"\n | \"github\"\n | \"dribbble\"\n | \"behance\"\n | \"website\";\n\nexport type SocialIconStyle =\n \"solid\" | \"outlined\" | \"rounded\" | \"square\" | \"circle\";\n\nexport type SocialIconSize = \"small\" | \"medium\" | \"large\";\n\nexport interface SocialIcon {\n id: string;\n platform: SocialPlatform;\n url: string;\n}\n\nexport interface SocialIconsBlock extends BaseBlock {\n type: \"social\";\n icons: SocialIcon[];\n iconStyle: SocialIconStyle;\n iconSize: SocialIconSize;\n spacing: number;\n align: \"left\" | \"center\" | \"right\";\n}\n\nexport interface SpacerBlock extends BaseBlock {\n type: \"spacer\";\n height: number;\n}\n\nexport interface HtmlBlock extends BaseBlock {\n type: \"html\";\n content: string;\n}\n\nexport interface MenuItemData {\n id: string;\n text: string;\n url: string;\n openInNewTab: boolean;\n bold: boolean;\n underline: boolean;\n color?: string;\n}\n\nexport interface MenuBlock extends BaseBlock {\n type: \"menu\";\n items: MenuItemData[];\n fontSize: number;\n fontFamily?: string;\n /** Base text/link color. Unset = inherit the document-level `textColor`. */\n color?: string;\n linkColor?: string;\n textAlign: \"left\" | \"center\" | \"right\";\n separator: string;\n separatorColor: string;\n spacing: number;\n}\n\nexport interface TableCellData {\n id: string;\n content: string;\n}\n\nexport interface TableRowData {\n id: string;\n cells: TableCellData[];\n}\n\nexport interface TableBlock extends BaseBlock {\n type: \"table\";\n rows: TableRowData[];\n hasHeaderRow: boolean;\n headerBackgroundColor?: string;\n borderColor: string;\n borderWidth: number;\n cellPadding: number;\n fontSize: number;\n fontFamily?: string;\n /** Text color. Unset = inherit the document-level `textColor`. */\n color?: string;\n textAlign: \"left\" | \"center\" | \"right\";\n}\n\nexport interface CountdownBlock extends BaseBlock {\n type: \"countdown\";\n targetDate: string;\n timezone: string;\n showDays: boolean;\n showHours: boolean;\n showMinutes: boolean;\n showSeconds: boolean;\n separator: \":\" | \"-\" | \" \";\n digitFontSize: number;\n digitColor: string;\n labelColor: string;\n labelFontSize: number;\n backgroundColor: string;\n fontFamily?: string;\n labelDays: string;\n labelHours: string;\n labelMinutes: string;\n labelSeconds: string;\n expiredMessage: string;\n expiredImageUrl: string;\n hideOnExpiry: boolean;\n}\n\nexport interface CustomBlock extends BaseBlock {\n type: \"custom\";\n customType: string;\n fieldValues: Record<string, unknown>;\n renderedHtml?: string;\n dataSourceFetched?: boolean;\n}\n\nexport type Block =\n | SectionBlock\n | TitleBlock\n | ParagraphBlock\n | ImageBlock\n | ButtonBlock\n | DividerBlock\n | VideoBlock\n | SocialIconsBlock\n | SpacerBlock\n | HtmlBlock\n | MenuBlock\n | TableBlock\n | CountdownBlock\n | CustomBlock;\n\nexport type BlockType = Block[\"type\"];\n","import type { SocialPlatform } from \"./blocks\";\n\n/**\n * SVG glyph (24×24 path) + brand color for each social platform.\n *\n * Single source of truth shared by two consumers that render icons in\n * different formats: the editor draws them as inline SVG (`SocialIconSvg.vue`)\n * and `@templatical/renderer` rasterizes them to PNGs\n * (`scripts/rasterize-social.mjs`) because Outlook's Word engine has no SVG\n * support. Typing as `Record<SocialPlatform, ...>` makes adding a platform to\n * the union a compile error until its glyph is supplied here, keeping both\n * consumers in lockstep.\n */\nexport interface SocialIconGlyph {\n /** SVG path data on a 24×24 viewBox. */\n path: string;\n /** Brand color as a 6-digit hex string. */\n color: string;\n}\n\nexport const SOCIAL_ICON_GLYPHS: Record<SocialPlatform, SocialIconGlyph> = {\n facebook: {\n color: \"#1877F2\",\n path: \"M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z\",\n },\n twitter: {\n color: \"#000000\",\n path: \"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z\",\n },\n instagram: {\n color: \"#E4405F\",\n path: \"M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z\",\n },\n linkedin: {\n color: \"#0A66C2\",\n path: \"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\",\n },\n youtube: {\n color: \"#FF0000\",\n path: \"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z\",\n },\n tiktok: {\n color: \"#000000\",\n path: \"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z\",\n },\n pinterest: {\n color: \"#BD081C\",\n path: \"M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z\",\n },\n email: {\n color: \"#6B7280\",\n path: \"M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z\",\n },\n website: {\n color: \"#6B7280\",\n path: \"M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z\",\n },\n whatsapp: {\n color: \"#25D366\",\n path: \"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z\",\n },\n telegram: {\n color: \"#26A5E4\",\n path: \"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z\",\n },\n discord: {\n color: \"#5865F2\",\n path: \"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z\",\n },\n snapchat: {\n color: \"#FFFC00\",\n path: \"M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146 1.124.347 2.317.535 3.554.535 6.627 0 12.017-5.373 12.017-12.001C24.034 5.367 18.644 0 12.017 0z\",\n },\n reddit: {\n color: \"#FF4500\",\n path: \"M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z\",\n },\n github: {\n color: \"#181717\",\n path: \"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\",\n },\n dribbble: {\n color: \"#EA4C89\",\n path: \"M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm10.12-10.358c-.35-.11-3.17-.953-6.384-.438 1.34 3.684 1.887 6.684 1.992 7.308 2.3-1.555 3.936-4.02 4.392-6.87zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4 1.73 1.358 3.92 2.166 6.29 2.166 1.42 0 2.77-.29 4-.814zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765.135-.045.27-.084.405-.12-.26-.585-.54-1.167-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855zm-2.42-8.955c.46.008 4.683.026 9.477-1.248-1.698-3.018-3.53-5.558-3.8-5.928-2.868 1.35-5.01 3.99-5.676 7.17zM9.6 2.052c.282.38 2.145 2.914 3.822 6 3.645-1.365 5.19-3.44 5.373-3.702-1.81-1.61-4.19-2.586-6.795-2.586-.825 0-1.63.1-2.4.285zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04.24.49.47.985.68 1.486.08.18.15.36.22.53 3.41-.43 6.8.26 7.14.33-.02-2.42-.88-4.64-2.31-6.38z\",\n },\n behance: {\n color: \"#1769FF\",\n path: \"M22 7h-7V5h7v2zm1.726 10c-.442 1.297-2.029 3-5.101 3-3.074 0-5.564-1.729-5.564-5.675 0-3.91 2.325-5.92 5.466-5.92 3.082 0 4.964 1.782 5.375 4.426.078.506.109 1.188.095 2.14H15.97c.13 3.211 3.483 3.312 4.588 2.029h3.168zm-7.686-4h4.965c-.105-1.547-1.136-2.219-2.477-2.219-1.466 0-2.277.768-2.488 2.219zm-9.574 6.988H0V5.021h6.953c5.476.081 5.58 5.444 2.72 6.906 3.461 1.26 3.577 8.061-3.207 8.061zM3 11h3.584c2.508 0 2.906-3-.312-3H3v3zm3.391 3H3v3.016h3.341c3.055 0 2.868-3.016.05-3.016z\",\n },\n};\n","import type {\n Block,\n ButtonBlock,\n CountdownBlock,\n CustomBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n ParagraphBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n TableBlock,\n TitleBlock,\n VideoBlock,\n} from \"./blocks\";\n\nexport function isSection(block: Block): block is SectionBlock {\n return block.type === \"section\";\n}\n\nexport function isTitle(block: Block): block is TitleBlock {\n return block.type === \"title\";\n}\n\nexport function isParagraph(block: Block): block is ParagraphBlock {\n return block.type === \"paragraph\";\n}\n\nexport function isImage(block: Block): block is ImageBlock {\n return block.type === \"image\";\n}\n\nexport function isButton(block: Block): block is ButtonBlock {\n return block.type === \"button\";\n}\n\nexport function isDivider(block: Block): block is DividerBlock {\n return block.type === \"divider\";\n}\n\nexport function isVideo(block: Block): block is VideoBlock {\n return block.type === \"video\";\n}\n\nexport function isSocialIcons(block: Block): block is SocialIconsBlock {\n return block.type === \"social\";\n}\n\nexport function isSpacer(block: Block): block is SpacerBlock {\n return block.type === \"spacer\";\n}\n\nexport function isHtml(block: Block): block is HtmlBlock {\n return block.type === \"html\";\n}\n\nexport function isMenu(block: Block): block is MenuBlock {\n return block.type === \"menu\";\n}\n\nexport function isTable(block: Block): block is TableBlock {\n return block.type === \"table\";\n}\n\nexport function isCountdown(block: Block): block is CountdownBlock {\n return block.type === \"countdown\";\n}\n\nexport function isCustomBlock(block: Block): block is CustomBlock {\n return block.type === \"custom\";\n}\n","import type {\n ButtonBlock,\n CountdownBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n ParagraphBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n TableBlock,\n TitleBlock,\n VideoBlock,\n} from \"./blocks\";\nimport type { TemplateSettings } from \"./template\";\n\ntype BlockDefaultsFor<T> = Partial<Omit<T, \"id\" | \"type\">>;\n\nexport interface BlockDefaults {\n title?: BlockDefaultsFor<TitleBlock>;\n paragraph?: BlockDefaultsFor<ParagraphBlock>;\n image?: BlockDefaultsFor<ImageBlock>;\n button?: BlockDefaultsFor<ButtonBlock>;\n divider?: BlockDefaultsFor<DividerBlock>;\n section?: BlockDefaultsFor<SectionBlock>;\n video?: BlockDefaultsFor<VideoBlock>;\n social?: BlockDefaultsFor<SocialIconsBlock>;\n spacer?: BlockDefaultsFor<SpacerBlock>;\n html?: BlockDefaultsFor<HtmlBlock>;\n menu?: BlockDefaultsFor<MenuBlock>;\n table?: BlockDefaultsFor<TableBlock>;\n countdown?: BlockDefaultsFor<CountdownBlock>;\n}\n\nexport type TemplateDefaults = Partial<TemplateSettings>;\n\n// ---------------------------------------------------------------------------\n// Built-in default values — single source of truth for factories & consumers\n// ---------------------------------------------------------------------------\n\n// `color` is intentionally omitted: a new Title inherits the document\n// `textColor` (see `TemplateSettings`) unless the user sets a per-block color.\nexport const TITLE_BLOCK_DEFAULTS: BlockDefaultsFor<TitleBlock> = {\n content: \"<p>Enter your title</p>\",\n level: 2,\n textAlign: \"left\",\n};\n\nexport const PARAGRAPH_BLOCK_DEFAULTS: BlockDefaultsFor<ParagraphBlock> = {\n content: \"<p>Enter your text here</p>\",\n};\n\nexport const IMAGE_BLOCK_DEFAULTS: BlockDefaultsFor<ImageBlock> = {\n src: \"\",\n alt: \"\",\n width: \"full\",\n align: \"center\",\n};\n\nexport const BUTTON_BLOCK_DEFAULTS: BlockDefaultsFor<ButtonBlock> = {\n text: \"Click Here\",\n url: \"\",\n backgroundColor: \"#333333\",\n textColor: \"#ffffff\",\n borderRadius: 6,\n fontSize: 15,\n buttonPadding: { top: 12, right: 24, bottom: 12, left: 24 },\n};\n\nexport const DIVIDER_BLOCK_DEFAULTS: BlockDefaultsFor<DividerBlock> = {\n lineStyle: \"solid\",\n color: \"#e0e0e0\",\n thickness: 1,\n width: \"full\",\n};\n\nexport const SECTION_BLOCK_DEFAULTS: BlockDefaultsFor<SectionBlock> = {\n columns: \"1\",\n};\n\nexport const VIDEO_BLOCK_DEFAULTS: BlockDefaultsFor<VideoBlock> = {\n url: \"\",\n thumbnailUrl: \"\",\n alt: \"Video\",\n width: \"full\",\n align: \"center\",\n};\n\nexport const SOCIAL_ICONS_BLOCK_DEFAULTS: BlockDefaultsFor<SocialIconsBlock> = {\n iconStyle: \"solid\",\n iconSize: \"medium\",\n spacing: 10,\n align: \"center\",\n};\n\nexport const SPACER_BLOCK_DEFAULTS: BlockDefaultsFor<SpacerBlock> = {\n height: 24,\n};\n\nexport const HTML_BLOCK_DEFAULTS: BlockDefaultsFor<HtmlBlock> = {\n content: \"\",\n};\n\n// `color` is intentionally omitted: a new Menu inherits the document\n// `textColor` unless the user sets a per-block color (or per-item `linkColor`).\nexport const MENU_BLOCK_DEFAULTS: BlockDefaultsFor<MenuBlock> = {\n fontSize: 15,\n textAlign: \"center\",\n separator: \"|\",\n separatorColor: \"#e0e0e0\",\n spacing: 10,\n};\n\n// `color` is intentionally omitted: a new Table inherits the document\n// `textColor` unless the user sets a per-block color.\nexport const TABLE_BLOCK_DEFAULTS: BlockDefaultsFor<TableBlock> = {\n hasHeaderRow: true,\n borderColor: \"#e0e0e0\",\n borderWidth: 1,\n cellPadding: 8,\n fontSize: 15,\n textAlign: \"left\",\n};\n\nexport const COUNTDOWN_BLOCK_DEFAULTS: BlockDefaultsFor<CountdownBlock> = {\n targetDate: \"\",\n timezone: \"UTC\",\n showDays: true,\n showHours: true,\n showMinutes: true,\n showSeconds: true,\n separator: \":\",\n digitFontSize: 32,\n digitColor: \"#1a1a1a\",\n labelColor: \"#6b7280\",\n labelFontSize: 12,\n backgroundColor: \"#ffffff\",\n labelDays: \"Days\",\n labelHours: \"Hours\",\n labelMinutes: \"Minutes\",\n labelSeconds: \"Seconds\",\n expiredMessage: \"This offer has expired\",\n expiredImageUrl: \"\",\n hideOnExpiry: false,\n};\n\nexport const DEFAULT_BLOCK_DEFAULTS: Required<BlockDefaults> = {\n title: TITLE_BLOCK_DEFAULTS,\n paragraph: PARAGRAPH_BLOCK_DEFAULTS,\n image: IMAGE_BLOCK_DEFAULTS,\n button: BUTTON_BLOCK_DEFAULTS,\n divider: DIVIDER_BLOCK_DEFAULTS,\n section: SECTION_BLOCK_DEFAULTS,\n video: VIDEO_BLOCK_DEFAULTS,\n social: SOCIAL_ICONS_BLOCK_DEFAULTS,\n spacer: SPACER_BLOCK_DEFAULTS,\n html: HTML_BLOCK_DEFAULTS,\n menu: MENU_BLOCK_DEFAULTS,\n table: TABLE_BLOCK_DEFAULTS,\n countdown: COUNTDOWN_BLOCK_DEFAULTS,\n};\n\nexport const DEFAULT_TEMPLATE_DEFAULTS: TemplateDefaults = {\n width: 600,\n backgroundColor: \"#ffffff\",\n textColor: \"#1a1a1a\",\n linkUnderline: true,\n fontFamily: \"Arial\",\n locale: \"en\",\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype\n );\n}\n\nexport function deepMergeDefaults<T extends object>(\n base: T,\n overrides: Partial<T>,\n): T {\n const result = { ...base };\n\n for (const key of Object.keys(overrides) as Array<keyof T>) {\n const baseVal = base[key];\n const overrideVal = overrides[key];\n\n if (overrideVal === undefined) {\n continue;\n }\n\n if (isPlainObject(baseVal) && isPlainObject(overrideVal)) {\n result[key] = deepMergeDefaults(\n baseVal,\n overrideVal as Partial<typeof baseVal>,\n ) as T[keyof T];\n } else {\n result[key] = overrideVal as T[keyof T];\n }\n }\n\n return result;\n}\n","import type { Block } from \"./blocks\";\nimport { DEFAULT_TEMPLATE_DEFAULTS } from \"./defaults\";\nimport type { TemplateDefaults } from \"./defaults\";\n\nexport interface TemplateSettings {\n width: number;\n backgroundColor: string;\n /**\n * Document-level default text color: the `<mj-text>` default in the rendered\n * MJML, inherited by every text block (Title, Paragraph, Menu, Table) that\n * doesn't set its own `color`. Required, defaulting to `#1a1a1a` (see\n * `DEFAULT_TEMPLATE_DEFAULTS`); customize the default per-consumer via\n * `templateDefaults`. A block's explicit `color` or an inline text-color\n * mark overrides it.\n */\n textColor: string;\n /**\n * Document-level link color: emitted as the global `a { color }` rule in the\n * rendered MJML, so it cascades to every link — rich-text and menu alike.\n * Optional: when unset, links inherit the surrounding text color\n * (`color: inherit`), preserving the pre-#352 default. A per-block or\n * per-item color (a Menu item's `color`, `MenuBlock.linkColor`) still\n * overrides it.\n */\n linkColor?: string;\n /**\n * Whether links are underlined document-wide: drives the global\n * `a { text-decoration }` rule (`underline` when true, `none` when false).\n * Required, defaulting to `true` (see `DEFAULT_TEMPLATE_DEFAULTS`) — the\n * common, more accessible email default. Applies to body (rich-text) links;\n * buttons and menu items carry their own inline `text-decoration` and are\n * unaffected. Set `false` to render links without an underline.\n */\n linkUnderline: boolean;\n fontFamily: string;\n preheaderText?: string;\n /**\n * BCP-47 language code for the rendered email's `<html lang>`. Drives\n * screen-reader pronunciation. Default `'en'` via `DEFAULT_TEMPLATE_DEFAULTS`.\n */\n locale: string;\n}\n\nexport interface TemplateContent {\n blocks: Block[];\n settings: TemplateSettings;\n}\n\nexport function createDefaultTemplateContent(\n defaultFontFamily = \"Arial\",\n templateDefaults?: TemplateDefaults,\n): TemplateContent {\n return {\n blocks: [],\n settings: {\n ...DEFAULT_TEMPLATE_DEFAULTS,\n fontFamily: defaultFontFamily,\n ...templateDefaults,\n } as TemplateSettings,\n };\n}\n","import type {\n Block,\n BlockStyles,\n BlockType,\n ButtonBlock,\n CountdownBlock,\n CustomBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n ParagraphBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n SpacingValue,\n TableBlock,\n TableCellData,\n TableRowData,\n TitleBlock,\n VideoBlock,\n} from \"./blocks\";\nimport type { CustomBlockDefinition, CustomBlockField } from \"./custom-blocks\";\nimport type { BlockDefaults } from \"./defaults\";\nimport {\n BUTTON_BLOCK_DEFAULTS,\n COUNTDOWN_BLOCK_DEFAULTS,\n deepMergeDefaults,\n DIVIDER_BLOCK_DEFAULTS,\n HTML_BLOCK_DEFAULTS,\n IMAGE_BLOCK_DEFAULTS,\n MENU_BLOCK_DEFAULTS,\n PARAGRAPH_BLOCK_DEFAULTS,\n SECTION_BLOCK_DEFAULTS,\n SOCIAL_ICONS_BLOCK_DEFAULTS,\n SPACER_BLOCK_DEFAULTS,\n TABLE_BLOCK_DEFAULTS,\n TITLE_BLOCK_DEFAULTS,\n VIDEO_BLOCK_DEFAULTS,\n} from \"./defaults\";\n\nfunction applyDefaults<T extends object>(\n base: T,\n partial: Partial<T> | undefined,\n): T {\n if (!partial || Object.keys(partial).length === 0) return base;\n return deepMergeDefaults(base, partial);\n}\n\nexport function generateId(): string {\n return crypto.randomUUID();\n}\n\nfunction createDefaultSpacing(value = 0): SpacingValue {\n return { top: value, right: value, bottom: value, left: value };\n}\n\nfunction createDefaultStyles(padding = 10): BlockStyles {\n return {\n padding: createDefaultSpacing(padding),\n };\n}\n\nexport function createTitleBlock(\n partial: Partial<TitleBlock> = {},\n): TitleBlock {\n const base: TitleBlock = {\n id: generateId(),\n type: \"title\",\n ...TITLE_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as TitleBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createParagraphBlock(\n partial: Partial<ParagraphBlock> = {},\n): ParagraphBlock {\n const base: ParagraphBlock = {\n id: generateId(),\n type: \"paragraph\",\n ...PARAGRAPH_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as ParagraphBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createImageBlock(\n partial: Partial<ImageBlock> = {},\n): ImageBlock {\n const base: ImageBlock = {\n id: generateId(),\n type: \"image\",\n ...IMAGE_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as ImageBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createButtonBlock(\n partial: Partial<ButtonBlock> = {},\n): ButtonBlock {\n const base: ButtonBlock = {\n id: generateId(),\n type: \"button\",\n ...BUTTON_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as ButtonBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createDividerBlock(\n partial: Partial<DividerBlock> = {},\n): DividerBlock {\n const base: DividerBlock = {\n id: generateId(),\n type: \"divider\",\n ...DIVIDER_BLOCK_DEFAULTS,\n styles: createDefaultStyles(20),\n } as DividerBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createSectionBlock(\n partial: Partial<SectionBlock> = {},\n): SectionBlock {\n const base: SectionBlock = {\n id: generateId(),\n type: \"section\",\n ...SECTION_BLOCK_DEFAULTS,\n children: [[]],\n styles: createDefaultStyles(20),\n } as SectionBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createVideoBlock(\n partial: Partial<VideoBlock> = {},\n): VideoBlock {\n const base: VideoBlock = {\n id: generateId(),\n type: \"video\",\n ...VIDEO_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as VideoBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createSocialIconsBlock(\n partial: Partial<SocialIconsBlock> = {},\n): SocialIconsBlock {\n const base: SocialIconsBlock = {\n id: generateId(),\n type: \"social\",\n icons: [],\n ...SOCIAL_ICONS_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as SocialIconsBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createSpacerBlock(\n partial: Partial<SpacerBlock> = {},\n): SpacerBlock {\n const base: SpacerBlock = {\n id: generateId(),\n type: \"spacer\",\n ...SPACER_BLOCK_DEFAULTS,\n styles: createDefaultStyles(0),\n } as SpacerBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createHtmlBlock(partial: Partial<HtmlBlock> = {}): HtmlBlock {\n const base: HtmlBlock = {\n id: generateId(),\n type: \"html\",\n ...HTML_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as HtmlBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createMenuBlock(partial: Partial<MenuBlock> = {}): MenuBlock {\n const base: MenuBlock = {\n id: generateId(),\n type: \"menu\",\n items: [],\n ...MENU_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as MenuBlock;\n return applyDefaults(base, partial);\n}\n\nfunction createDefaultTableRows(columns: number, rows: number): TableRowData[] {\n return Array.from({ length: rows }, () => ({\n id: generateId(),\n cells: Array.from({ length: columns }, (): TableCellData => ({\n id: generateId(),\n content: \"\",\n })),\n }));\n}\n\nexport function createTableBlock(\n partial: Partial<TableBlock> = {},\n): TableBlock {\n const base: TableBlock = {\n id: generateId(),\n type: \"table\",\n rows: createDefaultTableRows(3, 3),\n ...TABLE_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as TableBlock;\n return applyDefaults(base, partial);\n}\n\nexport function createCountdownBlock(\n partial: Partial<CountdownBlock> = {},\n): CountdownBlock {\n const base: CountdownBlock = {\n id: generateId(),\n type: \"countdown\",\n ...COUNTDOWN_BLOCK_DEFAULTS,\n styles: createDefaultStyles(),\n } as CountdownBlock;\n return applyDefaults(base, partial);\n}\n\nfunction getFieldDefault(field: CustomBlockField): unknown {\n if (field.type === \"repeatable\") {\n return field.default ?? [];\n }\n if (field.type === \"boolean\") {\n return field.default ?? false;\n }\n if (field.type === \"number\") {\n return field.default ?? 0;\n }\n\n return field.default ?? \"\";\n}\n\nexport function createCustomBlock(\n definition: CustomBlockDefinition,\n): CustomBlock {\n const fieldValues: Record<string, unknown> = {};\n\n for (const field of definition.fields) {\n fieldValues[field.key] = getFieldDefault(field);\n }\n\n const styles = applyDefaults(createDefaultStyles(), definition.defaultStyles);\n\n return {\n id: generateId(),\n type: \"custom\",\n customType: definition.type,\n fieldValues,\n styles,\n ...(definition.dataSource ? { dataSourceFetched: false } : {}),\n };\n}\n\nexport function createBlock(\n type: BlockType,\n blockDefaults?: BlockDefaults,\n): Block {\n switch (type) {\n case \"section\":\n return createSectionBlock(blockDefaults?.section);\n case \"title\":\n return createTitleBlock(blockDefaults?.title);\n case \"paragraph\":\n return createParagraphBlock(blockDefaults?.paragraph);\n case \"image\":\n return createImageBlock(blockDefaults?.image);\n case \"button\":\n return createButtonBlock(blockDefaults?.button);\n case \"divider\":\n return createDividerBlock(blockDefaults?.divider);\n case \"video\":\n return createVideoBlock(blockDefaults?.video);\n case \"social\":\n return createSocialIconsBlock(blockDefaults?.social);\n case \"spacer\":\n return createSpacerBlock(blockDefaults?.spacer);\n case \"html\":\n return createHtmlBlock(blockDefaults?.html);\n case \"menu\":\n return createMenuBlock(blockDefaults?.menu);\n case \"table\":\n return createTableBlock(blockDefaults?.table);\n case \"countdown\":\n return createCountdownBlock(blockDefaults?.countdown);\n default:\n throw new Error(`Unknown block type: ${type}`);\n }\n}\n\nexport function cloneBlock(block: Block): Block {\n const cloned = JSON.parse(JSON.stringify(block)) as Block;\n cloned.id = generateId();\n\n if (cloned.type === \"section\") {\n cloned.children = cloned.children.map((column) =>\n column.map((child) => cloneBlock(child)),\n );\n }\n\n return cloned;\n}\n","/**\n * Cycle-safe deep clone via a JSON round-trip.\n *\n * A naked `JSON.stringify` throws `Converting circular structure to JSON`\n * if the tree is self-referencing — e.g. a DOM element carrying a Sortable\n * expando back-ref (`HTMLDivElement.SortableXXX -> instance -> el -> div`)\n * leaks into block data through a drag handler inside a section. Both the\n * editor's public `getContent()` export path and the history snapshot path\n * must tolerate that: we drop the offending back-ref from the clone rather\n * than throw. Losing a transient DOM expando is harmless; the block data is\n * intact.\n *\n * The `WeakSet` replacer omits any object already seen on the current path,\n * which covers every cyclic shape. Template content is tree-shaped (and is\n * serialized to JSON for storage anyway), so dropping repeated references\n * never costs real data.\n */\nexport function safeClone<T>(value: T): T {\n const seen = new WeakSet<object>();\n return JSON.parse(\n JSON.stringify(value, (_key, val) => {\n if (typeof val === \"object\" && val !== null) {\n if (seen.has(val)) return undefined;\n seen.add(val);\n }\n return val;\n }),\n ) as T;\n}\n","import type { MergeTag } from \"./config\";\n\n// --- Syntax Presets ---\n\nexport interface SyntaxPreset {\n value: RegExp;\n logic: RegExp;\n}\n\nexport type SyntaxPresetName =\n \"liquid\" | \"handlebars\" | \"mailchimp\" | \"ampscript\";\n\nexport const SYNTAX_PRESETS: Record<SyntaxPresetName, SyntaxPreset> = {\n liquid: { value: /\\{\\{.+?\\}\\}/g, logic: /\\{%-?\\s*(\\w+).*?-?%\\}/g },\n handlebars: {\n value: /\\{\\{\\{?.+?\\}?\\}\\}/g,\n logic: /\\{\\{[#/](\\w+).*?\\}\\}/g,\n },\n mailchimp: { value: /\\*\\|\\w+\\|\\*/g, logic: /\\*\\|(\\w+)[:|].*?\\|\\*/g },\n ampscript: { value: /%%=.+?=%%/g, logic: /%%\\[\\s*(\\w+).*?\\]%%/g },\n};\n\nconst SYNTAX_TRIGGER_CHARS: Record<SyntaxPresetName, string> = {\n liquid: \"{{\",\n handlebars: \"{{\",\n mailchimp: \"*|\",\n ampscript: \"%%=\",\n};\n\nconst SYNTAX_CLOSING_CHARS: Record<SyntaxPresetName, string> = {\n liquid: \"}}\",\n handlebars: \"}}\",\n mailchimp: \"|*\",\n ampscript: \"=%%\",\n};\n\n/**\n * Resolves the autocomplete trigger string for a syntax preset.\n * Returns null when the syntax doesn't match any built-in preset\n * (custom regex syntax — autocomplete cannot be enabled safely).\n */\nexport function getSyntaxTriggerChar(syntax: SyntaxPreset): string | null {\n for (const name of Object.keys(SYNTAX_PRESETS) as SyntaxPresetName[]) {\n if (SYNTAX_PRESETS[name].value.source === syntax.value.source) {\n return SYNTAX_TRIGGER_CHARS[name];\n }\n }\n return null;\n}\n\n/**\n * Resolves the closing delimiter for a syntax preset (e.g. `}}` for liquid,\n * `|*` for mailchimp). The autocomplete trigger detector uses it to tell an\n * open tag (`{{first`) from a completed one (`{{first_name}}`) so the popup\n * doesn't reappear over a finished tag. Returns null for custom syntaxes —\n * the same set for which `getSyntaxTriggerChar` returns null.\n */\nexport function getSyntaxClosingChar(syntax: SyntaxPreset): string | null {\n for (const name of Object.keys(SYNTAX_PRESETS) as SyntaxPresetName[]) {\n if (SYNTAX_PRESETS[name].value.source === syntax.value.source) {\n return SYNTAX_CLOSING_CHARS[name];\n }\n }\n return null;\n}\n\nexport function resolveSyntax(\n syntax?: SyntaxPresetName | SyntaxPreset,\n): SyntaxPreset {\n if (!syntax) {\n return SYNTAX_PRESETS.liquid;\n }\n\n if (typeof syntax === \"string\") {\n return SYNTAX_PRESETS[syntax] ?? SYNTAX_PRESETS.liquid;\n }\n\n return syntax;\n}\n\n// --- Merge Tag Utilities ---\n\nfunction escapeRegExp(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction anchoredRegex(pattern: RegExp): RegExp {\n const source = pattern.source;\n const flags = pattern.flags.replace(\"g\", \"\");\n return new RegExp(`^${source}$`, flags);\n}\n\nexport function isMergeTagValue(value: string, syntax: SyntaxPreset): boolean {\n const trimmed = value?.trim() || \"\";\n // Handlebars (and similar) value regex is liberal enough to also match\n // logic tags like `{{#each items}}`. Exclude logic-shaped tags so callers\n // that rely on this discriminator (UI segmentation, label rendering)\n // don't misclassify them.\n if (anchoredRegex(syntax.logic).test(trimmed)) {\n return false;\n }\n return anchoredRegex(syntax.value).test(trimmed);\n}\n\nexport function getMergeTagLabel(value: string, mergeTags: MergeTag[]): string {\n const found = mergeTags.find((p) => p.value === value);\n if (found) {\n return found.label;\n }\n return value;\n}\n\nexport function resolveHtmlMergeTagLabels(\n html: string,\n mergeTags: MergeTag[],\n): string {\n return rewriteSpanByAttr(html, \"data-merge-tag\", (value) =>\n getMergeTagLabel(value, mergeTags),\n );\n}\n\n/**\n * The example value configured for `value`, or `undefined` when the tag is\n * unknown or declares none. Unlike `getMergeTagLabel`, which falls back to the\n * raw token, absence is reported as `undefined` so callers can choose their own\n * fallback — Sample mode falls back to the label, never to a raw token.\n */\nexport function getMergeTagSample(\n value: string,\n mergeTags: MergeTag[],\n): string | undefined {\n return mergeTags.find((p) => p.value === value)?.sample;\n}\n\n/**\n * Rewrite rich-text HTML for the previews' Sample mode.\n *\n * A tag **with** a sample is replaced by that value as bare text, wrapper and\n * all — dropping the `<span>` is what removes the merge-tag highlight, so the\n * value reads as ordinary content. A tag **without** one keeps its span and\n * shows its label, so it stays visibly dynamic; that difference is deliberate,\n * and it doubles as a signal that a sample is still unset.\n *\n * The unwrapped output is inert — `data-merge-tag` is gone from it — so this\n * result must only ever reach a preview surface, never `getContent()`, an\n * export or a send.\n */\nexport function substituteHtmlMergeTagSamples(\n html: string,\n mergeTags: MergeTag[],\n): string {\n return rewriteSpanByAttr(\n html,\n \"data-merge-tag\",\n (value) =>\n escapeTagText(\n getMergeTagSample(value, mergeTags) ??\n getMergeTagLabel(value, mergeTags),\n ),\n { unwrap: (value) => getMergeTagSample(value, mergeTags) !== undefined },\n );\n}\n\n/**\n * Whether any configured tag declares a `sample`.\n *\n * Gates the whole feature: with no samples anywhere the previews' Sample view\n * would be identical to the Label view, so the editor defaults to Label and\n * renders no toggle. That keeps the feature invisible until a consumer opts in.\n */\nexport function hasMergeTagSamples(mergeTags: MergeTag[]): boolean {\n return mergeTags.some((tag) => tag.sample !== undefined);\n}\n\n/**\n * Replace declared tokens with their sample values in a plain string — used for\n * values that reach no label-resolving component, namely `html` block content\n * and custom-block field values.\n *\n * Matching is on the exact configured `value`, so consumer text cannot\n * false-positive and `href=\"{{link}}\"` resolves correctly. Tags without a\n * sample are left untouched, which is what keeps a missing sample visible.\n */\nexport function substituteTextMergeTagSamples(\n text: string,\n mergeTags: MergeTag[],\n): string {\n if (!text) return text;\n\n let out = text;\n for (const tag of mergeTags) {\n if (tag.sample === undefined) continue;\n // split/join rather than a RegExp: the token is a literal, so this needs no\n // escaping of regex metacharacters (`{`, `|`, `*` all appear in real syntaxes).\n out = out.split(tag.value).join(tag.sample);\n }\n return out;\n}\n\n/**\n * Escape a sample before it is inlined into rich-text HTML. Samples are\n * consumer-authored strings, so `<`, `&` and quotes would otherwise alter the\n * surrounding markup.\n */\nfunction escapeTagText(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nexport function containsMergeTag(value: string, syntax: SyntaxPreset): boolean {\n if (!value) return false;\n\n const valueRegex = new RegExp(syntax.value.source, syntax.value.flags);\n const logicRegex = new RegExp(syntax.logic.source, syntax.logic.flags);\n\n return valueRegex.test(value) || logicRegex.test(value);\n}\n\nexport function restoreMergeTagMarkup(\n html: string,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): string {\n let result = html;\n\n for (const tag of mergeTags) {\n const escaped = escapeRegExp(tag.value);\n const pattern = new RegExp(`(?<!data-merge-tag=\")${escaped}`, \"g\");\n result = result.replace(pattern, (match) => {\n const label = getMergeTagLabel(match, mergeTags);\n return `<span data-merge-tag=\"${match}\">${label}</span>`;\n });\n }\n\n const logicRegex = new RegExp(\n `(?<!data-logic-merge-tag=\")${syntax.logic.source}`,\n syntax.logic.flags,\n );\n result = result.replace(logicRegex, (match) => {\n const keyword = getLogicMergeTagKeyword(match, syntax);\n return `<span data-logic-merge-tag=\"${match}\">${keyword}</span>`;\n });\n\n return result;\n}\n\nexport function isLogicMergeTagValue(\n value: string,\n syntax: SyntaxPreset,\n): boolean {\n return anchoredRegex(syntax.logic).test(value?.trim() || \"\");\n}\n\nexport function getLogicMergeTagKeyword(\n value: string,\n syntax: SyntaxPreset,\n): string {\n const regex = new RegExp(\n syntax.logic.source,\n syntax.logic.flags.replace(\"g\", \"\"),\n );\n const match = value.match(regex);\n return match && match[1] ? match[1].toUpperCase() : value;\n}\n\nexport function resolveHtmlLogicMergeTagLabels(\n html: string,\n syntax: SyntaxPreset,\n): string {\n return rewriteSpanByAttr(html, \"data-logic-merge-tag\", (value) =>\n getLogicMergeTagKeyword(value, syntax),\n );\n}\n\n/**\n * Walk `html` and rewrite the inner text of every `<span … {attrName}=\"…\">…</span>`\n * by passing the attribute value through `relabel`. Linear in `html.length`:\n * each `indexOf` advances the cursor monotonically, and no regex backtracking\n * can run over the whole string.\n *\n * Replaces the original `/<span[^>]*…[^>]*>(.*?)<\\/span>/g` pattern, which\n * was polynomial-ReDoS over inputs that contained many `<span` starts with\n * no closing `>`.\n */\nfunction rewriteSpanByAttr(\n html: string,\n attrName: string,\n relabel: (value: string) => string,\n options?: { unwrap?: (value: string) => boolean },\n): string {\n // Anchored on `>` per match. `[^<>\"]*` for the attribute value fails fast\n // on a missing closing quote instead of backtracking across the input.\n const attrPattern = new RegExp(`(?:^|\\\\s)${attrName}=\"([^\"<>]*)\"`);\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<span\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n const afterTagName = html[open + 5];\n if (\n afterTagName !== \">\" &&\n afterTagName !== \" \" &&\n afterTagName !== \"\\t\" &&\n afterTagName !== \"\\n\" &&\n afterTagName !== \"\\r\" &&\n afterTagName !== \"/\"\n ) {\n out += html.substring(i, open + 5);\n i = open + 5;\n continue;\n }\n const openEnd = html.indexOf(\">\", open + 5);\n if (openEnd === -1) {\n out += html.substring(i);\n break;\n }\n const closeStart = html.indexOf(\"</span>\", openEnd + 1);\n if (closeStart === -1) {\n out += html.substring(i);\n break;\n }\n const attrs = html.substring(open + 5, openEnd);\n const attrMatch = attrPattern.exec(attrs);\n if (!attrMatch) {\n // This `<span>` isn't the one we're looking for — emit up to and\n // including the `<span` literal and let the next iteration scan\n // inward. Skipping straight to the matching `</span>` would swallow\n // any nested merge-tag span in the same loop iteration.\n out += html.substring(i, open + 5);\n i = open + 5;\n continue;\n }\n const value = attrMatch[1];\n const newLabel = relabel(value);\n if (options?.unwrap?.(value)) {\n // Emit the text before the span, then the replacement alone — the open\n // and close tags are dropped, so the span's styling (the merge-tag\n // highlight) no longer applies to it. Decided per tag, so a tag that\n // resolved to a sample loses the highlight while one that fell back to\n // its label keeps it.\n out += html.substring(i, open);\n out += newLabel;\n } else {\n out += html.substring(i, openEnd + 1);\n out += newLabel;\n out += \"</span>\";\n }\n i = closeStart + 7;\n }\n return out;\n}\n","import type { SyntaxPreset, SyntaxPresetName } from \"./merge-tags\";\n\nexport type ViewportSize = \"desktop\" | \"mobile\";\n\nexport type UiTheme = \"light\" | \"dark\" | \"auto\";\n\nexport interface CustomFont {\n name: string;\n url: string;\n fallback?: string;\n}\n\nexport interface FontsConfig {\n defaultFallback?: string;\n defaultFont?: string;\n customFonts?: CustomFont[];\n /**\n * Restrict which of the seven built-in fonts the font picker offers.\n *\n * - `true` / omitted — every built-in is offered (the default).\n * - `false` — no built-ins; the picker lists only `customFonts`. Use this to\n * keep authors on an embedded brand kit's typefaces.\n * - `string[]` — an allowlist of built-in names to keep, matched\n * case-insensitively (`['Georgia', 'arial']`). A name that isn't a built-in\n * is logged with a warning and skipped — consistent with how\n * `paletteBlocks` treats an unknown entry — so a typo narrows the list\n * rather than silently offering all seven.\n *\n * Independent of `customFonts`: excluding the built-ins never removes a custom\n * font, and a custom font can still be the `defaultFont` when every built-in\n * is excluded. Filtering the picker never affects rendering — content already\n * using an excluded built-in still resolves to its proper fallback stack.\n */\n builtIns?: boolean | string[];\n}\n\nexport interface ColorsConfig {\n /**\n * Preset colors rendered as a clickable grid inside every color picker\n * popover. Each entry is a hex string (`'#0b5cff'`); clicking one applies it,\n * and the preset matching the current value is marked selected (matched\n * case-insensitively). The grid supplements the wheel and hex input unless\n * `allowCustom` is `false`.\n */\n presets?: string[];\n /**\n * Allow free-form color entry — the wheel and hex input — alongside the\n * presets. Defaults to `true`.\n *\n * Set to `false` together with `presets` to lock authors to the preset\n * palette: the popover then offers only the preset grid, with no wheel or hex\n * field (a white-label / brand-kit constraint). Ignored with a warning when\n * no `presets` are configured — hiding the only inputs would leave the picker\n * with no way to choose a color.\n */\n allowCustom?: boolean;\n}\n\nexport interface ExportResult {\n html: string;\n mjml: string;\n}\n\nexport interface MergeTag {\n label: string;\n value: string;\n /**\n * Optional grouping label used by the built-in merge tag picker to\n * section the list. When no tag in the configured array carries\n * `group`, the picker renders a plain flat list with no headers.\n * Ignored by the renderer and by typing-autocomplete.\n */\n group?: string;\n /**\n * Optional helper text shown beneath the tag in the built-in merge\n * tag picker. Not rendered anywhere else (toolbar, autocomplete,\n * MJML output) and not stored on the inserted document node.\n */\n description?: string;\n /**\n * Example value shown in place of this tag on preview surfaces — e.g.\n * `\"Ada\"` for `{{first_name}}` — so a preview reads like a delivered\n * email instead of a list of field names.\n *\n * Display-only, and only in the previews' Sample mode: never written to\n * the template, never sent, never present in MJML output. Setting it is\n * the whole opt-in; there is no accompanying flag. A tag without a\n * sample keeps showing its `label`.\n */\n sample?: string;\n}\n\nexport interface MediaResult {\n url: string;\n alt?: string;\n}\n\nexport interface MergeTagsConfig {\n syntax?: SyntaxPresetName | SyntaxPreset;\n tags?: MergeTag[];\n onRequest?: () => Promise<MergeTag | null>;\n /**\n * Enables typing-based autocomplete in rich text blocks and in every\n * merge-tag-enabled input/textarea field (toolbars, template settings,\n * custom-block text fields). When the user types the syntax opener\n * (e.g. `{{`), a popup lists matching `tags`. Both surfaces share the\n * same popup, so behavior is identical.\n *\n * Defaults to `true`. Effective only when `tags` is non-empty AND\n * `syntax` matches a built-in preset (custom regex syntaxes cannot be\n * mapped to a trigger string and silently disable autocomplete).\n */\n autocomplete?: boolean;\n}\n\n/**\n * A standalone logic tag — a control-flow token (e.g. `{% else %}`) inserted\n * at the cursor. Distinct from data merge tags; rendered as a keyword badge.\n */\nexport interface LogicTag {\n label: string;\n value: string;\n /** Optional grouping label used to section the logic picker. */\n group?: string;\n /** Optional helper text shown beneath the tag in the picker. */\n description?: string;\n}\n\n/**\n * A paired logic construct — an opening tag (`before`) and its matching closer\n * (`after`), e.g. `{% if vip %}` … `{% endif %}`. Selecting one inserts both\n * pills at once, wrapping the selection (or with the caret placed between\n * them). Shares its `before`/`after` shape with `DisplayCondition`.\n */\nexport interface LogicPair {\n label: string;\n /** Opening logic tag, including delimiters (e.g. `{% if vip %}`). */\n before: string;\n /** Closing logic tag, including delimiters (e.g. `{% endif %}`). */\n after: string;\n /** Optional grouping label used to section the logic picker. */\n group?: string;\n /** Optional helper text shown beneath the pair in the picker. */\n description?: string;\n}\n\n/**\n * Standalone logic-tag configuration, independent of `mergeTags`. `tags` are\n * single control-flow tokens inserted at the cursor; `pairs` are open/close\n * constructs inserted around the selection. Both appear in the built-in logic\n * picker, grouped together by `group`. Typed/pasted logic highlighting is\n * always on regardless of this.\n */\nexport interface LogicTagsConfig {\n tags?: LogicTag[];\n pairs?: LogicPair[];\n /**\n * Consumer-owned picker. When set, the \"Insert logic\" affordance calls this\n * instead of the built-in logic picker — return the chosen tag/pair (or\n * `null` to cancel). Mirrors `MergeTagsConfig.onRequest`; lets consumers use\n * their own UI and custom syntax. Precedence: `onRequest` → built-in picker.\n */\n onRequest?: () => Promise<LogicTag | LogicPair | null>;\n}\n\nexport interface DisplayCondition {\n label: string;\n before: string;\n after: string;\n group?: string;\n description?: string;\n}\n\nexport interface DisplayConditionsConfig {\n conditions: DisplayCondition[];\n allowCustom?: boolean;\n}\n\nexport interface ThemeOverrides {\n bg?: string;\n bgElevated?: string;\n bgHover?: string;\n bgActive?: string;\n border?: string;\n borderLight?: string;\n text?: string;\n textMuted?: string;\n textDim?: string;\n primary?: string;\n primaryHover?: string;\n primaryLight?: string;\n secondary?: string;\n secondaryHover?: string;\n secondaryLight?: string;\n success?: string;\n successLight?: string;\n warning?: string;\n warningLight?: string;\n danger?: string;\n dangerLight?: string;\n canvasBg?: string;\n dark?: Omit<ThemeOverrides, \"dark\">;\n}\n\nexport class SdkError extends Error {\n constructor(\n message: string,\n public readonly statusCode?: number,\n ) {\n super(message);\n this.name = \"SdkError\";\n }\n\n get isNotFound(): boolean {\n return this.statusCode === 404;\n }\n\n get isUnauthorized(): boolean {\n return this.statusCode === 401;\n }\n\n get isServerError(): boolean {\n return this.statusCode !== undefined && this.statusCode >= 500;\n }\n}\n"],"mappings":"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"}