@templatical/editor 0.26.2 → 0.26.3
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.
- package/dist/{AiFeatureMenu-BY6mubnR.js → AiFeatureMenu-Cy-PVrNT.js} +1 -1
- package/dist/{BlockPreviewCanvas-DbNOqSI1.js → BlockPreviewCanvas-BZKslwGI.js} +1 -1
- package/dist/{CloudEditor-DEnMNMbm.js → CloudEditor-CPRkQCRa.js} +4 -4
- package/dist/{CollaboratorBar-zvfvE7f-.js → CollaboratorBar-BGwbg9aN.js} +1 -1
- package/dist/{ParagraphEditor-CLvv5Ija.js → ParagraphEditor-6WN5jmXr.js} +4 -3
- package/dist/{SaveBlockDialog-fVp_PwZi.js → SaveBlockDialog-21vDlEBS.js} +3 -3
- package/dist/{SavedBlocksBrowserModal-KWlaiLlr.js → SavedBlocksBrowserModal-GMrajvfV.js} +4 -4
- package/dist/{SavedBlocksPanels-CQsZvWO_.js → SavedBlocksPanels-DEPmNhi6.js} +1 -1
- package/dist/{SavedBlocksPickBar-C3h8oL3r.js → SavedBlocksPickBar-Dk_IaMO9.js} +1 -1
- package/dist/{SnapshotHistory-DjA3Ej_i.js → SnapshotHistory-Cb4WW2gf.js} +1 -1
- package/dist/{TestEmailModal-EM0RP_9L.js → TestEmailModal-BHDKmd6B.js} +3 -3
- package/dist/{TestEmailPanel-B6goIAxT.js → TestEmailPanel-DubocPxf.js} +1 -1
- package/dist/{TitleEditor-JEaP9jVF.js → TitleEditor-Cy4B3UNe.js} +4 -3
- package/dist/{TplModal-DoneXIj_.js → TplModal-Dv9zgSWI.js} +1 -1
- package/dist/{blockTypeIcons-2_85L_CL.js → blockTypeIcons-C7wRSaT3.js} +1 -1
- package/dist/bundle-stats.json +5 -5
- package/dist/cdn/chunks/{AiFeatureMenu-Cwj8afsi.js → AiFeatureMenu-Cb9gaRBn.js} +2 -2
- package/dist/cdn/chunks/{AiFeatureMenu-Cwj8afsi.js.map → AiFeatureMenu-Cb9gaRBn.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-DPtiMsVz.js → BlockPreviewCanvas-msj619wT.js} +2 -2
- package/dist/cdn/chunks/{BlockPreviewCanvas-DPtiMsVz.js.map → BlockPreviewCanvas-msj619wT.js.map} +1 -1
- package/dist/cdn/chunks/{CloudEditor-CQ830jJt.js → CloudEditor-DbyEsIxu.js} +5 -5
- package/dist/cdn/chunks/{CloudEditor-CQ830jJt.js.map → CloudEditor-DbyEsIxu.js.map} +1 -1
- package/dist/cdn/chunks/{CollaboratorBar-BF469_AN.js → CollaboratorBar-G_sZDZXw.js} +2 -2
- package/dist/cdn/chunks/{CollaboratorBar-BF469_AN.js.map → CollaboratorBar-G_sZDZXw.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-Bzcma_Lm.js → ParagraphEditor-CZ5Z6AgG.js} +5 -4
- package/dist/cdn/chunks/{ParagraphEditor-Bzcma_Lm.js.map → ParagraphEditor-CZ5Z6AgG.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-BfNVeXXV.js → SaveBlockDialog-CVIVZEuy.js} +4 -4
- package/dist/cdn/chunks/{SaveBlockDialog-BfNVeXXV.js.map → SaveBlockDialog-CVIVZEuy.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-C59D4G5C.js → SavedBlocksBrowserModal-BkQXEUE2.js} +4 -4
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-C59D4G5C.js.map → SavedBlocksBrowserModal-BkQXEUE2.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-e4HyTZ8v.js → SavedBlocksPanels-CUtYoi5Z.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPanels-e4HyTZ8v.js.map → SavedBlocksPanels-CUtYoi5Z.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPickBar-DMHxPCM4.js → SavedBlocksPickBar-C80qJykl.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPickBar-DMHxPCM4.js.map → SavedBlocksPickBar-C80qJykl.js.map} +1 -1
- package/dist/cdn/chunks/{SnapshotHistory-80tAU_VX.js → SnapshotHistory-DFinMrg_.js} +2 -2
- package/dist/cdn/chunks/{SnapshotHistory-80tAU_VX.js.map → SnapshotHistory-DFinMrg_.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-DdWfAGNe.js → TestEmailModal-IxS-QjeH.js} +4 -4
- package/dist/cdn/chunks/{TestEmailModal-DdWfAGNe.js.map → TestEmailModal-IxS-QjeH.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailPanel-CZAG1gQE.js → TestEmailPanel-B8y7JabA.js} +2 -2
- package/dist/cdn/chunks/{TestEmailPanel-CZAG1gQE.js.map → TestEmailPanel-B8y7JabA.js.map} +1 -1
- package/dist/cdn/chunks/{TitleEditor-D0_LY2Ex.js → TitleEditor-C8WDXf4P.js} +5 -4
- package/dist/cdn/chunks/TitleEditor-C8WDXf4P.js.map +1 -0
- package/dist/cdn/chunks/{TplModal-iCmoKB2k.js → TplModal-Ce0QuHyo.js} +2 -2
- package/dist/cdn/chunks/{TplModal-iCmoKB2k.js.map → TplModal-Ce0QuHyo.js.map} +1 -1
- package/dist/cdn/chunks/{extensions-C7h52yuk.js → extensions-qhcN3RMT.js} +35 -21
- package/dist/cdn/chunks/extensions-qhcN3RMT.js.map +1 -0
- package/dist/cdn/chunks/{src-CUKUsRzX.js → src-BwbnXShT.js} +2 -2
- package/dist/cdn/chunks/{src-CUKUsRzX.js.map → src-BwbnXShT.js.map} +1 -1
- package/dist/cdn/chunks/{styles-CND3ey2g.js → styles-DLAfU4Uf.js} +4 -4
- package/dist/cdn/chunks/{styles-CND3ey2g.js.map → styles-DLAfU4Uf.js.map} +1 -1
- package/dist/cdn/chunks/{useEditorCore-CKQ9gmoo.js → useEditorCore-CcgxY-Gy.js} +3 -3
- package/dist/cdn/chunks/{useEditorCore-CKQ9gmoo.js.map → useEditorCore-CcgxY-Gy.js.map} +1 -1
- package/dist/cdn/editor.js +5 -5
- package/dist/{extensions-DzKNPITR.js → extensions-Cwg9S-0f.js} +34 -20
- package/dist/{styles-BK36D1sy.js → styles-Dx__pP9O.js} +3 -3
- package/dist/templatical-editor.js +5 -5
- package/dist/{useEditorCore-idmPLUeb.js → useEditorCore-P4UkhcyS.js} +2 -2
- package/package.json +7 -7
- package/dist/cdn/chunks/TitleEditor-D0_LY2Ex.js.map +0 -1
- package/dist/cdn/chunks/extensions-C7h52yuk.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestEmailModal-DdWfAGNe.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 {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\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 * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\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: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\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 v-if=\"!previewResolution.supersedesSamples.value\"\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 previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : 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 <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\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 {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\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 * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\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: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\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 v-if=\"!previewResolution.supersedesSamples.value\"\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 previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : 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 <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\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
|
+
{"version":3,"file":"TestEmailModal-IxS-QjeH.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 {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\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 * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\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: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\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 v-if=\"!previewResolution.supersedesSamples.value\"\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 previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : 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 <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\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 {\n EDITOR_KEY,\n MERGE_TAGS_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n RESOLVE_PREVIEW_KEY,\n} from \"../keys\";\nimport { usePreviewResolution } from \"../composables/usePreviewResolution\";\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 * Its own resolution instance rather than the editor's, because this dialog has\n * a recipient and the editor's preview does not — resolving for \"whoever is\n * selected here\" is a different question from resolving for the canvas.\n * `isActive` is the dialog's own visibility.\n */\nconst resolvePreviewHook = inject(RESOLVE_PREVIEW_KEY, undefined);\nconst previewResolution = usePreviewResolution({\n resolvePreview: resolvePreviewHook ?? undefined,\n getContent: () =>\n editor?.content.value ?? { blocks: [], settings: {} as never },\n isActive: () => props.visible,\n getRecipient: () => recipient.value || undefined,\n});\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: () =>\n // Off entirely while a resolver is configured — same rule as the canvas.\n previewResolution.isConfigured\n ? false\n : (sharedSampleMode?.value ?? localSampleMode.value),\n set: (on: boolean) => {\n if (sharedSampleMode) sharedSampleMode.value = on;\n else localSampleMode.value = on;\n },\n});\n\nconst previewBlocks = computed(\n () => previewResolution.content.value.blocks ?? [],\n);\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 v-if=\"!previewResolution.supersedesSamples.value\"\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 previewResolution.supersedesSamples.value\n ? t.previewResolution.hint\n : sampleMode\n ? t.testEmail.previewHintSample\n : 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 <!-- Skeleton while a *first* resolve is outstanding, so the dialog never\n flashes the unresolved template before the resolved one. Sized like\n the frame it replaces to avoid a layout jump. A re-resolve (recipient\n change) keeps the previous result instead — `isInitialResolve`. -->\n <div\n v-if=\"previewResolution.isInitialResolve.value\"\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"preview-resolution-loading\"\n class=\"tpl:flex tpl:flex-col tpl:gap-3\"\n >\n <span class=\"tpl:sr-only\">{{ t.previewResolution.resolving }}</span>\n <div\n v-for=\"n in 3\"\n :key=\"n\"\n aria-hidden=\"true\"\n class=\"tpl-pulse tpl:h-16 tpl:rounded-md tpl:bg-[var(--tpl-bg-hover)]\"\n ></div>\n </div>\n\n <!-- Resolution failed: the unresolved template renders below, so the\n dialog degrades rather than showing an empty frame. -->\n <p\n v-else-if=\"previewResolution.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mb-2 tpl:rounded-md tpl:px-3 tpl:py-2 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg-hover)]\"\n >\n {{ t.previewResolution.failed }}\n </p>\n\n <BlockPreviewCanvas\n v-if=\"!previewResolution.isInitialResolve.value\"\n :blocks=\"previewBlocks\"\n :viewport=\"previewViewport\"\n :apply-condition-filter=\"!previewResolution.isConfigured\"\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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"}
|
|
@@ -5,7 +5,7 @@ var o = /* @__PURE__ */ a({
|
|
|
5
5
|
__name: "TestEmailPanel",
|
|
6
6
|
props: { feature: {} },
|
|
7
7
|
setup(a) {
|
|
8
|
-
let o = a, s = t(() => import("./TestEmailModal-
|
|
8
|
+
let o = a, s = t(() => import("./TestEmailModal-IxS-QjeH.js"));
|
|
9
9
|
function c(e) {
|
|
10
10
|
o.feature.send(e);
|
|
11
11
|
}
|
|
@@ -32,4 +32,4 @@ var o = /* @__PURE__ */ a({
|
|
|
32
32
|
//#endregion
|
|
33
33
|
export { o as default };
|
|
34
34
|
|
|
35
|
-
//# sourceMappingURL=TestEmailPanel-
|
|
35
|
+
//# sourceMappingURL=TestEmailPanel-B8y7JabA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestEmailPanel-
|
|
1
|
+
{"version":3,"file":"TestEmailPanel-B8y7JabA.js","names":[],"sources":["../../../src/components/TestEmailPanel.vue","../../../src/components/TestEmailPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Mount point for the test-email dialog, shared by the OSS and Cloud editors.\n *\n * The dialog is `defineAsyncComponent` behind a `v-if` on its open state, so its\n * chunk is fetched only when the user actually opens it — `defineAsyncComponent`\n * triggers its `import()` on first render, not at definition time. The editors in\n * turn lazy-load *this* wrapper and render it only when a `TestEmailProvider` is\n * configured, so a consumer without one downloads none of it.\n *\n * Never static-import `TestEmailModal` into `Editor.vue` / `CloudEditor.vue`:\n * that collapses it into the main entry for every consumer, including those who\n * never configure a sender. Guarded by `tests/cdn-chunk-granularity.test.ts`.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n feature: UseTestEmailFeatureReturn;\n}>();\n\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\n\nfunction handleSend(recipient: string): void {\n void props.feature.send(recipient);\n}\n</script>\n\n<template>\n <TestEmailModal\n v-if=\"feature.isModalOpen.value\"\n :visible=\"feature.isModalOpen.value\"\n :allowed-recipients=\"feature.allowedRecipients.value\"\n :default-recipient=\"feature.defaultRecipient.value\"\n :is-sending=\"feature.isSending.value\"\n :just-sent=\"feature.justSent.value\"\n :error=\"feature.error.value\"\n @send=\"handleSend\"\n @close=\"feature.close()\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * Mount point for the test-email dialog, shared by the OSS and Cloud editors.\n *\n * The dialog is `defineAsyncComponent` behind a `v-if` on its open state, so its\n * chunk is fetched only when the user actually opens it — `defineAsyncComponent`\n * triggers its `import()` on first render, not at definition time. The editors in\n * turn lazy-load *this* wrapper and render it only when a `TestEmailProvider` is\n * configured, so a consumer without one downloads none of it.\n *\n * Never static-import `TestEmailModal` into `Editor.vue` / `CloudEditor.vue`:\n * that collapses it into the main entry for every consumer, including those who\n * never configure a sender. Guarded by `tests/cdn-chunk-granularity.test.ts`.\n */\nimport { defineAsyncComponent } from \"vue\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\n\nconst props = defineProps<{\n feature: UseTestEmailFeatureReturn;\n}>();\n\nconst TestEmailModal = defineAsyncComponent(\n () => import(\"./TestEmailModal.vue\"),\n);\n\nfunction handleSend(recipient: string): void {\n void props.feature.send(recipient);\n}\n</script>\n\n<template>\n <TestEmailModal\n v-if=\"feature.isModalOpen.value\"\n :visible=\"feature.isModalOpen.value\"\n :allowed-recipients=\"feature.allowedRecipients.value\"\n :default-recipient=\"feature.defaultRecipient.value\"\n :is-sending=\"feature.isSending.value\"\n :just-sent=\"feature.justSent.value\"\n :error=\"feature.error.value\"\n @send=\"handleSend\"\n @close=\"feature.close()\"\n />\n</template>\n"],"mappings":";;;;;;;EAiBA,IAAM,IAAQ,GAIR,IAAiB,QACf,OAAO,+BACf;EAEA,SAAS,EAAW,GAAyB;GAC3C,EAAW,QAAQ,KAAK,CAAS;EACnC;mBAKU,EAAA,QAAQ,YAAY,SAD5B,EAAA,GAAA,EAUE,EAAA,CAAA,GAAA;;GARC,SAAS,EAAA,QAAQ,YAAY;GAC7B,sBAAoB,EAAA,QAAQ,kBAAkB;GAC9C,qBAAmB,EAAA,QAAQ,iBAAiB;GAC5C,cAAY,EAAA,QAAQ,UAAU;GAC9B,aAAW,EAAA,QAAQ,SAAS;GAC5B,OAAO,EAAA,QAAQ,MAAM;GACrB,QAAM;GACN,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAQ,MAAK"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { K as e, M as t, T as n, ct as r, d as i, f as a, g as o, h as s, it as c, lt as l, m as u, p as d, st as f, u as p, v as m, x as h, y as g } from "./draggable-DCs9qL-b.js";
|
|
2
|
-
import { S as ee, x as te } from "./useEditorCore-
|
|
2
|
+
import { S as ee, x as te } from "./useEditorCore-CcgxY-Gy.js";
|
|
3
3
|
import { O as ne, T as _, U as v, V as y } from "./keys-Cmc2Yl3D.js";
|
|
4
4
|
import { t as b } from "./useI18n-UMs0F71x.js";
|
|
5
5
|
import { n as x } from "./usePopoverPosition-cf8Y-ynK.js";
|
|
@@ -29,11 +29,11 @@ var w = { class: "tpl-text-editor-wrapper tpl:relative" }, T = ["data-tpl-theme"
|
|
|
29
29
|
onDone: () => I("done"),
|
|
30
30
|
editorName: "TitleEditor",
|
|
31
31
|
async loadExtensions({ mergeTags: e, syntax: t, triggerChar: n, autocompleteEnabled: r, suggestionEmptyText: i }) {
|
|
32
|
-
let [{ Editor: a, EditorContent: o }, { default: s }, { default: c }, { MergeTagNode: l, MergeTagSuggestion: u, LogicMergeTagNode: d }] = await Promise.all([
|
|
32
|
+
let [{ Editor: a, EditorContent: o }, { default: s }, { default: c }, { MergeTagNode: l, MergeTagSuggestion: u, LogicMergeTagNode: d, LineBoundaryKeys: f }] = await Promise.all([
|
|
33
33
|
import("./tiptap-C44S1Cmh.js").then((e) => e.r),
|
|
34
34
|
import("./tiptap-C44S1Cmh.js").then((e) => e.i),
|
|
35
35
|
import("./tiptap-C44S1Cmh.js").then((e) => e.l),
|
|
36
|
-
import("./extensions-
|
|
36
|
+
import("./extensions-qhcN3RMT.js")
|
|
37
37
|
]);
|
|
38
38
|
return {
|
|
39
39
|
TiptapEditor: a,
|
|
@@ -62,6 +62,7 @@ var w = { class: "tpl-text-editor-wrapper tpl:relative" }, T = ["data-tpl-theme"
|
|
|
62
62
|
syntax: t
|
|
63
63
|
}),
|
|
64
64
|
d.configure({ syntax: t }),
|
|
65
|
+
f,
|
|
65
66
|
...r && n && e.length > 0 ? [u.configure({
|
|
66
67
|
mergeTags: e,
|
|
67
68
|
char: n,
|
|
@@ -195,4 +196,4 @@ var w = { class: "tpl-text-editor-wrapper tpl:relative" }, T = ["data-tpl-theme"
|
|
|
195
196
|
//#endregion
|
|
196
197
|
export { P as default };
|
|
197
198
|
|
|
198
|
-
//# sourceMappingURL=TitleEditor-
|
|
199
|
+
//# sourceMappingURL=TitleEditor-C8WDXf4P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TitleEditor-C8WDXf4P.js","names":[],"sources":["../../../src/components/blocks/TitleEditor.vue","../../../src/components/blocks/TitleEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables\";\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { TitleBlock as TitleBlockType } from \"@templatical/types\";\nimport {\n Bold,\n Braces,\n Italic,\n Link,\n LoaderCircle,\n ScanLine,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n} from \"../../keys\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst popoverRoot = usePopoverRoot();\n// Hide the floating toolbar while either built-in picker modal is open — see\n// ParagraphToolbar.vue for the same rationale.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"TitleEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { MergeTagNode, MergeTagSuggestion, LogicMergeTagNode, LineBoundaryKeys },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n strike: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n }),\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.titleEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-lg\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Bold -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('bold'),\n }\"\n :aria-label=\"t.titleEditor.bold\"\n :title=\"t.titleEditor.bold\"\n @click=\"editor?.chain().focus().toggleBold().run()\"\n >\n <Bold :size=\"16\" :stroke-width=\"2.5\" />\n </button>\n <!-- Italic -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('italic'),\n }\"\n :aria-label=\"t.titleEditor.italic\"\n :title=\"t.titleEditor.italic\"\n @click=\"editor?.chain().focus().toggleItalic().run()\"\n >\n <Italic :size=\"16\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n <!-- Link -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('link'),\n }\"\n :aria-label=\"t.titleEditor.addLink\"\n :title=\"t.titleEditor.addLink\"\n @click=\"openLinkDialog\"\n >\n <Link :size=\"16\" :stroke-width=\"2\" />\n </button>\n <!-- Add Merge Tag -->\n <span\n v-if=\"canRequestMergeTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canRequestMergeTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-150 tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"handleAddMergeTag\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n <!-- Insert Logic -->\n <span\n v-if=\"canInsertLogicTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canInsertLogicTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-150 tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"handleAddLogicTag\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables\";\nimport { useRichTextEditor } from \"../../composables/useRichTextEditor\";\nimport { usePopoverRoot } from \"../../composables/usePopoverRoot\";\nimport type { TitleBlock as TitleBlockType } from \"@templatical/types\";\nimport {\n Bold,\n Braces,\n Italic,\n Link,\n LoaderCircle,\n ScanLine,\n} from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport {\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n MERGE_TAG_PICKER_KEY,\n LOGIC_TAG_PICKER_KEY,\n} from \"../../keys\";\nimport RichTextLinkDialog from \"./RichTextLinkDialog.vue\";\nimport RichTextEditorContent from \"./RichTextEditorContent.vue\";\nimport { withLinkColor } from \"../../utils/linkColorExtension\";\n\nconst props = defineProps<{\n block: TitleBlockType;\n toolbarPosition: { top: number; left: number };\n}>();\n\nconst emit = defineEmits<{\n (e: \"done\"): void;\n}>();\n\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\nconst popoverRoot = usePopoverRoot();\n// Hide the floating toolbar while either built-in picker modal is open — see\n// ParagraphToolbar.vue for the same rationale.\nconst picker = inject(MERGE_TAG_PICKER_KEY, null);\nconst logicPicker = inject(LOGIC_TAG_PICKER_KEY, null);\nconst pickerIsOpen = computed(\n () => (picker?.isOpen.value ?? false) || (logicPicker?.isOpen.value ?? false),\n);\n\nconst { t } = useI18n();\n\nconst {\n editor,\n EditorContent,\n isLoading,\n initError,\n retry,\n showLinkDialog,\n linkUrl,\n linkColor,\n linkDialogRef,\n canRequestMergeTag,\n canInsertLogicTag,\n openLinkDialog,\n insertLink,\n removeLink,\n closeLinkDialog,\n handleLinkKeydown,\n handleAddMergeTag,\n handleAddLogicTag,\n} = useRichTextEditor({\n blockId: () => props.block.id,\n blockContent: () => props.block.content,\n onDone: () => emit(\"done\"),\n editorName: \"TitleEditor\",\n async loadExtensions({\n mergeTags,\n syntax,\n triggerChar,\n autocompleteEnabled,\n suggestionEmptyText,\n }) {\n const [\n { Editor: TiptapEditor, EditorContent: EC },\n { default: StarterKit },\n { default: LinkExt },\n { MergeTagNode, MergeTagSuggestion, LogicMergeTagNode, LineBoundaryKeys },\n ] = await Promise.all([\n import(\"@tiptap/vue-3\"),\n import(\"@tiptap/starter-kit\"),\n import(\"@tiptap/extension-link\"),\n import(\"../../extensions\"),\n ]);\n\n return {\n TiptapEditor,\n EC,\n extensions: [\n StarterKit.configure({\n heading: false,\n codeBlock: false,\n blockquote: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n strike: false,\n // StarterKit bundles @tiptap/extension-link (openOnClick: true).\n // Disable it so the LinkExt below (openOnClick: false) is the only\n // link extension — otherwise clicking a link while editing opens it.\n link: false,\n }),\n withLinkColor(LinkExt).configure({\n openOnClick: false,\n HTMLAttributes: {\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n }),\n MergeTagNode.configure({ mergeTags, syntax }),\n LogicMergeTagNode.configure({ syntax }),\n LineBoundaryKeys,\n ...(autocompleteEnabled && triggerChar && mergeTags.length > 0\n ? [\n MergeTagSuggestion.configure({\n mergeTags,\n char: triggerChar,\n emptyText: suggestionEmptyText,\n popoverRoot,\n }),\n ]\n : []),\n ],\n };\n },\n});\n</script>\n\n<template>\n <div class=\"tpl-text-editor-wrapper tpl:relative\">\n <Teleport v-if=\"popoverRoot && !pickerIsOpen\" :to=\"popoverRoot\">\n <div\n :data-tpl-theme=\"tplUiTheme\"\n role=\"toolbar\"\n :aria-label=\"t.titleEditor.toolbar\"\n class=\"tpl tpl-text-toolbar tpl:absolute tpl:z-popover tpl:flex tpl:items-center tpl:gap-1 tpl:rounded-lg tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:shadow-lg\"\n :style=\"{\n ...themeStyles,\n top: `${toolbarPosition.top}px`,\n left: `${toolbarPosition.left}px`,\n transform: 'translateY(-100%)',\n }\"\n >\n <template v-if=\"!isLoading && editor\">\n <!-- Bold -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('bold'),\n }\"\n :aria-label=\"t.titleEditor.bold\"\n :title=\"t.titleEditor.bold\"\n @click=\"editor?.chain().focus().toggleBold().run()\"\n >\n <Bold :size=\"16\" :stroke-width=\"2.5\" />\n </button>\n <!-- Italic -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('italic'),\n }\"\n :aria-label=\"t.titleEditor.italic\"\n :title=\"t.titleEditor.italic\"\n @click=\"editor?.chain().focus().toggleItalic().run()\"\n >\n <Italic :size=\"16\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n aria-hidden=\"true\"\n ></span>\n <!-- Link -->\n <button\n type=\"button\"\n class=\"tpl-text-toolbar-btn\"\n :class=\"{\n 'tpl-text-toolbar-btn--active': editor?.isActive('link'),\n }\"\n :aria-label=\"t.titleEditor.addLink\"\n :title=\"t.titleEditor.addLink\"\n @click=\"openLinkDialog\"\n >\n <Link :size=\"16\" :stroke-width=\"2\" />\n </button>\n <!-- Add Merge Tag -->\n <span\n v-if=\"canRequestMergeTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canRequestMergeTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-150 tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n @click=\"handleAddMergeTag\"\n >\n <ScanLine :size=\"16\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n <!-- Insert Logic -->\n <span\n v-if=\"canInsertLogicTag\"\n class=\"tpl:mx-1.5 tpl:h-6 tpl:w-px tpl:bg-[var(--tpl-border)]\"\n ></span>\n <button\n v-if=\"canInsertLogicTag\"\n type=\"button\"\n class=\"tpl:flex tpl:h-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-none tpl:bg-transparent tpl:px-2.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-150 tpl:hover:bg-[var(--tpl-bg-active)]\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n data-testid=\"insert-logic-button\"\n @click=\"handleAddLogicTag\"\n >\n <Braces :size=\"16\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n </template>\n <template v-else>\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:px-2 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n <LoaderCircle class=\"tpl-spinner\" :size=\"14\" :stroke-width=\"2\" />\n {{ t.errors.editorLoading }}\n </div>\n </template>\n </div>\n </Teleport>\n\n <RichTextEditorContent\n :editor=\"editor\"\n :editor-content=\"EditorContent\"\n :is-loading=\"isLoading\"\n :init-error=\"initError\"\n @retry=\"retry\"\n />\n\n <RichTextLinkDialog\n :visible=\"showLinkDialog\"\n :is-editing-link=\"editor?.isActive('link') ?? false\"\n v-model:dialog-ref=\"linkDialogRef\"\n v-model:link-url=\"linkUrl\"\n v-model:link-color=\"linkColor\"\n @close=\"closeLinkDialog\"\n @insert=\"insertLink\"\n @remove=\"removeLink\"\n @keydown=\"handleLinkKeydown\"\n />\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { F as e, M as t, T as n, V as r, Z as i, ct as a, d as o, f as s, g as c, h as l, it as u, l as d, m as f, n as p, p as m, x as h, y as g } from "./draggable-DCs9qL-b.js";
|
|
2
|
-
import { D as _ } from "./useEditorCore-
|
|
2
|
+
import { D as _ } from "./useEditorCore-CcgxY-Gy.js";
|
|
3
3
|
import { U as v, V as y } from "./keys-Cmc2Yl3D.js";
|
|
4
4
|
import { n as b } from "./usePopoverPosition-cf8Y-ynK.js";
|
|
5
5
|
//#region src/components/TplModal.vue?vue&type=script&setup=true&lang.ts
|
|
@@ -47,4 +47,4 @@ var x = ["data-tpl-theme"], S = /* @__PURE__ */ h({
|
|
|
47
47
|
//#endregion
|
|
48
48
|
export { S as t };
|
|
49
49
|
|
|
50
|
-
//# sourceMappingURL=TplModal-
|
|
50
|
+
//# sourceMappingURL=TplModal-Ce0QuHyo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TplModal-
|
|
1
|
+
{"version":3,"file":"TplModal-Ce0QuHyo.js","names":[],"sources":["../../../src/components/TplModal.vue","../../../src/components/TplModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useFocusTrap } from \"../composables\";\nimport { usePopoverRoot } from \"../composables/usePopoverRoot\";\nimport { THEME_STYLES_KEY, UI_THEME_KEY } from \"../keys\";\nimport { computed, inject, ref } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"keydown\", event: KeyboardEvent): void;\n}>();\n\nconst dialogRef = ref<HTMLElement | null>(null);\nconst isVisible = computed(() => props.visible);\nuseFocusTrap(dialogRef, isVisible);\n\nconst tplUiTheme = inject(UI_THEME_KEY);\n\n// The backdrop below carries the bare `tpl` class, which re-declares every\n// `--tpl-*` token from the base `.tpl` rule. A custom property declared on an\n// element beats one inherited from an ancestor, so that re-declaration shadows\n// the consumer's `theme` config — which lives as inline styles on the editor\n// root — for this whole teleported subtree. Re-applying `themeStyles` here is\n// what restores it, and doing it on the backdrop covers every dialog rendered\n// through this wrapper rather than each one remembering to. `data-tpl-theme`\n// does the same job for the dark token block. See the invariant in\n// `tests/theme-token-scope.test.ts` (issue #487).\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst popoverRoot = usePopoverRoot();\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n emit(\"close\");\n }\n emit(\"keydown\", event);\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot\" :to=\"popoverRoot\">\n <Transition\n enter-active-class=\"tpl:transition tpl:duration-150\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition tpl:duration-100\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <div\n v-if=\"visible\"\n :data-tpl-theme=\"tplUiTheme\"\n class=\"tpl tpl:fixed tpl:inset-0 tpl:z-modal tpl:flex tpl:items-center tpl:justify-center\"\n style=\"\n background-color: var(--tpl-overlay);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n :style=\"themeStyles\"\n @click.self=\"emit('close')\"\n @keydown=\"handleKeydown\"\n >\n <div ref=\"dialogRef\">\n <slot />\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { useFocusTrap } from \"../composables\";\nimport { usePopoverRoot } from \"../composables/usePopoverRoot\";\nimport { THEME_STYLES_KEY, UI_THEME_KEY } from \"../keys\";\nimport { computed, inject, ref } from \"vue\";\n\nconst props = defineProps<{\n visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"close\"): void;\n (e: \"keydown\", event: KeyboardEvent): void;\n}>();\n\nconst dialogRef = ref<HTMLElement | null>(null);\nconst isVisible = computed(() => props.visible);\nuseFocusTrap(dialogRef, isVisible);\n\nconst tplUiTheme = inject(UI_THEME_KEY);\n\n// The backdrop below carries the bare `tpl` class, which re-declares every\n// `--tpl-*` token from the base `.tpl` rule. A custom property declared on an\n// element beats one inherited from an ancestor, so that re-declaration shadows\n// the consumer's `theme` config — which lives as inline styles on the editor\n// root — for this whole teleported subtree. Re-applying `themeStyles` here is\n// what restores it, and doing it on the backdrop covers every dialog rendered\n// through this wrapper rather than each one remembering to. `data-tpl-theme`\n// does the same job for the dark token block. See the invariant in\n// `tests/theme-token-scope.test.ts` (issue #487).\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst popoverRoot = usePopoverRoot();\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n emit(\"close\");\n }\n emit(\"keydown\", event);\n}\n</script>\n\n<template>\n <Teleport v-if=\"popoverRoot\" :to=\"popoverRoot\">\n <Transition\n enter-active-class=\"tpl:transition tpl:duration-150\"\n enter-from-class=\"tpl:opacity-0\"\n enter-to-class=\"tpl:opacity-100\"\n leave-active-class=\"tpl:transition tpl:duration-100\"\n leave-from-class=\"tpl:opacity-100\"\n leave-to-class=\"tpl:opacity-0\"\n >\n <div\n v-if=\"visible\"\n :data-tpl-theme=\"tplUiTheme\"\n class=\"tpl tpl:fixed tpl:inset-0 tpl:z-modal tpl:flex tpl:items-center tpl:justify-center\"\n style=\"\n background-color: var(--tpl-overlay);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n :style=\"themeStyles\"\n @click.self=\"emit('close')\"\n @keydown=\"handleKeydown\"\n >\n <div ref=\"dialogRef\">\n <slot />\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n"],"mappings":";;;;;;;;;;EAMA,IAAM,IAAQ,GAIR,IAAO,GAKP,IAAY,EAAwB,IAAI,GACxC,IAAY,QAAe,EAAM,OAAO;EAC9C,EAAa,GAAW,CAAS;EAEjC,IAAM,IAAa,EAAO,CAAY,GAWhC,IAAc,EAAO,GAAkB,IAAI,GAC3C,IAAc,EAAe;EAEnC,SAAS,EAAc,GAA4B;GAIjD,AAHI,EAAM,QAAQ,YAChB,EAAK,OAAO,GAEd,EAAK,WAAW,CAAK;EACvB;mBAIkB,EAAA,CAAA,KAAhB,EAAA,GAAA,EA2BW,GAAA;;GA3BmB,IAAI,EAAA,CAAA;EAChC,GAAA,CAAA,EAyBa,GAAA;GAxBX,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;GACf,sBAAmB;GACnB,oBAAiB;GACjB,kBAAe;;GAEf,SAAA,QAgBM,CAfE,EAAA,WADR,EAAA,GAAA,EAgBM,OAAA;;IAdH,kBAAgB,EAAA,CAAA;IACjB,OAAM;IACN,OAIC,EAAA,CAJD;KAAA,oBAAA;KAAA,mBAAA;KAAA,2BAAA;IAAA,GAKQ,EAAA,CAAA,CAAW,CAAA;IAClB,SAAK,AAAA,EAAA,OAAA,GAAA,MAAO,EAAI,OAAA,GAAA,CAAA,MAAA,CAAA;IAChB,WAAS;GAEV,GAAA,CAAA,EAEM,OAAA;IAFG,SAAA;IAAJ,KAAI;GACP,GAAA,CAAA,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA,GAAA,IAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,CAAA"}
|
|
@@ -48,6 +48,20 @@ var P = E.create({
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
}), I = E.create({
|
|
51
|
+
name: "lineBoundaryKeys",
|
|
52
|
+
addKeyboardShortcuts() {
|
|
53
|
+
let e = (e) => ({ editor: t }) => {
|
|
54
|
+
let n = t.view;
|
|
55
|
+
if (!n || n.composing) return !1;
|
|
56
|
+
let r = typeof n.root?.getSelection == "function" ? n.root.getSelection() : n.dom?.ownerDocument?.getSelection?.() ?? null;
|
|
57
|
+
return !r || typeof r.modify != "function" || !r.anchorNode || !n.dom.contains(r.anchorNode) ? !1 : (r.modify("move", e, "lineboundary"), !0);
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
End: e("forward"),
|
|
61
|
+
Home: e("backward")
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}), L = E.create({
|
|
51
65
|
name: "lineHeight",
|
|
52
66
|
addOptions() {
|
|
53
67
|
return {
|
|
@@ -71,11 +85,11 @@ var P = E.create({
|
|
|
71
85
|
unsetLineHeight: () => ({ commands: e }) => this.options.types.every((t) => e.resetAttributes(t, "lineHeight"))
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
|
-
}),
|
|
88
|
+
}), R = [
|
|
75
89
|
"aria-label",
|
|
76
90
|
"data-tooltip",
|
|
77
91
|
"onKeydown"
|
|
78
|
-
],
|
|
92
|
+
], z = ["aria-label", "onKeydown"], B = ["aria-label"], V = /* @__PURE__ */ _({
|
|
79
93
|
__name: "LogicMergeTagNodeView",
|
|
80
94
|
props: {
|
|
81
95
|
node: {},
|
|
@@ -131,14 +145,14 @@ var P = E.create({
|
|
|
131
145
|
"data-tooltip": _.node.attrs.value,
|
|
132
146
|
onClick: d(M, ["stop"]),
|
|
133
147
|
onKeydown: [a(d(M, ["stop"]), ["enter"]), a(d(M, ["prevent", "stop"]), ["space"])]
|
|
134
|
-
}, f(E.value), 41,
|
|
148
|
+
}, f(E.value), 41, R)) : (t(), c("span", {
|
|
135
149
|
key: 2,
|
|
136
150
|
role: "button",
|
|
137
151
|
tabindex: "0",
|
|
138
152
|
"aria-label": u(w).mergeTag.editValue,
|
|
139
153
|
onClick: d(M, ["stop"]),
|
|
140
154
|
onKeydown: [a(d(M, ["stop"]), ["enter"]), a(d(M, ["prevent", "stop"]), ["space"])]
|
|
141
|
-
}, f(_.node.attrs.value), 41,
|
|
155
|
+
}, f(_.node.attrs.value), 41, z)), T.value ? (t(), c("button", {
|
|
142
156
|
key: 3,
|
|
143
157
|
type: "button",
|
|
144
158
|
"aria-label": u(w).mergeTag.deleteMergeTag,
|
|
@@ -163,25 +177,25 @@ var P = E.create({
|
|
|
163
177
|
y1: "6",
|
|
164
178
|
x2: "18",
|
|
165
179
|
y2: "18"
|
|
166
|
-
})], -1)]], 8,
|
|
180
|
+
})], -1)]], 8, B)) : l("", !0)]),
|
|
167
181
|
_: 1
|
|
168
182
|
}, 8, ["class", "style"]));
|
|
169
183
|
}
|
|
170
184
|
});
|
|
171
185
|
//#endregion
|
|
172
186
|
//#region src/extensions/isNodeSelected.ts
|
|
173
|
-
function
|
|
187
|
+
function H(e, t) {
|
|
174
188
|
let { $from: n, $to: r } = e.state.selection;
|
|
175
189
|
return n.pos === r.pos ? n.pos > 0 && n.nodeBefore?.type.name === t || n.nodeAfter?.type.name === t : !1;
|
|
176
190
|
}
|
|
177
191
|
//#endregion
|
|
178
192
|
//#region src/extensions/renderVueNodeView.ts
|
|
179
|
-
function
|
|
193
|
+
function U(e) {
|
|
180
194
|
return D(e);
|
|
181
195
|
}
|
|
182
196
|
//#endregion
|
|
183
197
|
//#region src/extensions/LogicMergeTagNode.ts
|
|
184
|
-
var
|
|
198
|
+
var W = w.create({
|
|
185
199
|
name: "logicMergeTagNode",
|
|
186
200
|
group: "inline",
|
|
187
201
|
inline: !0,
|
|
@@ -223,12 +237,12 @@ var U = w.create({
|
|
|
223
237
|
];
|
|
224
238
|
},
|
|
225
239
|
addNodeView() {
|
|
226
|
-
return
|
|
240
|
+
return U(V);
|
|
227
241
|
},
|
|
228
242
|
addKeyboardShortcuts() {
|
|
229
243
|
return {
|
|
230
|
-
Backspace: () =>
|
|
231
|
-
Delete: () =>
|
|
244
|
+
Backspace: () => H(this.editor, this.name),
|
|
245
|
+
Delete: () => H(this.editor, this.name)
|
|
232
246
|
};
|
|
233
247
|
},
|
|
234
248
|
addInputRules() {
|
|
@@ -261,11 +275,11 @@ var U = w.create({
|
|
|
261
275
|
}
|
|
262
276
|
})];
|
|
263
277
|
}
|
|
264
|
-
}),
|
|
278
|
+
}), G = [
|
|
265
279
|
"aria-label",
|
|
266
280
|
"data-tooltip",
|
|
267
281
|
"onKeydown"
|
|
268
|
-
],
|
|
282
|
+
], K = ["aria-label"], q = /* @__PURE__ */ _({
|
|
269
283
|
__name: "MergeTagNodeView",
|
|
270
284
|
props: {
|
|
271
285
|
node: {},
|
|
@@ -313,7 +327,7 @@ var U = w.create({
|
|
|
313
327
|
"data-tooltip": o.node.attrs.value,
|
|
314
328
|
onClick: d(w, ["stop"]),
|
|
315
329
|
onKeydown: [a(d(w, ["stop"]), ["enter"]), a(d(w, ["prevent", "stop"]), ["space"])]
|
|
316
|
-
}, f(v.value), 41,
|
|
330
|
+
}, f(v.value), 41, G)), h("button", {
|
|
317
331
|
type: "button",
|
|
318
332
|
"aria-label": u(_).mergeTag.deleteMergeTag,
|
|
319
333
|
class: "tpl-merge-tag-delete tpl:flex tpl:size-5 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:opacity-60 tpl:transition-all hover:tpl:opacity-100 tpl:text-[var(--tpl-primary)]",
|
|
@@ -337,11 +351,11 @@ var U = w.create({
|
|
|
337
351
|
y1: "6",
|
|
338
352
|
x2: "18",
|
|
339
353
|
y2: "18"
|
|
340
|
-
})], -1)]], 8,
|
|
354
|
+
})], -1)]], 8, K)]),
|
|
341
355
|
_: 1
|
|
342
356
|
}));
|
|
343
357
|
}
|
|
344
|
-
}),
|
|
358
|
+
}), J = w.create({
|
|
345
359
|
name: "mergeTagNode",
|
|
346
360
|
group: "inline",
|
|
347
361
|
inline: !0,
|
|
@@ -381,7 +395,7 @@ var U = w.create({
|
|
|
381
395
|
];
|
|
382
396
|
},
|
|
383
397
|
addNodeView() {
|
|
384
|
-
return
|
|
398
|
+
return U(q);
|
|
385
399
|
},
|
|
386
400
|
addCommands() {
|
|
387
401
|
return { insertMergeTag: (e) => ({ commands: t }) => t.insertContent({
|
|
@@ -394,8 +408,8 @@ var U = w.create({
|
|
|
394
408
|
},
|
|
395
409
|
addKeyboardShortcuts() {
|
|
396
410
|
return {
|
|
397
|
-
Backspace: () =>
|
|
398
|
-
Delete: () =>
|
|
411
|
+
Backspace: () => H(this.editor, this.name),
|
|
412
|
+
Delete: () => H(this.editor, this.name)
|
|
399
413
|
};
|
|
400
414
|
},
|
|
401
415
|
addInputRules() {
|
|
@@ -426,6 +440,6 @@ var U = w.create({
|
|
|
426
440
|
}
|
|
427
441
|
});
|
|
428
442
|
//#endregion
|
|
429
|
-
export { P as FontSize, F as LetterSpacing, I as LineHeight,
|
|
443
|
+
export { P as FontSize, F as LetterSpacing, I as LineBoundaryKeys, L as LineHeight, W as LogicMergeTagNode, J as MergeTagNode, N as MergeTagSuggestion, M as filterMergeTags, j as handleSuggestionKeyDown };
|
|
430
444
|
|
|
431
|
-
//# sourceMappingURL=extensions-
|
|
445
|
+
//# sourceMappingURL=extensions-qhcN3RMT.js.map
|