@templatical/editor 0.29.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiChatSidebar-DsYJwCLT.js → AiChatSidebar-DYDtzTDd.js} +8 -8
- package/dist/{AiFeatureMenu-CehKC-dh.js → AiFeatureMenu-BJ882XCi.js} +4 -4
- package/dist/{BlockIssueBadge-Dhk7Hv4m.js → BlockIssueBadge-D6y8vQpL.js} +2 -2
- package/dist/{BlockPreviewCanvas-BQZ7aTPr.js → BlockPreviewCanvas-DQNxLy7u.js} +1 -1
- package/dist/{CloudHeaderExtras-CaRdlYpY.js → CloudHeaderExtras-CqG3RaKw.js} +2 -2
- package/dist/{CloudPanels-CYqHk1Az.js → CloudPanels-B6xrk7kN.js} +3 -3
- package/dist/{CollaboratorBar-BZi12Mut.js → CollaboratorBar-by16gXQH.js} +2 -2
- package/dist/{ColorPicker-CXPSbnRD.js → ColorPicker-E5gqlCZE.js} +1 -1
- package/dist/{CommentsPanel-DnWdgQHg.js → CommentsPanel-B_kzvTHR.js} +1 -1
- package/dist/{CommentsSidebar-ZOqmuo4D.js → CommentsSidebar-gfzUX7cd.js} +10 -10
- package/dist/{CountdownToolbar-ByQVLqFF.js → CountdownToolbar-DU8UHx9A.js} +17 -17
- package/dist/{DesignReferenceSidebar-B9anB-Q0.js → DesignReferenceSidebar-DgndMB4H.js} +6 -6
- package/dist/{NumberWithSuffix-C-k0BP1m.js → FieldRow-0-UbQR4a.js} +23 -16
- package/dist/{IssuesPanel-CDk7lUna.js → IssuesPanel-DmJTb_SZ.js} +5 -5
- package/dist/{MergeTagModeToggle-sBEGugix.js → MergeTagModeToggle-CZnJ2Gz_.js} +3 -3
- package/dist/{MergeTagSuggestion-DPfXtDzP.js → MergeTagSuggestion-DyUcY_9e.js} +2 -2
- package/dist/{ParagraphEditor-gbp5gbFv.js → ParagraphEditor-CnB4kXle.js} +19 -19
- package/dist/{RestoreVersionDialog-DIHpGfFr.js → RestoreVersionDialog-BpOphPiE.js} +3 -3
- package/dist/{RichTextEditorContent-DZ6GiAUi.js → RichTextEditorContent-ChPNgHQ_.js} +3 -3
- package/dist/{SaveBlockDialog-QSj7tf1p.js → SaveBlockDialog-CKw4Va6j.js} +6 -6
- package/dist/{SavedBlocksBrowserModal-Bz2xaBzJ.js → SavedBlocksBrowserModal-SST-7EMK.js} +7 -7
- package/dist/{SavedBlocksPanels-RNgOnK9j.js → SavedBlocksPanels-ChV8bOgM.js} +1 -1
- package/dist/{SavedBlocksPickBar-CRmd3sD9.js → SavedBlocksPickBar-BbEeD5RB.js} +2 -2
- package/dist/{TemplateScoringPanel-CDwlSFEe.js → TemplateScoringPanel-De8BZpRU.js} +10 -10
- package/dist/{TestEmailModal-DclmELUc.js → TestEmailModal-4d2pXeHU.js} +7 -7
- package/dist/{TestEmailPanel-4i4n9Kup.js → TestEmailPanel-DEgCesNP.js} +1 -1
- package/dist/{TitleEditor-DMAeSWx3.js → TitleEditor-89pmKLVY.js} +8 -8
- package/dist/{TplModal-CjuKT36J.js → TplModal-DswccJRJ.js} +1 -1
- package/dist/{VersionHistoryMenu-B6zAcmAC.js → VersionHistoryMenu-DBxSJR1k.js} +4 -4
- package/dist/{VersionHistoryPanels-MqgexkhW.js → VersionHistoryPanels-CFLHDH9P.js} +1 -1
- package/dist/{VersionPreviewBanner-CQlSzQ2B.js → VersionPreviewBanner-DhG5RcRt.js} +1 -1
- package/dist/{blockTypeIcons-ejNais7n.js → blockTypeIcons-dpKs5GQc.js} +2 -2
- package/dist/bundle-stats.json +6 -6
- package/dist/{ca-DY0AYOLG.js → ca-DZSrLNXK.js} +2 -0
- package/dist/cdn/chunks/{AiChatSidebar-BE97qqqb.js → AiChatSidebar-DmYDRmvX.js} +6 -6
- package/dist/cdn/chunks/{AiChatSidebar-BE97qqqb.js.map → AiChatSidebar-DmYDRmvX.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-CNP54HPc.js → AiFeatureMenu-C0Rt08qb.js} +3 -3
- package/dist/cdn/chunks/{AiFeatureMenu-CNP54HPc.js.map → AiFeatureMenu-C0Rt08qb.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-Bry71Wjz.js → BlockIssueBadge-BBog2CMy.js} +2 -2
- package/dist/cdn/chunks/{BlockIssueBadge-Bry71Wjz.js.map → BlockIssueBadge-BBog2CMy.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-CUEZIwJ4.js → BlockPreviewCanvas-DNBjBKBI.js} +2 -2
- package/dist/cdn/chunks/{BlockPreviewCanvas-CUEZIwJ4.js.map → BlockPreviewCanvas-DNBjBKBI.js.map} +1 -1
- package/dist/cdn/chunks/{CloudHeaderExtras-Bz--_dKu.js → CloudHeaderExtras-Ditfv7y9.js} +3 -3
- package/dist/cdn/chunks/{CloudHeaderExtras-Bz--_dKu.js.map → CloudHeaderExtras-Ditfv7y9.js.map} +1 -1
- package/dist/cdn/chunks/{CloudPanels-BXcdYgy0.js → CloudPanels-C4U5bJO6.js} +4 -4
- package/dist/cdn/chunks/{CloudPanels-BXcdYgy0.js.map → CloudPanels-C4U5bJO6.js.map} +1 -1
- package/dist/cdn/chunks/{CollaboratorBar-CvvoIrKS.js → CollaboratorBar-B79JR9NH.js} +3 -3
- package/dist/cdn/chunks/{CollaboratorBar-CvvoIrKS.js.map → CollaboratorBar-B79JR9NH.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-Ww4xHHMI.js → ColorPicker-C-ZiwUV3.js} +2 -2
- package/dist/cdn/chunks/{ColorPicker-Ww4xHHMI.js.map → ColorPicker-C-ZiwUV3.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsPanel-DjId-XH-.js → CommentsPanel-D1Ke8PbO.js} +2 -2
- package/dist/cdn/chunks/{CommentsPanel-DjId-XH-.js.map → CommentsPanel-D1Ke8PbO.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-DXXGWSr4.js → CommentsSidebar-B-UyhIZN.js} +2 -2
- package/dist/cdn/chunks/{CommentsSidebar-DXXGWSr4.js.map → CommentsSidebar-B-UyhIZN.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-BD90NpY1.js → CountdownToolbar-DyOHOu44.js} +18 -18
- package/dist/cdn/chunks/{CountdownToolbar-BD90NpY1.js.map → CountdownToolbar-DyOHOu44.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-DY-laxO5.js → DesignReferenceSidebar-BCCX1FtP.js} +3 -3
- package/dist/cdn/chunks/{DesignReferenceSidebar-DY-laxO5.js.map → DesignReferenceSidebar-BCCX1FtP.js.map} +1 -1
- package/dist/cdn/chunks/{NumberWithSuffix-DOG0zHvY.js → FieldRow-CbcpsxZ4.js} +24 -17
- package/dist/cdn/chunks/FieldRow-CbcpsxZ4.js.map +1 -0
- package/dist/cdn/chunks/{IssuesPanel-iC2hgrXx.js → IssuesPanel-3vD3ImHF.js} +2 -2
- package/dist/cdn/chunks/{IssuesPanel-iC2hgrXx.js.map → IssuesPanel-3vD3ImHF.js.map} +1 -1
- package/dist/cdn/chunks/{MergeTagModeToggle-BRW3qErZ.js → MergeTagModeToggle-BU0SDRow.js} +3 -3
- package/dist/cdn/chunks/{MergeTagModeToggle-BRW3qErZ.js.map → MergeTagModeToggle-BU0SDRow.js.map} +1 -1
- package/dist/cdn/chunks/{MergeTagSuggestion-VP_nC-8W.js → MergeTagSuggestion-B9vL0VaG.js} +2 -2
- package/dist/cdn/chunks/{MergeTagSuggestion-VP_nC-8W.js.map → MergeTagSuggestion-B9vL0VaG.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-CPVnO48U.js → ParagraphEditor-CSwIuvoF.js} +22 -22
- package/dist/cdn/chunks/{ParagraphEditor-CPVnO48U.js.map → ParagraphEditor-CSwIuvoF.js.map} +1 -1
- package/dist/cdn/chunks/{RestoreVersionDialog--Blx-VKu.js → RestoreVersionDialog-Bvg2WvJN.js} +3 -3
- package/dist/cdn/chunks/{RestoreVersionDialog--Blx-VKu.js.map → RestoreVersionDialog-Bvg2WvJN.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-DpvOKKH-.js → RichTextEditorContent-DdkmEEpK.js} +3 -3
- package/dist/cdn/chunks/{RichTextEditorContent-DpvOKKH-.js.map → RichTextEditorContent-DdkmEEpK.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-DXQ2g_38.js → SaveBlockDialog-D-QO3LuR.js} +5 -5
- package/dist/cdn/chunks/{SaveBlockDialog-DXQ2g_38.js.map → SaveBlockDialog-D-QO3LuR.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-DWBvVkuk.js → SavedBlocksBrowserModal-BRdN56r7.js} +6 -6
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-DWBvVkuk.js.map → SavedBlocksBrowserModal-BRdN56r7.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-GgdpiPUq.js → SavedBlocksPanels-D9GTlLgb.js} +2 -2
- package/dist/cdn/chunks/{SavedBlocksPanels-GgdpiPUq.js.map → SavedBlocksPanels-D9GTlLgb.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPickBar-Dyb5xi1B.js → SavedBlocksPickBar-DSRcUeIo.js} +3 -3
- package/dist/cdn/chunks/{SavedBlocksPickBar-Dyb5xi1B.js.map → SavedBlocksPickBar-DSRcUeIo.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-B0YcaJsR.js → TemplateScoringPanel-Dy5oOVhB.js} +2 -2
- package/dist/cdn/chunks/{TemplateScoringPanel-B0YcaJsR.js.map → TemplateScoringPanel-Dy5oOVhB.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-B9SmAIKZ.js → TestEmailModal-B4L2lmIV.js} +7 -7
- package/dist/cdn/chunks/{TestEmailModal-B9SmAIKZ.js.map → TestEmailModal-B4L2lmIV.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailPanel-Q6alElB1.js → TestEmailPanel-cTPgEf-D.js} +2 -2
- package/dist/cdn/chunks/{TestEmailPanel-Q6alElB1.js.map → TestEmailPanel-cTPgEf-D.js.map} +1 -1
- package/dist/cdn/chunks/{TitleEditor-Tudoac5f.js → TitleEditor-BKjL2Zs1.js} +8 -8
- package/dist/cdn/chunks/{TitleEditor-Tudoac5f.js.map → TitleEditor-BKjL2Zs1.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-BNJIFnyj.js → TplModal-DxrgNmt9.js} +2 -2
- package/dist/cdn/chunks/{TplModal-BNJIFnyj.js.map → TplModal-DxrgNmt9.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryMenu-DJGcA_RX.js → VersionHistoryMenu-CMqZFUHE.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryMenu-DJGcA_RX.js.map → VersionHistoryMenu-CMqZFUHE.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryPanels-Crw5JA89.js → VersionHistoryPanels-CxIFBJsz.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryPanels-Crw5JA89.js.map → VersionHistoryPanels-CxIFBJsz.js.map} +1 -1
- package/dist/cdn/chunks/{VersionPreviewBanner-C8pfES5A.js → VersionPreviewBanner-CdsZvVYF.js} +2 -2
- package/dist/cdn/chunks/{VersionPreviewBanner-C8pfES5A.js.map → VersionPreviewBanner-CdsZvVYF.js.map} +1 -1
- package/dist/cdn/chunks/{blockTypeIcons-JUwZtKaq.js → blockTypeIcons-DXPd_hd1.js} +2 -2
- package/dist/cdn/chunks/{blockTypeIcons-JUwZtKaq.js.map → blockTypeIcons-DXPd_hd1.js.map} +1 -1
- package/dist/cdn/chunks/{ca-DY0AYOLG.js → ca-DZSrLNXK.js} +3 -1
- package/dist/cdn/chunks/ca-DZSrLNXK.js.map +1 -0
- package/dist/cdn/chunks/{cloud-C29LPXYn.js → cloud-CITqEHEc.js} +2 -2
- package/dist/cdn/chunks/{cloud-C29LPXYn.js.map → cloud-CITqEHEc.js.map} +1 -1
- package/dist/cdn/chunks/{createCloudRuntime-C6MJN7M6.js → createCloudRuntime-DwIXvrb0.js} +4 -4
- package/dist/cdn/chunks/{createCloudRuntime-C6MJN7M6.js.map → createCloudRuntime-DwIXvrb0.js.map} +1 -1
- package/dist/{de-CYLWugqb.js → cdn/chunks/de-DbayqpK-.js} +4 -0
- package/dist/cdn/chunks/{de-CYLWugqb.js.map → de-DbayqpK-.js.map} +1 -1
- package/dist/cdn/chunks/en-BzFt4b80.js +2 -0
- package/dist/cdn/chunks/{en-C3TM249M.js → en-l0ib5X0f.js} +3 -1
- package/dist/cdn/chunks/en-l0ib5X0f.js.map +1 -0
- package/dist/cdn/chunks/{es-D1SF4nCR.js → es-CMQ-5PJP.js} +3 -1
- package/dist/cdn/chunks/es-CMQ-5PJP.js.map +1 -0
- package/dist/cdn/chunks/{extensions-BQefRevf.js → extensions-C87Z6_23.js} +5 -5
- package/dist/cdn/chunks/{extensions-BQefRevf.js.map → extensions-C87Z6_23.js.map} +1 -1
- package/dist/{fr-CdLhJ7kw.js → cdn/chunks/fr-DNXmZWAq.js} +4 -0
- package/dist/cdn/chunks/fr-DNXmZWAq.js.map +1 -0
- package/dist/cdn/chunks/{icons-BTdLXRXn.js → icons-ciivb5BS.js} +10 -10
- package/dist/cdn/chunks/{icons-BTdLXRXn.js.map → icons-ciivb5BS.js.map} +1 -1
- package/dist/{nl-BvsLoyKp.js → cdn/chunks/nl-BylRRHYJ.js} +4 -0
- package/dist/cdn/chunks/nl-BylRRHYJ.js.map +1 -0
- package/dist/cdn/chunks/{preRenderCustomBlocks-CbA3RcAy.js → preRenderCustomBlocks-DyCIgmqJ.js} +2 -2
- package/dist/cdn/chunks/{preRenderCustomBlocks-CbA3RcAy.js.map → preRenderCustomBlocks-DyCIgmqJ.js.map} +1 -1
- package/dist/cdn/chunks/{pt-BR-CAGS5YXp.js → pt-BR-BVoJhpie.js} +3 -1
- package/dist/cdn/chunks/pt-BR-BVoJhpie.js.map +1 -0
- package/dist/cdn/chunks/{src-Cx70q30o.js → src-6J-yyZoD.js} +2 -2
- package/dist/cdn/chunks/{src-Cx70q30o.js.map → src-6J-yyZoD.js.map} +1 -1
- package/dist/cdn/chunks/{src-B_QJduQD.js → src-BD60gqPW.js} +72 -67
- package/dist/cdn/chunks/src-BD60gqPW.js.map +1 -0
- package/dist/cdn/chunks/{src-fxaI2EQc.js → src-Bid3cmHa.js} +4 -4
- package/dist/cdn/chunks/{src-fxaI2EQc.js.map → src-Bid3cmHa.js.map} +1 -1
- package/dist/cdn/chunks/{src-CjSrA_o4.js → src-Cf0ohOog.js} +166 -136
- package/dist/cdn/chunks/src-Cf0ohOog.js.map +1 -0
- package/dist/cdn/chunks/{tiptap-Bqy-H6CT.js → tiptap-Ey0FpaX7.js} +1710 -1700
- package/dist/cdn/chunks/tiptap-Ey0FpaX7.js.map +1 -0
- package/dist/cdn/chunks/{useEditorCore-o2emEjgF.js → useEditorCore-CTNB2Mkl.js} +351 -356
- package/dist/cdn/chunks/{useEditorCore-o2emEjgF.js.map → useEditorCore-CTNB2Mkl.js.map} +1 -1
- package/dist/cdn/chunks/{useMergeTag-BWt1DQS1.js → useMergeTag-lWo7So18.js} +2 -2
- package/dist/cdn/chunks/{useMergeTag-BWt1DQS1.js.map → useMergeTag-lWo7So18.js.map} +1 -1
- package/dist/cdn/editor.css +1 -1
- package/dist/cdn/editor.js +943 -911
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{check-CTe6VejT.js → check-80E6ZGin.js} +1 -1
- package/dist/{chevron-down-BBThVMu-.js → chevron-down-BmkLsn2c.js} +1 -1
- package/dist/{chevron-up-iECwf-Bc.js → chevron-up-DJSAzlUU.js} +1 -1
- package/dist/{circle-alert-Z_eeykrO.js → circle-alert-BsCfZryY.js} +1 -1
- package/dist/{clock-CEj1c29j.js → clock-BcLIngSK.js} +1 -1
- package/dist/{cloud-CHHQWdaR.js → cloud-DwJ8ebNl.js} +1 -1
- package/dist/{createCloudRuntime-WwKt9mgb.js → createCloudRuntime-CdXQdl34.js} +2 -2
- package/dist/{createLucideIcon-DphAT3tw.js → createLucideIcon-eeetEgBb.js} +2 -2
- package/dist/{cdn/chunks/de-CYLWugqb.js → de-DbayqpK-.js} +2 -2
- package/dist/{dist-nmzPlqlz.js → dist-BC1y_U2m.js} +151 -141
- package/dist/{dist-gWM8H2hz.js → dist-BFcAowKZ.js} +2 -2
- package/dist/dist-BQqIUwvB.js +5 -0
- package/dist/{dist-DLaIQ5H5.js → dist-BkAVGSRr.js} +6 -6
- package/dist/{dist-BfcqryVg.js → dist-CQ9T1yi-.js} +2 -2
- package/dist/{dist-CaR5TLiM.js → dist-CWC5t5J3.js} +2 -2
- package/dist/{dist-D2P8U3p6.js → dist-Cd0LvUlh.js} +2 -2
- package/dist/{dist-DSgmnQ-f.js → dist-CvW1nNzy.js} +2 -2
- package/dist/{dist-B1kLn1rs.js → dist-CzVIVbSa.js} +90 -85
- package/dist/{dist-9fjN9cuD.js → dist-D8MekYFl.js} +2 -2
- package/dist/{dist-DifptC-U.js → dist-Do83S47T.js} +2 -2
- package/dist/{dist-BdIwIlKz.js → dist-prgdllHG.js} +2 -2
- package/dist/dist-waDcdb1l.js +5 -0
- package/dist/{en-C3TM249M.js → en-l0ib5X0f.js} +2 -0
- package/dist/{es-D1SF4nCR.js → es-CMQ-5PJP.js} +2 -0
- package/dist/{extensions-B4gwZ36u.js → extensions-BYImwYeO.js} +15 -15
- package/dist/{cdn/chunks/fr-CdLhJ7kw.js → fr-DNXmZWAq.js} +2 -2
- package/dist/{image-up-W1INxCu8.js → image-up-BfxY85ar.js} +1 -1
- package/dist/{info-5t4WYKfh.js → info-LX4QFBr6.js} +1 -1
- package/dist/{loader-circle-DJCZ_JMY.js → loader-circle-Bwima0v8.js} +1 -1
- package/dist/{message-circle-Biu6lHh_.js → message-circle-Box-mf2k.js} +1 -1
- package/dist/{cdn/chunks/nl-BvsLoyKp.js → nl-BylRRHYJ.js} +2 -2
- package/dist/{pencil-DkPlMca7.js → pencil-BM9gSq_J.js} +1 -1
- package/dist/{preRenderCustomBlocks-BebjKtmw.js → preRenderCustomBlocks-2KyBJocY.js} +1 -1
- package/dist/{pt-BR-CAGS5YXp.js → pt-BR-BVoJhpie.js} +2 -0
- package/dist/{refresh-cw-DHbpkkol.js → refresh-cw-BuzFX0wH.js} +1 -1
- package/dist/{scan-line-DojwGzjU.js → scan-line-Duq1WXoG.js} +1 -1
- package/dist/{send-c6ppcE9I.js → send-DZ4nOnIA.js} +1 -1
- package/dist/{shield-check-F9T4ylNK.js → shield-check-Cz4BRYdv.js} +1 -1
- package/dist/{sparkles-D-0OiTZD.js → sparkles-DP8RqOeL.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/templatical-editor.js +986 -954
- package/dist/{text-align-start-kEEtoRrD.js → text-align-start-D7tXhjUP.js} +1 -1
- package/dist/{trash-2-DRQzhY9R.js → trash-2-BS7lZeyb.js} +1 -1
- package/dist/{triangle-alert-Bt4-GU3t.js → triangle-alert-Cx4mQGjx.js} +1 -1
- package/dist/{upload-D2wtjvH8.js → upload-L8tLVM10.js} +1 -1
- package/dist/{useEditorCore-CJp84vJY.js → useEditorCore-CtR46sbJ.js} +868 -873
- package/dist/{useMergeTag-D1Rsn4BO.js → useMergeTag-Cx36rtrt.js} +3 -3
- package/dist/{useScrollToBlock-B0Qpjfa4.js → useScrollToBlock-tndC8vKo.js} +1 -1
- package/dist/{x-YiG-TUGN.js → x-ChIQ7HwK.js} +1 -1
- package/package.json +22 -22
- package/dist/cdn/chunks/NumberWithSuffix-DOG0zHvY.js.map +0 -1
- package/dist/cdn/chunks/ca-DY0AYOLG.js.map +0 -1
- package/dist/cdn/chunks/en-C3TM249M.js.map +0 -1
- package/dist/cdn/chunks/en-DIwntrh3.js +0 -2
- package/dist/cdn/chunks/es-D1SF4nCR.js.map +0 -1
- package/dist/cdn/chunks/fr-CdLhJ7kw.js.map +0 -1
- package/dist/cdn/chunks/nl-BvsLoyKp.js.map +0 -1
- package/dist/cdn/chunks/pt-BR-CAGS5YXp.js.map +0 -1
- package/dist/cdn/chunks/src-B_QJduQD.js.map +0 -1
- package/dist/cdn/chunks/src-CjSrA_o4.js.map +0 -1
- package/dist/cdn/chunks/tiptap-Bqy-H6CT.js.map +0 -1
- package/dist/dist-CzMzshfS.js +0 -5
- package/dist/dist-DTVuNn8M.js +0 -5
package/dist/cdn/editor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.js","names":["$emit","$emit","$emit"],"sources":["../../src/composables/useCommentsFeature.ts","../../src/composables/useSavedBlocksFeature.ts","../../src/composables/useTemplatesFeature.ts","../../src/utils/toMjml.ts","../../src/composables/useTestEmailFeature.ts","../../src/composables/useVersionHistoryFeature.ts","../../src/types/auto-save.ts","../../src/utils/resolveLintOptions.ts","../../src/utils/renderProvider.ts","../../src/utils/normalizeMergeTagMarkup.ts","../../src/components/Canvas.vue","../../src/components/Canvas.vue","../../src/components/CustomBlockStylesheets.vue","../../src/components/CustomBlockStylesheets.vue","../../src/utils/resolvePaletteBlocks.ts","../../src/utils/resolveInsertPosition.ts","../../src/components/Sidebar.vue","../../src/components/Sidebar.vue","../../src/utils/mergeTagTrigger.ts","../../src/utils/textFieldCaret.ts","../../src/composables/useMergeTagAutocomplete.ts","../../src/composables/useMergeTagField.ts","../../src/components/MergeTagSegments.vue","../../src/components/MergeTagSegments.vue","../../src/components/MergeTagInsertButton.vue","../../src/components/MergeTagInsertButton.vue","../../src/components/LogicTagInsertButton.vue","../../src/components/LogicTagInsertButton.vue","../../src/components/MergeTagTextarea.vue","../../src/components/MergeTagTextarea.vue","../../src/components/TemplateSettings.vue","../../src/components/TemplateSettings.vue","../../src/components/MergeTagInput.vue","../../src/components/MergeTagInput.vue","../../src/components/toolbar/ButtonToolbar.vue","../../src/components/toolbar/ButtonToolbar.vue","../../src/components/SpacingControl.vue","../../src/components/SpacingControl.vue","../../src/components/toolbar/CollapsibleSection.vue","../../src/components/toolbar/CollapsibleSection.vue","../../src/components/toolbar/CommonBlockSettings.vue","../../src/components/toolbar/CommonBlockSettings.vue","../../src/components/toolbar/fields/BooleanField.vue","../../src/components/toolbar/fields/BooleanField.vue","../../src/components/toolbar/fields/FieldWrapper.vue","../../src/components/toolbar/fields/FieldWrapper.vue","../../src/components/toolbar/fields/ColorField.vue","../../src/components/toolbar/fields/ColorField.vue","../../src/components/toolbar/fields/ImageField.vue","../../src/components/toolbar/fields/ImageField.vue","../../src/components/toolbar/fields/NumberField.vue","../../src/components/toolbar/fields/NumberField.vue","../../src/components/toolbar/fields/RepeatableField.vue","../../src/components/toolbar/fields/RepeatableField.vue","../../src/components/toolbar/fields/SelectField.vue","../../src/components/toolbar/fields/SelectField.vue","../../src/components/toolbar/fields/TextField.vue","../../src/components/toolbar/fields/TextField.vue","../../src/components/toolbar/fields/TextareaField.vue","../../src/components/toolbar/fields/TextareaField.vue","../../src/components/toolbar/fields/index.ts","../../src/components/toolbar/CustomBlockToolbar.vue","../../src/components/toolbar/CustomBlockToolbar.vue","../../src/components/toolbar/DividerToolbar.vue","../../src/components/toolbar/DividerToolbar.vue","../../src/components/toolbar/HtmlToolbar.vue","../../src/components/toolbar/HtmlToolbar.vue","../../src/components/toolbar/ImageToolbar.vue","../../src/components/toolbar/ImageToolbar.vue","../../src/components/toolbar/MenuToolbar.vue","../../src/components/toolbar/MenuToolbar.vue","../../src/utils/rebalanceColumnChildren.ts","../../src/components/toolbar/SectionToolbar.vue","../../src/components/toolbar/SectionToolbar.vue","../../src/components/toolbar/SocialToolbar.vue","../../src/components/toolbar/SocialToolbar.vue","../../src/components/toolbar/SpacerToolbar.vue","../../src/components/toolbar/SpacerToolbar.vue","../../src/components/toolbar/TableToolbar.vue","../../src/components/toolbar/TableToolbar.vue","../../src/components/toolbar/TitleToolbar.vue","../../src/components/toolbar/TitleToolbar.vue","../../src/components/toolbar/VideoToolbar.vue","../../src/components/toolbar/VideoToolbar.vue","../../src/components/Toolbar.vue","../../src/components/Toolbar.vue","../../src/components/RightSidebar.vue","../../src/components/RightSidebar.vue","../../src/components/SmallScreenNotice.vue","../../src/components/SmallScreenNotice.vue","../../src/components/EditorFooter.vue","../../src/components/EditorFooter.vue","../../src/components/DarkModeToggle.vue","../../src/components/DarkModeToggle.vue","../../src/components/PreviewToggle.vue","../../src/components/PreviewToggle.vue","../../src/components/TemplateNameField.vue","../../src/components/TemplateNameField.vue","../../src/components/TemplateTimestamp.vue","../../src/components/TemplateTimestamp.vue","../../src/components/TemplateSaveStatus.vue","../../src/components/TemplateSaveStatus.vue","../../src/components/EditorHeader.vue","../../src/components/EditorHeader.vue","../../src/components/MergeTagPickerModal.vue","../../src/components/MergeTagPickerModal.vue","../../src/components/LogicTagBadge.vue","../../src/components/LogicTagBadge.vue","../../src/components/LogicTagPickerModal.vue","../../src/components/LogicTagPickerModal.vue","../../src/Editor.vue","../../src/Editor.vue","../../src/i18n/index.ts","../../src/index.ts"],"sourcesContent":["import { computed, ref, watch, type ComputedRef, type Ref } from \"vue\";\nimport { useComments, useCommentListener } from \"@templatical/core\";\nimport type { UseCommentsReturn } from \"@templatical/core\";\nimport type { CommentsProvider, EditorUser } from \"@templatical/types\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** Minimal slice of the editor this feature needs — OSS and Cloud alike. */\ninterface CommentsEditor {\n state: {\n readonly template?: { id: string } | null;\n };\n}\n\nexport interface UseCommentsFeatureOptions {\n /** Storage backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: CommentsProvider;\n editor: CommentsEditor;\n /**\n * Who is commenting (`init({ user })`). **Without one the feature reports\n * itself unavailable** — see {@link isAvailable}.\n */\n user?: EditorUser | null;\n /**\n * The panel's open state. Cloud passes its own so comments stay mutually\n * exclusive with the AI and scoring sidebars, which share the same 360px\n * gutter; OSS gets a local ref.\n */\n isOpen?: Ref<boolean>;\n onError?: (error: Error) => void;\n /**\n * Extra gate on top of a provider and a user being present — Cloud adds its\n * `commenting` plan entitlement and \"the template must be saved\". Read\n * reactively, so a Cloud entitlement that resolves after setup still lights the\n * feature up.\n */\n isAvailable?: () => boolean;\n /**\n * Whether a block id exists in the **stored** template. Cloud supplies this: a\n * comment is anchored server-side, so filtering to a block that only exists on\n * the canvas has nothing to show. Absent means \"assume it does\", which is right\n * for a store that accepts any anchor.\n */\n isBlockSaved?: (blockId: string) => boolean;\n}\n\nexport interface UseCommentsFeatureReturn {\n headless: UseCommentsReturn;\n\n isOpen: Ref<boolean>;\n isAvailable: ComputedRef<boolean>;\n /** True once a template exists — comments are keyed to one. */\n hasTemplate: ComputedRef<boolean>;\n unresolvedCount: ComputedRef<number>;\n\n /**\n * The block the panel should filter to, or `null` for no block filter. Owned\n * here rather than in the panel because a block's comment indicator sets it\n * *before* the lazy panel exists.\n */\n filterBlockId: Ref<string | null>;\n\n /** Toggle the panel. A no-op while unavailable. */\n toggle: () => void;\n close: () => void;\n /** Open the panel filtered to one block — what the canvas indicator calls. */\n openForBlock: (blockId: string) => void;\n\n capability: NonNullable<EditorCapabilities[\"comments\"]>;\n}\n\n/**\n * Shared glue for comments: the reactive thread list, the panel's open and filter\n * state, and the capability the shared chrome gates on.\n *\n * Both entry points construct this — OSS from `init({ comments })`, Cloud from\n * `createCloudCommentsProvider` — so the two run identical logic over different\n * transports. Nothing here is auth- or plan-aware; callers decide availability.\n *\n * **Nothing is fetched at mount.** The list is read when the panel opens, matching\n * saved blocks: a review panel nobody opens should cost no round-trip, and the\n * unresolved badge is worth less than a request on every editor boot.\n */\nexport function useCommentsFeature(\n options: UseCommentsFeatureOptions,\n): UseCommentsFeatureReturn {\n const { provider, editor } = options;\n\n const headless = useComments({\n provider,\n getTemplateId: () => editor.state.template?.id ?? null,\n // Read through a getter, never destructured: Cloud fills `user` from the JWT\n // and a consumer may swap it, and the same lesson as `allowedRecipients`\n // applies — a snapshot at setup pins the answer forever.\n getUser: () => options.user ?? null,\n onError: options.onError,\n });\n\n // Provider-driven realtime, or nothing at all when the provider has no\n // `subscribe`. It is deliberately wired here rather than inside `useComments`:\n // the subscription is a lifecycle concern (it follows the template and must be\n // torn down), and `useComments` is state.\n useCommentListener({\n comments: headless,\n provider,\n getTemplateId: () => editor.state.template?.id ?? null,\n });\n\n const isOpen = options.isOpen ?? ref(false);\n const filterBlockId = ref<string | null>(null);\n\n const hasTemplate = computed(() => !!editor.state.template?.id);\n const unresolvedCount = headless.unresolvedCount;\n\n const isAvailable = computed(() => {\n // No identity ⇒ unavailable, never anonymous. Every comment carries an author\n // and an unattributable one is worse than no comment feature.\n if (!options.user) return false;\n return options.isAvailable?.() ?? true;\n });\n\n // A panel left open when the feature goes away — a Cloud plan fetch that\n // withholds `commenting`, or a consumer clearing `user` — would sit there with\n // no trigger to close it.\n watch(isAvailable, (available) => {\n if (!available) isOpen.value = false;\n });\n\n // Load as soon as there is a template, not just when the panel opens. Unlike\n // saved blocks or version history — whose lists are read only by the surface\n // that fetches them — two indicators *outside* this panel derive from the whole\n // list: `unresolvedCount` feeds the header badge, and `commentCountByBlock`\n // feeds the per-block canvas markers. Deferring the fetch left both at zero, so\n // a template arriving with existing comments showed no sign of them anywhere\n // until the user happened to click Comments, and the badge then popped into\n // existence — the same \"reads 0, then pops\" failure that cost the saved-blocks\n // rail its count.\n //\n // Loading the lot up front is already the design's assumption: `comments.list`\n // is deliberately unpaginated precisely because those two aggregates are\n // computed over every thread.\n watch(\n [isAvailable, hasTemplate],\n ([available, attached]) => {\n if (!available || !attached) return;\n void headless.load().catch(() => {\n /* reported through onError */\n });\n },\n { immediate: true },\n );\n\n // Every open re-reads. History and comments both grow elsewhere, so a list\n // fetched once goes stale silently; only a *first* open has nothing to show\n // meanwhile, which the panel distinguishes for its skeleton.\n watch(isOpen, (open) => {\n if (!open || !hasTemplate.value) return;\n void headless.load().catch(() => {\n /* reported through onError */\n });\n });\n\n function toggle(): void {\n if (!isAvailable.value) return;\n isOpen.value = !isOpen.value;\n }\n\n function close(): void {\n isOpen.value = false;\n }\n\n function openForBlock(blockId: string): void {\n if (!isAvailable.value) return;\n filterBlockId.value = blockId;\n // Assigning `true` to an already-true ref fires no watcher, so an indicator\n // clicked while the panel is open must refresh explicitly — otherwise moving\n // between blocks shows a list read before the last change.\n if (isOpen.value) {\n if (hasTemplate.value) {\n void headless.load().catch(() => {\n /* reported through onError */\n });\n }\n return;\n }\n isOpen.value = true;\n }\n\n return {\n headless,\n\n isOpen,\n isAvailable,\n hasTemplate,\n unresolvedCount,\n\n filterBlockId,\n\n toggle,\n close,\n openForBlock,\n\n capability: {\n getBlockCount: (blockId: string) =>\n headless.commentCountByBlock.value.get(blockId) ?? 0,\n openForBlock,\n // Absent means \"any anchor is fine\", which is the right default for a store\n // that accepts one. Cloud supplies the real check because it renders from\n // the saved template.\n isBlockSaved: (blockId: string) =>\n options.isBlockSaved?.(blockId) ?? true,\n isAvailable,\n unresolvedCount,\n canCreate: headless.canCreate,\n canUpdate: headless.canUpdate,\n canDelete: headless.canDelete,\n canResolve: headless.canResolve,\n },\n };\n}\n","import { computed, provide, ref, watch, type ComputedRef, type Ref } from \"vue\";\nimport { useSavedBlocks, type UseSavedBlocksReturn } from \"@templatical/core\";\nimport type {\n Block,\n SavedBlock,\n SavedBlocksProvider,\n} from \"@templatical/types\";\nimport { cloneBlock } from \"@templatical/types\";\nimport { SAVED_BLOCKS_KEY } from \"../keys\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** Minimal slice of the editor this feature needs — satisfied by OSS and Cloud alike. */\ninterface SavedBlocksEditor {\n addBlock: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n index?: number,\n ) => void;\n /** Read reactively to cancel a pick session when preview mode turns on. */\n state: { previewMode: boolean };\n}\n\nexport interface UseSavedBlocksFeatureOptions {\n /** Storage backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: SavedBlocksProvider;\n editor: SavedBlocksEditor;\n onError?: (error: Error) => void;\n /**\n * Whether the feature may be used. Read reactively, so Cloud can defer to a\n * plan entitlement that only resolves after its async config fetch. Omit for\n * always-available (the OSS case: a configured provider is the whole gate).\n */\n isAvailable?: () => boolean;\n}\n\nexport interface UseSavedBlocksFeatureReturn {\n headless: UseSavedBlocksReturn;\n\n // --- Pick session ---\n isPicking: ComputedRef<boolean>;\n pickedIds: Ref<Set<string>>;\n pickedCount: ComputedRef<number>;\n startPicking: (blockId: string) => void;\n togglePick: (blockId: string) => void;\n isPicked: (blockId: string) => boolean;\n confirmPicking: () => void;\n cancelPicking: () => void;\n\n // --- Dialogs ---\n isSaveDialogOpen: Ref<boolean>;\n closeSaveDialog: () => void;\n isBrowserOpen: Ref<boolean>;\n openBrowser: () => void;\n closeBrowser: () => void;\n\n /** Insert a saved block's content into the canvas, with fresh block IDs. */\n insert: (saved: SavedBlock, insertIndex: number | undefined) => void;\n /** Re-read the list from the provider (used after a successful save). */\n refresh: () => void;\n /** Capability object handed to `useEditorCore` so shared UI can light up. */\n capability: NonNullable<EditorCapabilities[\"savedBlocks\"]>;\n count: ComputedRef<number>;\n isAvailable: ComputedRef<boolean>;\n}\n\n/**\n * Shared glue for the saved-blocks feature: reactive list, the canvas pick\n * session, the insert operation, and the capability object the shared UI gates\n * on.\n *\n * Both `Editor.vue` (OSS) and `useCloudInitialization` (Cloud) call this with\n * their own provider, so the two entry points run identical logic over\n * different transports. Nothing here is auth- or plan-aware — callers decide\n * whether the feature is available *before* constructing it.\n *\n * **The pick session is deliberately local to this feature.** Choosing which\n * blocks to save happens on the canvas (the user's mental model is spatial),\n * but it does NOT touch `EditorState.selectedBlockId`: keeping it a transient\n * mode avoids a breaking core-state change, leaves Cloud's single-block\n * selection broadcast and block locking untouched, and steers clear of\n * Sortable multi-drag. Promote it to real multi-select only if a second\n * consumer needs it.\n *\n * Must be called from `setup()`: it `provide()`s the headless instance under\n * {@link SAVED_BLOCKS_KEY} for the dialogs to inject.\n */\nexport function useSavedBlocksFeature(\n options: UseSavedBlocksFeatureOptions,\n): UseSavedBlocksFeatureReturn {\n const { provider, editor } = options;\n\n const headless = useSavedBlocks({ provider, onError: options.onError });\n\n const isSaveDialogOpen = ref(false);\n const isBrowserOpen = ref(false);\n\n // --- Pick session ---\n const picking = ref(false);\n const pickedIds = ref<Set<string>>(new Set());\n const isPicking = computed(() => picking.value);\n const pickedCount = computed(() => pickedIds.value.size);\n\n function startPicking(blockId: string): void {\n // Nothing to pick *for* when the provider withheld `create` — the dialog at\n // the end of the session would have no way to persist. The bookmark action\n // is already hidden; this covers programmatic callers.\n if (!headless.canCreate.value) return;\n // Preview mode has no block chrome to pick with, and its click handlers are\n // suppressed — a session there could never be completed or cancelled.\n if (editor.state.previewMode) return;\n pickedIds.value = new Set([blockId]);\n picking.value = true;\n }\n\n function togglePick(blockId: string): void {\n if (!picking.value) return;\n const next = new Set(pickedIds.value);\n if (next.has(blockId)) {\n next.delete(blockId);\n } else {\n next.add(blockId);\n }\n pickedIds.value = next;\n }\n\n function isPicked(blockId: string): boolean {\n return pickedIds.value.has(blockId);\n }\n\n function cancelPicking(): void {\n picking.value = false;\n pickedIds.value = new Set();\n }\n\n function confirmPicking(): void {\n if (pickedCount.value === 0) return;\n // Leave pick mode but keep the set — the dialog reads it to know what it's\n // saving. `closeSaveDialog` is what clears it.\n picking.value = false;\n isSaveDialogOpen.value = true;\n // The dialog offers the categories already in use as suggestions, and those\n // are derived from the loaded list — without this they'd be empty for anyone\n // who saves without ever opening the browser, and the derived-category model\n // would drift into near-duplicates.\n refresh();\n }\n\n function closeSaveDialog(): void {\n isSaveDialogOpen.value = false;\n pickedIds.value = new Set();\n }\n\n // A session can't survive entering preview mode: the canvas stops responding\n // to selection there, so the bar would be the only way out.\n watch(\n () => editor.state.previewMode,\n (previewMode) => {\n if (previewMode && picking.value) cancelPicking();\n },\n );\n\n function openBrowser(): void {\n isBrowserOpen.value = true;\n refresh();\n }\n\n function closeBrowser(): void {\n isBrowserOpen.value = false;\n }\n\n function insert(saved: SavedBlock, insertIndex: number | undefined): void {\n for (let i = 0; i < saved.content.length; i++) {\n // cloneBlock regenerates ids (recursively for section children) so the\n // inserted copy never collides with the stored one, or with blocks\n // already on the canvas from a previous insert of the same entry.\n const cloned = cloneBlock(saved.content[i]);\n const position = insertIndex !== undefined ? insertIndex + i : undefined;\n editor.addBlock(cloned, undefined, undefined, position);\n }\n isBrowserOpen.value = false;\n }\n\n function refresh(): void {\n // Fire-and-forget: `useSavedBlocks` already routes failures to `onError`,\n // and a stale list is not worth blocking the dialog's close on.\n void headless.load().catch(() => {});\n }\n\n const count = computed(() => headless.savedBlocks.value.length);\n const isAvailable = computed(() => options.isAvailable?.() ?? true);\n\n /**\n * Nothing is fetched at mount — the list loads when the browser or the save\n * dialog opens. So a consumer's `list()` latency can neither delay the editor\n * nor shift the sidebar: the rail entry is gated purely on availability,\n * never on whether anything happens to be saved.\n *\n * True only while there is *nothing to show*, which is what makes the browser\n * skeleton appear on a first open and stay away on a reopen — the previous\n * results render immediately while the refetch lands underneath. Without that\n * distinction the empty state would read \"No saved blocks yet\" for however\n * long the request takes, which is false rather than merely unhelpful.\n *\n * The browser modal derives that condition itself from the injected composable\n * — it is the only surface that displays the list, so there is no reason to\n * widen the capability with it.\n */\n\n provide(SAVED_BLOCKS_KEY, headless);\n\n return {\n headless,\n\n isPicking,\n pickedIds,\n pickedCount,\n startPicking,\n togglePick,\n isPicked,\n confirmPicking,\n cancelPicking,\n\n isSaveDialogOpen,\n closeSaveDialog,\n isBrowserOpen,\n openBrowser,\n closeBrowser,\n\n insert,\n refresh,\n capability: {\n startPicking,\n togglePick,\n isPicked,\n isPicking,\n confirmPicking,\n cancelPicking,\n openBrowser,\n count,\n isAvailable,\n canCreate: headless.canCreate,\n canUpdate: headless.canUpdate,\n canDelete: headless.canDelete,\n },\n count,\n isAvailable,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { useEventListener, useTimeoutFn } from \"@vueuse/core\";\nimport type {\n Template,\n TemplateContent,\n TemplateSaveTrigger,\n TemplatesProvider,\n} from \"@templatical/types\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** How long the header shows \"Saved\" before falling back to the dirty state. */\nconst SAVED_STATUS_MS = 3000;\n\n/** Minimal slice of the editor this feature needs. */\ninterface TemplatesEditor {\n state: {\n readonly template: {\n id: string;\n name?: string;\n createdAt?: string;\n updatedAt?: string;\n } | null;\n readonly isDirty: boolean;\n readonly isSaving: boolean;\n };\n setName: (name: string) => void;\n create: (input?: {\n name?: string;\n content?: TemplateContent;\n }) => Promise<Template>;\n load: (templateId: string) => Promise<Template>;\n save: (trigger?: TemplateSaveTrigger) => Promise<Template>;\n hasTemplate: () => boolean;\n}\n\n/**\n * A veto over saving, supplied by whoever configured the editor.\n *\n * The one implementation is Cloud's lint save-gate: with the server policy\n * `accessibility.blockOnError` on and error-severity issues present, a manual\n * save opens a confirmation and an autosave simply does not fire. The shape is\n * generic because the header's Save must be *routable* through a gate — without\n * that, unifying the two headers would have silently dropped `blockOnError`.\n */\nexport interface SaveGate {\n /**\n * Manual save (header button, Cmd+S). Runs the save, or defers it and raises a\n * confirmation. Resolves to whether it ran.\n */\n tryRunSave: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /**\n * Autosave. Runs the save only when the gate would not block, and does nothing\n * at all otherwise — no prompt on a debounce timer, and no write that would\n * demote the policy to a manual-save-only speed bump.\n */\n runUnlessBlocked: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n}\n\nexport interface UseTemplatesFeatureOptions {\n /** Storage backend — consumer-supplied via `init({ templates })`. */\n provider: TemplatesProvider;\n editor: TemplatesEditor;\n /**\n * Warn on tab close while there are unsaved changes. Defaults to on; pass\n * `false` to own that guard yourself.\n */\n guardUnsavedChanges?: boolean;\n /**\n * Extra gate on top of the provider being present. Read reactively, so a\n * future adapter can defer to an entitlement that resolves after setup. Omit\n * for always-available (the OSS case: a configured provider is the whole gate).\n */\n isAvailable?: () => boolean;\n /**\n * Optional veto over every save this feature performs. Read through a getter\n * rather than passed by value because Cloud builds its gate from\n * `core.templateLint`, which only exists after `useEditorCore()` — i.e. after\n * this composable is constructed.\n */\n getSaveGate?: () => SaveGate | null;\n}\n\nexport interface UseTemplatesFeatureReturn {\n /** Persist the loaded template. Rejects — see {@link requestSave} for UI. */\n save: (trigger?: TemplateSaveTrigger) => Promise<Template>;\n /** Persist the current content as a new template. Rejects on failure. */\n create: (input?: {\n name?: string;\n content?: TemplateContent;\n }) => Promise<Template>;\n /** Fetch a template. Passed straight through; saving status is untouched. */\n load: (templateId: string) => Promise<Template>;\n /**\n * Fire-and-forget save for the header button and Cmd+S: the outcome is already\n * reported through {@link status}, so nothing is left for a caller to handle.\n * A no-op while a save is running, without a template, or when the provider\n * withheld `save`.\n */\n requestSave: (trigger?: TemplateSaveTrigger) => void;\n /**\n * The autosave path. Identical to {@link requestSave} except that a configured\n * gate is consulted with `runUnlessBlocked` instead of `tryRunSave`, so a\n * blocked autosave is skipped silently rather than raising a prompt while the\n * user is typing. The template stays dirty and the header keeps saying so.\n */\n requestAutoSave: () => void;\n /**\n * Commit an inline rename and persist it. Empty (or whitespace-only) names and\n * no-change commits are ignored, so a stray blur can neither clear the name\n * nor trigger a pointless round-trip.\n */\n rename: (name: string) => void;\n /** The loaded template's name, or `undefined` when unnamed / not loaded. */\n name: ComputedRef<string | undefined>;\n /**\n * The stored template's write time and which field it came from, or `null`\n * when the provider supplies neither timestamp.\n */\n timestamp: ComputedRef<{\n iso: string;\n kind: \"updatedAt\" | \"createdAt\";\n } | null>;\n hasTemplate: ComputedRef<boolean>;\n isSaving: ComputedRef<boolean>;\n /**\n * What the header's status indicator shows. `\"saved\"` decays back to `\"idle\"`\n * after a few seconds; `\"error\"` stays until the next attempt, since a failure\n * the user never saw is worse than a stale badge.\n */\n status: Ref<\"idle\" | \"saved\" | \"error\">;\n /** The provider's own message for the last failure. Rendered in a tooltip. */\n errorMessage: Ref<string>;\n canCreate: ComputedRef<boolean>;\n canSave: ComputedRef<boolean>;\n isAvailable: ComputedRef<boolean>;\n capability: NonNullable<EditorCapabilities[\"templates\"]>;\n}\n\n/**\n * Shared glue for the templates feature: the save-status the header renders, the\n * inline rename, the unsaved-changes guard, and the capability shared chrome\n * gates on.\n *\n * Persistence itself lives in `@templatical/core`'s `useEditor`, over the\n * consumer's {@link TemplatesProvider} — this composable owns only what is\n * *about the UI*. It is constructed exactly when a provider is configured, so\n * an editor without one downloads it but never runs it.\n */\nexport function useTemplatesFeature(\n options: UseTemplatesFeatureOptions,\n): UseTemplatesFeatureReturn {\n const { provider, editor } = options;\n\n const status = ref<\"idle\" | \"saved\" | \"error\">(\"idle\");\n const errorMessage = ref(\"\");\n\n const { start: startSavedStatusClear, stop: stopSavedStatusClear } =\n useTimeoutFn(\n () => {\n status.value = \"idle\";\n },\n SAVED_STATUS_MS,\n { immediate: false },\n );\n\n const canCreate = computed(() => typeof provider.create === \"function\");\n const canSave = computed(() => typeof provider.save === \"function\");\n const isAvailable = computed(() => options.isAvailable?.() ?? true);\n\n const name = computed(() => editor.state.template?.name);\n // Prefers `updatedAt`; `createdAt` is the fallback for a store that records\n // only creation. Carries which one won, because labelling a never-updated\n // template \"Updated\" would be a claim the store never made.\n const timestamp = computed<{\n iso: string;\n kind: \"updatedAt\" | \"createdAt\";\n } | null>(() => {\n const template = editor.state.template;\n if (template?.updatedAt)\n return { iso: template.updatedAt, kind: \"updatedAt\" };\n if (template?.createdAt)\n return { iso: template.createdAt, kind: \"createdAt\" };\n return null;\n });\n const hasTemplate = computed(() => editor.state.template !== null);\n const isSaving = computed(() => editor.state.isSaving);\n\n function reportSuccess(): void {\n errorMessage.value = \"\";\n status.value = \"saved\";\n // Restart rather than stack: two saves inside the window would otherwise\n // clear the badge on the first timer, mid-way through the second's window.\n stopSavedStatusClear();\n startSavedStatusClear();\n }\n\n function reportFailure(error: unknown): void {\n stopSavedStatusClear();\n status.value = \"error\";\n errorMessage.value =\n error instanceof Error && error.message ? error.message : \"Save failed\";\n }\n\n async function save(trigger?: TemplateSaveTrigger): Promise<Template> {\n try {\n const template = await editor.save(trigger);\n reportSuccess();\n return template;\n } catch (error) {\n reportFailure(error);\n throw error;\n }\n }\n\n async function create(input?: {\n name?: string;\n content?: TemplateContent;\n }): Promise<Template> {\n try {\n const template = await editor.create(input);\n // Deliberately no \"saved\" badge. The badge acknowledges a save the user\n // asked for — an explicit `save()` or an autosave tick — and attaching a\n // template is neither. It also matched nothing: `load()` attaches without\n // announcing, so a new template flashed green where an existing one stayed\n // silent, for the same action. What a fresh template *has* is a\n // `createdAt`, which the header renders as its write time.\n //\n // The state is still reset, because a failure must not outlive the success\n // that followed it: a create that fails and is then retried would\n // otherwise leave \"Save failed\" on screen for a template that saved fine.\n stopSavedStatusClear();\n status.value = \"idle\";\n errorMessage.value = \"\";\n return template;\n } catch (error) {\n reportFailure(error);\n throw error;\n }\n }\n\n function load(templateId: string): Promise<Template> {\n // The badge describes one template, so it cannot outlive the switch to\n // another: a save that failed seconds ago would otherwise show \"Save failed\"\n // over freshly loaded content, and a green \"Saved\" still inside its decay\n // window would claim the incoming template had just been written. Reset up\n // front rather than on success — a load that fails still means the old\n // status no longer describes what the user is looking at, and if the old\n // template stays attached with edits, `isDirty` puts \"Unsaved\" back.\n stopSavedStatusClear();\n status.value = \"idle\";\n errorMessage.value = \"\";\n // Deliberately not wrapped beyond that: a failed *load* is not a failed\n // save, and flashing \"Save failed\" for it would be a lie. Core already\n // routes it to `onError` and rejects to the caller.\n return editor.load(templateId);\n }\n\n /**\n * An autosave tick that arrived while a save was in flight. `useAutoSave`'s\n * timer fires once and clears itself, and only a further content mutation\n * arms a new one — so dropping such a tick left autosave silent until the\n * user happened to type again.\n */\n let queuedAutoSave = false;\n\n /** Shared by both request paths — the outcome is reported through `status`. */\n function runSave(trigger: TemplateSaveTrigger): Promise<void> {\n return save(trigger).then(\n () => {\n if (!queuedAutoSave) return;\n queuedAutoSave = false;\n // Only what the finished save could not have covered.\n if (editor.state.isDirty) requestAutoSave();\n },\n () => {\n // Deliberately no retry on failure: the status indicator already shows\n // \"Save failed\", so autosave has not gone *silent*, and re-firing into a\n // failing endpoint would only double the traffic. The next content\n // change arms a fresh tick.\n queuedAutoSave = false;\n },\n );\n }\n\n /** True when there is nothing a save could do, whatever the gate says. */\n function cannotSave(): boolean {\n // The UI hides the button in each of these cases; this covers Cmd+S and\n // programmatic callers, where a refusal would surface as an error badge for\n // something the user never asked for.\n return !canSave.value || !hasTemplate.value || isSaving.value;\n }\n\n function requestSave(trigger: TemplateSaveTrigger = \"manual\"): void {\n if (cannotSave()) return;\n const gate = options.getSaveGate?.();\n if (gate) {\n void gate.tryRunSave(() => runSave(trigger));\n return;\n }\n void runSave(trigger);\n }\n\n function requestAutoSave(): void {\n // Split the transient condition out of `cannotSave()`: a save already in\n // flight is a \"later\", not a \"never\". Overlapping PATCHes of the whole\n // document must still never happen, so the tick is queued rather than run.\n if (!canSave.value || !hasTemplate.value) return;\n if (isSaving.value) {\n queuedAutoSave = true;\n return;\n }\n const gate = options.getSaveGate?.();\n if (gate) {\n void gate.runUnlessBlocked(() => runSave(\"autosave\"));\n return;\n }\n void runSave(\"autosave\");\n }\n\n function rename(newName: string): void {\n const trimmed = newName.trim();\n // An empty name is rejected rather than stored: the header would then have\n // nothing to render, and \"\" is far more likely a slip than an intent.\n if (!trimmed) return;\n if (trimmed === editor.state.template?.name) return;\n editor.setName(trimmed);\n requestSave(\"rename\");\n }\n\n // Tab close with unsaved work. Gated on a provider being configured — the\n // editor otherwise has no idea whether the consumer already persisted the\n // change, so it would be warning about nothing. Note this cannot cover SPA\n // route changes; `onDirtyChange` is what consumers guard their router with.\n if (options.guardUnsavedChanges !== false) {\n useEventListener(window, \"beforeunload\", (event: BeforeUnloadEvent) => {\n if (!editor.state.isDirty) return;\n event.preventDefault();\n // Some engines still show the prompt only when `returnValue` is set.\n event.returnValue = \"\";\n });\n }\n\n const capability: NonNullable<EditorCapabilities[\"templates\"]> = {\n save: requestSave,\n rename,\n name,\n timestamp,\n hasTemplate,\n isSaving,\n status,\n errorMessage,\n canCreate,\n canSave,\n isAvailable,\n };\n\n return {\n save,\n create,\n load,\n requestSave,\n requestAutoSave,\n rename,\n name,\n timestamp,\n hasTemplate,\n isSaving,\n status,\n errorMessage,\n canCreate,\n canSave,\n isAvailable,\n capability,\n };\n}\n","import type { CustomBlock, TemplateContent } from \"@templatical/types\";\nimport type { RenderFonts } from \"./renderProvider\";\n\n/**\n * Minimal slice of the editor surface needed by `toMjmlForInstance`.\n * Decoupled from the full `TemplaticalEditor` type so this helper can be\n * tested in isolation with a stub object.\n */\nexport interface ToMjmlSource {\n getContent(): TemplateContent;\n renderCustomBlock(block: CustomBlock): Promise<string>;\n /**\n * Optional. The fonts the editor is rendering with, forwarded to the renderer's\n * `customFonts` / `defaultFallbackFont`. Without it a template using a custom\n * face exports with no `<mj-font>` declaration and no fallback stack — i.e. the\n * mail client silently substitutes. Provided by both editors, and by anything\n * that has a fonts manager; a source that omits it produces byte-identical\n * MJML to one whose template uses no custom face.\n */\n getFonts?: () => RenderFonts;\n /**\n * Optional. Look up the definition-level CSS for a custom block type.\n * Returns `undefined` when the definition is unknown or has no\n * `stylesheet`. Wired to the renderer's `getCustomBlockStylesheet` option\n * so authored responsive/hover/font CSS lands in `<mj-head>` deduped per\n * definition. Omitting it is optional-by-design: the resolver is simply not\n * passed through, so no `<mj-head>` stylesheet is emitted.\n */\n getCustomBlockStylesheet?: (customType: string) => string | undefined;\n}\n\n/**\n * Lazy-load `@templatical/renderer` and render the editor's current content\n * to MJML, wiring the editor's own custom block resolver into the renderer's\n * `renderCustomBlock` callback.\n *\n * The renderer is an optional peer dependency (small, MIT-licensed). It is\n * only loaded when an export is actually requested. Consumers that don't\n * need MJML export at all (e.g., embedding the editor in an app where the\n * backend handles export) can omit the install entirely; calling `toMjml()`\n * in that case throws a clear error naming the missing package.\n *\n * The dynamic import is cached by the module system, so subsequent calls\n * skip the import overhead.\n */\n/**\n * Resolve `@templatical/renderer`, or `null` when it isn't installed.\n *\n * The single `import(\"@templatical/renderer\")` site in the package, so callers\n * can choose their own failure behaviour without duplicating the dynamic import\n * or matching on an error message. {@link toMjmlForInstance} throws when this\n * returns `null`; the test-email path degrades to JSON-only and warns once,\n * because there the renderer is opt-in and a missing install must not break\n * sending.\n *\n * Cheap to call repeatedly — the module system caches the import, so a second\n * call after a successful first one costs nothing.\n */\nexport async function tryLoadRenderer(): Promise<\n typeof import(\"@templatical/renderer\") | null\n> {\n try {\n return await import(\"@templatical/renderer\");\n } catch {\n return null;\n }\n}\n\nexport async function toMjmlForInstance(\n instance: ToMjmlSource,\n): Promise<string> {\n const renderer = await tryLoadRenderer();\n if (!renderer) {\n throw new Error(\n \"[Templatical] toMjml() requires the @templatical/renderer package. Please install it.\",\n );\n }\n const stylesheetResolver = instance.getCustomBlockStylesheet;\n const fonts = instance.getFonts?.();\n return renderer.renderToMjml(instance.getContent(), {\n renderCustomBlock: instance.renderCustomBlock,\n // Only pass through when the source actually provides a resolver, so a\n // caller that doesn't care about stylesheets sees an options shape (and MJML\n // output) with no stylesheet key at all rather than an explicit `undefined`.\n ...(stylesheetResolver\n ? {\n getCustomBlockStylesheet: (customType: string) =>\n stylesheetResolver(customType),\n }\n : {}),\n ...(fonts\n ? {\n customFonts: fonts.customFonts,\n defaultFallbackFont: fonts.defaultFallback,\n }\n : {}),\n });\n}\n","import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from \"vue\";\nimport type {\n TemplateContent,\n TestEmailPayload,\n TestEmailProvider,\n} from \"@templatical/types\";\nimport { logger } from \"../utils/logger\";\nimport { tryLoadRenderer } from \"../utils/toMjml\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** How long the dialog shows its success confirmation before closing itself. */\nconst SUCCESS_DISMISS_MS = 1200;\n\n/**\n * Why a send failed.\n *\n * Split by origin so the dialog knows whether it may render the text directly.\n * A provider's message is already user-presentable and is passed through\n * verbatim; an editor-generated refusal carries no text, because only the\n * component has the active locale.\n */\nexport type TestEmailError =\n { kind: \"provider\"; message: string } | { kind: \"recipientNotAllowed\" };\n\n/**\n * Turn a thrown value into an `Error`. A `catch` clause's binding is\n * `unknown` — a rejected `send` and a throwing `onSent` handler both need a\n * real `Error` to report, and both paths below share this.\n */\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nexport interface UseTestEmailFeatureOptions {\n /** Sending backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: TestEmailProvider;\n /** The template to send. Read at send time, never snapshotted. */\n getContent: () => TemplateContent;\n /**\n * Render the current template to MJML. Supplied by the entry point, which\n * already owns the `ToMjmlSource` for `editor.toMjml()`, so this composable\n * never learns about custom-block resolvers or stylesheets.\n *\n * Only ever called when the provider set `includeMjml`.\n */\n renderMjml?: () => Promise<string>;\n onError?: (error: Error) => void;\n /**\n * Extra gate on top of the provider being present — Cloud adds its plan\n * entitlement and \"template must be saved\" here. Read reactively.\n */\n isAvailable?: () => boolean;\n}\n\nexport interface UseTestEmailFeatureReturn {\n isModalOpen: Ref<boolean>;\n open: () => void;\n close: () => void;\n isSending: Ref<boolean>;\n /** True for a moment after a successful send, while the dialog confirms. */\n justSent: Ref<boolean>;\n error: Ref<TestEmailError | null>;\n send: (recipient: string) => Promise<void>;\n /**\n * The provider's allowlist, or `undefined` when unrestricted.\n *\n * A computed rather than a snapshot: Cloud exposes `allowedRecipients` as a\n * getter over a value that arrives with its JWT, so reading it once at setup\n * would freeze it at `[]` forever.\n */\n allowedRecipients: ComputedRef<string[] | undefined>;\n defaultRecipient: ComputedRef<string | undefined>;\n isAvailable: ComputedRef<boolean>;\n capability: NonNullable<EditorCapabilities[\"testEmail\"]>;\n}\n\nexport function useTestEmailFeature(\n options: UseTestEmailFeatureOptions,\n): UseTestEmailFeatureReturn {\n const { provider } = options;\n\n const isModalOpen = ref(false);\n const isSending = ref(false);\n const justSent = ref(false);\n const error = ref<TestEmailError | null>(null);\n\n let dismissTimer: ReturnType<typeof setTimeout> | null = null;\n /** One warning per editor instance, not per send — a retry shouldn't re-log. */\n let warnedAboutRenderer = false;\n\n function clearDismissTimer(): void {\n if (dismissTimer !== null) {\n clearTimeout(dismissTimer);\n dismissTimer = null;\n }\n }\n\n // The timer outlives a fast unmount otherwise, and fires into dead refs.\n onScopeDispose(clearDismissTimer);\n\n // IMPORTANT: read `provider.allowedRecipients` *inside* the computed. Cloud's\n // adapter implements it as a getter over a reactive source that starts empty\n // and fills once auth resolves — destructuring it at setup would pin it to\n // `[]`, which (see `isAvailable`) hides the trigger permanently.\n const allowedRecipients = computed(() => provider.allowedRecipients);\n\n const defaultRecipient = computed(() => {\n const preferred = provider.defaultRecipient;\n if (preferred === undefined) return undefined;\n const allowed = allowedRecipients.value;\n // A default outside the allowlist is ignored rather than smuggled in.\n if (allowed && !allowed.includes(preferred)) return undefined;\n return preferred;\n });\n\n const isAvailable = computed(() => {\n // An explicitly empty allowlist means nobody may be sent to, so the feature\n // is unusable and must not advertise itself. Distinct from `undefined`,\n // which means \"unrestricted\".\n if (allowedRecipients.value?.length === 0) return false;\n return options.isAvailable?.() ?? true;\n });\n\n function open(): void {\n if (!isAvailable.value) return;\n // Clear both, or a reopen shows the previous attempt's error or a stale tick\n // of the success state.\n error.value = null;\n justSent.value = false;\n clearDismissTimer();\n isModalOpen.value = true;\n }\n\n function close(): void {\n clearDismissTimer();\n isModalOpen.value = false;\n justSent.value = false;\n }\n\n /**\n * Build `payload.mjml` when the provider opted in.\n *\n * Returns `undefined` for the two non-fatal cases (not opted in; opted in but\n * the renderer isn't installed) and **throws** when rendering itself fails —\n * that means the template is broken, and silently sending JSON-only would hide\n * a real defect indefinitely.\n */\n async function resolveMjml(): Promise<string | undefined> {\n if (!provider.includeMjml || !options.renderMjml) return undefined;\n\n try {\n return await options.renderMjml();\n } catch (err) {\n // Probe only on the failure path, so the happy path pays nothing. A `null`\n // here means the package is genuinely absent; anything else is a render\n // error and belongs to the caller.\n if ((await tryLoadRenderer()) === null) {\n if (!warnedAboutRenderer) {\n warnedAboutRenderer = true;\n logger.warn(\n \"testEmail.includeMjml is set but @templatical/renderer is not installed — \" +\n \"sending the template as JSON only. Install it to receive `payload.mjml`.\",\n );\n }\n return undefined;\n }\n throw err;\n }\n }\n\n /**\n * Report a completed send to the provider's `onSent`, guarded so a throw\n * cannot reach `send`'s own try/catch — by the time this runs, `send` has\n * already succeeded and the dialog is already showing success.\n *\n * Mirrors `@templatical/core`'s `notifyHandler`, which this composable\n * cannot import: `error-reporting.ts` is not part of `@templatical/core`'s\n * package exports.\n */\n function notifySent(payload: TestEmailPayload): void {\n try {\n // Read off `provider` at call time, never destructured — the same\n // discipline `allowedRecipients`/`defaultRecipient` apply above, so a\n // handler that arrives or changes after setup is never missed.\n provider.onSent?.(payload);\n } catch (err) {\n options.onError?.(toError(err));\n }\n }\n\n async function send(recipient: string): Promise<void> {\n if (isSending.value) return;\n\n const allowed = allowedRecipients.value;\n if (allowed && !allowed.includes(recipient)) {\n // Unreachable through the picker, reachable by a programmatic open or a\n // tampered field. Refusing keeps a guarantee worth having: the editor\n // never calls `send` with an address outside a configured allowlist.\n error.value = { kind: \"recipientNotAllowed\" };\n return;\n }\n\n isSending.value = true;\n error.value = null;\n\n try {\n const mjml = await resolveMjml();\n const payload: TestEmailPayload = {\n recipient,\n content: options.getContent(),\n ...(mjml !== undefined ? { mjml } : {}),\n ...(allowed !== undefined ? { allowedRecipients: allowed } : {}),\n };\n await provider.send(payload);\n\n justSent.value = true;\n dismissTimer = setTimeout(close, SUCCESS_DISMISS_MS);\n notifySent(payload);\n } catch (err) {\n const wrapped = toError(err);\n error.value = { kind: \"provider\", message: wrapped.message };\n options.onError?.(wrapped);\n // Deliberately not re-thrown: the dialog surfaces `error` and stays open\n // for a retry, and nothing awaits this beyond the click handler.\n } finally {\n isSending.value = false;\n }\n }\n\n const capability: NonNullable<EditorCapabilities[\"testEmail\"]> = {\n open,\n isAvailable,\n };\n\n return {\n isModalOpen,\n open,\n close,\n isSending,\n justSent,\n error,\n send,\n allowedRecipients,\n defaultRecipient,\n isAvailable,\n capability,\n };\n}\n","import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from \"vue\";\nimport {\n useVersionHistory,\n type UseVersionHistoryReturn,\n} from \"@templatical/core\";\nimport type {\n UseAutoSaveReturn,\n UseConditionPreviewReturn,\n UseHistoryReturn,\n} from \"@templatical/core\";\nimport type {\n TemplateContent,\n TemplateVersion,\n VersionHistoryProvider,\n} from \"@templatical/types\";\nimport { safeClone } from \"@templatical/types\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** Minimal slice of the editor this feature needs — OSS and Cloud alike. */\ninterface VersionHistoryEditor {\n state: {\n readonly template?: { id: string } | null;\n readonly isDirty: boolean;\n };\n content: Ref<TemplateContent>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n}\n\n/**\n * How the restore confirmation persists unsaved work instead of only warning\n * about it.\n *\n * Both members are read at confirmation time rather than captured at setup, so\n * a late-bound save (Cloud's, which only exists once its lifecycle composable\n * is constructed) and a save that comes and goes (Cloud's lint gate) are both\n * expressible.\n */\nexport interface SaveBeforeRestore {\n /** Whether persisting is possible *right now*. */\n canSave: () => boolean;\n /** Persist the template. Rejects on failure; the confirmation stays up. */\n save: () => Promise<unknown>;\n}\n\nexport interface UseVersionHistoryFeatureOptions {\n /** Storage backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: VersionHistoryProvider;\n editor: VersionHistoryEditor;\n /**\n * Lets the restore confirmation offer \"save first\". Omit it — or supply one\n * whose `canSave` is false, which is what a `save: false` templates provider\n * amounts to — and the confirmation degrades to a plain \"these will be lost\",\n * because there is then nowhere to put them.\n */\n saveBeforeRestore?: SaveBeforeRestore | null;\n history: UseHistoryReturn;\n conditionPreview: UseConditionPreviewReturn;\n /** Paused for the duration of a preview, so previewed content is never saved. */\n autoSave: UseAutoSaveReturn | null;\n onError?: (error: Error) => void;\n /**\n * Whether the feature may be used. Read reactively, so Cloud can defer to a\n * plan entitlement that resolves after its async config fetch. Omit for\n * always-available (the OSS case: a configured provider is the whole gate).\n */\n isAvailable?: () => boolean;\n}\n\nexport interface UseVersionHistoryFeatureReturn {\n headless: UseVersionHistoryReturn;\n\n versions: ComputedRef<TemplateVersion[]>;\n isLoading: ComputedRef<boolean>;\n isRestoring: ComputedRef<boolean>;\n canCreate: ComputedRef<boolean>;\n canRestore: ComputedRef<boolean>;\n /** True once a template exists — there is no history before that. */\n hasTemplate: ComputedRef<boolean>;\n isAvailable: ComputedRef<boolean>;\n\n /** The version currently on the canvas, or `null` when editing normally. */\n previewingVersion: Ref<TemplateVersion | null>;\n isPreviewing: ComputedRef<boolean>;\n\n /** True while the unsaved-changes confirmation is up. */\n isConfirmingRestore: Ref<boolean>;\n /** Whether that confirmation may offer \"save first\" rather than only warn. */\n canSaveBeforeRestore: ComputedRef<boolean>;\n /**\n * True from the moment a confirmation action is taken until it settles —\n * covering the save as well as the restore, so a second click can't fire a\n * second save while the first is still in flight.\n */\n isConfirmBusy: ComputedRef<boolean>;\n\n /** Re-read the list. Called when the history control opens. */\n refresh: () => void;\n /**\n * Show a version on the canvas. **Synchronous once the preview is open and the\n * version's content is already in hand**, which is what keeps scrubbing\n * instant — see {@link TemplateVersion.content}.\n */\n navigate: (version: TemplateVersion) => Promise<void>;\n /**\n * What the banner's Restore button calls. Restores straight away when there is\n * nothing to lose; opens the confirmation when there are unsaved changes,\n * because {@link confirmRestore} discards the pre-preview backup and that work\n * would then exist nowhere.\n */\n requestRestore: () => Promise<void>;\n /** Confirmation's primary action: persist the unsaved work, then restore. */\n saveAndRestore: () => Promise<void>;\n /** Confirmation's destructive action: restore, losing the unsaved work. */\n discardAndRestore: () => Promise<void>;\n /** Dismiss the confirmation, staying in the preview with nothing restored. */\n cancelRestoreConfirm: () => void;\n /** Make the previewed version current. Rejects if the provider's restore does. */\n confirmRestore: () => Promise<void>;\n /** Leave the preview and put the pre-preview content back. */\n cancelPreview: () => void;\n\n capability: NonNullable<EditorCapabilities[\"versionHistory\"]>;\n}\n\n/**\n * Shared glue for version history: the reactive list, the preview-before-restore\n * flow, and the capability the shared chrome gates on.\n *\n * Both `Editor.vue` (OSS) and `useCloudInitialization` (Cloud) call this with\n * their own provider, so the two entry points run identical logic over different\n * transports. Nothing here is auth- or plan-aware — callers decide whether the\n * feature is available *before* constructing it.\n */\nexport function useVersionHistoryFeature(\n options: UseVersionHistoryFeatureOptions,\n): UseVersionHistoryFeatureReturn {\n const { provider, editor, history, conditionPreview, autoSave } = options;\n\n const headless = useVersionHistory({\n provider,\n getTemplateId: () => editor.state.template?.id ?? null,\n onError: options.onError,\n });\n\n const previewingVersion = ref<TemplateVersion | null>(null);\n const contentBeforePreview = ref<TemplateContent | null>(null);\n const isConfirmingRestore = ref(false);\n const isSavingBeforeRestore = ref(false);\n\n // Async functions must guard against post-unmount execution after every\n // `await` so we don't write to dead refs or fire side effects\n // (autoSave.pause, setContent) on a torn-down component.\n let destroyed = false;\n onScopeDispose(() => {\n destroyed = true;\n });\n\n const versions = computed(() => headless.versions.value);\n const isLoading = computed(() => headless.isLoading.value);\n const isRestoring = computed(() => headless.isRestoring.value);\n const canCreate = headless.canCreate;\n const canRestore = headless.canRestore;\n const hasTemplate = computed(() => !!editor.state.template?.id);\n const isAvailable = computed(() => options.isAvailable?.() ?? true);\n const isPreviewing = computed(() => previewingVersion.value !== null);\n const canSaveBeforeRestore = computed(\n () => options.saveBeforeRestore?.canSave() === true,\n );\n const isConfirmBusy = computed(\n () => isSavingBeforeRestore.value || isRestoring.value,\n );\n\n function refresh(): void {\n if (!hasTemplate.value) return;\n // Fire-and-forget: `useVersionHistory` already routes failures to `onError`,\n // and a stale list is not worth blocking the dropdown's open on.\n void headless.load().catch(() => {});\n }\n\n async function navigate(version: TemplateVersion): Promise<void> {\n if (destroyed) return;\n\n // A pending confirmation names the version it was raised for, so moving to\n // another one must retract it. Unreachable while the dialog is modal, but a\n // programmatic scrub reaches it today and an inline confirmation would.\n isConfirmingRestore.value = false;\n\n // Already previewing: this is a scrub. Take the synchronous path whenever\n // the content is in hand — a provider that hydrates its list (Cloud does)\n // never awaits here, and the canvas swaps in the same tick.\n if (previewingVersion.value) {\n const known = headless.peekContent(version);\n if (known) {\n previewingVersion.value = version;\n editor.setContent(known, false);\n return;\n }\n const content = await headless.resolveContent(version);\n if (destroyed) return;\n previewingVersion.value = version;\n editor.setContent(content, false);\n return;\n }\n\n // Entering the preview. Resolve first: a failed fetch must leave the user\n // editing, not stranded in a preview of nothing.\n const content = await headless.resolveContent(version);\n if (destroyed) return;\n\n // Opening a preview records nothing. The editor never authors a version —\n // whoever implements `TemplatesProvider.save` decides that. The unsaved work\n // that a confirmed restore would discard is protected at the confirmation\n // instead (see `requestRestore`), which is both where the loss happens and\n // the one place the user can be asked about it.\n //\n // `safeClone`, not `structuredClone`: what arrives here is a Vue reactive\n // proxy over the editor's content, and the structured-clone algorithm\n // refuses a proxy outright. It is also the repo's one deep-clone for\n // content, so a Sortable back-ref that leaked into block data is dropped\n // rather than thrown on.\n contentBeforePreview.value = safeClone(editor.content.value);\n\n autoSave?.pause();\n previewingVersion.value = version;\n editor.setContent(content, false);\n }\n\n async function requestRestore(): Promise<void> {\n if (!previewingVersion.value) return;\n\n // `setContent(…, false)` is what puts a version on the canvas, so `isDirty`\n // still describes the user's own work rather than the preview.\n if (editor.state.isDirty) {\n isConfirmingRestore.value = true;\n return;\n }\n await confirmRestore();\n }\n\n async function saveAndRestore(): Promise<void> {\n const version = previewingVersion.value;\n const before = contentBeforePreview.value;\n const saver = options.saveBeforeRestore;\n // The UI hides this action in each of these cases; a programmatic caller\n // gets a no-op rather than a save of the previewed version.\n if (!version || !before || !saver?.canSave()) return;\n if (isConfirmBusy.value) return;\n\n // The canvas is showing the *previewed* version and a save persists whatever\n // the editor holds, so put the user's work back before saving — otherwise\n // \"save first\" would save the very version they are about to restore.\n editor.setContent(before, false);\n isSavingBeforeRestore.value = true;\n try {\n await saver.save();\n } catch {\n // Already reported by whoever owns the save (status badge, `onError`).\n // Put the preview back so the banner and the canvas agree again, and leave\n // the confirmation up: nothing was saved and nothing was restored.\n if (destroyed) return;\n const previewed = headless.peekContent(version);\n if (previewed) editor.setContent(previewed, false);\n return;\n } finally {\n if (!destroyed) isSavingBeforeRestore.value = false;\n }\n if (destroyed) return;\n await confirmRestore();\n }\n\n async function discardAndRestore(): Promise<void> {\n // The dialog disables its buttons while busy; this covers a programmatic\n // caller, for whom a second restore mid-flight would append twice.\n if (isConfirmBusy.value) return;\n await confirmRestore();\n }\n\n function cancelRestoreConfirm(): void {\n isConfirmingRestore.value = false;\n }\n\n async function confirmRestore(): Promise<void> {\n if (!previewingVersion.value) return;\n\n try {\n const template = await headless.restore(previewingVersion.value.id);\n if (destroyed) return;\n applyRestored(template.content);\n // History is append-only: the restore added an entry, so the list the\n // dropdown shows is stale until it is re-read.\n await headless.load();\n if (destroyed) return;\n } catch (error) {\n // The canvas is still showing the previewed version (set in `navigate`),\n // so roll back to the pre-preview content before the `finally` discards\n // the backup — otherwise the user is silently left editing an un-restored\n // version with the banner gone, and the next autosave persists it.\n if (!destroyed && contentBeforePreview.value) {\n editor.setContent(contentBeforePreview.value, false);\n }\n throw error;\n } finally {\n if (!destroyed) {\n previewingVersion.value = null;\n contentBeforePreview.value = null;\n isConfirmingRestore.value = false;\n autoSave?.resume();\n }\n }\n }\n\n function applyRestored(content: TemplateContent): void {\n editor.setContent(content, false);\n history.clear();\n conditionPreview.reset();\n }\n\n function cancelPreview(): void {\n if (!previewingVersion.value || !contentBeforePreview.value) return;\n\n editor.setContent(contentBeforePreview.value, false);\n\n previewingVersion.value = null;\n contentBeforePreview.value = null;\n isConfirmingRestore.value = false;\n\n autoSave?.resume();\n }\n\n return {\n headless,\n\n versions,\n isLoading,\n isRestoring,\n canCreate,\n canRestore,\n hasTemplate,\n isAvailable,\n\n previewingVersion,\n isPreviewing,\n isConfirmingRestore,\n canSaveBeforeRestore,\n isConfirmBusy,\n\n refresh,\n navigate,\n requestRestore,\n saveAndRestore,\n discardAndRestore,\n cancelRestoreConfirm,\n confirmRestore,\n cancelPreview,\n\n capability: {\n refresh,\n isPreviewing,\n hasTemplate,\n isAvailable,\n canCreate,\n canRestore,\n },\n };\n}\n","/**\n * Whether the editor saves automatically after the user stops editing.\n *\n * The cadence is `changeDebounce` at the config root rather than a member here,\n * because one `useAutoSave` instance drives both the save and the `onChange`\n * notification — and `onChange` works with no templates provider, so a\n * provider-less consumer still needs to set it. `autoSave: false` with a\n * `changeDebounce` is therefore a real setting, not a dead one: it means notify\n * at that cadence and persist nothing.\n */\nexport type AutoSaveConfig = boolean;\n\n/**\n * Normalise an {@link AutoSaveConfig}.\n *\n * The one call site is `Editor.vue`'s `autoSaveEnabled`, which always passes\n * `false` as `defaultEnabled` — a consumer without a `templates` provider has\n * nothing to save to. `initCloud()` inlines its own default instead of\n * calling this (`config.templates?.autoSave ?? true`, in `index.ts`), since\n * Cloud always has a store to save to.\n */\nexport function resolveAutoSave(\n value: AutoSaveConfig | undefined,\n defaultEnabled: boolean,\n): boolean {\n return value ?? defaultEnabled;\n}\n","import type { LintOptions } from \"@templatical/quality\";\n\n/**\n * Build the `LintOptions` object the editor passes to the lint composable.\n *\n * In editor / cloud-editor mode the linter locale is forced to match the\n * editor UI locale — `lint.locale` set by the consumer is ignored.\n * Headless callers (`lintAccessibility(...)` / `lintStructure(...)` directly)\n * keep full control.\n */\nexport function resolveLintOptions(config: {\n locale?: string;\n lint?: LintOptions;\n}): LintOptions {\n return {\n ...config.lint,\n locale: config.locale,\n };\n}\n","import type {\n CustomBlock,\n CustomFont,\n RenderPayload,\n RenderProvider,\n TemplateContent,\n} from \"@templatical/types\";\nimport { safeClone } from \"@templatical/types\";\nimport { preRenderCustomBlocks } from \"./preRenderCustomBlocks\";\n\n/** The fonts half of a {@link RenderPayload}, as the editor resolves it. */\nexport interface RenderFonts {\n customFonts: CustomFont[];\n defaultFallback: string;\n}\n\n/** Minimal slice of the fonts manager the payload needs. */\nexport interface RenderFontsSource {\n customFonts: { value: CustomFont[] };\n defaultFallback: { value: string };\n}\n\n/**\n * The fonts the editor is *actually* rendering with.\n *\n * The canvas's font set and the payload's are the same set *by construction*\n * rather than by agreement: nothing can switch custom faces off for a plan, so\n * there is no second source of truth to keep in step.\n */\nexport function resolveRenderFonts(source: RenderFontsSource): RenderFonts {\n return {\n customFonts: source.customFonts.value,\n defaultFallback: source.defaultFallback.value,\n };\n}\n\n/** What {@link buildRenderPayload} needs from an editor instance. */\nexport interface RenderPayloadSource {\n getContent: () => TemplateContent;\n renderCustomBlock: (block: CustomBlock) => Promise<string>;\n getFonts: () => RenderFonts;\n}\n\n/**\n * Assemble the render-complete payload a {@link RenderProvider} is promised.\n *\n * \"Render-complete\" is the deal that lets the provider win over the bundled\n * renderer: local rendering sees custom blocks and fonts natively, a backend\n * doesn't, so the editor resolves both up front instead of each backend\n * rediscovering the problem.\n *\n * The content is a **defensive copy** (`safeClone`, same as `getContent()`, since\n * a drag inside a section can leave a Sortable expando cycle reachable from live\n * content). Pre-rendering writes `renderedHtml` onto blocks, and doing that to the\n * live tree would mark the editor dirty and make an export mutate the document.\n */\nexport async function buildRenderPayload(\n source: RenderPayloadSource,\n): Promise<RenderPayload> {\n const content = safeClone(source.getContent());\n await preRenderCustomBlocks(content, {\n renderCustomBlock: source.renderCustomBlock,\n });\n\n return { content, fonts: source.getFonts() };\n}\n\n/** What {@link createRenderMethods} needs to resolve each method. */\nexport interface RenderMethodsSource {\n /** The consumer's backend, when `render` was configured. */\n provider?: RenderProvider | null;\n buildPayload: () => Promise<RenderPayload>;\n /**\n * Render MJML with the bundled `@templatical/renderer`. Expected to reject with\n * the \"install @templatical/renderer\" error when the optional peer is absent —\n * that rejection *is* the `toMjml()` chain's final `throw`.\n */\n renderLocalMjml: () => Promise<string>;\n}\n\nexport interface RenderMethods {\n toMjml(): Promise<string>;\n toHtml(): Promise<string>;\n}\n\n/**\n * `editor.toMjml()` / `editor.toHtml()`, resolved **per method rather than per\n * provider** — a provider that implements one of the three methods is not thereby\n * claiming the others.\n *\n * | Call | Order |\n * |---|---|\n * | `toMjml()` | `provider.toMjml` → the bundled renderer → throw |\n * | `toHtml()` | `provider.toHtml` → `toMjml()` + `provider.compileMjml` → throw |\n *\n * `toHtml()` composes through `toMjml()` rather than forcing the local renderer,\n * so a provider whose MJML is authoritative (Cloud's is — it renders block types\n * a browser cannot) is not silently bypassed on the way to HTML. When the provider\n * only supplies `compileMjml`, that resolves to local MJML anyway, which is the\n * cheap tier working exactly as intended.\n *\n * **There is no local HTML path, ever.** The SDK deliberately does not bundle an\n * MJML compiler, so the last step is a rejection with instructions rather than a\n * guess.\n */\nexport function createRenderMethods(\n source: RenderMethodsSource,\n): RenderMethods {\n async function toMjml(): Promise<string> {\n const provider = source.provider;\n // Bound rather than narrowed in place: the call sits behind an `await`, and a\n // bound reference cannot be re-read as undefined afterwards.\n const providerToMjml = provider?.toMjml?.bind(provider);\n if (providerToMjml) {\n return providerToMjml(await source.buildPayload());\n }\n return source.renderLocalMjml();\n }\n\n async function toHtml(): Promise<string> {\n const provider = source.provider;\n\n const providerToHtml = provider?.toHtml?.bind(provider);\n if (providerToHtml) {\n return providerToHtml(await source.buildPayload());\n }\n\n const compileMjml = provider?.compileMjml?.bind(provider);\n if (compileMjml) {\n return compileMjml(await toMjml());\n }\n\n throw new Error(\n \"[Templatical] toHtml() requires a `render` provider implementing either \" +\n \"`toHtml` or `compileMjml`. The SDK does not bundle an MJML compiler, so \" +\n \"there is no local HTML path — point `compileMjml` at any mjml2html \" +\n \"endpoint (a hosted service, a container, or a CLI shell-out).\",\n );\n }\n\n return { toMjml, toHtml };\n}\n","import type { Ref } from \"vue\";\nimport type {\n Block,\n MergeTag,\n MergeTagsConfig,\n SectionBlock,\n SyntaxPreset,\n TemplateContent,\n TemplatesProvider,\n} from \"@templatical/types\";\nimport {\n containsMergeTag,\n getLogicMergeTagKeyword,\n getMergeTagLabel,\n resolveSyntax,\n} from \"@templatical/types\";\n\n/**\n * Convert bare merge-tag tokens in rich-text HTML into the `<span\n * data-merge-tag>` nodes the editor understands.\n *\n * A tag reaches stored content in one of two physical shapes. Anything a user\n * types or pastes is already a node, because `MergeTagNode`'s input and paste\n * rules convert it on the spot. What stays a bare token is content that never\n * passed through that pipeline — `init({ content })`, `setContent()`, a\n * template loaded from a store, or one produced by the `@templatical/import-*`\n * converters. A bare token renders as literal text: no label, no highlight, no\n * `sample` substitution, and it cannot be selected or deleted as a unit.\n *\n * Three properties of this implementation are load-bearing:\n *\n * 1. **It walks text nodes, never markup.** A token in attribute position —\n * `<a href=\"{{unsubscribe_url}}\">` is the common one — must be left exactly\n * as it is; wrapping it injects an element into an attribute value and\n * corrupts the document. A `TreeWalker` over text nodes cannot reach an\n * attribute at all, so this holds by construction rather than by guard.\n * A regex cannot do this job. Guarding with a `(?<!data-merge-tag=\")`\n * lookbehind protects the attribute of an already-converted span and\n * nothing else, so `<a href=\"{{unsubscribe_url}}\">` becomes an anchor whose\n * href contains a `<span>`. Telling text position from attribute position\n * needs parsing; no amount of lookbehind substitutes for it.\n *\n * 2. **It is idempotent structurally.** The walk rejects the whole subtree of\n * any element already carrying `data-merge-tag` / `data-logic-merge-tag`,\n * so an existing span's inner text is unreachable no matter what it says.\n * That covers the case where a tag's label *is* its value.\n *\n * 3. **It never builds markup by concatenation.** A consumer may configure a\n * syntax whose delimiters contain `<` / `>` — Smarty-style `<% $email %>` —\n * so a template string would emit an attribute no scanner could re-read.\n * Spans are created as elements and filled via `setAttribute` /\n * `textContent`, which leaves escaping to the serializer.\n *\n * Matching is driven by `syntax`, not by the declared tag list, so a template\n * migrated from another ESP has all of its tags made atomic rather than only\n * the handful the consumer has declared so far. An undeclared token falls back\n * to showing its raw value as the label, which `getMergeTagLabel` already does.\n * This mirrors typing: the input and paste rules are registered unconditionally\n * and match on syntax alone.\n */\n\ninterface TagHit {\n start: number;\n end: number;\n attr: string;\n value: string;\n label: string;\n}\n\nfunction globalRegex(pattern: RegExp): RegExp {\n const flags = pattern.flags.includes(\"g\")\n ? pattern.flags\n : `${pattern.flags}g`;\n return new RegExp(pattern.source, flags);\n}\n\nfunction collectHits(\n text: string,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): TagHit[] {\n const hits: TagHit[] = [];\n\n const scan = (\n pattern: RegExp,\n attr: string,\n label: (match: string) => string,\n ): void => {\n const regex = globalRegex(pattern);\n let match = regex.exec(text);\n while (match !== null) {\n const start = match.index;\n const end = start + match[0].length;\n // A zero-length match would spin `exec` forever on the same index.\n if (match[0].length === 0) {\n regex.lastIndex++;\n } else if (!hits.some((hit) => start < hit.end && end > hit.start)) {\n hits.push({\n start,\n end,\n attr,\n value: match[0],\n label: label(match[0]),\n });\n }\n match = regex.exec(text);\n }\n };\n\n // Logic first, so it wins any overlap. Some syntaxes' value regex is liberal\n // enough to also match a logic tag — handlebars' `{{.+?}}` swallows\n // `{{#each items}}` — and `isMergeTagValue` resolves that same way.\n scan(syntax.logic, \"data-logic-merge-tag\", (value) =>\n getLogicMergeTagKeyword(value, syntax),\n );\n scan(syntax.value, \"data-merge-tag\", (value) =>\n getMergeTagLabel(value, mergeTags),\n );\n\n return hits.sort((a, b) => a.start - b.start);\n}\n\n/** Text nodes not already inside a merge-tag span, in document order. */\nfunction collectNormalizableTextNodes(doc: Document): Text[] {\n const walker = doc.createTreeWalker(\n doc.body,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,\n {\n acceptNode(node: Node): number {\n if (node.nodeType !== 1) return NodeFilter.FILTER_ACCEPT;\n const el = node as Element;\n // REJECT (not SKIP) prunes the subtree, which is what makes a second\n // pass unable to see an existing span's inner text.\n return el.hasAttribute(\"data-merge-tag\") ||\n el.hasAttribute(\"data-logic-merge-tag\")\n ? NodeFilter.FILTER_REJECT\n : NodeFilter.FILTER_SKIP;\n },\n },\n );\n\n // Collected up front: replacing a node mid-walk invalidates the walker.\n const nodes: Text[] = [];\n let current = walker.nextNode();\n while (current !== null) {\n nodes.push(current as Text);\n current = walker.nextNode();\n }\n return nodes;\n}\n\nexport function normalizeMergeTagsInHtml(\n html: string,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): string {\n if (!html) return html;\n // Cheap reject before paying for a parse. A token that exists only in an\n // attribute still gets past this, and is then caught by the zero-replacement\n // short-circuit below.\n if (!containsMergeTag(html, syntax)) return html;\n if (typeof DOMParser === \"undefined\") return html;\n\n const doc = new DOMParser().parseFromString(\n `<!doctype html><body>${html}</body>`,\n \"text/html\",\n );\n\n let replacements = 0;\n for (const node of collectNormalizableTextNodes(doc)) {\n const text = node.data;\n const hits = collectHits(text, mergeTags, syntax);\n if (hits.length === 0) continue;\n\n const fragment = doc.createDocumentFragment();\n let cursor = 0;\n for (const hit of hits) {\n if (hit.start > cursor) {\n fragment.appendChild(\n doc.createTextNode(text.substring(cursor, hit.start)),\n );\n }\n const span = doc.createElement(\"span\");\n span.setAttribute(hit.attr, hit.value);\n span.textContent = hit.label;\n fragment.appendChild(span);\n cursor = hit.end;\n }\n if (cursor < text.length) {\n fragment.appendChild(doc.createTextNode(text.substring(cursor)));\n }\n\n node.parentNode?.replaceChild(fragment, node);\n replacements += hits.length;\n }\n\n // Nothing matched in text position, so return the input verbatim rather than\n // a re-serialization of it — otherwise content with no tags at all would be\n // rewritten (`<br/>` → `<br>`, attribute quoting, entity encoding) for no\n // reason. Where a replacement does happen some churn is unavoidable, and the\n // renderer accepts both shapes.\n if (replacements === 0) return html;\n\n return doc.body.innerHTML;\n}\n\n/**\n * The rich-text fields, and only those. Every other merge-tag-bearing field —\n * `ButtonBlock.text`/`url`, `ImageBlock.alt`, `HtmlBlock.content`, custom-block\n * `fieldValues`, `settings.preheaderText` — is a plain string rendered as text,\n * so a span written into one would be displayed literally on the canvas and\n * emitted into a `url=` attribute by the renderer.\n *\n * `TableCellData.content` is span-bearing to the renderer but is deliberately\n * excluded: `TableBlock.vue` hydrates a cell with `textContent` and writes\n * `innerText` back on blur, so the first focus-and-leave on a normalized cell\n * would persist the literal markup into stored content as text. The\n * editor/renderer asymmetry there is a separate, pre-existing gap.\n */\nfunction normalizeBlock(\n block: Block,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): Block {\n if (block.type === \"section\") {\n if (!Array.isArray(block.children)) return block;\n let changed = false;\n const children = block.children.map((column) => {\n if (!Array.isArray(column)) return column;\n let columnChanged = false;\n const next = column.map((child) => {\n const normalized = normalizeBlock(child, mergeTags, syntax);\n if (normalized !== child) columnChanged = true;\n return normalized;\n });\n if (!columnChanged) return column;\n changed = true;\n return next;\n });\n return changed ? ({ ...block, children } as SectionBlock) : block;\n }\n\n if (block.type !== \"title\" && block.type !== \"paragraph\") return block;\n if (typeof block.content !== \"string\") return block;\n\n const content = normalizeMergeTagsInHtml(block.content, mergeTags, syntax);\n return content === block.content ? block : { ...block, content };\n}\n\n/**\n * Normalize every rich-text field in a template. Returns the input reference\n * unchanged when nothing matched, so a template with no bare tokens costs no\n * allocation and keeps its object identity; otherwise returns a copy, leaving\n * the caller's content untouched.\n *\n * Shape is checked rather than trusted at every level. `TemplateContent` types\n * `blocks` as required and `SectionBlock.children` as a `Block[][]`, but this\n * runs at the SDK's entry points on an object the consumer hands us — a partial\n * or hand-rolled template must come back untouched, not turn a working `init()`\n * into a `TypeError`.\n */\nexport function normalizeMergeTagMarkup(\n content: TemplateContent,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): TemplateContent {\n if (!content || !Array.isArray(content.blocks)) return content;\n\n let changed = false;\n const blocks = content.blocks.map((block) => {\n const normalized = normalizeBlock(block, mergeTags, syntax);\n if (normalized !== block) changed = true;\n return normalized;\n });\n\n return changed ? { ...content, blocks } : content;\n}\n\n/**\n * `normalizeMergeTagMarkup` bound to the editor's own config shape, so the hook\n * sites don't each resolve tags and syntax themselves.\n *\n * An absent `mergeTags` still normalizes, under the liquid default. That is not\n * an oversight: `MergeTagNode`'s input and paste rules are registered\n * unconditionally with the same default, so a consumer who configured nothing\n * already gets a node the instant a user types `{{x}}`. Loading the same text\n * and leaving it inert would be the inconsistency.\n */\nexport function normalizeContentForConfig(\n content: TemplateContent,\n mergeTags: MergeTagsConfig | undefined,\n): TemplateContent {\n return normalizeMergeTagMarkup(\n content,\n mergeTags?.tags ?? [],\n resolveSyntax(mergeTags?.syntax),\n );\n}\n\n/**\n * Wrap a `TemplatesProvider` so content arriving from the store is normalized\n * before the editor ever sees it.\n *\n * Only `load` is wrapped. `save` sends content *out*, and what it sends is\n * already-normalized editor state; rewriting it here would change what the\n * store keeps for no benefit. `create` likewise carries content outward, and\n * the template it returns supplies identity and timestamps rather than the\n * content the editor goes on to edit.\n *\n * Wrapping the provider — rather than normalizing after `load()` resolves —\n * is what keeps Q4 true: core assigns already-normalized content to its state\n * and observes no mutation, so there is no dirty flag to suppress and no\n * autosave tick to swallow.\n */\n/**\n * The slice of the editor a content-restoring feature consumes. Structural on\n * purpose: it matches `useVersionHistoryFeature`'s own `VersionHistoryEditor`\n * without importing it, so this util keeps no dependency on a composable.\n */\ninterface ContentWritingEditor {\n state: {\n readonly template?: { id: string } | null;\n readonly isDirty: boolean;\n };\n content: Ref<TemplateContent>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n}\n\n/**\n * Wrap an editor so content written through `setContent` is normalized first.\n *\n * This is how version history is covered. Its content reaches the canvas by\n * three routes — `version.content` off a hydrated list, a `fetched` cache, and\n * `provider.get()` — so wrapping the *provider*, the way the `templates` path\n * does, would miss two of them. `setContent` is the one chokepoint they share.\n *\n * A version is not guaranteed to hold normalized content just because a save\n * produced it: a store carries whatever was written to it, and a backend may\n * version a template it imported rather than one the editor round-tripped.\n * Previewing such a version without this would put bare tokens back on a canvas\n * where every other tag is a chip.\n *\n * `markDirty` is forwarded untouched: every version-history write passes\n * `false`, and swallowing that would turn opening a preview into an unsaved\n * change. `setContent` is resolved per call rather than captured, because\n * collaboration replaces the editor's mutators with broadcasting versions\n * before this feature is built.\n */\nexport function withNormalizedContentWrites<T extends ContentWritingEditor>(\n editor: T,\n mergeTags: MergeTagsConfig | undefined,\n): ContentWritingEditor {\n return {\n get state() {\n return editor.state;\n },\n get content() {\n return editor.content;\n },\n setContent: (content: TemplateContent, markDirty?: boolean) =>\n editor.setContent(\n normalizeContentForConfig(content, mergeTags),\n markDirty,\n ),\n };\n}\n\nexport function withNormalizedTemplateLoads(\n provider: TemplatesProvider,\n mergeTags: MergeTagsConfig | undefined,\n): TemplatesProvider {\n return {\n ...provider,\n async load(id: string) {\n const template = await provider.load(id);\n const content = normalizeContentForConfig(template.content, mergeTags);\n return content === template.content ? template : { ...template, content };\n },\n };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useCloudI18n } from \"../composables/useCloudI18n\";\nimport type {\n Block,\n Collaborator,\n CustomBlock as CustomBlockType,\n TemplateContent,\n ViewportSize,\n} from \"@templatical/types\";\nimport { ImageUp, Sparkles, SquarePlus } from \"@lucide/vue\";\nimport { computed, inject, provide, ref, type Component } from \"vue\";\nimport {\n EDITOR_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n BLOCK_REGISTRY_KEY,\n CAPABILITIES_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n PREVIEW_RESOLUTION_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n requireInject,\n} from \"../keys\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport {\n getDocumentStyle,\n resolveBlockComponent,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n EMAIL_GUTTER,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport { readableTextColor } from \"../utils/readableTextColor\";\n\nimport BlockWrapper from \"./blocks/BlockWrapper.vue\";\nimport SectionBlock from \"./blocks/SectionBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\n\n// `countdown` is deliberately absent: it resolves from the block registry, which\n// `useEditorCore` populates with a lazy `defineAsyncComponent` so OSS bundles\n// don't carry a Cloud-only block. `resolveBlockComponent` checks the registry\n// first, so a static entry here would never be reached — but its static import\n// would still pull the module into the eager graph and defeat the laziness.\n// The other fallback maps (SectionBlock, BlockPreviewCanvas,\n// PreviewSectionBlock) omit it for the same reason.\nconst blockComponentMap: Record<string, Component> = {\n section: SectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n video: VideoBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n viewport: ViewportSize;\n content: TemplateContent;\n selectedBlockId: string | null;\n darkMode: boolean;\n previewMode: boolean;\n lockedBlocks?: Map<string, Collaborator>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select-block\", blockId: string | null): void;\n (e: \"open-ai-chat\"): void;\n (e: \"open-design-reference\"): void;\n}>();\n\nconst { t } = useI18n();\nconst { t: cloudT } = useCloudI18n();\n\nconst editor = requireInject(EDITOR_KEY, \"Canvas\");\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n/**\n * Merge-tag samples render on this canvas **only while preview mode is on**.\n * The `previewMode &&` is the load-bearing half: the canvas is the editing\n * surface, and substituted text in a block a user is about to edit would be\n * text they never wrote. Preview mode is already fully non-editing (clicks and\n * dragging are blocked above), so it is the one state where swapping the\n * displayed value is safe.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Null in headless mounts and when no resolver is configured.\nconst previewResolution = inject(PREVIEW_RESOLUTION_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(\n () =>\n props.previewMode &&\n (mergeTagSampleMode?.value ?? false) &&\n // A configured resolver owns what the preview shows. Substituting samples\n // underneath it would mix invented values with real ones, and on the\n // failure path would contradict the note saying the *unresolved* template\n // is showing.\n !previewResolution?.isConfigured,\n ),\n);\n\nconst canUseAiChat = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"chat\") ?? false),\n);\nconst canUseDesignToTemplate = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"designToTemplate\") ?? false),\n);\n\n/**\n * Whether the hand-toggled display-condition filter applies here. Owned by\n * `useEditorCore` and injected rather than re-derived — see the key's comment.\n */\nconst appliesConditionFilter = inject(APPLIES_CONDITION_FILTER_KEY, null);\n\nconst blocks = computed({\n get: () => props.content.blocks,\n set: (value: Block[]) => {\n editor.setContent({\n ...props.content,\n blocks: value,\n });\n },\n});\n\n// Shared with the preview canvas and the save dialog's scaled rows, so all\n// three lay out against the same width.\nconst viewportWidth = computed(() =>\n getEmailFrameWidth(props.content.settings, props.viewport),\n);\n\n// The \"stage\" represents the email body: the content column plus a gutter of\n// email background on each side (#230 — without it a full-width section's own\n// background hides the global one entirely). Mirrors how `mj-body\n// background-color` renders in the gutters around the centered content when\n// the email is sent. The neutral work area beyond the stage is preserved so\n// editor chrome stays legible.\n//\n// `EMAIL_GUTTER` comes from the shared helper rather than a local constant: the\n// preview surfaces show the same band, and two copies drift into two different\n// answers for how much of the body background an email is supposed to show.\nconst stageWidth = computed(() => viewportWidth.value + EMAIL_GUTTER * 2);\n\n// Canvas dark mode preview: simulates how the email will appear in recipients'\n// dark-themed email clients. Uses CSS filter inversion — independent of the\n// editor UI theme (light/dark/auto) which is controlled via uiTheme config.\n// The email bg lives on the `.tpl-canvas-bg` sibling layer (so it can be\n// inverted by `filter` without trapping block chrome in a stacking context);\n// the `.tpl-canvas` itself stays transparent so the bg layer shows through.\n// Shared with every other surface that renders blocks (the saved-block\n// previews), so a block looks the same wherever it is drawn.\nconst canvasStyle = computed(() => getDocumentStyle(props.content.settings));\n\n// Empty canvas: the whole dashed placeholder IS the Sortable drop zone.\n// `isEmptyCanvas` toggles the styling + the inline empty-state content.\nconst isEmptyCanvas = computed(\n () => blocks.value.length === 0 && !props.previewMode,\n);\n\n// Highlight the empty drop zone while a block is being dragged over it.\n// Uses a counter to handle dragenter/leave bubbling from descendants —\n// raw event toggling would flicker as the pointer moves between children.\nconst dragEnterDepth = ref(0);\nconst isDragOverEmpty = computed(\n () => isEmptyCanvas.value && dragEnterDepth.value > 0,\n);\n\nfunction handleEmptyDragEnter(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value += 1;\n}\n\nfunction handleEmptyDragLeave(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value = Math.max(0, dragEnterDepth.value - 1);\n}\n\nfunction handleEmptyDrop(): void {\n dragEnterDepth.value = 0;\n}\n\nfunction handleCanvasClick(event: MouseEvent): void {\n if (props.previewMode) {\n return;\n }\n // During a pick session, a stray background click must not clear anything —\n // the session is left only via the bar's Save/Cancel or Escape. Deselecting\n // here would also be meaningless, since picking doesn't touch selection.\n if (caps.savedBlocks?.isPicking.value) {\n return;\n }\n if (event.target === event.currentTarget) {\n emit(\"select-block\", null);\n }\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, blockComponentMap);\n}\n\nfunction getBlockLock(blockId: string): Collaborator | null {\n return props.lockedBlocks?.get(blockId) ?? null;\n}\n\n/**\n * A click on a block either toggles its pick (while a saved-blocks pick\n * session is running) or selects it. Preview mode and collaborator locks are\n * checked first, so a locked block can be neither selected nor picked.\n */\nfunction handleBlockSelect(blockId: string): void {\n if (props.previewMode || getBlockLock(blockId)) return;\n const savedBlocks = caps.savedBlocks;\n if (savedBlocks?.isPicking.value) {\n savedBlocks.togglePick(blockId);\n return;\n }\n emit(\"select-block\", blockId);\n}\n\nfunction isPickedBlock(blockId: string): boolean {\n return caps.savedBlocks?.isPicked(blockId) ?? false;\n}\n\n/** True while a pick session runs — swaps block chrome into picking behaviour. */\nconst isPicking = computed(() => caps.savedBlocks?.isPicking.value === true);\n\nfunction handleFetchData(\n block: Block,\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n): void {\n if (block.type !== \"custom\") {\n return;\n }\n\n editor.updateBlock(block.id, {\n fieldValues: payload.fieldValues,\n dataSourceFetched: payload.dataSourceFetched,\n } as Partial<CustomBlockType>);\n}\n</script>\n\n<template>\n <!-- STAGE — the email body. Sized to the content column plus a gutter of\n email background on each side, so the global background stays visible\n around full-width sections, mirroring how `mj-body background-color`\n renders in the gutters when sent (#230). The neutral work area beyond\n the stage is preserved so the canvas still reads as a floating card and\n editor chrome stays legible. -->\n <div\n class=\"tpl-canvas-stage tpl:relative tpl:flex tpl:justify-center tpl:rounded-lg\"\n :style=\"{\n width: `${stageWidth}px`,\n boxShadow: darkMode ? 'none' : 'var(--tpl-shadow-xl)',\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <!-- Background layer — holds the email bg and the dark-preview filter,\n spanning the full stage (content + gutters). Filter on this leaf\n layer (no descendants) does not create a containing block / stacking\n context that would trap block chrome (action bar, indicators) inside\n the wrapper. The filter has its own transition so the canvas fades\n smoothly between modes. -->\n <div\n class=\"tpl-canvas-bg tpl:absolute tpl:inset-0 tpl:rounded-lg tpl:pointer-events-none\"\n :style=\"{\n backgroundColor: content.settings.backgroundColor,\n ...(darkMode ? { filter: 'invert(1) hue-rotate(180deg)' } : {}),\n transition: 'filter 300ms ease',\n }\"\n />\n <!-- CONTENT COLUMN — the email content at its true width, centered in the\n stage. A full-width section fills this column but not the surrounding\n gutters, so the global background remains visible beside it. -->\n <div\n data-testid=\"canvas-wrapper\"\n role=\"region\"\n :aria-label=\"t.landmarks.canvas\"\n class=\"tpl-canvas-wrapper tpl:relative\"\n :style=\"{\n width: `${viewportWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <div\n class=\"tpl-canvas tpl:relative tpl:rounded-lg\"\n :class=\"{\n 'tpl-canvas--dark-mode': darkMode,\n 'tpl-preview-mode': previewMode,\n }\"\n :style=\"canvasStyle\"\n @click=\"handleCanvasClick\"\n >\n <!-- Preview resolution. The skeleton shows only on a *first* resolve —\n `isInitialResolve` — so switching recipient or reopening keeps the\n last good preview on screen instead of flashing over it. -->\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 tpl:p-6\"\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 still renders below, so\n the preview degrades rather than disappearing. -->\n <p\n v-else-if=\"previewResolution?.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mx-6 tpl:mt-4 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 <VueDraggable\n v-show=\"!previewResolution?.isInitialResolve.value\"\n v-model=\"blocks\"\n group=\"blocks\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-block-btn\"\n :invert-swap=\"true\"\n :inverted-swap-threshold=\"0.65\"\n :disabled=\"previewMode\"\n :draggable=\"'.tpl-block-item'\"\n :force-fallback=\"true\"\n :class=\"[\n 'tpl-canvas-blocks',\n isEmptyCanvas\n ? 'tpl-canvas-empty tpl:m-6 tpl:flex tpl:min-h-[400px] tpl:flex-col tpl:items-center tpl:justify-center tpl:rounded-xl tpl:border-2 tpl:border-dashed tpl:px-10 tpl:py-12 tpl:text-center tpl:bg-[var(--tpl-bg-elevated)] tpl:font-[var(--tpl-font-family)] tpl:transition-colors'\n : '',\n isEmptyCanvas && isDragOverEmpty\n ? 'tpl-canvas-empty--drag-over tpl:border-[var(--tpl-primary-hover)] tpl:bg-[var(--tpl-primary-light)]'\n : '',\n isEmptyCanvas && !isDragOverEmpty\n ? 'tpl:border-[var(--tpl-primary)]'\n : '',\n ]\"\n @dragenter=\"handleEmptyDragEnter\"\n @dragleave=\"handleEmptyDragLeave\"\n @drop=\"handleEmptyDrop\"\n >\n <!-- Empty-state content: rendered INSIDE the draggable so the whole\n dashed box is the drop zone, but excluded from sortable items via\n the `:draggable=\"'.tpl-block-item'\"` selector above — only\n `.tpl-block-item` children are sortable, this isn't. -->\n <div\n v-if=\"isEmptyCanvas\"\n class=\"tpl-canvas-empty-content tpl:flex tpl:flex-col tpl:items-center\"\n >\n <div\n class=\"tpl-canvas-empty-icon tpl:mb-4 tpl:text-[var(--tpl-primary)]\"\n >\n <SquarePlus :size=\"48\" :stroke-width=\"1\" />\n </div>\n <p\n class=\"tpl-canvas-empty-title tpl:m-0 tpl:mb-2 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-primary)]\"\n >\n {{ t.canvas.noBlocks }}\n </p>\n <p\n class=\"tpl-canvas-empty-text tpl:m-0 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.dragHint }}\n </p>\n <p\n v-if=\"canUseAiChat && cloudT\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintChat }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-ai-chat')\"\n >\n <Sparkles :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.aiAssistant }}\n </button>\n {{ t.canvas.aiHintChatSuffix }}\n </p>\n <p\n v-if=\"canUseDesignToTemplate && cloudT\"\n class=\"tpl:m-0 tpl:mt-4 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintDesign }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-design-reference')\"\n >\n <ImageUp :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.designToTemplate }}\n </button>\n {{ t.canvas.aiHintDesignSuffix }}\n </p>\n </div>\n <div\n v-for=\"block in blocks\"\n :key=\"block.id\"\n class=\"tpl-block-item\"\n v-show=\"\n appliesConditionFilter === false ||\n !conditionPreview?.isHidden(block.id)\n \"\n >\n <div class=\"tpl:relative\">\n <!-- Collaboration lock overlay -->\n <div\n v-if=\"getBlockLock(block.id)\"\n class=\"tpl-collab-lock tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-[4] tpl:rounded-sm\"\n :style=\"{\n outline: `2px solid ${getBlockLock(block.id)!.color}`,\n outlineOffset: '-1px',\n }\"\n >\n <span\n class=\"tpl:absolute tpl:-top-0.5 tpl:left-1/2 tpl:z-[5] tpl:flex tpl:-translate-x-1/2 tpl:-translate-y-full tpl:items-center tpl:gap-1 tpl:rounded-full tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:whitespace-nowrap\"\n :style=\"{\n backgroundColor: getBlockLock(block.id)!.color,\n color: readableTextColor(getBlockLock(block.id)!.color),\n }\"\n >\n <span\n class=\"tpl:inline-flex tpl:size-3 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[8px] tpl:font-bold\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 30%,\n transparent\n );\n \"\n >\n {{ getBlockLock(block.id)!.name.charAt(0) }}\n </span>\n {{ getBlockLock(block.id)!.name }}\n </span>\n </div>\n <BlockWrapper\n :block=\"block\"\n :is-selected=\"\n !previewMode &&\n !isPicking &&\n selectedBlockId === block.id &&\n !getBlockLock(block.id)\n \"\n :picked=\"isPickedBlock(block.id)\"\n :viewport=\"viewport\"\n :preview-mode=\"previewMode\"\n @select=\"handleBlockSelect(block.id)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n @fetch-data=\"handleFetchData(block, $event)\"\n @update=\"\n (updates: Partial<Block>) =>\n editor.updateBlock(block.id, updates)\n \"\n />\n </BlockWrapper>\n </div>\n </div>\n </VueDraggable>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* Dark-mode preview: invert only top-level `.tpl-block-content`. Applying\n filter to every `.tpl-block-content` would compound at nested levels\n (sections wrapping child blocks) and cancel out — `:not(...)` restricts\n it to outermost wrappers so the inversion is single-pass. Block chrome\n (action bar, indicators, overlays) lives outside `.tpl-block-content`\n and is therefore never filtered — no counter-filter / toggle flicker. */\n.tpl-canvas--dark-mode :deep(.tpl-block-content:not(.tpl-block-content *)) {\n filter: invert(1) hue-rotate(180deg);\n}\n:deep(.tpl-block-content) {\n transition: filter 300ms ease;\n}\n\n/* Counter-invert images so they look normal inside the inverted block\n content layer (canonical CSS-filter dark-mode trick). */\n.tpl-canvas--dark-mode :deep(.tpl-block-content img) {\n filter: invert(1) hue-rotate(180deg);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useCloudI18n } from \"../composables/useCloudI18n\";\nimport type {\n Block,\n Collaborator,\n CustomBlock as CustomBlockType,\n TemplateContent,\n ViewportSize,\n} from \"@templatical/types\";\nimport { ImageUp, Sparkles, SquarePlus } from \"@lucide/vue\";\nimport { computed, inject, provide, ref, type Component } from \"vue\";\nimport {\n EDITOR_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n BLOCK_REGISTRY_KEY,\n CAPABILITIES_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n PREVIEW_RESOLUTION_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n requireInject,\n} from \"../keys\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport {\n getDocumentStyle,\n resolveBlockComponent,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n EMAIL_GUTTER,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport { readableTextColor } from \"../utils/readableTextColor\";\n\nimport BlockWrapper from \"./blocks/BlockWrapper.vue\";\nimport SectionBlock from \"./blocks/SectionBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\n\n// `countdown` is deliberately absent: it resolves from the block registry, which\n// `useEditorCore` populates with a lazy `defineAsyncComponent` so OSS bundles\n// don't carry a Cloud-only block. `resolveBlockComponent` checks the registry\n// first, so a static entry here would never be reached — but its static import\n// would still pull the module into the eager graph and defeat the laziness.\n// The other fallback maps (SectionBlock, BlockPreviewCanvas,\n// PreviewSectionBlock) omit it for the same reason.\nconst blockComponentMap: Record<string, Component> = {\n section: SectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n video: VideoBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n viewport: ViewportSize;\n content: TemplateContent;\n selectedBlockId: string | null;\n darkMode: boolean;\n previewMode: boolean;\n lockedBlocks?: Map<string, Collaborator>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select-block\", blockId: string | null): void;\n (e: \"open-ai-chat\"): void;\n (e: \"open-design-reference\"): void;\n}>();\n\nconst { t } = useI18n();\nconst { t: cloudT } = useCloudI18n();\n\nconst editor = requireInject(EDITOR_KEY, \"Canvas\");\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n/**\n * Merge-tag samples render on this canvas **only while preview mode is on**.\n * The `previewMode &&` is the load-bearing half: the canvas is the editing\n * surface, and substituted text in a block a user is about to edit would be\n * text they never wrote. Preview mode is already fully non-editing (clicks and\n * dragging are blocked above), so it is the one state where swapping the\n * displayed value is safe.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Null in headless mounts and when no resolver is configured.\nconst previewResolution = inject(PREVIEW_RESOLUTION_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(\n () =>\n props.previewMode &&\n (mergeTagSampleMode?.value ?? false) &&\n // A configured resolver owns what the preview shows. Substituting samples\n // underneath it would mix invented values with real ones, and on the\n // failure path would contradict the note saying the *unresolved* template\n // is showing.\n !previewResolution?.isConfigured,\n ),\n);\n\nconst canUseAiChat = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"chat\") ?? false),\n);\nconst canUseDesignToTemplate = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"designToTemplate\") ?? false),\n);\n\n/**\n * Whether the hand-toggled display-condition filter applies here. Owned by\n * `useEditorCore` and injected rather than re-derived — see the key's comment.\n */\nconst appliesConditionFilter = inject(APPLIES_CONDITION_FILTER_KEY, null);\n\nconst blocks = computed({\n get: () => props.content.blocks,\n set: (value: Block[]) => {\n editor.setContent({\n ...props.content,\n blocks: value,\n });\n },\n});\n\n// Shared with the preview canvas and the save dialog's scaled rows, so all\n// three lay out against the same width.\nconst viewportWidth = computed(() =>\n getEmailFrameWidth(props.content.settings, props.viewport),\n);\n\n// The \"stage\" represents the email body: the content column plus a gutter of\n// email background on each side (#230 — without it a full-width section's own\n// background hides the global one entirely). Mirrors how `mj-body\n// background-color` renders in the gutters around the centered content when\n// the email is sent. The neutral work area beyond the stage is preserved so\n// editor chrome stays legible.\n//\n// `EMAIL_GUTTER` comes from the shared helper rather than a local constant: the\n// preview surfaces show the same band, and two copies drift into two different\n// answers for how much of the body background an email is supposed to show.\nconst stageWidth = computed(() => viewportWidth.value + EMAIL_GUTTER * 2);\n\n// Canvas dark mode preview: simulates how the email will appear in recipients'\n// dark-themed email clients. Uses CSS filter inversion — independent of the\n// editor UI theme (light/dark/auto) which is controlled via uiTheme config.\n// The email bg lives on the `.tpl-canvas-bg` sibling layer (so it can be\n// inverted by `filter` without trapping block chrome in a stacking context);\n// the `.tpl-canvas` itself stays transparent so the bg layer shows through.\n// Shared with every other surface that renders blocks (the saved-block\n// previews), so a block looks the same wherever it is drawn.\nconst canvasStyle = computed(() => getDocumentStyle(props.content.settings));\n\n// Empty canvas: the whole dashed placeholder IS the Sortable drop zone.\n// `isEmptyCanvas` toggles the styling + the inline empty-state content.\nconst isEmptyCanvas = computed(\n () => blocks.value.length === 0 && !props.previewMode,\n);\n\n// Highlight the empty drop zone while a block is being dragged over it.\n// Uses a counter to handle dragenter/leave bubbling from descendants —\n// raw event toggling would flicker as the pointer moves between children.\nconst dragEnterDepth = ref(0);\nconst isDragOverEmpty = computed(\n () => isEmptyCanvas.value && dragEnterDepth.value > 0,\n);\n\nfunction handleEmptyDragEnter(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value += 1;\n}\n\nfunction handleEmptyDragLeave(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value = Math.max(0, dragEnterDepth.value - 1);\n}\n\nfunction handleEmptyDrop(): void {\n dragEnterDepth.value = 0;\n}\n\nfunction handleCanvasClick(event: MouseEvent): void {\n if (props.previewMode) {\n return;\n }\n // During a pick session, a stray background click must not clear anything —\n // the session is left only via the bar's Save/Cancel or Escape. Deselecting\n // here would also be meaningless, since picking doesn't touch selection.\n if (caps.savedBlocks?.isPicking.value) {\n return;\n }\n if (event.target === event.currentTarget) {\n emit(\"select-block\", null);\n }\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, blockComponentMap);\n}\n\nfunction getBlockLock(blockId: string): Collaborator | null {\n return props.lockedBlocks?.get(blockId) ?? null;\n}\n\n/**\n * A click on a block either toggles its pick (while a saved-blocks pick\n * session is running) or selects it. Preview mode and collaborator locks are\n * checked first, so a locked block can be neither selected nor picked.\n */\nfunction handleBlockSelect(blockId: string): void {\n if (props.previewMode || getBlockLock(blockId)) return;\n const savedBlocks = caps.savedBlocks;\n if (savedBlocks?.isPicking.value) {\n savedBlocks.togglePick(blockId);\n return;\n }\n emit(\"select-block\", blockId);\n}\n\nfunction isPickedBlock(blockId: string): boolean {\n return caps.savedBlocks?.isPicked(blockId) ?? false;\n}\n\n/** True while a pick session runs — swaps block chrome into picking behaviour. */\nconst isPicking = computed(() => caps.savedBlocks?.isPicking.value === true);\n\nfunction handleFetchData(\n block: Block,\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n): void {\n if (block.type !== \"custom\") {\n return;\n }\n\n editor.updateBlock(block.id, {\n fieldValues: payload.fieldValues,\n dataSourceFetched: payload.dataSourceFetched,\n } as Partial<CustomBlockType>);\n}\n</script>\n\n<template>\n <!-- STAGE — the email body. Sized to the content column plus a gutter of\n email background on each side, so the global background stays visible\n around full-width sections, mirroring how `mj-body background-color`\n renders in the gutters when sent (#230). The neutral work area beyond\n the stage is preserved so the canvas still reads as a floating card and\n editor chrome stays legible. -->\n <div\n class=\"tpl-canvas-stage tpl:relative tpl:flex tpl:justify-center tpl:rounded-lg\"\n :style=\"{\n width: `${stageWidth}px`,\n boxShadow: darkMode ? 'none' : 'var(--tpl-shadow-xl)',\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <!-- Background layer — holds the email bg and the dark-preview filter,\n spanning the full stage (content + gutters). Filter on this leaf\n layer (no descendants) does not create a containing block / stacking\n context that would trap block chrome (action bar, indicators) inside\n the wrapper. The filter has its own transition so the canvas fades\n smoothly between modes. -->\n <div\n class=\"tpl-canvas-bg tpl:absolute tpl:inset-0 tpl:rounded-lg tpl:pointer-events-none\"\n :style=\"{\n backgroundColor: content.settings.backgroundColor,\n ...(darkMode ? { filter: 'invert(1) hue-rotate(180deg)' } : {}),\n transition: 'filter 300ms ease',\n }\"\n />\n <!-- CONTENT COLUMN — the email content at its true width, centered in the\n stage. A full-width section fills this column but not the surrounding\n gutters, so the global background remains visible beside it. -->\n <div\n data-testid=\"canvas-wrapper\"\n role=\"region\"\n :aria-label=\"t.landmarks.canvas\"\n class=\"tpl-canvas-wrapper tpl:relative\"\n :style=\"{\n width: `${viewportWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <div\n class=\"tpl-canvas tpl:relative tpl:rounded-lg\"\n :class=\"{\n 'tpl-canvas--dark-mode': darkMode,\n 'tpl-preview-mode': previewMode,\n }\"\n :style=\"canvasStyle\"\n @click=\"handleCanvasClick\"\n >\n <!-- Preview resolution. The skeleton shows only on a *first* resolve —\n `isInitialResolve` — so switching recipient or reopening keeps the\n last good preview on screen instead of flashing over it. -->\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 tpl:p-6\"\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 still renders below, so\n the preview degrades rather than disappearing. -->\n <p\n v-else-if=\"previewResolution?.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mx-6 tpl:mt-4 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 <VueDraggable\n v-show=\"!previewResolution?.isInitialResolve.value\"\n v-model=\"blocks\"\n group=\"blocks\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-block-btn\"\n :invert-swap=\"true\"\n :inverted-swap-threshold=\"0.65\"\n :disabled=\"previewMode\"\n :draggable=\"'.tpl-block-item'\"\n :force-fallback=\"true\"\n :class=\"[\n 'tpl-canvas-blocks',\n isEmptyCanvas\n ? 'tpl-canvas-empty tpl:m-6 tpl:flex tpl:min-h-[400px] tpl:flex-col tpl:items-center tpl:justify-center tpl:rounded-xl tpl:border-2 tpl:border-dashed tpl:px-10 tpl:py-12 tpl:text-center tpl:bg-[var(--tpl-bg-elevated)] tpl:font-[var(--tpl-font-family)] tpl:transition-colors'\n : '',\n isEmptyCanvas && isDragOverEmpty\n ? 'tpl-canvas-empty--drag-over tpl:border-[var(--tpl-primary-hover)] tpl:bg-[var(--tpl-primary-light)]'\n : '',\n isEmptyCanvas && !isDragOverEmpty\n ? 'tpl:border-[var(--tpl-primary)]'\n : '',\n ]\"\n @dragenter=\"handleEmptyDragEnter\"\n @dragleave=\"handleEmptyDragLeave\"\n @drop=\"handleEmptyDrop\"\n >\n <!-- Empty-state content: rendered INSIDE the draggable so the whole\n dashed box is the drop zone, but excluded from sortable items via\n the `:draggable=\"'.tpl-block-item'\"` selector above — only\n `.tpl-block-item` children are sortable, this isn't. -->\n <div\n v-if=\"isEmptyCanvas\"\n class=\"tpl-canvas-empty-content tpl:flex tpl:flex-col tpl:items-center\"\n >\n <div\n class=\"tpl-canvas-empty-icon tpl:mb-4 tpl:text-[var(--tpl-primary)]\"\n >\n <SquarePlus :size=\"48\" :stroke-width=\"1\" />\n </div>\n <p\n class=\"tpl-canvas-empty-title tpl:m-0 tpl:mb-2 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-primary)]\"\n >\n {{ t.canvas.noBlocks }}\n </p>\n <p\n class=\"tpl-canvas-empty-text tpl:m-0 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.dragHint }}\n </p>\n <p\n v-if=\"canUseAiChat && cloudT\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintChat }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-ai-chat')\"\n >\n <Sparkles :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.aiAssistant }}\n </button>\n {{ t.canvas.aiHintChatSuffix }}\n </p>\n <p\n v-if=\"canUseDesignToTemplate && cloudT\"\n class=\"tpl:m-0 tpl:mt-4 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintDesign }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-design-reference')\"\n >\n <ImageUp :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.designToTemplate }}\n </button>\n {{ t.canvas.aiHintDesignSuffix }}\n </p>\n </div>\n <div\n v-for=\"block in blocks\"\n :key=\"block.id\"\n class=\"tpl-block-item\"\n v-show=\"\n appliesConditionFilter === false ||\n !conditionPreview?.isHidden(block.id)\n \"\n >\n <div class=\"tpl:relative\">\n <!-- Collaboration lock overlay -->\n <div\n v-if=\"getBlockLock(block.id)\"\n class=\"tpl-collab-lock tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-[4] tpl:rounded-sm\"\n :style=\"{\n outline: `2px solid ${getBlockLock(block.id)!.color}`,\n outlineOffset: '-1px',\n }\"\n >\n <span\n class=\"tpl:absolute tpl:-top-0.5 tpl:left-1/2 tpl:z-[5] tpl:flex tpl:-translate-x-1/2 tpl:-translate-y-full tpl:items-center tpl:gap-1 tpl:rounded-full tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:whitespace-nowrap\"\n :style=\"{\n backgroundColor: getBlockLock(block.id)!.color,\n color: readableTextColor(getBlockLock(block.id)!.color),\n }\"\n >\n <span\n class=\"tpl:inline-flex tpl:size-3 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[8px] tpl:font-bold\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 30%,\n transparent\n );\n \"\n >\n {{ getBlockLock(block.id)!.name.charAt(0) }}\n </span>\n {{ getBlockLock(block.id)!.name }}\n </span>\n </div>\n <BlockWrapper\n :block=\"block\"\n :is-selected=\"\n !previewMode &&\n !isPicking &&\n selectedBlockId === block.id &&\n !getBlockLock(block.id)\n \"\n :picked=\"isPickedBlock(block.id)\"\n :viewport=\"viewport\"\n :preview-mode=\"previewMode\"\n @select=\"handleBlockSelect(block.id)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n @fetch-data=\"handleFetchData(block, $event)\"\n @update=\"\n (updates: Partial<Block>) =>\n editor.updateBlock(block.id, updates)\n \"\n />\n </BlockWrapper>\n </div>\n </div>\n </VueDraggable>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* Dark-mode preview: invert only top-level `.tpl-block-content`. Applying\n filter to every `.tpl-block-content` would compound at nested levels\n (sections wrapping child blocks) and cancel out — `:not(...)` restricts\n it to outermost wrappers so the inversion is single-pass. Block chrome\n (action bar, indicators, overlays) lives outside `.tpl-block-content`\n and is therefore never filtered — no counter-filter / toggle flicker. */\n.tpl-canvas--dark-mode :deep(.tpl-block-content:not(.tpl-block-content *)) {\n filter: invert(1) hue-rotate(180deg);\n}\n:deep(.tpl-block-content) {\n transition: filter 300ms ease;\n}\n\n/* Counter-invert images so they look normal inside the inverted block\n content layer (canonical CSS-filter dark-mode trick). */\n.tpl-canvas--dark-mode :deep(.tpl-block-content img) {\n filter: invert(1) hue-rotate(180deg);\n}\n</style>\n","<script lang=\"ts\">\nimport { defineComponent, h, inject, type ComputedRef } from \"vue\";\nimport { CUSTOM_BLOCK_STYLESHEETS_KEY } from \"../keys\";\n\n/**\n * Renders one `<style>` tag per unique custom-block definition stylesheet\n * currently in use by the editor's content. Mounted near the top of\n * `Editor.vue`'s root template so the rules sit early in the document order\n * and apply to the canvas.\n *\n * In shadow-DOM mode (`shadowDom: true`, default) the styles scope to the\n * editor's shadow root. In light-DOM mode they leak globally, matching the\n * existing behavior of the editor's published `dist/style.css` in that mode\n * — no new global-leak vector.\n *\n * Why a render function instead of `<template>`: Vue's template compiler\n * treats `<style>` inside `<template>` as a side-effect tag and refuses to\n * emit it in strict client-render compilation paths (notably the one used\n * by `vue-tsc` and Vitest's Vue plugin). The render function below\n * bypasses the template compiler entirely and produces the same DOM at\n * runtime — and `innerHTML` on a `<style>` element sets the CSS rules\n * directly, no escape/parse pass between authoring and the cssom.\n */\nexport default defineComponent({\n name: \"CustomBlockStylesheets\",\n setup() {\n const stylesheets = inject<ComputedRef<string[]> | null>(\n CUSTOM_BLOCK_STYLESHEETS_KEY,\n null,\n );\n\n return () => {\n if (!stylesheets) return null;\n return stylesheets.value.map((css, i) =>\n h(\"style\", {\n key: i,\n \"data-tpl-custom-block-stylesheet\": \"\",\n innerHTML: css,\n }),\n );\n };\n },\n});\n</script>\n","<script lang=\"ts\">\nimport { defineComponent, h, inject, type ComputedRef } from \"vue\";\nimport { CUSTOM_BLOCK_STYLESHEETS_KEY } from \"../keys\";\n\n/**\n * Renders one `<style>` tag per unique custom-block definition stylesheet\n * currently in use by the editor's content. Mounted near the top of\n * `Editor.vue`'s root template so the rules sit early in the document order\n * and apply to the canvas.\n *\n * In shadow-DOM mode (`shadowDom: true`, default) the styles scope to the\n * editor's shadow root. In light-DOM mode they leak globally, matching the\n * existing behavior of the editor's published `dist/style.css` in that mode\n * — no new global-leak vector.\n *\n * Why a render function instead of `<template>`: Vue's template compiler\n * treats `<style>` inside `<template>` as a side-effect tag and refuses to\n * emit it in strict client-render compilation paths (notably the one used\n * by `vue-tsc` and Vitest's Vue plugin). The render function below\n * bypasses the template compiler entirely and produces the same DOM at\n * runtime — and `innerHTML` on a `<style>` element sets the CSS rules\n * directly, no escape/parse pass between authoring and the cssom.\n */\nexport default defineComponent({\n name: \"CustomBlockStylesheets\",\n setup() {\n const stylesheets = inject<ComputedRef<string[]> | null>(\n CUSTOM_BLOCK_STYLESHEETS_KEY,\n null,\n );\n\n return () => {\n if (!stylesheets) return null;\n return stylesheets.value.map((css, i) =>\n h(\"style\", {\n key: i,\n \"data-tpl-custom-block-stylesheet\": \"\",\n innerHTML: css,\n }),\n );\n };\n },\n});\n</script>\n","/**\n * Filters and reorders a candidate list of block-palette items against a\n * consumer-supplied `paletteBlocks` allowlist.\n *\n * Each `paletteBlocks` entry is a block-type key that must match a candidate's\n * `type`: a bare built-in type (e.g. `\"image\"`) or a prefixed custom type\n * (e.g. `\"custom:qrcode\"`). The result's `items` contains exactly the\n * candidates named in `paletteBlocks`, in that order. Entries with no matching\n * candidate (a typo, an unregistered custom block, or a plan-gated built-in\n * like `countdown` outside Cloud) are collected into `unknown` so the caller\n * can warn and skip. A type listed more than once is rendered once.\n *\n * When `paletteBlocks` is `undefined` or empty the candidates are returned\n * unchanged (the default full palette). An empty allowlist is treated as \"not\n * configured\" rather than \"hide everything\", since a zero-block palette is\n * almost always a mistake.\n */\nexport function resolvePaletteBlocks<T extends { type: string }>(\n candidates: T[],\n paletteBlocks: string[] | undefined,\n): { items: T[]; unknown: string[] } {\n if (!paletteBlocks || paletteBlocks.length === 0) {\n return { items: candidates, unknown: [] };\n }\n\n const byType = new Map(candidates.map((c) => [c.type, c]));\n const items: T[] = [];\n const unknown: string[] = [];\n const seen = new Set<string>();\n\n for (const entry of paletteBlocks) {\n if (seen.has(entry)) continue;\n seen.add(entry);\n\n const candidate = byType.get(entry);\n if (candidate) {\n items.push(candidate);\n } else {\n unknown.push(entry);\n }\n }\n\n return { items, unknown };\n}\n","/**\n * Where a palette insert should land, expressed as the trailing arguments of\n * `editor.addBlock(block, targetSectionId?, columnIndex?, index?)`.\n *\n * An empty object means \"append at the end\" — every field absent, so spreading\n * it into `addBlock` passes `undefined` for all three and takes the push path.\n */\nexport interface InsertPosition {\n targetSectionId?: string;\n columnIndex?: number;\n index?: number;\n}\n\nexport interface ResolveInsertPositionOptions {\n /**\n * The type of block about to be inserted. Only `\"section\"` changes the\n * outcome — custom types (`custom:*`) behave like any other leaf block.\n */\n blockType: string;\n selectedBlockId: string | null;\n findBlockLocation: (blockId: string) => {\n targetSectionId?: string;\n columnIndex?: number;\n index: number;\n } | null;\n isBlockLocked: (blockId: string) => boolean;\n}\n\n/**\n * Resolve the insert position for a palette click: directly below the selected\n * block, falling back to appending at the end.\n *\n * Every fallback here exists because `addBlock` refuses the alternative\n * *silently*. Returning a position it rejects would turn the click into a\n * complete no-op — worse for the user than landing at the bottom, which is the\n * behaviour this whole function replaces. So the rule is: only ever return a\n * position `addBlock` will accept.\n *\n * Mirrors `duplicateBlock` in `@templatical/core` (resolve the reference\n * block's location, bump the index by one, append when it can't be resolved) so\n * click-to-insert and duplicate place blocks by the same rule.\n */\nexport function resolveInsertPosition(\n options: ResolveInsertPositionOptions,\n): InsertPosition {\n const { blockType, selectedBlockId, findBlockLocation, isBlockLocked } =\n options;\n\n if (!selectedBlockId) return {};\n\n // A selection can outlive its block — a collaborator removes it, an undo\n // drops it — so an unresolvable id means append, never a guessed index.\n const selected = findBlockLocation(selectedBlockId);\n if (!selected) return {};\n\n if (selected.targetSectionId === undefined) {\n return { index: selected.index + 1 };\n }\n\n if (blockType === \"section\") {\n // MJML forbids `mj-section` inside `mj-column`, so `addBlock` rejects the\n // nest outright. Land beside the section the selection sits in instead —\n // the nearest position that accepts a section at all. Sections never nest,\n // so the parent's own location is always top-level.\n const parent = findBlockLocation(selected.targetSectionId);\n if (!parent || parent.targetSectionId !== undefined) return {};\n return { index: parent.index + 1 };\n }\n\n // `addBlock` bails when the target section is locked, so a locked parent\n // means the column is unavailable — append rather than lose the block.\n if (isBlockLocked(selected.targetSectionId)) return {};\n\n return {\n targetSectionId: selected.targetSectionId,\n columnIndex: selected.columnIndex,\n index: selected.index + 1,\n };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { Block, BlockType } from \"@templatical/types\";\nimport { createBlock, createCustomBlock } from \"@templatical/types\";\nimport { Package } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport CustomBlockIcon from \"./CustomBlockIcon.vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport { resolvePaletteBlocks } from \"../utils/resolvePaletteBlocks\";\nimport { resolveInsertPosition } from \"../utils/resolveInsertPosition\";\nimport { useScrollToBlock } from \"../composables/useScrollToBlock\";\nimport { logger } from \"../utils/logger\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n PALETTE_BLOCKS_KEY,\n BLOCK_DEFAULTS_KEY,\n CAPABILITIES_KEY,\n EDITOR_KEY,\n} from \"../keys\";\n\ninterface BlockTypeItem {\n type: BlockType | string;\n label: string;\n isCustom?: boolean;\n icon?: string;\n}\n\nconst { t, format } = useI18n();\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\nconst paletteBlocks = inject(PALETTE_BLOCKS_KEY, undefined);\nconst blockDefaults = inject(BLOCK_DEFAULTS_KEY, undefined);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n// Availability only — deliberately NOT `count > 0`. Gating on the loaded count\n// meant the entry appeared only once the consumer's `list()` resolved, so a slow\n// endpoint shifted the rail mid-session and an empty library hid the feature\n// entirely. The list is now fetched when the browser opens instead.\nconst showSavedBlocksSection = computed(\n () => caps.savedBlocks?.isAvailable.value === true,\n);\n\nconst isExpanded = ref(false);\nconst isDragging = ref(false);\nconst railEl = ref<HTMLElement | null>(null);\n\n// `:focus-visible`, never `:focus`: a mouse click leaves the clicked palette\n// button focused, so a plain `:focus` test would pin the rail open from the\n// first click-to-insert onward — the very symptom this rail already had for\n// another reason (#568). Keyboard focus does keep it open, because collapsing\n// hides the label of the entry the user has focused.\nfunction hasKeyboardFocusInRail(): boolean {\n try {\n return railEl.value?.querySelector(\":focus-visible\") != null;\n } catch {\n // Firefox <85 / Safari <15.4 — reachable on the `shadowDom: false` tier,\n // whose browser floor predates the selector — throw on it. Reading that\n // as \"no keyboard focus\" keeps the collapse working there.\n return false;\n }\n}\n\nfunction handleSidebarLeave(): void {\n // Don't collapse while a Sortable drag is in progress. If the sidebar\n // shrinks (200px → 48px) DURING the drag's threshold-to-dragStarted\n // window, `dragEl.getBoundingClientRect()` returns a mid-transition\n // rect when Sortable's `_appendGhost` runs — the fallback ghost gets\n // stamped with that wrong rect and ends up visibly offset from the\n // cursor for the rest of the drag. Cleared on drag-end.\n if (isDragging.value) return;\n if (hasKeyboardFocusInRail()) return;\n isExpanded.value = false;\n}\n\nfunction handleDragChoose(): void {\n // `choose` fires synchronously inside Sortable's `_onTapStart` at\n // pointerdown, BEFORE any pointermove or mouseleave. `start` would be\n // too late — it fires only after the threshold-move dispatches\n // `_dragStarted`, by which time mouseleave may already have flipped\n // `isExpanded` and the sidebar may be mid-collapse.\n isDragging.value = true;\n}\n\nfunction handleDragUnchoose(): void {\n // `end` is not the counterpart of `choose` — Sortable gates it on\n // `Sortable.active`, which only `_dragStarted` sets, so a click on a palette\n // entry emits `choose` + `unchoose` and no `end` at all. Clearing the guard\n // only in `handleDragEnd` therefore latches it true on the first click and\n // `handleSidebarLeave` bails forever after: the rail stays expanded until\n // some later drag happens to complete (#568). `unchoose` fires on every\n // release, at drop time — long after `_appendGhost` captured its rect — so\n // it cannot re-open the race `handleDragChoose` guards.\n isDragging.value = false;\n}\n\nfunction handleDragEnd(): void {\n isDragging.value = false;\n // Collapse the rail on drop. A drag ends with the cursor out in the canvas,\n // so no `mouseleave` fires to collapse it — and `handleSidebarLeave` had\n // bailed out earlier because `isDragging` was true. Without this the rail\n // stays expanded until the next hover-in/hover-out cycle.\n isExpanded.value = false;\n}\n\nconst builtInBlockTypeOrder: string[] = [\n \"section\",\n \"image\",\n \"title\",\n \"paragraph\",\n \"button\",\n \"divider\",\n \"video\",\n \"social\",\n \"menu\",\n \"table\",\n \"spacer\",\n \"html\",\n];\n\nconst builtInBlockTypes = computed<BlockTypeItem[]>(() => {\n const types: BlockTypeItem[] = builtInBlockTypeOrder.map((type) => ({\n type,\n label: getBlockTypeLabel(type, t),\n }));\n\n // Countdown requires Templatical Cloud for server-side GIF rendering\n if (caps.plan) {\n types.splice(-1, 0, {\n type: \"countdown\",\n label: getBlockTypeLabel(\"countdown\", t),\n });\n }\n\n return types;\n});\n\nconst customBlockItems = computed<BlockTypeItem[]>(() => {\n return customBlockDefinitions.map((def) => ({\n type: `custom:${def.type}`,\n label: def.name,\n isCustom: true,\n icon: def.icon,\n }));\n});\n\nconst candidateBlockTypes = computed<BlockTypeItem[]>(() => [\n ...builtInBlockTypes.value,\n ...customBlockItems.value,\n]);\n\n// Apply the consumer's `paletteBlocks` allowlist + order when provided; otherwise show\n// the full default palette. Filtering only affects the palette — the block\n// registry stays complete, so existing content using a hidden type still renders.\nconst resolvedPalette = computed(() =>\n resolvePaletteBlocks(candidateBlockTypes.value, paletteBlocks),\n);\n\nconst blockTypes = computed<BlockTypeItem[]>(() => resolvedPalette.value.items);\n\n// Warn once per unknown `paletteBlocks` entry (a typo, an unregistered custom block, or\n// `countdown` outside a Cloud plan). Done in a watcher rather than inside the\n// computed so recomputes don't repeat the warning.\nconst warnedPaletteEntries = new Set<string>();\nwatch(\n () => resolvedPalette.value.unknown,\n (unknown) => {\n for (const entry of unknown) {\n if (warnedPaletteEntries.has(entry)) continue;\n warnedPaletteEntries.add(entry);\n logger.warn(\n `config.paletteBlocks: \"${entry}\" is not a built-in or registered custom block ` +\n `(use the \"custom:\" prefix for custom blocks) — skipping it in the palette.`,\n );\n }\n },\n { immediate: true },\n);\n\nfunction createBlockFromItem(item: BlockTypeItem): Block {\n if (item.isCustom) {\n const customType = item.type.replace(\"custom:\", \"\");\n const definition = customBlockDefinitions.find(\n (d) => d.type === customType,\n );\n if (definition) {\n return createCustomBlock(definition);\n }\n }\n\n return createBlock(item.type as BlockType, blockDefaults);\n}\n\nfunction insertBlockFromItem(item: BlockTypeItem): void {\n if (!editor) return;\n const block = createBlockFromItem(item);\n // Land below the selection, not at the end: on a template taller than the\n // viewport an appended block is below the fold, and the canvas does not\n // follow on its own, so the click reads as a no-op (issue #568).\n const { targetSectionId, columnIndex, index } = resolveInsertPosition({\n blockType: item.type,\n selectedBlockId: editor.state.selectedBlockId,\n findBlockLocation: editor.findBlockLocation,\n isBlockLocked: editor.isBlockLocked,\n });\n editor.addBlock(block, targetSectionId, columnIndex, index);\n editor.selectBlock(block.id);\n // Selecting is not visible by itself, and the append fallback above still\n // lands off-screen — the canvas has to follow either way.\n scrollToBlock(block.id);\n}\n\nfunction handlePaletteKeydown(event: KeyboardEvent, item: BlockTypeItem): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n insertBlockFromItem(item);\n }\n}\n</script>\n\n<template>\n <aside\n ref=\"railEl\"\n :aria-label=\"t.sidebarNav.palette\"\n class=\"tpl-sidebar-rail tpl:absolute tpl:top-14 tpl:bottom-0 tpl:left-0 tpl:z-40 tpl:flex tpl:flex-col tpl:overflow-hidden\"\n :style=\"{\n width: isExpanded ? '200px' : '48px',\n backgroundColor: 'var(--tpl-bg-elevated)',\n borderRight: '1px solid var(--tpl-border)',\n boxShadow: isExpanded ? 'var(--tpl-shadow-lg)' : 'none',\n transition:\n 'width 200ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 200ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n @mouseenter=\"isExpanded = true\"\n @mouseleave=\"handleSidebarLeave\"\n @focusin=\"isExpanded = true\"\n @focusout=\"isExpanded = false\"\n >\n <!-- Saved blocks browser trigger. Needs the feature to be available (a\n provider configured, and in Cloud the plan to allow it). -->\n <div\n v-if=\"showSavedBlocksSection\"\n class=\"tpl:border-b tpl:px-1 tpl:pb-1 tpl:border-[var(--tpl-border)]\"\n >\n <button\n type=\"button\"\n :aria-label=\"t.sidebarNav.browseSavedBlocks\"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)]\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"caps.savedBlocks?.openBrowser()\"\n >\n <Package :size=\"20\" :stroke-width=\"1.5\" class=\"tpl:shrink-0\" />\n <!-- No `flex-1`: the rail inherits `text-align: center`, so a label\n stretched to fill the row centres its glyphs and sits visibly\n further right than the palette labels below it. Shrink-to-fit\n matches them exactly. -->\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ t.savedBlocks.title }}\n </span>\n </button>\n </div>\n <!-- The palette is the rail's scroll region: `flex-1` makes it fill the\n height left by the (optional) modules trigger, `min-h-0` lets it\n shrink below its content so `overflow-y-auto` engages. Without this\n the rail's `overflow-hidden` clips items below the fold on short\n viewports with no way to reach them (#231). -->\n <VueDraggable\n :model-value=\"blockTypes\"\n :group=\"{ name: 'blocks', pull: 'clone', put: false }\"\n :clone=\"createBlockFromItem\"\n :sort=\"false\"\n :animation=\"150\"\n :force-fallback=\"true\"\n class=\"tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-0.5 tpl:overflow-y-auto tpl:p-1\"\n @choose=\"handleDragChoose\"\n @unchoose=\"handleDragUnchoose\"\n @end=\"handleDragEnd\"\n >\n <button\n v-for=\"blockType in blockTypes\"\n :key=\"blockType.type\"\n type=\"button\"\n :data-palette-type=\"blockType.type\"\n :aria-label=\"\n format(t.sidebarNav.insertBlock, { block: blockType.label })\n \"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:shrink-0 tpl:cursor-grab tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:active:cursor-grabbing\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"insertBlockFromItem(blockType)\"\n @keydown=\"handlePaletteKeydown($event, blockType)\"\n >\n <div\n class=\"tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:transition-transform tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:scale-105\"\n >\n <component\n :is=\"blockTypeIcons[blockType.type]\"\n v-if=\"blockTypeIcons[blockType.type]\"\n :size=\"20\"\n :stroke-width=\"1.5\"\n />\n <CustomBlockIcon\n v-else-if=\"blockType.isCustom\"\n :icon=\"blockType.icon\"\n :size=\"20\"\n />\n </div>\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ blockType.label }}\n </span>\n </button>\n </VueDraggable>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { Block, BlockType } from \"@templatical/types\";\nimport { createBlock, createCustomBlock } from \"@templatical/types\";\nimport { Package } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport CustomBlockIcon from \"./CustomBlockIcon.vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport { resolvePaletteBlocks } from \"../utils/resolvePaletteBlocks\";\nimport { resolveInsertPosition } from \"../utils/resolveInsertPosition\";\nimport { useScrollToBlock } from \"../composables/useScrollToBlock\";\nimport { logger } from \"../utils/logger\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n PALETTE_BLOCKS_KEY,\n BLOCK_DEFAULTS_KEY,\n CAPABILITIES_KEY,\n EDITOR_KEY,\n} from \"../keys\";\n\ninterface BlockTypeItem {\n type: BlockType | string;\n label: string;\n isCustom?: boolean;\n icon?: string;\n}\n\nconst { t, format } = useI18n();\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\nconst paletteBlocks = inject(PALETTE_BLOCKS_KEY, undefined);\nconst blockDefaults = inject(BLOCK_DEFAULTS_KEY, undefined);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n// Availability only — deliberately NOT `count > 0`. Gating on the loaded count\n// meant the entry appeared only once the consumer's `list()` resolved, so a slow\n// endpoint shifted the rail mid-session and an empty library hid the feature\n// entirely. The list is now fetched when the browser opens instead.\nconst showSavedBlocksSection = computed(\n () => caps.savedBlocks?.isAvailable.value === true,\n);\n\nconst isExpanded = ref(false);\nconst isDragging = ref(false);\nconst railEl = ref<HTMLElement | null>(null);\n\n// `:focus-visible`, never `:focus`: a mouse click leaves the clicked palette\n// button focused, so a plain `:focus` test would pin the rail open from the\n// first click-to-insert onward — the very symptom this rail already had for\n// another reason (#568). Keyboard focus does keep it open, because collapsing\n// hides the label of the entry the user has focused.\nfunction hasKeyboardFocusInRail(): boolean {\n try {\n return railEl.value?.querySelector(\":focus-visible\") != null;\n } catch {\n // Firefox <85 / Safari <15.4 — reachable on the `shadowDom: false` tier,\n // whose browser floor predates the selector — throw on it. Reading that\n // as \"no keyboard focus\" keeps the collapse working there.\n return false;\n }\n}\n\nfunction handleSidebarLeave(): void {\n // Don't collapse while a Sortable drag is in progress. If the sidebar\n // shrinks (200px → 48px) DURING the drag's threshold-to-dragStarted\n // window, `dragEl.getBoundingClientRect()` returns a mid-transition\n // rect when Sortable's `_appendGhost` runs — the fallback ghost gets\n // stamped with that wrong rect and ends up visibly offset from the\n // cursor for the rest of the drag. Cleared on drag-end.\n if (isDragging.value) return;\n if (hasKeyboardFocusInRail()) return;\n isExpanded.value = false;\n}\n\nfunction handleDragChoose(): void {\n // `choose` fires synchronously inside Sortable's `_onTapStart` at\n // pointerdown, BEFORE any pointermove or mouseleave. `start` would be\n // too late — it fires only after the threshold-move dispatches\n // `_dragStarted`, by which time mouseleave may already have flipped\n // `isExpanded` and the sidebar may be mid-collapse.\n isDragging.value = true;\n}\n\nfunction handleDragUnchoose(): void {\n // `end` is not the counterpart of `choose` — Sortable gates it on\n // `Sortable.active`, which only `_dragStarted` sets, so a click on a palette\n // entry emits `choose` + `unchoose` and no `end` at all. Clearing the guard\n // only in `handleDragEnd` therefore latches it true on the first click and\n // `handleSidebarLeave` bails forever after: the rail stays expanded until\n // some later drag happens to complete (#568). `unchoose` fires on every\n // release, at drop time — long after `_appendGhost` captured its rect — so\n // it cannot re-open the race `handleDragChoose` guards.\n isDragging.value = false;\n}\n\nfunction handleDragEnd(): void {\n isDragging.value = false;\n // Collapse the rail on drop. A drag ends with the cursor out in the canvas,\n // so no `mouseleave` fires to collapse it — and `handleSidebarLeave` had\n // bailed out earlier because `isDragging` was true. Without this the rail\n // stays expanded until the next hover-in/hover-out cycle.\n isExpanded.value = false;\n}\n\nconst builtInBlockTypeOrder: string[] = [\n \"section\",\n \"image\",\n \"title\",\n \"paragraph\",\n \"button\",\n \"divider\",\n \"video\",\n \"social\",\n \"menu\",\n \"table\",\n \"spacer\",\n \"html\",\n];\n\nconst builtInBlockTypes = computed<BlockTypeItem[]>(() => {\n const types: BlockTypeItem[] = builtInBlockTypeOrder.map((type) => ({\n type,\n label: getBlockTypeLabel(type, t),\n }));\n\n // Countdown requires Templatical Cloud for server-side GIF rendering\n if (caps.plan) {\n types.splice(-1, 0, {\n type: \"countdown\",\n label: getBlockTypeLabel(\"countdown\", t),\n });\n }\n\n return types;\n});\n\nconst customBlockItems = computed<BlockTypeItem[]>(() => {\n return customBlockDefinitions.map((def) => ({\n type: `custom:${def.type}`,\n label: def.name,\n isCustom: true,\n icon: def.icon,\n }));\n});\n\nconst candidateBlockTypes = computed<BlockTypeItem[]>(() => [\n ...builtInBlockTypes.value,\n ...customBlockItems.value,\n]);\n\n// Apply the consumer's `paletteBlocks` allowlist + order when provided; otherwise show\n// the full default palette. Filtering only affects the palette — the block\n// registry stays complete, so existing content using a hidden type still renders.\nconst resolvedPalette = computed(() =>\n resolvePaletteBlocks(candidateBlockTypes.value, paletteBlocks),\n);\n\nconst blockTypes = computed<BlockTypeItem[]>(() => resolvedPalette.value.items);\n\n// Warn once per unknown `paletteBlocks` entry (a typo, an unregistered custom block, or\n// `countdown` outside a Cloud plan). Done in a watcher rather than inside the\n// computed so recomputes don't repeat the warning.\nconst warnedPaletteEntries = new Set<string>();\nwatch(\n () => resolvedPalette.value.unknown,\n (unknown) => {\n for (const entry of unknown) {\n if (warnedPaletteEntries.has(entry)) continue;\n warnedPaletteEntries.add(entry);\n logger.warn(\n `config.paletteBlocks: \"${entry}\" is not a built-in or registered custom block ` +\n `(use the \"custom:\" prefix for custom blocks) — skipping it in the palette.`,\n );\n }\n },\n { immediate: true },\n);\n\nfunction createBlockFromItem(item: BlockTypeItem): Block {\n if (item.isCustom) {\n const customType = item.type.replace(\"custom:\", \"\");\n const definition = customBlockDefinitions.find(\n (d) => d.type === customType,\n );\n if (definition) {\n return createCustomBlock(definition);\n }\n }\n\n return createBlock(item.type as BlockType, blockDefaults);\n}\n\nfunction insertBlockFromItem(item: BlockTypeItem): void {\n if (!editor) return;\n const block = createBlockFromItem(item);\n // Land below the selection, not at the end: on a template taller than the\n // viewport an appended block is below the fold, and the canvas does not\n // follow on its own, so the click reads as a no-op (issue #568).\n const { targetSectionId, columnIndex, index } = resolveInsertPosition({\n blockType: item.type,\n selectedBlockId: editor.state.selectedBlockId,\n findBlockLocation: editor.findBlockLocation,\n isBlockLocked: editor.isBlockLocked,\n });\n editor.addBlock(block, targetSectionId, columnIndex, index);\n editor.selectBlock(block.id);\n // Selecting is not visible by itself, and the append fallback above still\n // lands off-screen — the canvas has to follow either way.\n scrollToBlock(block.id);\n}\n\nfunction handlePaletteKeydown(event: KeyboardEvent, item: BlockTypeItem): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n insertBlockFromItem(item);\n }\n}\n</script>\n\n<template>\n <aside\n ref=\"railEl\"\n :aria-label=\"t.sidebarNav.palette\"\n class=\"tpl-sidebar-rail tpl:absolute tpl:top-14 tpl:bottom-0 tpl:left-0 tpl:z-40 tpl:flex tpl:flex-col tpl:overflow-hidden\"\n :style=\"{\n width: isExpanded ? '200px' : '48px',\n backgroundColor: 'var(--tpl-bg-elevated)',\n borderRight: '1px solid var(--tpl-border)',\n boxShadow: isExpanded ? 'var(--tpl-shadow-lg)' : 'none',\n transition:\n 'width 200ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 200ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n @mouseenter=\"isExpanded = true\"\n @mouseleave=\"handleSidebarLeave\"\n @focusin=\"isExpanded = true\"\n @focusout=\"isExpanded = false\"\n >\n <!-- Saved blocks browser trigger. Needs the feature to be available (a\n provider configured, and in Cloud the plan to allow it). -->\n <div\n v-if=\"showSavedBlocksSection\"\n class=\"tpl:border-b tpl:px-1 tpl:pb-1 tpl:border-[var(--tpl-border)]\"\n >\n <button\n type=\"button\"\n :aria-label=\"t.sidebarNav.browseSavedBlocks\"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)]\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"caps.savedBlocks?.openBrowser()\"\n >\n <Package :size=\"20\" :stroke-width=\"1.5\" class=\"tpl:shrink-0\" />\n <!-- No `flex-1`: the rail inherits `text-align: center`, so a label\n stretched to fill the row centres its glyphs and sits visibly\n further right than the palette labels below it. Shrink-to-fit\n matches them exactly. -->\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ t.savedBlocks.title }}\n </span>\n </button>\n </div>\n <!-- The palette is the rail's scroll region: `flex-1` makes it fill the\n height left by the (optional) modules trigger, `min-h-0` lets it\n shrink below its content so `overflow-y-auto` engages. Without this\n the rail's `overflow-hidden` clips items below the fold on short\n viewports with no way to reach them (#231). -->\n <VueDraggable\n :model-value=\"blockTypes\"\n :group=\"{ name: 'blocks', pull: 'clone', put: false }\"\n :clone=\"createBlockFromItem\"\n :sort=\"false\"\n :animation=\"150\"\n :force-fallback=\"true\"\n class=\"tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-0.5 tpl:overflow-y-auto tpl:p-1\"\n @choose=\"handleDragChoose\"\n @unchoose=\"handleDragUnchoose\"\n @end=\"handleDragEnd\"\n >\n <button\n v-for=\"blockType in blockTypes\"\n :key=\"blockType.type\"\n type=\"button\"\n :data-palette-type=\"blockType.type\"\n :aria-label=\"\n format(t.sidebarNav.insertBlock, { block: blockType.label })\n \"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:shrink-0 tpl:cursor-grab tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:active:cursor-grabbing\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"insertBlockFromItem(blockType)\"\n @keydown=\"handlePaletteKeydown($event, blockType)\"\n >\n <div\n class=\"tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:transition-transform tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:scale-105\"\n >\n <component\n :is=\"blockTypeIcons[blockType.type]\"\n v-if=\"blockTypeIcons[blockType.type]\"\n :size=\"20\"\n :stroke-width=\"1.5\"\n />\n <CustomBlockIcon\n v-else-if=\"blockType.isCustom\"\n :icon=\"blockType.icon\"\n :size=\"20\"\n />\n </div>\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ blockType.label }}\n </span>\n </button>\n </VueDraggable>\n </aside>\n</template>\n","/**\n * Result of a merge-tag trigger scan — the caret sits inside an open tag.\n */\nexport interface OpenMergeTagTrigger {\n /** Index in the field value where the trigger opener starts. */\n triggerStart: number;\n /** Caret index the scan ran at (also the end of `query`). */\n caret: number;\n /** Text typed between the opener and the caret — the autocomplete filter. */\n query: string;\n}\n\n/**\n * Detects whether the caret sits inside an *open* merge-tag trigger: the user\n * has typed the syntax opener (e.g. `{{`) and is mid-typing the tag name with\n * no closing delimiter yet. Returns the opener position + query so the caller\n * can drive the autocomplete popup, or `null` when the caret is not inside an\n * open trigger.\n *\n * Mirrors `@tiptap/suggestion`'s behavior with `allowSpaces: false`: the run\n * between the opener and the caret ends at the first whitespace, and a closing\n * delimiter inside that run means the tag is already complete (`{{first_name}}`)\n * so the popup must not reappear over it.\n */\nexport function findOpenMergeTagTrigger(\n value: string,\n caret: number,\n triggerChar: string,\n closingChar: string | null,\n): OpenMergeTagTrigger | null {\n if (!triggerChar) return null;\n\n const before = value.slice(0, caret);\n const triggerStart = before.lastIndexOf(triggerChar);\n if (triggerStart === -1) return null;\n\n const query = before.slice(triggerStart + triggerChar.length);\n\n // Whitespace ends the query (allowSpaces: false parity).\n if (/\\s/.test(query)) return null;\n\n // A closing delimiter between the opener and the caret means the tag is\n // already complete — don't reopen the popup over a finished tag.\n if (closingChar && query.includes(closingChar)) return null;\n\n return { triggerStart, caret, query };\n}\n","/**\n * Viewport-space caret geometry for `<input>` / `<textarea>` elements.\n *\n * Native text controls expose no caret coordinates (unlike ProseMirror, which\n * hands the merge-tag popup a `clientRect()`). The standard workaround is a\n * hidden \"mirror\" element that clones the field's text metrics; the caret's\n * pixel position is read from a marker span placed at the caret offset. This\n * lets the input/textarea autocomplete anchor its popup at the caret exactly\n * like the rich-text editor does.\n *\n * Adapted from component/textarea-caret-position (MIT). Returns a viewport\n * `DOMRect` (same coordinate space as `Range.getBoundingClientRect`) so the\n * shared merge-tag popup controller positions it identically to the TipTap path.\n */\n\n// Computed-style properties copied onto the mirror so its text wraps and\n// measures exactly like the source field.\nconst MIRRORED_PROPERTIES = [\n \"boxSizing\",\n \"width\",\n \"borderTopWidth\",\n \"borderRightWidth\",\n \"borderBottomWidth\",\n \"borderLeftWidth\",\n \"borderStyle\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"fontStyle\",\n \"fontVariant\",\n \"fontWeight\",\n \"fontStretch\",\n \"fontSize\",\n \"lineHeight\",\n \"fontFamily\",\n \"textAlign\",\n \"textTransform\",\n \"textIndent\",\n \"letterSpacing\",\n \"wordSpacing\",\n \"tabSize\",\n \"whiteSpace\",\n \"wordWrap\",\n \"wordBreak\",\n] as const;\n\nfunction toRect(left: number, top: number, bottom: number): DOMRect {\n const height = bottom - top;\n return {\n left,\n top,\n bottom,\n right: left,\n width: 0,\n height,\n x: left,\n y: top,\n toJSON: () => ({}),\n } as DOMRect;\n}\n\n/**\n * Returns the viewport rect of the caret at `position` in a text field, or\n * `null` when the field isn't laid out (detached / no view). The rect is a\n * zero-width vertical strip at the caret, one line tall — matching the shape\n * `Range.getBoundingClientRect()` produces for a collapsed selection.\n */\nexport function getCaretRect(\n el: HTMLInputElement | HTMLTextAreaElement,\n position: number,\n): DOMRect | null {\n const doc = el.ownerDocument;\n const win = doc.defaultView;\n if (!win) return null;\n\n const isInput = el.tagName === \"INPUT\";\n const style = win.getComputedStyle(el);\n\n const mirror = doc.createElement(\"div\");\n mirror.setAttribute(\"aria-hidden\", \"true\");\n const s = mirror.style;\n s.position = \"absolute\";\n s.visibility = \"hidden\";\n s.top = \"0\";\n s.left = \"0\";\n s.overflow = \"hidden\";\n s.whiteSpace = isInput ? \"pre\" : \"pre-wrap\";\n s.wordWrap = isInput ? \"normal\" : \"break-word\";\n const target = s as unknown as Record<string, string>;\n const source = style as unknown as Record<string, string>;\n for (const prop of MIRRORED_PROPERTIES) {\n target[prop] = source[prop];\n }\n // A single-line input never wraps; let the mirror grow with the text so the\n // marker's horizontal offset tracks the caret.\n if (isInput) {\n s.width = \"auto\";\n s.whiteSpace = \"pre\";\n }\n\n const value = el.value;\n let head = value.slice(0, position);\n if (isInput) {\n // <input> collapses whitespace runs to a single rendered space; replace\n // with non-breaking spaces so the mirror's width matches the field.\n head = head.replace(/\\s/g, \" \");\n }\n mirror.textContent = head;\n\n const marker = doc.createElement(\"span\");\n // Remaining text (or a placeholder) gives the marker the caret line's height.\n marker.textContent = value.slice(position) || \".\";\n mirror.appendChild(marker);\n\n doc.body.appendChild(mirror);\n const markerTop = marker.offsetTop;\n const markerLeft = marker.offsetLeft;\n doc.body.removeChild(mirror);\n\n const elRect = el.getBoundingClientRect();\n const borderTop = parseFloat(style.borderTopWidth) || 0;\n const borderLeft = parseFloat(style.borderLeftWidth) || 0;\n const lineHeight =\n parseFloat(style.lineHeight) || parseFloat(style.fontSize) || 0;\n\n const left = elRect.left + markerLeft + borderLeft - el.scrollLeft;\n\n // A single-line input vertically centers its text, which the block-flow\n // mirror can't replicate — anchor vertically to the field itself so the\n // popup sits directly under the input. Textareas use the measured caret line.\n if (isInput) {\n return toRect(left, elRect.top, elRect.bottom);\n }\n\n const top = elRect.top + markerTop + borderTop - el.scrollTop;\n return toRect(left, top, top + lineHeight);\n}\n","import {\n getSyntaxClosingChar,\n getSyntaxTriggerChar,\n type MergeTag,\n type SyntaxPreset,\n} from \"@templatical/types\";\nimport {\n getCurrentScope,\n inject,\n isRef,\n nextTick,\n onScopeDispose,\n type Ref,\n} from \"vue\";\nimport type { Translations } from \"../i18n\";\nimport { TRANSLATIONS_KEY } from \"../keys\";\nimport {\n createMergeTagPopup,\n filterMergeTags,\n} from \"../extensions/MergeTagSuggestion\";\nimport {\n findOpenMergeTagTrigger,\n type OpenMergeTagTrigger,\n} from \"../utils/mergeTagTrigger\";\nimport { getCaretRect } from \"../utils/textFieldCaret\";\nimport { usePopoverRoot } from \"./usePopoverRoot\";\n\nexport interface UseMergeTagAutocompleteOptions {\n /** The `<input>` / `<textarea>` the popup anchors to. */\n elementRef: Ref<HTMLInputElement | HTMLTextAreaElement | null>;\n /** Reads the current field value. */\n modelValue: () => string;\n /** Emits a new field value. */\n emit: (value: string) => void;\n /** Available merge tags (the autocomplete filter source). */\n mergeTags: MergeTag[];\n /** Resolved merge-tag syntax. */\n syntax: SyntaxPreset;\n /** Consumer's `mergeTags.autocomplete` flag. */\n enabled: boolean;\n /**\n * Invoked right before a picked tag is emitted, so the field can keep edit\n * mode and not flip to the chip display while the caret is still active.\n */\n onInsert?: () => void;\n}\n\nexport interface UseMergeTagAutocompleteReturn {\n /**\n * Whether autocomplete can ever fire. Mirrors the rich-text gate exactly:\n * enabled + a built-in trigger syntax + at least one tag to filter.\n */\n available: boolean;\n /** Recompute the popup after the value or caret changed. */\n refresh: () => void;\n /**\n * Field `keydown` handler. Returns `true` when the popup consumed the key\n * (the caller must then `preventDefault`); `false` to let the field handle it.\n */\n handleKeydown: (event: KeyboardEvent) => boolean;\n /** Close the popup (e.g. on blur). Idempotent. */\n close: () => void;\n /** Whether the popup is currently open. */\n isOpen: () => boolean;\n}\n\n// Horizontal caret moves take the caret out of the query run — dismiss the\n// popup (like navigating away in the rich-text editor) and let the caret move.\nconst CARET_MOVE_KEYS = new Set([\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\n/**\n * Type-ahead merge-tag autocomplete for a native `<input>` / `<textarea>`.\n *\n * Drives the SAME popup controller as the rich-text (`@tiptap/suggestion`)\n * path — see `createMergeTagPopup` — so the popup's rendering, positioning,\n * keyboard behavior, and ARIA are identical across both surfaces. The only\n * field-specific work is here: detecting the open trigger in the raw string\n * (`findOpenMergeTagTrigger`), measuring the caret rect without ProseMirror\n * (`getCaretRect`), and replacing the typed fragment with the picked tag.\n */\nexport function useMergeTagAutocomplete(\n options: UseMergeTagAutocompleteOptions,\n): UseMergeTagAutocompleteReturn {\n const { elementRef, modelValue, emit, mergeTags, syntax, enabled, onInsert } =\n options;\n\n const triggerChar = getSyntaxTriggerChar(syntax);\n const closingChar = getSyntaxClosingChar(syntax);\n const available = enabled && triggerChar !== null && mergeTags.length > 0;\n\n // Resolve the empty-state label the same way the rich-text path does —\n // inject with a null default so headless callers (no translations provider)\n // don't throw, falling back to the English string.\n const injectedTranslations = inject(TRANSLATIONS_KEY, null) as\n Translations | Ref<Translations> | null;\n const resolvedTranslations = isRef(injectedTranslations)\n ? injectedTranslations.value\n : injectedTranslations;\n const emptyText =\n resolvedTranslations?.mergeTag?.suggestionEmpty ?? \"No matching merge tags\";\n\n const popoverRoot = usePopoverRoot();\n const popup = createMergeTagPopup(emptyText, popoverRoot);\n\n let disposed = false;\n if (getCurrentScope()) {\n onScopeDispose(() => {\n disposed = true;\n popup.close();\n });\n }\n\n function currentMatch(el: HTMLInputElement | HTMLTextAreaElement) {\n const value = el.value ?? \"\";\n const caret = el.selectionStart ?? value.length;\n return findOpenMergeTagTrigger(\n value,\n caret,\n triggerChar as string,\n closingChar,\n );\n }\n\n function applySelection(\n el: HTMLInputElement | HTMLTextAreaElement,\n match: OpenMergeTagTrigger,\n tag: MergeTag,\n ): void {\n const value = el.value ?? modelValue();\n // Absorb a matching closer sitting immediately after the caret so\n // `{{fir}}` → pick → `{{first_name}}`, never `{{first_name}}}}`.\n const afterCaret = value.slice(match.caret);\n const trailing =\n closingChar && afterCaret.startsWith(closingChar)\n ? closingChar.length\n : 0;\n const before = value.slice(0, match.triggerStart);\n const after = value.slice(match.caret + trailing);\n const newValue = before + tag.value + after;\n const newPos = before.length + tag.value.length;\n\n // Keep the field in edit mode BEFORE emitting: once the value gains a tag,\n // `hasMergeTags` flips true, and without this the field would swap to the\n // chip display and unmount the input mid-insert.\n onInsert?.();\n emit(newValue);\n popup.close();\n\n nextTick(() => {\n if (disposed) return;\n const node = elementRef.value;\n node?.focus();\n node?.setSelectionRange(newPos, newPos);\n });\n }\n\n function refresh(): void {\n if (!available || disposed) return;\n const el = elementRef.value;\n if (!el) {\n popup.close();\n return;\n }\n const match = currentMatch(el);\n if (!match) {\n popup.close();\n return;\n }\n\n const items = filterMergeTags(mergeTags, match.query);\n const getRect = () => getCaretRect(el, match.triggerStart);\n const onCommand = (tag: MergeTag) => applySelection(el, match, tag);\n\n if (popup.isOpen()) {\n popup.update({ items, getRect, onCommand });\n } else {\n popup.open({ items, getRect, anchorEl: el, onCommand });\n }\n }\n\n function handleKeydown(event: KeyboardEvent): boolean {\n if (!popup.isOpen()) return false;\n if (event.key === \"Escape\") {\n popup.close();\n return true;\n }\n if (CARET_MOVE_KEYS.has(event.key)) {\n popup.close();\n return false;\n }\n return popup.handleKeyDown(event);\n }\n\n return {\n available,\n refresh,\n handleKeydown,\n close: () => popup.close(),\n isOpen: () => popup.isOpen(),\n };\n}\n","import {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n} from \"@templatical/types\";\nimport {\n computed,\n getCurrentScope,\n nextTick,\n onScopeDispose,\n ref,\n type ComputedRef,\n type Ref,\n} from \"vue\";\nimport { useMergeTag } from \"./useMergeTag\";\nimport { useLogicTag } from \"./useLogicTag\";\nimport { useMergeTagAutocomplete } from \"./useMergeTagAutocomplete\";\n\nexport type MergeTagSegment =\n | { type: \"text\"; value: string }\n | { type: \"mergeTag\"; value: string; label: string }\n | { type: \"logicMergeTag\"; value: string; keyword: string };\n\nexport interface UseMergeTagFieldOptions {\n modelValue: () => string;\n emit: (value: string) => void;\n elementRef: Ref<HTMLInputElement | HTMLTextAreaElement | null>;\n}\n\nexport interface UseMergeTagFieldReturn {\n segments: ComputedRef<MergeTagSegment[]>;\n hasMergeTags: ComputedRef<boolean>;\n canRequestMergeTag: boolean;\n isRequestingMergeTag: Ref<boolean>;\n canInsertLogicTag: boolean;\n isRequestingLogicTag: Ref<boolean>;\n isEditing: Ref<boolean>;\n startEditing: () => void;\n stopEditing: () => void;\n handleInput: (event: Event) => void;\n handleKeydown: (event: KeyboardEvent) => void;\n handleClick: () => void;\n handleBlur: () => void;\n clearValue: () => void;\n insertMergeTag: () => Promise<void>;\n insertLogicTag: () => Promise<void>;\n /** Whether type-ahead autocomplete is active for this field. */\n autocompleteAvailable: boolean;\n}\n\nexport function useMergeTagField(\n options: UseMergeTagFieldOptions,\n): UseMergeTagFieldReturn {\n const { modelValue, emit, elementRef } = options;\n\n const {\n mergeTags,\n canRequestMergeTag,\n isRequesting: isRequestingMergeTag,\n isMergeTagValue,\n getMergeTagLabel,\n requestMergeTag,\n syntax,\n autocomplete,\n } = useMergeTag();\n\n const {\n canInsertLogicTag,\n isRequesting: isRequestingLogicTag,\n requestLogicTag,\n } = useLogicTag();\n\n const isEditing = ref(false);\n let insertingMergeTag = false;\n let disposed = false;\n if (getCurrentScope()) {\n onScopeDispose(() => {\n disposed = true;\n });\n }\n\n // Type-ahead autocomplete for the raw <input>/<textarea>, sharing the exact\n // popup used by the rich-text editor. `onInsert` keeps the field in edit mode\n // so the freshly inserted tag stays visible instead of flipping to chips.\n const typeahead = useMergeTagAutocomplete({\n elementRef,\n modelValue,\n emit,\n mergeTags,\n syntax,\n enabled: autocomplete,\n onInsert: () => {\n isEditing.value = true;\n },\n });\n\n const segments = computed((): MergeTagSegment[] => {\n const val = modelValue();\n if (!val) return [];\n\n const result: MergeTagSegment[] = [];\n const combinedSource = `(${syntax.value.source}|${syntax.logic.source})`;\n const regex = new RegExp(combinedSource, \"g\");\n let lastIndex = 0;\n let match;\n\n while ((match = regex.exec(val)) !== null) {\n if (match.index > lastIndex) {\n result.push({\n type: \"text\",\n value: val.slice(lastIndex, match.index),\n });\n }\n\n const matched = match[0];\n if (isMergeTagValue(matched)) {\n result.push({\n type: \"mergeTag\",\n value: matched,\n label: getMergeTagLabel(matched),\n });\n } else if (isLogicMergeTagValue(matched, syntax)) {\n result.push({\n type: \"logicMergeTag\",\n value: matched,\n keyword: getLogicMergeTagKeyword(matched, syntax),\n });\n } else {\n result.push({ type: \"text\", value: matched });\n }\n\n lastIndex = match.index + matched.length;\n }\n\n if (lastIndex < val.length) {\n result.push({ type: \"text\", value: val.slice(lastIndex) });\n }\n\n return result;\n });\n\n const hasMergeTags = computed(() =>\n segments.value.some(\n (s) => s.type === \"mergeTag\" || s.type === \"logicMergeTag\",\n ),\n );\n\n function startEditing(): void {\n isEditing.value = true;\n nextTick(() => {\n elementRef.value?.focus();\n const len = modelValue()?.length || 0;\n elementRef.value?.setSelectionRange(len, len);\n });\n }\n\n function stopEditing(): void {\n if (insertingMergeTag) return;\n isEditing.value = false;\n }\n\n function handleInput(event: Event): void {\n emit((event.target as HTMLInputElement | HTMLTextAreaElement).value);\n typeahead.refresh();\n }\n\n /**\n * Field keydown: let the autocomplete popup consume navigation/selection\n * keys first (Arrow/Enter/Tab/Escape). If it doesn't, Escape falls through\n * to leaving edit mode — preserving the prior `@keydown.escape` behavior.\n */\n function handleKeydown(event: KeyboardEvent): void {\n if (typeahead.handleKeydown(event)) {\n event.preventDefault();\n return;\n }\n if (event.key === \"Escape\") {\n stopEditing();\n }\n }\n\n /** A click may move the caret into or out of a trigger — re-evaluate. */\n function handleClick(): void {\n typeahead.refresh();\n }\n\n /** Blur closes the popup, then leaves edit mode. */\n function handleBlur(): void {\n typeahead.close();\n stopEditing();\n }\n\n function clearValue(): void {\n emit(\"\");\n }\n\n async function insertMergeTag(): Promise<void> {\n const cursorPos =\n isEditing.value && elementRef.value\n ? (elementRef.value.selectionStart ?? modelValue().length)\n : modelValue().length;\n\n insertingMergeTag = true;\n let mergeTag: Awaited<ReturnType<typeof requestMergeTag>>;\n try {\n mergeTag = await requestMergeTag();\n } finally {\n insertingMergeTag = false;\n }\n\n if (disposed) return;\n\n if (mergeTag) {\n const before = modelValue().slice(0, cursorPos);\n const after = modelValue().slice(cursorPos);\n const newValue = before + mergeTag.value + after;\n emit(newValue);\n\n isEditing.value = true;\n nextTick(() => {\n if (disposed) return;\n const newPos = cursorPos + mergeTag.value.length;\n elementRef.value?.focus();\n elementRef.value?.setSelectionRange(newPos, newPos);\n });\n }\n }\n\n async function insertLogicTag(): Promise<void> {\n // Read the selection straight from the element (kept focused via the\n // button's mousedown.prevent). Null only in display mode → append at end.\n const el = elementRef.value;\n const start = el?.selectionStart ?? modelValue().length;\n const end = el?.selectionEnd ?? start;\n\n insertingMergeTag = true;\n let item: Awaited<ReturnType<typeof requestLogicTag>>;\n try {\n item = await requestLogicTag();\n } finally {\n insertingMergeTag = false;\n }\n\n if (disposed || !item) return;\n\n const val = modelValue();\n let newValue: string;\n let newPos: number;\n if (\"before\" in item) {\n // Pair: wrap the selection with before/after. With no selection\n // (start === end) the caret lands between them so the user types the\n // wrapped content — mirrors the rich-text behavior.\n newValue =\n val.slice(0, start) +\n item.before +\n val.slice(start, end) +\n item.after +\n val.slice(end);\n newPos = start + item.before.length + (end - start);\n } else {\n // Tag: insert at the cursor.\n newValue = val.slice(0, start) + item.value + val.slice(start);\n newPos = start + item.value.length;\n }\n emit(newValue);\n\n isEditing.value = true;\n nextTick(() => {\n if (disposed) return;\n elementRef.value?.focus();\n elementRef.value?.setSelectionRange(newPos, newPos);\n });\n }\n\n return {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n stopEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n autocompleteAvailable: typeahead.available,\n };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { MergeTagSegment } from \"../composables/useMergeTagField\";\nimport { X } from \"@lucide/vue\";\n\ndefineProps<{\n segments: MergeTagSegment[];\n displayClass: string;\n pulse?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"edit\"): void;\n (e: \"clear\"): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction onEdit(): void {\n emit(\"edit\");\n}\n</script>\n\n<template>\n <div\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.clickToEdit\"\n :class=\"[displayClass, { 'tpl-pulse-fill': pulse }]\"\n @click=\"onEdit\"\n @keydown.enter=\"onEdit\"\n @keydown.space.prevent=\"onEdit\"\n >\n <template\n v-for=\"(seg, i) in segments\"\n :key=\"`${seg.type}-${i}-${seg.value}`\"\n >\n <span\n v-if=\"seg.type === 'mergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-primary) 20%,\n transparent\n );\n color: var(--tpl-primary);\n \"\n >\n {{ seg.label }}\n </span>\n <span\n v-else-if=\"seg.type === 'logicMergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: transparent;\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n >\n {{ seg.keyword }}\n </span>\n <span v-else class=\"tpl:text-sm tpl:text-[var(--tpl-text)]\">{{\n seg.value\n }}</span>\n </template>\n <button\n type=\"button\"\n class=\"tpl:ml-auto tpl:flex tpl:size-6 tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-[var(--tpl-text-dim)] tpl:opacity-60 tpl:transition-all tpl:hover:text-[var(--tpl-danger)] tpl:hover:opacity-100\"\n :aria-label=\"t.mergeTag.remove\"\n :title=\"t.mergeTag.remove\"\n @click.stop=\"emit('clear')\"\n >\n <X :size=\"12\" :stroke-width=\"2.5\" />\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { MergeTagSegment } from \"../composables/useMergeTagField\";\nimport { X } from \"@lucide/vue\";\n\ndefineProps<{\n segments: MergeTagSegment[];\n displayClass: string;\n pulse?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"edit\"): void;\n (e: \"clear\"): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction onEdit(): void {\n emit(\"edit\");\n}\n</script>\n\n<template>\n <div\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.clickToEdit\"\n :class=\"[displayClass, { 'tpl-pulse-fill': pulse }]\"\n @click=\"onEdit\"\n @keydown.enter=\"onEdit\"\n @keydown.space.prevent=\"onEdit\"\n >\n <template\n v-for=\"(seg, i) in segments\"\n :key=\"`${seg.type}-${i}-${seg.value}`\"\n >\n <span\n v-if=\"seg.type === 'mergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-primary) 20%,\n transparent\n );\n color: var(--tpl-primary);\n \"\n >\n {{ seg.label }}\n </span>\n <span\n v-else-if=\"seg.type === 'logicMergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: transparent;\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n >\n {{ seg.keyword }}\n </span>\n <span v-else class=\"tpl:text-sm tpl:text-[var(--tpl-text)]\">{{\n seg.value\n }}</span>\n </template>\n <button\n type=\"button\"\n class=\"tpl:ml-auto tpl:flex tpl:size-6 tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-[var(--tpl-text-dim)] tpl:opacity-60 tpl:transition-all tpl:hover:text-[var(--tpl-danger)] tpl:hover:opacity-100\"\n :aria-label=\"t.mergeTag.remove\"\n :title=\"t.mergeTag.remove\"\n @click.stop=\"emit('clear')\"\n >\n <X :size=\"12\" :stroke-width=\"2.5\" />\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { ScanLine } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTagBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[mergeTagBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n :disabled=\"disabled\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <ScanLine :size=\"12\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { ScanLine } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTagBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[mergeTagBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n :disabled=\"disabled\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <ScanLine :size=\"12\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Braces } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst logicBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[logicBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n :disabled=\"disabled\"\n data-testid=\"field-insert-logic-button\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <Braces :size=\"12\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Braces } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst logicBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[logicBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n :disabled=\"disabled\"\n data-testid=\"field-insert-logic-button\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <Braces :size=\"12\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n placeholder?: string;\n rows?: number;\n }>(),\n {\n placeholder: \"\",\n rows: 3,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: textareaRef,\n});\n\nconst textareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:placeholder:text-[var(--tpl-text-dim)] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-20 tpl:cursor-pointer tpl:items-start tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <textarea\n ref=\"textareaRef\"\n :class=\"textareaClass\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n placeholder?: string;\n rows?: number;\n }>(),\n {\n placeholder: \"\",\n rows: 3,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: textareaRef,\n});\n\nconst textareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:placeholder:text-[var(--tpl-text-dim)] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-20 tpl:cursor-pointer tpl:items-start tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <textarea\n ref=\"textareaRef\"\n :class=\"textareaClass\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"./ColorPicker.vue\";\nimport MergeTagTextarea from \"./MergeTagTextarea.vue\";\nimport ToggleSwitch from \"./ToggleSwitch.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type { TemplateSettings } from \"@templatical/types\";\nimport {\n cardClass,\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_BG_COLOR,\n} from \"../constants/styleConstants\";\nimport { Circle, Eye, Globe, Info, Square } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { FONTS_MANAGER_KEY, requireInject } from \"../keys\";\n\nconst props = defineProps<{\n settings: TemplateSettings;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst PREHEADER_MAX_LENGTH = 150;\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"TemplateSettings\");\nconst fontFamilies = computed(() => fontsManager.fonts.value);\n\n// If current font is not in available list (e.g., custom font when disabled), use default\nconst displayedFontFamily = computed(() => {\n const isAvailable = fontFamilies.value.some(\n (font) => font.value === props.settings.fontFamily,\n );\n return isAvailable\n ? props.settings.fontFamily\n : fontsManager.defaultFont.value;\n});\n\nconst widthPresets = [\n { value: 480, label: \"480px\" },\n { value: 600, label: \"600px\" },\n { value: 700, label: \"700px\" },\n { value: 800, label: \"800px\" },\n];\n</script>\n\n<template>\n <aside\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:gap-3 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Layout card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Square\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.layout }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{\n t.templateSettings.widthPreset\n }}</label>\n <div\n class=\"tpl:grid tpl:grid-cols-4 tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n v-for=\"preset in widthPresets\"\n :key=\"preset.value\"\n class=\"tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :style=\"{\n backgroundColor:\n settings.width === preset.value\n ? 'var(--tpl-bg)'\n : 'transparent',\n color:\n settings.width === preset.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow:\n settings.width === preset.value\n ? 'var(--tpl-shadow)'\n : 'none',\n }\"\n @click=\"emit('update', { width: preset.value })\"\n >\n {{ preset.label }}\n </button>\n </div>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.customWidth\n }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"settings.width\"\n min=\"300\"\n max=\"900\"\n @input=\"\n emit('update', {\n width: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n\n <!-- Appearance card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Circle\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.appearance }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\" data-testid=\"template-settings-background\">\n <label :class=\"labelClass\">{{\n t.templateSettings.backgroundColor\n }}</label>\n <ColorPicker\n :model-value=\"settings.backgroundColor\"\n :placeholder=\"DEFAULT_BG_COLOR\"\n @update:model-value=\"emit('update', { backgroundColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.textColor }}</label>\n <ColorPicker\n :model-value=\"settings.textColor ?? ''\"\n seed-color=\"#1a1a1a\"\n @update:model-value=\"emit('update', { textColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.linkColor }}</label>\n <ColorPicker\n :model-value=\"settings.linkColor ?? ''\"\n seed-color=\"#0066cc\"\n @update:model-value=\"\n emit('update', { linkColor: $event || undefined })\n \"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"settings.linkUnderline\"\n :label=\"t.templateSettings.linkUnderline\"\n @update:model-value=\"emit('update', { linkUnderline: $event })\"\n />\n </div>\n\n <div>\n <label :class=\"labelClass\">{{ t.templateSettings.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"displayedFontFamily\"\n @change=\"\n emit('update', {\n fontFamily: ($event.target as HTMLSelectElement).value,\n })\n \"\n >\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n </div>\n\n <!-- Language card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Globe\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.language }}</span>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.contentLocale\n }}</label>\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"settings.locale ?? ''\"\n placeholder=\"en\"\n spellcheck=\"false\"\n autocapitalize=\"off\"\n autocomplete=\"off\"\n @input=\"\n emit('update', {\n locale:\n ($event.target as HTMLInputElement).value.trim() || undefined,\n })\n \"\n />\n <p\n class=\"tpl:mt-1 tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.contentLocaleHint }}\n </p>\n </div>\n </div>\n\n <!-- Preheader card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Eye\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.preheaderText }}</span>\n </div>\n\n <div>\n <MergeTagTextarea\n :model-value=\"settings.preheaderText ?? ''\"\n :placeholder=\"t.templateSettings.preheaderTextPlaceholder\"\n :rows=\"2\"\n @update:model-value=\"\n emit('update', {\n preheaderText: $event.replace(/[\\r\\n]/g, ' ') || undefined,\n })\n \"\n />\n <div\n class=\"tpl:mt-1 tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.preheaderTextHint }}\n </span>\n <span\n class=\"tpl:shrink-0 tpl:text-xs tpl:tabular-nums tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ (settings.preheaderText ?? \"\").length }}/{{\n PREHEADER_MAX_LENGTH\n }}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Tips card -->\n <div\n class=\"tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <div\n class=\"tpl:mb-2.5 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text-muted)]\"\n >\n <Info :size=\"14\" :stroke-width=\"2\" />\n <span>{{ t.templateSettings.tips }}</span>\n </div>\n <ul\n class=\"tpl:m-0 tpl:pl-[18px] tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip1 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip2 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip3 }}\n </li>\n </ul>\n </div>\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"./ColorPicker.vue\";\nimport MergeTagTextarea from \"./MergeTagTextarea.vue\";\nimport ToggleSwitch from \"./ToggleSwitch.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type { TemplateSettings } from \"@templatical/types\";\nimport {\n cardClass,\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_BG_COLOR,\n} from \"../constants/styleConstants\";\nimport { Circle, Eye, Globe, Info, Square } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { FONTS_MANAGER_KEY, requireInject } from \"../keys\";\n\nconst props = defineProps<{\n settings: TemplateSettings;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst PREHEADER_MAX_LENGTH = 150;\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"TemplateSettings\");\nconst fontFamilies = computed(() => fontsManager.fonts.value);\n\n// If current font is not in available list (e.g., custom font when disabled), use default\nconst displayedFontFamily = computed(() => {\n const isAvailable = fontFamilies.value.some(\n (font) => font.value === props.settings.fontFamily,\n );\n return isAvailable\n ? props.settings.fontFamily\n : fontsManager.defaultFont.value;\n});\n\nconst widthPresets = [\n { value: 480, label: \"480px\" },\n { value: 600, label: \"600px\" },\n { value: 700, label: \"700px\" },\n { value: 800, label: \"800px\" },\n];\n</script>\n\n<template>\n <aside\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:gap-3 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Layout card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Square\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.layout }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{\n t.templateSettings.widthPreset\n }}</label>\n <div\n class=\"tpl:grid tpl:grid-cols-4 tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n v-for=\"preset in widthPresets\"\n :key=\"preset.value\"\n class=\"tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :style=\"{\n backgroundColor:\n settings.width === preset.value\n ? 'var(--tpl-bg)'\n : 'transparent',\n color:\n settings.width === preset.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow:\n settings.width === preset.value\n ? 'var(--tpl-shadow)'\n : 'none',\n }\"\n @click=\"emit('update', { width: preset.value })\"\n >\n {{ preset.label }}\n </button>\n </div>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.customWidth\n }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"settings.width\"\n min=\"300\"\n max=\"900\"\n @input=\"\n emit('update', {\n width: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n\n <!-- Appearance card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Circle\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.appearance }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\" data-testid=\"template-settings-background\">\n <label :class=\"labelClass\">{{\n t.templateSettings.backgroundColor\n }}</label>\n <ColorPicker\n :model-value=\"settings.backgroundColor\"\n :placeholder=\"DEFAULT_BG_COLOR\"\n @update:model-value=\"emit('update', { backgroundColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.textColor }}</label>\n <ColorPicker\n :model-value=\"settings.textColor ?? ''\"\n seed-color=\"#1a1a1a\"\n @update:model-value=\"emit('update', { textColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.linkColor }}</label>\n <ColorPicker\n :model-value=\"settings.linkColor ?? ''\"\n seed-color=\"#0066cc\"\n @update:model-value=\"\n emit('update', { linkColor: $event || undefined })\n \"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"settings.linkUnderline\"\n :label=\"t.templateSettings.linkUnderline\"\n @update:model-value=\"emit('update', { linkUnderline: $event })\"\n />\n </div>\n\n <div>\n <label :class=\"labelClass\">{{ t.templateSettings.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"displayedFontFamily\"\n @change=\"\n emit('update', {\n fontFamily: ($event.target as HTMLSelectElement).value,\n })\n \"\n >\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n </div>\n\n <!-- Language card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Globe\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.language }}</span>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.contentLocale\n }}</label>\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"settings.locale ?? ''\"\n placeholder=\"en\"\n spellcheck=\"false\"\n autocapitalize=\"off\"\n autocomplete=\"off\"\n @input=\"\n emit('update', {\n locale:\n ($event.target as HTMLInputElement).value.trim() || undefined,\n })\n \"\n />\n <p\n class=\"tpl:mt-1 tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.contentLocaleHint }}\n </p>\n </div>\n </div>\n\n <!-- Preheader card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Eye\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.preheaderText }}</span>\n </div>\n\n <div>\n <MergeTagTextarea\n :model-value=\"settings.preheaderText ?? ''\"\n :placeholder=\"t.templateSettings.preheaderTextPlaceholder\"\n :rows=\"2\"\n @update:model-value=\"\n emit('update', {\n preheaderText: $event.replace(/[\\r\\n]/g, ' ') || undefined,\n })\n \"\n />\n <div\n class=\"tpl:mt-1 tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.preheaderTextHint }}\n </span>\n <span\n class=\"tpl:shrink-0 tpl:text-xs tpl:tabular-nums tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ (settings.preheaderText ?? \"\").length }}/{{\n PREHEADER_MAX_LENGTH\n }}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Tips card -->\n <div\n class=\"tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <div\n class=\"tpl:mb-2.5 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text-muted)]\"\n >\n <Info :size=\"14\" :stroke-width=\"2\" />\n <span>{{ t.templateSettings.tips }}</span>\n </div>\n <ul\n class=\"tpl:m-0 tpl:pl-[18px] tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip1 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip2 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip3 }}\n </li>\n </ul>\n </div>\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport { inputClass } from \"../constants/styleConstants\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n type?: \"text\" | \"url\";\n placeholder?: string;\n pulse?: boolean;\n }>(),\n {\n type: \"text\",\n placeholder: \"\",\n pulse: false,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: inputRef,\n});\n\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-10 tpl:cursor-pointer tpl:items-center tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3.5 tpl:py-1.5 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n :pulse=\"pulse\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <input\n ref=\"inputRef\"\n :type=\"type\"\n :class=\"[inputClass, { 'tpl-pulse-fill': pulse }]\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.tpl-pulse-fill {\n animation: tpl-field-pulse 1s ease-out;\n}\n\n@keyframes tpl-field-pulse {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--tpl-warning) 30%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 6%, transparent);\n }\n 40% {\n box-shadow: 0 0 0 3px\n color-mix(in srgb, var(--tpl-warning) 15%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 5%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tpl-pulse-fill {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport { inputClass } from \"../constants/styleConstants\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n type?: \"text\" | \"url\";\n placeholder?: string;\n pulse?: boolean;\n }>(),\n {\n type: \"text\",\n placeholder: \"\",\n pulse: false,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: inputRef,\n});\n\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-10 tpl:cursor-pointer tpl:items-center tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3.5 tpl:py-1.5 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n :pulse=\"pulse\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <input\n ref=\"inputRef\"\n :type=\"type\"\n :class=\"[inputClass, { 'tpl-pulse-fill': pulse }]\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.tpl-pulse-fill {\n animation: tpl-field-pulse 1s ease-out;\n}\n\n@keyframes tpl-field-pulse {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--tpl-warning) 30%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 6%, transparent);\n }\n 40% {\n box-shadow: 0 0 0 3px\n color-mix(in srgb, var(--tpl-warning) 15%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 5%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tpl-pulse-fill {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ButtonBlock } from \"@templatical/types\";\n\nconst props = defineProps<{\n block: ButtonBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ButtonBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst DEFAULT_CUSTOM_WIDTH = 200;\n\nconst widthMode = computed<\"auto\" | \"full\" | \"custom\">(() => {\n if (props.block.width === \"full\") return \"full\";\n if (typeof props.block.width === \"number\") return \"custom\";\n return \"auto\";\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ButtonBlock>);\n}\n\nfunction updateWidthMode(mode: string): void {\n if (mode === \"full\") {\n updateField(\"width\", \"full\");\n return;\n }\n if (mode === \"custom\") {\n updateField(\n \"width\",\n typeof props.block.width === \"number\"\n ? props.block.width\n : DEFAULT_CUSTOM_WIDTH,\n );\n return;\n }\n updateField(\"width\", undefined);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render a 0px\n // button (#260). Ignore invalid values and keep the last valid width;\n // the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.button.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.text }}</label>\n <MergeTagInput\n :model-value=\"block.text\"\n type=\"text\"\n @update:model-value=\"updateField('text', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.url }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.button.urlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.button.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.background }}</label>\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.textColor }}</label>\n <ColorPicker\n :model-value=\"block.textColor\"\n @update:model-value=\"updateField('textColor', $event)\"\n />\n </div>\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'borderRadius',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"36\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.button.widthAuto }}</option>\n <option value=\"full\">{{ t.button.fullWidth }}</option>\n <option value=\"custom\">{{ t.button.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align ?? 'center'\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ButtonBlock } from \"@templatical/types\";\n\nconst props = defineProps<{\n block: ButtonBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ButtonBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst DEFAULT_CUSTOM_WIDTH = 200;\n\nconst widthMode = computed<\"auto\" | \"full\" | \"custom\">(() => {\n if (props.block.width === \"full\") return \"full\";\n if (typeof props.block.width === \"number\") return \"custom\";\n return \"auto\";\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ButtonBlock>);\n}\n\nfunction updateWidthMode(mode: string): void {\n if (mode === \"full\") {\n updateField(\"width\", \"full\");\n return;\n }\n if (mode === \"custom\") {\n updateField(\n \"width\",\n typeof props.block.width === \"number\"\n ? props.block.width\n : DEFAULT_CUSTOM_WIDTH,\n );\n return;\n }\n updateField(\"width\", undefined);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render a 0px\n // button (#260). Ignore invalid values and keep the last valid width;\n // the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.button.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.text }}</label>\n <MergeTagInput\n :model-value=\"block.text\"\n type=\"text\"\n @update:model-value=\"updateField('text', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.url }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.button.urlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.button.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.background }}</label>\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.textColor }}</label>\n <ColorPicker\n :model-value=\"block.textColor\"\n @update:model-value=\"updateField('textColor', $event)\"\n />\n </div>\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'borderRadius',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"36\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.button.widthAuto }}</option>\n <option value=\"full\">{{ t.button.fullWidth }}</option>\n <option value=\"custom\">{{ t.button.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align ?? 'center'\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Lock, LockOpen, Minus, Plus } from \"@lucide/vue\";\nimport { computed, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n modelValue: SpacingValue;\n label: string;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: SpacingValue): void;\n}>();\n\nconst { t } = useI18n();\n\ninterface SpacingValue {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nconst isUniform = computed(\n () =>\n props.modelValue.top === props.modelValue.right &&\n props.modelValue.right === props.modelValue.bottom &&\n props.modelValue.bottom === props.modelValue.left,\n);\n\nconst locked = ref(isUniform.value);\n\nwatch(isUniform, (uniform) => {\n if (!uniform && locked.value) {\n locked.value = false;\n }\n});\n\nfunction updateValue(direction: keyof SpacingValue, delta: number): void {\n const currentValue = props.modelValue[direction];\n const newValue = Math.max(0, currentValue + delta);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction setDirectValue(direction: keyof SpacingValue, value: number): void {\n const newValue = Math.max(0, value);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction toggleLock(): void {\n locked.value = !locked.value;\n if (locked.value) {\n const value = props.modelValue.top;\n emit(\"update:modelValue\", {\n top: value,\n right: value,\n bottom: value,\n left: value,\n });\n }\n}\n\nconst stepperBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:w-8 tpl:h-8 tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg)] tpl:border tpl:border-[var(--tpl-border)] tpl:cursor-pointer tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)] tpl:active:bg-[var(--tpl-bg-active)]\";\nconst inputClass =\n \"tpl:w-10 tpl:h-8 tpl:text-center tpl:text-xs tpl:font-medium tpl:border-y tpl:border-x-0 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\n</script>\n\n<template>\n <div class=\"spacing-control\">\n <label\n class=\"tpl:mb-2 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ label }}\n </label>\n\n <div class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-1.5\">\n <!-- Top -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.top\"\n :aria-label=\"t.spacingControl.top\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'top',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Middle row: Left - Lock - Right -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <!-- Left -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.left\"\n :aria-label=\"t.spacingControl.left\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'left',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Lock button -->\n <button\n class=\"tpl:flex tpl:h-8 tpl:w-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"\n locked\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n :aria-label=\"\n locked ? t.spacingControl.unlock : t.spacingControl.lockAll\n \"\n :title=\"locked ? t.spacingControl.unlock : t.spacingControl.lockAll\"\n @click=\"toggleLock\"\n >\n <Lock v-if=\"locked\" :size=\"14\" :stroke-width=\"2\" />\n <LockOpen v-else :size=\"14\" :stroke-width=\"2\" />\n </button>\n\n <!-- Right -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.right\"\n :aria-label=\"t.spacingControl.right\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'right',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Bottom -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.bottom\"\n :aria-label=\"t.spacingControl.bottom\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'bottom',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Lock, LockOpen, Minus, Plus } from \"@lucide/vue\";\nimport { computed, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n modelValue: SpacingValue;\n label: string;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: SpacingValue): void;\n}>();\n\nconst { t } = useI18n();\n\ninterface SpacingValue {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nconst isUniform = computed(\n () =>\n props.modelValue.top === props.modelValue.right &&\n props.modelValue.right === props.modelValue.bottom &&\n props.modelValue.bottom === props.modelValue.left,\n);\n\nconst locked = ref(isUniform.value);\n\nwatch(isUniform, (uniform) => {\n if (!uniform && locked.value) {\n locked.value = false;\n }\n});\n\nfunction updateValue(direction: keyof SpacingValue, delta: number): void {\n const currentValue = props.modelValue[direction];\n const newValue = Math.max(0, currentValue + delta);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction setDirectValue(direction: keyof SpacingValue, value: number): void {\n const newValue = Math.max(0, value);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction toggleLock(): void {\n locked.value = !locked.value;\n if (locked.value) {\n const value = props.modelValue.top;\n emit(\"update:modelValue\", {\n top: value,\n right: value,\n bottom: value,\n left: value,\n });\n }\n}\n\nconst stepperBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:w-8 tpl:h-8 tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg)] tpl:border tpl:border-[var(--tpl-border)] tpl:cursor-pointer tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)] tpl:active:bg-[var(--tpl-bg-active)]\";\nconst inputClass =\n \"tpl:w-10 tpl:h-8 tpl:text-center tpl:text-xs tpl:font-medium tpl:border-y tpl:border-x-0 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\n</script>\n\n<template>\n <div class=\"spacing-control\">\n <label\n class=\"tpl:mb-2 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ label }}\n </label>\n\n <div class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-1.5\">\n <!-- Top -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.top\"\n :aria-label=\"t.spacingControl.top\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'top',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Middle row: Left - Lock - Right -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <!-- Left -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.left\"\n :aria-label=\"t.spacingControl.left\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'left',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Lock button -->\n <button\n class=\"tpl:flex tpl:h-8 tpl:w-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"\n locked\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n :aria-label=\"\n locked ? t.spacingControl.unlock : t.spacingControl.lockAll\n \"\n :title=\"locked ? t.spacingControl.unlock : t.spacingControl.lockAll\"\n @click=\"toggleLock\"\n >\n <Lock v-if=\"locked\" :size=\"14\" :stroke-width=\"2\" />\n <LockOpen v-else :size=\"14\" :stroke-width=\"2\" />\n </button>\n\n <!-- Right -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.right\"\n :aria-label=\"t.spacingControl.right\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'right',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Bottom -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.bottom\"\n :aria-label=\"t.spacingControl.bottom\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'bottom',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ChevronDown } from \"@lucide/vue\";\n\ndefineProps<{\n title: string;\n open: boolean;\n noBorder?: boolean;\n /**\n * Optional test hook on the header button. The title is the only other way to\n * address a section, and it is localized — which the e2e selector rules ban.\n */\n headerTestId?: string;\n}>();\n\ndefineEmits<{\n (e: \"toggle\"): void;\n}>();\n</script>\n\n<template>\n <div\n class=\"tpl:py-3\"\n :class=\"noBorder ? '' : 'tpl:border-t tpl:border-[var(--tpl-border)]'\"\n >\n <button\n type=\"button\"\n :data-testid=\"headerTestId\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"$emit('toggle')\"\n >\n <ChevronDown\n class=\"tpl:transition-transform tpl:duration-200\"\n :class=\"open ? 'tpl:rotate-0' : 'tpl:-rotate-90'\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n <span>{{ title }}</span>\n </button>\n <div v-show=\"open\" class=\"tpl:mt-3\">\n <slot />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ChevronDown } from \"@lucide/vue\";\n\ndefineProps<{\n title: string;\n open: boolean;\n noBorder?: boolean;\n /**\n * Optional test hook on the header button. The title is the only other way to\n * address a section, and it is localized — which the e2e selector rules ban.\n */\n headerTestId?: string;\n}>();\n\ndefineEmits<{\n (e: \"toggle\"): void;\n}>();\n</script>\n\n<template>\n <div\n class=\"tpl:py-3\"\n :class=\"noBorder ? '' : 'tpl:border-t tpl:border-[var(--tpl-border)]'\"\n >\n <button\n type=\"button\"\n :data-testid=\"headerTestId\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"$emit('toggle')\"\n >\n <ChevronDown\n class=\"tpl:transition-transform tpl:duration-200\"\n :class=\"open ? 'tpl:rotate-0' : 'tpl:-rotate-90'\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n <span>{{ title }}</span>\n </button>\n <div v-show=\"open\" class=\"tpl:mt-3\">\n <slot />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport CollapsibleSection from \"./CollapsibleSection.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { Block, DisplayCondition } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed, inject, reactive, ref, watch, type Component } from \"vue\";\nimport {\n DISPLAY_CONDITIONS_KEY,\n ALLOW_CUSTOM_CONDITIONS_KEY,\n} from \"../../keys\";\n\ntype SectionKey = \"spacing\" | \"bg\" | \"display\" | \"condition\";\ntype VisibilityKey = \"desktop\" | \"mobile\";\n\nconst props = defineProps<{\n block: Block;\n isFirstSection?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst displayConditions = inject(DISPLAY_CONDITIONS_KEY, []);\nconst allowCustomConditions = inject(ALLOW_CUSTOM_CONDITIONS_KEY, false);\n\nconst openSections = reactive(new Set<SectionKey>());\nconst customConditionMode = ref(false);\nconst customBefore = ref(\"\");\nconst customAfter = ref(\"\");\n\nconst VISIBILITY_ITEMS: {\n key: VisibilityKey;\n icon: Component;\n labelKey: \"showOnDesktop\" | \"showOnMobile\";\n}[] = [\n { key: \"desktop\", icon: Monitor, labelKey: \"showOnDesktop\" },\n { key: \"mobile\", icon: Smartphone, labelKey: \"showOnMobile\" },\n];\n\nfunction toggleSection(key: SectionKey): void {\n if (openSections.has(key)) openSections.delete(key);\n else openSections.add(key);\n}\n\nconst hasDisplayConditions = computed(\n () => displayConditions.length > 0 || allowCustomConditions,\n);\n\nconst isCustomCondition = computed(() => {\n if (!props.block.displayCondition) return false;\n return !displayConditions.some(\n (c) => c.label === props.block.displayCondition?.label,\n );\n});\n\nfunction startCustomCondition(): void {\n customConditionMode.value = true;\n if (isCustomCondition.value && props.block.displayCondition) {\n customBefore.value = props.block.displayCondition.before;\n customAfter.value = props.block.displayCondition.after ?? \"\";\n } else {\n customBefore.value = \"\";\n customAfter.value = \"\";\n }\n}\n\nfunction applyCustomCondition(): void {\n if (!customBefore.value.trim()) return;\n emit(\"update\", {\n displayCondition: {\n label: t.blockSettings.customCondition,\n before: customBefore.value.trim(),\n after: customAfter.value.trim(),\n },\n });\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n}\n\nwatch(\n () => props.block.displayCondition,\n (condition) => {\n if (!condition) {\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n return;\n }\n if (isCustomCondition.value) {\n customBefore.value = condition.before;\n customAfter.value = condition.after ?? \"\";\n }\n },\n { immediate: true },\n);\n\nconst groupedDisplayConditions = computed(() => {\n const groups: Record<string, DisplayCondition[]> = {};\n for (const condition of displayConditions) {\n const group = condition.group ?? \"\";\n if (!groups[group]) groups[group] = [];\n groups[group].push(condition);\n }\n return groups;\n});\n\nfunction updateStyle(field: string, value: unknown): void {\n emit(\"update\", {\n styles: { ...props.block.styles, [field]: value },\n });\n}\n\nfunction isVisible(key: VisibilityKey): boolean {\n return props.block.visibility?.[key] !== false;\n}\n\nfunction toggleVisibility(key: VisibilityKey): void {\n const next: Record<VisibilityKey, boolean> = {\n desktop: isVisible(\"desktop\"),\n mobile: isVisible(\"mobile\"),\n };\n next[key] = !next[key];\n emit(\"update\", { visibility: next });\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col\" :class=\"isFirstSection ? '' : 'tpl:mt-4'\">\n <CollapsibleSection\n :title=\"t.blockSettings.spacing\"\n :open=\"openSections.has('spacing')\"\n :no-border=\"isFirstSection\"\n @toggle=\"toggleSection('spacing')\"\n >\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"block.styles.padding\"\n @update:model-value=\"updateStyle('padding', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.background\"\n :open=\"openSections.has('bg')\"\n @toggle=\"toggleSection('bg')\"\n >\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.styles.backgroundColor ?? ''\"\n @update:model-value=\"updateStyle('backgroundColor', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.display\"\n :open=\"openSections.has('display')\"\n @toggle=\"toggleSection('display')\"\n >\n <div class=\"tpl:space-y-2\">\n <ToggleSwitch\n v-for=\"item in VISIBILITY_ITEMS\"\n :key=\"item.key\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"isVisible(item.key)\"\n :label=\"t.blockSettings[item.labelKey]\"\n @update:model-value=\"toggleVisibility(item.key)\"\n >\n <span class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <component :is=\"item.icon\" :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.blockSettings[item.labelKey] }}\n </span>\n </ToggleSwitch>\n </div>\n </CollapsibleSection>\n\n <CollapsibleSection\n v-if=\"hasDisplayConditions\"\n header-test-id=\"display-condition-section\"\n :title=\"t.blockSettings.displayCondition\"\n :open=\"openSections.has('condition')\"\n @toggle=\"toggleSection('condition')\"\n >\n <div class=\"tpl:space-y-2\">\n <select\n data-testid=\"display-condition-select\"\n class=\"tpl:w-full tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:outline-none tpl:transition-all tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[0_0_0_3px_var(--tpl-primary-light)]\"\n :class=\"\n block.displayCondition\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-text)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]'\n \"\n :value=\"\n customConditionMode || isCustomCondition\n ? '__custom__'\n : (block.displayCondition?.label ?? '')\n \"\n @change=\"\n (e: Event) => {\n const label = (e.target as HTMLSelectElement).value;\n if (label === '__custom__') {\n startCustomCondition();\n return;\n }\n customConditionMode = false;\n if (!label) {\n emit('update', { displayCondition: undefined });\n return;\n }\n const condition = displayConditions.find(\n (c) => c.label === label,\n );\n if (condition) {\n emit('update', { displayCondition: condition });\n }\n }\n \"\n >\n <option value=\"\">{{ t.blockSettings.noCondition }}</option>\n <template\n v-for=\"(conditions, group) in groupedDisplayConditions\"\n :key=\"group\"\n >\n <optgroup v-if=\"group\" :label=\"String(group)\">\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </optgroup>\n <template v-else>\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </template>\n </template>\n <option v-if=\"allowCustomConditions\" value=\"__custom__\">\n {{ t.blockSettings.customCondition }}\n </option>\n </select>\n\n <template v-if=\"customConditionMode || isCustomCondition\">\n <div class=\"tpl:space-y-2\">\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionBefore }}</label\n >\n <textarea\n v-model=\"customBefore\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionAfter }}</label\n >\n <textarea\n v-model=\"customAfter\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div class=\"tpl:flex tpl:justify-end\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-[var(--tpl-primary)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:hover:opacity-90 tpl:disabled:opacity-50\"\n :disabled=\"!customBefore.trim()\"\n @click=\"applyCustomCondition\"\n >\n {{ t.blockSettings.applyCondition }}\n </button>\n </div>\n </div>\n </template>\n\n <template v-else-if=\"block.displayCondition && !isCustomCondition\">\n <p\n v-if=\"block.displayCondition.description\"\n class=\"tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ block.displayCondition.description }}\n </p>\n <div class=\"tpl:space-y-1\">\n <pre\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.before }}</pre>\n <pre\n v-if=\"block.displayCondition.after\"\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.after }}</pre>\n </div>\n </template>\n </div>\n </CollapsibleSection>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport CollapsibleSection from \"./CollapsibleSection.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { Block, DisplayCondition } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed, inject, reactive, ref, watch, type Component } from \"vue\";\nimport {\n DISPLAY_CONDITIONS_KEY,\n ALLOW_CUSTOM_CONDITIONS_KEY,\n} from \"../../keys\";\n\ntype SectionKey = \"spacing\" | \"bg\" | \"display\" | \"condition\";\ntype VisibilityKey = \"desktop\" | \"mobile\";\n\nconst props = defineProps<{\n block: Block;\n isFirstSection?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst displayConditions = inject(DISPLAY_CONDITIONS_KEY, []);\nconst allowCustomConditions = inject(ALLOW_CUSTOM_CONDITIONS_KEY, false);\n\nconst openSections = reactive(new Set<SectionKey>());\nconst customConditionMode = ref(false);\nconst customBefore = ref(\"\");\nconst customAfter = ref(\"\");\n\nconst VISIBILITY_ITEMS: {\n key: VisibilityKey;\n icon: Component;\n labelKey: \"showOnDesktop\" | \"showOnMobile\";\n}[] = [\n { key: \"desktop\", icon: Monitor, labelKey: \"showOnDesktop\" },\n { key: \"mobile\", icon: Smartphone, labelKey: \"showOnMobile\" },\n];\n\nfunction toggleSection(key: SectionKey): void {\n if (openSections.has(key)) openSections.delete(key);\n else openSections.add(key);\n}\n\nconst hasDisplayConditions = computed(\n () => displayConditions.length > 0 || allowCustomConditions,\n);\n\nconst isCustomCondition = computed(() => {\n if (!props.block.displayCondition) return false;\n return !displayConditions.some(\n (c) => c.label === props.block.displayCondition?.label,\n );\n});\n\nfunction startCustomCondition(): void {\n customConditionMode.value = true;\n if (isCustomCondition.value && props.block.displayCondition) {\n customBefore.value = props.block.displayCondition.before;\n customAfter.value = props.block.displayCondition.after ?? \"\";\n } else {\n customBefore.value = \"\";\n customAfter.value = \"\";\n }\n}\n\nfunction applyCustomCondition(): void {\n if (!customBefore.value.trim()) return;\n emit(\"update\", {\n displayCondition: {\n label: t.blockSettings.customCondition,\n before: customBefore.value.trim(),\n after: customAfter.value.trim(),\n },\n });\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n}\n\nwatch(\n () => props.block.displayCondition,\n (condition) => {\n if (!condition) {\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n return;\n }\n if (isCustomCondition.value) {\n customBefore.value = condition.before;\n customAfter.value = condition.after ?? \"\";\n }\n },\n { immediate: true },\n);\n\nconst groupedDisplayConditions = computed(() => {\n const groups: Record<string, DisplayCondition[]> = {};\n for (const condition of displayConditions) {\n const group = condition.group ?? \"\";\n if (!groups[group]) groups[group] = [];\n groups[group].push(condition);\n }\n return groups;\n});\n\nfunction updateStyle(field: string, value: unknown): void {\n emit(\"update\", {\n styles: { ...props.block.styles, [field]: value },\n });\n}\n\nfunction isVisible(key: VisibilityKey): boolean {\n return props.block.visibility?.[key] !== false;\n}\n\nfunction toggleVisibility(key: VisibilityKey): void {\n const next: Record<VisibilityKey, boolean> = {\n desktop: isVisible(\"desktop\"),\n mobile: isVisible(\"mobile\"),\n };\n next[key] = !next[key];\n emit(\"update\", { visibility: next });\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col\" :class=\"isFirstSection ? '' : 'tpl:mt-4'\">\n <CollapsibleSection\n :title=\"t.blockSettings.spacing\"\n :open=\"openSections.has('spacing')\"\n :no-border=\"isFirstSection\"\n @toggle=\"toggleSection('spacing')\"\n >\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"block.styles.padding\"\n @update:model-value=\"updateStyle('padding', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.background\"\n :open=\"openSections.has('bg')\"\n @toggle=\"toggleSection('bg')\"\n >\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.styles.backgroundColor ?? ''\"\n @update:model-value=\"updateStyle('backgroundColor', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.display\"\n :open=\"openSections.has('display')\"\n @toggle=\"toggleSection('display')\"\n >\n <div class=\"tpl:space-y-2\">\n <ToggleSwitch\n v-for=\"item in VISIBILITY_ITEMS\"\n :key=\"item.key\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"isVisible(item.key)\"\n :label=\"t.blockSettings[item.labelKey]\"\n @update:model-value=\"toggleVisibility(item.key)\"\n >\n <span class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <component :is=\"item.icon\" :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.blockSettings[item.labelKey] }}\n </span>\n </ToggleSwitch>\n </div>\n </CollapsibleSection>\n\n <CollapsibleSection\n v-if=\"hasDisplayConditions\"\n header-test-id=\"display-condition-section\"\n :title=\"t.blockSettings.displayCondition\"\n :open=\"openSections.has('condition')\"\n @toggle=\"toggleSection('condition')\"\n >\n <div class=\"tpl:space-y-2\">\n <select\n data-testid=\"display-condition-select\"\n class=\"tpl:w-full tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:outline-none tpl:transition-all tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[0_0_0_3px_var(--tpl-primary-light)]\"\n :class=\"\n block.displayCondition\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-text)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]'\n \"\n :value=\"\n customConditionMode || isCustomCondition\n ? '__custom__'\n : (block.displayCondition?.label ?? '')\n \"\n @change=\"\n (e: Event) => {\n const label = (e.target as HTMLSelectElement).value;\n if (label === '__custom__') {\n startCustomCondition();\n return;\n }\n customConditionMode = false;\n if (!label) {\n emit('update', { displayCondition: undefined });\n return;\n }\n const condition = displayConditions.find(\n (c) => c.label === label,\n );\n if (condition) {\n emit('update', { displayCondition: condition });\n }\n }\n \"\n >\n <option value=\"\">{{ t.blockSettings.noCondition }}</option>\n <template\n v-for=\"(conditions, group) in groupedDisplayConditions\"\n :key=\"group\"\n >\n <optgroup v-if=\"group\" :label=\"String(group)\">\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </optgroup>\n <template v-else>\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </template>\n </template>\n <option v-if=\"allowCustomConditions\" value=\"__custom__\">\n {{ t.blockSettings.customCondition }}\n </option>\n </select>\n\n <template v-if=\"customConditionMode || isCustomCondition\">\n <div class=\"tpl:space-y-2\">\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionBefore }}</label\n >\n <textarea\n v-model=\"customBefore\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionAfter }}</label\n >\n <textarea\n v-model=\"customAfter\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div class=\"tpl:flex tpl:justify-end\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-[var(--tpl-primary)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:hover:opacity-90 tpl:disabled:opacity-50\"\n :disabled=\"!customBefore.trim()\"\n @click=\"applyCustomCondition\"\n >\n {{ t.blockSettings.applyCondition }}\n </button>\n </div>\n </div>\n </template>\n\n <template v-else-if=\"block.displayCondition && !isCustomCondition\">\n <p\n v-if=\"block.displayCondition.description\"\n class=\"tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ block.displayCondition.description }}\n </p>\n <div class=\"tpl:space-y-1\">\n <pre\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.before }}</pre>\n <pre\n v-if=\"block.displayCondition.after\"\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.after }}</pre>\n </div>\n </template>\n </div>\n </CollapsibleSection>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockBooleanField } from \"@templatical/types\";\nimport { Lock } from \"@lucide/vue\";\nimport ToggleSwitch from \"../../ToggleSwitch.vue\";\n\ndefineProps<{\n field: CustomBlockBooleanField;\n modelValue: boolean;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl:mb-3.5\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n >\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"modelValue\"\n :label=\"field.label\"\n :required=\"field.required\"\n :disabled=\"readOnly\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n {{ field.label }}\n <Lock\n v-if=\"readOnly\"\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n />\n </ToggleSwitch>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockBooleanField } from \"@templatical/types\";\nimport { Lock } from \"@lucide/vue\";\nimport ToggleSwitch from \"../../ToggleSwitch.vue\";\n\ndefineProps<{\n field: CustomBlockBooleanField;\n modelValue: boolean;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl:mb-3.5\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n >\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"modelValue\"\n :label=\"field.label\"\n :required=\"field.required\"\n :disabled=\"readOnly\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n {{ field.label }}\n <Lock\n v-if=\"readOnly\"\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n />\n </ToggleSwitch>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { labelClass } from \"../../../constants/styleConstants\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport { Lock } from \"@lucide/vue\";\n\ndefineProps<{\n label: string;\n required?: boolean;\n readOnly?: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ label }}\n <!-- Same treatment as the asterisk below, and the same string the seven\n field components put on the input itself: `readOnly` here is only ever\n `field.readOnly && block.dataSourceFetched`, so \"loaded from your data\n source\" is the reason, not a generic lock. -->\n <span v-if=\"readOnly\" :title=\"t.customBlocks.dataSource.readOnlyTooltip\">\n <Lock\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n aria-hidden=\"true\"\n />\n <span class=\"tpl-sr-only\">{{\n t.customBlocks.dataSource.readOnlyTooltip\n }}</span>\n </span>\n <!-- The asterisk is a sighted convention, not a label: on its own it\n announces as \"asterisk\" or as nothing at all, so the requirement is\n invisible to a screen reader. The glyph is therefore hidden from the\n accessibility tree and the text carried alongside it, which is also\n what makes the state translatable rather than punctuation. -->\n <span\n v-if=\"required\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.required\"\n >\n <span aria-hidden=\"true\">*</span>\n <span class=\"tpl-sr-only\">{{ t.customBlocks.fields.required }}</span>\n </span>\n </label>\n <slot />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { labelClass } from \"../../../constants/styleConstants\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport { Lock } from \"@lucide/vue\";\n\ndefineProps<{\n label: string;\n required?: boolean;\n readOnly?: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ label }}\n <!-- Same treatment as the asterisk below, and the same string the seven\n field components put on the input itself: `readOnly` here is only ever\n `field.readOnly && block.dataSourceFetched`, so \"loaded from your data\n source\" is the reason, not a generic lock. -->\n <span v-if=\"readOnly\" :title=\"t.customBlocks.dataSource.readOnlyTooltip\">\n <Lock\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n aria-hidden=\"true\"\n />\n <span class=\"tpl-sr-only\">{{\n t.customBlocks.dataSource.readOnlyTooltip\n }}</span>\n </span>\n <!-- The asterisk is a sighted convention, not a label: on its own it\n announces as \"asterisk\" or as nothing at all, so the requirement is\n invisible to a screen reader. The glyph is therefore hidden from the\n accessibility tree and the text carried alongside it, which is also\n what makes the state translatable rather than punctuation. -->\n <span\n v-if=\"required\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.required\"\n >\n <span aria-hidden=\"true\">*</span>\n <span class=\"tpl-sr-only\">{{ t.customBlocks.fields.required }}</span>\n </span>\n </label>\n <slot />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockColorField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport ColorPicker from \"../../ColorPicker.vue\";\nimport { COLORS_KEY } from \"../../../keys\";\nimport { DEFAULT_RESOLVED_COLORS } from \"../../../utils/resolveColorsConfig\";\nimport { resolveFieldColors } from \"../../../utils/resolveFieldColors\";\n\nconst props = defineProps<{\n field: CustomBlockColorField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\n// Narrow the injected editor-wide palette by this field's own `presets` /\n// `allowCustom`. Presentational only: the field config is audited once per\n// block definition at registration time (`useEditorCore` →\n// `collectColorFieldIssues`), never here — this component mounts again on every\n// block selection and once per repeater item, so warning from here would repeat\n// the same message indefinitely.\nconst editorColors = inject(COLORS_KEY, DEFAULT_RESOLVED_COLORS);\nconst colors = computed(() => resolveFieldColors(props.field, editorColors));\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <ColorPicker\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :disabled=\"readOnly\"\n :presets=\"colors.presets\"\n :allow-custom=\"colors.allowCustom\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockColorField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport ColorPicker from \"../../ColorPicker.vue\";\nimport { COLORS_KEY } from \"../../../keys\";\nimport { DEFAULT_RESOLVED_COLORS } from \"../../../utils/resolveColorsConfig\";\nimport { resolveFieldColors } from \"../../../utils/resolveFieldColors\";\n\nconst props = defineProps<{\n field: CustomBlockColorField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\n// Narrow the injected editor-wide palette by this field's own `presets` /\n// `allowCustom`. Presentational only: the field config is audited once per\n// block definition at registration time (`useEditorCore` →\n// `collectColorFieldIssues`), never here — this component mounts again on every\n// block selection and once per repeater item, so warning from here would repeat\n// the same message indefinitely.\nconst editorColors = inject(COLORS_KEY, DEFAULT_RESOLVED_COLORS);\nconst colors = computed(() => resolveFieldColors(props.field, editorColors));\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <ColorPicker\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :disabled=\"readOnly\"\n :presets=\"colors.presets\"\n :allow-custom=\"colors.allowCustom\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockImageField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY } from \"../../../keys\";\nimport { useAliveFlag } from \"../../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../../composables/useImageDrop\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\nconst props = defineProps<{\n field: CustomBlockImageField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nasync function browseMedia(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n emit(\"update:modelValue\", result.url);\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !props.readOnly,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) emit(\"update:modelValue\", result.url);\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <input\n v-if=\"readOnly\"\n type=\"url\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <input\n v-else\n type=\"url\"\n :class=\"inputClass\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n @input=\"\n emit('update:modelValue', ($event.target as HTMLInputElement).value)\n \"\n />\n <button\n v-if=\"canBrowseMedia && !readOnly\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-primary)] tpl:bg-[var(--tpl-bg)]\"\n @click=\"browseMedia()\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockImageField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY } from \"../../../keys\";\nimport { useAliveFlag } from \"../../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../../composables/useImageDrop\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\nconst props = defineProps<{\n field: CustomBlockImageField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nasync function browseMedia(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n emit(\"update:modelValue\", result.url);\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !props.readOnly,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) emit(\"update:modelValue\", result.url);\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <input\n v-if=\"readOnly\"\n type=\"url\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <input\n v-else\n type=\"url\"\n :class=\"inputClass\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n @input=\"\n emit('update:modelValue', ($event.target as HTMLInputElement).value)\n \"\n />\n <button\n v-if=\"canBrowseMedia && !readOnly\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-primary)] tpl:bg-[var(--tpl-bg)]\"\n @click=\"browseMedia()\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockNumberField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockNumberField;\n modelValue: number;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: number): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n type=\"number\"\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :min=\"field.min\"\n :max=\"field.max\"\n :step=\"field.step\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @input=\"\n !readOnly &&\n emit(\n 'update:modelValue',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockNumberField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockNumberField;\n modelValue: number;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: number): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n type=\"number\"\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :min=\"field.min\"\n :max=\"field.max\"\n :step=\"field.step\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @input=\"\n !readOnly &&\n emit(\n 'update:modelValue',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockRepeatableField } from \"@templatical/types\";\nimport { Plus, Trash2 } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { resolveFieldComponent } from \"./index\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport { addItemBtnClass } from \"../../../constants/styleConstants\";\n\nconst props = defineProps<{\n field: CustomBlockRepeatableField;\n modelValue: Record<string, unknown>[];\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: Record<string, unknown>[]): void;\n}>();\n\nconst { t, format } = useI18n();\n\nconst items = computed(() => props.modelValue || []);\n\nconst canAdd = computed(\n () => !props.field.maxItems || items.value.length < props.field.maxItems,\n);\n\nconst canRemove = computed(\n () => !props.field.minItems || items.value.length > props.field.minItems,\n);\n\nfunction addItem(): void {\n if (!canAdd.value || props.readOnly) {\n return;\n }\n\n const newItem: Record<string, unknown> = {};\n for (const subField of props.field.fields) {\n newItem[subField.key] = subField.default ?? \"\";\n }\n\n emit(\"update:modelValue\", [...items.value, newItem]);\n}\n\nfunction removeItem(index: number): void {\n if (!canRemove.value || props.readOnly) {\n return;\n }\n\n const updated = [...items.value];\n updated.splice(index, 1);\n emit(\"update:modelValue\", updated);\n}\n\nfunction updateItemField(index: number, key: string, value: unknown): void {\n const updated = items.value.map((item, i) =>\n i === index ? { ...item, [key]: value } : item,\n );\n emit(\"update:modelValue\", updated);\n}\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"(item, index) in items\"\n :key=\"`${field.key}-${index}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-3\"\n >\n <div class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:justify-between\">\n <span\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n >\n #{{ index + 1 }}\n </span>\n <button\n v-if=\"canRemove && !readOnly\"\n type=\"button\"\n class=\"tpl:flex tpl:size-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.removeItem\"\n @click=\"removeItem(index)\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <template v-for=\"subField in field.fields\" :key=\"subField.key\">\n <component\n :is=\"resolveFieldComponent(subField.type)\"\n :field=\"subField\"\n :model-value=\"item[subField.key]\"\n :read-only=\"readOnly\"\n @update:model-value=\"updateItemField(index, subField.key, $event)\"\n />\n </template>\n </div>\n\n <button\n v-if=\"canAdd && !readOnly\"\n type=\"button\"\n :class=\"addItemBtnClass\"\n @click=\"addItem\"\n >\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.customBlocks.fields.addItem }}\n </button>\n\n <p\n v-if=\"!canAdd && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.customBlocks.fields.maxItemsReached }}\n </p>\n\n <!-- The counterpart to the message above: each explains one control that\n is absent rather than disabled, and a missing control with no\n explanation reads as a bug. Both can be true at once for a\n fixed-length list (`minItems === maxItems`), where the two together\n are what say the length is fixed. -->\n <p\n v-if=\"!canRemove && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.customBlocks.fields.minItemsRequired, {\n count: field.minItems ?? 0,\n })\n }}\n </p>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockRepeatableField } from \"@templatical/types\";\nimport { Plus, Trash2 } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { resolveFieldComponent } from \"./index\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport { addItemBtnClass } from \"../../../constants/styleConstants\";\n\nconst props = defineProps<{\n field: CustomBlockRepeatableField;\n modelValue: Record<string, unknown>[];\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: Record<string, unknown>[]): void;\n}>();\n\nconst { t, format } = useI18n();\n\nconst items = computed(() => props.modelValue || []);\n\nconst canAdd = computed(\n () => !props.field.maxItems || items.value.length < props.field.maxItems,\n);\n\nconst canRemove = computed(\n () => !props.field.minItems || items.value.length > props.field.minItems,\n);\n\nfunction addItem(): void {\n if (!canAdd.value || props.readOnly) {\n return;\n }\n\n const newItem: Record<string, unknown> = {};\n for (const subField of props.field.fields) {\n newItem[subField.key] = subField.default ?? \"\";\n }\n\n emit(\"update:modelValue\", [...items.value, newItem]);\n}\n\nfunction removeItem(index: number): void {\n if (!canRemove.value || props.readOnly) {\n return;\n }\n\n const updated = [...items.value];\n updated.splice(index, 1);\n emit(\"update:modelValue\", updated);\n}\n\nfunction updateItemField(index: number, key: string, value: unknown): void {\n const updated = items.value.map((item, i) =>\n i === index ? { ...item, [key]: value } : item,\n );\n emit(\"update:modelValue\", updated);\n}\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"(item, index) in items\"\n :key=\"`${field.key}-${index}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-3\"\n >\n <div class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:justify-between\">\n <span\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n >\n #{{ index + 1 }}\n </span>\n <button\n v-if=\"canRemove && !readOnly\"\n type=\"button\"\n class=\"tpl:flex tpl:size-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.removeItem\"\n @click=\"removeItem(index)\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <template v-for=\"subField in field.fields\" :key=\"subField.key\">\n <component\n :is=\"resolveFieldComponent(subField.type)\"\n :field=\"subField\"\n :model-value=\"item[subField.key]\"\n :read-only=\"readOnly\"\n @update:model-value=\"updateItemField(index, subField.key, $event)\"\n />\n </template>\n </div>\n\n <button\n v-if=\"canAdd && !readOnly\"\n type=\"button\"\n :class=\"addItemBtnClass\"\n @click=\"addItem\"\n >\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.customBlocks.fields.addItem }}\n </button>\n\n <p\n v-if=\"!canAdd && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.customBlocks.fields.maxItemsReached }}\n </p>\n\n <!-- The counterpart to the message above: each explains one control that\n is absent rather than disabled, and a missing control with no\n explanation reads as a bug. Both can be true at once for a\n fixed-length list (`minItems === maxItems`), where the two together\n are what say the length is fixed. -->\n <p\n v-if=\"!canRemove && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.customBlocks.fields.minItemsRequired, {\n count: field.minItems ?? 0,\n })\n }}\n </p>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockSelectField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockSelectField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <select\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @change=\"\n !readOnly &&\n emit('update:modelValue', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option\n v-for=\"option in field.options\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockSelectField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockSelectField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <select\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @change=\"\n !readOnly &&\n emit('update:modelValue', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option\n v-for=\"option in field.options\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagInput from \"../../MergeTagInput.vue\";\n\ndefineProps<{\n field: CustomBlockTextField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n v-if=\"readOnly\"\n type=\"text\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <MergeTagInput\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagInput from \"../../MergeTagInput.vue\";\n\ndefineProps<{\n field: CustomBlockTextField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n v-if=\"readOnly\"\n type=\"text\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <MergeTagInput\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextareaField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagTextarea from \"../../MergeTagTextarea.vue\";\n\ndefineProps<{\n field: CustomBlockTextareaField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst readOnlyTextareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:opacity-60 tpl:cursor-not-allowed\";\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <textarea\n v-if=\"readOnly\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n rows=\"3\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n :class=\"readOnlyTextareaClass\"\n />\n <MergeTagTextarea\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextareaField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagTextarea from \"../../MergeTagTextarea.vue\";\n\ndefineProps<{\n field: CustomBlockTextareaField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst readOnlyTextareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:opacity-60 tpl:cursor-not-allowed\";\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <textarea\n v-if=\"readOnly\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n rows=\"3\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n :class=\"readOnlyTextareaClass\"\n />\n <MergeTagTextarea\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","import type { CustomBlockFieldType } from \"@templatical/types\";\nimport type { Component } from \"vue\";\nimport BooleanField from \"./BooleanField.vue\";\nimport ColorField from \"./ColorField.vue\";\nimport ImageField from \"./ImageField.vue\";\nimport NumberField from \"./NumberField.vue\";\nimport RepeatableField from \"./RepeatableField.vue\";\nimport SelectField from \"./SelectField.vue\";\nimport TextField from \"./TextField.vue\";\nimport TextareaField from \"./TextareaField.vue\";\n\nexport const fieldComponentMap: Record<CustomBlockFieldType, Component> = {\n text: TextField,\n textarea: TextareaField,\n image: ImageField,\n color: ColorField,\n number: NumberField,\n select: SelectField,\n boolean: BooleanField,\n repeatable: RepeatableField,\n};\n\nexport function resolveFieldComponent(type: CustomBlockFieldType): Component {\n return fieldComponentMap[type] ?? TextField;\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useDataSourceFetch } from \"@templatical/core\";\nimport type { CustomBlock, CustomBlockField } from \"@templatical/types\";\nimport { CircleAlert, RefreshCw } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY } from \"../../keys\";\nimport { resolveFieldComponent } from \"./fields\";\n\nconst props = defineProps<{\n block: CustomBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"updateFieldValues\", values: Record<string, unknown>): void;\n (e: \"updateDataSourceFetched\", fetched: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst definition = computed(() =>\n customBlockDefinitions.find((d) => d.type === props.block.customType),\n);\n\nconst blockRef = computed(() => props.block);\n\nconst {\n isFetching,\n fetchError,\n fetch: fetchData,\n hasDataSource,\n needsFetch,\n} = useDataSourceFetch({\n definition,\n block: blockRef,\n onUpdate: (fieldValues, fetched) => {\n emit(\"updateFieldValues\", fieldValues);\n emit(\"updateDataSourceFetched\", fetched);\n },\n});\n\nfunction isFieldReadOnly(field: CustomBlockField): boolean {\n return (\n field.readOnly === true &&\n hasDataSource.value &&\n !!props.block.dataSourceFetched\n );\n}\n\nfunction updateField(key: string, value: unknown): void {\n emit(\"updateFieldValues\", {\n ...props.block.fieldValues,\n [key]: value,\n });\n}\n</script>\n\n<template>\n <div v-if=\"!definition\" class=\"tpl:p-4\">\n <p\n class=\"tpl:m-0 tpl:text-center tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.toolbar.noDefinition }}\n </p>\n </div>\n\n <div v-else>\n <p\n v-if=\"definition.description\"\n class=\"tpl:m-0 tpl:mb-3 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ definition.description }}\n </p>\n\n <div v-if=\"hasDataSource\" class=\"tpl:mb-4\">\n <!-- Before fetch: prominent button -->\n <button\n v-if=\"needsFetch && !isFetching\"\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-md tpl:px-3 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:bg-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n {{\n definition?.dataSource?.label || t.customBlocks.dataSource.fetchButton\n }}\n </button>\n\n <!-- Loading / Change button -->\n <div v-else class=\"tpl:flex tpl:h-[32px] tpl:items-center\">\n <div\n v-if=\"isFetching\"\n class=\"tpl:w-full tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.dataSource.fetching }}\n </div>\n <button\n v-else\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-primary)] tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n <RefreshCw :size=\"12\" />\n {{ t.customBlocks.dataSource.changeButton }}\n </button>\n </div>\n\n <p\n v-if=\"fetchError\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert :size=\"14\" class=\"tpl:shrink-0\" />\n {{ t.customBlocks.dataSource.fetchError }}\n </p>\n </div>\n\n <template v-for=\"field in definition.fields\" :key=\"field.key\">\n <component\n :is=\"resolveFieldComponent(field.type)\"\n :field=\"field\"\n :model-value=\"block.fieldValues[field.key]\"\n :read-only=\"isFieldReadOnly(field)\"\n @update:model-value=\"updateField(field.key, $event)\"\n />\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useDataSourceFetch } from \"@templatical/core\";\nimport type { CustomBlock, CustomBlockField } from \"@templatical/types\";\nimport { CircleAlert, RefreshCw } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY } from \"../../keys\";\nimport { resolveFieldComponent } from \"./fields\";\n\nconst props = defineProps<{\n block: CustomBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"updateFieldValues\", values: Record<string, unknown>): void;\n (e: \"updateDataSourceFetched\", fetched: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst definition = computed(() =>\n customBlockDefinitions.find((d) => d.type === props.block.customType),\n);\n\nconst blockRef = computed(() => props.block);\n\nconst {\n isFetching,\n fetchError,\n fetch: fetchData,\n hasDataSource,\n needsFetch,\n} = useDataSourceFetch({\n definition,\n block: blockRef,\n onUpdate: (fieldValues, fetched) => {\n emit(\"updateFieldValues\", fieldValues);\n emit(\"updateDataSourceFetched\", fetched);\n },\n});\n\nfunction isFieldReadOnly(field: CustomBlockField): boolean {\n return (\n field.readOnly === true &&\n hasDataSource.value &&\n !!props.block.dataSourceFetched\n );\n}\n\nfunction updateField(key: string, value: unknown): void {\n emit(\"updateFieldValues\", {\n ...props.block.fieldValues,\n [key]: value,\n });\n}\n</script>\n\n<template>\n <div v-if=\"!definition\" class=\"tpl:p-4\">\n <p\n class=\"tpl:m-0 tpl:text-center tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.toolbar.noDefinition }}\n </p>\n </div>\n\n <div v-else>\n <p\n v-if=\"definition.description\"\n class=\"tpl:m-0 tpl:mb-3 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ definition.description }}\n </p>\n\n <div v-if=\"hasDataSource\" class=\"tpl:mb-4\">\n <!-- Before fetch: prominent button -->\n <button\n v-if=\"needsFetch && !isFetching\"\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-md tpl:px-3 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:bg-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n {{\n definition?.dataSource?.label || t.customBlocks.dataSource.fetchButton\n }}\n </button>\n\n <!-- Loading / Change button -->\n <div v-else class=\"tpl:flex tpl:h-[32px] tpl:items-center\">\n <div\n v-if=\"isFetching\"\n class=\"tpl:w-full tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.dataSource.fetching }}\n </div>\n <button\n v-else\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-primary)] tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n <RefreshCw :size=\"12\" />\n {{ t.customBlocks.dataSource.changeButton }}\n </button>\n </div>\n\n <p\n v-if=\"fetchError\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert :size=\"14\" class=\"tpl:shrink-0\" />\n {{ t.customBlocks.dataSource.fetchError }}\n </p>\n </div>\n\n <template v-for=\"field in definition.fields\" :key=\"field.key\">\n <component\n :is=\"resolveFieldComponent(field.type)\"\n :field=\"field\"\n :model-value=\"block.fieldValues[field.key]\"\n :read-only=\"isFieldReadOnly(field)\"\n @update:model-value=\"updateField(field.key, $event)\"\n />\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { DividerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: DividerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<DividerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<DividerBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.style }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'solid', label: t.divider.solid },\n { value: 'dashed', label: t.divider.dashed },\n { value: 'dotted', label: t.divider.dotted },\n ]\"\n :model-value=\"block.lineStyle\"\n @update:model-value=\"updateField('lineStyle', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.color }}</label>\n <ColorPicker\n :model-value=\"block.color\"\n @update:model-value=\"updateField('color', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.thickness }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.thickness\"\n min=\"1\"\n max=\"10\"\n @input=\"\n updateField(\n 'thickness',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { DividerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: DividerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<DividerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<DividerBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.style }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'solid', label: t.divider.solid },\n { value: 'dashed', label: t.divider.dashed },\n { value: 'dotted', label: t.divider.dotted },\n ]\"\n :model-value=\"block.lineStyle\"\n @update:model-value=\"updateField('lineStyle', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.color }}</label>\n <ColorPicker\n :model-value=\"block.color\"\n @update:model-value=\"updateField('color', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.thickness }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.thickness\"\n min=\"1\"\n max=\"10\"\n @input=\"\n updateField(\n 'thickness',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { HtmlBlock } from \"@templatical/types\";\nimport { Info } from \"@lucide/vue\";\n\ndefineProps<{\n block: HtmlBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<HtmlBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.html.content }}</label>\n <textarea\n :value=\"block.content\"\n :placeholder=\"'<div>...</div>'\"\n rows=\"10\"\n :class=\"monoTextareaClass\"\n @input=\"\n emit('update', {\n content: ($event.target as HTMLTextAreaElement).value,\n })\n \"\n />\n <p\n class=\"tpl:mt-1.5 tpl:flex tpl:items-start tpl:gap-1.5 tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Info :size=\"12\" class=\"tpl:mt-0.5 tpl:shrink-0\" />\n {{ t.html.sanitizationHint }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { HtmlBlock } from \"@templatical/types\";\nimport { Info } from \"@lucide/vue\";\n\ndefineProps<{\n block: HtmlBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<HtmlBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.html.content }}</label>\n <textarea\n :value=\"block.content\"\n :placeholder=\"'<div>...</div>'\"\n rows=\"10\"\n :class=\"monoTextareaClass\"\n @input=\"\n emit('update', {\n content: ($event.target as HTMLTextAreaElement).value,\n })\n \"\n />\n <p\n class=\"tpl:mt-1.5 tpl:flex tpl:items-start tpl:gap-1.5 tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Info :size=\"12\" class=\"tpl:mt-0.5 tpl:shrink-0\" />\n {{ t.html.sanitizationHint }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ImageBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../composables/useImageDrop\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: ImageBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ImageBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nconst pulseSrc = ref(false);\nconst pulseAlt = ref(false);\n\nconst { start: startPulseSrc } = useTimeoutFn(\n () => {\n pulseSrc.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nconst WIDTH_PRESETS = [300, 400, 500];\nconst DEFAULT_CUSTOM_WIDTH = 350;\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst widthMode = computed(() => {\n const w = props.block.width;\n if (w === \"full\") return \"full\";\n if (WIDTH_PRESETS.includes(w)) return String(w);\n return \"custom\";\n});\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ImageBlock>);\n}\n\nfunction updateWidthMode(value: string): void {\n if (value === \"custom\") {\n const w = props.block.width;\n if (typeof w !== \"number\" || WIDTH_PRESETS.includes(w)) {\n updateField(\"width\", DEFAULT_CUSTOM_WIDTH);\n }\n return;\n }\n updateField(\"width\", value === \"full\" ? \"full\" : Number(value));\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // Same guard as the custom width below: an empty number field yields\n // Number(\"\") === 0, which would collapse the image to nothing. Absent means\n // \"keep the aspect ratio\", so an invalid value must not overwrite the last\n // valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render an\n // invisible image (#259). Ignore invalid values and keep the last valid\n // width; the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\n// A merge-tag src is a deliberate dynamic value — never clobber it via drop.\nconst hasMergeTagSrc = computed(() =>\n containsMergeTag(props.block.src, mergeTagSyntax),\n);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !hasMergeTagSrc.value,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.imageUrl }}</label>\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <MergeTagInput\n :model-value=\"block.src\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n :pulse=\"pulseSrc\"\n @update:model-value=\"updateField('src', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </div>\n <div v-if=\"containsMergeTag(block.src, mergeTagSyntax)\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\"\n >{{ t.image.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.image.placeholderUrlPlaceholder\"\n :title=\"t.image.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.image.altTextPlaceholder\"\n :pulse=\"pulseAlt\"\n :disabled=\"block.decorative === true\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n <ToggleSwitch\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.decorative === true\"\n :label=\"t.image.decorative\"\n @update:model-value=\"updateField('decorative', $event)\"\n >\n <span>\n {{ t.image.decorative }}\n <span class=\"tpl:block tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.decorativeHint }}\n </span>\n </span>\n </ToggleSwitch>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"full\">{{ t.image.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n <option value=\"custom\">{{ t.image.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"image-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.image.heightAuto }}</option>\n <option value=\"custom\">{{ t.image.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"image-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.linkUrl }}</label>\n <MergeTagInput\n :model-value=\"block.linkUrl || ''\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n @update:model-value=\"updateField('linkUrl', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.linkUrl\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.linkOpenInNewTab ?? false\"\n :label=\"t.image.openInNewTab\"\n @update:model-value=\"updateField('linkOpenInNewTab', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ImageBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../composables/useImageDrop\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: ImageBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ImageBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nconst pulseSrc = ref(false);\nconst pulseAlt = ref(false);\n\nconst { start: startPulseSrc } = useTimeoutFn(\n () => {\n pulseSrc.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nconst WIDTH_PRESETS = [300, 400, 500];\nconst DEFAULT_CUSTOM_WIDTH = 350;\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst widthMode = computed(() => {\n const w = props.block.width;\n if (w === \"full\") return \"full\";\n if (WIDTH_PRESETS.includes(w)) return String(w);\n return \"custom\";\n});\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ImageBlock>);\n}\n\nfunction updateWidthMode(value: string): void {\n if (value === \"custom\") {\n const w = props.block.width;\n if (typeof w !== \"number\" || WIDTH_PRESETS.includes(w)) {\n updateField(\"width\", DEFAULT_CUSTOM_WIDTH);\n }\n return;\n }\n updateField(\"width\", value === \"full\" ? \"full\" : Number(value));\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // Same guard as the custom width below: an empty number field yields\n // Number(\"\") === 0, which would collapse the image to nothing. Absent means\n // \"keep the aspect ratio\", so an invalid value must not overwrite the last\n // valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render an\n // invisible image (#259). Ignore invalid values and keep the last valid\n // width; the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\n// A merge-tag src is a deliberate dynamic value — never clobber it via drop.\nconst hasMergeTagSrc = computed(() =>\n containsMergeTag(props.block.src, mergeTagSyntax),\n);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !hasMergeTagSrc.value,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.imageUrl }}</label>\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <MergeTagInput\n :model-value=\"block.src\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n :pulse=\"pulseSrc\"\n @update:model-value=\"updateField('src', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </div>\n <div v-if=\"containsMergeTag(block.src, mergeTagSyntax)\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\"\n >{{ t.image.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.image.placeholderUrlPlaceholder\"\n :title=\"t.image.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.image.altTextPlaceholder\"\n :pulse=\"pulseAlt\"\n :disabled=\"block.decorative === true\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n <ToggleSwitch\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.decorative === true\"\n :label=\"t.image.decorative\"\n @update:model-value=\"updateField('decorative', $event)\"\n >\n <span>\n {{ t.image.decorative }}\n <span class=\"tpl:block tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.decorativeHint }}\n </span>\n </span>\n </ToggleSwitch>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"full\">{{ t.image.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n <option value=\"custom\">{{ t.image.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"image-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.image.heightAuto }}</option>\n <option value=\"custom\">{{ t.image.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"image-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.linkUrl }}</label>\n <MergeTagInput\n :model-value=\"block.linkUrl || ''\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n @update:model-value=\"updateField('linkUrl', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.linkUrl\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.linkOpenInNewTab ?? false\"\n :label=\"t.image.openInNewTab\"\n @update:model-value=\"updateField('linkOpenInNewTab', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: MenuBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<MenuBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst ITEM_TOGGLES = computed(() => [\n { key: \"openInNewTab\" as const, label: t.menu.openInNewTab },\n { key: \"bold\" as const, label: t.menu.bold },\n { key: \"underline\" as const, label: t.menu.underline },\n]);\n\nconst ALIGN_OPTIONS = computed(() => [\n { value: \"left\", label: t.title.alignLeft, icon: AlignLeft },\n { value: \"center\", label: t.title.alignCenter, icon: AlignCenter },\n { value: \"right\", label: t.title.alignRight, icon: AlignRight },\n]);\n\nfunction updateField(field: keyof MenuBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<MenuBlock>);\n}\n\nfunction addMenuItem(): void {\n const newItem: MenuItemData = {\n id: generateId(),\n text: \"\",\n url: \"\",\n openInNewTab: false,\n bold: false,\n underline: false,\n };\n emit(\"update\", { items: [...props.block.items, newItem] });\n}\n\nfunction updateMenuItem(\n itemId: string,\n field: keyof MenuItemData,\n value: unknown,\n): void {\n const updatedItems = props.block.items.map((item) =>\n item.id === itemId ? { ...item, [field]: value } : item,\n );\n emit(\"update\", { items: updatedItems });\n}\n\nfunction removeMenuItem(itemId: string): void {\n emit(\"update\", {\n items: props.block.items.filter((item) => item.id !== itemId),\n });\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.menu.items\">\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"item in block.items\"\n :key=\"item.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"item.text\"\n :placeholder=\"t.menu.text\"\n @input=\"\n updateMenuItem(\n item.id,\n 'text',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.menu.removeItem\"\n @click=\"removeMenuItem(item.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"item.url\"\n type=\"url\"\n :placeholder=\"t.menu.urlPlaceholder\"\n @update:model-value=\"updateMenuItem(item.id, 'url', $event)\"\n />\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:gap-x-3 tpl:gap-y-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <ToggleSwitch\n v-for=\"toggle in ITEM_TOGGLES\"\n :key=\"toggle.key\"\n :model-value=\"item[toggle.key]\"\n :label=\"toggle.label\"\n @update:model-value=\"updateMenuItem(item.id, toggle.key, $event)\"\n />\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <label :class=\"labelClass\" class=\"tpl:!mb-0\">{{\n t.menu.color\n }}</label>\n <ColorPicker\n swatch-only\n :model-value=\"item.color || block.linkColor || block.color || ''\"\n @update:model-value=\"updateMenuItem(item.id, 'color', $event)\"\n />\n </div>\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addMenuItem\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.menu.addItem }}\n </button>\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontSize\">\n <NumberWithSuffix\n :model-value=\"block.fontSize\"\n :min=\"8\"\n :max=\"48\"\n suffix=\"px\"\n @update:model-value=\"updateField('fontSize', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.color\">\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.linkColor\">\n <ColorPicker\n :model-value=\"block.linkColor || block.color || ''\"\n @update:model-value=\"updateField('linkColor', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.textAlign\">\n <SlidingPillSelect\n :options=\"ALIGN_OPTIONS\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separator\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.separator\"\n @input=\"\n updateField('separator', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separatorColor\">\n <ColorPicker\n :model-value=\"block.separatorColor\"\n @update:model-value=\"updateField('separatorColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.spacing\">\n <NumberWithSuffix\n :model-value=\"block.spacing\"\n :min=\"0\"\n :max=\"50\"\n suffix=\"px\"\n @update:model-value=\"updateField('spacing', $event)\"\n />\n </FieldRow>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: MenuBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<MenuBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst ITEM_TOGGLES = computed(() => [\n { key: \"openInNewTab\" as const, label: t.menu.openInNewTab },\n { key: \"bold\" as const, label: t.menu.bold },\n { key: \"underline\" as const, label: t.menu.underline },\n]);\n\nconst ALIGN_OPTIONS = computed(() => [\n { value: \"left\", label: t.title.alignLeft, icon: AlignLeft },\n { value: \"center\", label: t.title.alignCenter, icon: AlignCenter },\n { value: \"right\", label: t.title.alignRight, icon: AlignRight },\n]);\n\nfunction updateField(field: keyof MenuBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<MenuBlock>);\n}\n\nfunction addMenuItem(): void {\n const newItem: MenuItemData = {\n id: generateId(),\n text: \"\",\n url: \"\",\n openInNewTab: false,\n bold: false,\n underline: false,\n };\n emit(\"update\", { items: [...props.block.items, newItem] });\n}\n\nfunction updateMenuItem(\n itemId: string,\n field: keyof MenuItemData,\n value: unknown,\n): void {\n const updatedItems = props.block.items.map((item) =>\n item.id === itemId ? { ...item, [field]: value } : item,\n );\n emit(\"update\", { items: updatedItems });\n}\n\nfunction removeMenuItem(itemId: string): void {\n emit(\"update\", {\n items: props.block.items.filter((item) => item.id !== itemId),\n });\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.menu.items\">\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"item in block.items\"\n :key=\"item.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"item.text\"\n :placeholder=\"t.menu.text\"\n @input=\"\n updateMenuItem(\n item.id,\n 'text',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.menu.removeItem\"\n @click=\"removeMenuItem(item.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"item.url\"\n type=\"url\"\n :placeholder=\"t.menu.urlPlaceholder\"\n @update:model-value=\"updateMenuItem(item.id, 'url', $event)\"\n />\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:gap-x-3 tpl:gap-y-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <ToggleSwitch\n v-for=\"toggle in ITEM_TOGGLES\"\n :key=\"toggle.key\"\n :model-value=\"item[toggle.key]\"\n :label=\"toggle.label\"\n @update:model-value=\"updateMenuItem(item.id, toggle.key, $event)\"\n />\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <label :class=\"labelClass\" class=\"tpl:!mb-0\">{{\n t.menu.color\n }}</label>\n <ColorPicker\n swatch-only\n :model-value=\"item.color || block.linkColor || block.color || ''\"\n @update:model-value=\"updateMenuItem(item.id, 'color', $event)\"\n />\n </div>\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addMenuItem\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.menu.addItem }}\n </button>\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontSize\">\n <NumberWithSuffix\n :model-value=\"block.fontSize\"\n :min=\"8\"\n :max=\"48\"\n suffix=\"px\"\n @update:model-value=\"updateField('fontSize', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.color\">\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.linkColor\">\n <ColorPicker\n :model-value=\"block.linkColor || block.color || ''\"\n @update:model-value=\"updateField('linkColor', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.textAlign\">\n <SlidingPillSelect\n :options=\"ALIGN_OPTIONS\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separator\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.separator\"\n @input=\"\n updateField('separator', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separatorColor\">\n <ColorPicker\n :model-value=\"block.separatorColor\"\n @update:model-value=\"updateField('separatorColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.spacing\">\n <NumberWithSuffix\n :model-value=\"block.spacing\"\n :min=\"0\"\n :max=\"50\"\n suffix=\"px\"\n @update:model-value=\"updateField('spacing', $event)\"\n />\n </FieldRow>\n</template>\n","import type { Block, ColumnLayout } from \"@templatical/types\";\n\nfunction expectedColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\n\n/**\n * Resize a section's `children` array to match a new column layout, without\n * dropping any blocks. Growing pads with empty columns; shrinking merges\n * any excess columns' blocks into the last kept column so the user never\n * silently loses content.\n */\nexport function rebalanceColumnChildren(\n current: Block[][],\n layout: ColumnLayout,\n): Block[][] {\n const target = expectedColumnCount(layout);\n\n if (current.length === target) {\n return current;\n }\n\n if (current.length < target) {\n const padding = Array.from(\n { length: target - current.length },\n () => [] as Block[],\n );\n return [...current, ...padding];\n }\n\n const kept = current.slice(0, target);\n const overflow = current.slice(target).flat();\n const merged = [...kept[target - 1], ...overflow];\n return [...kept.slice(0, target - 1), merged];\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport type {\n ColumnLayout,\n SectionBlock,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport { rebalanceColumnChildren } from \"../../utils/rebalanceColumnChildren\";\n\nconst props = defineProps<{\n block: SectionBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SectionBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst columnOptions = computed(() => [\n { value: \"1\" as ColumnLayout, label: t.section.column1 },\n { value: \"2\" as ColumnLayout, label: t.section.column2 },\n { value: \"3\" as ColumnLayout, label: t.section.column3 },\n { value: \"1-2\" as ColumnLayout, label: t.section.ratio12 },\n { value: \"2-1\" as ColumnLayout, label: t.section.ratio21 },\n]);\n\nfunction handleColumnsChange(event: Event): void {\n const columns = (event.target as HTMLSelectElement).value as ColumnLayout;\n const children = rebalanceColumnChildren(props.block.children, columns);\n emit(\"update\", { columns, children });\n}\n\nfunction handleStackOnMobileChange(checked: boolean): void {\n emit(\"update\", { stackOnMobile: checked });\n}\n\nfunction handleBorderRadiusChange(event: Event): void {\n const borderRadius = Number((event.target as HTMLInputElement).value);\n emit(\"update\", { borderRadius });\n}\n\nfunction setWrapperEnabled(enabled: boolean): void {\n emit(\"update\", {\n wrapper: enabled\n ? { padding: { top: 20, right: 20, bottom: 20, left: 20 } }\n : undefined,\n });\n}\n\nfunction updateWrapper(patch: Partial<SectionWrapper>): void {\n emit(\"update\", { wrapper: { ...props.block.wrapper, ...patch } });\n}\n\nfunction handleWrapperPadding(value: SpacingValue): void {\n updateWrapper({ padding: value });\n}\n\nfunction handleWrapperRadius(event: Event): void {\n updateWrapper({\n borderRadius: Number((event.target as HTMLInputElement).value),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.columns }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.columns\"\n @change=\"handleColumnsChange\"\n >\n <option\n v-for=\"option in columnOptions\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </div>\n <div v-if=\"block.columns !== '1'\" class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.stackOnMobile !== false\"\n :label=\"t.section.stackOnMobile\"\n @update:model-value=\"handleStackOnMobileChange($event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleBorderRadiusChange\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"!!block.wrapper\"\n :label=\"t.section.wrapperEnable\"\n @update:model-value=\"setWrapperEnabled($event)\"\n />\n <div\n v-if=\"block.wrapper\"\n class=\"tpl:mt-3 tpl:ml-0.5 tpl:space-y-3 tpl:border-l tpl:border-[var(--tpl-border)] tpl:pl-3\"\n >\n <div>\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.wrapper.backgroundColor ?? ''\"\n @update:model-value=\"updateWrapper({ backgroundColor: $event })\"\n />\n </div>\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"\n block.wrapper.padding ?? { top: 0, right: 0, bottom: 0, left: 0 }\n \"\n @update:model-value=\"handleWrapperPadding\"\n />\n <div>\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.wrapper.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleWrapperRadius\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport type {\n ColumnLayout,\n SectionBlock,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport { rebalanceColumnChildren } from \"../../utils/rebalanceColumnChildren\";\n\nconst props = defineProps<{\n block: SectionBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SectionBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst columnOptions = computed(() => [\n { value: \"1\" as ColumnLayout, label: t.section.column1 },\n { value: \"2\" as ColumnLayout, label: t.section.column2 },\n { value: \"3\" as ColumnLayout, label: t.section.column3 },\n { value: \"1-2\" as ColumnLayout, label: t.section.ratio12 },\n { value: \"2-1\" as ColumnLayout, label: t.section.ratio21 },\n]);\n\nfunction handleColumnsChange(event: Event): void {\n const columns = (event.target as HTMLSelectElement).value as ColumnLayout;\n const children = rebalanceColumnChildren(props.block.children, columns);\n emit(\"update\", { columns, children });\n}\n\nfunction handleStackOnMobileChange(checked: boolean): void {\n emit(\"update\", { stackOnMobile: checked });\n}\n\nfunction handleBorderRadiusChange(event: Event): void {\n const borderRadius = Number((event.target as HTMLInputElement).value);\n emit(\"update\", { borderRadius });\n}\n\nfunction setWrapperEnabled(enabled: boolean): void {\n emit(\"update\", {\n wrapper: enabled\n ? { padding: { top: 20, right: 20, bottom: 20, left: 20 } }\n : undefined,\n });\n}\n\nfunction updateWrapper(patch: Partial<SectionWrapper>): void {\n emit(\"update\", { wrapper: { ...props.block.wrapper, ...patch } });\n}\n\nfunction handleWrapperPadding(value: SpacingValue): void {\n updateWrapper({ padding: value });\n}\n\nfunction handleWrapperRadius(event: Event): void {\n updateWrapper({\n borderRadius: Number((event.target as HTMLInputElement).value),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.columns }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.columns\"\n @change=\"handleColumnsChange\"\n >\n <option\n v-for=\"option in columnOptions\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </div>\n <div v-if=\"block.columns !== '1'\" class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.stackOnMobile !== false\"\n :label=\"t.section.stackOnMobile\"\n @update:model-value=\"handleStackOnMobileChange($event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleBorderRadiusChange\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"!!block.wrapper\"\n :label=\"t.section.wrapperEnable\"\n @update:model-value=\"setWrapperEnabled($event)\"\n />\n <div\n v-if=\"block.wrapper\"\n class=\"tpl:mt-3 tpl:ml-0.5 tpl:space-y-3 tpl:border-l tpl:border-[var(--tpl-border)] tpl:pl-3\"\n >\n <div>\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.wrapper.backgroundColor ?? ''\"\n @update:model-value=\"updateWrapper({ backgroundColor: $event })\"\n />\n </div>\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"\n block.wrapper.padding ?? { top: 0, right: 0, bottom: 0, left: 0 }\n \"\n @update:model-value=\"handleWrapperPadding\"\n />\n <div>\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.wrapper.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleWrapperRadius\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport { socialPlatformOptions } from \"../../constants/socialIcons\";\nimport type {\n SocialIcon,\n SocialIconsBlock,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\n\nconst props = defineProps<{\n block: SocialIconsBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SocialIconsBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<SocialIconsBlock>);\n}\n\nfunction addSocialIcon(): void {\n const newIcon: SocialIcon = {\n id: generateId(),\n platform: \"facebook\",\n url: \"\",\n };\n emit(\"update\", { icons: [...props.block.icons, newIcon] });\n}\n\nfunction updateSocialIcon(\n iconId: string,\n field: keyof SocialIcon,\n value: string,\n): void {\n const updatedIcons = props.block.icons.map((icon) =>\n icon.id === iconId ? { ...icon, [field]: value } : icon,\n );\n emit(\"update\", { icons: updatedIcons });\n}\n\nfunction removeSocialIcon(iconId: string): void {\n emit(\"update\", {\n icons: props.block.icons.filter((icon) => icon.id !== iconId),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.icons }}</label>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"icon in block.icons\"\n :key=\"icon.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <select\n data-testid=\"social-platform-select\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"icon.platform\"\n @change=\"\n updateSocialIcon(\n icon.id,\n 'platform',\n ($event.target as HTMLSelectElement).value as SocialPlatform,\n )\n \"\n >\n <option\n v-for=\"platform in socialPlatformOptions\"\n :key=\"platform\"\n :value=\"platform\"\n >\n {{ t.social.platforms[platform] }}\n </option>\n </select>\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.social.removeIcon\"\n @click=\"removeSocialIcon(icon.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"icon.url\"\n type=\"url\"\n :placeholder=\"t.social.urlPlaceholder\"\n @update:model-value=\"updateSocialIcon(icon.id, 'url', $event)\"\n />\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addSocialIcon\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.social.addIcon }}\n </button>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.style }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.iconStyle\"\n @change=\"\n updateField(\n 'iconStyle',\n ($event.target as HTMLSelectElement).value as SocialIconStyle,\n )\n \"\n >\n <option value=\"solid\">{{ t.social.styleSolid }}</option>\n <option value=\"outlined\">{{ t.social.styleOutlined }}</option>\n <option value=\"rounded\">{{ t.social.styleRounded }}</option>\n <option value=\"square\">{{ t.social.styleSquare }}</option>\n <option value=\"circle\">{{ t.social.styleCircle }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.size }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'small', label: t.social.sizeSmall },\n { value: 'medium', label: t.social.sizeMedium },\n { value: 'large', label: t.social.sizeLarge },\n ]\"\n :model-value=\"block.iconSize\"\n @update:model-value=\"updateField('iconSize', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.spacing }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.spacing\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'spacing',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport { socialPlatformOptions } from \"../../constants/socialIcons\";\nimport type {\n SocialIcon,\n SocialIconsBlock,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\n\nconst props = defineProps<{\n block: SocialIconsBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SocialIconsBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<SocialIconsBlock>);\n}\n\nfunction addSocialIcon(): void {\n const newIcon: SocialIcon = {\n id: generateId(),\n platform: \"facebook\",\n url: \"\",\n };\n emit(\"update\", { icons: [...props.block.icons, newIcon] });\n}\n\nfunction updateSocialIcon(\n iconId: string,\n field: keyof SocialIcon,\n value: string,\n): void {\n const updatedIcons = props.block.icons.map((icon) =>\n icon.id === iconId ? { ...icon, [field]: value } : icon,\n );\n emit(\"update\", { icons: updatedIcons });\n}\n\nfunction removeSocialIcon(iconId: string): void {\n emit(\"update\", {\n icons: props.block.icons.filter((icon) => icon.id !== iconId),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.icons }}</label>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"icon in block.icons\"\n :key=\"icon.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <select\n data-testid=\"social-platform-select\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"icon.platform\"\n @change=\"\n updateSocialIcon(\n icon.id,\n 'platform',\n ($event.target as HTMLSelectElement).value as SocialPlatform,\n )\n \"\n >\n <option\n v-for=\"platform in socialPlatformOptions\"\n :key=\"platform\"\n :value=\"platform\"\n >\n {{ t.social.platforms[platform] }}\n </option>\n </select>\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.social.removeIcon\"\n @click=\"removeSocialIcon(icon.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"icon.url\"\n type=\"url\"\n :placeholder=\"t.social.urlPlaceholder\"\n @update:model-value=\"updateSocialIcon(icon.id, 'url', $event)\"\n />\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addSocialIcon\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.social.addIcon }}\n </button>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.style }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.iconStyle\"\n @change=\"\n updateField(\n 'iconStyle',\n ($event.target as HTMLSelectElement).value as SocialIconStyle,\n )\n \"\n >\n <option value=\"solid\">{{ t.social.styleSolid }}</option>\n <option value=\"outlined\">{{ t.social.styleOutlined }}</option>\n <option value=\"rounded\">{{ t.social.styleRounded }}</option>\n <option value=\"square\">{{ t.social.styleSquare }}</option>\n <option value=\"circle\">{{ t.social.styleCircle }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.size }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'small', label: t.social.sizeSmall },\n { value: 'medium', label: t.social.sizeMedium },\n { value: 'large', label: t.social.sizeLarge },\n ]\"\n :model-value=\"block.iconSize\"\n @update:model-value=\"updateField('iconSize', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.spacing }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.spacing\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'spacing',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { SpacerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: SpacerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SpacerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.spacer.height }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n <input\n type=\"range\"\n class=\"tpl:mt-2 tpl:w-full tpl:accent-[var(--tpl-primary)]\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { SpacerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: SpacerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SpacerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.spacer.height }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n <input\n type=\"range\"\n class=\"tpl:mt-2 tpl:w-full tpl:accent-[var(--tpl-primary)]\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_TABLE_ROW_BG,\n} from \"../../constants/styleConstants\";\nimport type {\n TableBlock,\n TableCellData,\n TableRowData,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Minus, Plus } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: TableBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TableBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst tableColumnCount = computed(() => {\n return props.block.rows.length > 0 ? props.block.rows[0].cells.length : 0;\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TableBlock>);\n}\n\nfunction addTableRow(): void {\n const columnCount =\n props.block.rows.length > 0 ? props.block.rows[0].cells.length : 3;\n const newRow: TableRowData = {\n id: generateId(),\n cells: Array.from({ length: columnCount }, (): TableCellData => ({\n id: generateId(),\n content: \"\",\n })),\n };\n emit(\"update\", { rows: [...props.block.rows, newRow] });\n}\n\nfunction removeTableRow(rowId: string): void {\n emit(\"update\", {\n rows: props.block.rows.filter((row) => row.id !== rowId),\n });\n}\n\nfunction addTableColumn(): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: [...row.cells, { id: generateId(), content: \"\" } as TableCellData],\n }));\n emit(\"update\", { rows: updatedRows });\n}\n\nfunction removeTableColumn(colIndex: number): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: row.cells.filter((_, i) => i !== colIndex),\n }));\n emit(\"update\", { rows: updatedRows });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.dimensions }}</label>\n <div class=\"tpl:flex tpl:items-center tpl:gap-3\">\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.rows\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"block.rows.length <= 1\"\n @click=\"removeTableRow(block.rows[block.rows.length - 1].id)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ block.rows.length }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableRow\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.columns\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"tableColumnCount <= 1\"\n @click=\"removeTableColumn(tableColumnCount - 1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ tableColumnCount }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableColumn\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.hasHeaderRow\"\n :label=\"t.table.hasHeaderRow\"\n @update:model-value=\"updateField('hasHeaderRow', $event)\"\n />\n </div>\n <div v-if=\"block.hasHeaderRow\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.headerBackgroundColor }}</label>\n <ColorPicker\n :model-value=\"block.headerBackgroundColor ?? ''\"\n :seed-color=\"DEFAULT_TABLE_ROW_BG\"\n :placeholder=\"t.table.noHeaderBg\"\n @update:model-value=\"updateField('headerBackgroundColor', $event || null)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderColor }}</label>\n <ColorPicker\n :model-value=\"block.borderColor\"\n @update:model-value=\"updateField('borderColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderWidth }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderWidth\"\n min=\"0\"\n max=\"10\"\n @input=\"\n updateField(\n 'borderWidth',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.cellPadding }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.cellPadding\"\n min=\"0\"\n max=\"30\"\n @input=\"\n updateField(\n 'cellPadding',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"32\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.textAlign }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_TABLE_ROW_BG,\n} from \"../../constants/styleConstants\";\nimport type {\n TableBlock,\n TableCellData,\n TableRowData,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Minus, Plus } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: TableBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TableBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst tableColumnCount = computed(() => {\n return props.block.rows.length > 0 ? props.block.rows[0].cells.length : 0;\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TableBlock>);\n}\n\nfunction addTableRow(): void {\n const columnCount =\n props.block.rows.length > 0 ? props.block.rows[0].cells.length : 3;\n const newRow: TableRowData = {\n id: generateId(),\n cells: Array.from({ length: columnCount }, (): TableCellData => ({\n id: generateId(),\n content: \"\",\n })),\n };\n emit(\"update\", { rows: [...props.block.rows, newRow] });\n}\n\nfunction removeTableRow(rowId: string): void {\n emit(\"update\", {\n rows: props.block.rows.filter((row) => row.id !== rowId),\n });\n}\n\nfunction addTableColumn(): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: [...row.cells, { id: generateId(), content: \"\" } as TableCellData],\n }));\n emit(\"update\", { rows: updatedRows });\n}\n\nfunction removeTableColumn(colIndex: number): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: row.cells.filter((_, i) => i !== colIndex),\n }));\n emit(\"update\", { rows: updatedRows });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.dimensions }}</label>\n <div class=\"tpl:flex tpl:items-center tpl:gap-3\">\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.rows\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"block.rows.length <= 1\"\n @click=\"removeTableRow(block.rows[block.rows.length - 1].id)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ block.rows.length }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableRow\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.columns\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"tableColumnCount <= 1\"\n @click=\"removeTableColumn(tableColumnCount - 1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ tableColumnCount }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableColumn\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.hasHeaderRow\"\n :label=\"t.table.hasHeaderRow\"\n @update:model-value=\"updateField('hasHeaderRow', $event)\"\n />\n </div>\n <div v-if=\"block.hasHeaderRow\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.headerBackgroundColor }}</label>\n <ColorPicker\n :model-value=\"block.headerBackgroundColor ?? ''\"\n :seed-color=\"DEFAULT_TABLE_ROW_BG\"\n :placeholder=\"t.table.noHeaderBg\"\n @update:model-value=\"updateField('headerBackgroundColor', $event || null)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderColor }}</label>\n <ColorPicker\n :model-value=\"block.borderColor\"\n @update:model-value=\"updateField('borderColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderWidth }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderWidth\"\n min=\"0\"\n max=\"10\"\n @input=\"\n updateField(\n 'borderWidth',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.cellPadding }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.cellPadding\"\n min=\"0\"\n max=\"30\"\n @input=\"\n updateField(\n 'cellPadding',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"32\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.textAlign }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass, labelClass } from \"../../constants/styleConstants\";\nimport type { TitleBlock } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight } from \"@lucide/vue\";\n\ndefineProps<{\n block: TitleBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TitleBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TitleBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.level }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.level\"\n @change=\"\n updateField('level', Number(($event.target as HTMLSelectElement).value))\n \"\n >\n <option :value=\"1\">{{ t.title.heading1 }}</option>\n <option :value=\"2\">{{ t.title.heading2 }}</option>\n <option :value=\"3\">{{ t.title.heading3 }}</option>\n <option :value=\"4\">{{ t.title.heading4 }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass, labelClass } from \"../../constants/styleConstants\";\nimport type { TitleBlock } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight } from \"@lucide/vue\";\n\ndefineProps<{\n block: TitleBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TitleBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TitleBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.level }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.level\"\n @change=\"\n updateField('level', Number(($event.target as HTMLSelectElement).value))\n \"\n >\n <option :value=\"1\">{{ t.title.heading1 }}</option>\n <option :value=\"2\">{{ t.title.heading2 }}</option>\n <option :value=\"3\">{{ t.title.heading3 }}</option>\n <option :value=\"4\">{{ t.title.heading4 }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { VideoBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: VideoBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<VideoBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\nconst urlHasMergeTag = computed(() =>\n containsMergeTag(props.block.url, mergeTagSyntax),\n);\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nconst pulseThumbnail = ref(false);\nconst { start: startPulseThumbnail } = useTimeoutFn(\n () => {\n pulseThumbnail.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nfunction updateField(field: keyof VideoBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<VideoBlock>);\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // An empty number field yields Number(\"\") === 0, which would collapse the\n // thumbnail. Absent means \"keep the aspect ratio\", so an invalid value must\n // not overwrite the last valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"thumbnailUrl\", result.url);\n pulseThumbnail.value = true;\n startPulseThumbnail();\n }\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.videoUrl }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.video.videoUrlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.video.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div v-if=\"urlHasMergeTag\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.video.placeholderUrlPlaceholder\"\n :title=\"t.video.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.customThumbnail }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <MergeTagInput\n :model-value=\"block.thumbnailUrl\"\n type=\"url\"\n :placeholder=\"t.video.thumbnailPlaceholder\"\n :pulse=\"pulseThumbnail\"\n @update:model-value=\"updateField('thumbnailUrl', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.video.altTextPlaceholder\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.width\"\n @change=\"\n updateField(\n 'width',\n ($event.target as HTMLSelectElement).value === 'full'\n ? 'full'\n : Number(($event.target as HTMLSelectElement).value),\n )\n \"\n >\n <option value=\"full\">{{ t.video.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"video-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.video.heightAuto }}</option>\n <option value=\"custom\">{{ t.video.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"video-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { VideoBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: VideoBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<VideoBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\nconst urlHasMergeTag = computed(() =>\n containsMergeTag(props.block.url, mergeTagSyntax),\n);\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nconst pulseThumbnail = ref(false);\nconst { start: startPulseThumbnail } = useTimeoutFn(\n () => {\n pulseThumbnail.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nfunction updateField(field: keyof VideoBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<VideoBlock>);\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // An empty number field yields Number(\"\") === 0, which would collapse the\n // thumbnail. Absent means \"keep the aspect ratio\", so an invalid value must\n // not overwrite the last valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"thumbnailUrl\", result.url);\n pulseThumbnail.value = true;\n startPulseThumbnail();\n }\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.videoUrl }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.video.videoUrlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.video.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div v-if=\"urlHasMergeTag\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.video.placeholderUrlPlaceholder\"\n :title=\"t.video.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.customThumbnail }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <MergeTagInput\n :model-value=\"block.thumbnailUrl\"\n type=\"url\"\n :placeholder=\"t.video.thumbnailPlaceholder\"\n :pulse=\"pulseThumbnail\"\n @update:model-value=\"updateField('thumbnailUrl', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.video.altTextPlaceholder\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.width\"\n @change=\"\n updateField(\n 'width',\n ($event.target as HTMLSelectElement).value === 'full'\n ? 'full'\n : Number(($event.target as HTMLSelectElement).value),\n )\n \"\n >\n <option value=\"full\">{{ t.video.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"video-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.video.heightAuto }}</option>\n <option value=\"custom\">{{ t.video.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"video-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport ButtonToolbar from \"./toolbar/ButtonToolbar.vue\";\nimport CommonBlockSettings from \"./toolbar/CommonBlockSettings.vue\";\nconst CountdownToolbar = defineAsyncComponent(\n () => import(\"./toolbar/CountdownToolbar.vue\"),\n);\nimport CustomBlockToolbar from \"./toolbar/CustomBlockToolbar.vue\";\nimport DividerToolbar from \"./toolbar/DividerToolbar.vue\";\nimport HtmlToolbar from \"./toolbar/HtmlToolbar.vue\";\nimport ImageToolbar from \"./toolbar/ImageToolbar.vue\";\nimport MenuToolbar from \"./toolbar/MenuToolbar.vue\";\nimport SectionToolbar from \"./toolbar/SectionToolbar.vue\";\nimport SocialToolbar from \"./toolbar/SocialToolbar.vue\";\nimport SpacerToolbar from \"./toolbar/SpacerToolbar.vue\";\nimport TableToolbar from \"./toolbar/TableToolbar.vue\";\nimport TitleToolbar from \"./toolbar/TitleToolbar.vue\";\nimport VideoToolbar from \"./toolbar/VideoToolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type {\n Block,\n ButtonBlock,\n CountdownBlock,\n CustomBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n TableBlock,\n TitleBlock,\n VideoBlock,\n} from \"@templatical/types\";\nimport { isCustomBlock } from \"@templatical/types\";\nimport { Code, Copy, Trash2 } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport {\n FONTS_MANAGER_KEY,\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n requireInject,\n} from \"../keys\";\n\nconst props = defineProps<{\n block: Block;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n (e: \"delete\"): void;\n (e: \"duplicate\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"Toolbar\");\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst blockType = computed(() => props.block.type);\n\nconst isCustom = computed(() => isCustomBlock(props.block));\n\nconst customBlockDefinition = computed(() => {\n if (!isCustom.value) {\n return undefined;\n }\n return customBlockDefinitions.find(\n (d) => d.type === (props.block as CustomBlock).customType,\n );\n});\n\nconst blockTypeLabel = computed(() => {\n if (isCustom.value) {\n return (\n customBlockDefinition.value?.name ??\n (props.block as CustomBlock).customType\n );\n }\n\n return getBlockTypeLabel(blockType.value, t);\n});\n\n// Font families from shared fontsManager (provided by Editor.vue)\nconst fontFamilies = fontsManager.fonts;\n\nfunction handleUpdate(updates: Partial<Block>): void {\n emit(\"update\", updates);\n}\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.blockToolbar\"\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3.5\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-[var(--tpl-primary)]\"\n >\n <component\n :is=\"blockTypeIcons[blockType]\"\n v-if=\"blockTypeIcons[blockType]\"\n :size=\"16\"\n :stroke-width=\"1.5\"\n />\n <Code v-else-if=\"isCustom\" :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ blockTypeLabel }}\n </h3>\n </div>\n <div class=\"tpl:flex tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)] tpl:hover:text-[var(--tpl-text)]\"\n :title=\"t.toolbar.duplicate\"\n @click=\"emit('duplicate')\"\n >\n <Copy :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.toolbar.delete\"\n @click=\"emit('delete')\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <template v-if=\"isCustom\">\n <CustomBlockToolbar\n :block=\"block as CustomBlock\"\n @update-field-values=\"emit('update', { fieldValues: $event })\"\n @update-data-source-fetched=\"\n emit('update', { dataSourceFetched: $event })\n \"\n />\n </template>\n\n <SectionToolbar\n v-else-if=\"blockType === 'section'\"\n :block=\"block as SectionBlock\"\n @update=\"handleUpdate\"\n />\n\n <TitleToolbar\n v-else-if=\"blockType === 'title'\"\n :block=\"block as TitleBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Paragraph block: no text-specific sidebar controls — all formatting is in the TipTap toolbar -->\n <template v-else-if=\"blockType === 'paragraph'\" />\n\n <ImageToolbar\n v-else-if=\"blockType === 'image'\"\n :block=\"block as ImageBlock\"\n @update=\"handleUpdate\"\n />\n\n <VideoToolbar\n v-else-if=\"blockType === 'video'\"\n :block=\"block as VideoBlock\"\n @update=\"handleUpdate\"\n />\n\n <ButtonToolbar\n v-else-if=\"blockType === 'button'\"\n :block=\"block as ButtonBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <DividerToolbar\n v-else-if=\"blockType === 'divider'\"\n :block=\"block as DividerBlock\"\n @update=\"handleUpdate\"\n />\n\n <SocialToolbar\n v-else-if=\"blockType === 'social'\"\n :block=\"block as SocialIconsBlock\"\n @update=\"handleUpdate\"\n />\n\n <MenuToolbar\n v-else-if=\"blockType === 'menu'\"\n :block=\"block as MenuBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <TableToolbar\n v-else-if=\"blockType === 'table'\"\n :block=\"block as TableBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <SpacerToolbar\n v-else-if=\"blockType === 'spacer'\"\n :block=\"block as SpacerBlock\"\n @update=\"handleUpdate\"\n />\n\n <HtmlToolbar\n v-else-if=\"blockType === 'html'\"\n :block=\"block as HtmlBlock\"\n @update=\"handleUpdate\"\n />\n\n <CountdownToolbar\n v-else-if=\"blockType === 'countdown'\"\n :block=\"block as CountdownBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Common block settings -->\n <CommonBlockSettings\n :block=\"block\"\n :is-first-section=\"blockType === 'paragraph'\"\n @update=\"handleUpdate\"\n />\n </div>\n </aside>\n</template>\n\n<style scoped>\n.tpl-collapsible {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-collapsible--open {\n grid-template-rows: 1fr;\n}\n\n.tpl-collapsible > div {\n overflow: hidden;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport ButtonToolbar from \"./toolbar/ButtonToolbar.vue\";\nimport CommonBlockSettings from \"./toolbar/CommonBlockSettings.vue\";\nconst CountdownToolbar = defineAsyncComponent(\n () => import(\"./toolbar/CountdownToolbar.vue\"),\n);\nimport CustomBlockToolbar from \"./toolbar/CustomBlockToolbar.vue\";\nimport DividerToolbar from \"./toolbar/DividerToolbar.vue\";\nimport HtmlToolbar from \"./toolbar/HtmlToolbar.vue\";\nimport ImageToolbar from \"./toolbar/ImageToolbar.vue\";\nimport MenuToolbar from \"./toolbar/MenuToolbar.vue\";\nimport SectionToolbar from \"./toolbar/SectionToolbar.vue\";\nimport SocialToolbar from \"./toolbar/SocialToolbar.vue\";\nimport SpacerToolbar from \"./toolbar/SpacerToolbar.vue\";\nimport TableToolbar from \"./toolbar/TableToolbar.vue\";\nimport TitleToolbar from \"./toolbar/TitleToolbar.vue\";\nimport VideoToolbar from \"./toolbar/VideoToolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type {\n Block,\n ButtonBlock,\n CountdownBlock,\n CustomBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n TableBlock,\n TitleBlock,\n VideoBlock,\n} from \"@templatical/types\";\nimport { isCustomBlock } from \"@templatical/types\";\nimport { Code, Copy, Trash2 } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport {\n FONTS_MANAGER_KEY,\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n requireInject,\n} from \"../keys\";\n\nconst props = defineProps<{\n block: Block;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n (e: \"delete\"): void;\n (e: \"duplicate\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"Toolbar\");\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst blockType = computed(() => props.block.type);\n\nconst isCustom = computed(() => isCustomBlock(props.block));\n\nconst customBlockDefinition = computed(() => {\n if (!isCustom.value) {\n return undefined;\n }\n return customBlockDefinitions.find(\n (d) => d.type === (props.block as CustomBlock).customType,\n );\n});\n\nconst blockTypeLabel = computed(() => {\n if (isCustom.value) {\n return (\n customBlockDefinition.value?.name ??\n (props.block as CustomBlock).customType\n );\n }\n\n return getBlockTypeLabel(blockType.value, t);\n});\n\n// Font families from shared fontsManager (provided by Editor.vue)\nconst fontFamilies = fontsManager.fonts;\n\nfunction handleUpdate(updates: Partial<Block>): void {\n emit(\"update\", updates);\n}\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.blockToolbar\"\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3.5\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-[var(--tpl-primary)]\"\n >\n <component\n :is=\"blockTypeIcons[blockType]\"\n v-if=\"blockTypeIcons[blockType]\"\n :size=\"16\"\n :stroke-width=\"1.5\"\n />\n <Code v-else-if=\"isCustom\" :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ blockTypeLabel }}\n </h3>\n </div>\n <div class=\"tpl:flex tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)] tpl:hover:text-[var(--tpl-text)]\"\n :title=\"t.toolbar.duplicate\"\n @click=\"emit('duplicate')\"\n >\n <Copy :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.toolbar.delete\"\n @click=\"emit('delete')\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <template v-if=\"isCustom\">\n <CustomBlockToolbar\n :block=\"block as CustomBlock\"\n @update-field-values=\"emit('update', { fieldValues: $event })\"\n @update-data-source-fetched=\"\n emit('update', { dataSourceFetched: $event })\n \"\n />\n </template>\n\n <SectionToolbar\n v-else-if=\"blockType === 'section'\"\n :block=\"block as SectionBlock\"\n @update=\"handleUpdate\"\n />\n\n <TitleToolbar\n v-else-if=\"blockType === 'title'\"\n :block=\"block as TitleBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Paragraph block: no text-specific sidebar controls — all formatting is in the TipTap toolbar -->\n <template v-else-if=\"blockType === 'paragraph'\" />\n\n <ImageToolbar\n v-else-if=\"blockType === 'image'\"\n :block=\"block as ImageBlock\"\n @update=\"handleUpdate\"\n />\n\n <VideoToolbar\n v-else-if=\"blockType === 'video'\"\n :block=\"block as VideoBlock\"\n @update=\"handleUpdate\"\n />\n\n <ButtonToolbar\n v-else-if=\"blockType === 'button'\"\n :block=\"block as ButtonBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <DividerToolbar\n v-else-if=\"blockType === 'divider'\"\n :block=\"block as DividerBlock\"\n @update=\"handleUpdate\"\n />\n\n <SocialToolbar\n v-else-if=\"blockType === 'social'\"\n :block=\"block as SocialIconsBlock\"\n @update=\"handleUpdate\"\n />\n\n <MenuToolbar\n v-else-if=\"blockType === 'menu'\"\n :block=\"block as MenuBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <TableToolbar\n v-else-if=\"blockType === 'table'\"\n :block=\"block as TableBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <SpacerToolbar\n v-else-if=\"blockType === 'spacer'\"\n :block=\"block as SpacerBlock\"\n @update=\"handleUpdate\"\n />\n\n <HtmlToolbar\n v-else-if=\"blockType === 'html'\"\n :block=\"block as HtmlBlock\"\n @update=\"handleUpdate\"\n />\n\n <CountdownToolbar\n v-else-if=\"blockType === 'countdown'\"\n :block=\"block as CountdownBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Common block settings -->\n <CommonBlockSettings\n :block=\"block\"\n :is-first-section=\"blockType === 'paragraph'\"\n @update=\"handleUpdate\"\n />\n </div>\n </aside>\n</template>\n\n<style scoped>\n.tpl-collapsible {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-collapsible--open {\n grid-template-rows: 1fr;\n}\n\n.tpl-collapsible > div {\n overflow: hidden;\n}\n</style>\n","<script setup lang=\"ts\">\nimport TemplateSettingsPanel from \"./TemplateSettings.vue\";\nimport Toolbar from \"./Toolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { TEMPLATE_LINT_KEY } from \"../keys\";\nimport type { Block, TemplateSettings } from \"@templatical/types\";\nimport { ListChecks, LayoutTemplate, PanelTop, Settings } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, inject, ref, watch } from \"vue\";\n\nconst IssuesPanel = defineAsyncComponent(\n () => import(\"./sidebar/IssuesPanel.vue\"),\n);\n\nconst props = defineProps<{\n selectedBlock: Block | null;\n settings: TemplateSettings;\n shiftedLeft?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update-block\", updates: Partial<Block>): void;\n (e: \"delete-block\"): void;\n (e: \"duplicate-block\"): void;\n (e: \"update-settings\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst { t } = useI18n();\n\ntype Tab = \"content\" | \"settings\" | \"issues\";\nconst activeTab = ref<Tab>(\"content\");\n\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst lintEnabled = computed(() => lint !== null);\nconst issueCount = computed(() => lint?.issues.value.length ?? 0);\n\nfunction tabClass(tab: Tab): string {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return \"tpl:flex-1 tpl:text-[var(--tpl-primary)]\";\n }\n return \"tpl:shrink-0 tpl:text-[var(--tpl-text-muted)] tpl:hover:text-[var(--tpl-text)]\";\n}\n\nfunction tabStyle(tab: Tab): Record<string, string> {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return {\n backgroundColor: \"var(--tpl-bg)\",\n // `-sm` is the step the shadow vocabulary assigns to a lifted control;\n // `-md` is dialog and side-panel depth, which overstates a 32px tab.\n boxShadow: \"var(--tpl-shadow-sm)\",\n };\n }\n return { backgroundColor: \"transparent\" };\n}\n\nwatch(\n () => props.selectedBlock,\n (newBlock) => {\n if (newBlock) {\n activeTab.value = \"content\";\n }\n },\n);\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.rightSidebar\"\n class=\"tpl-right-sidebar tpl:absolute tpl:top-14 tpl:bottom-0 tpl:z-40 tpl:flex tpl:w-[320px] tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)] tpl:transition-all tpl:duration-200 tpl:border-l tpl:border-[var(--tpl-border)]\"\n :class=\"shiftedLeft ? 'tpl:right-[360px]' : 'tpl:right-0'\"\n >\n <div\n role=\"tablist\"\n class=\"tpl:relative tpl:flex tpl:gap-1 tpl:border-b tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-active)] tpl:p-1.5\"\n >\n <button\n id=\"tpl-tab-content\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'content'\"\n aria-controls=\"tpl-tabpanel-content\"\n :aria-label=\"t.sidebar.content\"\n :title=\"t.sidebar.content\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('content')\"\n :style=\"tabStyle('content')\"\n @click=\"activeTab = 'content'\"\n >\n <PanelTop :size=\"14\" :stroke-width=\"2\" />\n <span v-if=\"activeTab === 'content'\">{{ t.sidebar.content }}</span>\n </button>\n <button\n id=\"tpl-tab-settings\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'settings'\"\n aria-controls=\"tpl-tabpanel-settings\"\n :aria-label=\"t.sidebar.settings\"\n :title=\"t.sidebar.settings\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('settings')\"\n :style=\"tabStyle('settings')\"\n @click=\"activeTab = 'settings'\"\n >\n <Settings :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'settings'\">{{ t.sidebar.settings }}</span>\n </button>\n <button\n v-if=\"lintEnabled\"\n id=\"tpl-tab-issues\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'issues'\"\n aria-controls=\"tpl-tabpanel-issues\"\n :aria-label=\"t.issues.panelTabLabel\"\n :title=\"t.issues.panelTabLabel\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('issues')\"\n :style=\"tabStyle('issues')\"\n @click=\"activeTab = 'issues'\"\n >\n <ListChecks :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'issues'\">\n {{ t.issues.panelTabLabel }}\n </span>\n <span\n v-if=\"issueCount > 0\"\n class=\"tpl:ml-1 tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-1.5 tpl:text-[10px]\"\n >\n {{ issueCount }}\n </span>\n </button>\n </div>\n\n <div\n v-if=\"activeTab === 'content'\"\n id=\"tpl-tabpanel-content\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-content\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <Toolbar\n v-if=\"selectedBlock\"\n :block=\"selectedBlock\"\n @update=\"emit('update-block', $event)\"\n @delete=\"emit('delete-block')\"\n @duplicate=\"emit('duplicate-block')\"\n />\n <div\n v-else\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:px-6 tpl:py-10 tpl:text-center tpl:text-[var(--tpl-text-muted)]\"\n >\n <div class=\"tpl:mb-4 tpl:text-[var(--tpl-text-dim)]\">\n <LayoutTemplate :size=\"40\" :stroke-width=\"1.5\" />\n </div>\n <h3\n class=\"tpl:m-0 tpl:mb-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.sidebar.noSelection }}\n </h3>\n <p class=\"tpl:m-0 tpl:text-sm tpl:leading-normal\">\n {{ t.sidebar.noSelectionHint }}\n </p>\n </div>\n </div>\n\n <div\n v-if=\"activeTab === 'settings'\"\n id=\"tpl-tabpanel-settings\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-settings\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <TemplateSettingsPanel\n :settings=\"settings\"\n @update=\"emit('update-settings', $event)\"\n />\n </div>\n\n <div\n v-if=\"activeTab === 'issues' && lintEnabled\"\n id=\"tpl-tabpanel-issues\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-issues\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <IssuesPanel />\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport TemplateSettingsPanel from \"./TemplateSettings.vue\";\nimport Toolbar from \"./Toolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { TEMPLATE_LINT_KEY } from \"../keys\";\nimport type { Block, TemplateSettings } from \"@templatical/types\";\nimport { ListChecks, LayoutTemplate, PanelTop, Settings } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, inject, ref, watch } from \"vue\";\n\nconst IssuesPanel = defineAsyncComponent(\n () => import(\"./sidebar/IssuesPanel.vue\"),\n);\n\nconst props = defineProps<{\n selectedBlock: Block | null;\n settings: TemplateSettings;\n shiftedLeft?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update-block\", updates: Partial<Block>): void;\n (e: \"delete-block\"): void;\n (e: \"duplicate-block\"): void;\n (e: \"update-settings\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst { t } = useI18n();\n\ntype Tab = \"content\" | \"settings\" | \"issues\";\nconst activeTab = ref<Tab>(\"content\");\n\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst lintEnabled = computed(() => lint !== null);\nconst issueCount = computed(() => lint?.issues.value.length ?? 0);\n\nfunction tabClass(tab: Tab): string {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return \"tpl:flex-1 tpl:text-[var(--tpl-primary)]\";\n }\n return \"tpl:shrink-0 tpl:text-[var(--tpl-text-muted)] tpl:hover:text-[var(--tpl-text)]\";\n}\n\nfunction tabStyle(tab: Tab): Record<string, string> {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return {\n backgroundColor: \"var(--tpl-bg)\",\n // `-sm` is the step the shadow vocabulary assigns to a lifted control;\n // `-md` is dialog and side-panel depth, which overstates a 32px tab.\n boxShadow: \"var(--tpl-shadow-sm)\",\n };\n }\n return { backgroundColor: \"transparent\" };\n}\n\nwatch(\n () => props.selectedBlock,\n (newBlock) => {\n if (newBlock) {\n activeTab.value = \"content\";\n }\n },\n);\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.rightSidebar\"\n class=\"tpl-right-sidebar tpl:absolute tpl:top-14 tpl:bottom-0 tpl:z-40 tpl:flex tpl:w-[320px] tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)] tpl:transition-all tpl:duration-200 tpl:border-l tpl:border-[var(--tpl-border)]\"\n :class=\"shiftedLeft ? 'tpl:right-[360px]' : 'tpl:right-0'\"\n >\n <div\n role=\"tablist\"\n class=\"tpl:relative tpl:flex tpl:gap-1 tpl:border-b tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-active)] tpl:p-1.5\"\n >\n <button\n id=\"tpl-tab-content\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'content'\"\n aria-controls=\"tpl-tabpanel-content\"\n :aria-label=\"t.sidebar.content\"\n :title=\"t.sidebar.content\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('content')\"\n :style=\"tabStyle('content')\"\n @click=\"activeTab = 'content'\"\n >\n <PanelTop :size=\"14\" :stroke-width=\"2\" />\n <span v-if=\"activeTab === 'content'\">{{ t.sidebar.content }}</span>\n </button>\n <button\n id=\"tpl-tab-settings\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'settings'\"\n aria-controls=\"tpl-tabpanel-settings\"\n :aria-label=\"t.sidebar.settings\"\n :title=\"t.sidebar.settings\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('settings')\"\n :style=\"tabStyle('settings')\"\n @click=\"activeTab = 'settings'\"\n >\n <Settings :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'settings'\">{{ t.sidebar.settings }}</span>\n </button>\n <button\n v-if=\"lintEnabled\"\n id=\"tpl-tab-issues\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'issues'\"\n aria-controls=\"tpl-tabpanel-issues\"\n :aria-label=\"t.issues.panelTabLabel\"\n :title=\"t.issues.panelTabLabel\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('issues')\"\n :style=\"tabStyle('issues')\"\n @click=\"activeTab = 'issues'\"\n >\n <ListChecks :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'issues'\">\n {{ t.issues.panelTabLabel }}\n </span>\n <span\n v-if=\"issueCount > 0\"\n class=\"tpl:ml-1 tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-1.5 tpl:text-[10px]\"\n >\n {{ issueCount }}\n </span>\n </button>\n </div>\n\n <div\n v-if=\"activeTab === 'content'\"\n id=\"tpl-tabpanel-content\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-content\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <Toolbar\n v-if=\"selectedBlock\"\n :block=\"selectedBlock\"\n @update=\"emit('update-block', $event)\"\n @delete=\"emit('delete-block')\"\n @duplicate=\"emit('duplicate-block')\"\n />\n <div\n v-else\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:px-6 tpl:py-10 tpl:text-center tpl:text-[var(--tpl-text-muted)]\"\n >\n <div class=\"tpl:mb-4 tpl:text-[var(--tpl-text-dim)]\">\n <LayoutTemplate :size=\"40\" :stroke-width=\"1.5\" />\n </div>\n <h3\n class=\"tpl:m-0 tpl:mb-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.sidebar.noSelection }}\n </h3>\n <p class=\"tpl:m-0 tpl:text-sm tpl:leading-normal\">\n {{ t.sidebar.noSelectionHint }}\n </p>\n </div>\n </div>\n\n <div\n v-if=\"activeTab === 'settings'\"\n id=\"tpl-tabpanel-settings\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-settings\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <TemplateSettingsPanel\n :settings=\"settings\"\n @update=\"emit('update-settings', $event)\"\n />\n </div>\n\n <div\n v-if=\"activeTab === 'issues' && lintEnabled\"\n id=\"tpl-tabpanel-issues\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-issues\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <IssuesPanel />\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport { Monitor } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\n// Full-cover gate shown below the usable-width breakpoint (issue #235).\n// Rendered as the last child of `.tpl` and given a literal z-index above all\n// chrome (header z-50, sidebars z-40) AND `.tpl-popover-root` (z 10000), so the\n// opaque overlay covers everything including any open popover/modal.\n//\n// The z-index is a LITERAL, deliberately not `var(--z-modal)` or a `tpl:z-*`\n// utility: the semantic z tokens live on `:root`, unreachable from inside the\n// shadow root, and Tailwind 4 derives z utilities from `--z-index-*` (not the\n// project's `--z-*`), so `tpl:z-modal` compiles to nothing. `.tpl-popover-root`\n// uses the same literal trick for the same reason.\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl-small-screen-notice tpl:absolute tpl:inset-0 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-6 tpl:py-10 tpl:text-center tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)]\"\n style=\"z-index: 10001\"\n role=\"status\"\n data-testid=\"small-screen-notice\"\n >\n <div class=\"tpl:text-[var(--tpl-text-dim)]\">\n <Monitor :size=\"48\" :stroke-width=\"1.5\" />\n </div>\n <h2\n class=\"tpl:m-0 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.smallScreen.title }}\n </h2>\n <p class=\"tpl:m-0 tpl:max-w-sm tpl:text-sm tpl:leading-normal\">\n {{ t.smallScreen.message }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Monitor } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\n// Full-cover gate shown below the usable-width breakpoint (issue #235).\n// Rendered as the last child of `.tpl` and given a literal z-index above all\n// chrome (header z-50, sidebars z-40) AND `.tpl-popover-root` (z 10000), so the\n// opaque overlay covers everything including any open popover/modal.\n//\n// The z-index is a LITERAL, deliberately not `var(--z-modal)` or a `tpl:z-*`\n// utility: the semantic z tokens live on `:root`, unreachable from inside the\n// shadow root, and Tailwind 4 derives z utilities from `--z-index-*` (not the\n// project's `--z-*`), so `tpl:z-modal` compiles to nothing. `.tpl-popover-root`\n// uses the same literal trick for the same reason.\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl-small-screen-notice tpl:absolute tpl:inset-0 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-6 tpl:py-10 tpl:text-center tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)]\"\n style=\"z-index: 10001\"\n role=\"status\"\n data-testid=\"small-screen-notice\"\n >\n <div class=\"tpl:text-[var(--tpl-text-dim)]\">\n <Monitor :size=\"48\" :stroke-width=\"1.5\" />\n </div>\n <h2\n class=\"tpl:m-0 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.smallScreen.title }}\n </h2>\n <p class=\"tpl:m-0 tpl:max-w-sm tpl:text-sm tpl:leading-normal\">\n {{ t.smallScreen.message }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n /** Positioning classes for the footer (left/right offsets). */\n positionClass?: string | string[];\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <footer\n class=\"tpl:pointer-events-none tpl:absolute tpl:bottom-0 tpl:z-50 tpl:flex tpl:h-8 tpl:items-center tpl:justify-end tpl:pr-4 tpl:text-[9px] tpl:opacity-90 tpl:transition-all tpl:duration-300 tpl:text-[var(--tpl-text-dim)]\"\n :class=\"positionClass\"\n >\n <div\n class=\"tpl:pointer-events-auto tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-tl-lg tpl:p-1\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-canvas-bg) 85%,\n transparent\n );\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n >\n <span>{{ t.footer.poweredBy }}</span>\n <a\n href=\"https://templatical.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1 tpl:font-medium tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-muted)]\"\n style=\"text-decoration: none\"\n >\n <img\n width=\"14\"\n height=\"14\"\n src=\"https://templatical.com/logo.svg\"\n alt=\"\"\n />\n Templatical\n </a>\n <span class=\"tpl:text-[var(--tpl-border)]\">·</span>\n <a\n href=\"https://github.com/templatical/sdk\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-dim)]\"\n style=\"text-decoration: none\"\n >\n {{ t.footer.openSource }}\n </a>\n </div>\n </footer>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n /** Positioning classes for the footer (left/right offsets). */\n positionClass?: string | string[];\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <footer\n class=\"tpl:pointer-events-none tpl:absolute tpl:bottom-0 tpl:z-50 tpl:flex tpl:h-8 tpl:items-center tpl:justify-end tpl:pr-4 tpl:text-[9px] tpl:opacity-90 tpl:transition-all tpl:duration-300 tpl:text-[var(--tpl-text-dim)]\"\n :class=\"positionClass\"\n >\n <div\n class=\"tpl:pointer-events-auto tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-tl-lg tpl:p-1\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-canvas-bg) 85%,\n transparent\n );\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n >\n <span>{{ t.footer.poweredBy }}</span>\n <a\n href=\"https://templatical.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1 tpl:font-medium tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-muted)]\"\n style=\"text-decoration: none\"\n >\n <img\n width=\"14\"\n height=\"14\"\n src=\"https://templatical.com/logo.svg\"\n alt=\"\"\n />\n Templatical\n </a>\n <span class=\"tpl:text-[var(--tpl-border)]\">·</span>\n <a\n href=\"https://github.com/templatical/sdk\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-dim)]\"\n style=\"text-decoration: none\"\n >\n {{ t.footer.openSource }}\n </a>\n </div>\n </footer>\n</template>\n","<!--\n Canvas Dark Mode Preview Toggle\n\n This toggle simulates how the email template will look when the recipient's\n email client uses dark mode (e.g. Gmail, Outlook, Apple Mail in dark theme).\n It applies a CSS filter (invert + hue-rotate) to the canvas area only.\n\n This is NOT the editor UI theme toggle. The editor UI theme (light/dark/auto)\n is controlled externally via the `uiTheme` config option or `editor.setTheme()`.\n-->\n<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Moon, Sun } from \"@lucide/vue\";\n\ndefineProps<{\n darkMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", darkMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-dark-mode-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: darkMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: darkMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :title=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :aria-pressed=\"darkMode\"\n @click=\"emit('change', !darkMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Moon v-if=\"darkMode\" key=\"moon\" :size=\"18\" :stroke-width=\"1.5\" />\n <Sun v-else key=\"sun\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-dark-mode-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-dark-mode-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<!--\n Canvas Dark Mode Preview Toggle\n\n This toggle simulates how the email template will look when the recipient's\n email client uses dark mode (e.g. Gmail, Outlook, Apple Mail in dark theme).\n It applies a CSS filter (invert + hue-rotate) to the canvas area only.\n\n This is NOT the editor UI theme toggle. The editor UI theme (light/dark/auto)\n is controlled externally via the `uiTheme` config option or `editor.setTheme()`.\n-->\n<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Moon, Sun } from \"@lucide/vue\";\n\ndefineProps<{\n darkMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", darkMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-dark-mode-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: darkMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: darkMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :title=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :aria-pressed=\"darkMode\"\n @click=\"emit('change', !darkMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Moon v-if=\"darkMode\" key=\"moon\" :size=\"18\" :stroke-width=\"1.5\" />\n <Sun v-else key=\"sun\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-dark-mode-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-dark-mode-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Eye, EyeOff } from \"@lucide/vue\";\n\ndefineProps<{\n previewMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", previewMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-preview-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: previewMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: previewMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :title=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :aria-pressed=\"previewMode\"\n @click=\"emit('change', !previewMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Eye v-if=\"previewMode\" key=\"eye\" :size=\"18\" :stroke-width=\"1.5\" />\n <EyeOff v-else key=\"eye-off\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-preview-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-preview-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Eye, EyeOff } from \"@lucide/vue\";\n\ndefineProps<{\n previewMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", previewMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-preview-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: previewMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: previewMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :title=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :aria-pressed=\"previewMode\"\n @click=\"emit('change', !previewMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Eye v-if=\"previewMode\" key=\"eye\" :size=\"18\" :stroke-width=\"1.5\" />\n <EyeOff v-else key=\"eye-off\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-preview-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-preview-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, ref } from \"vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { inputClass } from \"../constants/styleConstants\";\n\nconst props = defineProps<{\n /** The loaded template's name. Undefined renders the \"Untitled\" placeholder. */\n name?: string;\n /**\n * Whether the name may be changed. False when the provider withheld `save`:\n * there would be nowhere for the new name to go, so it renders as plain text.\n */\n editable: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"commit\", name: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst isEditing = ref(false);\nconst draft = ref(\"\");\nconst inputEl = ref<HTMLInputElement | null>(null);\n\nasync function startEditing(): Promise<void> {\n if (!props.editable) return;\n draft.value = props.name ?? \"\";\n isEditing.value = true;\n await nextTick();\n inputEl.value?.focus();\n inputEl.value?.select();\n}\n\nfunction commit(): void {\n // Escape already left edit mode, and removing the input fires `blur` — which\n // would otherwise commit the very draft the user just cancelled.\n if (!isEditing.value) return;\n isEditing.value = false;\n const next = draft.value.trim();\n // An empty name is rejected by reverting: the header would have nothing to\n // render, and a cleared field is far more likely a slip than an intent.\n if (!next || next === props.name) return;\n emit(\"commit\", next);\n}\n\nfunction cancel(): void {\n isEditing.value = false;\n}\n</script>\n\n<template>\n <input\n v-if=\"isEditing\"\n ref=\"inputEl\"\n v-model=\"draft\"\n type=\"text\"\n data-testid=\"template-name-input\"\n :aria-label=\"t.header.templateName\"\n :class=\"[\n inputClass,\n 'tpl:h-8 tpl:max-w-[260px] tpl:text-sm tpl:font-medium',\n ]\"\n @keydown.enter.prevent=\"commit\"\n @keydown.esc.prevent=\"cancel\"\n @blur=\"commit\"\n />\n <button\n v-else-if=\"editable\"\n type=\"button\"\n data-testid=\"template-name\"\n :title=\"t.header.rename\"\n :aria-label=\"t.header.rename\"\n class=\"tpl:max-w-[260px] tpl:cursor-pointer tpl:truncate tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-transparent tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:border-[var(--tpl-border)] tpl:hover:bg-[var(--tpl-bg-hover)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n @click=\"startEditing\"\n >\n {{ name || t.header.untitled }}\n </button>\n <span\n v-else\n data-testid=\"template-name\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n >\n {{ name || t.header.untitled }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { nextTick, ref } from \"vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { inputClass } from \"../constants/styleConstants\";\n\nconst props = defineProps<{\n /** The loaded template's name. Undefined renders the \"Untitled\" placeholder. */\n name?: string;\n /**\n * Whether the name may be changed. False when the provider withheld `save`:\n * there would be nowhere for the new name to go, so it renders as plain text.\n */\n editable: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"commit\", name: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst isEditing = ref(false);\nconst draft = ref(\"\");\nconst inputEl = ref<HTMLInputElement | null>(null);\n\nasync function startEditing(): Promise<void> {\n if (!props.editable) return;\n draft.value = props.name ?? \"\";\n isEditing.value = true;\n await nextTick();\n inputEl.value?.focus();\n inputEl.value?.select();\n}\n\nfunction commit(): void {\n // Escape already left edit mode, and removing the input fires `blur` — which\n // would otherwise commit the very draft the user just cancelled.\n if (!isEditing.value) return;\n isEditing.value = false;\n const next = draft.value.trim();\n // An empty name is rejected by reverting: the header would have nothing to\n // render, and a cleared field is far more likely a slip than an intent.\n if (!next || next === props.name) return;\n emit(\"commit\", next);\n}\n\nfunction cancel(): void {\n isEditing.value = false;\n}\n</script>\n\n<template>\n <input\n v-if=\"isEditing\"\n ref=\"inputEl\"\n v-model=\"draft\"\n type=\"text\"\n data-testid=\"template-name-input\"\n :aria-label=\"t.header.templateName\"\n :class=\"[\n inputClass,\n 'tpl:h-8 tpl:max-w-[260px] tpl:text-sm tpl:font-medium',\n ]\"\n @keydown.enter.prevent=\"commit\"\n @keydown.esc.prevent=\"cancel\"\n @blur=\"commit\"\n />\n <button\n v-else-if=\"editable\"\n type=\"button\"\n data-testid=\"template-name\"\n :title=\"t.header.rename\"\n :aria-label=\"t.header.rename\"\n class=\"tpl:max-w-[260px] tpl:cursor-pointer tpl:truncate tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-transparent tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:border-[var(--tpl-border)] tpl:hover:bg-[var(--tpl-bg-hover)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n @click=\"startEditing\"\n >\n {{ name || t.header.untitled }}\n </button>\n <span\n v-else\n data-testid=\"template-name\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n >\n {{ name || t.header.untitled }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\n\nconst props = defineProps<{\n /** ISO 8601 timestamp, straight from the store. */\n iso: string;\n /** Which field it came from — decides the \"Updated\"/\"Created\" wording. */\n kind: \"updatedAt\" | \"createdAt\";\n}>();\n\nconst { t, format } = useI18n();\n\n// The header stays open for as long as the session does, so a label computed\n// once would sit at \"Just now\" for hours. A minute is the formatter's smallest\n// bucket, so nothing finer would show anyway.\nconst tick = ref(0);\nuseIntervalFn(() => {\n tick.value += 1;\n}, 60000);\n\nconst label = computed(() => {\n // Depended on for its side effect: `formatRelativeTime` reads the clock\n // itself, so the tick is the only thing that can invalidate this.\n void tick.value;\n const relative = formatRelativeTime(props.iso, t.time, format);\n // Null covers a value that does not parse and one further into the future\n // than clock skew explains. Falling back to an absolute date would put a\n // number where the eye expects chrome, so the line is dropped instead.\n if (relative === null) return \"\";\n // The shared buckets are phrased to stand alone, so the smallest one is\n // capitalised (\"Just now\") and reads wrong inside \"Updated {time}\". Matched\n // against the label rather than re-deriving a one-minute threshold, so what\n // counts as \"just now\" stays `formatRelativeTime`'s decision alone.\n const isJustNow = relative === t.time.justNow;\n if (props.kind === \"updatedAt\") {\n return isJustNow\n ? t.header.updatedJustNow\n : format(t.header.updatedAt, { time: relative });\n }\n return isJustNow\n ? t.header.createdJustNow\n : format(t.header.createdAt, { time: relative });\n});\n\n/** Full date for the tooltip — locale-formatted, so it needs no i18n keys. */\nconst absolute = computed(() => {\n const parsed = new Date(props.iso);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\n});\n</script>\n\n<template>\n <!-- `px-2` aligns the line under the name field's text, which carries the same\n padding for its hover affordance. Present whether or not the name renders,\n so the left column reads the same in both. -->\n <span\n v-if=\"label\"\n data-testid=\"template-timestamp\"\n :title=\"absolute\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:text-xs tpl:leading-tight tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ label }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\n\nconst props = defineProps<{\n /** ISO 8601 timestamp, straight from the store. */\n iso: string;\n /** Which field it came from — decides the \"Updated\"/\"Created\" wording. */\n kind: \"updatedAt\" | \"createdAt\";\n}>();\n\nconst { t, format } = useI18n();\n\n// The header stays open for as long as the session does, so a label computed\n// once would sit at \"Just now\" for hours. A minute is the formatter's smallest\n// bucket, so nothing finer would show anyway.\nconst tick = ref(0);\nuseIntervalFn(() => {\n tick.value += 1;\n}, 60000);\n\nconst label = computed(() => {\n // Depended on for its side effect: `formatRelativeTime` reads the clock\n // itself, so the tick is the only thing that can invalidate this.\n void tick.value;\n const relative = formatRelativeTime(props.iso, t.time, format);\n // Null covers a value that does not parse and one further into the future\n // than clock skew explains. Falling back to an absolute date would put a\n // number where the eye expects chrome, so the line is dropped instead.\n if (relative === null) return \"\";\n // The shared buckets are phrased to stand alone, so the smallest one is\n // capitalised (\"Just now\") and reads wrong inside \"Updated {time}\". Matched\n // against the label rather than re-deriving a one-minute threshold, so what\n // counts as \"just now\" stays `formatRelativeTime`'s decision alone.\n const isJustNow = relative === t.time.justNow;\n if (props.kind === \"updatedAt\") {\n return isJustNow\n ? t.header.updatedJustNow\n : format(t.header.updatedAt, { time: relative });\n }\n return isJustNow\n ? t.header.createdJustNow\n : format(t.header.createdAt, { time: relative });\n});\n\n/** Full date for the tooltip — locale-formatted, so it needs no i18n keys. */\nconst absolute = computed(() => {\n const parsed = new Date(props.iso);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\n});\n</script>\n\n<template>\n <!-- `px-2` aligns the line under the name field's text, which carries the same\n padding for its hover affordance. Present whether or not the name renders,\n so the left column reads the same in both. -->\n <span\n v-if=\"label\"\n data-testid=\"template-timestamp\"\n :title=\"absolute\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:text-xs tpl:leading-tight tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ label }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { Check, CircleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n status: \"idle\" | \"saved\" | \"error\";\n /** The provider's failure message, shown in a tooltip on the error state. */\n errorMessage: string;\n isDirty: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <!-- Three mutually exclusive states, in priority order: a failure outranks\n everything (it needs acknowledging), a fresh success outranks the dirty\n flag, and \"unsaved\" is the resting state once either decays. Rendered\n only when a templates provider can actually save — without one the editor\n never learns that a save completed, so this would read \"unsaved\" forever. -->\n <div\n v-if=\"status === 'error'\"\n aria-live=\"assertive\"\n data-testid=\"save-status-error\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"errorMessage\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saveFailed }}\n </div>\n <div\n v-else-if=\"status === 'saved'\"\n aria-live=\"polite\"\n data-testid=\"save-status-saved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saved }}\n </div>\n <div\n v-else-if=\"isDirty\"\n aria-live=\"polite\"\n data-testid=\"save-status-unsaved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ t.header.unsaved }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Check, CircleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n status: \"idle\" | \"saved\" | \"error\";\n /** The provider's failure message, shown in a tooltip on the error state. */\n errorMessage: string;\n isDirty: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <!-- Three mutually exclusive states, in priority order: a failure outranks\n everything (it needs acknowledging), a fresh success outranks the dirty\n flag, and \"unsaved\" is the resting state once either decays. Rendered\n only when a templates provider can actually save — without one the editor\n never learns that a save completed, so this would read \"unsaved\" forever. -->\n <div\n v-if=\"status === 'error'\"\n aria-live=\"assertive\"\n data-testid=\"save-status-error\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"errorMessage\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saveFailed }}\n </div>\n <div\n v-else-if=\"status === 'saved'\"\n aria-live=\"polite\"\n data-testid=\"save-status-saved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saved }}\n </div>\n <div\n v-else-if=\"isDirty\"\n aria-live=\"polite\"\n data-testid=\"save-status-unsaved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ t.header.unsaved }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { LoaderCircle, MessageCircle, Save, Send } from \"@lucide/vue\";\n\nimport type {\n BaseEditorReturn,\n UseEditorCoreReturn,\n} from \"../composables/useEditorCore\";\nimport type { UseCommentsFeatureReturn } from \"../composables/useCommentsFeature\";\nimport type { UseTemplatesFeatureReturn } from \"../composables/useTemplatesFeature\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\nimport type { UseVersionHistoryFeatureReturn } from \"../composables/useVersionHistoryFeature\";\nimport {\n primaryBtnCompactClass,\n secondaryBtnCompactClass,\n} from \"../constants/styleConstants\";\n\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport DarkModeToggle from \"./DarkModeToggle.vue\";\nimport PreviewToggle from \"./PreviewToggle.vue\";\nimport TemplateNameField from \"./TemplateNameField.vue\";\nimport TemplateTimestamp from \"./TemplateTimestamp.vue\";\nimport TemplateSaveStatus from \"./TemplateSaveStatus.vue\";\n\n// Lazy: an editor with no `versionHistory` provider never downloads the control.\nconst VersionHistoryMenu = defineAsyncComponent(\n () => import(\"./VersionHistoryMenu.vue\"),\n);\n\n/**\n * The editor's one header. Both entry points render this — `initCloud()` adds its\n * own controls through the `left-extras` / `right-extras` slots rather than\n * through a second header component, which is what keeps the two layouts from\n * drifting apart again (they already had: only Cloud carried `min-w-[200px]`, so\n * the OSS centre column was never actually centred).\n *\n * The two slots are in the anchored columns on purpose. There is deliberately no\n * centre slot: that track's width must stay constant, and a slot is the one\n * thing a guard test cannot stop someone filling. See the centre column's own\n * comment for why width changes there move the Preview button.\n *\n * Everything here is capability-driven, so the same markup covers \"no providers\n * configured at all\" and a fully-wired Cloud session. Nothing in this file knows\n * that Cloud exists.\n */\ndefineProps<{\n editor: BaseEditorReturn;\n core: UseEditorCoreReturn;\n /** Null when no `TemplatesProvider` is configured — no name, save or status. */\n templates: UseTemplatesFeatureReturn | null;\n /**\n * Whether to render the inline name field, from `config.templates?.nameField`.\n * False hides it whether or not the provider can save — a consumer whose\n * store has no name column, or whose own chrome owns the name.\n */\n showTemplateName: boolean;\n testEmail: UseTestEmailFeatureReturn | null;\n versionHistory: UseVersionHistoryFeatureReturn | null;\n /** Null when no `CommentsProvider` (or no `user`) is configured. */\n comments: UseCommentsFeatureReturn | null;\n}>();\n</script>\n\n<template>\n <!-- Opaque, and `--tpl-shadow-sm` because that is the step the shadow\n vocabulary assigns to sticky chrome. `.tpl-body` starts at this header's\n own height, so nothing ever passes beneath it: a translucent fill has an\n identical surface behind it and a backdrop blur has nothing to blur,\n which costs a compositing layer to render exactly `--tpl-bg`. -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <!-- Left: the template's identity — the inline-editable name, and under it\n when the store dates its writes. Stacking the two needs 6px of bottom\n padding: the name is a button, so it carries border + `py-1` + half its\n leading above its text, while the timestamp carries nothing below its\n own. Centring the boxes therefore puts all of that lead at the top and\n reads as a misaligned pair. Only when both render — either alone is\n already centred on its own box. `min-w-[200px]` matches the right column\n so the centre controls are actually centred. Nothing renders before a\n template exists; there is neither a name nor a write time yet. The stack\n is gated as a whole so an empty one cannot add a `gap-2.5` before the\n cloud extras. -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-2.5\"\n >\n <div\n v-if=\"\n templates?.isAvailable.value &&\n templates.hasTemplate.value &&\n (showTemplateName || templates.timestamp.value)\n \"\n class=\"tpl:flex tpl:min-w-0 tpl:flex-col tpl:items-start\"\n :class=\"{\n 'tpl:pb-1.5': showTemplateName && templates.timestamp.value !== null,\n }\"\n >\n <TemplateNameField\n v-if=\"showTemplateName\"\n :name=\"templates.name.value\"\n :editable=\"templates.canSave.value\"\n @commit=\"templates.rename\"\n />\n <TemplateTimestamp\n v-if=\"templates.timestamp.value\"\n :iso=\"templates.timestamp.value.iso\"\n :kind=\"templates.timestamp.value.kind\"\n />\n </div>\n <!-- Version history — the third piece of \"which template, and which\n version of it\", after the name and the write time. It sits here and\n not in the right column because that column's content already\n overflows its track: `justify-end` packs to the right edge, so an\n extra control there spills *leftward* over the centre track and\n swallows clicks meant for the Preview button. Measured — it covered\n it with the menu's own back-arrow. This column packs from the left\n and has the room.\n\n Needs a template as well as a provider: a version belongs to a\n template id, so before one is loaded there is nothing to list. -->\n <VersionHistoryMenu\n v-if=\"\n versionHistory?.isAvailable.value && versionHistory.hasTemplate.value\n \"\n :versions=\"versionHistory.versions.value\"\n :is-loading=\"versionHistory.isLoading.value\"\n :is-restoring=\"versionHistory.isRestoring.value\"\n :previewing-id=\"versionHistory.previewingVersion.value?.id ?? null\"\n @open=\"versionHistory.refresh()\"\n @navigate=\"versionHistory.navigate($event)\"\n />\n <slot name=\"left-extras\" />\n </div>\n\n <!-- Center: the three \"how am I viewing the canvas\" controls, and nothing\n else — ever.\n\n The grid is `1fr auto 1fr`, so this track is exactly max-content wide\n and the equal `fr` columns centre it. Any width change therefore\n redistributes symmetrically about the header's centre and moves every\n sibling by half the delta, whichever side of the change it sits on.\n DOM order buys nothing. So a conditional control here drags the Preview\n button out from under the cursor — 114.5px when the sample/label toggle\n used to live here, and repeatedly in Cloud as collaborators came and\n went (#574).\n\n Hence the rule: nothing in this track may be conditional. No `v-if`, no\n `v-show`, no slot. Conditional controls go in the edge-anchored `fr`\n columns, which grow away from their anchored edge; preview-only ones go\n on the preview surface, where `Editor.vue` floats them over the canvas.\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n </div>\n\n <!-- Right: save status → Comments → [cloud extras: AI] → test email → Save.\n Save is last: it is the primary action. `justify-end` anchors the row to\n the right edge, so a control appearing at its start extends the row\n leftward and moves nothing already in it — but note the row is already\n at the edge of its track at common widths, so anything added here\n overflows onto the centre rather than fitting. -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Gated on a provider that can actually save. Without one the editor\n never learns that a save completed, so the badge could only ever read\n \"unsaved\" — worse than showing nothing. -->\n <TemplateSaveStatus\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n :status=\"templates.status.value\"\n :error-message=\"templates.errorMessage.value\"\n :is-dirty=\"editor.state.isDirty\"\n />\n\n <!-- Comments. Needs a template as well as a provider — a comment is keyed\n to a template id, so before one is loaded there is nothing to attach a\n thread to. Shared chrome, because comments are a shared\n provider-backed feature rather than a Cloud one. -->\n <button\n v-if=\"comments?.isAvailable.value && comments.hasTemplate.value\"\n type=\"button\"\n data-testid=\"comments-trigger\"\n :aria-label=\"\n comments.unresolvedCount.value > 0\n ? `${core.t.comments.button} (${comments.unresolvedCount.value})`\n : core.t.comments.button\n \"\n :aria-expanded=\"comments.isOpen.value\"\n :class=\"secondaryBtnCompactClass\"\n :style=\"\n comments.isOpen.value\n ? {\n backgroundColor: 'var(--tpl-primary-light)',\n borderColor: 'var(--tpl-primary-light)',\n }\n : undefined\n \"\n @click=\"comments.toggle()\"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.comments.button }}\n <span\n v-if=\"comments.unresolvedCount.value > 0 && !comments.isOpen.value\"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </button>\n\n <slot name=\"right-extras\" />\n\n <!-- The shared recipe, like every control in this row. Shared rather than\n hand-rolled so all three keep one height — a bespoke string is how\n this button came to sit 4px shorter than its neighbours. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n :disabled=\"testEmail.isSending.value\"\n :class=\"secondaryBtnCompactClass\"\n @click=\"testEmail.open()\"\n >\n <Send v-if=\"!testEmail.isSending.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.testEmail.button }}\n </button>\n\n <!-- Save is the editor's one primary, and it only takes that treatment\n while there is something to save. Amber announces intent or selection,\n so a Save that looks identical dirty or clean announces nothing — and\n it would be the loudest element in chrome that exists to recede behind\n the canvas. Gated on `hasTemplate` too, or a disabled button would\n light up before there is anywhere to save to.\n\n Amber with the paper colour on it is 2.80:1 in light mode. That is\n accepted rather than overlooked: every amber surface and accent in the\n editor shares this pairing, and correcting this one control alone would\n make it read as a different kind of thing. The Amber-Accent Exception\n in DESIGN.md records the decision and its limits — chief among them\n that amber must never be the only carrier of a state, which is why the\n `TemplateSaveStatus` badge says \"Unsaved\" in words beside it.\n\n The light lift is the same condition, and it is Flat-At-Rest applied\n rather than bent: a surface paints no shadow until something happens to\n it, and unsaved work is that something. It sits inline rather than in\n the recipe because the recipe is shared, and a primary button at rest\n elsewhere should still be flat.\n\n Hidden entirely when the provider withheld `save` — the read-only\n equivalent of the saved-blocks permission discipline: loading a\n template and editing it locally still works, there is simply nothing\n to persist. Disabled (with a reason in the tooltip) until a template\n exists, since `save()` patches an id. -->\n <button\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n type=\"button\"\n data-testid=\"template-save\"\n :class=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? primaryBtnCompactClass\n : secondaryBtnCompactClass\n \"\n :style=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? { boxShadow: 'var(--tpl-shadow-sm)' }\n : undefined\n \"\n :disabled=\"templates.isSaving.value || !templates.hasTemplate.value\"\n :title=\"\n templates.hasTemplate.value\n ? core.t.header.save\n : core.t.header.saveNoTemplate\n \"\n @click=\"templates.requestSave()\"\n >\n <Save v-if=\"!templates.isSaving.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{\n templates.isSaving.value ? core.t.header.saving : core.t.header.save\n }}\n </button>\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { LoaderCircle, MessageCircle, Save, Send } from \"@lucide/vue\";\n\nimport type {\n BaseEditorReturn,\n UseEditorCoreReturn,\n} from \"../composables/useEditorCore\";\nimport type { UseCommentsFeatureReturn } from \"../composables/useCommentsFeature\";\nimport type { UseTemplatesFeatureReturn } from \"../composables/useTemplatesFeature\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\nimport type { UseVersionHistoryFeatureReturn } from \"../composables/useVersionHistoryFeature\";\nimport {\n primaryBtnCompactClass,\n secondaryBtnCompactClass,\n} from \"../constants/styleConstants\";\n\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport DarkModeToggle from \"./DarkModeToggle.vue\";\nimport PreviewToggle from \"./PreviewToggle.vue\";\nimport TemplateNameField from \"./TemplateNameField.vue\";\nimport TemplateTimestamp from \"./TemplateTimestamp.vue\";\nimport TemplateSaveStatus from \"./TemplateSaveStatus.vue\";\n\n// Lazy: an editor with no `versionHistory` provider never downloads the control.\nconst VersionHistoryMenu = defineAsyncComponent(\n () => import(\"./VersionHistoryMenu.vue\"),\n);\n\n/**\n * The editor's one header. Both entry points render this — `initCloud()` adds its\n * own controls through the `left-extras` / `right-extras` slots rather than\n * through a second header component, which is what keeps the two layouts from\n * drifting apart again (they already had: only Cloud carried `min-w-[200px]`, so\n * the OSS centre column was never actually centred).\n *\n * The two slots are in the anchored columns on purpose. There is deliberately no\n * centre slot: that track's width must stay constant, and a slot is the one\n * thing a guard test cannot stop someone filling. See the centre column's own\n * comment for why width changes there move the Preview button.\n *\n * Everything here is capability-driven, so the same markup covers \"no providers\n * configured at all\" and a fully-wired Cloud session. Nothing in this file knows\n * that Cloud exists.\n */\ndefineProps<{\n editor: BaseEditorReturn;\n core: UseEditorCoreReturn;\n /** Null when no `TemplatesProvider` is configured — no name, save or status. */\n templates: UseTemplatesFeatureReturn | null;\n /**\n * Whether to render the inline name field, from `config.templates?.nameField`.\n * False hides it whether or not the provider can save — a consumer whose\n * store has no name column, or whose own chrome owns the name.\n */\n showTemplateName: boolean;\n testEmail: UseTestEmailFeatureReturn | null;\n versionHistory: UseVersionHistoryFeatureReturn | null;\n /** Null when no `CommentsProvider` (or no `user`) is configured. */\n comments: UseCommentsFeatureReturn | null;\n}>();\n</script>\n\n<template>\n <!-- Opaque, and `--tpl-shadow-sm` because that is the step the shadow\n vocabulary assigns to sticky chrome. `.tpl-body` starts at this header's\n own height, so nothing ever passes beneath it: a translucent fill has an\n identical surface behind it and a backdrop blur has nothing to blur,\n which costs a compositing layer to render exactly `--tpl-bg`. -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <!-- Left: the template's identity — the inline-editable name, and under it\n when the store dates its writes. Stacking the two needs 6px of bottom\n padding: the name is a button, so it carries border + `py-1` + half its\n leading above its text, while the timestamp carries nothing below its\n own. Centring the boxes therefore puts all of that lead at the top and\n reads as a misaligned pair. Only when both render — either alone is\n already centred on its own box. `min-w-[200px]` matches the right column\n so the centre controls are actually centred. Nothing renders before a\n template exists; there is neither a name nor a write time yet. The stack\n is gated as a whole so an empty one cannot add a `gap-2.5` before the\n cloud extras. -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-2.5\"\n >\n <div\n v-if=\"\n templates?.isAvailable.value &&\n templates.hasTemplate.value &&\n (showTemplateName || templates.timestamp.value)\n \"\n class=\"tpl:flex tpl:min-w-0 tpl:flex-col tpl:items-start\"\n :class=\"{\n 'tpl:pb-1.5': showTemplateName && templates.timestamp.value !== null,\n }\"\n >\n <TemplateNameField\n v-if=\"showTemplateName\"\n :name=\"templates.name.value\"\n :editable=\"templates.canSave.value\"\n @commit=\"templates.rename\"\n />\n <TemplateTimestamp\n v-if=\"templates.timestamp.value\"\n :iso=\"templates.timestamp.value.iso\"\n :kind=\"templates.timestamp.value.kind\"\n />\n </div>\n <!-- Version history — the third piece of \"which template, and which\n version of it\", after the name and the write time. It sits here and\n not in the right column because that column's content already\n overflows its track: `justify-end` packs to the right edge, so an\n extra control there spills *leftward* over the centre track and\n swallows clicks meant for the Preview button. Measured — it covered\n it with the menu's own back-arrow. This column packs from the left\n and has the room.\n\n Needs a template as well as a provider: a version belongs to a\n template id, so before one is loaded there is nothing to list. -->\n <VersionHistoryMenu\n v-if=\"\n versionHistory?.isAvailable.value && versionHistory.hasTemplate.value\n \"\n :versions=\"versionHistory.versions.value\"\n :is-loading=\"versionHistory.isLoading.value\"\n :is-restoring=\"versionHistory.isRestoring.value\"\n :previewing-id=\"versionHistory.previewingVersion.value?.id ?? null\"\n @open=\"versionHistory.refresh()\"\n @navigate=\"versionHistory.navigate($event)\"\n />\n <slot name=\"left-extras\" />\n </div>\n\n <!-- Center: the three \"how am I viewing the canvas\" controls, and nothing\n else — ever.\n\n The grid is `1fr auto 1fr`, so this track is exactly max-content wide\n and the equal `fr` columns centre it. Any width change therefore\n redistributes symmetrically about the header's centre and moves every\n sibling by half the delta, whichever side of the change it sits on.\n DOM order buys nothing. So a conditional control here drags the Preview\n button out from under the cursor — 114.5px when the sample/label toggle\n used to live here, and repeatedly in Cloud as collaborators came and\n went (#574).\n\n Hence the rule: nothing in this track may be conditional. No `v-if`, no\n `v-show`, no slot. Conditional controls go in the edge-anchored `fr`\n columns, which grow away from their anchored edge; preview-only ones go\n on the preview surface, where `Editor.vue` floats them over the canvas.\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n </div>\n\n <!-- Right: save status → Comments → [cloud extras: AI] → test email → Save.\n Save is last: it is the primary action. `justify-end` anchors the row to\n the right edge, so a control appearing at its start extends the row\n leftward and moves nothing already in it — but note the row is already\n at the edge of its track at common widths, so anything added here\n overflows onto the centre rather than fitting. -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Gated on a provider that can actually save. Without one the editor\n never learns that a save completed, so the badge could only ever read\n \"unsaved\" — worse than showing nothing. -->\n <TemplateSaveStatus\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n :status=\"templates.status.value\"\n :error-message=\"templates.errorMessage.value\"\n :is-dirty=\"editor.state.isDirty\"\n />\n\n <!-- Comments. Needs a template as well as a provider — a comment is keyed\n to a template id, so before one is loaded there is nothing to attach a\n thread to. Shared chrome, because comments are a shared\n provider-backed feature rather than a Cloud one. -->\n <button\n v-if=\"comments?.isAvailable.value && comments.hasTemplate.value\"\n type=\"button\"\n data-testid=\"comments-trigger\"\n :aria-label=\"\n comments.unresolvedCount.value > 0\n ? `${core.t.comments.button} (${comments.unresolvedCount.value})`\n : core.t.comments.button\n \"\n :aria-expanded=\"comments.isOpen.value\"\n :class=\"secondaryBtnCompactClass\"\n :style=\"\n comments.isOpen.value\n ? {\n backgroundColor: 'var(--tpl-primary-light)',\n borderColor: 'var(--tpl-primary-light)',\n }\n : undefined\n \"\n @click=\"comments.toggle()\"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.comments.button }}\n <span\n v-if=\"comments.unresolvedCount.value > 0 && !comments.isOpen.value\"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </button>\n\n <slot name=\"right-extras\" />\n\n <!-- The shared recipe, like every control in this row. Shared rather than\n hand-rolled so all three keep one height — a bespoke string is how\n this button came to sit 4px shorter than its neighbours. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n :disabled=\"testEmail.isSending.value\"\n :class=\"secondaryBtnCompactClass\"\n @click=\"testEmail.open()\"\n >\n <Send v-if=\"!testEmail.isSending.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.testEmail.button }}\n </button>\n\n <!-- Save is the editor's one primary, and it only takes that treatment\n while there is something to save. Amber announces intent or selection,\n so a Save that looks identical dirty or clean announces nothing — and\n it would be the loudest element in chrome that exists to recede behind\n the canvas. Gated on `hasTemplate` too, or a disabled button would\n light up before there is anywhere to save to.\n\n Amber with the paper colour on it is 2.80:1 in light mode. That is\n accepted rather than overlooked: every amber surface and accent in the\n editor shares this pairing, and correcting this one control alone would\n make it read as a different kind of thing. The Amber-Accent Exception\n in DESIGN.md records the decision and its limits — chief among them\n that amber must never be the only carrier of a state, which is why the\n `TemplateSaveStatus` badge says \"Unsaved\" in words beside it.\n\n The light lift is the same condition, and it is Flat-At-Rest applied\n rather than bent: a surface paints no shadow until something happens to\n it, and unsaved work is that something. It sits inline rather than in\n the recipe because the recipe is shared, and a primary button at rest\n elsewhere should still be flat.\n\n Hidden entirely when the provider withheld `save` — the read-only\n equivalent of the saved-blocks permission discipline: loading a\n template and editing it locally still works, there is simply nothing\n to persist. Disabled (with a reason in the tooltip) until a template\n exists, since `save()` patches an id. -->\n <button\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n type=\"button\"\n data-testid=\"template-save\"\n :class=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? primaryBtnCompactClass\n : secondaryBtnCompactClass\n \"\n :style=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? { boxShadow: 'var(--tpl-shadow-sm)' }\n : undefined\n \"\n :disabled=\"templates.isSaving.value || !templates.hasTemplate.value\"\n :title=\"\n templates.hasTemplate.value\n ? core.t.header.save\n : core.t.header.saveNoTemplate\n \"\n @click=\"templates.requestSave()\"\n >\n <Save v-if=\"!templates.isSaving.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{\n templates.isSaving.value ? core.t.header.saving : core.t.header.save\n }}\n </button>\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport {\n computed,\n inject,\n nextTick,\n ref,\n watch,\n type ComponentPublicInstance,\n} from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { ChevronDown, Search, X } from \"@lucide/vue\";\nimport type { MergeTag } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n MERGE_TAG_PICKER_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\n\n// Picker singleton — provided by useEditorCore. We require it explicitly so\n// the modal is unmountable only in a non-editor context (which we don't\n// support). `requireInject` throws a clear message rather than letting a\n// silent inject default mask the misuse.\nconst picker = requireInject(MERGE_TAG_PICKER_KEY, \"MergeTagPickerModal\");\nconst { t, format } = useI18n();\n\n// Theming — same pattern as every other OSS panel (ParagraphToolbar,\n// RichTextLinkDialog). The panel carries the `tpl` token class, so it must\n// re-establish the theme locally: `data-tpl-theme` selects the dark token\n// block, and the inline `themeStyles` apply the consumer's `theme` config\n// overrides. Without these the base `.tpl` rule shadows both with light\n// defaults and the picker ignores dark mode + custom theme colors.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\n// --- Search ---\nconst rawSearch = ref(\"\");\n// 200ms debounce keeps filtering responsive at 1000+ tags. The raw value\n// drives the input element (so typing feels instant); the filter reads\n// the debounced copy.\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-merge-tag-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\n// --- Highlight (keyboard nav state) ---\nconst highlightedIndex = ref(0);\n\n// --- Grouping mode ---\n// Active only when at least one tag carries `group`. When inactive, the\n// list renders flat with no headers (not even \"Other\"). The plan locks\n// this down in \"Decisions locked in\" #7.\nconst hasGroups = computed(() =>\n picker.tags.value.some((tag) => Boolean(tag.group)),\n);\n\n// --- Collapsed groups (by label) ---\n// Reset on every modal open so each session starts with all groups\n// visible. Persistence across opens would surprise users who expanded\n// in a previous edit; the cost of re-collapsing is one click.\nconst collapsedGroups = ref<Set<string>>(new Set());\nfunction isCollapsed(group: string): boolean {\n return collapsedGroups.value.has(group);\n}\nfunction toggleGroup(group: string): void {\n const next = new Set(collapsedGroups.value);\n if (next.has(group)) next.delete(group);\n else next.add(group);\n collapsedGroups.value = next;\n // Re-anchor highlight to the first visible tag — index of a\n // previously-highlighted item may now point elsewhere after the\n // collapse rearranges visibleTags.\n highlightedIndex.value = 0;\n}\n\n// --- Filtered tags ---\n// Case-insensitive substring match against label, value, description.\n// Insertion order is preserved (per decision #9).\nconst filteredTags = computed<MergeTag[]>(() => {\n const query = search.value.trim().toLowerCase();\n if (!query) return picker.tags.value;\n return picker.tags.value.filter((tag) => {\n const haystack = [tag.label, tag.value, tag.description ?? \"\"]\n .join(\" \")\n .toLowerCase();\n return haystack.includes(query);\n });\n});\n\n// --- Group order (insertion order of first appearance) ---\n// Computed against the *full* tags list (not the filtered one) so the\n// pill row stays stable as the user types. The pills represent the\n// configured groupings; filter just adjusts which items are shown.\nconst groupOrder = computed<string[]>(() => {\n if (!hasGroups.value) return [];\n const otherLabel = t.mergeTag.picker.otherGroup;\n const seen = new Set<string>();\n const order: string[] = [];\n for (const tag of picker.tags.value) {\n const key = tag.group ?? otherLabel;\n if (seen.has(key)) continue;\n seen.add(key);\n order.push(key);\n }\n return order;\n});\n\n// --- Rendered rows (header + tag entries) ---\n// Headers are only emitted in grouped mode AND only when there is no\n// active search query (per decisions #6 + #7). Flat-list mode and\n// search-active mode both render a header-less sequence.\n//\n// Collapsed groups still emit their header row (so the header stays\n// clickable), but skip their tag rows entirely — those tags are then\n// absent from `visibleTags` and the highlight skips over them.\ntype Row =\n | { kind: \"header\"; group: string; count: number }\n | { kind: \"tag\"; tag: MergeTag; index: number };\n\nconst rows = computed<Row[]>(() => {\n const tags = filteredTags.value;\n if (!hasGroups.value || searchActive.value) {\n return tags.map((tag, index) => ({ kind: \"tag\", tag, index }));\n }\n\n // Grouped mode: bucket by `group`, ungrouped → \"Other\".\n const otherLabel = t.mergeTag.picker.otherGroup;\n const buckets = new Map<string, MergeTag[]>();\n // Insertion order over the first appearance of each group label.\n const order: string[] = [];\n for (const tag of tags) {\n const key = tag.group ?? otherLabel;\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(tag);\n }\n\n const result: Row[] = [];\n let tagIndex = 0;\n for (const groupKey of order) {\n const items = buckets.get(groupKey)!;\n result.push({ kind: \"header\", group: groupKey, count: items.length });\n if (collapsedGroups.value.has(groupKey)) continue;\n for (const tag of items) {\n result.push({ kind: \"tag\", tag, index: tagIndex });\n tagIndex++;\n }\n }\n return result;\n});\n\n// Flat list of tag-rows only — used for highlight bookkeeping. Keyboard\n// nav skips headers, so the highlight index addresses this list directly.\nconst visibleTags = computed(() =>\n rows.value.flatMap((row) => (row.kind === \"tag\" ? [row.tag] : [])),\n);\n\n// --- Lifecycle: focus search + reset state when modal opens ---\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n collapsedGroups.value = new Set();\n // Search input is mounted as part of the same render cycle the\n // modal opens; nextTick guarantees the ref is bound before focus.\n nextTick(() => {\n searchInputRef.value?.focus();\n });\n }\n },\n);\n\n// Keep the highlight inside the visible-tags bounds whenever the\n// filtered list shrinks (e.g. user typed a more specific query).\nwatch(visibleTags, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\n// --- Actions ---\nfunction selectTag(tag: MergeTag): void {\n picker.resolve(tag);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleTags.value.length === 0) return;\n const next = highlightedIndex.value + delta;\n // Stop at edges (don't wrap) — feels more predictable in a long list.\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleTags.value.length - 1, next),\n );\n scrollHighlightIntoView();\n}\n\nconst listRef = ref<HTMLElement | null>(null);\n// One ref per group header so the pill row can `scrollIntoView` precisely.\n// Bound by callback-ref in the template; cleared (set to null) when Vue\n// unmounts the header during re-render.\nconst headerRefs = ref<Record<string, HTMLElement | null>>({});\n// Vue's callback-ref type widens to Element | ComponentPublicInstance | null\n// because the same ref shape works for both DOM elements and components.\n// Headers are always native buttons, so coerce to HTMLElement once here.\nfunction setHeaderRef(\n group: string,\n el: Element | ComponentPublicInstance | null,\n): void {\n headerRefs.value[group] = (el as HTMLElement | null) ?? null;\n}\n\nfunction scrollHighlightIntoView(): void {\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n const el = container.querySelector<HTMLElement>(\n `[data-merge-tag-index=\"${highlightedIndex.value}\"]`,\n );\n el?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction jumpToGroup(group: string): void {\n // Expand the group if it was collapsed — otherwise scrolling reveals\n // only the header, which is confusing for a \"jump to\" affordance.\n if (collapsedGroups.value.has(group)) {\n const next = new Set(collapsedGroups.value);\n next.delete(group);\n collapsedGroups.value = next;\n }\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n // Sum the height of every child rendered BEFORE the target header.\n // Avoids two browser quirks:\n // - `scrollIntoView({ block: \"start\" })` short-circuits when the\n // element is currently sticky-pinned at top.\n // - `offsetTop` historically misreported on sticky elements in some\n // Chromium versions; offsetHeight on each in-flow child is stable.\n // Each child is a top-level DOM row (header button or tag button),\n // and there are at most a few dozen, so the linear scan is cheap.\n let targetScrollTop = 0;\n let found = false;\n for (const child of Array.from(container.children) as HTMLElement[]) {\n if (\n child.dataset.testid === \"merge-tag-picker-group-header\" &&\n child.dataset.groupName === group\n ) {\n found = true;\n break;\n }\n targetScrollTop += child.offsetHeight;\n }\n if (!found) return;\n container.scrollTo({ top: targetScrollTop, behavior: \"smooth\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const tag = visibleTags.value[highlightedIndex.value];\n if (tag) selectTag(tag);\n }\n // Esc is forwarded by TplModal via its own keydown→close path.\n}\n\nfunction activeOptionId(): string | undefined {\n if (visibleTags.value.length === 0) return undefined;\n return `${listId}-opt-${highlightedIndex.value}`;\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\n\n// --- Empty states ---\n// Distinguish two cases so the messaging is honest:\n// - the consumer configured zero tags (defensive — button shouldn't even\n// render in that case but a programmatic open() should still degrade)\n// - the active search matched nothing\n//\n// `filteredTags` (NOT `visibleTags`) is the correct signal for \"no\n// results\" — visibleTags also excludes tags inside collapsed groups, so\n// collapsing every group would otherwise falsely trigger the empty\n// state and hide the headers the user wants to see.\nconst isEmptyConfig = computed(() => picker.tags.value.length === 0);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && filteredTags.value.length === 0,\n);\n\n// --- Pill row visibility ---\n// Pills jump-link to group headers; they only make sense when there are\n// at least 2 groups AND the user isn't currently searching (search\n// collapses the grouped view to a flat list, so the pills' targets\n// disappear).\nconst showPillRow = computed(\n () => hasGroups.value && groupOrder.value.length > 1 && !searchActive.value,\n);\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"merge-tag-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <!-- Header -->\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.mergeTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.mergeTag.picker.close\"\n data-testid=\"merge-tag-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <!-- Search -->\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.mergeTag.picker.searchPlaceholder\"\n :aria-label=\"t.mergeTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"merge-tag-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <!-- Group pills — visible only when grouping is active and the\n user isn't searching. Click a pill to jump (and expand) the\n matching group. Horizontally scrollable so a long group list\n degrades gracefully. -->\n <div\n v-if=\"showPillRow\"\n class=\"tpl:flex tpl:gap-1.5 tpl:overflow-x-auto tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2\"\n data-testid=\"merge-tag-picker-group-pills\"\n >\n <button\n v-for=\"group in groupOrder\"\n :key=\"group\"\n type=\"button\"\n class=\"tpl:flex tpl:flex-shrink-0 tpl:cursor-pointer tpl:items-center tpl:rounded-full tpl:border tpl:border-[var(--tpl-border)] tpl:bg-transparent tpl:px-2.5 tpl:py-0.5 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n data-testid=\"merge-tag-picker-group-pill\"\n :data-group-name=\"group\"\n @click=\"jumpToGroup(group)\"\n >\n {{ group }}\n </button>\n </div>\n\n <!-- Body / list -->\n <!-- `position: relative` makes this the offsetParent for the sticky\n group headers below, so `header.offsetTop` reliably reports each\n header's natural in-flow position. jumpToGroup() depends on this\n — see the comment in jumpToGroup() for why scrollIntoView isn't\n sufficient. -->\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.mergeTag.picker.title\"\n data-testid=\"merge-tag-picker-list\"\n >\n <template v-if=\"isEmptyConfig\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.empty }}\n </div>\n </template>\n <template v-else-if=\"isNoResults\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.noResults }}\n </div>\n </template>\n <template v-else>\n <template v-for=\"(row, rowIndex) in rows\" :key=\"rowIndex\">\n <button\n v-if=\"row.kind === 'header'\"\n :ref=\"(el) => setHeaderRef(row.group, el)\"\n type=\"button\"\n :aria-expanded=\"!isCollapsed(row.group)\"\n :aria-controls=\"listId\"\n class=\"tpl:sticky tpl:top-0 tpl:z-10 tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-[var(--tpl-bg-elevated)] tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-left tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)]\"\n data-testid=\"merge-tag-picker-group-header\"\n :data-group-name=\"row.group\"\n :data-group-collapsed=\"isCollapsed(row.group) ? 'true' : 'false'\"\n @click=\"toggleGroup(row.group)\"\n >\n <ChevronDown\n :size=\"12\"\n :stroke-width=\"2\"\n class=\"tpl:transition-transform\"\n :class=\"isCollapsed(row.group) ? 'tpl:-rotate-90' : ''\"\n />\n <span>{{ row.group }}</span>\n <span class=\"tpl:ml-1 tpl:font-normal\">\n ({{\n format(t.mergeTag.picker.groupCount, { count: row.count })\n }})\n </span>\n </button>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-merge-tag-index=\"row.index\"\n :data-merge-tag-value=\"row.tag.value\"\n :title=\"\n row.tag.description\n ? `${row.tag.value} — ${row.tag.description}`\n : row.tag.value\n \"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"merge-tag-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectTag(row.tag)\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.tag.label\n }}</span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.value }}\n </span>\n <span\n v-if=\"row.tag.description\"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.description }}\n </span>\n <!-- What the previews will render in Sample mode, so an author\n knows before inserting. Shown only when configured. -->\n <span\n v-if=\"row.tag.sample !== undefined\"\n data-testid=\"merge-tag-picker-sample\"\n class=\"tpl:line-clamp-1 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.mergeTag.picker.sample, { sample: row.tag.sample })\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport {\n computed,\n inject,\n nextTick,\n ref,\n watch,\n type ComponentPublicInstance,\n} from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { ChevronDown, Search, X } from \"@lucide/vue\";\nimport type { MergeTag } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n MERGE_TAG_PICKER_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\n\n// Picker singleton — provided by useEditorCore. We require it explicitly so\n// the modal is unmountable only in a non-editor context (which we don't\n// support). `requireInject` throws a clear message rather than letting a\n// silent inject default mask the misuse.\nconst picker = requireInject(MERGE_TAG_PICKER_KEY, \"MergeTagPickerModal\");\nconst { t, format } = useI18n();\n\n// Theming — same pattern as every other OSS panel (ParagraphToolbar,\n// RichTextLinkDialog). The panel carries the `tpl` token class, so it must\n// re-establish the theme locally: `data-tpl-theme` selects the dark token\n// block, and the inline `themeStyles` apply the consumer's `theme` config\n// overrides. Without these the base `.tpl` rule shadows both with light\n// defaults and the picker ignores dark mode + custom theme colors.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\n// --- Search ---\nconst rawSearch = ref(\"\");\n// 200ms debounce keeps filtering responsive at 1000+ tags. The raw value\n// drives the input element (so typing feels instant); the filter reads\n// the debounced copy.\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-merge-tag-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\n// --- Highlight (keyboard nav state) ---\nconst highlightedIndex = ref(0);\n\n// --- Grouping mode ---\n// Active only when at least one tag carries `group`. When inactive, the\n// list renders flat with no headers (not even \"Other\"). The plan locks\n// this down in \"Decisions locked in\" #7.\nconst hasGroups = computed(() =>\n picker.tags.value.some((tag) => Boolean(tag.group)),\n);\n\n// --- Collapsed groups (by label) ---\n// Reset on every modal open so each session starts with all groups\n// visible. Persistence across opens would surprise users who expanded\n// in a previous edit; the cost of re-collapsing is one click.\nconst collapsedGroups = ref<Set<string>>(new Set());\nfunction isCollapsed(group: string): boolean {\n return collapsedGroups.value.has(group);\n}\nfunction toggleGroup(group: string): void {\n const next = new Set(collapsedGroups.value);\n if (next.has(group)) next.delete(group);\n else next.add(group);\n collapsedGroups.value = next;\n // Re-anchor highlight to the first visible tag — index of a\n // previously-highlighted item may now point elsewhere after the\n // collapse rearranges visibleTags.\n highlightedIndex.value = 0;\n}\n\n// --- Filtered tags ---\n// Case-insensitive substring match against label, value, description.\n// Insertion order is preserved (per decision #9).\nconst filteredTags = computed<MergeTag[]>(() => {\n const query = search.value.trim().toLowerCase();\n if (!query) return picker.tags.value;\n return picker.tags.value.filter((tag) => {\n const haystack = [tag.label, tag.value, tag.description ?? \"\"]\n .join(\" \")\n .toLowerCase();\n return haystack.includes(query);\n });\n});\n\n// --- Group order (insertion order of first appearance) ---\n// Computed against the *full* tags list (not the filtered one) so the\n// pill row stays stable as the user types. The pills represent the\n// configured groupings; filter just adjusts which items are shown.\nconst groupOrder = computed<string[]>(() => {\n if (!hasGroups.value) return [];\n const otherLabel = t.mergeTag.picker.otherGroup;\n const seen = new Set<string>();\n const order: string[] = [];\n for (const tag of picker.tags.value) {\n const key = tag.group ?? otherLabel;\n if (seen.has(key)) continue;\n seen.add(key);\n order.push(key);\n }\n return order;\n});\n\n// --- Rendered rows (header + tag entries) ---\n// Headers are only emitted in grouped mode AND only when there is no\n// active search query (per decisions #6 + #7). Flat-list mode and\n// search-active mode both render a header-less sequence.\n//\n// Collapsed groups still emit their header row (so the header stays\n// clickable), but skip their tag rows entirely — those tags are then\n// absent from `visibleTags` and the highlight skips over them.\ntype Row =\n | { kind: \"header\"; group: string; count: number }\n | { kind: \"tag\"; tag: MergeTag; index: number };\n\nconst rows = computed<Row[]>(() => {\n const tags = filteredTags.value;\n if (!hasGroups.value || searchActive.value) {\n return tags.map((tag, index) => ({ kind: \"tag\", tag, index }));\n }\n\n // Grouped mode: bucket by `group`, ungrouped → \"Other\".\n const otherLabel = t.mergeTag.picker.otherGroup;\n const buckets = new Map<string, MergeTag[]>();\n // Insertion order over the first appearance of each group label.\n const order: string[] = [];\n for (const tag of tags) {\n const key = tag.group ?? otherLabel;\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(tag);\n }\n\n const result: Row[] = [];\n let tagIndex = 0;\n for (const groupKey of order) {\n const items = buckets.get(groupKey)!;\n result.push({ kind: \"header\", group: groupKey, count: items.length });\n if (collapsedGroups.value.has(groupKey)) continue;\n for (const tag of items) {\n result.push({ kind: \"tag\", tag, index: tagIndex });\n tagIndex++;\n }\n }\n return result;\n});\n\n// Flat list of tag-rows only — used for highlight bookkeeping. Keyboard\n// nav skips headers, so the highlight index addresses this list directly.\nconst visibleTags = computed(() =>\n rows.value.flatMap((row) => (row.kind === \"tag\" ? [row.tag] : [])),\n);\n\n// --- Lifecycle: focus search + reset state when modal opens ---\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n collapsedGroups.value = new Set();\n // Search input is mounted as part of the same render cycle the\n // modal opens; nextTick guarantees the ref is bound before focus.\n nextTick(() => {\n searchInputRef.value?.focus();\n });\n }\n },\n);\n\n// Keep the highlight inside the visible-tags bounds whenever the\n// filtered list shrinks (e.g. user typed a more specific query).\nwatch(visibleTags, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\n// --- Actions ---\nfunction selectTag(tag: MergeTag): void {\n picker.resolve(tag);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleTags.value.length === 0) return;\n const next = highlightedIndex.value + delta;\n // Stop at edges (don't wrap) — feels more predictable in a long list.\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleTags.value.length - 1, next),\n );\n scrollHighlightIntoView();\n}\n\nconst listRef = ref<HTMLElement | null>(null);\n// One ref per group header so the pill row can `scrollIntoView` precisely.\n// Bound by callback-ref in the template; cleared (set to null) when Vue\n// unmounts the header during re-render.\nconst headerRefs = ref<Record<string, HTMLElement | null>>({});\n// Vue's callback-ref type widens to Element | ComponentPublicInstance | null\n// because the same ref shape works for both DOM elements and components.\n// Headers are always native buttons, so coerce to HTMLElement once here.\nfunction setHeaderRef(\n group: string,\n el: Element | ComponentPublicInstance | null,\n): void {\n headerRefs.value[group] = (el as HTMLElement | null) ?? null;\n}\n\nfunction scrollHighlightIntoView(): void {\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n const el = container.querySelector<HTMLElement>(\n `[data-merge-tag-index=\"${highlightedIndex.value}\"]`,\n );\n el?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction jumpToGroup(group: string): void {\n // Expand the group if it was collapsed — otherwise scrolling reveals\n // only the header, which is confusing for a \"jump to\" affordance.\n if (collapsedGroups.value.has(group)) {\n const next = new Set(collapsedGroups.value);\n next.delete(group);\n collapsedGroups.value = next;\n }\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n // Sum the height of every child rendered BEFORE the target header.\n // Avoids two browser quirks:\n // - `scrollIntoView({ block: \"start\" })` short-circuits when the\n // element is currently sticky-pinned at top.\n // - `offsetTop` historically misreported on sticky elements in some\n // Chromium versions; offsetHeight on each in-flow child is stable.\n // Each child is a top-level DOM row (header button or tag button),\n // and there are at most a few dozen, so the linear scan is cheap.\n let targetScrollTop = 0;\n let found = false;\n for (const child of Array.from(container.children) as HTMLElement[]) {\n if (\n child.dataset.testid === \"merge-tag-picker-group-header\" &&\n child.dataset.groupName === group\n ) {\n found = true;\n break;\n }\n targetScrollTop += child.offsetHeight;\n }\n if (!found) return;\n container.scrollTo({ top: targetScrollTop, behavior: \"smooth\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const tag = visibleTags.value[highlightedIndex.value];\n if (tag) selectTag(tag);\n }\n // Esc is forwarded by TplModal via its own keydown→close path.\n}\n\nfunction activeOptionId(): string | undefined {\n if (visibleTags.value.length === 0) return undefined;\n return `${listId}-opt-${highlightedIndex.value}`;\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\n\n// --- Empty states ---\n// Distinguish two cases so the messaging is honest:\n// - the consumer configured zero tags (defensive — button shouldn't even\n// render in that case but a programmatic open() should still degrade)\n// - the active search matched nothing\n//\n// `filteredTags` (NOT `visibleTags`) is the correct signal for \"no\n// results\" — visibleTags also excludes tags inside collapsed groups, so\n// collapsing every group would otherwise falsely trigger the empty\n// state and hide the headers the user wants to see.\nconst isEmptyConfig = computed(() => picker.tags.value.length === 0);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && filteredTags.value.length === 0,\n);\n\n// --- Pill row visibility ---\n// Pills jump-link to group headers; they only make sense when there are\n// at least 2 groups AND the user isn't currently searching (search\n// collapses the grouped view to a flat list, so the pills' targets\n// disappear).\nconst showPillRow = computed(\n () => hasGroups.value && groupOrder.value.length > 1 && !searchActive.value,\n);\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"merge-tag-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <!-- Header -->\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.mergeTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.mergeTag.picker.close\"\n data-testid=\"merge-tag-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <!-- Search -->\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.mergeTag.picker.searchPlaceholder\"\n :aria-label=\"t.mergeTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"merge-tag-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <!-- Group pills — visible only when grouping is active and the\n user isn't searching. Click a pill to jump (and expand) the\n matching group. Horizontally scrollable so a long group list\n degrades gracefully. -->\n <div\n v-if=\"showPillRow\"\n class=\"tpl:flex tpl:gap-1.5 tpl:overflow-x-auto tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2\"\n data-testid=\"merge-tag-picker-group-pills\"\n >\n <button\n v-for=\"group in groupOrder\"\n :key=\"group\"\n type=\"button\"\n class=\"tpl:flex tpl:flex-shrink-0 tpl:cursor-pointer tpl:items-center tpl:rounded-full tpl:border tpl:border-[var(--tpl-border)] tpl:bg-transparent tpl:px-2.5 tpl:py-0.5 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n data-testid=\"merge-tag-picker-group-pill\"\n :data-group-name=\"group\"\n @click=\"jumpToGroup(group)\"\n >\n {{ group }}\n </button>\n </div>\n\n <!-- Body / list -->\n <!-- `position: relative` makes this the offsetParent for the sticky\n group headers below, so `header.offsetTop` reliably reports each\n header's natural in-flow position. jumpToGroup() depends on this\n — see the comment in jumpToGroup() for why scrollIntoView isn't\n sufficient. -->\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.mergeTag.picker.title\"\n data-testid=\"merge-tag-picker-list\"\n >\n <template v-if=\"isEmptyConfig\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.empty }}\n </div>\n </template>\n <template v-else-if=\"isNoResults\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.noResults }}\n </div>\n </template>\n <template v-else>\n <template v-for=\"(row, rowIndex) in rows\" :key=\"rowIndex\">\n <button\n v-if=\"row.kind === 'header'\"\n :ref=\"(el) => setHeaderRef(row.group, el)\"\n type=\"button\"\n :aria-expanded=\"!isCollapsed(row.group)\"\n :aria-controls=\"listId\"\n class=\"tpl:sticky tpl:top-0 tpl:z-10 tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-[var(--tpl-bg-elevated)] tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-left tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)]\"\n data-testid=\"merge-tag-picker-group-header\"\n :data-group-name=\"row.group\"\n :data-group-collapsed=\"isCollapsed(row.group) ? 'true' : 'false'\"\n @click=\"toggleGroup(row.group)\"\n >\n <ChevronDown\n :size=\"12\"\n :stroke-width=\"2\"\n class=\"tpl:transition-transform\"\n :class=\"isCollapsed(row.group) ? 'tpl:-rotate-90' : ''\"\n />\n <span>{{ row.group }}</span>\n <span class=\"tpl:ml-1 tpl:font-normal\">\n ({{\n format(t.mergeTag.picker.groupCount, { count: row.count })\n }})\n </span>\n </button>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-merge-tag-index=\"row.index\"\n :data-merge-tag-value=\"row.tag.value\"\n :title=\"\n row.tag.description\n ? `${row.tag.value} — ${row.tag.description}`\n : row.tag.value\n \"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"merge-tag-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectTag(row.tag)\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.tag.label\n }}</span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.value }}\n </span>\n <span\n v-if=\"row.tag.description\"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.description }}\n </span>\n <!-- What the previews will render in Sample mode, so an author\n knows before inserting. Shown only when configured. -->\n <span\n v-if=\"row.tag.sample !== undefined\"\n data-testid=\"merge-tag-picker-sample\"\n class=\"tpl:line-clamp-1 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.mergeTag.picker.sample, { sample: row.tag.sample })\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n type SyntaxPreset,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\n// Small outlined keyword chip (IF, ENDIF, FOR…) shown next to a tag in the\n// merge tag picker and the typing-autocomplete list. It marks a configured\n// tag whose `value` is a logic tag rather than a data tag, so authors can\n// tell at a glance which entries insert control flow. The visual mirrors the\n// on-canvas logic badge (outlined, primary color) for consistency. Renders\n// nothing when the value isn't logic-shaped, so callers can drop it into any\n// row unconditionally.\nconst props = defineProps<{ value: string; syntax: SyntaxPreset }>();\n\nconst isLogic = computed(() => isLogicMergeTagValue(props.value, props.syntax));\nconst keyword = computed(() =>\n getLogicMergeTagKeyword(props.value, props.syntax),\n);\n</script>\n\n<template>\n <span\n v-if=\"isLogic\"\n class=\"tpl:inline-flex tpl:flex-shrink-0 tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:leading-none tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n style=\"\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n data-testid=\"merge-tag-logic-badge\"\n :data-logic-keyword=\"keyword\"\n >\n {{ keyword }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n type SyntaxPreset,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\n// Small outlined keyword chip (IF, ENDIF, FOR…) shown next to a tag in the\n// merge tag picker and the typing-autocomplete list. It marks a configured\n// tag whose `value` is a logic tag rather than a data tag, so authors can\n// tell at a glance which entries insert control flow. The visual mirrors the\n// on-canvas logic badge (outlined, primary color) for consistency. Renders\n// nothing when the value isn't logic-shaped, so callers can drop it into any\n// row unconditionally.\nconst props = defineProps<{ value: string; syntax: SyntaxPreset }>();\n\nconst isLogic = computed(() => isLogicMergeTagValue(props.value, props.syntax));\nconst keyword = computed(() =>\n getLogicMergeTagKeyword(props.value, props.syntax),\n);\n</script>\n\n<template>\n <span\n v-if=\"isLogic\"\n class=\"tpl:inline-flex tpl:flex-shrink-0 tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:leading-none tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n style=\"\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n data-testid=\"merge-tag-logic-badge\"\n :data-logic-keyword=\"keyword\"\n >\n {{ keyword }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject, nextTick, ref, watch } from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { Search, X } from \"@lucide/vue\";\nimport type { LogicPair, LogicTag, SyntaxPreset } from \"@templatical/types\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport LogicTagBadge from \"./LogicTagBadge.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n LOGIC_TAG_PICKER_KEY,\n MERGE_TAG_SYNTAX_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\nimport type { LogicTagPickerResult } from \"../composables/useLogicTagPicker\";\n\n// Picker singleton — provided by useEditorCore. Required explicitly so the\n// modal only functions inside an editor context.\nconst picker = requireInject(LOGIC_TAG_PICKER_KEY, \"LogicTagPickerModal\");\nconst { t } = useI18n();\nconst syntax = inject<SyntaxPreset>(\n MERGE_TAG_SYNTAX_KEY,\n SYNTAX_PRESETS.liquid,\n);\n\n// Theming — same pattern as MergeTagPickerModal: re-establish the theme\n// locally so the `.tpl` token root resolves dark mode + consumer overrides.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\nconst rawSearch = ref(\"\");\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-logic-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\nconst highlightedIndex = ref(0);\n\nfunction matches(haystackParts: (string | undefined)[]): boolean {\n const query = search.value.trim().toLowerCase();\n if (!query) return true;\n return haystackParts.filter(Boolean).join(\" \").toLowerCase().includes(query);\n}\n\nconst filteredTags = computed<LogicTag[]>(() =>\n picker.tags.value.filter((tag) =>\n matches([tag.label, tag.value, tag.description]),\n ),\n);\nconst filteredPairs = computed<LogicPair[]>(() =>\n picker.pairs.value.filter((pair) =>\n matches([pair.label, pair.before, pair.after, pair.description]),\n ),\n);\n\n// Tags and pairs share one grouped list: a `group` section holds both its\n// standalone tags and its open/close pairs, so a group used by both (e.g.\n// \"Conditions\") renders once instead of appearing separately for each kind.\n// The badge shape — one pill for a tag, two for a pair — signals\n// insert-vs-wrap per row.\ntype Entry =\n | { kind: \"tag\"; tag: LogicTag; group?: string }\n | { kind: \"pair\"; pair: LogicPair; group?: string };\n\n// Pairs first, then standalone tags, so within a shared group the wrapping\n// constructs (e.g. If VIP) sort ahead of single tokens (e.g. Else). Group\n// order follows first appearance across this combined sequence.\nconst filteredEntries = computed<Entry[]>(() => [\n ...filteredPairs.value.map((pair): Entry => ({\n kind: \"pair\",\n pair,\n group: pair.group,\n })),\n ...filteredTags.value.map((tag): Entry => ({\n kind: \"tag\",\n tag,\n group: tag.group,\n })),\n]);\n\nconst hasGroups = computed(\n () =>\n picker.tags.value.some((tag) => Boolean(tag.group)) ||\n picker.pairs.value.some((pair) => Boolean(pair.group)),\n);\n\n// Rows drive rendering; `items` is the parallel flat list of selectable\n// entries in render order, so `highlightedIndex` (keyboard nav) maps 1:1.\ntype Row =\n | { kind: \"header\"; label: string }\n | { kind: \"tag\"; tag: LogicTag; index: number }\n | { kind: \"pair\"; pair: LogicPair; index: number };\n\nfunction bucket(entries: Entry[]): [string, Entry[]][] {\n const otherLabel = t.logicTag.picker.otherGroup;\n const map = new Map<string, Entry[]>();\n const order: string[] = [];\n for (const entry of entries) {\n const key = entry.group ?? otherLabel;\n if (!map.has(key)) {\n map.set(key, []);\n order.push(key);\n }\n map.get(key)!.push(entry);\n }\n return order.map((key) => [key, map.get(key)!]);\n}\n\nconst built = computed(() => {\n const rows: Row[] = [];\n const items: LogicTagPickerResult[] = [];\n let idx = 0;\n\n const pushEntry = (entry: Entry): void => {\n if (entry.kind === \"tag\") {\n rows.push({ kind: \"tag\", tag: entry.tag, index: idx });\n items.push(entry.tag);\n } else {\n rows.push({ kind: \"pair\", pair: entry.pair, index: idx });\n items.push(entry.pair);\n }\n idx++;\n };\n\n const entries = filteredEntries.value;\n if (searchActive.value || !hasGroups.value) {\n // Flat list: search flattens groups (matches the merge-tag picker), and\n // an ungrouped config has no headers to render.\n for (const entry of entries) pushEntry(entry);\n } else {\n for (const [group, groupEntries] of bucket(entries)) {\n rows.push({ kind: \"header\", label: group });\n for (const entry of groupEntries) pushEntry(entry);\n }\n }\n\n return { rows, items };\n});\n\nconst visibleItems = computed(() => built.value.items);\n\nconst isEmptyConfig = computed(\n () => picker.tags.value.length === 0 && picker.pairs.value.length === 0,\n);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && visibleItems.value.length === 0,\n);\n\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n nextTick(() => searchInputRef.value?.focus());\n }\n },\n);\n\nwatch(visibleItems, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\nconst listRef = ref<HTMLElement | null>(null);\n\nfunction selectItem(item: LogicTagPickerResult): void {\n picker.resolve(item);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleItems.value.length === 0) return;\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleItems.value.length - 1, highlightedIndex.value + delta),\n );\n nextTick(() => {\n listRef.value\n ?.querySelector<HTMLElement>(\n `[data-logic-index=\"${highlightedIndex.value}\"]`,\n )\n ?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const item = visibleItems.value[highlightedIndex.value];\n if (item) selectItem(item);\n }\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\nfunction activeOptionId(): string | undefined {\n return visibleItems.value.length === 0\n ? undefined\n : `${listId}-opt-${highlightedIndex.value}`;\n}\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"logic-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.logicTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.logicTag.picker.close\"\n data-testid=\"logic-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.logicTag.picker.searchPlaceholder\"\n :aria-label=\"t.logicTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"logic-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.logicTag.picker.title\"\n data-testid=\"logic-picker-list\"\n >\n <div\n v-if=\"isEmptyConfig\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.empty }}\n </div>\n <div\n v-else-if=\"isNoResults\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.noResults }}\n </div>\n <template v-else>\n <template v-for=\"(row, rowIndex) in built.rows\" :key=\"rowIndex\">\n <div\n v-if=\"row.kind === 'header'\"\n class=\"tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-header\"\n :data-group-name=\"row.label\"\n >\n {{ row.label }}\n </div>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-logic-index=\"row.index\"\n :data-logic-kind=\"row.kind\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"logic-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectItem(row.kind === 'tag' ? row.tag : row.pair)\"\n >\n <span\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.kind === \"tag\" ? row.tag.label : row.pair.label\n }}</span>\n <span\n v-if=\"row.kind === 'tag'\"\n class=\"tpl:flex tpl:items-center\"\n >\n <LogicTagBadge :value=\"row.tag.value\" :syntax=\"syntax\" />\n </span>\n <span v-else class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <LogicTagBadge :value=\"row.pair.before\" :syntax=\"syntax\" />\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">…</span>\n <LogicTagBadge :value=\"row.pair.after\" :syntax=\"syntax\" />\n </span>\n </span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.value\n : `${row.pair.before} … ${row.pair.after}`\n }}\n </span>\n <span\n v-if=\"\n (row.kind === 'tag'\n ? row.tag.description\n : row.pair.description) as string | undefined\n \"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.description\n : row.pair.description\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject, nextTick, ref, watch } from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { Search, X } from \"@lucide/vue\";\nimport type { LogicPair, LogicTag, SyntaxPreset } from \"@templatical/types\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport LogicTagBadge from \"./LogicTagBadge.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n LOGIC_TAG_PICKER_KEY,\n MERGE_TAG_SYNTAX_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\nimport type { LogicTagPickerResult } from \"../composables/useLogicTagPicker\";\n\n// Picker singleton — provided by useEditorCore. Required explicitly so the\n// modal only functions inside an editor context.\nconst picker = requireInject(LOGIC_TAG_PICKER_KEY, \"LogicTagPickerModal\");\nconst { t } = useI18n();\nconst syntax = inject<SyntaxPreset>(\n MERGE_TAG_SYNTAX_KEY,\n SYNTAX_PRESETS.liquid,\n);\n\n// Theming — same pattern as MergeTagPickerModal: re-establish the theme\n// locally so the `.tpl` token root resolves dark mode + consumer overrides.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\nconst rawSearch = ref(\"\");\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-logic-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\nconst highlightedIndex = ref(0);\n\nfunction matches(haystackParts: (string | undefined)[]): boolean {\n const query = search.value.trim().toLowerCase();\n if (!query) return true;\n return haystackParts.filter(Boolean).join(\" \").toLowerCase().includes(query);\n}\n\nconst filteredTags = computed<LogicTag[]>(() =>\n picker.tags.value.filter((tag) =>\n matches([tag.label, tag.value, tag.description]),\n ),\n);\nconst filteredPairs = computed<LogicPair[]>(() =>\n picker.pairs.value.filter((pair) =>\n matches([pair.label, pair.before, pair.after, pair.description]),\n ),\n);\n\n// Tags and pairs share one grouped list: a `group` section holds both its\n// standalone tags and its open/close pairs, so a group used by both (e.g.\n// \"Conditions\") renders once instead of appearing separately for each kind.\n// The badge shape — one pill for a tag, two for a pair — signals\n// insert-vs-wrap per row.\ntype Entry =\n | { kind: \"tag\"; tag: LogicTag; group?: string }\n | { kind: \"pair\"; pair: LogicPair; group?: string };\n\n// Pairs first, then standalone tags, so within a shared group the wrapping\n// constructs (e.g. If VIP) sort ahead of single tokens (e.g. Else). Group\n// order follows first appearance across this combined sequence.\nconst filteredEntries = computed<Entry[]>(() => [\n ...filteredPairs.value.map((pair): Entry => ({\n kind: \"pair\",\n pair,\n group: pair.group,\n })),\n ...filteredTags.value.map((tag): Entry => ({\n kind: \"tag\",\n tag,\n group: tag.group,\n })),\n]);\n\nconst hasGroups = computed(\n () =>\n picker.tags.value.some((tag) => Boolean(tag.group)) ||\n picker.pairs.value.some((pair) => Boolean(pair.group)),\n);\n\n// Rows drive rendering; `items` is the parallel flat list of selectable\n// entries in render order, so `highlightedIndex` (keyboard nav) maps 1:1.\ntype Row =\n | { kind: \"header\"; label: string }\n | { kind: \"tag\"; tag: LogicTag; index: number }\n | { kind: \"pair\"; pair: LogicPair; index: number };\n\nfunction bucket(entries: Entry[]): [string, Entry[]][] {\n const otherLabel = t.logicTag.picker.otherGroup;\n const map = new Map<string, Entry[]>();\n const order: string[] = [];\n for (const entry of entries) {\n const key = entry.group ?? otherLabel;\n if (!map.has(key)) {\n map.set(key, []);\n order.push(key);\n }\n map.get(key)!.push(entry);\n }\n return order.map((key) => [key, map.get(key)!]);\n}\n\nconst built = computed(() => {\n const rows: Row[] = [];\n const items: LogicTagPickerResult[] = [];\n let idx = 0;\n\n const pushEntry = (entry: Entry): void => {\n if (entry.kind === \"tag\") {\n rows.push({ kind: \"tag\", tag: entry.tag, index: idx });\n items.push(entry.tag);\n } else {\n rows.push({ kind: \"pair\", pair: entry.pair, index: idx });\n items.push(entry.pair);\n }\n idx++;\n };\n\n const entries = filteredEntries.value;\n if (searchActive.value || !hasGroups.value) {\n // Flat list: search flattens groups (matches the merge-tag picker), and\n // an ungrouped config has no headers to render.\n for (const entry of entries) pushEntry(entry);\n } else {\n for (const [group, groupEntries] of bucket(entries)) {\n rows.push({ kind: \"header\", label: group });\n for (const entry of groupEntries) pushEntry(entry);\n }\n }\n\n return { rows, items };\n});\n\nconst visibleItems = computed(() => built.value.items);\n\nconst isEmptyConfig = computed(\n () => picker.tags.value.length === 0 && picker.pairs.value.length === 0,\n);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && visibleItems.value.length === 0,\n);\n\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n nextTick(() => searchInputRef.value?.focus());\n }\n },\n);\n\nwatch(visibleItems, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\nconst listRef = ref<HTMLElement | null>(null);\n\nfunction selectItem(item: LogicTagPickerResult): void {\n picker.resolve(item);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleItems.value.length === 0) return;\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleItems.value.length - 1, highlightedIndex.value + delta),\n );\n nextTick(() => {\n listRef.value\n ?.querySelector<HTMLElement>(\n `[data-logic-index=\"${highlightedIndex.value}\"]`,\n )\n ?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const item = visibleItems.value[highlightedIndex.value];\n if (item) selectItem(item);\n }\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\nfunction activeOptionId(): string | undefined {\n return visibleItems.value.length === 0\n ? undefined\n : `${listId}-opt-${highlightedIndex.value}`;\n}\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"logic-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.logicTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.logicTag.picker.close\"\n data-testid=\"logic-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.logicTag.picker.searchPlaceholder\"\n :aria-label=\"t.logicTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"logic-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.logicTag.picker.title\"\n data-testid=\"logic-picker-list\"\n >\n <div\n v-if=\"isEmptyConfig\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.empty }}\n </div>\n <div\n v-else-if=\"isNoResults\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.noResults }}\n </div>\n <template v-else>\n <template v-for=\"(row, rowIndex) in built.rows\" :key=\"rowIndex\">\n <div\n v-if=\"row.kind === 'header'\"\n class=\"tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-header\"\n :data-group-name=\"row.label\"\n >\n {{ row.label }}\n </div>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-logic-index=\"row.index\"\n :data-logic-kind=\"row.kind\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"logic-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectItem(row.kind === 'tag' ? row.tag : row.pair)\"\n >\n <span\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.kind === \"tag\" ? row.tag.label : row.pair.label\n }}</span>\n <span\n v-if=\"row.kind === 'tag'\"\n class=\"tpl:flex tpl:items-center\"\n >\n <LogicTagBadge :value=\"row.tag.value\" :syntax=\"syntax\" />\n </span>\n <span v-else class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <LogicTagBadge :value=\"row.pair.before\" :syntax=\"syntax\" />\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">…</span>\n <LogicTagBadge :value=\"row.pair.after\" :syntax=\"syntax\" />\n </span>\n </span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.value\n : `${row.pair.before} … ${row.pair.after}`\n }}\n </span>\n <span\n v-if=\"\n (row.kind === 'tag'\n ? row.tag.description\n : row.pair.description) as string | undefined\n \"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.description\n : row.pair.description\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onMounted,\n onUnmounted,\n ref,\n watch,\n} from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\n// Type-only, so no cloud module is statically reachable from the OSS entry —\n// see `cloud/runtime.ts` for why the seam is shaped this way.\nimport type { CloudRuntime } from \"./cloud/runtime\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useCommentsFeature } from \"./composables/useCommentsFeature\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTemplatesFeature } from \"./composables/useTemplatesFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useVersionHistoryFeature } from \"./composables/useVersionHistoryFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveAutoSave } from \"./types/auto-save\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { logger } from \"./utils/logger\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport { resolveRenderFonts } from \"./utils/renderProvider\";\nimport {\n withNormalizedContentWrites,\n withNormalizedTemplateLoads,\n} from \"./utils/normalizeMergeTagMarkup\";\nimport type { Translations } from \"./i18n\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw } from \"@lucide/vue\";\nimport { warningBtnCompactClass } from \"./constants/styleConstants\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport EditorHeader from \"./components/EditorHeader.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n /**\n * Cloud's adapter wiring, supplied only by `initCloud()`.\n *\n * There is **one** editor component. Cloud is a set of providers on\n * `config` plus this runtime, which exists purely for the handful of\n * composables that must run at a specific point inside this `setup()` —\n * collaboration before the history interceptor, the lint save-gate after\n * `useEditorCore`. Everything else it contributes arrives through the\n * ordinary config keys an OSS consumer would fill in themselves.\n */\n cloud?: CloudRuntime;\n}>();\n\n// The fourth place consumer content enters (the other three are the entry\n// points in `index.ts`: the mount seed, `setContent` and `create`). A template\n// fetched from the store never passes through the public API — core assigns it\n// to state itself — so the provider is wrapped here, on its way in. Wrapping\n// rather than normalizing after the load resolves is what keeps the load from\n// registering as an edit: core is handed content that is already correct.\nconst templatesProvider = props.config.templates\n ? withNormalizedTemplateLoads(props.config.templates, props.config.mergeTags)\n : undefined;\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n // Seeds a *new* template's body font when no `content` was supplied. Only the\n // deleted Cloud core passed this, so `init({ fonts: { defaultFont } })` never\n // reached a blank template.\n defaultFontFamily: props.config.fonts?.defaultFont,\n templateDefaults: props.config.templateDefaults,\n templates: templatesProvider,\n onError: props.config.onError,\n // Cloud's collaborators lock the blocks they are editing. The map is\n // forward-declared by the runtime because `useCollaboration` — which fills\n // it — can only be built after this call.\n lockedBlocks: props.cloud?.lockedBlocks,\n});\n\n// Collaboration wraps the editor's mutators so they broadcast; `useEditorCore`'s\n// history interceptor wraps them again below. Reversing that order would push\n// local history entries for remote operations and drift the peers apart, which\n// is the whole reason this runs here rather than alongside the rest.\nconst cloudAttachment = props.cloud ? props.cloud.attach({ editor }) : null;\n\n// --- Templates (opt-in: only when a storage provider is configured) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header's name field, status indicator and save button — and what\n// makes Cmd+S mean \"persist now\".\nconst templates = templatesProvider\n ? useTemplatesFeature({\n provider: templatesProvider,\n editor,\n guardUnsavedChanges: props.config.templates?.unsavedChangesGuard,\n // Cloud's lint save-gate, when there is one. Read through a getter\n // because the gate needs `core.templateLint`, which does not exist yet.\n // Without this the collapse would have silently dropped the server's\n // `accessibility.blockOnError` policy.\n getSaveGate: props.cloud ? () => props.cloud!.getSaveGate() : undefined,\n })\n : null;\n\n// Reported to the consumer regardless of whether a provider is configured: a\n// `beforeunload` guard cannot cover SPA route changes, so an embedder needs this\n// to guard their own router — including when they persist via `onChange`.\nif (props.config.onDirtyChange) {\n watch(\n () => editor.state.isDirty,\n (isDirty) => props.config.onDirtyChange!(isDirty),\n );\n}\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n // Cloud's store is plan-gated; a consumer's own store never is.\n isAvailable: props.cloud\n ? () => props.cloud!.isSavedBlocksAvailable()\n : undefined,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n // Cloud folds in the plan feature and \"the template must be saved\", both\n // constraints of *its* sending path rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isTestEmailAvailable()\n : undefined,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Comments (opt-in: only with a storage provider *and* a `user`) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header trigger and the per-block comment indicators.\n//\n// No `user` means no provider call at all — an unattributable comment is worse\n// than no comment feature, so `isAvailable` stays false and nothing renders.\nconst comments = props.config.comments\n ? useCommentsFeature({\n provider: props.config.comments,\n editor,\n user: props.config.user,\n // Cloud keeps comments mutually exclusive with its AI and scoring sidebars:\n // they share one 360px gutter, and two of them open at once would overlap.\n isOpen: cloudAttachment?.panelState.commentsOpen,\n onError: props.config.onError,\n // Cloud folds in the `commenting` plan feature and \"the template must be\n // saved\", both constraints of *its* store rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isCommentsAvailable()\n : undefined,\n // Cloud anchors a comment to a block in the saved template; a consumer's\n // store accepts whatever anchor it is given.\n isBlockSaved: props.cloud\n ? (blockId: string) => props.cloud!.isBlockSaved(blockId)\n : undefined,\n })\n : null;\n\nconst CommentsPanel = defineAsyncComponent(\n () => import(\"./components/CommentsPanel.vue\"),\n);\n\n// --- Cloud chrome (opt-in: only under `initCloud()`) ---\n// One lazy wrapper for every panel, overlay and modal Cloud adds, plus one for\n// its header controls. Same discipline as `SavedBlocksPanels` / `TestEmailPanel`:\n// an OSS consumer never downloads any of it.\nconst CloudPanels = defineAsyncComponent(\n () => import(\"./cloud/components/CloudPanels.vue\"),\n);\nconst CloudHeaderExtras = defineAsyncComponent(\n () => import(\"./cloud/components/CloudHeaderExtras.vue\"),\n);\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// The header control lives in `EditorHeader`; this is the preview banner, which\n// renders outside the header. Both are lazy.\nconst VersionHistoryPanels = defineAsyncComponent(\n () => import(\"./components/VersionHistoryPanels.vue\"),\n);\n\n// --- Auto-save ---\n// One debounced tick drives everything a consumer can ask for on a content\n// change: the `onChange` notification, and, when `templates.autoSave` is on,\n// the save itself. Sharing a single `useAutoSave` instance keeps the two in\n// step and inherits the pause-during-undo/redo behaviour `useEditorCore` wires\n// up around it. `config.changeDebounce` sets the cadence for both.\nconst autoSaveEnabled = resolveAutoSave(\n props.config.templates?.autoSave,\n false,\n);\nconst isAutoSaving = autoSaveEnabled && templates !== null;\n// The one way autosave can still be inert once a provider exists is its\n// `save` being `false` — a read-only store — which `requestAutoSave()` below\n// already no-ops per tick for; this warning is the only signal the consumer\n// gets. `templates !== null` is redundant with `autoSaveEnabled` at runtime\n// (the provider that enables autosave is the same provider that makes\n// `templates` non-null) but stays as an explicit conjunct so TypeScript can\n// narrow `templates.canSave.value` below without a non-null assertion.\nif (autoSaveEnabled && templates !== null && !templates.canSave.value) {\n logger.warn(\n \"config.templates.autoSave is on but this provider's save is false — \" +\n \"there is nothing to persist to. Give it a real save function, or \" +\n \"use `onChange` to persist the content yourself.\",\n );\n}\n// `changeDebounce` alone does not enable autosave — only `templates.autoSave`\n// does. A config with `changeDebounce` set, a provider present, `autoSave`\n// left unset, and no `onChange` has nothing consuming the timer at all,\n// which reads as intentional `onChange`-only pacing but is easy to reach by\n// mistake. Warn on exactly that combination; `autoSave: false` is a stated\n// decision and must stay silent.\nif (\n props.config.changeDebounce !== undefined &&\n templates !== null &&\n props.config.templates?.autoSave === undefined &&\n !props.config.onChange\n) {\n logger.warn(\n \"config.changeDebounce is set but nothing will use it — \" +\n \"config.templates.autoSave is unset and there is no onChange either, \" +\n \"so the timer has no consumer. Set config.templates.autoSave, or add \" +\n \"onChange, so the debounce takes effect.\",\n );\n}\n\nconst autoSaveOptions =\n props.config.onChange || isAutoSaving\n ? {\n onChange: () => {\n props.config.onChange?.(\n JSON.parse(JSON.stringify(editor.state.content)),\n );\n // `requestAutoSave()` already no-ops without a saveable template, so a\n // read-only provider costs one predicate per tick and nothing else.\n // It is the *ungated* request only in the sense that it never raises a\n // prompt: Cloud's lint gate still refuses it, which is what keeps\n // `accessibility.blockOnError` a policy rather than a manual-save\n // speed bump.\n if (isAutoSaving) templates!.requestAutoSave();\n },\n ...(props.config.changeDebounce !== undefined\n ? { debounce: props.config.changeDebounce }\n : {}),\n }\n : null;\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\n// A named object rather than an inline literal: `useVersionHistoryFeature` can\n// only be built after core (it needs history/conditionPreview/autoSave), so its\n// capability is assigned in below, into the very object core provided.\nconst capabilities: EditorCapabilities = {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(templates ? { templates: templates.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n ...(comments ? { comments: comments.capability } : {}),\n};\n\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n // Cloud swaps in its own media browser. Not plan-gated: an entitlement here\n // would fire when a consumer is *not* using Cloud storage, i.e. backwards.\n onRequestMedia: props.cloud?.onRequestMedia ?? props.config.onRequestMedia,\n resolvePreview: props.config.resolvePreview,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions,\n capabilities,\n editorRoot: props.shadowRoot,\n historyOptions: props.cloud\n ? { isRemoteOperation: () => props.cloud!.isRemoteOperation() }\n : undefined,\n keyboardOptions: props.cloud\n ? { onBeforeUndo: () => props.cloud!.onBeforeUndo() }\n : undefined,\n});\n\n// Cloud's setup-time wiring that needs `core`: the lint save-gate (reads\n// `core.templateLint`), the collab undo warning (reads `core.history.canUndo`),\n// and the capability entries Cloud contributes — mutated into the very object\n// `useEditorCore` provided, so injecting components see them on first render.\nconst cloudReady =\n props.cloud && cloudAttachment\n ? props.cloud.ready({ core, capabilities })\n : null;\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// Constructed *after* `useEditorCore` because it drives `history` /\n// `conditionPreview` / `autoSave`, which core owns. Its capability is therefore\n// assigned into the object above rather than passed in — same object reference\n// core provided, mutated during setup, so injecting components see it on their\n// first render.\nconst versionHistory = props.config.versionHistory\n ? useVersionHistoryFeature({\n provider: props.config.versionHistory,\n // The fifth content-in path. A stored version holds whatever was written\n // to it, so it can carry bare tokens like any other loaded content, and\n // previewing one would put them on a canvas where every other tag is a\n // chip. Wrapped at `setContent` rather than at the provider, because\n // version content also arrives off the hydrated list and out of the\n // `fetched` cache without touching `get()`.\n editor: withNormalizedContentWrites(editor, props.config.mergeTags),\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n // Lets the restore confirmation offer to persist unsaved work instead of\n // only warning about it. Without a templates provider — or with one whose\n // `save` is `false` — there is nowhere to put that work, and the\n // confirmation says so rather than offering an action that can't happen.\n // Cloud additionally refuses while the lint gate would block: stacking a\n // second modal on the confirmation would be worse than saying there is\n // currently nowhere to put the work.\n saveBeforeRestore: templates\n ? {\n canSave: () =>\n templates.canSave.value &&\n !(props.cloud?.getSaveGate()?.shouldBlock.value ?? false),\n save: () => templates.save(\"restore\"),\n }\n : null,\n onError: props.config.onError,\n })\n : null;\n\nif (versionHistory) capabilities.versionHistory = versionHistory.capability;\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n getFonts: () => resolveRenderFonts(props.fontsManager),\n });\n}\n\n/**\n * Left/right insets for the canvas body and the footer, which must always agree.\n *\n * Cloud's sidebars widen the right gutter when one is open; without a cloud\n * runtime `rightPanelOpen` is simply never true.\n */\n/**\n * Whether a 360px feature panel occupies the right-hand gutter.\n *\n * The comments panel is the one an OSS session can open, so it counts on its own.\n * In Cloud it also drives `rightPanelOpen` (they share the panel state, which is\n * what keeps comments mutually exclusive with the AI and scoring sidebars), so the\n * `||` is redundant there and load-bearing here.\n *\n * Two things read this, and **both must**: the canvas/footer insets below, and\n * `RightSidebar`'s `shifted-left` — the properties panel sits at `right-0` too, so\n * a panel opening over it would swallow every click meant for the panel.\n */\nconst rightPanelOpen = computed(\n () =>\n comments?.isOpen.value === true ||\n cloudAttachment?.panelState.rightPanelOpen.value === true,\n);\n\nconst bodyInsetClass = computed(() => {\n if (editor.state.previewMode) return \"tpl:left-0 tpl:right-0\";\n return rightPanelOpen.value\n ? \"tpl:left-12 tpl:right-[680px]\"\n : \"tpl:left-12 tpl:right-[320px]\";\n});\n\n/** Canvas affordances that open a Cloud panel. No-ops without a runtime. */\nfunction openCloudPanel(panel: \"ai-chat\" | \"design-reference\"): void {\n if (!cloudAttachment) return;\n if (panel === \"ai-chat\") cloudAttachment.panelState.aiChatOpen.value = true;\n else cloudAttachment.panelState.designReferenceOpen.value = true;\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n props.cloud?.destroy();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\n// The lifecycle trio comes from the templates feature when one exists, so a\n// programmatic save also drives the header's status. Without a provider it comes\n// straight from core, whose own methods reject with the actionable error.\nconst templateLifecycle = templates ?? editor;\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n isDirty: () => editor.state.isDirty,\n create: templateLifecycle.create,\n load: templateLifecycle.load,\n save: templateLifecycle.save,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Chrome wrapper — the clip belongs here, never on the root and never\n around `.tpl-popover-root`. Safari paints a `position: fixed`\n descendant clipped to an ancestor's `overflow: hidden` box while still\n resolving its layout against the viewport, so a dialog teleported\n under a clipping ancestor is cut off wherever it extends past the\n container, and its backdrop dims only the container's own area\n (#633). Keeping the clip on this element bounds the header, rails,\n canvas and footer without reaching the popover root, and this box is\n identical to the root's, so everything anchored `absolute` inside\n lands exactly where it would against the root. Locked by\n `tests/popover-root-clip-scope.test.ts`. -->\n <div class=\"tpl:absolute tpl:inset-0 tpl:overflow-hidden\">\n <!-- One header for both entry points. Cloud's own controls arrive through\n the three slots; everything else here is capability-gated, so the same\n markup covers \"no providers at all\" and a fully-wired Cloud session. -->\n <EditorHeader\n :editor=\"editor\"\n :core=\"core\"\n :templates=\"templates\"\n :show-template-name=\"config.templates?.nameField !== false\"\n :test-email=\"testEmail\"\n :version-history=\"versionHistory\"\n :comments=\"comments\"\n >\n <template v-if=\"cloudAttachment\" #left-extras>\n <CloudHeaderExtras part=\"left\" :cloud=\"cloudAttachment\" />\n </template>\n <template v-if=\"cloudAttachment\" #right-extras>\n <CloudHeaderExtras part=\"right\" :cloud=\"cloudAttachment\" />\n </template>\n </EditorHeader>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n bodyInsetClass,\n versionHistory?.isPreviewing.value ? 'tpl:top-[104px]' : 'tpl:top-14',\n ]\"\n >\n <!-- Preview chrome, floated over the canvas.\n\n A zero-height sticky layer holding one absolutely-positioned,\n canvas-centred **column**. Zero height means the layer contributes no\n space of its own, so nothing here can push the canvas around — which\n is what lets the merge-tag toggle live over the preview instead of in\n the header, whose centre track re-centres on every width change and\n so dragged the Preview button 114.5px sideways each time the toggle\n appeared (#574).\n\n A column rather than two fixed offsets. Both pills are reachable at\n once — the restore pill needs hidden blocks, the toggle needs preview\n mode without a `resolvePreview`, and nothing makes those exclusive —\n so they have to stack. Doing that with a hardcoded second offset put\n the restore pill 48px down even when it rendered alone, which in\n editing mode (where the toggle never shows) dropped it onto the first\n block's content. Flow solves what the offset got wrong: whichever\n pills render sit tight under the header, in order.\n\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div class=\"tpl-preview-overlay tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <div\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:flex tpl:-translate-x-1/2 tpl:flex-col tpl:items-center tpl:gap-2\"\n >\n <!-- Sample / Label. Preview mode only: merge tags are never\n substituted on the editing canvas, so the choice would have no\n effect there. A configured `resolvePreview` supersedes samples\n entirely.\n\n The wrapper carries only the shadow that lifts the control off\n the canvas — no `backdrop-filter`, because the toggle's own\n `--tpl-bg-hover` fill is opaque and covers this box exactly, so a\n blur would composite a layer to show nothing. -->\n <Transition name=\"tpl-preview-pill\">\n <div\n v-if=\"\n editor.state.previewMode &&\n !core.previewResolution.supersedesSamples.value\n \"\n data-testid=\"merge-tag-mode-toggle-anchor\"\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:shadow-[var(--tpl-shadow-md)]\"\n >\n <MergeTagModeToggle\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n </Transition>\n <!-- Show all hidden blocks. The shared warning recipe, not a bespoke\n string: it has to read as the Sample/Label switch's sibling,\n since the two stack here, and a hand-rolled pill is how this one\n came to sit 8px shorter with a different radius and a 1.85:1\n label. `warningBtnCompactClass` carries why the amber is on the\n border rather than the text. -->\n <Transition name=\"tpl-preview-pill\">\n <button\n v-if=\"\n core.conditionPreview.hasHiddenBlocks.value &&\n core.appliesConditionFilter.value\n \"\n type=\"button\"\n :class=\"warningBtnCompactClass\"\n class=\"tpl:shadow-[var(--tpl-shadow-md)]\"\n data-testid=\"restore-hidden-blocks\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"core.previewResolution.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"cloudAttachment?.collaboration?.lockedBlocks.value\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"openCloudPanel('ai-chat')\"\n @open-design-reference=\"openCloudPanel('design-reference')\"\n />\n </main>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[bodyInsetClass]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"rightPanelOpen\"\n @update-block=\"\n (updates) =>\n editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n </div>\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modals.\n Every popup mounts here rather than at the page's `<body>`, so it\n renders inside the editor's effective DOM root and keeps the shadow\n root's adopted stylesheet; a body-level teleport leaves the shadow\n tree and renders unstyled. It also has to stay a child of `.tpl`, which\n declares `--tpl-base-size` and the colour tokens — `@theme inline`\n rebases the whole Tailwind length scale onto that variable, so a\n popup hoisted out would lose the sizing scale as well as the palette.\n Enforced by the `no-teleport-to-body` ESLint rule and\n `tests/global-refs-audit.test.ts`. -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- The comments sidebar. Only mounted when a provider and a `user` are\n configured; the sidebar's own chunk loads the first time it opens. -->\n <CommentsPanel v-if=\"comments?.isAvailable.value\" :feature=\"comments\" />\n\n <!-- Version-history chrome outside the header (the preview banner). Only\n mounted when a provider is configured; the banner's chunk loads the\n first time a version is previewed. -->\n <VersionHistoryPanels\n v-if=\"versionHistory?.isAvailable.value\"\n :feature=\"versionHistory\"\n />\n\n <!-- Cloud sidebars, modals and overlays. One lazy wrapper, mounted only\n under `initCloud()`; an OSS consumer downloads none of it. -->\n <CloudPanels\n v-if=\"cloud && cloudAttachment && cloudReady\"\n :editor=\"editor\"\n :core=\"core\"\n :runtime=\"cloud\"\n :cloud=\"cloudAttachment\"\n :ready=\"cloudReady\"\n :locale=\"config.locale\"\n />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-preview-pill-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-preview-pill-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-preview-pill-enter-from,\n.tpl-preview-pill-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-preview-pill-enter-to,\n.tpl-preview-pill-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onMounted,\n onUnmounted,\n ref,\n watch,\n} from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\n// Type-only, so no cloud module is statically reachable from the OSS entry —\n// see `cloud/runtime.ts` for why the seam is shaped this way.\nimport type { CloudRuntime } from \"./cloud/runtime\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useCommentsFeature } from \"./composables/useCommentsFeature\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTemplatesFeature } from \"./composables/useTemplatesFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useVersionHistoryFeature } from \"./composables/useVersionHistoryFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveAutoSave } from \"./types/auto-save\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { logger } from \"./utils/logger\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport { resolveRenderFonts } from \"./utils/renderProvider\";\nimport {\n withNormalizedContentWrites,\n withNormalizedTemplateLoads,\n} from \"./utils/normalizeMergeTagMarkup\";\nimport type { Translations } from \"./i18n\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw } from \"@lucide/vue\";\nimport { warningBtnCompactClass } from \"./constants/styleConstants\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport EditorHeader from \"./components/EditorHeader.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n /**\n * Cloud's adapter wiring, supplied only by `initCloud()`.\n *\n * There is **one** editor component. Cloud is a set of providers on\n * `config` plus this runtime, which exists purely for the handful of\n * composables that must run at a specific point inside this `setup()` —\n * collaboration before the history interceptor, the lint save-gate after\n * `useEditorCore`. Everything else it contributes arrives through the\n * ordinary config keys an OSS consumer would fill in themselves.\n */\n cloud?: CloudRuntime;\n}>();\n\n// The fourth place consumer content enters (the other three are the entry\n// points in `index.ts`: the mount seed, `setContent` and `create`). A template\n// fetched from the store never passes through the public API — core assigns it\n// to state itself — so the provider is wrapped here, on its way in. Wrapping\n// rather than normalizing after the load resolves is what keeps the load from\n// registering as an edit: core is handed content that is already correct.\nconst templatesProvider = props.config.templates\n ? withNormalizedTemplateLoads(props.config.templates, props.config.mergeTags)\n : undefined;\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n // Seeds a *new* template's body font when no `content` was supplied. Only the\n // deleted Cloud core passed this, so `init({ fonts: { defaultFont } })` never\n // reached a blank template.\n defaultFontFamily: props.config.fonts?.defaultFont,\n templateDefaults: props.config.templateDefaults,\n templates: templatesProvider,\n onError: props.config.onError,\n // Cloud's collaborators lock the blocks they are editing. The map is\n // forward-declared by the runtime because `useCollaboration` — which fills\n // it — can only be built after this call.\n lockedBlocks: props.cloud?.lockedBlocks,\n});\n\n// Collaboration wraps the editor's mutators so they broadcast; `useEditorCore`'s\n// history interceptor wraps them again below. Reversing that order would push\n// local history entries for remote operations and drift the peers apart, which\n// is the whole reason this runs here rather than alongside the rest.\nconst cloudAttachment = props.cloud ? props.cloud.attach({ editor }) : null;\n\n// --- Templates (opt-in: only when a storage provider is configured) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header's name field, status indicator and save button — and what\n// makes Cmd+S mean \"persist now\".\nconst templates = templatesProvider\n ? useTemplatesFeature({\n provider: templatesProvider,\n editor,\n guardUnsavedChanges: props.config.templates?.unsavedChangesGuard,\n // Cloud's lint save-gate, when there is one. Read through a getter\n // because the gate needs `core.templateLint`, which does not exist yet.\n // Without this the collapse would have silently dropped the server's\n // `accessibility.blockOnError` policy.\n getSaveGate: props.cloud ? () => props.cloud!.getSaveGate() : undefined,\n })\n : null;\n\n// Reported to the consumer regardless of whether a provider is configured: a\n// `beforeunload` guard cannot cover SPA route changes, so an embedder needs this\n// to guard their own router — including when they persist via `onChange`.\nif (props.config.onDirtyChange) {\n watch(\n () => editor.state.isDirty,\n (isDirty) => props.config.onDirtyChange!(isDirty),\n );\n}\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n // Cloud's store is plan-gated; a consumer's own store never is.\n isAvailable: props.cloud\n ? () => props.cloud!.isSavedBlocksAvailable()\n : undefined,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n // Cloud folds in the plan feature and \"the template must be saved\", both\n // constraints of *its* sending path rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isTestEmailAvailable()\n : undefined,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Comments (opt-in: only with a storage provider *and* a `user`) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header trigger and the per-block comment indicators.\n//\n// No `user` means no provider call at all — an unattributable comment is worse\n// than no comment feature, so `isAvailable` stays false and nothing renders.\nconst comments = props.config.comments\n ? useCommentsFeature({\n provider: props.config.comments,\n editor,\n user: props.config.user,\n // Cloud keeps comments mutually exclusive with its AI and scoring sidebars:\n // they share one 360px gutter, and two of them open at once would overlap.\n isOpen: cloudAttachment?.panelState.commentsOpen,\n onError: props.config.onError,\n // Cloud folds in the `commenting` plan feature and \"the template must be\n // saved\", both constraints of *its* store rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isCommentsAvailable()\n : undefined,\n // Cloud anchors a comment to a block in the saved template; a consumer's\n // store accepts whatever anchor it is given.\n isBlockSaved: props.cloud\n ? (blockId: string) => props.cloud!.isBlockSaved(blockId)\n : undefined,\n })\n : null;\n\nconst CommentsPanel = defineAsyncComponent(\n () => import(\"./components/CommentsPanel.vue\"),\n);\n\n// --- Cloud chrome (opt-in: only under `initCloud()`) ---\n// One lazy wrapper for every panel, overlay and modal Cloud adds, plus one for\n// its header controls. Same discipline as `SavedBlocksPanels` / `TestEmailPanel`:\n// an OSS consumer never downloads any of it.\nconst CloudPanels = defineAsyncComponent(\n () => import(\"./cloud/components/CloudPanels.vue\"),\n);\nconst CloudHeaderExtras = defineAsyncComponent(\n () => import(\"./cloud/components/CloudHeaderExtras.vue\"),\n);\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// The header control lives in `EditorHeader`; this is the preview banner, which\n// renders outside the header. Both are lazy.\nconst VersionHistoryPanels = defineAsyncComponent(\n () => import(\"./components/VersionHistoryPanels.vue\"),\n);\n\n// --- Auto-save ---\n// One debounced tick drives everything a consumer can ask for on a content\n// change: the `onChange` notification, and, when `templates.autoSave` is on,\n// the save itself. Sharing a single `useAutoSave` instance keeps the two in\n// step and inherits the pause-during-undo/redo behaviour `useEditorCore` wires\n// up around it. `config.changeDebounce` sets the cadence for both.\nconst autoSaveEnabled = resolveAutoSave(\n props.config.templates?.autoSave,\n false,\n);\nconst isAutoSaving = autoSaveEnabled && templates !== null;\n// The one way autosave can still be inert once a provider exists is its\n// `save` being `false` — a read-only store — which `requestAutoSave()` below\n// already no-ops per tick for; this warning is the only signal the consumer\n// gets. `templates !== null` is redundant with `autoSaveEnabled` at runtime\n// (the provider that enables autosave is the same provider that makes\n// `templates` non-null) but stays as an explicit conjunct so TypeScript can\n// narrow `templates.canSave.value` below without a non-null assertion.\nif (autoSaveEnabled && templates !== null && !templates.canSave.value) {\n logger.warn(\n \"config.templates.autoSave is on but this provider's save is false — \" +\n \"there is nothing to persist to. Give it a real save function, or \" +\n \"use `onChange` to persist the content yourself.\",\n );\n}\n// `changeDebounce` alone does not enable autosave — only `templates.autoSave`\n// does. A config with `changeDebounce` set, a provider present, `autoSave`\n// left unset, and no `onChange` has nothing consuming the timer at all,\n// which reads as intentional `onChange`-only pacing but is easy to reach by\n// mistake. Warn on exactly that combination; `autoSave: false` is a stated\n// decision and must stay silent.\nif (\n props.config.changeDebounce !== undefined &&\n templates !== null &&\n props.config.templates?.autoSave === undefined &&\n !props.config.onChange\n) {\n logger.warn(\n \"config.changeDebounce is set but nothing will use it — \" +\n \"config.templates.autoSave is unset and there is no onChange either, \" +\n \"so the timer has no consumer. Set config.templates.autoSave, or add \" +\n \"onChange, so the debounce takes effect.\",\n );\n}\n\nconst autoSaveOptions =\n props.config.onChange || isAutoSaving\n ? {\n onChange: () => {\n props.config.onChange?.(\n JSON.parse(JSON.stringify(editor.state.content)),\n );\n // `requestAutoSave()` already no-ops without a saveable template, so a\n // read-only provider costs one predicate per tick and nothing else.\n // It is the *ungated* request only in the sense that it never raises a\n // prompt: Cloud's lint gate still refuses it, which is what keeps\n // `accessibility.blockOnError` a policy rather than a manual-save\n // speed bump.\n if (isAutoSaving) templates!.requestAutoSave();\n },\n ...(props.config.changeDebounce !== undefined\n ? { debounce: props.config.changeDebounce }\n : {}),\n }\n : null;\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\n// A named object rather than an inline literal: `useVersionHistoryFeature` can\n// only be built after core (it needs history/conditionPreview/autoSave), so its\n// capability is assigned in below, into the very object core provided.\nconst capabilities: EditorCapabilities = {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(templates ? { templates: templates.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n ...(comments ? { comments: comments.capability } : {}),\n};\n\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n // Cloud swaps in its own media browser. Not plan-gated: an entitlement here\n // would fire when a consumer is *not* using Cloud storage, i.e. backwards.\n onRequestMedia: props.cloud?.onRequestMedia ?? props.config.onRequestMedia,\n resolvePreview: props.config.resolvePreview,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions,\n capabilities,\n editorRoot: props.shadowRoot,\n historyOptions: props.cloud\n ? { isRemoteOperation: () => props.cloud!.isRemoteOperation() }\n : undefined,\n keyboardOptions: props.cloud\n ? { onBeforeUndo: () => props.cloud!.onBeforeUndo() }\n : undefined,\n});\n\n// Cloud's setup-time wiring that needs `core`: the lint save-gate (reads\n// `core.templateLint`), the collab undo warning (reads `core.history.canUndo`),\n// and the capability entries Cloud contributes — mutated into the very object\n// `useEditorCore` provided, so injecting components see them on first render.\nconst cloudReady =\n props.cloud && cloudAttachment\n ? props.cloud.ready({ core, capabilities })\n : null;\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// Constructed *after* `useEditorCore` because it drives `history` /\n// `conditionPreview` / `autoSave`, which core owns. Its capability is therefore\n// assigned into the object above rather than passed in — same object reference\n// core provided, mutated during setup, so injecting components see it on their\n// first render.\nconst versionHistory = props.config.versionHistory\n ? useVersionHistoryFeature({\n provider: props.config.versionHistory,\n // The fifth content-in path. A stored version holds whatever was written\n // to it, so it can carry bare tokens like any other loaded content, and\n // previewing one would put them on a canvas where every other tag is a\n // chip. Wrapped at `setContent` rather than at the provider, because\n // version content also arrives off the hydrated list and out of the\n // `fetched` cache without touching `get()`.\n editor: withNormalizedContentWrites(editor, props.config.mergeTags),\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n // Lets the restore confirmation offer to persist unsaved work instead of\n // only warning about it. Without a templates provider — or with one whose\n // `save` is `false` — there is nowhere to put that work, and the\n // confirmation says so rather than offering an action that can't happen.\n // Cloud additionally refuses while the lint gate would block: stacking a\n // second modal on the confirmation would be worse than saying there is\n // currently nowhere to put the work.\n saveBeforeRestore: templates\n ? {\n canSave: () =>\n templates.canSave.value &&\n !(props.cloud?.getSaveGate()?.shouldBlock.value ?? false),\n save: () => templates.save(\"restore\"),\n }\n : null,\n onError: props.config.onError,\n })\n : null;\n\nif (versionHistory) capabilities.versionHistory = versionHistory.capability;\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n getFonts: () => resolveRenderFonts(props.fontsManager),\n });\n}\n\n/**\n * Left/right insets for the canvas body and the footer, which must always agree.\n *\n * Cloud's sidebars widen the right gutter when one is open; without a cloud\n * runtime `rightPanelOpen` is simply never true.\n */\n/**\n * Whether a 360px feature panel occupies the right-hand gutter.\n *\n * The comments panel is the one an OSS session can open, so it counts on its own.\n * In Cloud it also drives `rightPanelOpen` (they share the panel state, which is\n * what keeps comments mutually exclusive with the AI and scoring sidebars), so the\n * `||` is redundant there and load-bearing here.\n *\n * Two things read this, and **both must**: the canvas/footer insets below, and\n * `RightSidebar`'s `shifted-left` — the properties panel sits at `right-0` too, so\n * a panel opening over it would swallow every click meant for the panel.\n */\nconst rightPanelOpen = computed(\n () =>\n comments?.isOpen.value === true ||\n cloudAttachment?.panelState.rightPanelOpen.value === true,\n);\n\nconst bodyInsetClass = computed(() => {\n if (editor.state.previewMode) return \"tpl:left-0 tpl:right-0\";\n return rightPanelOpen.value\n ? \"tpl:left-12 tpl:right-[680px]\"\n : \"tpl:left-12 tpl:right-[320px]\";\n});\n\n/** Canvas affordances that open a Cloud panel. No-ops without a runtime. */\nfunction openCloudPanel(panel: \"ai-chat\" | \"design-reference\"): void {\n if (!cloudAttachment) return;\n if (panel === \"ai-chat\") cloudAttachment.panelState.aiChatOpen.value = true;\n else cloudAttachment.panelState.designReferenceOpen.value = true;\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n props.cloud?.destroy();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\n// The lifecycle trio comes from the templates feature when one exists, so a\n// programmatic save also drives the header's status. Without a provider it comes\n// straight from core, whose own methods reject with the actionable error.\nconst templateLifecycle = templates ?? editor;\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n isDirty: () => editor.state.isDirty,\n create: templateLifecycle.create,\n load: templateLifecycle.load,\n save: templateLifecycle.save,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Chrome wrapper — the clip belongs here, never on the root and never\n around `.tpl-popover-root`. Safari paints a `position: fixed`\n descendant clipped to an ancestor's `overflow: hidden` box while still\n resolving its layout against the viewport, so a dialog teleported\n under a clipping ancestor is cut off wherever it extends past the\n container, and its backdrop dims only the container's own area\n (#633). Keeping the clip on this element bounds the header, rails,\n canvas and footer without reaching the popover root, and this box is\n identical to the root's, so everything anchored `absolute` inside\n lands exactly where it would against the root. Locked by\n `tests/popover-root-clip-scope.test.ts`. -->\n <div class=\"tpl:absolute tpl:inset-0 tpl:overflow-hidden\">\n <!-- One header for both entry points. Cloud's own controls arrive through\n the three slots; everything else here is capability-gated, so the same\n markup covers \"no providers at all\" and a fully-wired Cloud session. -->\n <EditorHeader\n :editor=\"editor\"\n :core=\"core\"\n :templates=\"templates\"\n :show-template-name=\"config.templates?.nameField !== false\"\n :test-email=\"testEmail\"\n :version-history=\"versionHistory\"\n :comments=\"comments\"\n >\n <template v-if=\"cloudAttachment\" #left-extras>\n <CloudHeaderExtras part=\"left\" :cloud=\"cloudAttachment\" />\n </template>\n <template v-if=\"cloudAttachment\" #right-extras>\n <CloudHeaderExtras part=\"right\" :cloud=\"cloudAttachment\" />\n </template>\n </EditorHeader>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n bodyInsetClass,\n versionHistory?.isPreviewing.value ? 'tpl:top-[104px]' : 'tpl:top-14',\n ]\"\n >\n <!-- Preview chrome, floated over the canvas.\n\n A zero-height sticky layer holding one absolutely-positioned,\n canvas-centred **column**. Zero height means the layer contributes no\n space of its own, so nothing here can push the canvas around — which\n is what lets the merge-tag toggle live over the preview instead of in\n the header, whose centre track re-centres on every width change and\n so dragged the Preview button 114.5px sideways each time the toggle\n appeared (#574).\n\n A column rather than two fixed offsets. Both pills are reachable at\n once — the restore pill needs hidden blocks, the toggle needs preview\n mode without a `resolvePreview`, and nothing makes those exclusive —\n so they have to stack. Doing that with a hardcoded second offset put\n the restore pill 48px down even when it rendered alone, which in\n editing mode (where the toggle never shows) dropped it onto the first\n block's content. Flow solves what the offset got wrong: whichever\n pills render sit tight under the header, in order.\n\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div class=\"tpl-preview-overlay tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <div\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:flex tpl:-translate-x-1/2 tpl:flex-col tpl:items-center tpl:gap-2\"\n >\n <!-- Sample / Label. Preview mode only: merge tags are never\n substituted on the editing canvas, so the choice would have no\n effect there. A configured `resolvePreview` supersedes samples\n entirely.\n\n The wrapper carries only the shadow that lifts the control off\n the canvas — no `backdrop-filter`, because the toggle's own\n `--tpl-bg-hover` fill is opaque and covers this box exactly, so a\n blur would composite a layer to show nothing. -->\n <Transition name=\"tpl-preview-pill\">\n <div\n v-if=\"\n editor.state.previewMode &&\n !core.previewResolution.supersedesSamples.value\n \"\n data-testid=\"merge-tag-mode-toggle-anchor\"\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:shadow-[var(--tpl-shadow-md)]\"\n >\n <MergeTagModeToggle\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n </Transition>\n <!-- Show all hidden blocks. The shared warning recipe, not a bespoke\n string: it has to read as the Sample/Label switch's sibling,\n since the two stack here, and a hand-rolled pill is how this one\n came to sit 8px shorter with a different radius and a 1.85:1\n label. `warningBtnCompactClass` carries why the amber is on the\n border rather than the text. -->\n <Transition name=\"tpl-preview-pill\">\n <button\n v-if=\"\n core.conditionPreview.hasHiddenBlocks.value &&\n core.appliesConditionFilter.value\n \"\n type=\"button\"\n :class=\"warningBtnCompactClass\"\n class=\"tpl:shadow-[var(--tpl-shadow-md)]\"\n data-testid=\"restore-hidden-blocks\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"core.previewResolution.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"cloudAttachment?.collaboration?.lockedBlocks.value\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"openCloudPanel('ai-chat')\"\n @open-design-reference=\"openCloudPanel('design-reference')\"\n />\n </main>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[bodyInsetClass]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"rightPanelOpen\"\n @update-block=\"\n (updates) =>\n editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n </div>\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modals.\n Every popup mounts here rather than at the page's `<body>`, so it\n renders inside the editor's effective DOM root and keeps the shadow\n root's adopted stylesheet; a body-level teleport leaves the shadow\n tree and renders unstyled. It also has to stay a child of `.tpl`, which\n declares `--tpl-base-size` and the colour tokens — `@theme inline`\n rebases the whole Tailwind length scale onto that variable, so a\n popup hoisted out would lose the sizing scale as well as the palette.\n Enforced by the `no-teleport-to-body` ESLint rule and\n `tests/global-refs-audit.test.ts`. -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- The comments sidebar. Only mounted when a provider and a `user` are\n configured; the sidebar's own chunk loads the first time it opens. -->\n <CommentsPanel v-if=\"comments?.isAvailable.value\" :feature=\"comments\" />\n\n <!-- Version-history chrome outside the header (the preview banner). Only\n mounted when a provider is configured; the banner's chunk loads the\n first time a version is previewed. -->\n <VersionHistoryPanels\n v-if=\"versionHistory?.isAvailable.value\"\n :feature=\"versionHistory\"\n />\n\n <!-- Cloud sidebars, modals and overlays. One lazy wrapper, mounted only\n under `initCloud()`; an OSS consumer downloads none of it. -->\n <CloudPanels\n v-if=\"cloud && cloudAttachment && cloudReady\"\n :editor=\"editor\"\n :core=\"core\"\n :runtime=\"cloud\"\n :cloud=\"cloudAttachment\"\n :ready=\"cloudReady\"\n :locale=\"config.locale\"\n />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-preview-pill-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-preview-pill-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-preview-pill-enter-from,\n.tpl-preview-pill-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-preview-pill-enter-to,\n.tpl-preview-pill-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","/// <reference types=\"vite/client\" />\n\nimport type en from \"./locales/en\";\nimport type cloudEn from \"./locales/cloud/en\";\n\nexport type Translations = typeof en;\nexport type CloudTranslations = typeof cloudEn;\nexport type TranslationKey = keyof Translations;\n\n// Vite resolves these globs at build time. Adding a new locale file\n// automatically registers it — no array to keep in sync.\nconst ossModules = import.meta.glob<{ default: Translations }>(\n \"./locales/*.ts\",\n);\nconst cloudModules = import.meta.glob<{ default: CloudTranslations }>(\n \"./locales/cloud/*.ts\",\n);\n\nfunction localesFromGlob(modules: Record<string, unknown>): string[] {\n return Object.keys(modules)\n .map((path) => path.match(/\\/([^/]+)\\.ts$/)?.[1])\n .filter((locale): locale is string => Boolean(locale));\n}\n\nconst supportedLocales = localesFromGlob(ossModules);\nconst supportedCloudLocales = localesFromGlob(cloudModules);\n\nfunction canonicalize(locale: string): string {\n return locale.trim().replace(/_/g, \"-\").toLowerCase();\n}\n\nfunction findSupportedLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n const canonical = canonicalize(locale);\n return supported.find((s) => canonicalize(s) === canonical);\n}\n\n/**\n * Get the base language code from a locale string.\n * e.g., 'en-GB' -> 'en', 'de_DE' -> 'de'\n */\nexport function getBaseLocale(locale: string): string {\n return canonicalize(locale).split(\"-\")[0];\n}\n\nfunction tryResolveLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n return (\n findSupportedLocale(locale, supported) ??\n findSupportedLocale(getBaseLocale(locale), supported)\n );\n}\n\nfunction resolveLocale(locale: string, supported: string[]): string {\n return tryResolveLocale(locale, supported) ?? \"en\";\n}\n\n/**\n * Load OSS translations for a given locale.\n * Falls back to English if the locale is not supported.\n */\nexport async function loadTranslations(locale: string): Promise<Translations> {\n const target = resolveLocale(locale, supportedLocales);\n const loader = ossModules[`./locales/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/**\n * Load cloud translations for a given locale.\n * Cloud translations cover features available only via initCloud() —\n * AI, comments, collaboration, scoring, snapshots, plan limits, etc.\n * OSS contributors don't need to translate these; cloud locales fall back\n * to English independently of the OSS locale.\n */\nexport async function loadCloudTranslations(\n locale: string,\n): Promise<CloudTranslations> {\n const target = resolveLocale(locale, supportedCloudLocales);\n const loader = cloudModules[`./locales/cloud/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/** Check if a locale has OSS translations (matched by exact locale, then base). */\nexport function isLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedLocales) !== undefined;\n}\n\n/** Check if a locale has cloud translations (matched by exact locale, then base). */\nexport function isCloudLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedCloudLocales) !== undefined;\n}\n\n/** List of OSS-supported locales. */\nexport function getSupportedLocales(): string[] {\n return [...supportedLocales];\n}\n\n/** List of cloud-supported locales. May be a subset of OSS locales. */\nexport function getSupportedCloudLocales(): string[] {\n return [...supportedCloudLocales];\n}\n","// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- ambient-module declaration must be loaded for cross-package typecheck (workspace path alias resolves to source)\n/// <reference path=\"./virtual-modules.d.ts\" />\nimport { createApp, h, ref, type App, type Ref } from \"vue\";\nimport { DEFAULT_AUTO_SAVE_DEBOUNCE_MS } from \"@templatical/core\";\nimport type {\n BlockDefaults,\n ColorsConfig,\n CommentsProvider,\n CustomBlock,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n EditorUser,\n FontsConfig,\n LogicTagsConfig,\n MediaResult,\n MergeTagsConfig,\n RenderProvider,\n SavedBlocksProvider,\n TestEmailProvider,\n Template,\n TemplateContent,\n TemplateDefaults,\n TemplatesProvider,\n ThemeOverrides,\n VersionHistoryProvider,\n UiTheme,\n ResolvePreview,\n} from \"@templatical/types\";\nimport { createDefaultTemplateContent, safeClone } from \"@templatical/types\";\nimport type { MediaRequestContext } from \"@templatical/media-library\";\n\nimport Editor from \"./Editor.vue\";\nimport type { CloudRuntime } from \"./cloud/runtime\";\nimport type { TemplaticalCloudEditorConfig } from \"./cloud/cloudConfig\";\nimport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nimport { loadTranslations, loadCloudTranslations } from \"./i18n\";\nimport { useFonts } from \"./composables\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport { normalizeContentForConfig } from \"./utils/normalizeMergeTagMarkup\";\nimport {\n buildRenderPayload,\n createRenderMethods,\n resolveRenderFonts,\n} from \"./utils/renderProvider\";\nimport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n// Compiled-CSS-as-string for shadow root adoption. The `virtual:editor-css`\n// module is owned by `scripts/inline-style-css-plugin.ts` — at build time it\n// captures every emitted CSS asset (Tailwind utilities + every `.vue` SFC\n// `<style>` block + `styles/index.css` rules) and replaces this import's\n// runtime value with the full library CSS string. In dev/test the plugin\n// returns `styles/index.css` source as a fallback.\n//\n// Separate concern from the side-effecting `import \"./styles/index.css\"` in\n// `Editor.vue`, which injects styles into `document.head` for light-DOM mode and\n// survives untouched.\n//\n// Ambient declaration for `virtual:editor-css` lives in `virtual-modules.d.ts`\n// referenced at the top of this file via triple-slash so it's visible to any\n// consumer typechecking through the workspace path alias.\nimport editorStylesInline from \"virtual:editor-css\";\n\n// ---------------------------------------------------------------------------\n// OSS config + return types\n// ---------------------------------------------------------------------------\n\nexport interface TemplaticalEditorConfig {\n /**\n * Where to mount the editor — a CSS selector or an `HTMLElement`.\n *\n * Layout caveat: do NOT apply `transform`, `filter`, `perspective`, or\n * `will-change` to an ancestor of this element. Each establishes a CSS\n * containing block for `position: fixed`, which offsets the editor's\n * floating UI (color pickers, rich-text toolbars) and its drag ghost away\n * from their anchor — even when the transform's computed value is `none`\n * (an active/animated transform still promotes the element). For a\n * scroll/entrance effect on a wrapper, animate `opacity` instead of\n * `transform`.\n */\n container: string | HTMLElement;\n content?: TemplateContent;\n\n /**\n * Mount the editor inside a Shadow DOM (open mode) for CSS isolation\n * from the host page. Defaults to `true` — host stylesheets cannot\n * cascade past the shadow boundary into editor elements (`p`, `a`,\n * `input`, etc.), and editor utility classes never collide with host\n * class names.\n *\n * Set to `false` to mount in light DOM. Opt out when:\n * - Your host integration uses `document.querySelector` to reach\n * editor internals (with shadow DOM, use `container.shadowRoot\n * .querySelector(...)` instead).\n * - You need to support Firefox <101 or Safari <16.4, which lack the\n * `adoptedStyleSheets` API the shadow path relies on.\n *\n * Light-mode consumers should keep this set to `false` explicitly so\n * future SDK changes don't silently flip the default again.\n *\n * @default true\n */\n shadowDom?: boolean;\n\n onChange?: (content: TemplateContent) => void;\n onError?: (error: Error) => void;\n\n /**\n * Called whenever the editor's unsaved-changes state flips — `true` on the\n * first edit after a save, `false` once a save completes.\n *\n * Works with or without a `templates` provider, because the editor's own\n * `beforeunload` guard cannot cover SPA route changes: an embedded editor's\n * consumer has to guard their router themselves, and this is what they guard\n * it with.\n */\n onDirtyChange?: (isDirty: boolean) => void;\n\n /**\n * Storage backend for **the template itself** — the save/load lifecycle around\n * whatever is on the canvas.\n *\n * The editor owns the header's name field, save button and save-status\n * indicator, the Cmd+S shortcut, autosave and the unsaved-changes guard; you\n * own persistence. Three methods:\n *\n * ```ts\n * const editor = await init({\n * container,\n * templates: {\n * load: (id) => fetch(`/api/templates/${id}`).then((r) => r.json()),\n * create: (input) =>\n * fetch(\"/api/templates\", {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(input),\n * }).then((r) => r.json()),\n * save: (id, patch) =>\n * fetch(`/api/templates/${id}`, {\n * method: \"PATCH\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(patch),\n * }).then((r) => r.json()),\n * },\n * });\n *\n * await editor.load(\"tpl_123\");\n * ```\n *\n * Autosave (`templates.autoSave`), the unsaved-changes guard\n * (`templates.unsavedChangesGuard`) and the header's inline name field\n * (`templates.nameField`) are configured on this same object — grouped here\n * because none of the three mean anything without somewhere to save to.\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * name field, no save button, no status indicator — and `create()` / `load()` /\n * `save()` reject with an explanatory error. Persist the content yourself from\n * `onChange` instead; Cmd+S then flushes that notification immediately rather\n * than waiting out the debounce.\n *\n * Opening a template is deliberately imperative: there is no `templateId`\n * config key and no in-editor template browser, because choosing which\n * template to open belongs to your application.\n */\n templates?: TemplatesProvider;\n\n /**\n * Rendering backend — how the template becomes MJML, and how MJML becomes the\n * HTML you send.\n *\n * Separate from `templates` on purpose: saving and rendering run at different\n * frequencies (autosave would compile MJML on every debounce tick) and fail in\n * different ways. Every method is optional, and `editor.toMjml()` /\n * `editor.toHtml()` resolve each one independently:\n *\n * | Call | Order |\n * |---|---|\n * | `toMjml()` | `render.toMjml` → the bundled `@templatical/renderer` → throw |\n * | `toHtml()` | `render.toHtml` → `toMjml()`'s result + `render.compileMjml` → throw |\n *\n * **Omitted by default**, in which case `toMjml()` renders locally and\n * `toHtml()` rejects — the SDK deliberately bundles no MJML compiler, so there\n * is no local HTML path. One endpoint is enough to close that gap:\n *\n * ```ts\n * init({\n * container,\n * render: {\n * compileMjml: (mjml) =>\n * fetch(\"/api/mjml\", { method: \"POST\", body: mjml }).then((r) => r.text()),\n * },\n * });\n * ```\n *\n * Provider methods receive a **render-complete** payload: custom blocks already\n * resolved to `renderedHtml`, and the editor's effective fonts. Both are things\n * a backend cannot reconstruct from the template JSON alone.\n */\n render?: RenderProvider;\n\n /**\n * Storage backend for the template's **version history** — the past states a\n * user can browse, preview and restore.\n *\n * The editor owns the header's history control, the preview banner and the\n * restore flow; you own the storage. `list` and `get` are the operations;\n * `create` and `restore` can each be turned off by passing `false` instead of\n * a function.\n *\n * ```ts\n * init({\n * container,\n * versionHistory: {\n * list: (templateId) =>\n * fetch(`/api/templates/${templateId}/versions`).then((r) => r.json()),\n * get: (templateId, versionId) =>\n * fetch(`/api/templates/${templateId}/versions/${versionId}`)\n * .then((r) => r.json())\n * .then((v) => v.content),\n * create: false,\n * restore: (templateId, versionId) =>\n * fetch(`/api/templates/${templateId}/versions/${versionId}/restore`, {\n * method: \"POST\",\n * }).then((r) => r.json()),\n * },\n * });\n * ```\n *\n * **Omitted by default** — the control does not render and none of its UI is\n * downloaded. It also stays hidden until a template is loaded, since a version\n * belongs to a template id.\n *\n * **The editor never records a version by itself.** Whichever\n * `TemplatesProvider.save` you supply decides whether a save also records one,\n * so throttling and retention stay with the side that pays for the storage.\n */\n versionHistory?: VersionHistoryProvider;\n\n /**\n * How long the editor waits after the last edit before it fires `onChange`\n * and, when `templates.autoSave` is on, saves.\n *\n * One timer drives both, so they cannot drift apart. Set it with no templates\n * provider to pace `onChange` alone.\n *\n * @default 2000\n */\n changeDebounce?: number;\n\n onRequestMedia?: OnRequestMedia;\n\n /**\n * Resolves the template for preview surfaces — typically evaluating logic\n * tags (`{% if %}` … `{% endif %}`) against real data on your backend.\n *\n * Called when a preview opens and when the test-email recipient changes,\n * debounced. Display-only: the result never reaches `getContent()`, a send or\n * an export. Rejecting is safe — the preview falls back to the unresolved\n * template and says so.\n *\n * Supersedes `MergeTag.sample` when configured: real data beats example data,\n * so the Sample/Label switch stops rendering.\n */\n resolvePreview?: ResolvePreview;\n\n /**\n * Display-only resolver for image `src` values. The canvas calls this to\n * obtain a preview URL for a src the user entered; the content model (and\n * `toMjml()` output) always keeps the canonical value. Return `null` — or\n * the input value — to use the src as-is.\n *\n * Use this when templates reference images by a value that isn't directly\n * displayable, e.g. plain file names resolved to ephemeral `blob:` URLs\n * from local storage:\n *\n * ```ts\n * resolveImageUrl: async (src) => {\n * const file = await fileStore.lookup(src);\n * return file ? URL.createObjectURL(file) : null;\n * }\n * ```\n *\n * The resolver is called once per committed src value (typing in the src\n * input is debounced, so partial values never reach it) and results are\n * cached per src for the editor's lifetime — including failures, which\n * fall back to displaying the src verbatim. Note the caching applies to\n * transient failures too: a src that failed to resolve stays unresolved\n * until the editor is re-initialized (a re-resolve hook may be added\n * later). Applies to image srcs, design-time placeholder previews, and\n * explicit video thumbnails; auto-derived provider thumbnails (YouTube/\n * Vimeo) are already real URLs and are never resolved.\n */\n resolveImageUrl?: ResolveImageUrl;\n\n mergeTags?: MergeTagsConfig;\n /**\n * Standalone logic tags — control-flow tokens (`{% else %}`) and open/close\n * pairs (`{% if %}` … `{% endif %}`) inserted from a dedicated \"Insert logic\"\n * button. Separate from `mergeTags`; typed/pasted logic is highlighted\n * regardless of this config.\n */\n logicTags?: LogicTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n customBlocks?: CustomBlockDefinition[];\n\n /**\n * Allowlist + order for the block palette. When set, only the listed block\n * types appear in the sidebar palette, in exactly this order — unlisted\n * built-ins (e.g. `video`, `table`) are hidden. When omitted, the full\n * default palette is shown.\n *\n * Reference built-in blocks by their bare type (`'section'`, `'image'`, …)\n * and custom blocks by their `custom:`-prefixed type (`'custom:qrcode'`),\n * so they can be interleaved freely:\n *\n * ```ts\n * paletteBlocks: ['section', 'title', 'image', 'custom:qrcode', 'button']\n * ```\n *\n * Unknown entries — a typo, an unregistered custom block, or `countdown`\n * outside a Cloud plan — are logged with a warning and skipped. Filtering\n * the palette never affects rendering: existing content using a hidden\n * block type still renders correctly.\n */\n paletteBlocks?: string[];\n\n /**\n * Render each HTML block's raw content as a live preview in the editor\n * canvas instead of the static placeholder card. **Off by default.**\n *\n * ```ts\n * htmlBlockPreview: true // shorthand for { enabled: true }\n * htmlBlockPreview: { enabled: true }\n * ```\n *\n * The content is rendered verbatim inside a sandboxed `<iframe>`\n * (`sandbox=\"allow-same-origin\"`, **no** `allow-scripts`): scripts and\n * inline event handlers never execute and styles can't bleed into the rest\n * of the editor. This is a preview-only setting — the MJML/HTML export path\n * renders HTML blocks regardless of it.\n *\n * @default false\n */\n htmlBlockPreview?: HtmlBlockPreviewConfig;\n\n fonts?: FontsConfig;\n\n /**\n * Editor-wide color-picker palette. `presets` appear as a clickable grid in\n * every color picker popover (block toolbars, template settings, rich-text\n * color, custom-block color fields) — clicking one applies it, and the preset\n * matching the current value is marked selected.\n *\n * ```ts\n * colors: {\n * presets: ['#0b5cff', '#111827', '#6b7280', '#ffffff'],\n * allowCustom: false, // lock authors to the presets above\n * }\n * ```\n *\n * `allowCustom` defaults to `true`. Set it to `false` (together with\n * `presets`) to hide the wheel and hex input so authors can only pick from the\n * palette — useful when embedding the editor as a white-label / brand-kit\n * tool. `allowCustom: false` is ignored, with a warning, when no `presets` are\n * configured, since that would leave the picker with no way to set a color.\n */\n colors?: ColorsConfig;\n\n /**\n * Storage backend for **saved blocks** — reusable groups of blocks a user\n * saves from the canvas and re-inserts into other templates.\n *\n * The editor owns the UI (a save action on each block, a searchable browser\n * with preview, insert-at-position, rename, delete); you own persistence.\n * Implement the four methods of `SavedBlocksProvider` against your own API,\n * or use the bundled browser-local provider for demos and prototypes:\n *\n * ```ts\n * import { init, createLocalStorageSavedBlocksProvider } from \"@templatical/editor\";\n *\n * init({ container, savedBlocks: createLocalStorageSavedBlocksProvider() });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * save action, no sidebar entry, and none of its UI code is downloaded.\n *\n * Not to be confused with `customBlocks`, which registers developer-defined\n * block *types* with their own templates and field schemas.\n */\n savedBlocks?: SavedBlocksProvider;\n\n /**\n * Sending backend for **test emails** — letting a user mail themselves the\n * template they're editing.\n *\n * The editor owns the trigger, the dialog, recipient validation and the\n * sending/success/error states; you own delivery. One method is enough:\n *\n * ```ts\n * init({\n * container,\n * testEmail: {\n * send: ({ recipient, content }) =>\n * fetch(\"/api/test-email\", {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify({ recipient, content }),\n * }).then((r) => {\n * if (!r.ok) throw new Error(\"Could not send the test email\");\n * }),\n * },\n * });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * button, and none of its UI code is downloaded.\n *\n * `allowedRecipients` restricts the picker but is **not** a security boundary:\n * it lives in the user's browser. Validate the recipient on your server.\n */\n testEmail?: TestEmailProvider;\n\n /**\n * Storage backend for **comments** — the review conversation on a template.\n *\n * The editor owns the panel, the threading, the resolve flow, the per-block\n * indicators and the jump-to-block affordance; you own persistence. Five\n * methods, each mutation turn-off-able by passing `false`:\n *\n * ```ts\n * init({\n * container,\n * user: { id: \"u_7\", name: \"Ada\" },\n * comments: {\n * list: (templateId) =>\n * fetch(`/api/templates/${templateId}/comments`).then((r) => r.json()),\n * create: (templateId, input) =>\n * fetch(`/api/templates/${templateId}/comments`, {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(input),\n * }).then((r) => r.json()),\n * update: (templateId, commentId, patch) =>\n * fetch(`/api/templates/${templateId}/comments/${commentId}`, {\n * method: \"PATCH\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(patch),\n * }).then((r) => r.json()),\n * delete: async (templateId, commentId) => {\n * await fetch(`/api/templates/${templateId}/comments/${commentId}`, {\n * method: \"DELETE\",\n * });\n * },\n * setResolved: (templateId, commentId, resolved) =>\n * fetch(`/api/templates/${templateId}/comments/${commentId}/resolve`, {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify({ resolved }),\n * }).then((r) => r.json()),\n * },\n * });\n * ```\n *\n * **Requires {@link user}** — with no identity the feature reports itself\n * unavailable and nothing renders, rather than writing anonymous comments.\n *\n * **Omitted by default**, and the panel also stays hidden until a template is\n * loaded, since a comment belongs to a template id.\n *\n * The same object also carries outward events — `onCreated`, `onUpdated`,\n * `onDeleted`, `onResolved`, `onUnresolved` — each called with the comment and\n * whether the change was made locally or arrived through `subscribe`. That's\n * the hook for a \"3 new comments\" badge outside the editor.\n *\n * Realtime is an optional `subscribe` on the provider, not a prerequisite:\n * without it comments work identically, you simply see a colleague's on the next\n * read rather than immediately.\n */\n comments?: CommentsProvider;\n\n /**\n * Who is using the editor — needed by any feature that attributes work to a\n * person. Today that is {@link comments}; collaboration presence will want the\n * same answer, which is why this is a top-level key rather than part of the\n * comments provider.\n *\n * ```ts\n * init({ container, user: { id: \"u_7\", name: \"Ada Lovelace\" } });\n * ```\n *\n * Not a security boundary — it identifies the user to the editor's UI, in the\n * user's own browser. Attribute writes server-side from the session your backend\n * already trusts.\n */\n user?: EditorUser;\n\n blockDefaults?: BlockDefaults;\n templateDefaults?: TemplateDefaults;\n\n theme?: ThemeOverrides;\n uiTheme?: UiTheme;\n locale?: string;\n\n /**\n * Show the \"Powered by Templatical\" footer. Defaults to `true`.\n * Set to `false` to hide the footer (no attribution required by the license).\n */\n branding?: boolean;\n\n /**\n * Show a \"use a larger screen\" notice instead of the editor chrome on\n * viewports narrower than ~768px. Defaults to `true`.\n *\n * The drag-and-drop editor is a desktop-class tool — the block palette,\n * canvas, and properties panel can't lay out usably on a phone, and touch\n * dragging is impractical. Rather than render a broken, cramped layout, the\n * editor shows a clear message below the breakpoint.\n *\n * Set to `false` if you handle small screens yourself (e.g. you embed the\n * editor in a deliberately narrow desktop pane). The check is viewport-based,\n * so it targets actual small devices, not narrow containers on a wide screen.\n *\n * @default true\n */\n smallScreenNotice?: boolean;\n\n /**\n * Template linter (`@templatical/quality`) configuration. Runs every\n * linter exported by the package (accessibility + structure).\n *\n * - When unset, the linter loads on demand once the user opens the panel.\n * - When `disabled: true`, the optional peer is never imported (saves the\n * chunk download) and the sidebar tab + inline badges are suppressed.\n * - `rules`/`thresholds` follow the shape exported by `@templatical/quality`.\n */\n lint?: import(\"@templatical/quality\").LintOptions;\n}\n\n/** Function type for media browser requests, used by both OSS and Cloud editors. */\nexport type OnRequestMedia = (\n context?: MediaRequestContext,\n) => Promise<MediaResult | null>;\n\ninterface TemplaticalEditorBase {\n getContent(): TemplateContent;\n setContent(content: TemplateContent): void;\n setTheme(theme: UiTheme): void;\n unmount(): void;\n /**\n * Render the current template to MJML.\n *\n * Resolves `render.toMjml` first when a `render` provider supplies it, then the\n * bundled `@templatical/renderer` (which resolves custom blocks through the\n * editor's own registry). Rejects with a clear error when neither is available —\n * i.e. no provider method *and* the optional `@templatical/renderer` peer isn't\n * installed.\n */\n toMjml(): Promise<string>;\n /**\n * Render the current template to sending-ready HTML.\n *\n * Resolves `render.toHtml` first, then `toMjml()`'s output through\n * `render.compileMjml`. **Requires one of the two** — the SDK bundles no MJML\n * compiler, so with no `render` provider this always rejects, and the error says\n * which method to add.\n */\n toHtml(): Promise<string>;\n}\n\nexport interface TemplaticalEditor extends TemplaticalEditorBase {\n /**\n * Persist the current content as a new template through the configured\n * `templates` provider, and make the result the editor's template.\n *\n * `input.content`, when given, replaces the editor's content first. Always\n * present on the type; rejects with an explanatory error when no provider is\n * configured or when the provider set `create: false` — the same convention as\n * {@link toMjml}.\n */\n create(input?: {\n name?: string;\n content?: TemplateContent;\n }): Promise<Template>;\n /** Load a template by id and make it the editor's content. */\n load(templateId: string): Promise<Template>;\n /**\n * Persist the loaded template — name and content, as one patch. Rejects when\n * no provider is configured, when the provider set `save: false`, or when no\n * template has been created or loaded yet.\n */\n save(): Promise<Template>;\n /**\n * Whether there are edits the editor knows aren't persisted. Cleared by a\n * successful `save()` / `create()` / `load()`.\n *\n * Read it in a router guard; `onDirtyChange` is the push-based counterpart.\n */\n isDirty(): boolean;\n /**\n * Render a single custom block to its HTML representation, using the\n * registered custom block definition's template and the block's current\n * field values. Exposed for headless callers that want to reuse the\n * editor's renderer (e.g., to drive `@templatical/renderer`'s\n * `renderCustomBlock` option from outside the editor instance).\n */\n renderCustomBlock(block: CustomBlock): Promise<string>;\n /**\n * Look up the definition-level `stylesheet` for a registered custom block\n * type. Returns the raw CSS string, or `undefined` when the type is unknown\n * or the definition has no stylesheet. Exposed for headless callers that\n * want to drive `@templatical/renderer`'s `getCustomBlockStylesheet` option\n * from outside the editor instance.\n */\n getCustomBlockStylesheet(customType: string): string | undefined;\n}\n\n/**\n * `initCloud()` returns the **same** editor `init()` does.\n *\n * That is the point rather than a tidy-up: Cloud is a set of provider\n * implementations behind the same interfaces a consumer would implement, so if\n * the two entry points still returned different shapes, the claim would be\n * false. It also means moving between them is a config change and never a\n * rewrite of the calling code.\n *\n * Three cloud-only members went with the convergence: `create()` takes\n * `init()`'s `{ name?, content? }` input object now, rather than a bare\n * `TemplateContent`; `setThemeOverrides` is gone (`config.theme` is applied\n * at init on both entry points, and the entitlement that gated changing it\n * later is gone); and `sendTestEmail` is gone (the shared dialog is the\n * supported path).\n */\nexport type TemplaticalCloudEditor = TemplaticalEditor;\n\n// ---------------------------------------------------------------------------\n// Shadow root helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Module-cached `CSSStyleSheet` built once from the inline editor CSS string.\n * `adoptedStyleSheets` accepts the same sheet object across multiple shadow\n * roots — sharing one sheet costs zero per-instance memory, regardless of\n * how many editors mount.\n */\nlet cachedEditorStyleSheet: CSSStyleSheet | null = null;\nfunction getEditorStyleSheet(): CSSStyleSheet {\n if (cachedEditorStyleSheet === null) {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(editorStylesInline);\n cachedEditorStyleSheet = sheet;\n }\n return cachedEditorStyleSheet;\n}\n\ninterface MountTarget {\n target: Element;\n shadowRoot: ShadowRoot | null;\n /**\n * Disposer for any dev-mode side effects attached to the shadow root\n * (currently: the document.head `<style>` mirror's MutationObserver).\n * Always safe to call — no-op when no side effects were registered.\n */\n cleanup: () => void;\n}\n\n/**\n * Dev-only: mirror every `<style>` tag in `document.head` into the\n * shadow root's `adoptedStyleSheets`, and observe `document.head` so\n * Vite's HMR-injected style updates flow through to the shadow root\n * automatically.\n *\n * Background: Vite dev injects each `.vue` `<style scoped>` block as a\n * separate `<style>` element in `document.head` via HMR. Those don't\n * cross the shadow boundary, so a shadow-mounted editor in dev would\n * render with only the bundled `styles/index.css` rules — every SFC\n * scoped style (block selection outlines, sidebar layout, etc.) missing.\n *\n * In production builds, `inline-style-css-plugin.ts` captures every\n * emitted CSS asset at `generateBundle` time and inlines the full\n * library CSS as a single string adopted by the shadow root. This\n * dev-only mirror does the same thing at runtime by observing whatever\n * Vite ends up injecting.\n *\n * The dead-code-elimination on `import.meta.env.DEV` ensures the\n * observer + filter logic is stripped from production bundles entirely.\n *\n * Caveats:\n * - In a real consumer's Vite-dev environment, this would also adopt\n * the consumer's page-level styles (whatever they put in\n * `document.head`). That's harmless when consumers install the\n * editor from npm dist (the dev branch is dead-coded out). It only\n * matters when a consumer source-resolves this package, which is\n * unusual outside this repo's own playground.\n * - `replaceSync` strips `@import` rules per the CSSOM spec. Styles\n * containing `@import` are skipped silently (the catch below). The\n * primary bundled sheet covers Tailwind imports already, so this\n * should never matter in practice.\n */\nfunction attachDevStyleMirror(shadowRoot: ShadowRoot): () => void {\n if (!import.meta.env?.DEV) return () => {};\n\n function buildSheets(): CSSStyleSheet[] {\n const sheets: CSSStyleSheet[] = [];\n // shadow-ok: dev-only HMR style mirror (production path is plugin-driven via adoptedStyleSheets)\n document.head.querySelectorAll(\"style\").forEach((el) => {\n const text = el.textContent;\n if (!text) return;\n try {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(text);\n sheets.push(sheet);\n } catch {\n // Skip styles that contain disallowed constructs (e.g. `@import`).\n }\n });\n return sheets;\n }\n\n function refresh(): void {\n // Keep the editor's primary (bundled) sheet first so its declarations\n // are the cascade fallback; dev sheets adopted after may override them\n // — same precedence model as production where everything ends up in\n // one stylesheet anyway.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet(), ...buildSheets()];\n }\n\n refresh();\n\n const observer = new MutationObserver(() => refresh());\n // childList catches new/removed <style> tags; characterData + subtree\n // catches Vite HMR mutating an existing style's textContent in place.\n // shadow-ok: dev-only HMR style mirror observer\n observer.observe(document.head, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n\n return () => observer.disconnect();\n}\n\n/**\n * Resolve where Vue should mount: directly on the consumer's container\n * (light-DOM mode) or on a fresh `<div>` inside a newly-attached open\n * shadow root (shadow mode). In shadow mode, also attaches the editor's\n * cached `CSSStyleSheet` to the root so chrome renders with the right\n * styles inside the boundary, plus a dev-only mirror that observes\n * `document.head` (see `attachDevStyleMirror`).\n *\n * Idempotent: a second call on the same container reuses any existing\n * shadow root, clearing its contents so a prior mount's stale DOM doesn't\n * accumulate.\n */\nfunction resolveMountTarget(\n container: Element,\n shadowDom: boolean | undefined,\n): MountTarget {\n if (!shadowDom) {\n return { target: container, shadowRoot: null, cleanup: () => {} };\n }\n\n const shadowRoot =\n container.shadowRoot ?? container.attachShadow({ mode: \"open\" });\n\n // Adopt the editor stylesheet. Idempotent — repeated assignment of the\n // same sheet is fine. Dev mirror (below) will overwrite this with the\n // primary sheet + mirrored sheets, then keep them in sync.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet()];\n\n // Clear stale content from a prior mount (re-init on same container).\n while (shadowRoot.firstChild) {\n shadowRoot.removeChild(shadowRoot.firstChild);\n }\n\n const host = document.createElement(\"div\");\n host.className = \"tpl-editor-host\";\n // Match the container's layout. Without an explicit size the host is a\n // default block element (height: auto) and the editor's `tpl:h-full`\n // template root collapses to content height because the height-100% chain\n // breaks at the shadow boundary. `width: 100%` is redundant for block\n // elements but harmless and explicit.\n host.style.cssText = \"display:block;height:100%;width:100%;\";\n shadowRoot.appendChild(host);\n\n const cleanup = attachDevStyleMirror(shadowRoot);\n\n return { target: host, shadowRoot, cleanup };\n}\n\n// ---------------------------------------------------------------------------\n// OSS init — sync\n// ---------------------------------------------------------------------------\n\ninterface OssEntry {\n app: App;\n editorRef: Ref<InstanceType<typeof Editor> | null>;\n /** Tear down dev-mode side effects (style mirror observer, etc.). */\n cleanup: () => void;\n}\n\n// Per-container registry so two `init()` calls with different containers\n// produce independent editor instances (multi-instance support). Re-init\n// on the same container still auto-unmounts the previous instance.\nconst ossEntries = new Map<Element, OssEntry>();\n\n// \"Last init'd\" container preserves the legacy single-instance behavior\n// of the top-level `unmount()` export — the playground (and other one-\n// editor consumers) calls bare `unmount()` and expects it to tear down\n// whatever was most recently mounted.\nlet lastOssContainer: Element | null = null;\n\nfunction unmountOssContainer(container: Element): void {\n const entry = ossEntries.get(container);\n if (!entry) return;\n entry.cleanup();\n entry.app.unmount();\n ossEntries.delete(container);\n if (lastOssContainer === container) {\n lastOssContainer = null;\n }\n}\n\nexport async function init(\n config: TemplaticalEditorConfig,\n): Promise<TemplaticalEditor> {\n return mountEditor(config);\n}\n\n/**\n * The one mount path. `init()` calls it with no runtime; `initCloud()` calls it\n * with Cloud's, having already resolved auth, the plan and every adapter.\n */\nasync function mountEditor(\n config: TemplaticalEditorConfig,\n cloud?: CloudRuntime,\n): Promise<TemplaticalEditor> {\n const container =\n typeof config.container === \"string\"\n ? document.querySelector(config.container)\n : config.container;\n\n if (!container) {\n throw new Error(\n `[Templatical] Container element not found: ${config.container}`,\n );\n }\n\n // Load translations before mounting so child components can use useI18n synchronously\n const translations = await loadTranslations(config.locale ?? \"en\");\n\n // Create fonts manager to pass to Editor\n const fontsManager = useFonts(config.fonts);\n\n // Auto-unmount any prior instance on the SAME container *after* awaits\n // — checking before the await would let two concurrent init() calls\n // both pass the guard and orphan the first-mounted app on this\n // container.\n unmountOssContainer(container);\n\n const mount = resolveMountTarget(container, config.shadowDom ?? true);\n const editorRef: Ref<InstanceType<typeof Editor> | null> = ref(null);\n\n // Merge tags reach us in one of two shapes, and only one of them behaves like\n // a tag. Anything typed or pasted is already a `<span data-merge-tag>`, since\n // `MergeTagNode`'s input rules convert it on the spot; content that arrived\n // any other way — a consumer's stored JSON, an `@templatical/import-*`\n // conversion — still holds bare `{{tokens}}` that render as plain text.\n // Normalizing here, before the seed reaches `Editor.vue`, means core is\n // handed content that is already correct and never observes a mutation, so\n // nothing is marked dirty and no autosave tick fires for a load.\n //\n // This is one of four places content enters. The other three:\n // `instance.setContent` and `instance.create` below, and the `templates`\n // provider's `load`, wrapped in `Editor.vue` where it reaches core.\n if (config.content) {\n config.content = normalizeContentForConfig(\n config.content,\n config.mergeTags,\n );\n }\n\n const app = createApp({\n setup() {\n return () =>\n h(Editor, {\n config,\n translations,\n fontsManager,\n shadowRoot: mount.shadowRoot ?? undefined,\n cloud,\n ref: editorRef,\n });\n },\n });\n\n app.mount(mount.target);\n\n ossEntries.set(container, { app, editorRef, cleanup: mount.cleanup });\n lastOssContainer = container;\n\n const instance: TemplaticalEditor = {\n getContent() {\n // safeClone (not a naked JSON.stringify): a drag inside a section can\n // leave a Sortable expando cycle reachable from live content, which a\n // plain stringify chokes on (#203). The clone drops the back-ref.\n if (editorRef.value) {\n return safeClone(editorRef.value.getContent());\n }\n return safeClone(config.content ?? createDefaultTemplateContent());\n },\n setContent(content: TemplateContent) {\n // Normalized once and used for both writes: `getContent()` falls back to\n // `config.content` before mount, so a raw write-back here would hand back\n // different content than the mounted editor holds.\n const normalized = normalizeContentForConfig(content, config.mergeTags);\n if (editorRef.value) {\n editorRef.value.setContent(normalized);\n }\n config.content = normalized;\n },\n setTheme(theme: UiTheme) {\n if (editorRef.value) {\n editorRef.value.setTheme(theme);\n }\n },\n unmount: () => unmountOssContainer(container),\n create(input?: { name?: string; content?: TemplateContent }) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n // Supplied content becomes editor state before the provider is called,\n // so it is consumer content arriving in and gets the same treatment as\n // `setContent`. A contentless create is forwarded as-is — core then\n // persists the state it already holds, which is normalized already.\n return editorRef.value.create(\n input?.content\n ? {\n ...input,\n content: normalizeContentForConfig(\n input.content,\n config.mergeTags,\n ),\n }\n : input,\n );\n },\n load(templateId: string) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.load(templateId);\n },\n save() {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.save();\n },\n isDirty() {\n // Pre-mount there is nothing the editor could have changed, so the honest\n // answer is \"no unsaved edits\" rather than a throw.\n return editorRef.value?.isDirty() ?? false;\n },\n renderCustomBlock(block: CustomBlock) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.renderCustomBlock(block);\n },\n getCustomBlockStylesheet(customType: string) {\n // Pre-mount: registry is empty, so no definition has a stylesheet to\n // resolve. Returning undefined here matches the \"unknown type\" branch\n // and keeps the renderer's `getCustomBlockStylesheet` resolver total.\n if (!editorRef.value) {\n return undefined;\n }\n return editorRef.value.getCustomBlockStylesheet(customType);\n },\n toMjml: () => render.toMjml(),\n toHtml: () => render.toHtml(),\n };\n\n // Both methods resolve per call, so a provider added later in the config — or a\n // provider that implements only one method — behaves the same as one that\n // implements all three. `instance` is captured lazily, which is what lets the\n // payload builder read the editor's live content.\n const render = createRenderMethods({\n provider: config.render,\n buildPayload: () =>\n buildRenderPayload({\n getContent: () => instance.getContent(),\n renderCustomBlock: (block: CustomBlock) =>\n instance.renderCustomBlock(block),\n getFonts: () => resolveRenderFonts(fontsManager),\n }),\n renderLocalMjml: () =>\n toMjmlForInstance({\n getContent: () => instance.getContent(),\n renderCustomBlock: (block: CustomBlock) =>\n instance.renderCustomBlock(block),\n getCustomBlockStylesheet: (customType: string) =>\n instance.getCustomBlockStylesheet(customType),\n getFonts: () => resolveRenderFonts(fontsManager),\n }),\n });\n\n return instance;\n}\n\n// ---------------------------------------------------------------------------\n// Cloud init — a thin adapter-wiring wrapper over init()\n// ---------------------------------------------------------------------------\n\n/**\n * Mount the editor against Templatical Cloud.\n *\n * **This is `init()` with Cloud's adapters filled in**, and deliberately nothing\n * more. Templates, rendering and version history are providers, so Cloud's entry\n * point is adapter wiring rather than a second editor:\n *\n * 1. Build the auth manager and complete the handshake.\n * 2. Health-check the API, and fetch the plan config.\n * 3. Build Cloud's adapters over that auth manager.\n * 4. Delegate to `init()`.\n *\n * A failure in steps 1–2 **rejects**, rather than mounting an editor that shows\n * an error overlay. That is the one place the wrapper is genuinely not `init()`:\n * `init()` cannot fail after it mounts, and OSS should not grow the ability. A\n * session that dies *later* — an auth refresh that cannot renew the token — does\n * still surface as an overlay, because by then there is an editor to cover.\n *\n * `templates`, `comments` and `versionHistory` are all keyed to a template\n * id Cloud issued, which also anchors collaboration, AI rewrite, scoring and\n * the server-side export — a store Cloud never issued ids for would degrade\n * all of them silently. Cloud therefore keeps each key's storage, but each\n * key is still accepted, for its configuration and events:\n * `templates.load`/`create`/`save`,\n * `comments.list`/`create`/`update`/`delete`/`setResolved`, and\n * `versionHistory.list`/`get`/`create`/`restore` are ignored with a warning\n * naming them, while `templates`' `autoSave`, `unsavedChangesGuard`,\n * `nameField`, `onSaved`, `onCreated`, `onLoaded`, `comments`' `onCreated`,\n * `onUpdated`, `onDeleted`, `onResolved`, `onUnresolved`, and\n * `versionHistory`'s `onCreated`, `onRestored` are all honoured.\n *\n * `render` is not a key here at all, unlike `init()` — Cloud renders\n * server-side for test email, sends and exports, so a supplied renderer\n * would change only what you preview and export, never what Cloud delivers.\n * `resolvePreview` is the same key with the same type on both entry points,\n * so upgrading an OSS integration is a deletion. `savedBlocks` and\n * `testEmail` are the same key on both entry points too, but Cloud widens\n * each type to also accept an events-only shape — `boolean |\n * SavedBlocksOptions | SavedBlocksProvider` and `Pick<TestEmailOptions,\n * \"onSent\" | \"defaultRecipient\"> | TestEmailProvider` — so upgrading is\n * still a deletion: drop the key to adopt Cloud's store or sender, or leave\n * it exactly as it is to keep your own.\n *\n * `user` is not a key either: Cloud signs comment writes against the auth token's\n * `user` claim, so it fills `init({ user })` from there rather than letting a\n * browser name someone else.\n */\nexport async function initCloud(\n config: TemplaticalCloudEditorConfig,\n): Promise<TemplaticalCloudEditor> {\n // Dynamic imports — every cloud module is tree-shaken from the OSS bundle, and\n // an OSS consumer who never calls this downloads none of it.\n const [{ bootstrapCloud }, cloudTranslations] = await Promise.all([\n import(\"./cloud/createCloudRuntime\"),\n loadCloudTranslations(config.locale ?? \"en\"),\n ]);\n\n const { runtime, providers, user } = await bootstrapCloud({\n config,\n cloudTranslations,\n });\n\n return mountEditor(\n {\n container: config.container,\n content: config.content,\n shadowDom: config.shadowDom,\n locale: config.locale,\n uiTheme: config.uiTheme,\n theme: config.theme,\n branding: config.branding,\n smallScreenNotice: config.smallScreenNotice,\n blockDefaults: config.blockDefaults,\n templateDefaults: config.templateDefaults,\n customBlocks: config.customBlocks,\n paletteBlocks: config.paletteBlocks,\n htmlBlockPreview: config.htmlBlockPreview,\n colors: config.colors,\n fonts: config.fonts,\n mergeTags: config.mergeTags,\n logicTags: config.logicTags,\n displayConditions: config.displayConditions,\n resolvePreview: config.resolvePreview,\n lint: config.lint,\n onChange: config.onChange,\n onError: config.onError,\n onDirtyChange: config.onDirtyChange,\n // Cloud's adapters, behind the very keys a consumer would fill in\n // themselves. `savedBlocks` is absent when the consumer passed `false`.\n // `templates` spreads Cloud's own provider first so its load/create/save\n // always win over a consumer's config. Autosave defaults on here, unlike\n // `init()`, because Cloud always has a store to save to; the debounce\n // falls back to the same shared constant `useAutoSave` already defaults\n // to, made explicit so a consumer's own `changeDebounce` reaches it.\n templates: {\n ...providers.templates,\n autoSave: config.templates?.autoSave ?? true,\n unsavedChangesGuard: config.templates?.unsavedChangesGuard,\n nameField: config.templates?.nameField,\n },\n changeDebounce: config.changeDebounce ?? DEFAULT_AUTO_SAVE_DEBOUNCE_MS,\n render: providers.render,\n versionHistory: providers.versionHistory,\n savedBlocks: providers.savedBlocks,\n testEmail: providers.testEmail,\n comments: providers.comments,\n // From the JWT. Undefined when the project's token carries no `user` claim,\n // which leaves comments unavailable rather than anonymous.\n user,\n },\n runtime,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Unmount helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Unmount the most-recently-created OSS editor. Single-instance legacy\n * API — callers managing multiple editors should use `instance.unmount()`\n * from each returned object, which targets the specific container.\n */\nexport function unmount(): void {\n if (lastOssContainer) {\n unmountOssContainer(lastOssContainer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TemplaticalCloudEditorConfig } from \"./cloud/cloudConfig\";\nexport type {\n BlockDefaults,\n TemplateContent,\n TemplateDefaults,\n ThemeOverrides,\n UiTheme,\n MergeTagsConfig,\n LogicTagsConfig,\n LogicTag,\n LogicPair,\n DisplayConditionsConfig,\n CustomBlockDefinition,\n ViewportSize,\n ColorsConfig,\n CustomFont,\n FontsConfig,\n RenderPayload,\n RenderProvider,\n SavedBlock,\n SavedBlocksListParams,\n SavedBlocksOptions,\n SavedBlocksProvider,\n Template,\n TemplatePatch,\n TemplatesOptions,\n TemplateSaveTrigger,\n TemplatesProvider,\n TestEmailOptions,\n TestEmailPayload,\n TestEmailProvider,\n VersionHistoryOptions,\n CommentsOptions,\n CommentEventMeta,\n} from \"@templatical/types\";\n\n// Bundled browser-local saved-blocks provider. Re-exported here (rather than\n// leaving it to `@templatical/core`) because consumers install only this\n// package — core is bundled inline and isn't resolvable on their side.\nexport { createLocalStorageSavedBlocksProvider } from \"@templatical/core\";\nexport type { LocalStorageSavedBlocksOptions } from \"@templatical/core\";\n\nexport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nexport type { UseFontsReturn, FontOption } from \"./composables/useFonts\";\nexport { useFonts } from \"./composables/useFonts\";\nexport type { EditorCapabilities } from \"./types/editor-capabilities\";\nexport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n\nexport {\n getSupportedLocales,\n getSupportedCloudLocales,\n isLocaleSupported,\n isCloudLocaleSupported,\n getBaseLocale,\n} from \"./i18n\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"editor.js","names":["$emit","$emit","$emit"],"sources":["../../src/composables/useCommentsFeature.ts","../../src/composables/useSavedBlocksFeature.ts","../../src/composables/useTemplatesFeature.ts","../../src/utils/toMjml.ts","../../src/composables/useTestEmailFeature.ts","../../src/composables/useVersionHistoryFeature.ts","../../src/types/auto-save.ts","../../src/utils/resolveLintOptions.ts","../../src/utils/renderProvider.ts","../../src/utils/normalizeMergeTagMarkup.ts","../../src/components/Canvas.vue","../../src/components/Canvas.vue","../../src/components/CustomBlockStylesheets.vue","../../src/components/CustomBlockStylesheets.vue","../../src/utils/resolvePaletteBlocks.ts","../../src/utils/resolveInsertPosition.ts","../../src/components/Sidebar.vue","../../src/components/Sidebar.vue","../../src/utils/mergeTagTrigger.ts","../../src/utils/textFieldCaret.ts","../../src/composables/useMergeTagAutocomplete.ts","../../src/composables/useMergeTagField.ts","../../src/components/MergeTagSegments.vue","../../src/components/MergeTagSegments.vue","../../src/components/MergeTagInsertButton.vue","../../src/components/MergeTagInsertButton.vue","../../src/components/LogicTagInsertButton.vue","../../src/components/LogicTagInsertButton.vue","../../src/components/MergeTagTextarea.vue","../../src/components/MergeTagTextarea.vue","../../src/components/TemplateSettings.vue","../../src/components/TemplateSettings.vue","../../src/components/MergeTagInput.vue","../../src/components/MergeTagInput.vue","../../src/components/toolbar/ButtonToolbar.vue","../../src/components/toolbar/ButtonToolbar.vue","../../src/components/SpacingControl.vue","../../src/components/SpacingControl.vue","../../src/components/toolbar/CollapsibleSection.vue","../../src/components/toolbar/CollapsibleSection.vue","../../src/components/toolbar/CommonBlockSettings.vue","../../src/components/toolbar/CommonBlockSettings.vue","../../src/components/toolbar/fields/BooleanField.vue","../../src/components/toolbar/fields/BooleanField.vue","../../src/components/toolbar/fields/FieldWrapper.vue","../../src/components/toolbar/fields/FieldWrapper.vue","../../src/components/toolbar/fields/ColorField.vue","../../src/components/toolbar/fields/ColorField.vue","../../src/components/toolbar/fields/ImageField.vue","../../src/components/toolbar/fields/ImageField.vue","../../src/components/toolbar/fields/NumberField.vue","../../src/components/toolbar/fields/NumberField.vue","../../src/components/toolbar/fields/RepeatableField.vue","../../src/components/toolbar/fields/RepeatableField.vue","../../src/components/toolbar/fields/SelectField.vue","../../src/components/toolbar/fields/SelectField.vue","../../src/components/toolbar/fields/TextField.vue","../../src/components/toolbar/fields/TextField.vue","../../src/components/toolbar/fields/TextareaField.vue","../../src/components/toolbar/fields/TextareaField.vue","../../src/components/toolbar/fields/index.ts","../../src/components/toolbar/CustomBlockToolbar.vue","../../src/components/toolbar/CustomBlockToolbar.vue","../../src/components/toolbar/DividerToolbar.vue","../../src/components/toolbar/DividerToolbar.vue","../../src/components/toolbar/HtmlToolbar.vue","../../src/components/toolbar/HtmlToolbar.vue","../../src/components/toolbar/ImageToolbar.vue","../../src/components/toolbar/ImageToolbar.vue","../../src/components/toolbar/MenuToolbar.vue","../../src/components/toolbar/MenuToolbar.vue","../../src/components/toolbar/ParagraphToolbar.vue","../../src/components/toolbar/ParagraphToolbar.vue","../../src/utils/rebalanceColumnChildren.ts","../../src/components/toolbar/SectionToolbar.vue","../../src/components/toolbar/SectionToolbar.vue","../../src/components/toolbar/SocialToolbar.vue","../../src/components/toolbar/SocialToolbar.vue","../../src/components/toolbar/SpacerToolbar.vue","../../src/components/toolbar/SpacerToolbar.vue","../../src/components/toolbar/TableToolbar.vue","../../src/components/toolbar/TableToolbar.vue","../../src/components/toolbar/TitleToolbar.vue","../../src/components/toolbar/TitleToolbar.vue","../../src/components/toolbar/VideoToolbar.vue","../../src/components/toolbar/VideoToolbar.vue","../../src/components/Toolbar.vue","../../src/components/Toolbar.vue","../../src/components/RightSidebar.vue","../../src/components/RightSidebar.vue","../../src/components/SmallScreenNotice.vue","../../src/components/SmallScreenNotice.vue","../../src/components/EditorFooter.vue","../../src/components/EditorFooter.vue","../../src/components/DarkModeToggle.vue","../../src/components/DarkModeToggle.vue","../../src/components/PreviewToggle.vue","../../src/components/PreviewToggle.vue","../../src/components/TemplateNameField.vue","../../src/components/TemplateNameField.vue","../../src/components/TemplateTimestamp.vue","../../src/components/TemplateTimestamp.vue","../../src/components/TemplateSaveStatus.vue","../../src/components/TemplateSaveStatus.vue","../../src/components/EditorHeader.vue","../../src/components/EditorHeader.vue","../../src/components/MergeTagPickerModal.vue","../../src/components/MergeTagPickerModal.vue","../../src/components/LogicTagBadge.vue","../../src/components/LogicTagBadge.vue","../../src/components/LogicTagPickerModal.vue","../../src/components/LogicTagPickerModal.vue","../../src/Editor.vue","../../src/Editor.vue","../../src/i18n/index.ts","../../src/index.ts"],"sourcesContent":["import { computed, ref, watch, type ComputedRef, type Ref } from \"vue\";\nimport { useComments, useCommentListener } from \"@templatical/core\";\nimport type { UseCommentsReturn } from \"@templatical/core\";\nimport type { CommentsProvider, EditorUser } from \"@templatical/types\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** Minimal slice of the editor this feature needs — OSS and Cloud alike. */\ninterface CommentsEditor {\n state: {\n readonly template?: { id: string } | null;\n };\n}\n\nexport interface UseCommentsFeatureOptions {\n /** Storage backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: CommentsProvider;\n editor: CommentsEditor;\n /**\n * Who is commenting (`init({ user })`). **Without one the feature reports\n * itself unavailable** — see {@link isAvailable}.\n */\n user?: EditorUser | null;\n /**\n * The panel's open state. Cloud passes its own so comments stay mutually\n * exclusive with the AI and scoring sidebars, which share the same 360px\n * gutter; OSS gets a local ref.\n */\n isOpen?: Ref<boolean>;\n onError?: (error: Error) => void;\n /**\n * Extra gate on top of a provider and a user being present — Cloud adds its\n * `commenting` plan entitlement and \"the template must be saved\". Read\n * reactively, so a Cloud entitlement that resolves after setup still lights the\n * feature up.\n */\n isAvailable?: () => boolean;\n /**\n * Whether a block id exists in the **stored** template. Cloud supplies this: a\n * comment is anchored server-side, so filtering to a block that only exists on\n * the canvas has nothing to show. Absent means \"assume it does\", which is right\n * for a store that accepts any anchor.\n */\n isBlockSaved?: (blockId: string) => boolean;\n}\n\nexport interface UseCommentsFeatureReturn {\n headless: UseCommentsReturn;\n\n isOpen: Ref<boolean>;\n isAvailable: ComputedRef<boolean>;\n /** True once a template exists — comments are keyed to one. */\n hasTemplate: ComputedRef<boolean>;\n unresolvedCount: ComputedRef<number>;\n\n /**\n * The block the panel should filter to, or `null` for no block filter. Owned\n * here rather than in the panel because a block's comment indicator sets it\n * *before* the lazy panel exists.\n */\n filterBlockId: Ref<string | null>;\n\n /** Toggle the panel. A no-op while unavailable. */\n toggle: () => void;\n close: () => void;\n /** Open the panel filtered to one block — what the canvas indicator calls. */\n openForBlock: (blockId: string) => void;\n\n capability: NonNullable<EditorCapabilities[\"comments\"]>;\n}\n\n/**\n * Shared glue for comments: the reactive thread list, the panel's open and filter\n * state, and the capability the shared chrome gates on.\n *\n * Both entry points construct this — OSS from `init({ comments })`, Cloud from\n * `createCloudCommentsProvider` — so the two run identical logic over different\n * transports. Nothing here is auth- or plan-aware; callers decide availability.\n *\n * **Nothing is fetched at mount.** The list is read when the panel opens, matching\n * saved blocks: a review panel nobody opens should cost no round-trip, and the\n * unresolved badge is worth less than a request on every editor boot.\n */\nexport function useCommentsFeature(\n options: UseCommentsFeatureOptions,\n): UseCommentsFeatureReturn {\n const { provider, editor } = options;\n\n const headless = useComments({\n provider,\n getTemplateId: () => editor.state.template?.id ?? null,\n // Read through a getter, never destructured: Cloud fills `user` from the JWT\n // and a consumer may swap it, and the same lesson as `allowedRecipients`\n // applies — a snapshot at setup pins the answer forever.\n getUser: () => options.user ?? null,\n onError: options.onError,\n });\n\n // Provider-driven realtime, or nothing at all when the provider has no\n // `subscribe`. It is deliberately wired here rather than inside `useComments`:\n // the subscription is a lifecycle concern (it follows the template and must be\n // torn down), and `useComments` is state.\n useCommentListener({\n comments: headless,\n provider,\n getTemplateId: () => editor.state.template?.id ?? null,\n });\n\n const isOpen = options.isOpen ?? ref(false);\n const filterBlockId = ref<string | null>(null);\n\n const hasTemplate = computed(() => !!editor.state.template?.id);\n const unresolvedCount = headless.unresolvedCount;\n\n const isAvailable = computed(() => {\n // No identity ⇒ unavailable, never anonymous. Every comment carries an author\n // and an unattributable one is worse than no comment feature.\n if (!options.user) return false;\n return options.isAvailable?.() ?? true;\n });\n\n // A panel left open when the feature goes away — a Cloud plan fetch that\n // withholds `commenting`, or a consumer clearing `user` — would sit there with\n // no trigger to close it.\n watch(isAvailable, (available) => {\n if (!available) isOpen.value = false;\n });\n\n // Load as soon as there is a template, not just when the panel opens. Unlike\n // saved blocks or version history — whose lists are read only by the surface\n // that fetches them — two indicators *outside* this panel derive from the whole\n // list: `unresolvedCount` feeds the header badge, and `commentCountByBlock`\n // feeds the per-block canvas markers. Deferring the fetch left both at zero, so\n // a template arriving with existing comments showed no sign of them anywhere\n // until the user happened to click Comments, and the badge then popped into\n // existence — the same \"reads 0, then pops\" failure that cost the saved-blocks\n // rail its count.\n //\n // Loading the lot up front is already the design's assumption: `comments.list`\n // is deliberately unpaginated precisely because those two aggregates are\n // computed over every thread.\n watch(\n [isAvailable, hasTemplate],\n ([available, attached]) => {\n if (!available || !attached) return;\n void headless.load().catch(() => {\n /* reported through onError */\n });\n },\n { immediate: true },\n );\n\n // Every open re-reads. History and comments both grow elsewhere, so a list\n // fetched once goes stale silently; only a *first* open has nothing to show\n // meanwhile, which the panel distinguishes for its skeleton.\n watch(isOpen, (open) => {\n if (!open || !hasTemplate.value) return;\n void headless.load().catch(() => {\n /* reported through onError */\n });\n });\n\n function toggle(): void {\n if (!isAvailable.value) return;\n isOpen.value = !isOpen.value;\n }\n\n function close(): void {\n isOpen.value = false;\n }\n\n function openForBlock(blockId: string): void {\n if (!isAvailable.value) return;\n filterBlockId.value = blockId;\n // Assigning `true` to an already-true ref fires no watcher, so an indicator\n // clicked while the panel is open must refresh explicitly — otherwise moving\n // between blocks shows a list read before the last change.\n if (isOpen.value) {\n if (hasTemplate.value) {\n void headless.load().catch(() => {\n /* reported through onError */\n });\n }\n return;\n }\n isOpen.value = true;\n }\n\n return {\n headless,\n\n isOpen,\n isAvailable,\n hasTemplate,\n unresolvedCount,\n\n filterBlockId,\n\n toggle,\n close,\n openForBlock,\n\n capability: {\n getBlockCount: (blockId: string) =>\n headless.commentCountByBlock.value.get(blockId) ?? 0,\n openForBlock,\n // Absent means \"any anchor is fine\", which is the right default for a store\n // that accepts one. Cloud supplies the real check because it renders from\n // the saved template.\n isBlockSaved: (blockId: string) =>\n options.isBlockSaved?.(blockId) ?? true,\n isAvailable,\n unresolvedCount,\n canCreate: headless.canCreate,\n canUpdate: headless.canUpdate,\n canDelete: headless.canDelete,\n canResolve: headless.canResolve,\n },\n };\n}\n","import { computed, provide, ref, watch, type ComputedRef, type Ref } from \"vue\";\nimport { useSavedBlocks, type UseSavedBlocksReturn } from \"@templatical/core\";\nimport type {\n Block,\n SavedBlock,\n SavedBlocksProvider,\n} from \"@templatical/types\";\nimport { cloneBlock } from \"@templatical/types\";\nimport { SAVED_BLOCKS_KEY } from \"../keys\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** Minimal slice of the editor this feature needs — satisfied by OSS and Cloud alike. */\ninterface SavedBlocksEditor {\n addBlock: (\n block: Block,\n targetSectionId?: string,\n columnIndex?: number,\n index?: number,\n ) => void;\n /** Read reactively to cancel a pick session when preview mode turns on. */\n state: { previewMode: boolean };\n}\n\nexport interface UseSavedBlocksFeatureOptions {\n /** Storage backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: SavedBlocksProvider;\n editor: SavedBlocksEditor;\n onError?: (error: Error) => void;\n /**\n * Whether the feature may be used. Read reactively, so Cloud can defer to a\n * plan entitlement that only resolves after its async config fetch. Omit for\n * always-available (the OSS case: a configured provider is the whole gate).\n */\n isAvailable?: () => boolean;\n}\n\nexport interface UseSavedBlocksFeatureReturn {\n headless: UseSavedBlocksReturn;\n\n // --- Pick session ---\n isPicking: ComputedRef<boolean>;\n pickedIds: Ref<Set<string>>;\n pickedCount: ComputedRef<number>;\n startPicking: (blockId: string) => void;\n togglePick: (blockId: string) => void;\n isPicked: (blockId: string) => boolean;\n confirmPicking: () => void;\n cancelPicking: () => void;\n\n // --- Dialogs ---\n isSaveDialogOpen: Ref<boolean>;\n closeSaveDialog: () => void;\n isBrowserOpen: Ref<boolean>;\n openBrowser: () => void;\n closeBrowser: () => void;\n\n /** Insert a saved block's content into the canvas, with fresh block IDs. */\n insert: (saved: SavedBlock, insertIndex: number | undefined) => void;\n /** Re-read the list from the provider (used after a successful save). */\n refresh: () => void;\n /** Capability object handed to `useEditorCore` so shared UI can light up. */\n capability: NonNullable<EditorCapabilities[\"savedBlocks\"]>;\n count: ComputedRef<number>;\n isAvailable: ComputedRef<boolean>;\n}\n\n/**\n * Shared glue for the saved-blocks feature: reactive list, the canvas pick\n * session, the insert operation, and the capability object the shared UI gates\n * on.\n *\n * Both `Editor.vue` (OSS) and `useCloudInitialization` (Cloud) call this with\n * their own provider, so the two entry points run identical logic over\n * different transports. Nothing here is auth- or plan-aware — callers decide\n * whether the feature is available *before* constructing it.\n *\n * **The pick session is deliberately local to this feature.** Choosing which\n * blocks to save happens on the canvas (the user's mental model is spatial),\n * but it does NOT touch `EditorState.selectedBlockId`: keeping it a transient\n * mode avoids a breaking core-state change, leaves Cloud's single-block\n * selection broadcast and block locking untouched, and steers clear of\n * Sortable multi-drag. Promote it to real multi-select only if a second\n * consumer needs it.\n *\n * Must be called from `setup()`: it `provide()`s the headless instance under\n * {@link SAVED_BLOCKS_KEY} for the dialogs to inject.\n */\nexport function useSavedBlocksFeature(\n options: UseSavedBlocksFeatureOptions,\n): UseSavedBlocksFeatureReturn {\n const { provider, editor } = options;\n\n const headless = useSavedBlocks({ provider, onError: options.onError });\n\n const isSaveDialogOpen = ref(false);\n const isBrowserOpen = ref(false);\n\n // --- Pick session ---\n const picking = ref(false);\n const pickedIds = ref<Set<string>>(new Set());\n const isPicking = computed(() => picking.value);\n const pickedCount = computed(() => pickedIds.value.size);\n\n function startPicking(blockId: string): void {\n // Nothing to pick *for* when the provider withheld `create` — the dialog at\n // the end of the session would have no way to persist. The bookmark action\n // is already hidden; this covers programmatic callers.\n if (!headless.canCreate.value) return;\n // Preview mode has no block chrome to pick with, and its click handlers are\n // suppressed — a session there could never be completed or cancelled.\n if (editor.state.previewMode) return;\n pickedIds.value = new Set([blockId]);\n picking.value = true;\n }\n\n function togglePick(blockId: string): void {\n if (!picking.value) return;\n const next = new Set(pickedIds.value);\n if (next.has(blockId)) {\n next.delete(blockId);\n } else {\n next.add(blockId);\n }\n pickedIds.value = next;\n }\n\n function isPicked(blockId: string): boolean {\n return pickedIds.value.has(blockId);\n }\n\n function cancelPicking(): void {\n picking.value = false;\n pickedIds.value = new Set();\n }\n\n function confirmPicking(): void {\n if (pickedCount.value === 0) return;\n // Leave pick mode but keep the set — the dialog reads it to know what it's\n // saving. `closeSaveDialog` is what clears it.\n picking.value = false;\n isSaveDialogOpen.value = true;\n // The dialog offers the categories already in use as suggestions, and those\n // are derived from the loaded list — without this they'd be empty for anyone\n // who saves without ever opening the browser, and the derived-category model\n // would drift into near-duplicates.\n refresh();\n }\n\n function closeSaveDialog(): void {\n isSaveDialogOpen.value = false;\n pickedIds.value = new Set();\n }\n\n // A session can't survive entering preview mode: the canvas stops responding\n // to selection there, so the bar would be the only way out.\n watch(\n () => editor.state.previewMode,\n (previewMode) => {\n if (previewMode && picking.value) cancelPicking();\n },\n );\n\n function openBrowser(): void {\n isBrowserOpen.value = true;\n refresh();\n }\n\n function closeBrowser(): void {\n isBrowserOpen.value = false;\n }\n\n function insert(saved: SavedBlock, insertIndex: number | undefined): void {\n for (let i = 0; i < saved.content.length; i++) {\n // cloneBlock regenerates ids (recursively for section children) so the\n // inserted copy never collides with the stored one, or with blocks\n // already on the canvas from a previous insert of the same entry.\n const cloned = cloneBlock(saved.content[i]);\n const position = insertIndex !== undefined ? insertIndex + i : undefined;\n editor.addBlock(cloned, undefined, undefined, position);\n }\n isBrowserOpen.value = false;\n }\n\n function refresh(): void {\n // Fire-and-forget: `useSavedBlocks` already routes failures to `onError`,\n // and a stale list is not worth blocking the dialog's close on.\n void headless.load().catch(() => {});\n }\n\n const count = computed(() => headless.savedBlocks.value.length);\n const isAvailable = computed(() => options.isAvailable?.() ?? true);\n\n /**\n * Nothing is fetched at mount — the list loads when the browser or the save\n * dialog opens. So a consumer's `list()` latency can neither delay the editor\n * nor shift the sidebar: the rail entry is gated purely on availability,\n * never on whether anything happens to be saved.\n *\n * True only while there is *nothing to show*, which is what makes the browser\n * skeleton appear on a first open and stay away on a reopen — the previous\n * results render immediately while the refetch lands underneath. Without that\n * distinction the empty state would read \"No saved blocks yet\" for however\n * long the request takes, which is false rather than merely unhelpful.\n *\n * The browser modal derives that condition itself from the injected composable\n * — it is the only surface that displays the list, so there is no reason to\n * widen the capability with it.\n */\n\n provide(SAVED_BLOCKS_KEY, headless);\n\n return {\n headless,\n\n isPicking,\n pickedIds,\n pickedCount,\n startPicking,\n togglePick,\n isPicked,\n confirmPicking,\n cancelPicking,\n\n isSaveDialogOpen,\n closeSaveDialog,\n isBrowserOpen,\n openBrowser,\n closeBrowser,\n\n insert,\n refresh,\n capability: {\n startPicking,\n togglePick,\n isPicked,\n isPicking,\n confirmPicking,\n cancelPicking,\n openBrowser,\n count,\n isAvailable,\n canCreate: headless.canCreate,\n canUpdate: headless.canUpdate,\n canDelete: headless.canDelete,\n },\n count,\n isAvailable,\n };\n}\n","import { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { useEventListener, useTimeoutFn } from \"@vueuse/core\";\nimport type {\n Template,\n TemplateContent,\n TemplateSaveTrigger,\n TemplatesProvider,\n} from \"@templatical/types\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** How long the header shows \"Saved\" before falling back to the dirty state. */\nconst SAVED_STATUS_MS = 3000;\n\n/** Minimal slice of the editor this feature needs. */\ninterface TemplatesEditor {\n state: {\n readonly template: {\n id: string;\n name?: string;\n createdAt?: string;\n updatedAt?: string;\n } | null;\n readonly isDirty: boolean;\n readonly isSaving: boolean;\n };\n setName: (name: string) => void;\n create: (input?: {\n name?: string;\n content?: TemplateContent;\n }) => Promise<Template>;\n load: (templateId: string) => Promise<Template>;\n save: (trigger?: TemplateSaveTrigger) => Promise<Template>;\n hasTemplate: () => boolean;\n}\n\n/**\n * A veto over saving, supplied by whoever configured the editor.\n *\n * The one implementation is Cloud's lint save-gate: with the server policy\n * `accessibility.blockOnError` on and error-severity issues present, a manual\n * save opens a confirmation and an autosave simply does not fire. The shape is\n * generic because the header's Save must be *routable* through a gate — without\n * that, unifying the two headers would have silently dropped `blockOnError`.\n */\nexport interface SaveGate {\n /**\n * Manual save (header button, Cmd+S). Runs the save, or defers it and raises a\n * confirmation. Resolves to whether it ran.\n */\n tryRunSave: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n /**\n * Autosave. Runs the save only when the gate would not block, and does nothing\n * at all otherwise — no prompt on a debounce timer, and no write that would\n * demote the policy to a manual-save-only speed bump.\n */\n runUnlessBlocked: (run: () => Promise<unknown> | unknown) => Promise<boolean>;\n}\n\nexport interface UseTemplatesFeatureOptions {\n /** Storage backend — consumer-supplied via `init({ templates })`. */\n provider: TemplatesProvider;\n editor: TemplatesEditor;\n /**\n * Warn on tab close while there are unsaved changes. Defaults to on; pass\n * `false` to own that guard yourself.\n */\n guardUnsavedChanges?: boolean;\n /**\n * Extra gate on top of the provider being present. Read reactively, so a\n * future adapter can defer to an entitlement that resolves after setup. Omit\n * for always-available (the OSS case: a configured provider is the whole gate).\n */\n isAvailable?: () => boolean;\n /**\n * Optional veto over every save this feature performs. Read through a getter\n * rather than passed by value because Cloud builds its gate from\n * `core.templateLint`, which only exists after `useEditorCore()` — i.e. after\n * this composable is constructed.\n */\n getSaveGate?: () => SaveGate | null;\n}\n\nexport interface UseTemplatesFeatureReturn {\n /** Persist the loaded template. Rejects — see {@link requestSave} for UI. */\n save: (trigger?: TemplateSaveTrigger) => Promise<Template>;\n /** Persist the current content as a new template. Rejects on failure. */\n create: (input?: {\n name?: string;\n content?: TemplateContent;\n }) => Promise<Template>;\n /** Fetch a template. Passed straight through; saving status is untouched. */\n load: (templateId: string) => Promise<Template>;\n /**\n * Fire-and-forget save for the header button and Cmd+S: the outcome is already\n * reported through {@link status}, so nothing is left for a caller to handle.\n * A no-op while a save is running, without a template, or when the provider\n * withheld `save`.\n */\n requestSave: (trigger?: TemplateSaveTrigger) => void;\n /**\n * The autosave path. Identical to {@link requestSave} except that a configured\n * gate is consulted with `runUnlessBlocked` instead of `tryRunSave`, so a\n * blocked autosave is skipped silently rather than raising a prompt while the\n * user is typing. The template stays dirty and the header keeps saying so.\n */\n requestAutoSave: () => void;\n /**\n * Commit an inline rename and persist it. Empty (or whitespace-only) names and\n * no-change commits are ignored, so a stray blur can neither clear the name\n * nor trigger a pointless round-trip.\n */\n rename: (name: string) => void;\n /** The loaded template's name, or `undefined` when unnamed / not loaded. */\n name: ComputedRef<string | undefined>;\n /**\n * The stored template's write time and which field it came from, or `null`\n * when the provider supplies neither timestamp.\n */\n timestamp: ComputedRef<{\n iso: string;\n kind: \"updatedAt\" | \"createdAt\";\n } | null>;\n hasTemplate: ComputedRef<boolean>;\n isSaving: ComputedRef<boolean>;\n /**\n * What the header's status indicator shows. `\"saved\"` decays back to `\"idle\"`\n * after a few seconds; `\"error\"` stays until the next attempt, since a failure\n * the user never saw is worse than a stale badge.\n */\n status: Ref<\"idle\" | \"saved\" | \"error\">;\n /** The provider's own message for the last failure. Rendered in a tooltip. */\n errorMessage: Ref<string>;\n canCreate: ComputedRef<boolean>;\n canSave: ComputedRef<boolean>;\n isAvailable: ComputedRef<boolean>;\n capability: NonNullable<EditorCapabilities[\"templates\"]>;\n}\n\n/**\n * Shared glue for the templates feature: the save-status the header renders, the\n * inline rename, the unsaved-changes guard, and the capability shared chrome\n * gates on.\n *\n * Persistence itself lives in `@templatical/core`'s `useEditor`, over the\n * consumer's {@link TemplatesProvider} — this composable owns only what is\n * *about the UI*. It is constructed exactly when a provider is configured, so\n * an editor without one downloads it but never runs it.\n */\nexport function useTemplatesFeature(\n options: UseTemplatesFeatureOptions,\n): UseTemplatesFeatureReturn {\n const { provider, editor } = options;\n\n const status = ref<\"idle\" | \"saved\" | \"error\">(\"idle\");\n const errorMessage = ref(\"\");\n\n const { start: startSavedStatusClear, stop: stopSavedStatusClear } =\n useTimeoutFn(\n () => {\n status.value = \"idle\";\n },\n SAVED_STATUS_MS,\n { immediate: false },\n );\n\n const canCreate = computed(() => typeof provider.create === \"function\");\n const canSave = computed(() => typeof provider.save === \"function\");\n const isAvailable = computed(() => options.isAvailable?.() ?? true);\n\n const name = computed(() => editor.state.template?.name);\n // Prefers `updatedAt`; `createdAt` is the fallback for a store that records\n // only creation. Carries which one won, because labelling a never-updated\n // template \"Updated\" would be a claim the store never made.\n const timestamp = computed<{\n iso: string;\n kind: \"updatedAt\" | \"createdAt\";\n } | null>(() => {\n const template = editor.state.template;\n if (template?.updatedAt)\n return { iso: template.updatedAt, kind: \"updatedAt\" };\n if (template?.createdAt)\n return { iso: template.createdAt, kind: \"createdAt\" };\n return null;\n });\n const hasTemplate = computed(() => editor.state.template !== null);\n const isSaving = computed(() => editor.state.isSaving);\n\n function reportSuccess(): void {\n errorMessage.value = \"\";\n status.value = \"saved\";\n // Restart rather than stack: two saves inside the window would otherwise\n // clear the badge on the first timer, mid-way through the second's window.\n stopSavedStatusClear();\n startSavedStatusClear();\n }\n\n function reportFailure(error: unknown): void {\n stopSavedStatusClear();\n status.value = \"error\";\n errorMessage.value =\n error instanceof Error && error.message ? error.message : \"Save failed\";\n }\n\n async function save(trigger?: TemplateSaveTrigger): Promise<Template> {\n try {\n const template = await editor.save(trigger);\n reportSuccess();\n return template;\n } catch (error) {\n reportFailure(error);\n throw error;\n }\n }\n\n async function create(input?: {\n name?: string;\n content?: TemplateContent;\n }): Promise<Template> {\n try {\n const template = await editor.create(input);\n // Deliberately no \"saved\" badge. The badge acknowledges a save the user\n // asked for — an explicit `save()` or an autosave tick — and attaching a\n // template is neither. It also matched nothing: `load()` attaches without\n // announcing, so a new template flashed green where an existing one stayed\n // silent, for the same action. What a fresh template *has* is a\n // `createdAt`, which the header renders as its write time.\n //\n // The state is still reset, because a failure must not outlive the success\n // that followed it: a create that fails and is then retried would\n // otherwise leave \"Save failed\" on screen for a template that saved fine.\n stopSavedStatusClear();\n status.value = \"idle\";\n errorMessage.value = \"\";\n return template;\n } catch (error) {\n reportFailure(error);\n throw error;\n }\n }\n\n function load(templateId: string): Promise<Template> {\n // The badge describes one template, so it cannot outlive the switch to\n // another: a save that failed seconds ago would otherwise show \"Save failed\"\n // over freshly loaded content, and a green \"Saved\" still inside its decay\n // window would claim the incoming template had just been written. Reset up\n // front rather than on success — a load that fails still means the old\n // status no longer describes what the user is looking at, and if the old\n // template stays attached with edits, `isDirty` puts \"Unsaved\" back.\n stopSavedStatusClear();\n status.value = \"idle\";\n errorMessage.value = \"\";\n // Deliberately not wrapped beyond that: a failed *load* is not a failed\n // save, and flashing \"Save failed\" for it would be a lie. Core already\n // routes it to `onError` and rejects to the caller.\n return editor.load(templateId);\n }\n\n /**\n * An autosave tick that arrived while a save was in flight. `useAutoSave`'s\n * timer fires once and clears itself, and only a further content mutation\n * arms a new one — so dropping such a tick left autosave silent until the\n * user happened to type again.\n */\n let queuedAutoSave = false;\n\n /** Shared by both request paths — the outcome is reported through `status`. */\n function runSave(trigger: TemplateSaveTrigger): Promise<void> {\n return save(trigger).then(\n () => {\n if (!queuedAutoSave) return;\n queuedAutoSave = false;\n // Only what the finished save could not have covered.\n if (editor.state.isDirty) requestAutoSave();\n },\n () => {\n // Deliberately no retry on failure: the status indicator already shows\n // \"Save failed\", so autosave has not gone *silent*, and re-firing into a\n // failing endpoint would only double the traffic. The next content\n // change arms a fresh tick.\n queuedAutoSave = false;\n },\n );\n }\n\n /** True when there is nothing a save could do, whatever the gate says. */\n function cannotSave(): boolean {\n // The UI hides the button in each of these cases; this covers Cmd+S and\n // programmatic callers, where a refusal would surface as an error badge for\n // something the user never asked for.\n return !canSave.value || !hasTemplate.value || isSaving.value;\n }\n\n function requestSave(trigger: TemplateSaveTrigger = \"manual\"): void {\n if (cannotSave()) return;\n const gate = options.getSaveGate?.();\n if (gate) {\n void gate.tryRunSave(() => runSave(trigger));\n return;\n }\n void runSave(trigger);\n }\n\n function requestAutoSave(): void {\n // Split the transient condition out of `cannotSave()`: a save already in\n // flight is a \"later\", not a \"never\". Overlapping PATCHes of the whole\n // document must still never happen, so the tick is queued rather than run.\n if (!canSave.value || !hasTemplate.value) return;\n if (isSaving.value) {\n queuedAutoSave = true;\n return;\n }\n const gate = options.getSaveGate?.();\n if (gate) {\n void gate.runUnlessBlocked(() => runSave(\"autosave\"));\n return;\n }\n void runSave(\"autosave\");\n }\n\n function rename(newName: string): void {\n const trimmed = newName.trim();\n // An empty name is rejected rather than stored: the header would then have\n // nothing to render, and \"\" is far more likely a slip than an intent.\n if (!trimmed) return;\n if (trimmed === editor.state.template?.name) return;\n editor.setName(trimmed);\n requestSave(\"rename\");\n }\n\n // Tab close with unsaved work. Gated on a provider being configured — the\n // editor otherwise has no idea whether the consumer already persisted the\n // change, so it would be warning about nothing. Note this cannot cover SPA\n // route changes; `onDirtyChange` is what consumers guard their router with.\n if (options.guardUnsavedChanges !== false) {\n useEventListener(window, \"beforeunload\", (event: BeforeUnloadEvent) => {\n if (!editor.state.isDirty) return;\n event.preventDefault();\n // Some engines still show the prompt only when `returnValue` is set.\n event.returnValue = \"\";\n });\n }\n\n const capability: NonNullable<EditorCapabilities[\"templates\"]> = {\n save: requestSave,\n rename,\n name,\n timestamp,\n hasTemplate,\n isSaving,\n status,\n errorMessage,\n canCreate,\n canSave,\n isAvailable,\n };\n\n return {\n save,\n create,\n load,\n requestSave,\n requestAutoSave,\n rename,\n name,\n timestamp,\n hasTemplate,\n isSaving,\n status,\n errorMessage,\n canCreate,\n canSave,\n isAvailable,\n capability,\n };\n}\n","import type { CustomBlock, TemplateContent } from \"@templatical/types\";\nimport type { RenderFonts } from \"./renderProvider\";\n\n/**\n * Minimal slice of the editor surface needed by `toMjmlForInstance`.\n * Decoupled from the full `TemplaticalEditor` type so this helper can be\n * tested in isolation with a stub object.\n */\nexport interface ToMjmlSource {\n getContent(): TemplateContent;\n renderCustomBlock(block: CustomBlock): Promise<string>;\n /**\n * Optional. The fonts the editor is rendering with, forwarded to the renderer's\n * `customFonts` / `defaultFallbackFont`. Without it a template using a custom\n * face exports with no `<mj-font>` declaration and no fallback stack — i.e. the\n * mail client silently substitutes. Provided by both editors, and by anything\n * that has a fonts manager; a source that omits it produces byte-identical\n * MJML to one whose template uses no custom face.\n */\n getFonts?: () => RenderFonts;\n /**\n * Optional. Look up the definition-level CSS for a custom block type.\n * Returns `undefined` when the definition is unknown or has no\n * `stylesheet`. Wired to the renderer's `getCustomBlockStylesheet` option\n * so authored responsive/hover/font CSS lands in `<mj-head>` deduped per\n * definition. Omitting it is optional-by-design: the resolver is simply not\n * passed through, so no `<mj-head>` stylesheet is emitted.\n */\n getCustomBlockStylesheet?: (customType: string) => string | undefined;\n}\n\n/**\n * Lazy-load `@templatical/renderer` and render the editor's current content\n * to MJML, wiring the editor's own custom block resolver into the renderer's\n * `renderCustomBlock` callback.\n *\n * The renderer is an optional peer dependency (small, MIT-licensed). It is\n * only loaded when an export is actually requested. Consumers that don't\n * need MJML export at all (e.g., embedding the editor in an app where the\n * backend handles export) can omit the install entirely; calling `toMjml()`\n * in that case throws a clear error naming the missing package.\n *\n * The dynamic import is cached by the module system, so subsequent calls\n * skip the import overhead.\n */\n/**\n * Resolve `@templatical/renderer`, or `null` when it isn't installed.\n *\n * The single `import(\"@templatical/renderer\")` site in the package, so callers\n * can choose their own failure behaviour without duplicating the dynamic import\n * or matching on an error message. {@link toMjmlForInstance} throws when this\n * returns `null`; the test-email path degrades to JSON-only and warns once,\n * because there the renderer is opt-in and a missing install must not break\n * sending.\n *\n * Cheap to call repeatedly — the module system caches the import, so a second\n * call after a successful first one costs nothing.\n */\nexport async function tryLoadRenderer(): Promise<\n typeof import(\"@templatical/renderer\") | null\n> {\n try {\n return await import(\"@templatical/renderer\");\n } catch {\n return null;\n }\n}\n\nexport async function toMjmlForInstance(\n instance: ToMjmlSource,\n): Promise<string> {\n const renderer = await tryLoadRenderer();\n if (!renderer) {\n throw new Error(\n \"[Templatical] toMjml() requires the @templatical/renderer package. Please install it.\",\n );\n }\n const stylesheetResolver = instance.getCustomBlockStylesheet;\n const fonts = instance.getFonts?.();\n return renderer.renderToMjml(instance.getContent(), {\n renderCustomBlock: instance.renderCustomBlock,\n // Only pass through when the source actually provides a resolver, so a\n // caller that doesn't care about stylesheets sees an options shape (and MJML\n // output) with no stylesheet key at all rather than an explicit `undefined`.\n ...(stylesheetResolver\n ? {\n getCustomBlockStylesheet: (customType: string) =>\n stylesheetResolver(customType),\n }\n : {}),\n ...(fonts\n ? {\n customFonts: fonts.customFonts,\n defaultFallbackFont: fonts.defaultFallback,\n }\n : {}),\n });\n}\n","import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from \"vue\";\nimport type {\n TemplateContent,\n TestEmailPayload,\n TestEmailProvider,\n} from \"@templatical/types\";\nimport { logger } from \"../utils/logger\";\nimport { tryLoadRenderer } from \"../utils/toMjml\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** How long the dialog shows its success confirmation before closing itself. */\nconst SUCCESS_DISMISS_MS = 1200;\n\n/**\n * Why a send failed.\n *\n * Split by origin so the dialog knows whether it may render the text directly.\n * A provider's message is already user-presentable and is passed through\n * verbatim; an editor-generated refusal carries no text, because only the\n * component has the active locale.\n */\nexport type TestEmailError =\n { kind: \"provider\"; message: string } | { kind: \"recipientNotAllowed\" };\n\n/**\n * Turn a thrown value into an `Error`. A `catch` clause's binding is\n * `unknown` — a rejected `send` and a throwing `onSent` handler both need a\n * real `Error` to report, and both paths below share this.\n */\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nexport interface UseTestEmailFeatureOptions {\n /** Sending backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: TestEmailProvider;\n /** The template to send. Read at send time, never snapshotted. */\n getContent: () => TemplateContent;\n /**\n * Render the current template to MJML. Supplied by the entry point, which\n * already owns the `ToMjmlSource` for `editor.toMjml()`, so this composable\n * never learns about custom-block resolvers or stylesheets.\n *\n * Only ever called when the provider set `includeMjml`.\n */\n renderMjml?: () => Promise<string>;\n onError?: (error: Error) => void;\n /**\n * Extra gate on top of the provider being present — Cloud adds its plan\n * entitlement and \"template must be saved\" here. Read reactively.\n */\n isAvailable?: () => boolean;\n}\n\nexport interface UseTestEmailFeatureReturn {\n isModalOpen: Ref<boolean>;\n open: () => void;\n close: () => void;\n isSending: Ref<boolean>;\n /** True for a moment after a successful send, while the dialog confirms. */\n justSent: Ref<boolean>;\n error: Ref<TestEmailError | null>;\n send: (recipient: string) => Promise<void>;\n /**\n * The provider's allowlist, or `undefined` when unrestricted.\n *\n * A computed rather than a snapshot: Cloud exposes `allowedRecipients` as a\n * getter over a value that arrives with its JWT, so reading it once at setup\n * would freeze it at `[]` forever.\n */\n allowedRecipients: ComputedRef<string[] | undefined>;\n defaultRecipient: ComputedRef<string | undefined>;\n isAvailable: ComputedRef<boolean>;\n capability: NonNullable<EditorCapabilities[\"testEmail\"]>;\n}\n\nexport function useTestEmailFeature(\n options: UseTestEmailFeatureOptions,\n): UseTestEmailFeatureReturn {\n const { provider } = options;\n\n const isModalOpen = ref(false);\n const isSending = ref(false);\n const justSent = ref(false);\n const error = ref<TestEmailError | null>(null);\n\n let dismissTimer: ReturnType<typeof setTimeout> | null = null;\n /** One warning per editor instance, not per send — a retry shouldn't re-log. */\n let warnedAboutRenderer = false;\n\n function clearDismissTimer(): void {\n if (dismissTimer !== null) {\n clearTimeout(dismissTimer);\n dismissTimer = null;\n }\n }\n\n // The timer outlives a fast unmount otherwise, and fires into dead refs.\n onScopeDispose(clearDismissTimer);\n\n // IMPORTANT: read `provider.allowedRecipients` *inside* the computed. Cloud's\n // adapter implements it as a getter over a reactive source that starts empty\n // and fills once auth resolves — destructuring it at setup would pin it to\n // `[]`, which (see `isAvailable`) hides the trigger permanently.\n const allowedRecipients = computed(() => provider.allowedRecipients);\n\n const defaultRecipient = computed(() => {\n const preferred = provider.defaultRecipient;\n if (preferred === undefined) return undefined;\n const allowed = allowedRecipients.value;\n // A default outside the allowlist is ignored rather than smuggled in.\n if (allowed && !allowed.includes(preferred)) return undefined;\n return preferred;\n });\n\n const isAvailable = computed(() => {\n // An explicitly empty allowlist means nobody may be sent to, so the feature\n // is unusable and must not advertise itself. Distinct from `undefined`,\n // which means \"unrestricted\".\n if (allowedRecipients.value?.length === 0) return false;\n return options.isAvailable?.() ?? true;\n });\n\n function open(): void {\n if (!isAvailable.value) return;\n // Clear both, or a reopen shows the previous attempt's error or a stale tick\n // of the success state.\n error.value = null;\n justSent.value = false;\n clearDismissTimer();\n isModalOpen.value = true;\n }\n\n function close(): void {\n clearDismissTimer();\n isModalOpen.value = false;\n justSent.value = false;\n }\n\n /**\n * Build `payload.mjml` when the provider opted in.\n *\n * Returns `undefined` for the two non-fatal cases (not opted in; opted in but\n * the renderer isn't installed) and **throws** when rendering itself fails —\n * that means the template is broken, and silently sending JSON-only would hide\n * a real defect indefinitely.\n */\n async function resolveMjml(): Promise<string | undefined> {\n if (!provider.includeMjml || !options.renderMjml) return undefined;\n\n try {\n return await options.renderMjml();\n } catch (err) {\n // Probe only on the failure path, so the happy path pays nothing. A `null`\n // here means the package is genuinely absent; anything else is a render\n // error and belongs to the caller.\n if ((await tryLoadRenderer()) === null) {\n if (!warnedAboutRenderer) {\n warnedAboutRenderer = true;\n logger.warn(\n \"testEmail.includeMjml is set but @templatical/renderer is not installed — \" +\n \"sending the template as JSON only. Install it to receive `payload.mjml`.\",\n );\n }\n return undefined;\n }\n throw err;\n }\n }\n\n /**\n * Report a completed send to the provider's `onSent`, guarded so a throw\n * cannot reach `send`'s own try/catch — by the time this runs, `send` has\n * already succeeded and the dialog is already showing success.\n *\n * Mirrors `@templatical/core`'s `notifyHandler`, which this composable\n * cannot import: `error-reporting.ts` is not part of `@templatical/core`'s\n * package exports.\n */\n function notifySent(payload: TestEmailPayload): void {\n try {\n // Read off `provider` at call time, never destructured — the same\n // discipline `allowedRecipients`/`defaultRecipient` apply above, so a\n // handler that arrives or changes after setup is never missed.\n provider.onSent?.(payload);\n } catch (err) {\n options.onError?.(toError(err));\n }\n }\n\n async function send(recipient: string): Promise<void> {\n if (isSending.value) return;\n\n const allowed = allowedRecipients.value;\n if (allowed && !allowed.includes(recipient)) {\n // Unreachable through the picker, reachable by a programmatic open or a\n // tampered field. Refusing keeps a guarantee worth having: the editor\n // never calls `send` with an address outside a configured allowlist.\n error.value = { kind: \"recipientNotAllowed\" };\n return;\n }\n\n isSending.value = true;\n error.value = null;\n\n try {\n const mjml = await resolveMjml();\n const payload: TestEmailPayload = {\n recipient,\n content: options.getContent(),\n ...(mjml !== undefined ? { mjml } : {}),\n ...(allowed !== undefined ? { allowedRecipients: allowed } : {}),\n };\n await provider.send(payload);\n\n justSent.value = true;\n dismissTimer = setTimeout(close, SUCCESS_DISMISS_MS);\n notifySent(payload);\n } catch (err) {\n const wrapped = toError(err);\n error.value = { kind: \"provider\", message: wrapped.message };\n options.onError?.(wrapped);\n // Deliberately not re-thrown: the dialog surfaces `error` and stays open\n // for a retry, and nothing awaits this beyond the click handler.\n } finally {\n isSending.value = false;\n }\n }\n\n const capability: NonNullable<EditorCapabilities[\"testEmail\"]> = {\n open,\n isAvailable,\n };\n\n return {\n isModalOpen,\n open,\n close,\n isSending,\n justSent,\n error,\n send,\n allowedRecipients,\n defaultRecipient,\n isAvailable,\n capability,\n };\n}\n","import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from \"vue\";\nimport {\n useVersionHistory,\n type UseVersionHistoryReturn,\n} from \"@templatical/core\";\nimport type {\n UseAutoSaveReturn,\n UseConditionPreviewReturn,\n UseHistoryReturn,\n} from \"@templatical/core\";\nimport type {\n TemplateContent,\n TemplateVersion,\n VersionHistoryProvider,\n} from \"@templatical/types\";\nimport { safeClone } from \"@templatical/types\";\nimport type { EditorCapabilities } from \"../types/editor-capabilities\";\n\n/** Minimal slice of the editor this feature needs — OSS and Cloud alike. */\ninterface VersionHistoryEditor {\n state: {\n readonly template?: { id: string } | null;\n readonly isDirty: boolean;\n };\n content: Ref<TemplateContent>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n}\n\n/**\n * How the restore confirmation persists unsaved work instead of only warning\n * about it.\n *\n * Both members are read at confirmation time rather than captured at setup, so\n * a late-bound save (Cloud's, which only exists once its lifecycle composable\n * is constructed) and a save that comes and goes (Cloud's lint gate) are both\n * expressible.\n */\nexport interface SaveBeforeRestore {\n /** Whether persisting is possible *right now*. */\n canSave: () => boolean;\n /** Persist the template. Rejects on failure; the confirmation stays up. */\n save: () => Promise<unknown>;\n}\n\nexport interface UseVersionHistoryFeatureOptions {\n /** Storage backend — consumer-supplied in OSS, the Cloud adapter in Cloud. */\n provider: VersionHistoryProvider;\n editor: VersionHistoryEditor;\n /**\n * Lets the restore confirmation offer \"save first\". Omit it — or supply one\n * whose `canSave` is false, which is what a `save: false` templates provider\n * amounts to — and the confirmation degrades to a plain \"these will be lost\",\n * because there is then nowhere to put them.\n */\n saveBeforeRestore?: SaveBeforeRestore | null;\n history: UseHistoryReturn;\n conditionPreview: UseConditionPreviewReturn;\n /** Paused for the duration of a preview, so previewed content is never saved. */\n autoSave: UseAutoSaveReturn | null;\n onError?: (error: Error) => void;\n /**\n * Whether the feature may be used. Read reactively, so Cloud can defer to a\n * plan entitlement that resolves after its async config fetch. Omit for\n * always-available (the OSS case: a configured provider is the whole gate).\n */\n isAvailable?: () => boolean;\n}\n\nexport interface UseVersionHistoryFeatureReturn {\n headless: UseVersionHistoryReturn;\n\n versions: ComputedRef<TemplateVersion[]>;\n isLoading: ComputedRef<boolean>;\n isRestoring: ComputedRef<boolean>;\n canCreate: ComputedRef<boolean>;\n canRestore: ComputedRef<boolean>;\n /** True once a template exists — there is no history before that. */\n hasTemplate: ComputedRef<boolean>;\n isAvailable: ComputedRef<boolean>;\n\n /** The version currently on the canvas, or `null` when editing normally. */\n previewingVersion: Ref<TemplateVersion | null>;\n isPreviewing: ComputedRef<boolean>;\n\n /** True while the unsaved-changes confirmation is up. */\n isConfirmingRestore: Ref<boolean>;\n /** Whether that confirmation may offer \"save first\" rather than only warn. */\n canSaveBeforeRestore: ComputedRef<boolean>;\n /**\n * True from the moment a confirmation action is taken until it settles —\n * covering the save as well as the restore, so a second click can't fire a\n * second save while the first is still in flight.\n */\n isConfirmBusy: ComputedRef<boolean>;\n\n /** Re-read the list. Called when the history control opens. */\n refresh: () => void;\n /**\n * Show a version on the canvas. **Synchronous once the preview is open and the\n * version's content is already in hand**, which is what keeps scrubbing\n * instant — see {@link TemplateVersion.content}.\n */\n navigate: (version: TemplateVersion) => Promise<void>;\n /**\n * What the banner's Restore button calls. Restores straight away when there is\n * nothing to lose; opens the confirmation when there are unsaved changes,\n * because {@link confirmRestore} discards the pre-preview backup and that work\n * would then exist nowhere.\n */\n requestRestore: () => Promise<void>;\n /** Confirmation's primary action: persist the unsaved work, then restore. */\n saveAndRestore: () => Promise<void>;\n /** Confirmation's destructive action: restore, losing the unsaved work. */\n discardAndRestore: () => Promise<void>;\n /** Dismiss the confirmation, staying in the preview with nothing restored. */\n cancelRestoreConfirm: () => void;\n /** Make the previewed version current. Rejects if the provider's restore does. */\n confirmRestore: () => Promise<void>;\n /** Leave the preview and put the pre-preview content back. */\n cancelPreview: () => void;\n\n capability: NonNullable<EditorCapabilities[\"versionHistory\"]>;\n}\n\n/**\n * Shared glue for version history: the reactive list, the preview-before-restore\n * flow, and the capability the shared chrome gates on.\n *\n * Both `Editor.vue` (OSS) and `useCloudInitialization` (Cloud) call this with\n * their own provider, so the two entry points run identical logic over different\n * transports. Nothing here is auth- or plan-aware — callers decide whether the\n * feature is available *before* constructing it.\n */\nexport function useVersionHistoryFeature(\n options: UseVersionHistoryFeatureOptions,\n): UseVersionHistoryFeatureReturn {\n const { provider, editor, history, conditionPreview, autoSave } = options;\n\n const headless = useVersionHistory({\n provider,\n getTemplateId: () => editor.state.template?.id ?? null,\n onError: options.onError,\n });\n\n const previewingVersion = ref<TemplateVersion | null>(null);\n const contentBeforePreview = ref<TemplateContent | null>(null);\n const isConfirmingRestore = ref(false);\n const isSavingBeforeRestore = ref(false);\n\n // Async functions must guard against post-unmount execution after every\n // `await` so we don't write to dead refs or fire side effects\n // (autoSave.pause, setContent) on a torn-down component.\n let destroyed = false;\n onScopeDispose(() => {\n destroyed = true;\n });\n\n const versions = computed(() => headless.versions.value);\n const isLoading = computed(() => headless.isLoading.value);\n const isRestoring = computed(() => headless.isRestoring.value);\n const canCreate = headless.canCreate;\n const canRestore = headless.canRestore;\n const hasTemplate = computed(() => !!editor.state.template?.id);\n const isAvailable = computed(() => options.isAvailable?.() ?? true);\n const isPreviewing = computed(() => previewingVersion.value !== null);\n const canSaveBeforeRestore = computed(\n () => options.saveBeforeRestore?.canSave() === true,\n );\n const isConfirmBusy = computed(\n () => isSavingBeforeRestore.value || isRestoring.value,\n );\n\n function refresh(): void {\n if (!hasTemplate.value) return;\n // Fire-and-forget: `useVersionHistory` already routes failures to `onError`,\n // and a stale list is not worth blocking the dropdown's open on.\n void headless.load().catch(() => {});\n }\n\n async function navigate(version: TemplateVersion): Promise<void> {\n if (destroyed) return;\n\n // A pending confirmation names the version it was raised for, so moving to\n // another one must retract it. Unreachable while the dialog is modal, but a\n // programmatic scrub reaches it today and an inline confirmation would.\n isConfirmingRestore.value = false;\n\n // Already previewing: this is a scrub. Take the synchronous path whenever\n // the content is in hand — a provider that hydrates its list (Cloud does)\n // never awaits here, and the canvas swaps in the same tick.\n if (previewingVersion.value) {\n const known = headless.peekContent(version);\n if (known) {\n previewingVersion.value = version;\n editor.setContent(known, false);\n return;\n }\n const content = await headless.resolveContent(version);\n if (destroyed) return;\n previewingVersion.value = version;\n editor.setContent(content, false);\n return;\n }\n\n // Entering the preview. Resolve first: a failed fetch must leave the user\n // editing, not stranded in a preview of nothing.\n const content = await headless.resolveContent(version);\n if (destroyed) return;\n\n // Opening a preview records nothing. The editor never authors a version —\n // whoever implements `TemplatesProvider.save` decides that. The unsaved work\n // that a confirmed restore would discard is protected at the confirmation\n // instead (see `requestRestore`), which is both where the loss happens and\n // the one place the user can be asked about it.\n //\n // `safeClone`, not `structuredClone`: what arrives here is a Vue reactive\n // proxy over the editor's content, and the structured-clone algorithm\n // refuses a proxy outright. It is also the repo's one deep-clone for\n // content, so a Sortable back-ref that leaked into block data is dropped\n // rather than thrown on.\n contentBeforePreview.value = safeClone(editor.content.value);\n\n autoSave?.pause();\n previewingVersion.value = version;\n editor.setContent(content, false);\n }\n\n async function requestRestore(): Promise<void> {\n if (!previewingVersion.value) return;\n\n // `setContent(…, false)` is what puts a version on the canvas, so `isDirty`\n // still describes the user's own work rather than the preview.\n if (editor.state.isDirty) {\n isConfirmingRestore.value = true;\n return;\n }\n await confirmRestore();\n }\n\n async function saveAndRestore(): Promise<void> {\n const version = previewingVersion.value;\n const before = contentBeforePreview.value;\n const saver = options.saveBeforeRestore;\n // The UI hides this action in each of these cases; a programmatic caller\n // gets a no-op rather than a save of the previewed version.\n if (!version || !before || !saver?.canSave()) return;\n if (isConfirmBusy.value) return;\n\n // The canvas is showing the *previewed* version and a save persists whatever\n // the editor holds, so put the user's work back before saving — otherwise\n // \"save first\" would save the very version they are about to restore.\n editor.setContent(before, false);\n isSavingBeforeRestore.value = true;\n try {\n await saver.save();\n } catch {\n // Already reported by whoever owns the save (status badge, `onError`).\n // Put the preview back so the banner and the canvas agree again, and leave\n // the confirmation up: nothing was saved and nothing was restored.\n if (destroyed) return;\n const previewed = headless.peekContent(version);\n if (previewed) editor.setContent(previewed, false);\n return;\n } finally {\n if (!destroyed) isSavingBeforeRestore.value = false;\n }\n if (destroyed) return;\n await confirmRestore();\n }\n\n async function discardAndRestore(): Promise<void> {\n // The dialog disables its buttons while busy; this covers a programmatic\n // caller, for whom a second restore mid-flight would append twice.\n if (isConfirmBusy.value) return;\n await confirmRestore();\n }\n\n function cancelRestoreConfirm(): void {\n isConfirmingRestore.value = false;\n }\n\n async function confirmRestore(): Promise<void> {\n if (!previewingVersion.value) return;\n\n try {\n const template = await headless.restore(previewingVersion.value.id);\n if (destroyed) return;\n applyRestored(template.content);\n // History is append-only: the restore added an entry, so the list the\n // dropdown shows is stale until it is re-read.\n await headless.load();\n if (destroyed) return;\n } catch (error) {\n // The canvas is still showing the previewed version (set in `navigate`),\n // so roll back to the pre-preview content before the `finally` discards\n // the backup — otherwise the user is silently left editing an un-restored\n // version with the banner gone, and the next autosave persists it.\n if (!destroyed && contentBeforePreview.value) {\n editor.setContent(contentBeforePreview.value, false);\n }\n throw error;\n } finally {\n if (!destroyed) {\n previewingVersion.value = null;\n contentBeforePreview.value = null;\n isConfirmingRestore.value = false;\n autoSave?.resume();\n }\n }\n }\n\n function applyRestored(content: TemplateContent): void {\n editor.setContent(content, false);\n history.clear();\n conditionPreview.reset();\n }\n\n function cancelPreview(): void {\n if (!previewingVersion.value || !contentBeforePreview.value) return;\n\n editor.setContent(contentBeforePreview.value, false);\n\n previewingVersion.value = null;\n contentBeforePreview.value = null;\n isConfirmingRestore.value = false;\n\n autoSave?.resume();\n }\n\n return {\n headless,\n\n versions,\n isLoading,\n isRestoring,\n canCreate,\n canRestore,\n hasTemplate,\n isAvailable,\n\n previewingVersion,\n isPreviewing,\n isConfirmingRestore,\n canSaveBeforeRestore,\n isConfirmBusy,\n\n refresh,\n navigate,\n requestRestore,\n saveAndRestore,\n discardAndRestore,\n cancelRestoreConfirm,\n confirmRestore,\n cancelPreview,\n\n capability: {\n refresh,\n isPreviewing,\n hasTemplate,\n isAvailable,\n canCreate,\n canRestore,\n },\n };\n}\n","/**\n * Whether the editor saves automatically after the user stops editing.\n *\n * The cadence is `changeDebounce` at the config root rather than a member here,\n * because one `useAutoSave` instance drives both the save and the `onChange`\n * notification — and `onChange` works with no templates provider, so a\n * provider-less consumer still needs to set it. `autoSave: false` with a\n * `changeDebounce` is therefore a real setting, not a dead one: it means notify\n * at that cadence and persist nothing.\n */\nexport type AutoSaveConfig = boolean;\n\n/**\n * Normalise an {@link AutoSaveConfig}.\n *\n * The one call site is `Editor.vue`'s `autoSaveEnabled`, which always passes\n * `false` as `defaultEnabled` — a consumer without a `templates` provider has\n * nothing to save to. `initCloud()` inlines its own default instead of\n * calling this (`config.templates?.autoSave ?? true`, in `index.ts`), since\n * Cloud always has a store to save to.\n */\nexport function resolveAutoSave(\n value: AutoSaveConfig | undefined,\n defaultEnabled: boolean,\n): boolean {\n return value ?? defaultEnabled;\n}\n","import type { LintOptions } from \"@templatical/quality\";\n\n/**\n * Build the `LintOptions` object the editor passes to the lint composable.\n *\n * In editor / cloud-editor mode the linter locale is forced to match the\n * editor UI locale — `lint.locale` set by the consumer is ignored.\n * Headless callers (`lintAccessibility(...)` / `lintStructure(...)` directly)\n * keep full control.\n */\nexport function resolveLintOptions(config: {\n locale?: string;\n lint?: LintOptions;\n}): LintOptions {\n return {\n ...config.lint,\n locale: config.locale,\n };\n}\n","import type {\n CustomBlock,\n CustomFont,\n RenderPayload,\n RenderProvider,\n TemplateContent,\n} from \"@templatical/types\";\nimport { safeClone } from \"@templatical/types\";\nimport { preRenderCustomBlocks } from \"./preRenderCustomBlocks\";\n\n/** The fonts half of a {@link RenderPayload}, as the editor resolves it. */\nexport interface RenderFonts {\n customFonts: CustomFont[];\n defaultFallback: string;\n}\n\n/** Minimal slice of the fonts manager the payload needs. */\nexport interface RenderFontsSource {\n customFonts: { value: CustomFont[] };\n defaultFallback: { value: string };\n}\n\n/**\n * The fonts the editor is *actually* rendering with.\n *\n * The canvas's font set and the payload's are the same set *by construction*\n * rather than by agreement: nothing can switch custom faces off for a plan, so\n * there is no second source of truth to keep in step.\n */\nexport function resolveRenderFonts(source: RenderFontsSource): RenderFonts {\n return {\n customFonts: source.customFonts.value,\n defaultFallback: source.defaultFallback.value,\n };\n}\n\n/** What {@link buildRenderPayload} needs from an editor instance. */\nexport interface RenderPayloadSource {\n getContent: () => TemplateContent;\n renderCustomBlock: (block: CustomBlock) => Promise<string>;\n getFonts: () => RenderFonts;\n}\n\n/**\n * Assemble the render-complete payload a {@link RenderProvider} is promised.\n *\n * \"Render-complete\" is the deal that lets the provider win over the bundled\n * renderer: local rendering sees custom blocks and fonts natively, a backend\n * doesn't, so the editor resolves both up front instead of each backend\n * rediscovering the problem.\n *\n * The content is a **defensive copy** (`safeClone`, same as `getContent()`, since\n * a drag inside a section can leave a Sortable expando cycle reachable from live\n * content). Pre-rendering writes `renderedHtml` onto blocks, and doing that to the\n * live tree would mark the editor dirty and make an export mutate the document.\n */\nexport async function buildRenderPayload(\n source: RenderPayloadSource,\n): Promise<RenderPayload> {\n const content = safeClone(source.getContent());\n await preRenderCustomBlocks(content, {\n renderCustomBlock: source.renderCustomBlock,\n });\n\n return { content, fonts: source.getFonts() };\n}\n\n/** What {@link createRenderMethods} needs to resolve each method. */\nexport interface RenderMethodsSource {\n /** The consumer's backend, when `render` was configured. */\n provider?: RenderProvider | null;\n buildPayload: () => Promise<RenderPayload>;\n /**\n * Render MJML with the bundled `@templatical/renderer`. Expected to reject with\n * the \"install @templatical/renderer\" error when the optional peer is absent —\n * that rejection *is* the `toMjml()` chain's final `throw`.\n */\n renderLocalMjml: () => Promise<string>;\n}\n\nexport interface RenderMethods {\n toMjml(): Promise<string>;\n toHtml(): Promise<string>;\n}\n\n/**\n * `editor.toMjml()` / `editor.toHtml()`, resolved **per method rather than per\n * provider** — a provider that implements one of the three methods is not thereby\n * claiming the others.\n *\n * | Call | Order |\n * |---|---|\n * | `toMjml()` | `provider.toMjml` → the bundled renderer → throw |\n * | `toHtml()` | `provider.toHtml` → `toMjml()` + `provider.compileMjml` → throw |\n *\n * `toHtml()` composes through `toMjml()` rather than forcing the local renderer,\n * so a provider whose MJML is authoritative (Cloud's is — it renders block types\n * a browser cannot) is not silently bypassed on the way to HTML. When the provider\n * only supplies `compileMjml`, that resolves to local MJML anyway, which is the\n * cheap tier working exactly as intended.\n *\n * **There is no local HTML path, ever.** The SDK deliberately does not bundle an\n * MJML compiler, so the last step is a rejection with instructions rather than a\n * guess.\n */\nexport function createRenderMethods(\n source: RenderMethodsSource,\n): RenderMethods {\n async function toMjml(): Promise<string> {\n const provider = source.provider;\n // Bound rather than narrowed in place: the call sits behind an `await`, and a\n // bound reference cannot be re-read as undefined afterwards.\n const providerToMjml = provider?.toMjml?.bind(provider);\n if (providerToMjml) {\n return providerToMjml(await source.buildPayload());\n }\n return source.renderLocalMjml();\n }\n\n async function toHtml(): Promise<string> {\n const provider = source.provider;\n\n const providerToHtml = provider?.toHtml?.bind(provider);\n if (providerToHtml) {\n return providerToHtml(await source.buildPayload());\n }\n\n const compileMjml = provider?.compileMjml?.bind(provider);\n if (compileMjml) {\n return compileMjml(await toMjml());\n }\n\n throw new Error(\n \"[Templatical] toHtml() requires a `render` provider implementing either \" +\n \"`toHtml` or `compileMjml`. The SDK does not bundle an MJML compiler, so \" +\n \"there is no local HTML path — point `compileMjml` at any mjml2html \" +\n \"endpoint (a hosted service, a container, or a CLI shell-out).\",\n );\n }\n\n return { toMjml, toHtml };\n}\n","import type { Ref } from \"vue\";\nimport type {\n Block,\n MergeTag,\n MergeTagsConfig,\n SectionBlock,\n SyntaxPreset,\n TemplateContent,\n TemplatesProvider,\n} from \"@templatical/types\";\nimport {\n containsMergeTag,\n getLogicMergeTagKeyword,\n getMergeTagLabel,\n resolveSyntax,\n} from \"@templatical/types\";\n\n/**\n * Convert bare merge-tag tokens in rich-text HTML into the `<span\n * data-merge-tag>` nodes the editor understands.\n *\n * A tag reaches stored content in one of two physical shapes. Anything a user\n * types or pastes is already a node, because `MergeTagNode`'s input and paste\n * rules convert it on the spot. What stays a bare token is content that never\n * passed through that pipeline — `init({ content })`, `setContent()`, a\n * template loaded from a store, or one produced by the `@templatical/import-*`\n * converters. A bare token renders as literal text: no label, no highlight, no\n * `sample` substitution, and it cannot be selected or deleted as a unit.\n *\n * Three properties of this implementation are load-bearing:\n *\n * 1. **It walks text nodes, never markup.** A token in attribute position —\n * `<a href=\"{{unsubscribe_url}}\">` is the common one — must be left exactly\n * as it is; wrapping it injects an element into an attribute value and\n * corrupts the document. A `TreeWalker` over text nodes cannot reach an\n * attribute at all, so this holds by construction rather than by guard.\n * A regex cannot do this job. Guarding with a `(?<!data-merge-tag=\")`\n * lookbehind protects the attribute of an already-converted span and\n * nothing else, so `<a href=\"{{unsubscribe_url}}\">` becomes an anchor whose\n * href contains a `<span>`. Telling text position from attribute position\n * needs parsing; no amount of lookbehind substitutes for it.\n *\n * 2. **It is idempotent structurally.** The walk rejects the whole subtree of\n * any element already carrying `data-merge-tag` / `data-logic-merge-tag`,\n * so an existing span's inner text is unreachable no matter what it says.\n * That covers the case where a tag's label *is* its value.\n *\n * 3. **It never builds markup by concatenation.** A consumer may configure a\n * syntax whose delimiters contain `<` / `>` — Smarty-style `<% $email %>` —\n * so a template string would emit an attribute no scanner could re-read.\n * Spans are created as elements and filled via `setAttribute` /\n * `textContent`, which leaves escaping to the serializer.\n *\n * Matching is driven by `syntax`, not by the declared tag list, so a template\n * migrated from another ESP has all of its tags made atomic rather than only\n * the handful the consumer has declared so far. An undeclared token falls back\n * to showing its raw value as the label, which `getMergeTagLabel` already does.\n * This mirrors typing: the input and paste rules are registered unconditionally\n * and match on syntax alone.\n */\n\ninterface TagHit {\n start: number;\n end: number;\n attr: string;\n value: string;\n label: string;\n}\n\nfunction globalRegex(pattern: RegExp): RegExp {\n const flags = pattern.flags.includes(\"g\")\n ? pattern.flags\n : `${pattern.flags}g`;\n return new RegExp(pattern.source, flags);\n}\n\nfunction collectHits(\n text: string,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): TagHit[] {\n const hits: TagHit[] = [];\n\n const scan = (\n pattern: RegExp,\n attr: string,\n label: (match: string) => string,\n ): void => {\n const regex = globalRegex(pattern);\n let match = regex.exec(text);\n while (match !== null) {\n const start = match.index;\n const end = start + match[0].length;\n // A zero-length match would spin `exec` forever on the same index.\n if (match[0].length === 0) {\n regex.lastIndex++;\n } else if (!hits.some((hit) => start < hit.end && end > hit.start)) {\n hits.push({\n start,\n end,\n attr,\n value: match[0],\n label: label(match[0]),\n });\n }\n match = regex.exec(text);\n }\n };\n\n // Logic first, so it wins any overlap. Some syntaxes' value regex is liberal\n // enough to also match a logic tag — handlebars' `{{.+?}}` swallows\n // `{{#each items}}` — and `isMergeTagValue` resolves that same way.\n scan(syntax.logic, \"data-logic-merge-tag\", (value) =>\n getLogicMergeTagKeyword(value, syntax),\n );\n scan(syntax.value, \"data-merge-tag\", (value) =>\n getMergeTagLabel(value, mergeTags),\n );\n\n return hits.sort((a, b) => a.start - b.start);\n}\n\n/** Text nodes not already inside a merge-tag span, in document order. */\nfunction collectNormalizableTextNodes(doc: Document): Text[] {\n const walker = doc.createTreeWalker(\n doc.body,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,\n {\n acceptNode(node: Node): number {\n if (node.nodeType !== 1) return NodeFilter.FILTER_ACCEPT;\n const el = node as Element;\n // REJECT (not SKIP) prunes the subtree, which is what makes a second\n // pass unable to see an existing span's inner text.\n return el.hasAttribute(\"data-merge-tag\") ||\n el.hasAttribute(\"data-logic-merge-tag\")\n ? NodeFilter.FILTER_REJECT\n : NodeFilter.FILTER_SKIP;\n },\n },\n );\n\n // Collected up front: replacing a node mid-walk invalidates the walker.\n const nodes: Text[] = [];\n let current = walker.nextNode();\n while (current !== null) {\n nodes.push(current as Text);\n current = walker.nextNode();\n }\n return nodes;\n}\n\nexport function normalizeMergeTagsInHtml(\n html: string,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): string {\n if (!html) return html;\n // Cheap reject before paying for a parse. A token that exists only in an\n // attribute still gets past this, and is then caught by the zero-replacement\n // short-circuit below.\n if (!containsMergeTag(html, syntax)) return html;\n if (typeof DOMParser === \"undefined\") return html;\n\n const doc = new DOMParser().parseFromString(\n `<!doctype html><body>${html}</body>`,\n \"text/html\",\n );\n\n let replacements = 0;\n for (const node of collectNormalizableTextNodes(doc)) {\n const text = node.data;\n const hits = collectHits(text, mergeTags, syntax);\n if (hits.length === 0) continue;\n\n const fragment = doc.createDocumentFragment();\n let cursor = 0;\n for (const hit of hits) {\n if (hit.start > cursor) {\n fragment.appendChild(\n doc.createTextNode(text.substring(cursor, hit.start)),\n );\n }\n const span = doc.createElement(\"span\");\n span.setAttribute(hit.attr, hit.value);\n span.textContent = hit.label;\n fragment.appendChild(span);\n cursor = hit.end;\n }\n if (cursor < text.length) {\n fragment.appendChild(doc.createTextNode(text.substring(cursor)));\n }\n\n node.parentNode?.replaceChild(fragment, node);\n replacements += hits.length;\n }\n\n // Nothing matched in text position, so return the input verbatim rather than\n // a re-serialization of it — otherwise content with no tags at all would be\n // rewritten (`<br/>` → `<br>`, attribute quoting, entity encoding) for no\n // reason. Where a replacement does happen some churn is unavoidable, and the\n // renderer accepts both shapes.\n if (replacements === 0) return html;\n\n return doc.body.innerHTML;\n}\n\n/**\n * The rich-text fields, and only those. Every other merge-tag-bearing field —\n * `ButtonBlock.text`/`url`, `ImageBlock.alt`, `HtmlBlock.content`, custom-block\n * `fieldValues`, `settings.preheaderText` — is a plain string rendered as text,\n * so a span written into one would be displayed literally on the canvas and\n * emitted into a `url=` attribute by the renderer.\n *\n * `TableCellData.content` is span-bearing to the renderer but is deliberately\n * excluded: `TableBlock.vue` hydrates a cell with `textContent` and writes\n * `innerText` back on blur, so the first focus-and-leave on a normalized cell\n * would persist the literal markup into stored content as text. The\n * editor/renderer asymmetry there is a separate, pre-existing gap.\n */\nfunction normalizeBlock(\n block: Block,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): Block {\n if (block.type === \"section\") {\n if (!Array.isArray(block.children)) return block;\n let changed = false;\n const children = block.children.map((column) => {\n if (!Array.isArray(column)) return column;\n let columnChanged = false;\n const next = column.map((child) => {\n const normalized = normalizeBlock(child, mergeTags, syntax);\n if (normalized !== child) columnChanged = true;\n return normalized;\n });\n if (!columnChanged) return column;\n changed = true;\n return next;\n });\n return changed ? ({ ...block, children } as SectionBlock) : block;\n }\n\n if (block.type !== \"title\" && block.type !== \"paragraph\") return block;\n if (typeof block.content !== \"string\") return block;\n\n const content = normalizeMergeTagsInHtml(block.content, mergeTags, syntax);\n return content === block.content ? block : { ...block, content };\n}\n\n/**\n * Normalize every rich-text field in a template. Returns the input reference\n * unchanged when nothing matched, so a template with no bare tokens costs no\n * allocation and keeps its object identity; otherwise returns a copy, leaving\n * the caller's content untouched.\n *\n * Shape is checked rather than trusted at every level. `TemplateContent` types\n * `blocks` as required and `SectionBlock.children` as a `Block[][]`, but this\n * runs at the SDK's entry points on an object the consumer hands us — a partial\n * or hand-rolled template must come back untouched, not turn a working `init()`\n * into a `TypeError`.\n */\nexport function normalizeMergeTagMarkup(\n content: TemplateContent,\n mergeTags: MergeTag[],\n syntax: SyntaxPreset,\n): TemplateContent {\n if (!content || !Array.isArray(content.blocks)) return content;\n\n let changed = false;\n const blocks = content.blocks.map((block) => {\n const normalized = normalizeBlock(block, mergeTags, syntax);\n if (normalized !== block) changed = true;\n return normalized;\n });\n\n return changed ? { ...content, blocks } : content;\n}\n\n/**\n * `normalizeMergeTagMarkup` bound to the editor's own config shape, so the hook\n * sites don't each resolve tags and syntax themselves.\n *\n * An absent `mergeTags` still normalizes, under the liquid default. That is not\n * an oversight: `MergeTagNode`'s input and paste rules are registered\n * unconditionally with the same default, so a consumer who configured nothing\n * already gets a node the instant a user types `{{x}}`. Loading the same text\n * and leaving it inert would be the inconsistency.\n */\nexport function normalizeContentForConfig(\n content: TemplateContent,\n mergeTags: MergeTagsConfig | undefined,\n): TemplateContent {\n return normalizeMergeTagMarkup(\n content,\n mergeTags?.tags ?? [],\n resolveSyntax(mergeTags?.syntax),\n );\n}\n\n/**\n * Wrap a `TemplatesProvider` so content arriving from the store is normalized\n * before the editor ever sees it.\n *\n * Only `load` is wrapped. `save` sends content *out*, and what it sends is\n * already-normalized editor state; rewriting it here would change what the\n * store keeps for no benefit. `create` likewise carries content outward, and\n * the template it returns supplies identity and timestamps rather than the\n * content the editor goes on to edit.\n *\n * Wrapping the provider — rather than normalizing after `load()` resolves —\n * is what keeps Q4 true: core assigns already-normalized content to its state\n * and observes no mutation, so there is no dirty flag to suppress and no\n * autosave tick to swallow.\n */\n/**\n * The slice of the editor a content-restoring feature consumes. Structural on\n * purpose: it matches `useVersionHistoryFeature`'s own `VersionHistoryEditor`\n * without importing it, so this util keeps no dependency on a composable.\n */\ninterface ContentWritingEditor {\n state: {\n readonly template?: { id: string } | null;\n readonly isDirty: boolean;\n };\n content: Ref<TemplateContent>;\n setContent: (content: TemplateContent, markDirty?: boolean) => void;\n}\n\n/**\n * Wrap an editor so content written through `setContent` is normalized first.\n *\n * This is how version history is covered. Its content reaches the canvas by\n * three routes — `version.content` off a hydrated list, a `fetched` cache, and\n * `provider.get()` — so wrapping the *provider*, the way the `templates` path\n * does, would miss two of them. `setContent` is the one chokepoint they share.\n *\n * A version is not guaranteed to hold normalized content just because a save\n * produced it: a store carries whatever was written to it, and a backend may\n * version a template it imported rather than one the editor round-tripped.\n * Previewing such a version without this would put bare tokens back on a canvas\n * where every other tag is a chip.\n *\n * `markDirty` is forwarded untouched: every version-history write passes\n * `false`, and swallowing that would turn opening a preview into an unsaved\n * change. `setContent` is resolved per call rather than captured, because\n * collaboration replaces the editor's mutators with broadcasting versions\n * before this feature is built.\n */\nexport function withNormalizedContentWrites<T extends ContentWritingEditor>(\n editor: T,\n mergeTags: MergeTagsConfig | undefined,\n): ContentWritingEditor {\n return {\n get state() {\n return editor.state;\n },\n get content() {\n return editor.content;\n },\n setContent: (content: TemplateContent, markDirty?: boolean) =>\n editor.setContent(\n normalizeContentForConfig(content, mergeTags),\n markDirty,\n ),\n };\n}\n\nexport function withNormalizedTemplateLoads(\n provider: TemplatesProvider,\n mergeTags: MergeTagsConfig | undefined,\n): TemplatesProvider {\n return {\n ...provider,\n async load(id: string) {\n const template = await provider.load(id);\n const content = normalizeContentForConfig(template.content, mergeTags);\n return content === template.content ? template : { ...template, content };\n },\n };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useCloudI18n } from \"../composables/useCloudI18n\";\nimport type {\n Block,\n Collaborator,\n CustomBlock as CustomBlockType,\n TemplateContent,\n ViewportSize,\n} from \"@templatical/types\";\nimport { ImageUp, Sparkles, SquarePlus } from \"@lucide/vue\";\nimport { computed, inject, provide, ref, type Component } from \"vue\";\nimport {\n EDITOR_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n BLOCK_REGISTRY_KEY,\n CAPABILITIES_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n PREVIEW_RESOLUTION_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n requireInject,\n} from \"../keys\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport {\n getDocumentStyle,\n resolveBlockComponent,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n EMAIL_GUTTER,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport { readableTextColor } from \"../utils/readableTextColor\";\n\nimport BlockWrapper from \"./blocks/BlockWrapper.vue\";\nimport SectionBlock from \"./blocks/SectionBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\n\n// `countdown` is deliberately absent: it resolves from the block registry, which\n// `useEditorCore` populates with a lazy `defineAsyncComponent` so OSS bundles\n// don't carry a Cloud-only block. `resolveBlockComponent` checks the registry\n// first, so a static entry here would never be reached — but its static import\n// would still pull the module into the eager graph and defeat the laziness.\n// The other fallback maps (SectionBlock, BlockPreviewCanvas,\n// PreviewSectionBlock) omit it for the same reason.\nconst blockComponentMap: Record<string, Component> = {\n section: SectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n video: VideoBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n viewport: ViewportSize;\n content: TemplateContent;\n selectedBlockId: string | null;\n darkMode: boolean;\n previewMode: boolean;\n lockedBlocks?: Map<string, Collaborator>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select-block\", blockId: string | null): void;\n (e: \"open-ai-chat\"): void;\n (e: \"open-design-reference\"): void;\n}>();\n\nconst { t } = useI18n();\nconst { t: cloudT } = useCloudI18n();\n\nconst editor = requireInject(EDITOR_KEY, \"Canvas\");\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n/**\n * Merge-tag samples render on this canvas **only while preview mode is on**.\n * The `previewMode &&` is the load-bearing half: the canvas is the editing\n * surface, and substituted text in a block a user is about to edit would be\n * text they never wrote. Preview mode is already fully non-editing (clicks and\n * dragging are blocked above), so it is the one state where swapping the\n * displayed value is safe.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Null in headless mounts and when no resolver is configured.\nconst previewResolution = inject(PREVIEW_RESOLUTION_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(\n () =>\n props.previewMode &&\n (mergeTagSampleMode?.value ?? false) &&\n // A configured resolver owns what the preview shows. Substituting samples\n // underneath it would mix invented values with real ones, and on the\n // failure path would contradict the note saying the *unresolved* template\n // is showing.\n !previewResolution?.isConfigured,\n ),\n);\n\nconst canUseAiChat = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"chat\") ?? false),\n);\nconst canUseDesignToTemplate = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"designToTemplate\") ?? false),\n);\n\n/**\n * Whether the hand-toggled display-condition filter applies here. Owned by\n * `useEditorCore` and injected rather than re-derived — see the key's comment.\n */\nconst appliesConditionFilter = inject(APPLIES_CONDITION_FILTER_KEY, null);\n\nconst blocks = computed({\n get: () => props.content.blocks,\n set: (value: Block[]) => {\n editor.setContent({\n ...props.content,\n blocks: value,\n });\n },\n});\n\n// Shared with the preview canvas and the save dialog's scaled rows, so all\n// three lay out against the same width.\nconst viewportWidth = computed(() =>\n getEmailFrameWidth(props.content.settings, props.viewport),\n);\n\n// The \"stage\" represents the email body: the content column plus a gutter of\n// email background on each side (#230 — without it a full-width section's own\n// background hides the global one entirely). Mirrors how `mj-body\n// background-color` renders in the gutters around the centered content when\n// the email is sent. The neutral work area beyond the stage is preserved so\n// editor chrome stays legible.\n//\n// `EMAIL_GUTTER` comes from the shared helper rather than a local constant: the\n// preview surfaces show the same band, and two copies drift into two different\n// answers for how much of the body background an email is supposed to show.\nconst stageWidth = computed(() => viewportWidth.value + EMAIL_GUTTER * 2);\n\n// Canvas dark mode preview: simulates how the email will appear in recipients'\n// dark-themed email clients. Uses CSS filter inversion — independent of the\n// editor UI theme (light/dark/auto) which is controlled via uiTheme config.\n// The email bg lives on the `.tpl-canvas-bg` sibling layer (so it can be\n// inverted by `filter` without trapping block chrome in a stacking context);\n// the `.tpl-canvas` itself stays transparent so the bg layer shows through.\n// Shared with every other surface that renders blocks (the saved-block\n// previews), so a block looks the same wherever it is drawn.\nconst canvasStyle = computed(() => getDocumentStyle(props.content.settings));\n\n// Empty canvas: the whole dashed placeholder IS the Sortable drop zone.\n// `isEmptyCanvas` toggles the styling + the inline empty-state content.\nconst isEmptyCanvas = computed(\n () => blocks.value.length === 0 && !props.previewMode,\n);\n\n// Highlight the empty drop zone while a block is being dragged over it.\n// Uses a counter to handle dragenter/leave bubbling from descendants —\n// raw event toggling would flicker as the pointer moves between children.\nconst dragEnterDepth = ref(0);\nconst isDragOverEmpty = computed(\n () => isEmptyCanvas.value && dragEnterDepth.value > 0,\n);\n\nfunction handleEmptyDragEnter(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value += 1;\n}\n\nfunction handleEmptyDragLeave(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value = Math.max(0, dragEnterDepth.value - 1);\n}\n\nfunction handleEmptyDrop(): void {\n dragEnterDepth.value = 0;\n}\n\nfunction handleCanvasClick(event: MouseEvent): void {\n if (props.previewMode) {\n return;\n }\n // During a pick session, a stray background click must not clear anything —\n // the session is left only via the bar's Save/Cancel or Escape. Deselecting\n // here would also be meaningless, since picking doesn't touch selection.\n if (caps.savedBlocks?.isPicking.value) {\n return;\n }\n if (event.target === event.currentTarget) {\n emit(\"select-block\", null);\n }\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, blockComponentMap);\n}\n\nfunction getBlockLock(blockId: string): Collaborator | null {\n return props.lockedBlocks?.get(blockId) ?? null;\n}\n\n/**\n * A click on a block either toggles its pick (while a saved-blocks pick\n * session is running) or selects it. Preview mode and collaborator locks are\n * checked first, so a locked block can be neither selected nor picked.\n */\nfunction handleBlockSelect(blockId: string): void {\n if (props.previewMode || getBlockLock(blockId)) return;\n const savedBlocks = caps.savedBlocks;\n if (savedBlocks?.isPicking.value) {\n savedBlocks.togglePick(blockId);\n return;\n }\n emit(\"select-block\", blockId);\n}\n\nfunction isPickedBlock(blockId: string): boolean {\n return caps.savedBlocks?.isPicked(blockId) ?? false;\n}\n\n/** True while a pick session runs — swaps block chrome into picking behaviour. */\nconst isPicking = computed(() => caps.savedBlocks?.isPicking.value === true);\n\nfunction handleFetchData(\n block: Block,\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n): void {\n if (block.type !== \"custom\") {\n return;\n }\n\n editor.updateBlock(block.id, {\n fieldValues: payload.fieldValues,\n dataSourceFetched: payload.dataSourceFetched,\n } as Partial<CustomBlockType>);\n}\n</script>\n\n<template>\n <!-- STAGE — the email body. Sized to the content column plus a gutter of\n email background on each side, so the global background stays visible\n around full-width sections, mirroring how `mj-body background-color`\n renders in the gutters when sent (#230). The neutral work area beyond\n the stage is preserved so the canvas still reads as a floating card and\n editor chrome stays legible. -->\n <div\n class=\"tpl-canvas-stage tpl:relative tpl:flex tpl:justify-center tpl:rounded-lg\"\n :style=\"{\n width: `${stageWidth}px`,\n boxShadow: darkMode ? 'none' : 'var(--tpl-shadow-xl)',\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <!-- Background layer — holds the email bg and the dark-preview filter,\n spanning the full stage (content + gutters). Filter on this leaf\n layer (no descendants) does not create a containing block / stacking\n context that would trap block chrome (action bar, indicators) inside\n the wrapper. The filter has its own transition so the canvas fades\n smoothly between modes. -->\n <div\n class=\"tpl-canvas-bg tpl:absolute tpl:inset-0 tpl:rounded-lg tpl:pointer-events-none\"\n :style=\"{\n backgroundColor: content.settings.backgroundColor,\n ...(darkMode ? { filter: 'invert(1) hue-rotate(180deg)' } : {}),\n transition: 'filter 300ms ease',\n }\"\n />\n <!-- CONTENT COLUMN — the email content at its true width, centered in the\n stage. A full-width section fills this column but not the surrounding\n gutters, so the global background remains visible beside it. -->\n <div\n data-testid=\"canvas-wrapper\"\n role=\"region\"\n :aria-label=\"t.landmarks.canvas\"\n class=\"tpl-canvas-wrapper tpl:relative\"\n :style=\"{\n width: `${viewportWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <div\n class=\"tpl-canvas tpl:relative tpl:rounded-lg\"\n :class=\"{\n 'tpl-canvas--dark-mode': darkMode,\n 'tpl-preview-mode': previewMode,\n }\"\n :style=\"canvasStyle\"\n @click=\"handleCanvasClick\"\n >\n <!-- Preview resolution. The skeleton shows only on a *first* resolve —\n `isInitialResolve` — so switching recipient or reopening keeps the\n last good preview on screen instead of flashing over it. -->\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 tpl:p-6\"\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 still renders below, so\n the preview degrades rather than disappearing. -->\n <p\n v-else-if=\"previewResolution?.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mx-6 tpl:mt-4 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 <VueDraggable\n v-show=\"!previewResolution?.isInitialResolve.value\"\n v-model=\"blocks\"\n group=\"blocks\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-block-btn\"\n :invert-swap=\"true\"\n :inverted-swap-threshold=\"0.65\"\n :disabled=\"previewMode\"\n :draggable=\"'.tpl-block-item'\"\n :force-fallback=\"true\"\n :class=\"[\n 'tpl-canvas-blocks',\n isEmptyCanvas\n ? 'tpl-canvas-empty tpl:m-6 tpl:flex tpl:min-h-[400px] tpl:flex-col tpl:items-center tpl:justify-center tpl:rounded-xl tpl:border-2 tpl:border-dashed tpl:px-10 tpl:py-12 tpl:text-center tpl:bg-[var(--tpl-bg-elevated)] tpl:font-[var(--tpl-font-family)] tpl:transition-colors'\n : '',\n isEmptyCanvas && isDragOverEmpty\n ? 'tpl-canvas-empty--drag-over tpl:border-[var(--tpl-primary-hover)] tpl:bg-[var(--tpl-primary-light)]'\n : '',\n isEmptyCanvas && !isDragOverEmpty\n ? 'tpl:border-[var(--tpl-primary)]'\n : '',\n ]\"\n @dragenter=\"handleEmptyDragEnter\"\n @dragleave=\"handleEmptyDragLeave\"\n @drop=\"handleEmptyDrop\"\n >\n <!-- Empty-state content: rendered INSIDE the draggable so the whole\n dashed box is the drop zone, but excluded from sortable items via\n the `:draggable=\"'.tpl-block-item'\"` selector above — only\n `.tpl-block-item` children are sortable, this isn't. -->\n <div\n v-if=\"isEmptyCanvas\"\n class=\"tpl-canvas-empty-content tpl:flex tpl:flex-col tpl:items-center\"\n >\n <div\n class=\"tpl-canvas-empty-icon tpl:mb-4 tpl:text-[var(--tpl-primary)]\"\n >\n <SquarePlus :size=\"48\" :stroke-width=\"1\" />\n </div>\n <p\n class=\"tpl-canvas-empty-title tpl:m-0 tpl:mb-2 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-primary)]\"\n >\n {{ t.canvas.noBlocks }}\n </p>\n <p\n class=\"tpl-canvas-empty-text tpl:m-0 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.dragHint }}\n </p>\n <p\n v-if=\"canUseAiChat && cloudT\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintChat }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-ai-chat')\"\n >\n <Sparkles :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.aiAssistant }}\n </button>\n {{ t.canvas.aiHintChatSuffix }}\n </p>\n <p\n v-if=\"canUseDesignToTemplate && cloudT\"\n class=\"tpl:m-0 tpl:mt-4 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintDesign }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-design-reference')\"\n >\n <ImageUp :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.designToTemplate }}\n </button>\n {{ t.canvas.aiHintDesignSuffix }}\n </p>\n </div>\n <div\n v-for=\"block in blocks\"\n :key=\"block.id\"\n class=\"tpl-block-item\"\n v-show=\"\n appliesConditionFilter === false ||\n !conditionPreview?.isHidden(block.id)\n \"\n >\n <div class=\"tpl:relative\">\n <!-- Collaboration lock overlay -->\n <div\n v-if=\"getBlockLock(block.id)\"\n class=\"tpl-collab-lock tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-[4] tpl:rounded-sm\"\n :style=\"{\n outline: `2px solid ${getBlockLock(block.id)!.color}`,\n outlineOffset: '-1px',\n }\"\n >\n <span\n class=\"tpl:absolute tpl:-top-0.5 tpl:left-1/2 tpl:z-[5] tpl:flex tpl:-translate-x-1/2 tpl:-translate-y-full tpl:items-center tpl:gap-1 tpl:rounded-full tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:whitespace-nowrap\"\n :style=\"{\n backgroundColor: getBlockLock(block.id)!.color,\n color: readableTextColor(getBlockLock(block.id)!.color),\n }\"\n >\n <span\n class=\"tpl:inline-flex tpl:size-3 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[8px] tpl:font-bold\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 30%,\n transparent\n );\n \"\n >\n {{ getBlockLock(block.id)!.name.charAt(0) }}\n </span>\n {{ getBlockLock(block.id)!.name }}\n </span>\n </div>\n <BlockWrapper\n :block=\"block\"\n :is-selected=\"\n !previewMode &&\n !isPicking &&\n selectedBlockId === block.id &&\n !getBlockLock(block.id)\n \"\n :picked=\"isPickedBlock(block.id)\"\n :viewport=\"viewport\"\n :preview-mode=\"previewMode\"\n @select=\"handleBlockSelect(block.id)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n @fetch-data=\"handleFetchData(block, $event)\"\n @update=\"\n (updates: Partial<Block>) =>\n editor.updateBlock(block.id, updates)\n \"\n />\n </BlockWrapper>\n </div>\n </div>\n </VueDraggable>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* Dark-mode preview: invert only top-level `.tpl-block-content`. Applying\n filter to every `.tpl-block-content` would compound at nested levels\n (sections wrapping child blocks) and cancel out — `:not(...)` restricts\n it to outermost wrappers so the inversion is single-pass. Block chrome\n (action bar, indicators, overlays) lives outside `.tpl-block-content`\n and is therefore never filtered — no counter-filter / toggle flicker. */\n.tpl-canvas--dark-mode :deep(.tpl-block-content:not(.tpl-block-content *)) {\n filter: invert(1) hue-rotate(180deg);\n}\n:deep(.tpl-block-content) {\n transition: filter 300ms ease;\n}\n\n/* Counter-invert images so they look normal inside the inverted block\n content layer (canonical CSS-filter dark-mode trick). */\n.tpl-canvas--dark-mode :deep(.tpl-block-content img) {\n filter: invert(1) hue-rotate(180deg);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { useCloudI18n } from \"../composables/useCloudI18n\";\nimport type {\n Block,\n Collaborator,\n CustomBlock as CustomBlockType,\n TemplateContent,\n ViewportSize,\n} from \"@templatical/types\";\nimport { ImageUp, Sparkles, SquarePlus } from \"@lucide/vue\";\nimport { computed, inject, provide, ref, type Component } from \"vue\";\nimport {\n EDITOR_KEY,\n APPLIES_CONDITION_FILTER_KEY,\n CONDITION_PREVIEW_KEY,\n BLOCK_REGISTRY_KEY,\n CAPABILITIES_KEY,\n MERGE_TAG_SAMPLE_MODE_KEY,\n PREVIEW_RESOLUTION_KEY,\n USE_MERGE_TAG_SAMPLES_KEY,\n requireInject,\n} from \"../keys\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport {\n getDocumentStyle,\n resolveBlockComponent,\n} from \"../utils/blockComponentResolver\";\nimport {\n EMAIL_FRAME_WIDTH_TRANSITION,\n EMAIL_GUTTER,\n getEmailFrameWidth,\n} from \"../utils/emailFrameWidth\";\nimport { readableTextColor } from \"../utils/readableTextColor\";\n\nimport BlockWrapper from \"./blocks/BlockWrapper.vue\";\nimport SectionBlock from \"./blocks/SectionBlock.vue\";\nimport TitleBlock from \"./blocks/TitleBlock.vue\";\nimport ParagraphBlock from \"./blocks/ParagraphBlock.vue\";\nimport ImageBlock from \"./blocks/ImageBlock.vue\";\nimport ButtonBlock from \"./blocks/ButtonBlock.vue\";\nimport DividerBlock from \"./blocks/DividerBlock.vue\";\nimport SpacerBlock from \"./blocks/SpacerBlock.vue\";\nimport HtmlBlock from \"./blocks/HtmlBlock.vue\";\nimport SocialIconsBlock from \"./blocks/SocialIconsBlock.vue\";\nimport MenuBlock from \"./blocks/MenuBlock.vue\";\nimport TableBlock from \"./blocks/TableBlock.vue\";\nimport CustomBlock from \"./blocks/CustomBlock.vue\";\nimport VideoBlock from \"./blocks/VideoBlock.vue\";\n\n// `countdown` is deliberately absent: it resolves from the block registry, which\n// `useEditorCore` populates with a lazy `defineAsyncComponent` so OSS bundles\n// don't carry a Cloud-only block. `resolveBlockComponent` checks the registry\n// first, so a static entry here would never be reached — but its static import\n// would still pull the module into the eager graph and defeat the laziness.\n// The other fallback maps (SectionBlock, BlockPreviewCanvas,\n// PreviewSectionBlock) omit it for the same reason.\nconst blockComponentMap: Record<string, Component> = {\n section: SectionBlock,\n title: TitleBlock,\n paragraph: ParagraphBlock,\n image: ImageBlock,\n button: ButtonBlock,\n divider: DividerBlock,\n spacer: SpacerBlock,\n html: HtmlBlock,\n social: SocialIconsBlock,\n menu: MenuBlock,\n table: TableBlock,\n video: VideoBlock,\n custom: CustomBlock,\n};\n\nconst props = defineProps<{\n viewport: ViewportSize;\n content: TemplateContent;\n selectedBlockId: string | null;\n darkMode: boolean;\n previewMode: boolean;\n lockedBlocks?: Map<string, Collaborator>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"select-block\", blockId: string | null): void;\n (e: \"open-ai-chat\"): void;\n (e: \"open-design-reference\"): void;\n}>();\n\nconst { t } = useI18n();\nconst { t: cloudT } = useCloudI18n();\n\nconst editor = requireInject(EDITOR_KEY, \"Canvas\");\nconst conditionPreview = inject(CONDITION_PREVIEW_KEY, null);\nconst blockRegistry = inject(BLOCK_REGISTRY_KEY, null);\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n/**\n * Merge-tag samples render on this canvas **only while preview mode is on**.\n * The `previewMode &&` is the load-bearing half: the canvas is the editing\n * surface, and substituted text in a block a user is about to edit would be\n * text they never wrote. Preview mode is already fully non-editing (clicks and\n * dragging are blocked above), so it is the one state where swapping the\n * displayed value is safe.\n */\nconst mergeTagSampleMode = inject(MERGE_TAG_SAMPLE_MODE_KEY, null);\n// Null in headless mounts and when no resolver is configured.\nconst previewResolution = inject(PREVIEW_RESOLUTION_KEY, null);\nprovide(\n USE_MERGE_TAG_SAMPLES_KEY,\n computed(\n () =>\n props.previewMode &&\n (mergeTagSampleMode?.value ?? false) &&\n // A configured resolver owns what the preview shows. Substituting samples\n // underneath it would mix invented values with real ones, and on the\n // failure path would contradict the note saying the *unresolved* template\n // is showing.\n !previewResolution?.isConfigured,\n ),\n);\n\nconst canUseAiChat = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"chat\") ?? false),\n);\nconst canUseDesignToTemplate = computed(\n () =>\n (caps.plan?.hasFeature(\"ai_generation\") ?? false) &&\n (caps.ai?.isFeatureEnabled(\"designToTemplate\") ?? false),\n);\n\n/**\n * Whether the hand-toggled display-condition filter applies here. Owned by\n * `useEditorCore` and injected rather than re-derived — see the key's comment.\n */\nconst appliesConditionFilter = inject(APPLIES_CONDITION_FILTER_KEY, null);\n\nconst blocks = computed({\n get: () => props.content.blocks,\n set: (value: Block[]) => {\n editor.setContent({\n ...props.content,\n blocks: value,\n });\n },\n});\n\n// Shared with the preview canvas and the save dialog's scaled rows, so all\n// three lay out against the same width.\nconst viewportWidth = computed(() =>\n getEmailFrameWidth(props.content.settings, props.viewport),\n);\n\n// The \"stage\" represents the email body: the content column plus a gutter of\n// email background on each side (#230 — without it a full-width section's own\n// background hides the global one entirely). Mirrors how `mj-body\n// background-color` renders in the gutters around the centered content when\n// the email is sent. The neutral work area beyond the stage is preserved so\n// editor chrome stays legible.\n//\n// `EMAIL_GUTTER` comes from the shared helper rather than a local constant: the\n// preview surfaces show the same band, and two copies drift into two different\n// answers for how much of the body background an email is supposed to show.\nconst stageWidth = computed(() => viewportWidth.value + EMAIL_GUTTER * 2);\n\n// Canvas dark mode preview: simulates how the email will appear in recipients'\n// dark-themed email clients. Uses CSS filter inversion — independent of the\n// editor UI theme (light/dark/auto) which is controlled via uiTheme config.\n// The email bg lives on the `.tpl-canvas-bg` sibling layer (so it can be\n// inverted by `filter` without trapping block chrome in a stacking context);\n// the `.tpl-canvas` itself stays transparent so the bg layer shows through.\n// Shared with every other surface that renders blocks (the saved-block\n// previews), so a block looks the same wherever it is drawn.\nconst canvasStyle = computed(() => getDocumentStyle(props.content.settings));\n\n// Empty canvas: the whole dashed placeholder IS the Sortable drop zone.\n// `isEmptyCanvas` toggles the styling + the inline empty-state content.\nconst isEmptyCanvas = computed(\n () => blocks.value.length === 0 && !props.previewMode,\n);\n\n// Highlight the empty drop zone while a block is being dragged over it.\n// Uses a counter to handle dragenter/leave bubbling from descendants —\n// raw event toggling would flicker as the pointer moves between children.\nconst dragEnterDepth = ref(0);\nconst isDragOverEmpty = computed(\n () => isEmptyCanvas.value && dragEnterDepth.value > 0,\n);\n\nfunction handleEmptyDragEnter(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value += 1;\n}\n\nfunction handleEmptyDragLeave(): void {\n if (!isEmptyCanvas.value) return;\n dragEnterDepth.value = Math.max(0, dragEnterDepth.value - 1);\n}\n\nfunction handleEmptyDrop(): void {\n dragEnterDepth.value = 0;\n}\n\nfunction handleCanvasClick(event: MouseEvent): void {\n if (props.previewMode) {\n return;\n }\n // During a pick session, a stray background click must not clear anything —\n // the session is left only via the bar's Save/Cancel or Escape. Deselecting\n // here would also be meaningless, since picking doesn't touch selection.\n if (caps.savedBlocks?.isPicking.value) {\n return;\n }\n if (event.target === event.currentTarget) {\n emit(\"select-block\", null);\n }\n}\n\nfunction getBlockComponent(block: Block): Component | null {\n return resolveBlockComponent(block, blockRegistry, blockComponentMap);\n}\n\nfunction getBlockLock(blockId: string): Collaborator | null {\n return props.lockedBlocks?.get(blockId) ?? null;\n}\n\n/**\n * A click on a block either toggles its pick (while a saved-blocks pick\n * session is running) or selects it. Preview mode and collaborator locks are\n * checked first, so a locked block can be neither selected nor picked.\n */\nfunction handleBlockSelect(blockId: string): void {\n if (props.previewMode || getBlockLock(blockId)) return;\n const savedBlocks = caps.savedBlocks;\n if (savedBlocks?.isPicking.value) {\n savedBlocks.togglePick(blockId);\n return;\n }\n emit(\"select-block\", blockId);\n}\n\nfunction isPickedBlock(blockId: string): boolean {\n return caps.savedBlocks?.isPicked(blockId) ?? false;\n}\n\n/** True while a pick session runs — swaps block chrome into picking behaviour. */\nconst isPicking = computed(() => caps.savedBlocks?.isPicking.value === true);\n\nfunction handleFetchData(\n block: Block,\n payload: {\n fieldValues: Record<string, unknown>;\n dataSourceFetched: boolean;\n },\n): void {\n if (block.type !== \"custom\") {\n return;\n }\n\n editor.updateBlock(block.id, {\n fieldValues: payload.fieldValues,\n dataSourceFetched: payload.dataSourceFetched,\n } as Partial<CustomBlockType>);\n}\n</script>\n\n<template>\n <!-- STAGE — the email body. Sized to the content column plus a gutter of\n email background on each side, so the global background stays visible\n around full-width sections, mirroring how `mj-body background-color`\n renders in the gutters when sent (#230). The neutral work area beyond\n the stage is preserved so the canvas still reads as a floating card and\n editor chrome stays legible. -->\n <div\n class=\"tpl-canvas-stage tpl:relative tpl:flex tpl:justify-center tpl:rounded-lg\"\n :style=\"{\n width: `${stageWidth}px`,\n boxShadow: darkMode ? 'none' : 'var(--tpl-shadow-xl)',\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <!-- Background layer — holds the email bg and the dark-preview filter,\n spanning the full stage (content + gutters). Filter on this leaf\n layer (no descendants) does not create a containing block / stacking\n context that would trap block chrome (action bar, indicators) inside\n the wrapper. The filter has its own transition so the canvas fades\n smoothly between modes. -->\n <div\n class=\"tpl-canvas-bg tpl:absolute tpl:inset-0 tpl:rounded-lg tpl:pointer-events-none\"\n :style=\"{\n backgroundColor: content.settings.backgroundColor,\n ...(darkMode ? { filter: 'invert(1) hue-rotate(180deg)' } : {}),\n transition: 'filter 300ms ease',\n }\"\n />\n <!-- CONTENT COLUMN — the email content at its true width, centered in the\n stage. A full-width section fills this column but not the surrounding\n gutters, so the global background remains visible beside it. -->\n <div\n data-testid=\"canvas-wrapper\"\n role=\"region\"\n :aria-label=\"t.landmarks.canvas\"\n class=\"tpl-canvas-wrapper tpl:relative\"\n :style=\"{\n width: `${viewportWidth}px`,\n transition: EMAIL_FRAME_WIDTH_TRANSITION,\n }\"\n >\n <div\n class=\"tpl-canvas tpl:relative tpl:rounded-lg\"\n :class=\"{\n 'tpl-canvas--dark-mode': darkMode,\n 'tpl-preview-mode': previewMode,\n }\"\n :style=\"canvasStyle\"\n @click=\"handleCanvasClick\"\n >\n <!-- Preview resolution. The skeleton shows only on a *first* resolve —\n `isInitialResolve` — so switching recipient or reopening keeps the\n last good preview on screen instead of flashing over it. -->\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 tpl:p-6\"\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 still renders below, so\n the preview degrades rather than disappearing. -->\n <p\n v-else-if=\"previewResolution?.hasFailed.value\"\n role=\"status\"\n data-testid=\"preview-resolution-failed\"\n class=\"tpl:mx-6 tpl:mt-4 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 <VueDraggable\n v-show=\"!previewResolution?.isInitialResolve.value\"\n v-model=\"blocks\"\n group=\"blocks\"\n :animation=\"150\"\n ghost-class=\"tpl-ghost\"\n drag-class=\"tpl-dragging\"\n handle=\".tpl-block-btn\"\n :invert-swap=\"true\"\n :inverted-swap-threshold=\"0.65\"\n :disabled=\"previewMode\"\n :draggable=\"'.tpl-block-item'\"\n :force-fallback=\"true\"\n :class=\"[\n 'tpl-canvas-blocks',\n isEmptyCanvas\n ? 'tpl-canvas-empty tpl:m-6 tpl:flex tpl:min-h-[400px] tpl:flex-col tpl:items-center tpl:justify-center tpl:rounded-xl tpl:border-2 tpl:border-dashed tpl:px-10 tpl:py-12 tpl:text-center tpl:bg-[var(--tpl-bg-elevated)] tpl:font-[var(--tpl-font-family)] tpl:transition-colors'\n : '',\n isEmptyCanvas && isDragOverEmpty\n ? 'tpl-canvas-empty--drag-over tpl:border-[var(--tpl-primary-hover)] tpl:bg-[var(--tpl-primary-light)]'\n : '',\n isEmptyCanvas && !isDragOverEmpty\n ? 'tpl:border-[var(--tpl-primary)]'\n : '',\n ]\"\n @dragenter=\"handleEmptyDragEnter\"\n @dragleave=\"handleEmptyDragLeave\"\n @drop=\"handleEmptyDrop\"\n >\n <!-- Empty-state content: rendered INSIDE the draggable so the whole\n dashed box is the drop zone, but excluded from sortable items via\n the `:draggable=\"'.tpl-block-item'\"` selector above — only\n `.tpl-block-item` children are sortable, this isn't. -->\n <div\n v-if=\"isEmptyCanvas\"\n class=\"tpl-canvas-empty-content tpl:flex tpl:flex-col tpl:items-center\"\n >\n <div\n class=\"tpl-canvas-empty-icon tpl:mb-4 tpl:text-[var(--tpl-primary)]\"\n >\n <SquarePlus :size=\"48\" :stroke-width=\"1\" />\n </div>\n <p\n class=\"tpl-canvas-empty-title tpl:m-0 tpl:mb-2 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-primary)]\"\n >\n {{ t.canvas.noBlocks }}\n </p>\n <p\n class=\"tpl-canvas-empty-text tpl:m-0 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.dragHint }}\n </p>\n <p\n v-if=\"canUseAiChat && cloudT\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintChat }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-ai-chat')\"\n >\n <Sparkles :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.aiAssistant }}\n </button>\n {{ t.canvas.aiHintChatSuffix }}\n </p>\n <p\n v-if=\"canUseDesignToTemplate && cloudT\"\n class=\"tpl:m-0 tpl:mt-4 tpl:flex tpl:flex-wrap tpl:items-center tpl:justify-center tpl:gap-x-1 tpl:gap-y-0.5 tpl:text-sm tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.canvas.aiHintDesign }}\n <button\n class=\"tpl:inline-flex tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:gap-1 tpl:whitespace-nowrap tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-0.5 tpl:text-sm tpl:font-semibold tpl:transition-colors tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary-hover)]\"\n @click=\"emit('open-design-reference')\"\n >\n <ImageUp :size=\"14\" :stroke-width=\"2\" />\n {{ cloudT.aiMenu.designToTemplate }}\n </button>\n {{ t.canvas.aiHintDesignSuffix }}\n </p>\n </div>\n <div\n v-for=\"block in blocks\"\n :key=\"block.id\"\n class=\"tpl-block-item\"\n v-show=\"\n appliesConditionFilter === false ||\n !conditionPreview?.isHidden(block.id)\n \"\n >\n <div class=\"tpl:relative\">\n <!-- Collaboration lock overlay -->\n <div\n v-if=\"getBlockLock(block.id)\"\n class=\"tpl-collab-lock tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-[4] tpl:rounded-sm\"\n :style=\"{\n outline: `2px solid ${getBlockLock(block.id)!.color}`,\n outlineOffset: '-1px',\n }\"\n >\n <span\n class=\"tpl:absolute tpl:-top-0.5 tpl:left-1/2 tpl:z-[5] tpl:flex tpl:-translate-x-1/2 tpl:-translate-y-full tpl:items-center tpl:gap-1 tpl:rounded-full tpl:px-2 tpl:py-0.5 tpl:text-[10px] tpl:font-medium tpl:whitespace-nowrap\"\n :style=\"{\n backgroundColor: getBlockLock(block.id)!.color,\n color: readableTextColor(getBlockLock(block.id)!.color),\n }\"\n >\n <span\n class=\"tpl:inline-flex tpl:size-3 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[8px] tpl:font-bold\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-bg) 30%,\n transparent\n );\n \"\n >\n {{ getBlockLock(block.id)!.name.charAt(0) }}\n </span>\n {{ getBlockLock(block.id)!.name }}\n </span>\n </div>\n <BlockWrapper\n :block=\"block\"\n :is-selected=\"\n !previewMode &&\n !isPicking &&\n selectedBlockId === block.id &&\n !getBlockLock(block.id)\n \"\n :picked=\"isPickedBlock(block.id)\"\n :viewport=\"viewport\"\n :preview-mode=\"previewMode\"\n @select=\"handleBlockSelect(block.id)\"\n >\n <component\n :is=\"getBlockComponent(block)\"\n :block=\"block\"\n :viewport=\"viewport\"\n @fetch-data=\"handleFetchData(block, $event)\"\n @update=\"\n (updates: Partial<Block>) =>\n editor.updateBlock(block.id, updates)\n \"\n />\n </BlockWrapper>\n </div>\n </div>\n </VueDraggable>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* Dark-mode preview: invert only top-level `.tpl-block-content`. Applying\n filter to every `.tpl-block-content` would compound at nested levels\n (sections wrapping child blocks) and cancel out — `:not(...)` restricts\n it to outermost wrappers so the inversion is single-pass. Block chrome\n (action bar, indicators, overlays) lives outside `.tpl-block-content`\n and is therefore never filtered — no counter-filter / toggle flicker. */\n.tpl-canvas--dark-mode :deep(.tpl-block-content:not(.tpl-block-content *)) {\n filter: invert(1) hue-rotate(180deg);\n}\n:deep(.tpl-block-content) {\n transition: filter 300ms ease;\n}\n\n/* Counter-invert images so they look normal inside the inverted block\n content layer (canonical CSS-filter dark-mode trick). */\n.tpl-canvas--dark-mode :deep(.tpl-block-content img) {\n filter: invert(1) hue-rotate(180deg);\n}\n</style>\n","<script lang=\"ts\">\nimport { defineComponent, h, inject, type ComputedRef } from \"vue\";\nimport { CUSTOM_BLOCK_STYLESHEETS_KEY } from \"../keys\";\n\n/**\n * Renders one `<style>` tag per unique custom-block definition stylesheet\n * currently in use by the editor's content. Mounted near the top of\n * `Editor.vue`'s root template so the rules sit early in the document order\n * and apply to the canvas.\n *\n * In shadow-DOM mode (`shadowDom: true`, default) the styles scope to the\n * editor's shadow root. In light-DOM mode they leak globally, matching the\n * existing behavior of the editor's published `dist/style.css` in that mode\n * — no new global-leak vector.\n *\n * Why a render function instead of `<template>`: Vue's template compiler\n * treats `<style>` inside `<template>` as a side-effect tag and refuses to\n * emit it in strict client-render compilation paths (notably the one used\n * by `vue-tsc` and Vitest's Vue plugin). The render function below\n * bypasses the template compiler entirely and produces the same DOM at\n * runtime — and `innerHTML` on a `<style>` element sets the CSS rules\n * directly, no escape/parse pass between authoring and the cssom.\n */\nexport default defineComponent({\n name: \"CustomBlockStylesheets\",\n setup() {\n const stylesheets = inject<ComputedRef<string[]> | null>(\n CUSTOM_BLOCK_STYLESHEETS_KEY,\n null,\n );\n\n return () => {\n if (!stylesheets) return null;\n return stylesheets.value.map((css, i) =>\n h(\"style\", {\n key: i,\n \"data-tpl-custom-block-stylesheet\": \"\",\n innerHTML: css,\n }),\n );\n };\n },\n});\n</script>\n","<script lang=\"ts\">\nimport { defineComponent, h, inject, type ComputedRef } from \"vue\";\nimport { CUSTOM_BLOCK_STYLESHEETS_KEY } from \"../keys\";\n\n/**\n * Renders one `<style>` tag per unique custom-block definition stylesheet\n * currently in use by the editor's content. Mounted near the top of\n * `Editor.vue`'s root template so the rules sit early in the document order\n * and apply to the canvas.\n *\n * In shadow-DOM mode (`shadowDom: true`, default) the styles scope to the\n * editor's shadow root. In light-DOM mode they leak globally, matching the\n * existing behavior of the editor's published `dist/style.css` in that mode\n * — no new global-leak vector.\n *\n * Why a render function instead of `<template>`: Vue's template compiler\n * treats `<style>` inside `<template>` as a side-effect tag and refuses to\n * emit it in strict client-render compilation paths (notably the one used\n * by `vue-tsc` and Vitest's Vue plugin). The render function below\n * bypasses the template compiler entirely and produces the same DOM at\n * runtime — and `innerHTML` on a `<style>` element sets the CSS rules\n * directly, no escape/parse pass between authoring and the cssom.\n */\nexport default defineComponent({\n name: \"CustomBlockStylesheets\",\n setup() {\n const stylesheets = inject<ComputedRef<string[]> | null>(\n CUSTOM_BLOCK_STYLESHEETS_KEY,\n null,\n );\n\n return () => {\n if (!stylesheets) return null;\n return stylesheets.value.map((css, i) =>\n h(\"style\", {\n key: i,\n \"data-tpl-custom-block-stylesheet\": \"\",\n innerHTML: css,\n }),\n );\n };\n },\n});\n</script>\n","/**\n * Filters and reorders a candidate list of block-palette items against a\n * consumer-supplied `paletteBlocks` allowlist.\n *\n * Each `paletteBlocks` entry is a block-type key that must match a candidate's\n * `type`: a bare built-in type (e.g. `\"image\"`) or a prefixed custom type\n * (e.g. `\"custom:qrcode\"`). The result's `items` contains exactly the\n * candidates named in `paletteBlocks`, in that order. Entries with no matching\n * candidate (a typo, an unregistered custom block, or a plan-gated built-in\n * like `countdown` outside Cloud) are collected into `unknown` so the caller\n * can warn and skip. A type listed more than once is rendered once.\n *\n * When `paletteBlocks` is `undefined` or empty the candidates are returned\n * unchanged (the default full palette). An empty allowlist is treated as \"not\n * configured\" rather than \"hide everything\", since a zero-block palette is\n * almost always a mistake.\n */\nexport function resolvePaletteBlocks<T extends { type: string }>(\n candidates: T[],\n paletteBlocks: string[] | undefined,\n): { items: T[]; unknown: string[] } {\n if (!paletteBlocks || paletteBlocks.length === 0) {\n return { items: candidates, unknown: [] };\n }\n\n const byType = new Map(candidates.map((c) => [c.type, c]));\n const items: T[] = [];\n const unknown: string[] = [];\n const seen = new Set<string>();\n\n for (const entry of paletteBlocks) {\n if (seen.has(entry)) continue;\n seen.add(entry);\n\n const candidate = byType.get(entry);\n if (candidate) {\n items.push(candidate);\n } else {\n unknown.push(entry);\n }\n }\n\n return { items, unknown };\n}\n","/**\n * Where a palette insert should land, expressed as the trailing arguments of\n * `editor.addBlock(block, targetSectionId?, columnIndex?, index?)`.\n *\n * An empty object means \"append at the end\" — every field absent, so spreading\n * it into `addBlock` passes `undefined` for all three and takes the push path.\n */\nexport interface InsertPosition {\n targetSectionId?: string;\n columnIndex?: number;\n index?: number;\n}\n\nexport interface ResolveInsertPositionOptions {\n /**\n * The type of block about to be inserted. Only `\"section\"` changes the\n * outcome — custom types (`custom:*`) behave like any other leaf block.\n */\n blockType: string;\n selectedBlockId: string | null;\n findBlockLocation: (blockId: string) => {\n targetSectionId?: string;\n columnIndex?: number;\n index: number;\n } | null;\n isBlockLocked: (blockId: string) => boolean;\n}\n\n/**\n * Resolve the insert position for a palette click: directly below the selected\n * block, falling back to appending at the end.\n *\n * Every fallback here exists because `addBlock` refuses the alternative\n * *silently*. Returning a position it rejects would turn the click into a\n * complete no-op — worse for the user than landing at the bottom, which is the\n * behaviour this whole function replaces. So the rule is: only ever return a\n * position `addBlock` will accept.\n *\n * Mirrors `duplicateBlock` in `@templatical/core` (resolve the reference\n * block's location, bump the index by one, append when it can't be resolved) so\n * click-to-insert and duplicate place blocks by the same rule.\n */\nexport function resolveInsertPosition(\n options: ResolveInsertPositionOptions,\n): InsertPosition {\n const { blockType, selectedBlockId, findBlockLocation, isBlockLocked } =\n options;\n\n if (!selectedBlockId) return {};\n\n // A selection can outlive its block — a collaborator removes it, an undo\n // drops it — so an unresolvable id means append, never a guessed index.\n const selected = findBlockLocation(selectedBlockId);\n if (!selected) return {};\n\n if (selected.targetSectionId === undefined) {\n return { index: selected.index + 1 };\n }\n\n if (blockType === \"section\") {\n // MJML forbids `mj-section` inside `mj-column`, so `addBlock` rejects the\n // nest outright. Land beside the section the selection sits in instead —\n // the nearest position that accepts a section at all. Sections never nest,\n // so the parent's own location is always top-level.\n const parent = findBlockLocation(selected.targetSectionId);\n if (!parent || parent.targetSectionId !== undefined) return {};\n return { index: parent.index + 1 };\n }\n\n // `addBlock` bails when the target section is locked, so a locked parent\n // means the column is unavailable — append rather than lose the block.\n if (isBlockLocked(selected.targetSectionId)) return {};\n\n return {\n targetSectionId: selected.targetSectionId,\n columnIndex: selected.columnIndex,\n index: selected.index + 1,\n };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { Block, BlockType } from \"@templatical/types\";\nimport { createBlock, createCustomBlock } from \"@templatical/types\";\nimport { Package } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport CustomBlockIcon from \"./CustomBlockIcon.vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport { resolvePaletteBlocks } from \"../utils/resolvePaletteBlocks\";\nimport { resolveInsertPosition } from \"../utils/resolveInsertPosition\";\nimport { useScrollToBlock } from \"../composables/useScrollToBlock\";\nimport { logger } from \"../utils/logger\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n PALETTE_BLOCKS_KEY,\n BLOCK_DEFAULTS_KEY,\n CAPABILITIES_KEY,\n EDITOR_KEY,\n} from \"../keys\";\n\ninterface BlockTypeItem {\n type: BlockType | string;\n label: string;\n isCustom?: boolean;\n icon?: string;\n}\n\nconst { t, format } = useI18n();\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\nconst paletteBlocks = inject(PALETTE_BLOCKS_KEY, undefined);\nconst blockDefaults = inject(BLOCK_DEFAULTS_KEY, undefined);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n// Availability only — deliberately NOT `count > 0`. Gating on the loaded count\n// meant the entry appeared only once the consumer's `list()` resolved, so a slow\n// endpoint shifted the rail mid-session and an empty library hid the feature\n// entirely. The list is now fetched when the browser opens instead.\nconst showSavedBlocksSection = computed(\n () => caps.savedBlocks?.isAvailable.value === true,\n);\n\nconst isExpanded = ref(false);\nconst isDragging = ref(false);\nconst railEl = ref<HTMLElement | null>(null);\n\n// `:focus-visible`, never `:focus`: a mouse click leaves the clicked palette\n// button focused, so a plain `:focus` test would pin the rail open from the\n// first click-to-insert onward — the very symptom this rail already had for\n// another reason (#568). Keyboard focus does keep it open, because collapsing\n// hides the label of the entry the user has focused.\nfunction hasKeyboardFocusInRail(): boolean {\n try {\n return railEl.value?.querySelector(\":focus-visible\") != null;\n } catch {\n // Firefox <85 / Safari <15.4 — reachable on the `shadowDom: false` tier,\n // whose browser floor predates the selector — throw on it. Reading that\n // as \"no keyboard focus\" keeps the collapse working there.\n return false;\n }\n}\n\nfunction handleSidebarLeave(): void {\n // Don't collapse while a Sortable drag is in progress. If the sidebar\n // shrinks (200px → 48px) DURING the drag's threshold-to-dragStarted\n // window, `dragEl.getBoundingClientRect()` returns a mid-transition\n // rect when Sortable's `_appendGhost` runs — the fallback ghost gets\n // stamped with that wrong rect and ends up visibly offset from the\n // cursor for the rest of the drag. Cleared on drag-end.\n if (isDragging.value) return;\n if (hasKeyboardFocusInRail()) return;\n isExpanded.value = false;\n}\n\nfunction handleDragChoose(): void {\n // `choose` fires synchronously inside Sortable's `_onTapStart` at\n // pointerdown, BEFORE any pointermove or mouseleave. `start` would be\n // too late — it fires only after the threshold-move dispatches\n // `_dragStarted`, by which time mouseleave may already have flipped\n // `isExpanded` and the sidebar may be mid-collapse.\n isDragging.value = true;\n}\n\nfunction handleDragUnchoose(): void {\n // `end` is not the counterpart of `choose` — Sortable gates it on\n // `Sortable.active`, which only `_dragStarted` sets, so a click on a palette\n // entry emits `choose` + `unchoose` and no `end` at all. Clearing the guard\n // only in `handleDragEnd` therefore latches it true on the first click and\n // `handleSidebarLeave` bails forever after: the rail stays expanded until\n // some later drag happens to complete (#568). `unchoose` fires on every\n // release, at drop time — long after `_appendGhost` captured its rect — so\n // it cannot re-open the race `handleDragChoose` guards.\n isDragging.value = false;\n}\n\nfunction handleDragEnd(): void {\n isDragging.value = false;\n // Collapse the rail on drop. A drag ends with the cursor out in the canvas,\n // so no `mouseleave` fires to collapse it — and `handleSidebarLeave` had\n // bailed out earlier because `isDragging` was true. Without this the rail\n // stays expanded until the next hover-in/hover-out cycle.\n isExpanded.value = false;\n}\n\nconst builtInBlockTypeOrder: string[] = [\n \"section\",\n \"image\",\n \"title\",\n \"paragraph\",\n \"button\",\n \"divider\",\n \"video\",\n \"social\",\n \"menu\",\n \"table\",\n \"spacer\",\n \"html\",\n];\n\nconst builtInBlockTypes = computed<BlockTypeItem[]>(() => {\n const types: BlockTypeItem[] = builtInBlockTypeOrder.map((type) => ({\n type,\n label: getBlockTypeLabel(type, t),\n }));\n\n // Countdown requires Templatical Cloud for server-side GIF rendering\n if (caps.plan) {\n types.splice(-1, 0, {\n type: \"countdown\",\n label: getBlockTypeLabel(\"countdown\", t),\n });\n }\n\n return types;\n});\n\nconst customBlockItems = computed<BlockTypeItem[]>(() => {\n return customBlockDefinitions.map((def) => ({\n type: `custom:${def.type}`,\n label: def.name,\n isCustom: true,\n icon: def.icon,\n }));\n});\n\nconst candidateBlockTypes = computed<BlockTypeItem[]>(() => [\n ...builtInBlockTypes.value,\n ...customBlockItems.value,\n]);\n\n// Apply the consumer's `paletteBlocks` allowlist + order when provided; otherwise show\n// the full default palette. Filtering only affects the palette — the block\n// registry stays complete, so existing content using a hidden type still renders.\nconst resolvedPalette = computed(() =>\n resolvePaletteBlocks(candidateBlockTypes.value, paletteBlocks),\n);\n\nconst blockTypes = computed<BlockTypeItem[]>(() => resolvedPalette.value.items);\n\n// Warn once per unknown `paletteBlocks` entry (a typo, an unregistered custom block, or\n// `countdown` outside a Cloud plan). Done in a watcher rather than inside the\n// computed so recomputes don't repeat the warning.\nconst warnedPaletteEntries = new Set<string>();\nwatch(\n () => resolvedPalette.value.unknown,\n (unknown) => {\n for (const entry of unknown) {\n if (warnedPaletteEntries.has(entry)) continue;\n warnedPaletteEntries.add(entry);\n logger.warn(\n `config.paletteBlocks: \"${entry}\" is not a built-in or registered custom block ` +\n `(use the \"custom:\" prefix for custom blocks) — skipping it in the palette.`,\n );\n }\n },\n { immediate: true },\n);\n\nfunction createBlockFromItem(item: BlockTypeItem): Block {\n if (item.isCustom) {\n const customType = item.type.replace(\"custom:\", \"\");\n const definition = customBlockDefinitions.find(\n (d) => d.type === customType,\n );\n if (definition) {\n return createCustomBlock(definition);\n }\n }\n\n return createBlock(item.type as BlockType, blockDefaults);\n}\n\nfunction insertBlockFromItem(item: BlockTypeItem): void {\n if (!editor) return;\n const block = createBlockFromItem(item);\n // Land below the selection, not at the end: on a template taller than the\n // viewport an appended block is below the fold, and the canvas does not\n // follow on its own, so the click reads as a no-op (issue #568).\n const { targetSectionId, columnIndex, index } = resolveInsertPosition({\n blockType: item.type,\n selectedBlockId: editor.state.selectedBlockId,\n findBlockLocation: editor.findBlockLocation,\n isBlockLocked: editor.isBlockLocked,\n });\n editor.addBlock(block, targetSectionId, columnIndex, index);\n editor.selectBlock(block.id);\n // Selecting is not visible by itself, and the append fallback above still\n // lands off-screen — the canvas has to follow either way.\n scrollToBlock(block.id);\n}\n\nfunction handlePaletteKeydown(event: KeyboardEvent, item: BlockTypeItem): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n insertBlockFromItem(item);\n }\n}\n</script>\n\n<template>\n <aside\n ref=\"railEl\"\n :aria-label=\"t.sidebarNav.palette\"\n class=\"tpl-sidebar-rail tpl:absolute tpl:top-14 tpl:bottom-0 tpl:left-0 tpl:z-40 tpl:flex tpl:flex-col tpl:overflow-hidden\"\n :style=\"{\n width: isExpanded ? '200px' : '48px',\n backgroundColor: 'var(--tpl-bg-elevated)',\n borderRight: '1px solid var(--tpl-border)',\n boxShadow: isExpanded ? 'var(--tpl-shadow-lg)' : 'none',\n transition:\n 'width 200ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 200ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n @mouseenter=\"isExpanded = true\"\n @mouseleave=\"handleSidebarLeave\"\n @focusin=\"isExpanded = true\"\n @focusout=\"isExpanded = false\"\n >\n <!-- Saved blocks browser trigger. Needs the feature to be available (a\n provider configured, and in Cloud the plan to allow it). -->\n <div\n v-if=\"showSavedBlocksSection\"\n class=\"tpl:border-b tpl:px-1 tpl:pb-1 tpl:border-[var(--tpl-border)]\"\n >\n <button\n type=\"button\"\n :aria-label=\"t.sidebarNav.browseSavedBlocks\"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)]\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"caps.savedBlocks?.openBrowser()\"\n >\n <Package :size=\"20\" :stroke-width=\"1.5\" class=\"tpl:shrink-0\" />\n <!-- No `flex-1`: the rail inherits `text-align: center`, so a label\n stretched to fill the row centres its glyphs and sits visibly\n further right than the palette labels below it. Shrink-to-fit\n matches them exactly. -->\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ t.savedBlocks.title }}\n </span>\n </button>\n </div>\n <!-- The palette is the rail's scroll region: `flex-1` makes it fill the\n height left by the (optional) modules trigger, `min-h-0` lets it\n shrink below its content so `overflow-y-auto` engages. Without this\n the rail's `overflow-hidden` clips items below the fold on short\n viewports with no way to reach them (#231). -->\n <VueDraggable\n :model-value=\"blockTypes\"\n :group=\"{ name: 'blocks', pull: 'clone', put: false }\"\n :clone=\"createBlockFromItem\"\n :sort=\"false\"\n :animation=\"150\"\n :force-fallback=\"true\"\n class=\"tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-0.5 tpl:overflow-y-auto tpl:p-1\"\n @choose=\"handleDragChoose\"\n @unchoose=\"handleDragUnchoose\"\n @end=\"handleDragEnd\"\n >\n <button\n v-for=\"blockType in blockTypes\"\n :key=\"blockType.type\"\n type=\"button\"\n :data-palette-type=\"blockType.type\"\n :aria-label=\"\n format(t.sidebarNav.insertBlock, { block: blockType.label })\n \"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:shrink-0 tpl:cursor-grab tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:active:cursor-grabbing\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"insertBlockFromItem(blockType)\"\n @keydown=\"handlePaletteKeydown($event, blockType)\"\n >\n <div\n class=\"tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:transition-transform tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:scale-105\"\n >\n <component\n :is=\"blockTypeIcons[blockType.type]\"\n v-if=\"blockTypeIcons[blockType.type]\"\n :size=\"20\"\n :stroke-width=\"1.5\"\n />\n <CustomBlockIcon\n v-else-if=\"blockType.isCustom\"\n :icon=\"blockType.icon\"\n :size=\"20\"\n />\n </div>\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ blockType.label }}\n </span>\n </button>\n </VueDraggable>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { Block, BlockType } from \"@templatical/types\";\nimport { createBlock, createCustomBlock } from \"@templatical/types\";\nimport { Package } from \"@lucide/vue\";\nimport { computed, inject, ref, watch } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport CustomBlockIcon from \"./CustomBlockIcon.vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport { resolvePaletteBlocks } from \"../utils/resolvePaletteBlocks\";\nimport { resolveInsertPosition } from \"../utils/resolveInsertPosition\";\nimport { useScrollToBlock } from \"../composables/useScrollToBlock\";\nimport { logger } from \"../utils/logger\";\nimport {\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n PALETTE_BLOCKS_KEY,\n BLOCK_DEFAULTS_KEY,\n CAPABILITIES_KEY,\n EDITOR_KEY,\n} from \"../keys\";\n\ninterface BlockTypeItem {\n type: BlockType | string;\n label: string;\n isCustom?: boolean;\n icon?: string;\n}\n\nconst { t, format } = useI18n();\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\nconst paletteBlocks = inject(PALETTE_BLOCKS_KEY, undefined);\nconst blockDefaults = inject(BLOCK_DEFAULTS_KEY, undefined);\nconst editor = inject(EDITOR_KEY, null);\nconst scrollToBlock = useScrollToBlock();\n\nconst caps = inject(CAPABILITIES_KEY, {});\n\n// Availability only — deliberately NOT `count > 0`. Gating on the loaded count\n// meant the entry appeared only once the consumer's `list()` resolved, so a slow\n// endpoint shifted the rail mid-session and an empty library hid the feature\n// entirely. The list is now fetched when the browser opens instead.\nconst showSavedBlocksSection = computed(\n () => caps.savedBlocks?.isAvailable.value === true,\n);\n\nconst isExpanded = ref(false);\nconst isDragging = ref(false);\nconst railEl = ref<HTMLElement | null>(null);\n\n// `:focus-visible`, never `:focus`: a mouse click leaves the clicked palette\n// button focused, so a plain `:focus` test would pin the rail open from the\n// first click-to-insert onward — the very symptom this rail already had for\n// another reason (#568). Keyboard focus does keep it open, because collapsing\n// hides the label of the entry the user has focused.\nfunction hasKeyboardFocusInRail(): boolean {\n try {\n return railEl.value?.querySelector(\":focus-visible\") != null;\n } catch {\n // Firefox <85 / Safari <15.4 — reachable on the `shadowDom: false` tier,\n // whose browser floor predates the selector — throw on it. Reading that\n // as \"no keyboard focus\" keeps the collapse working there.\n return false;\n }\n}\n\nfunction handleSidebarLeave(): void {\n // Don't collapse while a Sortable drag is in progress. If the sidebar\n // shrinks (200px → 48px) DURING the drag's threshold-to-dragStarted\n // window, `dragEl.getBoundingClientRect()` returns a mid-transition\n // rect when Sortable's `_appendGhost` runs — the fallback ghost gets\n // stamped with that wrong rect and ends up visibly offset from the\n // cursor for the rest of the drag. Cleared on drag-end.\n if (isDragging.value) return;\n if (hasKeyboardFocusInRail()) return;\n isExpanded.value = false;\n}\n\nfunction handleDragChoose(): void {\n // `choose` fires synchronously inside Sortable's `_onTapStart` at\n // pointerdown, BEFORE any pointermove or mouseleave. `start` would be\n // too late — it fires only after the threshold-move dispatches\n // `_dragStarted`, by which time mouseleave may already have flipped\n // `isExpanded` and the sidebar may be mid-collapse.\n isDragging.value = true;\n}\n\nfunction handleDragUnchoose(): void {\n // `end` is not the counterpart of `choose` — Sortable gates it on\n // `Sortable.active`, which only `_dragStarted` sets, so a click on a palette\n // entry emits `choose` + `unchoose` and no `end` at all. Clearing the guard\n // only in `handleDragEnd` therefore latches it true on the first click and\n // `handleSidebarLeave` bails forever after: the rail stays expanded until\n // some later drag happens to complete (#568). `unchoose` fires on every\n // release, at drop time — long after `_appendGhost` captured its rect — so\n // it cannot re-open the race `handleDragChoose` guards.\n isDragging.value = false;\n}\n\nfunction handleDragEnd(): void {\n isDragging.value = false;\n // Collapse the rail on drop. A drag ends with the cursor out in the canvas,\n // so no `mouseleave` fires to collapse it — and `handleSidebarLeave` had\n // bailed out earlier because `isDragging` was true. Without this the rail\n // stays expanded until the next hover-in/hover-out cycle.\n isExpanded.value = false;\n}\n\nconst builtInBlockTypeOrder: string[] = [\n \"section\",\n \"image\",\n \"title\",\n \"paragraph\",\n \"button\",\n \"divider\",\n \"video\",\n \"social\",\n \"menu\",\n \"table\",\n \"spacer\",\n \"html\",\n];\n\nconst builtInBlockTypes = computed<BlockTypeItem[]>(() => {\n const types: BlockTypeItem[] = builtInBlockTypeOrder.map((type) => ({\n type,\n label: getBlockTypeLabel(type, t),\n }));\n\n // Countdown requires Templatical Cloud for server-side GIF rendering\n if (caps.plan) {\n types.splice(-1, 0, {\n type: \"countdown\",\n label: getBlockTypeLabel(\"countdown\", t),\n });\n }\n\n return types;\n});\n\nconst customBlockItems = computed<BlockTypeItem[]>(() => {\n return customBlockDefinitions.map((def) => ({\n type: `custom:${def.type}`,\n label: def.name,\n isCustom: true,\n icon: def.icon,\n }));\n});\n\nconst candidateBlockTypes = computed<BlockTypeItem[]>(() => [\n ...builtInBlockTypes.value,\n ...customBlockItems.value,\n]);\n\n// Apply the consumer's `paletteBlocks` allowlist + order when provided; otherwise show\n// the full default palette. Filtering only affects the palette — the block\n// registry stays complete, so existing content using a hidden type still renders.\nconst resolvedPalette = computed(() =>\n resolvePaletteBlocks(candidateBlockTypes.value, paletteBlocks),\n);\n\nconst blockTypes = computed<BlockTypeItem[]>(() => resolvedPalette.value.items);\n\n// Warn once per unknown `paletteBlocks` entry (a typo, an unregistered custom block, or\n// `countdown` outside a Cloud plan). Done in a watcher rather than inside the\n// computed so recomputes don't repeat the warning.\nconst warnedPaletteEntries = new Set<string>();\nwatch(\n () => resolvedPalette.value.unknown,\n (unknown) => {\n for (const entry of unknown) {\n if (warnedPaletteEntries.has(entry)) continue;\n warnedPaletteEntries.add(entry);\n logger.warn(\n `config.paletteBlocks: \"${entry}\" is not a built-in or registered custom block ` +\n `(use the \"custom:\" prefix for custom blocks) — skipping it in the palette.`,\n );\n }\n },\n { immediate: true },\n);\n\nfunction createBlockFromItem(item: BlockTypeItem): Block {\n if (item.isCustom) {\n const customType = item.type.replace(\"custom:\", \"\");\n const definition = customBlockDefinitions.find(\n (d) => d.type === customType,\n );\n if (definition) {\n return createCustomBlock(definition);\n }\n }\n\n return createBlock(item.type as BlockType, blockDefaults);\n}\n\nfunction insertBlockFromItem(item: BlockTypeItem): void {\n if (!editor) return;\n const block = createBlockFromItem(item);\n // Land below the selection, not at the end: on a template taller than the\n // viewport an appended block is below the fold, and the canvas does not\n // follow on its own, so the click reads as a no-op (issue #568).\n const { targetSectionId, columnIndex, index } = resolveInsertPosition({\n blockType: item.type,\n selectedBlockId: editor.state.selectedBlockId,\n findBlockLocation: editor.findBlockLocation,\n isBlockLocked: editor.isBlockLocked,\n });\n editor.addBlock(block, targetSectionId, columnIndex, index);\n editor.selectBlock(block.id);\n // Selecting is not visible by itself, and the append fallback above still\n // lands off-screen — the canvas has to follow either way.\n scrollToBlock(block.id);\n}\n\nfunction handlePaletteKeydown(event: KeyboardEvent, item: BlockTypeItem): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n insertBlockFromItem(item);\n }\n}\n</script>\n\n<template>\n <aside\n ref=\"railEl\"\n :aria-label=\"t.sidebarNav.palette\"\n class=\"tpl-sidebar-rail tpl:absolute tpl:top-14 tpl:bottom-0 tpl:left-0 tpl:z-40 tpl:flex tpl:flex-col tpl:overflow-hidden\"\n :style=\"{\n width: isExpanded ? '200px' : '48px',\n backgroundColor: 'var(--tpl-bg-elevated)',\n borderRight: '1px solid var(--tpl-border)',\n boxShadow: isExpanded ? 'var(--tpl-shadow-lg)' : 'none',\n transition:\n 'width 200ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 200ms cubic-bezier(0.16, 1, 0.3, 1)',\n }\"\n @mouseenter=\"isExpanded = true\"\n @mouseleave=\"handleSidebarLeave\"\n @focusin=\"isExpanded = true\"\n @focusout=\"isExpanded = false\"\n >\n <!-- Saved blocks browser trigger. Needs the feature to be available (a\n provider configured, and in Cloud the plan to allow it). -->\n <div\n v-if=\"showSavedBlocksSection\"\n class=\"tpl:border-b tpl:px-1 tpl:pb-1 tpl:border-[var(--tpl-border)]\"\n >\n <button\n type=\"button\"\n :aria-label=\"t.sidebarNav.browseSavedBlocks\"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)]\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"caps.savedBlocks?.openBrowser()\"\n >\n <Package :size=\"20\" :stroke-width=\"1.5\" class=\"tpl:shrink-0\" />\n <!-- No `flex-1`: the rail inherits `text-align: center`, so a label\n stretched to fill the row centres its glyphs and sits visibly\n further right than the palette labels below it. Shrink-to-fit\n matches them exactly. -->\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ t.savedBlocks.title }}\n </span>\n </button>\n </div>\n <!-- The palette is the rail's scroll region: `flex-1` makes it fill the\n height left by the (optional) modules trigger, `min-h-0` lets it\n shrink below its content so `overflow-y-auto` engages. Without this\n the rail's `overflow-hidden` clips items below the fold on short\n viewports with no way to reach them (#231). -->\n <VueDraggable\n :model-value=\"blockTypes\"\n :group=\"{ name: 'blocks', pull: 'clone', put: false }\"\n :clone=\"createBlockFromItem\"\n :sort=\"false\"\n :animation=\"150\"\n :force-fallback=\"true\"\n class=\"tpl:flex tpl:min-h-0 tpl:flex-1 tpl:flex-col tpl:gap-0.5 tpl:overflow-y-auto tpl:p-1\"\n @choose=\"handleDragChoose\"\n @unchoose=\"handleDragUnchoose\"\n @end=\"handleDragEnd\"\n >\n <button\n v-for=\"blockType in blockTypes\"\n :key=\"blockType.type\"\n type=\"button\"\n :data-palette-type=\"blockType.type\"\n :aria-label=\"\n format(t.sidebarNav.insertBlock, { block: blockType.label })\n \"\n class=\"tpl:flex tpl:h-10 tpl:w-full tpl:shrink-0 tpl:cursor-grab tpl:items-center tpl:gap-3 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:px-3 tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:active:cursor-grabbing\"\n :style=\"{\n justifyContent: isExpanded ? 'flex-start' : 'center',\n }\"\n @click=\"insertBlockFromItem(blockType)\"\n @keydown=\"handlePaletteKeydown($event, blockType)\"\n >\n <div\n class=\"tpl:flex tpl:shrink-0 tpl:items-center tpl:justify-center tpl:transition-transform tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:scale-105\"\n >\n <component\n :is=\"blockTypeIcons[blockType.type]\"\n v-if=\"blockTypeIcons[blockType.type]\"\n :size=\"20\"\n :stroke-width=\"1.5\"\n />\n <CustomBlockIcon\n v-else-if=\"blockType.isCustom\"\n :icon=\"blockType.icon\"\n :size=\"20\"\n />\n </div>\n <span\n v-if=\"isExpanded\"\n class=\"tpl:truncate tpl:text-sm tpl:font-medium\"\n >\n {{ blockType.label }}\n </span>\n </button>\n </VueDraggable>\n </aside>\n</template>\n","/**\n * Result of a merge-tag trigger scan — the caret sits inside an open tag.\n */\nexport interface OpenMergeTagTrigger {\n /** Index in the field value where the trigger opener starts. */\n triggerStart: number;\n /** Caret index the scan ran at (also the end of `query`). */\n caret: number;\n /** Text typed between the opener and the caret — the autocomplete filter. */\n query: string;\n}\n\n/**\n * Detects whether the caret sits inside an *open* merge-tag trigger: the user\n * has typed the syntax opener (e.g. `{{`) and is mid-typing the tag name with\n * no closing delimiter yet. Returns the opener position + query so the caller\n * can drive the autocomplete popup, or `null` when the caret is not inside an\n * open trigger.\n *\n * Mirrors `@tiptap/suggestion`'s behavior with `allowSpaces: false`: the run\n * between the opener and the caret ends at the first whitespace, and a closing\n * delimiter inside that run means the tag is already complete (`{{first_name}}`)\n * so the popup must not reappear over it.\n */\nexport function findOpenMergeTagTrigger(\n value: string,\n caret: number,\n triggerChar: string,\n closingChar: string | null,\n): OpenMergeTagTrigger | null {\n if (!triggerChar) return null;\n\n const before = value.slice(0, caret);\n const triggerStart = before.lastIndexOf(triggerChar);\n if (triggerStart === -1) return null;\n\n const query = before.slice(triggerStart + triggerChar.length);\n\n // Whitespace ends the query (allowSpaces: false parity).\n if (/\\s/.test(query)) return null;\n\n // A closing delimiter between the opener and the caret means the tag is\n // already complete — don't reopen the popup over a finished tag.\n if (closingChar && query.includes(closingChar)) return null;\n\n return { triggerStart, caret, query };\n}\n","/**\n * Viewport-space caret geometry for `<input>` / `<textarea>` elements.\n *\n * Native text controls expose no caret coordinates (unlike ProseMirror, which\n * hands the merge-tag popup a `clientRect()`). The standard workaround is a\n * hidden \"mirror\" element that clones the field's text metrics; the caret's\n * pixel position is read from a marker span placed at the caret offset. This\n * lets the input/textarea autocomplete anchor its popup at the caret exactly\n * like the rich-text editor does.\n *\n * Adapted from component/textarea-caret-position (MIT). Returns a viewport\n * `DOMRect` (same coordinate space as `Range.getBoundingClientRect`) so the\n * shared merge-tag popup controller positions it identically to the TipTap path.\n */\n\n// Computed-style properties copied onto the mirror so its text wraps and\n// measures exactly like the source field.\nconst MIRRORED_PROPERTIES = [\n \"boxSizing\",\n \"width\",\n \"borderTopWidth\",\n \"borderRightWidth\",\n \"borderBottomWidth\",\n \"borderLeftWidth\",\n \"borderStyle\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"fontStyle\",\n \"fontVariant\",\n \"fontWeight\",\n \"fontStretch\",\n \"fontSize\",\n \"lineHeight\",\n \"fontFamily\",\n \"textAlign\",\n \"textTransform\",\n \"textIndent\",\n \"letterSpacing\",\n \"wordSpacing\",\n \"tabSize\",\n \"whiteSpace\",\n \"wordWrap\",\n \"wordBreak\",\n] as const;\n\nfunction toRect(left: number, top: number, bottom: number): DOMRect {\n const height = bottom - top;\n return {\n left,\n top,\n bottom,\n right: left,\n width: 0,\n height,\n x: left,\n y: top,\n toJSON: () => ({}),\n } as DOMRect;\n}\n\n/**\n * Returns the viewport rect of the caret at `position` in a text field, or\n * `null` when the field isn't laid out (detached / no view). The rect is a\n * zero-width vertical strip at the caret, one line tall — matching the shape\n * `Range.getBoundingClientRect()` produces for a collapsed selection.\n */\nexport function getCaretRect(\n el: HTMLInputElement | HTMLTextAreaElement,\n position: number,\n): DOMRect | null {\n const doc = el.ownerDocument;\n const win = doc.defaultView;\n if (!win) return null;\n\n const isInput = el.tagName === \"INPUT\";\n const style = win.getComputedStyle(el);\n\n const mirror = doc.createElement(\"div\");\n mirror.setAttribute(\"aria-hidden\", \"true\");\n const s = mirror.style;\n s.position = \"absolute\";\n s.visibility = \"hidden\";\n s.top = \"0\";\n s.left = \"0\";\n s.overflow = \"hidden\";\n s.whiteSpace = isInput ? \"pre\" : \"pre-wrap\";\n s.wordWrap = isInput ? \"normal\" : \"break-word\";\n const target = s as unknown as Record<string, string>;\n const source = style as unknown as Record<string, string>;\n for (const prop of MIRRORED_PROPERTIES) {\n target[prop] = source[prop];\n }\n // A single-line input never wraps; let the mirror grow with the text so the\n // marker's horizontal offset tracks the caret.\n if (isInput) {\n s.width = \"auto\";\n s.whiteSpace = \"pre\";\n }\n\n const value = el.value;\n let head = value.slice(0, position);\n if (isInput) {\n // <input> collapses whitespace runs to a single rendered space; replace\n // with non-breaking spaces so the mirror's width matches the field.\n head = head.replace(/\\s/g, \" \");\n }\n mirror.textContent = head;\n\n const marker = doc.createElement(\"span\");\n // Remaining text (or a placeholder) gives the marker the caret line's height.\n marker.textContent = value.slice(position) || \".\";\n mirror.appendChild(marker);\n\n doc.body.appendChild(mirror);\n const markerTop = marker.offsetTop;\n const markerLeft = marker.offsetLeft;\n doc.body.removeChild(mirror);\n\n const elRect = el.getBoundingClientRect();\n const borderTop = parseFloat(style.borderTopWidth) || 0;\n const borderLeft = parseFloat(style.borderLeftWidth) || 0;\n const lineHeight =\n parseFloat(style.lineHeight) || parseFloat(style.fontSize) || 0;\n\n const left = elRect.left + markerLeft + borderLeft - el.scrollLeft;\n\n // A single-line input vertically centers its text, which the block-flow\n // mirror can't replicate — anchor vertically to the field itself so the\n // popup sits directly under the input. Textareas use the measured caret line.\n if (isInput) {\n return toRect(left, elRect.top, elRect.bottom);\n }\n\n const top = elRect.top + markerTop + borderTop - el.scrollTop;\n return toRect(left, top, top + lineHeight);\n}\n","import {\n getSyntaxClosingChar,\n getSyntaxTriggerChar,\n type MergeTag,\n type SyntaxPreset,\n} from \"@templatical/types\";\nimport {\n getCurrentScope,\n inject,\n isRef,\n nextTick,\n onScopeDispose,\n type Ref,\n} from \"vue\";\nimport type { Translations } from \"../i18n\";\nimport { TRANSLATIONS_KEY } from \"../keys\";\nimport {\n createMergeTagPopup,\n filterMergeTags,\n} from \"../extensions/MergeTagSuggestion\";\nimport {\n findOpenMergeTagTrigger,\n type OpenMergeTagTrigger,\n} from \"../utils/mergeTagTrigger\";\nimport { getCaretRect } from \"../utils/textFieldCaret\";\nimport { usePopoverRoot } from \"./usePopoverRoot\";\n\nexport interface UseMergeTagAutocompleteOptions {\n /** The `<input>` / `<textarea>` the popup anchors to. */\n elementRef: Ref<HTMLInputElement | HTMLTextAreaElement | null>;\n /** Reads the current field value. */\n modelValue: () => string;\n /** Emits a new field value. */\n emit: (value: string) => void;\n /** Available merge tags (the autocomplete filter source). */\n mergeTags: MergeTag[];\n /** Resolved merge-tag syntax. */\n syntax: SyntaxPreset;\n /** Consumer's `mergeTags.autocomplete` flag. */\n enabled: boolean;\n /**\n * Invoked right before a picked tag is emitted, so the field can keep edit\n * mode and not flip to the chip display while the caret is still active.\n */\n onInsert?: () => void;\n}\n\nexport interface UseMergeTagAutocompleteReturn {\n /**\n * Whether autocomplete can ever fire. Mirrors the rich-text gate exactly:\n * enabled + a built-in trigger syntax + at least one tag to filter.\n */\n available: boolean;\n /** Recompute the popup after the value or caret changed. */\n refresh: () => void;\n /**\n * Field `keydown` handler. Returns `true` when the popup consumed the key\n * (the caller must then `preventDefault`); `false` to let the field handle it.\n */\n handleKeydown: (event: KeyboardEvent) => boolean;\n /** Close the popup (e.g. on blur). Idempotent. */\n close: () => void;\n /** Whether the popup is currently open. */\n isOpen: () => boolean;\n}\n\n// Horizontal caret moves take the caret out of the query run — dismiss the\n// popup (like navigating away in the rich-text editor) and let the caret move.\nconst CARET_MOVE_KEYS = new Set([\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"]);\n\n/**\n * Type-ahead merge-tag autocomplete for a native `<input>` / `<textarea>`.\n *\n * Drives the SAME popup controller as the rich-text (`@tiptap/suggestion`)\n * path — see `createMergeTagPopup` — so the popup's rendering, positioning,\n * keyboard behavior, and ARIA are identical across both surfaces. The only\n * field-specific work is here: detecting the open trigger in the raw string\n * (`findOpenMergeTagTrigger`), measuring the caret rect without ProseMirror\n * (`getCaretRect`), and replacing the typed fragment with the picked tag.\n */\nexport function useMergeTagAutocomplete(\n options: UseMergeTagAutocompleteOptions,\n): UseMergeTagAutocompleteReturn {\n const { elementRef, modelValue, emit, mergeTags, syntax, enabled, onInsert } =\n options;\n\n const triggerChar = getSyntaxTriggerChar(syntax);\n const closingChar = getSyntaxClosingChar(syntax);\n const available = enabled && triggerChar !== null && mergeTags.length > 0;\n\n // Resolve the empty-state label the same way the rich-text path does —\n // inject with a null default so headless callers (no translations provider)\n // don't throw, falling back to the English string.\n const injectedTranslations = inject(TRANSLATIONS_KEY, null) as\n Translations | Ref<Translations> | null;\n const resolvedTranslations = isRef(injectedTranslations)\n ? injectedTranslations.value\n : injectedTranslations;\n const emptyText =\n resolvedTranslations?.mergeTag?.suggestionEmpty ?? \"No matching merge tags\";\n\n const popoverRoot = usePopoverRoot();\n const popup = createMergeTagPopup(emptyText, popoverRoot);\n\n let disposed = false;\n if (getCurrentScope()) {\n onScopeDispose(() => {\n disposed = true;\n popup.close();\n });\n }\n\n function currentMatch(el: HTMLInputElement | HTMLTextAreaElement) {\n const value = el.value ?? \"\";\n const caret = el.selectionStart ?? value.length;\n return findOpenMergeTagTrigger(\n value,\n caret,\n triggerChar as string,\n closingChar,\n );\n }\n\n function applySelection(\n el: HTMLInputElement | HTMLTextAreaElement,\n match: OpenMergeTagTrigger,\n tag: MergeTag,\n ): void {\n const value = el.value ?? modelValue();\n // Absorb a matching closer sitting immediately after the caret so\n // `{{fir}}` → pick → `{{first_name}}`, never `{{first_name}}}}`.\n const afterCaret = value.slice(match.caret);\n const trailing =\n closingChar && afterCaret.startsWith(closingChar)\n ? closingChar.length\n : 0;\n const before = value.slice(0, match.triggerStart);\n const after = value.slice(match.caret + trailing);\n const newValue = before + tag.value + after;\n const newPos = before.length + tag.value.length;\n\n // Keep the field in edit mode BEFORE emitting: once the value gains a tag,\n // `hasMergeTags` flips true, and without this the field would swap to the\n // chip display and unmount the input mid-insert.\n onInsert?.();\n emit(newValue);\n popup.close();\n\n nextTick(() => {\n if (disposed) return;\n const node = elementRef.value;\n node?.focus();\n node?.setSelectionRange(newPos, newPos);\n });\n }\n\n function refresh(): void {\n if (!available || disposed) return;\n const el = elementRef.value;\n if (!el) {\n popup.close();\n return;\n }\n const match = currentMatch(el);\n if (!match) {\n popup.close();\n return;\n }\n\n const items = filterMergeTags(mergeTags, match.query);\n const getRect = () => getCaretRect(el, match.triggerStart);\n const onCommand = (tag: MergeTag) => applySelection(el, match, tag);\n\n if (popup.isOpen()) {\n popup.update({ items, getRect, onCommand });\n } else {\n popup.open({ items, getRect, anchorEl: el, onCommand });\n }\n }\n\n function handleKeydown(event: KeyboardEvent): boolean {\n if (!popup.isOpen()) return false;\n if (event.key === \"Escape\") {\n popup.close();\n return true;\n }\n if (CARET_MOVE_KEYS.has(event.key)) {\n popup.close();\n return false;\n }\n return popup.handleKeyDown(event);\n }\n\n return {\n available,\n refresh,\n handleKeydown,\n close: () => popup.close(),\n isOpen: () => popup.isOpen(),\n };\n}\n","import {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n} from \"@templatical/types\";\nimport {\n computed,\n getCurrentScope,\n nextTick,\n onScopeDispose,\n ref,\n type ComputedRef,\n type Ref,\n} from \"vue\";\nimport { useMergeTag } from \"./useMergeTag\";\nimport { useLogicTag } from \"./useLogicTag\";\nimport { useMergeTagAutocomplete } from \"./useMergeTagAutocomplete\";\n\nexport type MergeTagSegment =\n | { type: \"text\"; value: string }\n | { type: \"mergeTag\"; value: string; label: string }\n | { type: \"logicMergeTag\"; value: string; keyword: string };\n\nexport interface UseMergeTagFieldOptions {\n modelValue: () => string;\n emit: (value: string) => void;\n elementRef: Ref<HTMLInputElement | HTMLTextAreaElement | null>;\n}\n\nexport interface UseMergeTagFieldReturn {\n segments: ComputedRef<MergeTagSegment[]>;\n hasMergeTags: ComputedRef<boolean>;\n canRequestMergeTag: boolean;\n isRequestingMergeTag: Ref<boolean>;\n canInsertLogicTag: boolean;\n isRequestingLogicTag: Ref<boolean>;\n isEditing: Ref<boolean>;\n startEditing: () => void;\n stopEditing: () => void;\n handleInput: (event: Event) => void;\n handleKeydown: (event: KeyboardEvent) => void;\n handleClick: () => void;\n handleBlur: () => void;\n clearValue: () => void;\n insertMergeTag: () => Promise<void>;\n insertLogicTag: () => Promise<void>;\n /** Whether type-ahead autocomplete is active for this field. */\n autocompleteAvailable: boolean;\n}\n\nexport function useMergeTagField(\n options: UseMergeTagFieldOptions,\n): UseMergeTagFieldReturn {\n const { modelValue, emit, elementRef } = options;\n\n const {\n mergeTags,\n canRequestMergeTag,\n isRequesting: isRequestingMergeTag,\n isMergeTagValue,\n getMergeTagLabel,\n requestMergeTag,\n syntax,\n autocomplete,\n } = useMergeTag();\n\n const {\n canInsertLogicTag,\n isRequesting: isRequestingLogicTag,\n requestLogicTag,\n } = useLogicTag();\n\n const isEditing = ref(false);\n let insertingMergeTag = false;\n let disposed = false;\n if (getCurrentScope()) {\n onScopeDispose(() => {\n disposed = true;\n });\n }\n\n // Type-ahead autocomplete for the raw <input>/<textarea>, sharing the exact\n // popup used by the rich-text editor. `onInsert` keeps the field in edit mode\n // so the freshly inserted tag stays visible instead of flipping to chips.\n const typeahead = useMergeTagAutocomplete({\n elementRef,\n modelValue,\n emit,\n mergeTags,\n syntax,\n enabled: autocomplete,\n onInsert: () => {\n isEditing.value = true;\n },\n });\n\n const segments = computed((): MergeTagSegment[] => {\n const val = modelValue();\n if (!val) return [];\n\n const result: MergeTagSegment[] = [];\n const combinedSource = `(${syntax.value.source}|${syntax.logic.source})`;\n const regex = new RegExp(combinedSource, \"g\");\n let lastIndex = 0;\n let match;\n\n while ((match = regex.exec(val)) !== null) {\n if (match.index > lastIndex) {\n result.push({\n type: \"text\",\n value: val.slice(lastIndex, match.index),\n });\n }\n\n const matched = match[0];\n if (isMergeTagValue(matched)) {\n result.push({\n type: \"mergeTag\",\n value: matched,\n label: getMergeTagLabel(matched),\n });\n } else if (isLogicMergeTagValue(matched, syntax)) {\n result.push({\n type: \"logicMergeTag\",\n value: matched,\n keyword: getLogicMergeTagKeyword(matched, syntax),\n });\n } else {\n result.push({ type: \"text\", value: matched });\n }\n\n lastIndex = match.index + matched.length;\n }\n\n if (lastIndex < val.length) {\n result.push({ type: \"text\", value: val.slice(lastIndex) });\n }\n\n return result;\n });\n\n const hasMergeTags = computed(() =>\n segments.value.some(\n (s) => s.type === \"mergeTag\" || s.type === \"logicMergeTag\",\n ),\n );\n\n function startEditing(): void {\n isEditing.value = true;\n nextTick(() => {\n elementRef.value?.focus();\n const len = modelValue()?.length || 0;\n elementRef.value?.setSelectionRange(len, len);\n });\n }\n\n function stopEditing(): void {\n if (insertingMergeTag) return;\n isEditing.value = false;\n }\n\n function handleInput(event: Event): void {\n emit((event.target as HTMLInputElement | HTMLTextAreaElement).value);\n typeahead.refresh();\n }\n\n /**\n * Field keydown: let the autocomplete popup consume navigation/selection\n * keys first (Arrow/Enter/Tab/Escape). If it doesn't, Escape falls through\n * to leaving edit mode — preserving the prior `@keydown.escape` behavior.\n */\n function handleKeydown(event: KeyboardEvent): void {\n if (typeahead.handleKeydown(event)) {\n event.preventDefault();\n return;\n }\n if (event.key === \"Escape\") {\n stopEditing();\n }\n }\n\n /** A click may move the caret into or out of a trigger — re-evaluate. */\n function handleClick(): void {\n typeahead.refresh();\n }\n\n /** Blur closes the popup, then leaves edit mode. */\n function handleBlur(): void {\n typeahead.close();\n stopEditing();\n }\n\n function clearValue(): void {\n emit(\"\");\n }\n\n async function insertMergeTag(): Promise<void> {\n const cursorPos =\n isEditing.value && elementRef.value\n ? (elementRef.value.selectionStart ?? modelValue().length)\n : modelValue().length;\n\n insertingMergeTag = true;\n let mergeTag: Awaited<ReturnType<typeof requestMergeTag>>;\n try {\n mergeTag = await requestMergeTag();\n } finally {\n insertingMergeTag = false;\n }\n\n if (disposed) return;\n\n if (mergeTag) {\n const before = modelValue().slice(0, cursorPos);\n const after = modelValue().slice(cursorPos);\n const newValue = before + mergeTag.value + after;\n emit(newValue);\n\n isEditing.value = true;\n nextTick(() => {\n if (disposed) return;\n const newPos = cursorPos + mergeTag.value.length;\n elementRef.value?.focus();\n elementRef.value?.setSelectionRange(newPos, newPos);\n });\n }\n }\n\n async function insertLogicTag(): Promise<void> {\n // Read the selection straight from the element (kept focused via the\n // button's mousedown.prevent). Null only in display mode → append at end.\n const el = elementRef.value;\n const start = el?.selectionStart ?? modelValue().length;\n const end = el?.selectionEnd ?? start;\n\n insertingMergeTag = true;\n let item: Awaited<ReturnType<typeof requestLogicTag>>;\n try {\n item = await requestLogicTag();\n } finally {\n insertingMergeTag = false;\n }\n\n if (disposed || !item) return;\n\n const val = modelValue();\n let newValue: string;\n let newPos: number;\n if (\"before\" in item) {\n // Pair: wrap the selection with before/after. With no selection\n // (start === end) the caret lands between them so the user types the\n // wrapped content — mirrors the rich-text behavior.\n newValue =\n val.slice(0, start) +\n item.before +\n val.slice(start, end) +\n item.after +\n val.slice(end);\n newPos = start + item.before.length + (end - start);\n } else {\n // Tag: insert at the cursor.\n newValue = val.slice(0, start) + item.value + val.slice(start);\n newPos = start + item.value.length;\n }\n emit(newValue);\n\n isEditing.value = true;\n nextTick(() => {\n if (disposed) return;\n elementRef.value?.focus();\n elementRef.value?.setSelectionRange(newPos, newPos);\n });\n }\n\n return {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n stopEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n autocompleteAvailable: typeahead.available,\n };\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { MergeTagSegment } from \"../composables/useMergeTagField\";\nimport { X } from \"@lucide/vue\";\n\ndefineProps<{\n segments: MergeTagSegment[];\n displayClass: string;\n pulse?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"edit\"): void;\n (e: \"clear\"): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction onEdit(): void {\n emit(\"edit\");\n}\n</script>\n\n<template>\n <div\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.clickToEdit\"\n :class=\"[displayClass, { 'tpl-pulse-fill': pulse }]\"\n @click=\"onEdit\"\n @keydown.enter=\"onEdit\"\n @keydown.space.prevent=\"onEdit\"\n >\n <template\n v-for=\"(seg, i) in segments\"\n :key=\"`${seg.type}-${i}-${seg.value}`\"\n >\n <span\n v-if=\"seg.type === 'mergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-primary) 20%,\n transparent\n );\n color: var(--tpl-primary);\n \"\n >\n {{ seg.label }}\n </span>\n <span\n v-else-if=\"seg.type === 'logicMergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: transparent;\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n >\n {{ seg.keyword }}\n </span>\n <span v-else class=\"tpl:text-sm tpl:text-[var(--tpl-text)]\">{{\n seg.value\n }}</span>\n </template>\n <button\n type=\"button\"\n class=\"tpl:ml-auto tpl:flex tpl:size-6 tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-[var(--tpl-text-dim)] tpl:opacity-60 tpl:transition-all tpl:hover:text-[var(--tpl-danger)] tpl:hover:opacity-100\"\n :aria-label=\"t.mergeTag.remove\"\n :title=\"t.mergeTag.remove\"\n @click.stop=\"emit('clear')\"\n >\n <X :size=\"12\" :stroke-width=\"2.5\" />\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport type { MergeTagSegment } from \"../composables/useMergeTagField\";\nimport { X } from \"@lucide/vue\";\n\ndefineProps<{\n segments: MergeTagSegment[];\n displayClass: string;\n pulse?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"edit\"): void;\n (e: \"clear\"): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction onEdit(): void {\n emit(\"edit\");\n}\n</script>\n\n<template>\n <div\n role=\"button\"\n tabindex=\"0\"\n :aria-label=\"t.mergeTag.clickToEdit\"\n :class=\"[displayClass, { 'tpl-pulse-fill': pulse }]\"\n @click=\"onEdit\"\n @keydown.enter=\"onEdit\"\n @keydown.space.prevent=\"onEdit\"\n >\n <template\n v-for=\"(seg, i) in segments\"\n :key=\"`${seg.type}-${i}-${seg.value}`\"\n >\n <span\n v-if=\"seg.type === 'mergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:gap-1 tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.9em] tpl:font-medium\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-primary) 20%,\n transparent\n );\n color: var(--tpl-primary);\n \"\n >\n {{ seg.label }}\n </span>\n <span\n v-else-if=\"seg.type === 'logicMergeTag'\"\n class=\"tpl-tooltip tpl:inline-flex tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[0.8em] tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n :data-tooltip=\"seg.value\"\n style=\"\n background-color: transparent;\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n >\n {{ seg.keyword }}\n </span>\n <span v-else class=\"tpl:text-sm tpl:text-[var(--tpl-text)]\">{{\n seg.value\n }}</span>\n </template>\n <button\n type=\"button\"\n class=\"tpl:ml-auto tpl:flex tpl:size-6 tpl:shrink-0 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-full tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-[var(--tpl-text-dim)] tpl:opacity-60 tpl:transition-all tpl:hover:text-[var(--tpl-danger)] tpl:hover:opacity-100\"\n :aria-label=\"t.mergeTag.remove\"\n :title=\"t.mergeTag.remove\"\n @click.stop=\"emit('clear')\"\n >\n <X :size=\"12\" :stroke-width=\"2.5\" />\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { ScanLine } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTagBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[mergeTagBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n :disabled=\"disabled\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <ScanLine :size=\"12\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { ScanLine } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst mergeTagBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[mergeTagBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.mergeTag.insert\"\n :title=\"t.mergeTag.insert\"\n :disabled=\"disabled\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <ScanLine :size=\"12\" :stroke-width=\"2\" />\n {{ t.mergeTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Braces } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst logicBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[logicBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n :disabled=\"disabled\"\n data-testid=\"field-insert-logic-button\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <Braces :size=\"12\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Braces } from \"@lucide/vue\";\n\ndefineProps<{\n disabled?: boolean;\n}>();\n\ndefineEmits<{\n (e: \"insert\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst logicBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:px-2 tpl:py-1 tpl:text-xs tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:duration-[120ms] tpl:hover:bg-[var(--tpl-primary-light)] tpl:hover:text-[var(--tpl-primary)] tpl:hover:border-[var(--tpl-primary)]\";\n</script>\n\n<template>\n <button\n type=\"button\"\n :class=\"[logicBtnClass, 'tpl:mt-1.5']\"\n :aria-label=\"t.logicTag.insert\"\n :title=\"t.logicTag.insert\"\n :disabled=\"disabled\"\n data-testid=\"field-insert-logic-button\"\n @mousedown.prevent\n @click=\"$emit('insert')\"\n >\n <Braces :size=\"12\" :stroke-width=\"2\" />\n {{ t.logicTag.insertShort }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n placeholder?: string;\n rows?: number;\n }>(),\n {\n placeholder: \"\",\n rows: 3,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: textareaRef,\n});\n\nconst textareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:placeholder:text-[var(--tpl-text-dim)] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-20 tpl:cursor-pointer tpl:items-start tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <textarea\n ref=\"textareaRef\"\n :class=\"textareaClass\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n placeholder?: string;\n rows?: number;\n }>(),\n {\n placeholder: \"\",\n rows: 3,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: textareaRef,\n});\n\nconst textareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:placeholder:text-[var(--tpl-text-dim)] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-20 tpl:cursor-pointer tpl:items-start tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <textarea\n ref=\"textareaRef\"\n :class=\"textareaClass\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"./ColorPicker.vue\";\nimport MergeTagTextarea from \"./MergeTagTextarea.vue\";\nimport ToggleSwitch from \"./ToggleSwitch.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type { TemplateSettings } from \"@templatical/types\";\nimport {\n cardClass,\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_BG_COLOR,\n} from \"../constants/styleConstants\";\nimport { Circle, Eye, Globe, Info, Square } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { FONTS_MANAGER_KEY, requireInject } from \"../keys\";\n\nconst props = defineProps<{\n settings: TemplateSettings;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst PREHEADER_MAX_LENGTH = 150;\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"TemplateSettings\");\nconst fontFamilies = computed(() => fontsManager.fonts.value);\n\n// If current font is not in available list (e.g., custom font when disabled), use default\nconst displayedFontFamily = computed(() => {\n const isAvailable = fontFamilies.value.some(\n (font) => font.value === props.settings.fontFamily,\n );\n return isAvailable\n ? props.settings.fontFamily\n : fontsManager.defaultFont.value;\n});\n\nconst widthPresets = [\n { value: 480, label: \"480px\" },\n { value: 600, label: \"600px\" },\n { value: 700, label: \"700px\" },\n { value: 800, label: \"800px\" },\n];\n</script>\n\n<template>\n <aside\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:gap-3 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Layout card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Square\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.layout }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{\n t.templateSettings.widthPreset\n }}</label>\n <div\n class=\"tpl:grid tpl:grid-cols-4 tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n v-for=\"preset in widthPresets\"\n :key=\"preset.value\"\n class=\"tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :style=\"{\n backgroundColor:\n settings.width === preset.value\n ? 'var(--tpl-bg)'\n : 'transparent',\n color:\n settings.width === preset.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow:\n settings.width === preset.value\n ? 'var(--tpl-shadow)'\n : 'none',\n }\"\n @click=\"emit('update', { width: preset.value })\"\n >\n {{ preset.label }}\n </button>\n </div>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.customWidth\n }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"settings.width\"\n min=\"300\"\n max=\"900\"\n @input=\"\n emit('update', {\n width: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n\n <!-- Appearance card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Circle\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.appearance }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\" data-testid=\"template-settings-background\">\n <label :class=\"labelClass\">{{\n t.templateSettings.backgroundColor\n }}</label>\n <ColorPicker\n :model-value=\"settings.backgroundColor\"\n :placeholder=\"DEFAULT_BG_COLOR\"\n @update:model-value=\"emit('update', { backgroundColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.textColor }}</label>\n <ColorPicker\n :model-value=\"settings.textColor ?? ''\"\n seed-color=\"#1a1a1a\"\n @update:model-value=\"emit('update', { textColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.linkColor }}</label>\n <ColorPicker\n :model-value=\"settings.linkColor ?? ''\"\n seed-color=\"#0066cc\"\n @update:model-value=\"\n emit('update', { linkColor: $event || undefined })\n \"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"settings.linkUnderline\"\n :label=\"t.templateSettings.linkUnderline\"\n @update:model-value=\"emit('update', { linkUnderline: $event })\"\n />\n </div>\n\n <div>\n <label :class=\"labelClass\">{{ t.templateSettings.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"displayedFontFamily\"\n @change=\"\n emit('update', {\n fontFamily: ($event.target as HTMLSelectElement).value,\n })\n \"\n >\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n </div>\n\n <!-- Language card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Globe\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.language }}</span>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.contentLocale\n }}</label>\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"settings.locale ?? ''\"\n placeholder=\"en\"\n spellcheck=\"false\"\n autocapitalize=\"off\"\n autocomplete=\"off\"\n @input=\"\n emit('update', {\n locale:\n ($event.target as HTMLInputElement).value.trim() || undefined,\n })\n \"\n />\n <p\n class=\"tpl:mt-1 tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.contentLocaleHint }}\n </p>\n </div>\n </div>\n\n <!-- Preheader card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Eye\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.preheaderText }}</span>\n </div>\n\n <div>\n <MergeTagTextarea\n :model-value=\"settings.preheaderText ?? ''\"\n :placeholder=\"t.templateSettings.preheaderTextPlaceholder\"\n :rows=\"2\"\n @update:model-value=\"\n emit('update', {\n preheaderText: $event.replace(/[\\r\\n]/g, ' ') || undefined,\n })\n \"\n />\n <div\n class=\"tpl:mt-1 tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.preheaderTextHint }}\n </span>\n <span\n class=\"tpl:shrink-0 tpl:text-xs tpl:tabular-nums tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ (settings.preheaderText ?? \"\").length }}/{{\n PREHEADER_MAX_LENGTH\n }}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Tips card -->\n <div\n class=\"tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <div\n class=\"tpl:mb-2.5 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text-muted)]\"\n >\n <Info :size=\"14\" :stroke-width=\"2\" />\n <span>{{ t.templateSettings.tips }}</span>\n </div>\n <ul\n class=\"tpl:m-0 tpl:pl-[18px] tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip1 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip2 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip3 }}\n </li>\n </ul>\n </div>\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"./ColorPicker.vue\";\nimport MergeTagTextarea from \"./MergeTagTextarea.vue\";\nimport ToggleSwitch from \"./ToggleSwitch.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type { TemplateSettings } from \"@templatical/types\";\nimport {\n cardClass,\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_BG_COLOR,\n} from \"../constants/styleConstants\";\nimport { Circle, Eye, Globe, Info, Square } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { FONTS_MANAGER_KEY, requireInject } from \"../keys\";\n\nconst props = defineProps<{\n settings: TemplateSettings;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst PREHEADER_MAX_LENGTH = 150;\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"TemplateSettings\");\nconst fontFamilies = computed(() => fontsManager.fonts.value);\n\n// If current font is not in available list (e.g., custom font when disabled), use default\nconst displayedFontFamily = computed(() => {\n const isAvailable = fontFamilies.value.some(\n (font) => font.value === props.settings.fontFamily,\n );\n return isAvailable\n ? props.settings.fontFamily\n : fontsManager.defaultFont.value;\n});\n\nconst widthPresets = [\n { value: 480, label: \"480px\" },\n { value: 600, label: \"600px\" },\n { value: 700, label: \"700px\" },\n { value: 800, label: \"800px\" },\n];\n</script>\n\n<template>\n <aside\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:gap-3 tpl:overflow-y-auto tpl:p-4\"\n >\n <!-- Layout card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Square\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.layout }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{\n t.templateSettings.widthPreset\n }}</label>\n <div\n class=\"tpl:grid tpl:grid-cols-4 tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:p-1 tpl:bg-[var(--tpl-bg-hover)]\"\n >\n <button\n v-for=\"preset in widthPresets\"\n :key=\"preset.value\"\n class=\"tpl:cursor-pointer tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-2 tpl:py-1.5 tpl:text-sm tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :style=\"{\n backgroundColor:\n settings.width === preset.value\n ? 'var(--tpl-bg)'\n : 'transparent',\n color:\n settings.width === preset.value\n ? 'var(--tpl-primary)'\n : 'var(--tpl-text-muted)',\n boxShadow:\n settings.width === preset.value\n ? 'var(--tpl-shadow)'\n : 'none',\n }\"\n @click=\"emit('update', { width: preset.value })\"\n >\n {{ preset.label }}\n </button>\n </div>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.customWidth\n }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"settings.width\"\n min=\"300\"\n max=\"900\"\n @input=\"\n emit('update', {\n width: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n\n <!-- Appearance card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Circle\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.appearance }}</span>\n </div>\n\n <div class=\"tpl:mb-3.5\" data-testid=\"template-settings-background\">\n <label :class=\"labelClass\">{{\n t.templateSettings.backgroundColor\n }}</label>\n <ColorPicker\n :model-value=\"settings.backgroundColor\"\n :placeholder=\"DEFAULT_BG_COLOR\"\n @update:model-value=\"emit('update', { backgroundColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.textColor }}</label>\n <ColorPicker\n :model-value=\"settings.textColor ?? ''\"\n seed-color=\"#1a1a1a\"\n @update:model-value=\"emit('update', { textColor: $event })\"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.templateSettings.linkColor }}</label>\n <ColorPicker\n :model-value=\"settings.linkColor ?? ''\"\n seed-color=\"#0066cc\"\n @update:model-value=\"\n emit('update', { linkColor: $event || undefined })\n \"\n />\n </div>\n\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"settings.linkUnderline\"\n :label=\"t.templateSettings.linkUnderline\"\n @update:model-value=\"emit('update', { linkUnderline: $event })\"\n />\n </div>\n\n <div>\n <label :class=\"labelClass\">{{ t.templateSettings.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"displayedFontFamily\"\n @change=\"\n emit('update', {\n fontFamily: ($event.target as HTMLSelectElement).value,\n })\n \"\n >\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n </div>\n\n <!-- Language card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Globe\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.language }}</span>\n </div>\n\n <div>\n <label :class=\"labelClass\">{{\n t.templateSettings.contentLocale\n }}</label>\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"settings.locale ?? ''\"\n placeholder=\"en\"\n spellcheck=\"false\"\n autocapitalize=\"off\"\n autocomplete=\"off\"\n @input=\"\n emit('update', {\n locale:\n ($event.target as HTMLInputElement).value.trim() || undefined,\n })\n \"\n />\n <p\n class=\"tpl:mt-1 tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.contentLocaleHint }}\n </p>\n </div>\n </div>\n\n <!-- Preheader card -->\n <div :class=\"cardClass\">\n <div\n class=\"tpl:mb-3.5 tpl:flex tpl:items-center tpl:gap-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n <Eye\n class=\"tpl:text-[var(--tpl-text-muted)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <span>{{ t.templateSettings.preheaderText }}</span>\n </div>\n\n <div>\n <MergeTagTextarea\n :model-value=\"settings.preheaderText ?? ''\"\n :placeholder=\"t.templateSettings.preheaderTextPlaceholder\"\n :rows=\"2\"\n @update:model-value=\"\n emit('update', {\n preheaderText: $event.replace(/[\\r\\n]/g, ' ') || undefined,\n })\n \"\n />\n <div\n class=\"tpl:mt-1 tpl:flex tpl:items-start tpl:justify-between tpl:gap-2\"\n >\n <span\n class=\"tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.templateSettings.preheaderTextHint }}\n </span>\n <span\n class=\"tpl:shrink-0 tpl:text-xs tpl:tabular-nums tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ (settings.preheaderText ?? \"\").length }}/{{\n PREHEADER_MAX_LENGTH\n }}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Tips card -->\n <div\n class=\"tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:p-3\"\n >\n <div\n class=\"tpl:mb-2.5 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text-muted)]\"\n >\n <Info :size=\"14\" :stroke-width=\"2\" />\n <span>{{ t.templateSettings.tips }}</span>\n </div>\n <ul\n class=\"tpl:m-0 tpl:pl-[18px] tpl:text-xs tpl:leading-relaxed tpl:text-[var(--tpl-text-dim)]\"\n >\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip1 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip2 }}\n </li>\n <li class=\"tpl:mb-1 tpl:last:mb-0\">\n {{ t.templateSettings.tip3 }}\n </li>\n </ul>\n </div>\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport { inputClass } from \"../constants/styleConstants\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n type?: \"text\" | \"url\";\n placeholder?: string;\n pulse?: boolean;\n }>(),\n {\n type: \"text\",\n placeholder: \"\",\n pulse: false,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: inputRef,\n});\n\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-10 tpl:cursor-pointer tpl:items-center tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3.5 tpl:py-1.5 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n :pulse=\"pulse\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <input\n ref=\"inputRef\"\n :type=\"type\"\n :class=\"[inputClass, { 'tpl-pulse-fill': pulse }]\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.tpl-pulse-fill {\n animation: tpl-field-pulse 1s ease-out;\n}\n\n@keyframes tpl-field-pulse {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--tpl-warning) 30%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 6%, transparent);\n }\n 40% {\n box-shadow: 0 0 0 3px\n color-mix(in srgb, var(--tpl-warning) 15%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 5%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tpl-pulse-fill {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useMergeTagField } from \"../composables/useMergeTagField\";\nimport { inputClass } from \"../constants/styleConstants\";\nimport MergeTagSegments from \"./MergeTagSegments.vue\";\nimport MergeTagInsertButton from \"./MergeTagInsertButton.vue\";\nimport LogicTagInsertButton from \"./LogicTagInsertButton.vue\";\nimport { ref } from \"vue\";\n\nconst props = withDefaults(\n defineProps<{\n modelValue: string;\n type?: \"text\" | \"url\";\n placeholder?: string;\n pulse?: boolean;\n }>(),\n {\n type: \"text\",\n placeholder: \"\",\n pulse: false,\n },\n);\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nconst {\n segments,\n hasMergeTags,\n canRequestMergeTag,\n isRequestingMergeTag,\n canInsertLogicTag,\n isRequestingLogicTag,\n isEditing,\n startEditing,\n handleInput,\n handleKeydown,\n handleClick,\n handleBlur,\n clearValue,\n insertMergeTag,\n insertLogicTag,\n} = useMergeTagField({\n modelValue: () => props.modelValue,\n emit: (value) => emit(\"update:modelValue\", value),\n elementRef: inputRef,\n});\n\nconst displayClass =\n \"tpl:flex tpl:w-full tpl:min-h-10 tpl:cursor-pointer tpl:items-center tpl:flex-wrap tpl:gap-1 tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:shadow-[var(--tpl-shadow-sm)] tpl:bg-[var(--tpl-bg)] tpl:border-[var(--tpl-border)] tpl:px-3.5 tpl:py-1.5 tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\";\n</script>\n\n<template>\n <div v-if=\"hasMergeTags && !isEditing\">\n <MergeTagSegments\n :segments=\"segments\"\n :display-class=\"displayClass\"\n :pulse=\"pulse\"\n @edit=\"startEditing\"\n @clear=\"clearValue\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n <div v-else>\n <input\n ref=\"inputRef\"\n :type=\"type\"\n :class=\"[inputClass, { 'tpl-pulse-fill': pulse }]\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n @input=\"handleInput\"\n @keydown=\"handleKeydown\"\n @click=\"handleClick\"\n @blur=\"handleBlur\"\n />\n <div\n v-if=\"canRequestMergeTag || canInsertLogicTag\"\n class=\"tpl:flex tpl:flex-wrap tpl:gap-1.5\"\n >\n <MergeTagInsertButton\n v-if=\"canRequestMergeTag\"\n :disabled=\"isRequestingMergeTag\"\n @insert=\"insertMergeTag\"\n />\n <LogicTagInsertButton\n v-if=\"canInsertLogicTag\"\n :disabled=\"isRequestingLogicTag\"\n @insert=\"insertLogicTag\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.tpl-pulse-fill {\n animation: tpl-field-pulse 1s ease-out;\n}\n\n@keyframes tpl-field-pulse {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--tpl-warning) 30%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 6%, transparent);\n }\n 40% {\n box-shadow: 0 0 0 3px\n color-mix(in srgb, var(--tpl-warning) 15%, transparent);\n background-color: color-mix(in srgb, var(--tpl-warning) 5%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tpl-pulse-fill {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ButtonBlock } from \"@templatical/types\";\n\nconst props = defineProps<{\n block: ButtonBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ButtonBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst DEFAULT_CUSTOM_WIDTH = 200;\n\nconst widthMode = computed<\"auto\" | \"full\" | \"custom\">(() => {\n if (props.block.width === \"full\") return \"full\";\n if (typeof props.block.width === \"number\") return \"custom\";\n return \"auto\";\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ButtonBlock>);\n}\n\nfunction updateWidthMode(mode: string): void {\n if (mode === \"full\") {\n updateField(\"width\", \"full\");\n return;\n }\n if (mode === \"custom\") {\n updateField(\n \"width\",\n typeof props.block.width === \"number\"\n ? props.block.width\n : DEFAULT_CUSTOM_WIDTH,\n );\n return;\n }\n updateField(\"width\", undefined);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render a 0px\n // button (#260). Ignore invalid values and keep the last valid width;\n // the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.button.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.text }}</label>\n <MergeTagInput\n :model-value=\"block.text\"\n type=\"text\"\n @update:model-value=\"updateField('text', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.url }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.button.urlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.button.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.background }}</label>\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.textColor }}</label>\n <ColorPicker\n :model-value=\"block.textColor\"\n @update:model-value=\"updateField('textColor', $event)\"\n />\n </div>\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'borderRadius',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"36\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.button.widthAuto }}</option>\n <option value=\"full\">{{ t.button.fullWidth }}</option>\n <option value=\"custom\">{{ t.button.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align ?? 'center'\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ButtonBlock } from \"@templatical/types\";\n\nconst props = defineProps<{\n block: ButtonBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ButtonBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst DEFAULT_CUSTOM_WIDTH = 200;\n\nconst widthMode = computed<\"auto\" | \"full\" | \"custom\">(() => {\n if (props.block.width === \"full\") return \"full\";\n if (typeof props.block.width === \"number\") return \"custom\";\n return \"auto\";\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ButtonBlock>);\n}\n\nfunction updateWidthMode(mode: string): void {\n if (mode === \"full\") {\n updateField(\"width\", \"full\");\n return;\n }\n if (mode === \"custom\") {\n updateField(\n \"width\",\n typeof props.block.width === \"number\"\n ? props.block.width\n : DEFAULT_CUSTOM_WIDTH,\n );\n return;\n }\n updateField(\"width\", undefined);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render a 0px\n // button (#260). Ignore invalid values and keep the last valid width;\n // the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.button.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.text }}</label>\n <MergeTagInput\n :model-value=\"block.text\"\n type=\"text\"\n @update:model-value=\"updateField('text', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.url }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.button.urlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.button.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.background }}</label>\n <ColorPicker\n :model-value=\"block.backgroundColor\"\n @update:model-value=\"updateField('backgroundColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.textColor }}</label>\n <ColorPicker\n :model-value=\"block.textColor\"\n @update:model-value=\"updateField('textColor', $event)\"\n />\n </div>\n <div class=\"tpl:grid tpl:grid-cols-2 tpl:gap-3\">\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'borderRadius',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"36\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.button.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.button.widthAuto }}</option>\n <option value=\"full\">{{ t.button.fullWidth }}</option>\n <option value=\"custom\">{{ t.button.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align ?? 'center'\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Lock, LockOpen, Minus, Plus } from \"@lucide/vue\";\nimport { computed, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n modelValue: SpacingValue;\n label: string;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: SpacingValue): void;\n}>();\n\nconst { t } = useI18n();\n\ninterface SpacingValue {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nconst isUniform = computed(\n () =>\n props.modelValue.top === props.modelValue.right &&\n props.modelValue.right === props.modelValue.bottom &&\n props.modelValue.bottom === props.modelValue.left,\n);\n\nconst locked = ref(isUniform.value);\n\nwatch(isUniform, (uniform) => {\n if (!uniform && locked.value) {\n locked.value = false;\n }\n});\n\nfunction updateValue(direction: keyof SpacingValue, delta: number): void {\n const currentValue = props.modelValue[direction];\n const newValue = Math.max(0, currentValue + delta);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction setDirectValue(direction: keyof SpacingValue, value: number): void {\n const newValue = Math.max(0, value);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction toggleLock(): void {\n locked.value = !locked.value;\n if (locked.value) {\n const value = props.modelValue.top;\n emit(\"update:modelValue\", {\n top: value,\n right: value,\n bottom: value,\n left: value,\n });\n }\n}\n\nconst stepperBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:w-8 tpl:h-8 tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg)] tpl:border tpl:border-[var(--tpl-border)] tpl:cursor-pointer tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)] tpl:active:bg-[var(--tpl-bg-active)]\";\nconst inputClass =\n \"tpl:w-10 tpl:h-8 tpl:text-center tpl:text-xs tpl:font-medium tpl:border-y tpl:border-x-0 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\n</script>\n\n<template>\n <div class=\"spacing-control\">\n <label\n class=\"tpl:mb-2 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ label }}\n </label>\n\n <div class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-1.5\">\n <!-- Top -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.top\"\n :aria-label=\"t.spacingControl.top\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'top',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Middle row: Left - Lock - Right -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <!-- Left -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.left\"\n :aria-label=\"t.spacingControl.left\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'left',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Lock button -->\n <button\n class=\"tpl:flex tpl:h-8 tpl:w-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"\n locked\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n :aria-label=\"\n locked ? t.spacingControl.unlock : t.spacingControl.lockAll\n \"\n :title=\"locked ? t.spacingControl.unlock : t.spacingControl.lockAll\"\n @click=\"toggleLock\"\n >\n <Lock v-if=\"locked\" :size=\"14\" :stroke-width=\"2\" />\n <LockOpen v-else :size=\"14\" :stroke-width=\"2\" />\n </button>\n\n <!-- Right -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.right\"\n :aria-label=\"t.spacingControl.right\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'right',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Bottom -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.bottom\"\n :aria-label=\"t.spacingControl.bottom\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'bottom',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Lock, LockOpen, Minus, Plus } from \"@lucide/vue\";\nimport { computed, ref, watch } from \"vue\";\n\nconst props = defineProps<{\n modelValue: SpacingValue;\n label: string;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: SpacingValue): void;\n}>();\n\nconst { t } = useI18n();\n\ninterface SpacingValue {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nconst isUniform = computed(\n () =>\n props.modelValue.top === props.modelValue.right &&\n props.modelValue.right === props.modelValue.bottom &&\n props.modelValue.bottom === props.modelValue.left,\n);\n\nconst locked = ref(isUniform.value);\n\nwatch(isUniform, (uniform) => {\n if (!uniform && locked.value) {\n locked.value = false;\n }\n});\n\nfunction updateValue(direction: keyof SpacingValue, delta: number): void {\n const currentValue = props.modelValue[direction];\n const newValue = Math.max(0, currentValue + delta);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction setDirectValue(direction: keyof SpacingValue, value: number): void {\n const newValue = Math.max(0, value);\n\n if (locked.value) {\n emit(\"update:modelValue\", {\n top: newValue,\n right: newValue,\n bottom: newValue,\n left: newValue,\n });\n } else {\n emit(\"update:modelValue\", {\n ...props.modelValue,\n [direction]: newValue,\n });\n }\n}\n\nfunction toggleLock(): void {\n locked.value = !locked.value;\n if (locked.value) {\n const value = props.modelValue.top;\n emit(\"update:modelValue\", {\n top: value,\n right: value,\n bottom: value,\n left: value,\n });\n }\n}\n\nconst stepperBtnClass =\n \"tpl:flex tpl:items-center tpl:justify-center tpl:w-8 tpl:h-8 tpl:text-[var(--tpl-text-muted)] tpl:bg-[var(--tpl-bg)] tpl:border tpl:border-[var(--tpl-border)] tpl:cursor-pointer tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)] tpl:active:bg-[var(--tpl-bg-active)]\";\nconst inputClass =\n \"tpl:w-10 tpl:h-8 tpl:text-center tpl:text-xs tpl:font-medium tpl:border-y tpl:border-x-0 tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-text)] tpl:bg-[var(--tpl-bg)] tpl:outline-none tpl:transition-all tpl:duration-[120ms] tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[var(--tpl-ring)]\";\n</script>\n\n<template>\n <div class=\"spacing-control\">\n <label\n class=\"tpl:mb-2 tpl:block tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ label }}\n </label>\n\n <div class=\"tpl:flex tpl:flex-col tpl:items-center tpl:gap-1.5\">\n <!-- Top -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.top\"\n :aria-label=\"t.spacingControl.top\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'top',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseTop\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('top', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Middle row: Left - Lock - Right -->\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <!-- Left -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.left\"\n :aria-label=\"t.spacingControl.left\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'left',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseLeft\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('left', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <!-- Lock button -->\n <button\n class=\"tpl:flex tpl:h-8 tpl:w-8 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"\n locked\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n :aria-label=\"\n locked ? t.spacingControl.unlock : t.spacingControl.lockAll\n \"\n :title=\"locked ? t.spacingControl.unlock : t.spacingControl.lockAll\"\n @click=\"toggleLock\"\n >\n <Lock v-if=\"locked\" :size=\"14\" :stroke-width=\"2\" />\n <LockOpen v-else :size=\"14\" :stroke-width=\"2\" />\n </button>\n\n <!-- Right -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.right\"\n :aria-label=\"t.spacingControl.right\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'right',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseRight\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('right', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <!-- Bottom -->\n <div class=\"tpl:flex tpl:items-center\">\n <button\n :aria-label=\"t.spacingControl.decreaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-l-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', -1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <input\n type=\"number\"\n :class=\"inputClass\"\n :value=\"modelValue.bottom\"\n :aria-label=\"t.spacingControl.bottom\"\n min=\"0\"\n @input=\"\n setDirectValue(\n 'bottom',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <button\n :aria-label=\"t.spacingControl.increaseBottom\"\n :class=\"[stepperBtnClass, 'tpl:rounded-r-[var(--tpl-radius-sm)]']\"\n @click=\"updateValue('bottom', 1)\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ChevronDown } from \"@lucide/vue\";\n\ndefineProps<{\n title: string;\n open: boolean;\n noBorder?: boolean;\n /**\n * Optional test hook on the header button. The title is the only other way to\n * address a section, and it is localized — which the e2e selector rules ban.\n */\n headerTestId?: string;\n}>();\n\ndefineEmits<{\n (e: \"toggle\"): void;\n}>();\n</script>\n\n<template>\n <div\n class=\"tpl:py-3\"\n :class=\"noBorder ? '' : 'tpl:border-t tpl:border-[var(--tpl-border)]'\"\n >\n <button\n type=\"button\"\n :data-testid=\"headerTestId\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"$emit('toggle')\"\n >\n <ChevronDown\n class=\"tpl:transition-transform tpl:duration-200\"\n :class=\"open ? 'tpl:rotate-0' : 'tpl:-rotate-90'\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n <span>{{ title }}</span>\n </button>\n <div v-show=\"open\" class=\"tpl:mt-3\">\n <slot />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ChevronDown } from \"@lucide/vue\";\n\ndefineProps<{\n title: string;\n open: boolean;\n noBorder?: boolean;\n /**\n * Optional test hook on the header button. The title is the only other way to\n * address a section, and it is localized — which the e2e selector rules ban.\n */\n headerTestId?: string;\n}>();\n\ndefineEmits<{\n (e: \"toggle\"): void;\n}>();\n</script>\n\n<template>\n <div\n class=\"tpl:py-3\"\n :class=\"noBorder ? '' : 'tpl:border-t tpl:border-[var(--tpl-border)]'\"\n >\n <button\n type=\"button\"\n :data-testid=\"headerTestId\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-transparent tpl:p-0 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n @click=\"$emit('toggle')\"\n >\n <ChevronDown\n class=\"tpl:transition-transform tpl:duration-200\"\n :class=\"open ? 'tpl:rotate-0' : 'tpl:-rotate-90'\"\n :size=\"12\"\n :stroke-width=\"2\"\n />\n <span>{{ title }}</span>\n </button>\n <div v-show=\"open\" class=\"tpl:mt-3\">\n <slot />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport CollapsibleSection from \"./CollapsibleSection.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { Block, DisplayCondition } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed, inject, reactive, ref, watch, type Component } from \"vue\";\nimport {\n DISPLAY_CONDITIONS_KEY,\n ALLOW_CUSTOM_CONDITIONS_KEY,\n} from \"../../keys\";\n\ntype SectionKey = \"spacing\" | \"bg\" | \"display\" | \"condition\";\ntype VisibilityKey = \"desktop\" | \"mobile\";\n\nconst props = defineProps<{\n block: Block;\n isFirstSection?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst displayConditions = inject(DISPLAY_CONDITIONS_KEY, []);\nconst allowCustomConditions = inject(ALLOW_CUSTOM_CONDITIONS_KEY, false);\n\nconst openSections = reactive(new Set<SectionKey>());\nconst customConditionMode = ref(false);\nconst customBefore = ref(\"\");\nconst customAfter = ref(\"\");\n\nconst VISIBILITY_ITEMS: {\n key: VisibilityKey;\n icon: Component;\n labelKey: \"showOnDesktop\" | \"showOnMobile\";\n}[] = [\n { key: \"desktop\", icon: Monitor, labelKey: \"showOnDesktop\" },\n { key: \"mobile\", icon: Smartphone, labelKey: \"showOnMobile\" },\n];\n\nfunction toggleSection(key: SectionKey): void {\n if (openSections.has(key)) openSections.delete(key);\n else openSections.add(key);\n}\n\nconst hasDisplayConditions = computed(\n () => displayConditions.length > 0 || allowCustomConditions,\n);\n\nconst isCustomCondition = computed(() => {\n if (!props.block.displayCondition) return false;\n return !displayConditions.some(\n (c) => c.label === props.block.displayCondition?.label,\n );\n});\n\nfunction startCustomCondition(): void {\n customConditionMode.value = true;\n if (isCustomCondition.value && props.block.displayCondition) {\n customBefore.value = props.block.displayCondition.before;\n customAfter.value = props.block.displayCondition.after ?? \"\";\n } else {\n customBefore.value = \"\";\n customAfter.value = \"\";\n }\n}\n\nfunction applyCustomCondition(): void {\n if (!customBefore.value.trim()) return;\n emit(\"update\", {\n displayCondition: {\n label: t.blockSettings.customCondition,\n before: customBefore.value.trim(),\n after: customAfter.value.trim(),\n },\n });\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n}\n\nwatch(\n () => props.block.displayCondition,\n (condition) => {\n if (!condition) {\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n return;\n }\n if (isCustomCondition.value) {\n customBefore.value = condition.before;\n customAfter.value = condition.after ?? \"\";\n }\n },\n { immediate: true },\n);\n\nconst groupedDisplayConditions = computed(() => {\n const groups: Record<string, DisplayCondition[]> = {};\n for (const condition of displayConditions) {\n const group = condition.group ?? \"\";\n if (!groups[group]) groups[group] = [];\n groups[group].push(condition);\n }\n return groups;\n});\n\nfunction updateStyle(field: string, value: unknown): void {\n emit(\"update\", {\n styles: { ...props.block.styles, [field]: value },\n });\n}\n\nfunction isVisible(key: VisibilityKey): boolean {\n return props.block.visibility?.[key] !== false;\n}\n\nfunction toggleVisibility(key: VisibilityKey): void {\n const next: Record<VisibilityKey, boolean> = {\n desktop: isVisible(\"desktop\"),\n mobile: isVisible(\"mobile\"),\n };\n next[key] = !next[key];\n emit(\"update\", { visibility: next });\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col\" :class=\"isFirstSection ? '' : 'tpl:mt-4'\">\n <CollapsibleSection\n :title=\"t.blockSettings.spacing\"\n :open=\"openSections.has('spacing')\"\n :no-border=\"isFirstSection\"\n @toggle=\"toggleSection('spacing')\"\n >\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"block.styles.padding\"\n @update:model-value=\"updateStyle('padding', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.background\"\n :open=\"openSections.has('bg')\"\n @toggle=\"toggleSection('bg')\"\n >\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.styles.backgroundColor ?? ''\"\n @update:model-value=\"updateStyle('backgroundColor', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.display\"\n :open=\"openSections.has('display')\"\n @toggle=\"toggleSection('display')\"\n >\n <div class=\"tpl:space-y-2\">\n <ToggleSwitch\n v-for=\"item in VISIBILITY_ITEMS\"\n :key=\"item.key\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"isVisible(item.key)\"\n :label=\"t.blockSettings[item.labelKey]\"\n @update:model-value=\"toggleVisibility(item.key)\"\n >\n <span class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <component :is=\"item.icon\" :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.blockSettings[item.labelKey] }}\n </span>\n </ToggleSwitch>\n </div>\n </CollapsibleSection>\n\n <CollapsibleSection\n v-if=\"hasDisplayConditions\"\n header-test-id=\"display-condition-section\"\n :title=\"t.blockSettings.displayCondition\"\n :open=\"openSections.has('condition')\"\n @toggle=\"toggleSection('condition')\"\n >\n <div class=\"tpl:space-y-2\">\n <select\n data-testid=\"display-condition-select\"\n class=\"tpl:w-full tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:outline-none tpl:transition-all tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[0_0_0_3px_var(--tpl-primary-light)]\"\n :class=\"\n block.displayCondition\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-text)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]'\n \"\n :value=\"\n customConditionMode || isCustomCondition\n ? '__custom__'\n : (block.displayCondition?.label ?? '')\n \"\n @change=\"\n (e: Event) => {\n const label = (e.target as HTMLSelectElement).value;\n if (label === '__custom__') {\n startCustomCondition();\n return;\n }\n customConditionMode = false;\n if (!label) {\n emit('update', { displayCondition: undefined });\n return;\n }\n const condition = displayConditions.find(\n (c) => c.label === label,\n );\n if (condition) {\n emit('update', { displayCondition: condition });\n }\n }\n \"\n >\n <option value=\"\">{{ t.blockSettings.noCondition }}</option>\n <template\n v-for=\"(conditions, group) in groupedDisplayConditions\"\n :key=\"group\"\n >\n <optgroup v-if=\"group\" :label=\"String(group)\">\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </optgroup>\n <template v-else>\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </template>\n </template>\n <option v-if=\"allowCustomConditions\" value=\"__custom__\">\n {{ t.blockSettings.customCondition }}\n </option>\n </select>\n\n <template v-if=\"customConditionMode || isCustomCondition\">\n <div class=\"tpl:space-y-2\">\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionBefore }}</label\n >\n <textarea\n v-model=\"customBefore\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionAfter }}</label\n >\n <textarea\n v-model=\"customAfter\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div class=\"tpl:flex tpl:justify-end\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-[var(--tpl-primary)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:hover:opacity-90 tpl:disabled:opacity-50\"\n :disabled=\"!customBefore.trim()\"\n @click=\"applyCustomCondition\"\n >\n {{ t.blockSettings.applyCondition }}\n </button>\n </div>\n </div>\n </template>\n\n <template v-else-if=\"block.displayCondition && !isCustomCondition\">\n <p\n v-if=\"block.displayCondition.description\"\n class=\"tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ block.displayCondition.description }}\n </p>\n <div class=\"tpl:space-y-1\">\n <pre\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.before }}</pre>\n <pre\n v-if=\"block.displayCondition.after\"\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.after }}</pre>\n </div>\n </template>\n </div>\n </CollapsibleSection>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport CollapsibleSection from \"./CollapsibleSection.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { Block, DisplayCondition } from \"@templatical/types\";\nimport { Monitor, Smartphone } from \"@lucide/vue\";\nimport { computed, inject, reactive, ref, watch, type Component } from \"vue\";\nimport {\n DISPLAY_CONDITIONS_KEY,\n ALLOW_CUSTOM_CONDITIONS_KEY,\n} from \"../../keys\";\n\ntype SectionKey = \"spacing\" | \"bg\" | \"display\" | \"condition\";\ntype VisibilityKey = \"desktop\" | \"mobile\";\n\nconst props = defineProps<{\n block: Block;\n isFirstSection?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst displayConditions = inject(DISPLAY_CONDITIONS_KEY, []);\nconst allowCustomConditions = inject(ALLOW_CUSTOM_CONDITIONS_KEY, false);\n\nconst openSections = reactive(new Set<SectionKey>());\nconst customConditionMode = ref(false);\nconst customBefore = ref(\"\");\nconst customAfter = ref(\"\");\n\nconst VISIBILITY_ITEMS: {\n key: VisibilityKey;\n icon: Component;\n labelKey: \"showOnDesktop\" | \"showOnMobile\";\n}[] = [\n { key: \"desktop\", icon: Monitor, labelKey: \"showOnDesktop\" },\n { key: \"mobile\", icon: Smartphone, labelKey: \"showOnMobile\" },\n];\n\nfunction toggleSection(key: SectionKey): void {\n if (openSections.has(key)) openSections.delete(key);\n else openSections.add(key);\n}\n\nconst hasDisplayConditions = computed(\n () => displayConditions.length > 0 || allowCustomConditions,\n);\n\nconst isCustomCondition = computed(() => {\n if (!props.block.displayCondition) return false;\n return !displayConditions.some(\n (c) => c.label === props.block.displayCondition?.label,\n );\n});\n\nfunction startCustomCondition(): void {\n customConditionMode.value = true;\n if (isCustomCondition.value && props.block.displayCondition) {\n customBefore.value = props.block.displayCondition.before;\n customAfter.value = props.block.displayCondition.after ?? \"\";\n } else {\n customBefore.value = \"\";\n customAfter.value = \"\";\n }\n}\n\nfunction applyCustomCondition(): void {\n if (!customBefore.value.trim()) return;\n emit(\"update\", {\n displayCondition: {\n label: t.blockSettings.customCondition,\n before: customBefore.value.trim(),\n after: customAfter.value.trim(),\n },\n });\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n}\n\nwatch(\n () => props.block.displayCondition,\n (condition) => {\n if (!condition) {\n customConditionMode.value = false;\n customBefore.value = \"\";\n customAfter.value = \"\";\n return;\n }\n if (isCustomCondition.value) {\n customBefore.value = condition.before;\n customAfter.value = condition.after ?? \"\";\n }\n },\n { immediate: true },\n);\n\nconst groupedDisplayConditions = computed(() => {\n const groups: Record<string, DisplayCondition[]> = {};\n for (const condition of displayConditions) {\n const group = condition.group ?? \"\";\n if (!groups[group]) groups[group] = [];\n groups[group].push(condition);\n }\n return groups;\n});\n\nfunction updateStyle(field: string, value: unknown): void {\n emit(\"update\", {\n styles: { ...props.block.styles, [field]: value },\n });\n}\n\nfunction isVisible(key: VisibilityKey): boolean {\n return props.block.visibility?.[key] !== false;\n}\n\nfunction toggleVisibility(key: VisibilityKey): void {\n const next: Record<VisibilityKey, boolean> = {\n desktop: isVisible(\"desktop\"),\n mobile: isVisible(\"mobile\"),\n };\n next[key] = !next[key];\n emit(\"update\", { visibility: next });\n}\n</script>\n\n<template>\n <div class=\"tpl:flex tpl:flex-col\" :class=\"isFirstSection ? '' : 'tpl:mt-4'\">\n <CollapsibleSection\n :title=\"t.blockSettings.spacing\"\n :open=\"openSections.has('spacing')\"\n :no-border=\"isFirstSection\"\n @toggle=\"toggleSection('spacing')\"\n >\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"block.styles.padding\"\n @update:model-value=\"updateStyle('padding', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.background\"\n :open=\"openSections.has('bg')\"\n @toggle=\"toggleSection('bg')\"\n >\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.styles.backgroundColor ?? ''\"\n @update:model-value=\"updateStyle('backgroundColor', $event)\"\n />\n </CollapsibleSection>\n\n <CollapsibleSection\n :title=\"t.blockSettings.display\"\n :open=\"openSections.has('display')\"\n @toggle=\"toggleSection('display')\"\n >\n <div class=\"tpl:space-y-2\">\n <ToggleSwitch\n v-for=\"item in VISIBILITY_ITEMS\"\n :key=\"item.key\"\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"isVisible(item.key)\"\n :label=\"t.blockSettings[item.labelKey]\"\n @update:model-value=\"toggleVisibility(item.key)\"\n >\n <span class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <component :is=\"item.icon\" :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.blockSettings[item.labelKey] }}\n </span>\n </ToggleSwitch>\n </div>\n </CollapsibleSection>\n\n <CollapsibleSection\n v-if=\"hasDisplayConditions\"\n header-test-id=\"display-condition-section\"\n :title=\"t.blockSettings.displayCondition\"\n :open=\"openSections.has('condition')\"\n @toggle=\"toggleSection('condition')\"\n >\n <div class=\"tpl:space-y-2\">\n <select\n data-testid=\"display-condition-select\"\n class=\"tpl:w-full tpl:rounded-md tpl:border tpl:px-2.5 tpl:py-2 tpl:text-xs tpl:outline-none tpl:transition-all tpl:focus:border-[var(--tpl-primary)] tpl:focus:shadow-[0_0_0_3px_var(--tpl-primary-light)]\"\n :class=\"\n block.displayCondition\n ? 'tpl:border-[var(--tpl-primary)] tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-text)]'\n : 'tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text)]'\n \"\n :value=\"\n customConditionMode || isCustomCondition\n ? '__custom__'\n : (block.displayCondition?.label ?? '')\n \"\n @change=\"\n (e: Event) => {\n const label = (e.target as HTMLSelectElement).value;\n if (label === '__custom__') {\n startCustomCondition();\n return;\n }\n customConditionMode = false;\n if (!label) {\n emit('update', { displayCondition: undefined });\n return;\n }\n const condition = displayConditions.find(\n (c) => c.label === label,\n );\n if (condition) {\n emit('update', { displayCondition: condition });\n }\n }\n \"\n >\n <option value=\"\">{{ t.blockSettings.noCondition }}</option>\n <template\n v-for=\"(conditions, group) in groupedDisplayConditions\"\n :key=\"group\"\n >\n <optgroup v-if=\"group\" :label=\"String(group)\">\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </optgroup>\n <template v-else>\n <option\n v-for=\"condition in conditions\"\n :key=\"condition.label\"\n :value=\"condition.label\"\n >\n {{ condition.label }}\n </option>\n </template>\n </template>\n <option v-if=\"allowCustomConditions\" value=\"__custom__\">\n {{ t.blockSettings.customCondition }}\n </option>\n </select>\n\n <template v-if=\"customConditionMode || isCustomCondition\">\n <div class=\"tpl:space-y-2\">\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionBefore }}</label\n >\n <textarea\n v-model=\"customBefore\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div>\n <label\n class=\"tpl:mb-1 tpl:block tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n >{{ t.blockSettings.customConditionAfter }}</label\n >\n <textarea\n v-model=\"customAfter\"\n rows=\"2\"\n :class=\"monoTextareaClass\"\n />\n </div>\n <div class=\"tpl:flex tpl:justify-end\">\n <button\n type=\"button\"\n class=\"tpl:cursor-pointer tpl:rounded-md tpl:border-none tpl:bg-[var(--tpl-primary)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:hover:opacity-90 tpl:disabled:opacity-50\"\n :disabled=\"!customBefore.trim()\"\n @click=\"applyCustomCondition\"\n >\n {{ t.blockSettings.applyCondition }}\n </button>\n </div>\n </div>\n </template>\n\n <template v-else-if=\"block.displayCondition && !isCustomCondition\">\n <p\n v-if=\"block.displayCondition.description\"\n class=\"tpl:text-[11px] tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ block.displayCondition.description }}\n </p>\n <div class=\"tpl:space-y-1\">\n <pre\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.before }}</pre>\n <pre\n v-if=\"block.displayCondition.after\"\n class=\"tpl:m-0 tpl:overflow-x-auto tpl:rounded tpl:bg-[var(--tpl-bg)] tpl:p-2 tpl:font-mono tpl:text-[10px] tpl:text-[var(--tpl-text-muted)]\"\n >{{ block.displayCondition.after }}</pre>\n </div>\n </template>\n </div>\n </CollapsibleSection>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockBooleanField } from \"@templatical/types\";\nimport { Lock } from \"@lucide/vue\";\nimport ToggleSwitch from \"../../ToggleSwitch.vue\";\n\ndefineProps<{\n field: CustomBlockBooleanField;\n modelValue: boolean;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl:mb-3.5\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n >\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"modelValue\"\n :label=\"field.label\"\n :required=\"field.required\"\n :disabled=\"readOnly\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n {{ field.label }}\n <Lock\n v-if=\"readOnly\"\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n />\n </ToggleSwitch>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockBooleanField } from \"@templatical/types\";\nimport { Lock } from \"@lucide/vue\";\nimport ToggleSwitch from \"../../ToggleSwitch.vue\";\n\ndefineProps<{\n field: CustomBlockBooleanField;\n modelValue: boolean;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl:mb-3.5\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n >\n <ToggleSwitch\n class=\"tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"modelValue\"\n :label=\"field.label\"\n :required=\"field.required\"\n :disabled=\"readOnly\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n {{ field.label }}\n <Lock\n v-if=\"readOnly\"\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n />\n </ToggleSwitch>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { labelClass } from \"../../../constants/styleConstants\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport { Lock } from \"@lucide/vue\";\n\ndefineProps<{\n label: string;\n required?: boolean;\n readOnly?: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ label }}\n <!-- Same treatment as the asterisk below, and the same string the seven\n field components put on the input itself: `readOnly` here is only ever\n `field.readOnly && block.dataSourceFetched`, so \"loaded from your data\n source\" is the reason, not a generic lock. -->\n <span v-if=\"readOnly\" :title=\"t.customBlocks.dataSource.readOnlyTooltip\">\n <Lock\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n aria-hidden=\"true\"\n />\n <span class=\"tpl-sr-only\">{{\n t.customBlocks.dataSource.readOnlyTooltip\n }}</span>\n </span>\n <!-- The asterisk is a sighted convention, not a label: on its own it\n announces as \"asterisk\" or as nothing at all, so the requirement is\n invisible to a screen reader. The glyph is therefore hidden from the\n accessibility tree and the text carried alongside it, which is also\n what makes the state translatable rather than punctuation. -->\n <span\n v-if=\"required\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.required\"\n >\n <span aria-hidden=\"true\">*</span>\n <span class=\"tpl-sr-only\">{{ t.customBlocks.fields.required }}</span>\n </span>\n </label>\n <slot />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { labelClass } from \"../../../constants/styleConstants\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport { Lock } from \"@lucide/vue\";\n\ndefineProps<{\n label: string;\n required?: boolean;\n readOnly?: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ label }}\n <!-- Same treatment as the asterisk below, and the same string the seven\n field components put on the input itself: `readOnly` here is only ever\n `field.readOnly && block.dataSourceFetched`, so \"loaded from your data\n source\" is the reason, not a generic lock. -->\n <span v-if=\"readOnly\" :title=\"t.customBlocks.dataSource.readOnlyTooltip\">\n <Lock\n :size=\"12\"\n class=\"tpl:inline tpl:text-[var(--tpl-text-dim)]\"\n aria-hidden=\"true\"\n />\n <span class=\"tpl-sr-only\">{{\n t.customBlocks.dataSource.readOnlyTooltip\n }}</span>\n </span>\n <!-- The asterisk is a sighted convention, not a label: on its own it\n announces as \"asterisk\" or as nothing at all, so the requirement is\n invisible to a screen reader. The glyph is therefore hidden from the\n accessibility tree and the text carried alongside it, which is also\n what makes the state translatable rather than punctuation. -->\n <span\n v-if=\"required\"\n class=\"tpl:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.required\"\n >\n <span aria-hidden=\"true\">*</span>\n <span class=\"tpl-sr-only\">{{ t.customBlocks.fields.required }}</span>\n </span>\n </label>\n <slot />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockColorField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport ColorPicker from \"../../ColorPicker.vue\";\nimport { COLORS_KEY } from \"../../../keys\";\nimport { DEFAULT_RESOLVED_COLORS } from \"../../../utils/resolveColorsConfig\";\nimport { resolveFieldColors } from \"../../../utils/resolveFieldColors\";\n\nconst props = defineProps<{\n field: CustomBlockColorField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\n// Narrow the injected editor-wide palette by this field's own `presets` /\n// `allowCustom`. Presentational only: the field config is audited once per\n// block definition at registration time (`useEditorCore` →\n// `collectColorFieldIssues`), never here — this component mounts again on every\n// block selection and once per repeater item, so warning from here would repeat\n// the same message indefinitely.\nconst editorColors = inject(COLORS_KEY, DEFAULT_RESOLVED_COLORS);\nconst colors = computed(() => resolveFieldColors(props.field, editorColors));\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <ColorPicker\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :disabled=\"readOnly\"\n :presets=\"colors.presets\"\n :allow-custom=\"colors.allowCustom\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject } from \"vue\";\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockColorField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport ColorPicker from \"../../ColorPicker.vue\";\nimport { COLORS_KEY } from \"../../../keys\";\nimport { DEFAULT_RESOLVED_COLORS } from \"../../../utils/resolveColorsConfig\";\nimport { resolveFieldColors } from \"../../../utils/resolveFieldColors\";\n\nconst props = defineProps<{\n field: CustomBlockColorField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\n// Narrow the injected editor-wide palette by this field's own `presets` /\n// `allowCustom`. Presentational only: the field config is audited once per\n// block definition at registration time (`useEditorCore` →\n// `collectColorFieldIssues`), never here — this component mounts again on every\n// block selection and once per repeater item, so warning from here would repeat\n// the same message indefinitely.\nconst editorColors = inject(COLORS_KEY, DEFAULT_RESOLVED_COLORS);\nconst colors = computed(() => resolveFieldColors(props.field, editorColors));\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <ColorPicker\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :disabled=\"readOnly\"\n :presets=\"colors.presets\"\n :allow-custom=\"colors.allowCustom\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockImageField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY } from \"../../../keys\";\nimport { useAliveFlag } from \"../../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../../composables/useImageDrop\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\nconst props = defineProps<{\n field: CustomBlockImageField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nasync function browseMedia(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n emit(\"update:modelValue\", result.url);\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !props.readOnly,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) emit(\"update:modelValue\", result.url);\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <input\n v-if=\"readOnly\"\n type=\"url\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <input\n v-else\n type=\"url\"\n :class=\"inputClass\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n @input=\"\n emit('update:modelValue', ($event.target as HTMLInputElement).value)\n \"\n />\n <button\n v-if=\"canBrowseMedia && !readOnly\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-primary)] tpl:bg-[var(--tpl-bg)]\"\n @click=\"browseMedia()\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockImageField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY } from \"../../../keys\";\nimport { useAliveFlag } from \"../../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../../composables/useImageDrop\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\nconst props = defineProps<{\n field: CustomBlockImageField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nasync function browseMedia(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n emit(\"update:modelValue\", result.url);\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !props.readOnly,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) emit(\"update:modelValue\", result.url);\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <input\n v-if=\"readOnly\"\n type=\"url\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <input\n v-else\n type=\"url\"\n :class=\"inputClass\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder || 'https://...'\"\n @input=\"\n emit('update:modelValue', ($event.target as HTMLInputElement).value)\n \"\n />\n <button\n v-if=\"canBrowseMedia && !readOnly\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:border-[var(--tpl-border)] tpl:text-[var(--tpl-primary)] tpl:bg-[var(--tpl-bg)]\"\n @click=\"browseMedia()\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockNumberField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockNumberField;\n modelValue: number;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: number): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n type=\"number\"\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :min=\"field.min\"\n :max=\"field.max\"\n :step=\"field.step\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @input=\"\n !readOnly &&\n emit(\n 'update:modelValue',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockNumberField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockNumberField;\n modelValue: number;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: number): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n type=\"number\"\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n :min=\"field.min\"\n :max=\"field.max\"\n :step=\"field.step\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @input=\"\n !readOnly &&\n emit(\n 'update:modelValue',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockRepeatableField } from \"@templatical/types\";\nimport { Plus, Trash2 } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { resolveFieldComponent } from \"./index\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport { addItemBtnClass } from \"../../../constants/styleConstants\";\n\nconst props = defineProps<{\n field: CustomBlockRepeatableField;\n modelValue: Record<string, unknown>[];\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: Record<string, unknown>[]): void;\n}>();\n\nconst { t, format } = useI18n();\n\nconst items = computed(() => props.modelValue || []);\n\nconst canAdd = computed(\n () => !props.field.maxItems || items.value.length < props.field.maxItems,\n);\n\nconst canRemove = computed(\n () => !props.field.minItems || items.value.length > props.field.minItems,\n);\n\nfunction addItem(): void {\n if (!canAdd.value || props.readOnly) {\n return;\n }\n\n const newItem: Record<string, unknown> = {};\n for (const subField of props.field.fields) {\n newItem[subField.key] = subField.default ?? \"\";\n }\n\n emit(\"update:modelValue\", [...items.value, newItem]);\n}\n\nfunction removeItem(index: number): void {\n if (!canRemove.value || props.readOnly) {\n return;\n }\n\n const updated = [...items.value];\n updated.splice(index, 1);\n emit(\"update:modelValue\", updated);\n}\n\nfunction updateItemField(index: number, key: string, value: unknown): void {\n const updated = items.value.map((item, i) =>\n i === index ? { ...item, [key]: value } : item,\n );\n emit(\"update:modelValue\", updated);\n}\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"(item, index) in items\"\n :key=\"`${field.key}-${index}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-3\"\n >\n <div class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:justify-between\">\n <span\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n >\n #{{ index + 1 }}\n </span>\n <button\n v-if=\"canRemove && !readOnly\"\n type=\"button\"\n class=\"tpl:flex tpl:size-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.removeItem\"\n @click=\"removeItem(index)\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <template v-for=\"subField in field.fields\" :key=\"subField.key\">\n <component\n :is=\"resolveFieldComponent(subField.type)\"\n :field=\"subField\"\n :model-value=\"item[subField.key]\"\n :read-only=\"readOnly\"\n @update:model-value=\"updateItemField(index, subField.key, $event)\"\n />\n </template>\n </div>\n\n <button\n v-if=\"canAdd && !readOnly\"\n type=\"button\"\n :class=\"addItemBtnClass\"\n @click=\"addItem\"\n >\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.customBlocks.fields.addItem }}\n </button>\n\n <p\n v-if=\"!canAdd && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.customBlocks.fields.maxItemsReached }}\n </p>\n\n <!-- The counterpart to the message above: each explains one control that\n is absent rather than disabled, and a missing control with no\n explanation reads as a bug. Both can be true at once for a\n fixed-length list (`minItems === maxItems`), where the two together\n are what say the length is fixed. -->\n <p\n v-if=\"!canRemove && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.customBlocks.fields.minItemsRequired, {\n count: field.minItems ?? 0,\n })\n }}\n </p>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockRepeatableField } from \"@templatical/types\";\nimport { Plus, Trash2 } from \"@lucide/vue\";\nimport { computed } from \"vue\";\nimport { resolveFieldComponent } from \"./index\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport { addItemBtnClass } from \"../../../constants/styleConstants\";\n\nconst props = defineProps<{\n field: CustomBlockRepeatableField;\n modelValue: Record<string, unknown>[];\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: Record<string, unknown>[]): void;\n}>();\n\nconst { t, format } = useI18n();\n\nconst items = computed(() => props.modelValue || []);\n\nconst canAdd = computed(\n () => !props.field.maxItems || items.value.length < props.field.maxItems,\n);\n\nconst canRemove = computed(\n () => !props.field.minItems || items.value.length > props.field.minItems,\n);\n\nfunction addItem(): void {\n if (!canAdd.value || props.readOnly) {\n return;\n }\n\n const newItem: Record<string, unknown> = {};\n for (const subField of props.field.fields) {\n newItem[subField.key] = subField.default ?? \"\";\n }\n\n emit(\"update:modelValue\", [...items.value, newItem]);\n}\n\nfunction removeItem(index: number): void {\n if (!canRemove.value || props.readOnly) {\n return;\n }\n\n const updated = [...items.value];\n updated.splice(index, 1);\n emit(\"update:modelValue\", updated);\n}\n\nfunction updateItemField(index: number, key: string, value: unknown): void {\n const updated = items.value.map((item, i) =>\n i === index ? { ...item, [key]: value } : item,\n );\n emit(\"update:modelValue\", updated);\n}\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"(item, index) in items\"\n :key=\"`${field.key}-${index}`\"\n class=\"tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-3\"\n >\n <div class=\"tpl:mb-2 tpl:flex tpl:items-center tpl:justify-between\">\n <span\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n >\n #{{ index + 1 }}\n </span>\n <button\n v-if=\"canRemove && !readOnly\"\n type=\"button\"\n class=\"tpl:flex tpl:size-6 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.customBlocks.fields.removeItem\"\n @click=\"removeItem(index)\"\n >\n <Trash2 :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n\n <template v-for=\"subField in field.fields\" :key=\"subField.key\">\n <component\n :is=\"resolveFieldComponent(subField.type)\"\n :field=\"subField\"\n :model-value=\"item[subField.key]\"\n :read-only=\"readOnly\"\n @update:model-value=\"updateItemField(index, subField.key, $event)\"\n />\n </template>\n </div>\n\n <button\n v-if=\"canAdd && !readOnly\"\n type=\"button\"\n :class=\"addItemBtnClass\"\n @click=\"addItem\"\n >\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.customBlocks.fields.addItem }}\n </button>\n\n <p\n v-if=\"!canAdd && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ t.customBlocks.fields.maxItemsReached }}\n </p>\n\n <!-- The counterpart to the message above: each explains one control that\n is absent rather than disabled, and a missing control with no\n explanation reads as a bug. Both can be true at once for a\n fixed-length list (`minItems === maxItems`), where the two together\n are what say the length is fixed. -->\n <p\n v-if=\"!canRemove && !readOnly\"\n class=\"tpl:m-0 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.customBlocks.fields.minItemsRequired, {\n count: field.minItems ?? 0,\n })\n }}\n </p>\n </div>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockSelectField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockSelectField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <select\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @change=\"\n !readOnly &&\n emit('update:modelValue', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option\n v-for=\"option in field.options\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockSelectField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\n\ndefineProps<{\n field: CustomBlockSelectField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <select\n :class=\"[inputClass, readOnly && 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :disabled=\"readOnly\"\n :title=\"readOnly ? t.customBlocks.dataSource.readOnlyTooltip : undefined\"\n @change=\"\n !readOnly &&\n emit('update:modelValue', ($event.target as HTMLSelectElement).value)\n \"\n >\n <option\n v-for=\"option in field.options\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagInput from \"../../MergeTagInput.vue\";\n\ndefineProps<{\n field: CustomBlockTextField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n v-if=\"readOnly\"\n type=\"text\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <MergeTagInput\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextField } from \"@templatical/types\";\nimport { inputClass } from \"../../../constants/styleConstants\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagInput from \"../../MergeTagInput.vue\";\n\ndefineProps<{\n field: CustomBlockTextField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <input\n v-if=\"readOnly\"\n type=\"text\"\n :class=\"[inputClass, 'tpl:opacity-60 tpl:cursor-not-allowed']\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n />\n <MergeTagInput\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextareaField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagTextarea from \"../../MergeTagTextarea.vue\";\n\ndefineProps<{\n field: CustomBlockTextareaField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst readOnlyTextareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:opacity-60 tpl:cursor-not-allowed\";\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <textarea\n v-if=\"readOnly\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n rows=\"3\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n :class=\"readOnlyTextareaClass\"\n />\n <MergeTagTextarea\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../../composables/useI18n\";\nimport type { CustomBlockTextareaField } from \"@templatical/types\";\nimport FieldWrapper from \"./FieldWrapper.vue\";\nimport MergeTagTextarea from \"../../MergeTagTextarea.vue\";\n\ndefineProps<{\n field: CustomBlockTextareaField;\n modelValue: string;\n readOnly?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update:modelValue\", value: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst readOnlyTextareaClass =\n \"tpl:w-full tpl:resize-y tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-2 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:opacity-60 tpl:cursor-not-allowed\";\n</script>\n\n<template>\n <FieldWrapper\n :label=\"field.label\"\n :required=\"field.required\"\n :read-only=\"readOnly\"\n >\n <textarea\n v-if=\"readOnly\"\n :value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n rows=\"3\"\n disabled\n :title=\"t.customBlocks.dataSource.readOnlyTooltip\"\n :class=\"readOnlyTextareaClass\"\n />\n <MergeTagTextarea\n v-else\n :model-value=\"modelValue\"\n :placeholder=\"field.placeholder\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n />\n </FieldWrapper>\n</template>\n","import type { CustomBlockFieldType } from \"@templatical/types\";\nimport type { Component } from \"vue\";\nimport BooleanField from \"./BooleanField.vue\";\nimport ColorField from \"./ColorField.vue\";\nimport ImageField from \"./ImageField.vue\";\nimport NumberField from \"./NumberField.vue\";\nimport RepeatableField from \"./RepeatableField.vue\";\nimport SelectField from \"./SelectField.vue\";\nimport TextField from \"./TextField.vue\";\nimport TextareaField from \"./TextareaField.vue\";\n\nexport const fieldComponentMap: Record<CustomBlockFieldType, Component> = {\n text: TextField,\n textarea: TextareaField,\n image: ImageField,\n color: ColorField,\n number: NumberField,\n select: SelectField,\n boolean: BooleanField,\n repeatable: RepeatableField,\n};\n\nexport function resolveFieldComponent(type: CustomBlockFieldType): Component {\n return fieldComponentMap[type] ?? TextField;\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useDataSourceFetch } from \"@templatical/core\";\nimport type { CustomBlock, CustomBlockField } from \"@templatical/types\";\nimport { CircleAlert, RefreshCw } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY } from \"../../keys\";\nimport { resolveFieldComponent } from \"./fields\";\n\nconst props = defineProps<{\n block: CustomBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"updateFieldValues\", values: Record<string, unknown>): void;\n (e: \"updateDataSourceFetched\", fetched: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst definition = computed(() =>\n customBlockDefinitions.find((d) => d.type === props.block.customType),\n);\n\nconst blockRef = computed(() => props.block);\n\nconst {\n isFetching,\n fetchError,\n fetch: fetchData,\n hasDataSource,\n needsFetch,\n} = useDataSourceFetch({\n definition,\n block: blockRef,\n onUpdate: (fieldValues, fetched) => {\n emit(\"updateFieldValues\", fieldValues);\n emit(\"updateDataSourceFetched\", fetched);\n },\n});\n\nfunction isFieldReadOnly(field: CustomBlockField): boolean {\n return (\n field.readOnly === true &&\n hasDataSource.value &&\n !!props.block.dataSourceFetched\n );\n}\n\nfunction updateField(key: string, value: unknown): void {\n emit(\"updateFieldValues\", {\n ...props.block.fieldValues,\n [key]: value,\n });\n}\n</script>\n\n<template>\n <div v-if=\"!definition\" class=\"tpl:p-4\">\n <p\n class=\"tpl:m-0 tpl:text-center tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.toolbar.noDefinition }}\n </p>\n </div>\n\n <div v-else>\n <p\n v-if=\"definition.description\"\n class=\"tpl:m-0 tpl:mb-3 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ definition.description }}\n </p>\n\n <div v-if=\"hasDataSource\" class=\"tpl:mb-4\">\n <!-- Before fetch: prominent button -->\n <button\n v-if=\"needsFetch && !isFetching\"\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-md tpl:px-3 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:bg-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n {{\n definition?.dataSource?.label || t.customBlocks.dataSource.fetchButton\n }}\n </button>\n\n <!-- Loading / Change button -->\n <div v-else class=\"tpl:flex tpl:h-[32px] tpl:items-center\">\n <div\n v-if=\"isFetching\"\n class=\"tpl:w-full tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.dataSource.fetching }}\n </div>\n <button\n v-else\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-primary)] tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n <RefreshCw :size=\"12\" />\n {{ t.customBlocks.dataSource.changeButton }}\n </button>\n </div>\n\n <p\n v-if=\"fetchError\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert :size=\"14\" class=\"tpl:shrink-0\" />\n {{ t.customBlocks.dataSource.fetchError }}\n </p>\n </div>\n\n <template v-for=\"field in definition.fields\" :key=\"field.key\">\n <component\n :is=\"resolveFieldComponent(field.type)\"\n :field=\"field\"\n :model-value=\"block.fieldValues[field.key]\"\n :read-only=\"isFieldReadOnly(field)\"\n @update:model-value=\"updateField(field.key, $event)\"\n />\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { useDataSourceFetch } from \"@templatical/core\";\nimport type { CustomBlock, CustomBlockField } from \"@templatical/types\";\nimport { CircleAlert, RefreshCw } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { CUSTOM_BLOCK_DEFINITIONS_KEY } from \"../../keys\";\nimport { resolveFieldComponent } from \"./fields\";\n\nconst props = defineProps<{\n block: CustomBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"updateFieldValues\", values: Record<string, unknown>): void;\n (e: \"updateDataSourceFetched\", fetched: boolean): void;\n}>();\n\nconst { t } = useI18n();\n\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst definition = computed(() =>\n customBlockDefinitions.find((d) => d.type === props.block.customType),\n);\n\nconst blockRef = computed(() => props.block);\n\nconst {\n isFetching,\n fetchError,\n fetch: fetchData,\n hasDataSource,\n needsFetch,\n} = useDataSourceFetch({\n definition,\n block: blockRef,\n onUpdate: (fieldValues, fetched) => {\n emit(\"updateFieldValues\", fieldValues);\n emit(\"updateDataSourceFetched\", fetched);\n },\n});\n\nfunction isFieldReadOnly(field: CustomBlockField): boolean {\n return (\n field.readOnly === true &&\n hasDataSource.value &&\n !!props.block.dataSourceFetched\n );\n}\n\nfunction updateField(key: string, value: unknown): void {\n emit(\"updateFieldValues\", {\n ...props.block.fieldValues,\n [key]: value,\n });\n}\n</script>\n\n<template>\n <div v-if=\"!definition\" class=\"tpl:p-4\">\n <p\n class=\"tpl:m-0 tpl:text-center tpl:text-sm tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.toolbar.noDefinition }}\n </p>\n </div>\n\n <div v-else>\n <p\n v-if=\"definition.description\"\n class=\"tpl:m-0 tpl:mb-3 tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ definition.description }}\n </p>\n\n <div v-if=\"hasDataSource\" class=\"tpl:mb-4\">\n <!-- Before fetch: prominent button -->\n <button\n v-if=\"needsFetch && !isFetching\"\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-2 tpl:rounded-md tpl:px-3 tpl:py-2.5 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-on-primary)] tpl:transition-all tpl:bg-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n {{\n definition?.dataSource?.label || t.customBlocks.dataSource.fetchButton\n }}\n </button>\n\n <!-- Loading / Change button -->\n <div v-else class=\"tpl:flex tpl:h-[32px] tpl:items-center\">\n <div\n v-if=\"isFetching\"\n class=\"tpl:w-full tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n {{ t.customBlocks.dataSource.fetching }}\n </div>\n <button\n v-else\n type=\"button\"\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:px-3 tpl:py-1.5 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-primary)] tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"fetchData\"\n >\n <RefreshCw :size=\"12\" />\n {{ t.customBlocks.dataSource.changeButton }}\n </button>\n </div>\n\n <p\n v-if=\"fetchError\"\n class=\"tpl:m-0 tpl:mt-2 tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n >\n <CircleAlert :size=\"14\" class=\"tpl:shrink-0\" />\n {{ t.customBlocks.dataSource.fetchError }}\n </p>\n </div>\n\n <template v-for=\"field in definition.fields\" :key=\"field.key\">\n <component\n :is=\"resolveFieldComponent(field.type)\"\n :field=\"field\"\n :model-value=\"block.fieldValues[field.key]\"\n :read-only=\"isFieldReadOnly(field)\"\n @update:model-value=\"updateField(field.key, $event)\"\n />\n </template>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { DividerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: DividerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<DividerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<DividerBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.style }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'solid', label: t.divider.solid },\n { value: 'dashed', label: t.divider.dashed },\n { value: 'dotted', label: t.divider.dotted },\n ]\"\n :model-value=\"block.lineStyle\"\n @update:model-value=\"updateField('lineStyle', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.color }}</label>\n <ColorPicker\n :model-value=\"block.color\"\n @update:model-value=\"updateField('color', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.thickness }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.thickness\"\n min=\"1\"\n max=\"10\"\n @input=\"\n updateField(\n 'thickness',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { DividerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: DividerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<DividerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<DividerBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.style }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'solid', label: t.divider.solid },\n { value: 'dashed', label: t.divider.dashed },\n { value: 'dotted', label: t.divider.dotted },\n ]\"\n :model-value=\"block.lineStyle\"\n @update:model-value=\"updateField('lineStyle', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.color }}</label>\n <ColorPicker\n :model-value=\"block.color\"\n @update:model-value=\"updateField('color', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.divider.thickness }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.thickness\"\n min=\"1\"\n max=\"10\"\n @input=\"\n updateField(\n 'thickness',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { HtmlBlock } from \"@templatical/types\";\nimport { Info } from \"@lucide/vue\";\n\ndefineProps<{\n block: HtmlBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<HtmlBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.html.content }}</label>\n <textarea\n :value=\"block.content\"\n :placeholder=\"'<div>...</div>'\"\n rows=\"10\"\n :class=\"monoTextareaClass\"\n @input=\"\n emit('update', {\n content: ($event.target as HTMLTextAreaElement).value,\n })\n \"\n />\n <p\n class=\"tpl:mt-1.5 tpl:flex tpl:items-start tpl:gap-1.5 tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Info :size=\"12\" class=\"tpl:mt-0.5 tpl:shrink-0\" />\n {{ t.html.sanitizationHint }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport { labelClass, monoTextareaClass } from \"../../constants/styleConstants\";\nimport type { HtmlBlock } from \"@templatical/types\";\nimport { Info } from \"@lucide/vue\";\n\ndefineProps<{\n block: HtmlBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<HtmlBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.html.content }}</label>\n <textarea\n :value=\"block.content\"\n :placeholder=\"'<div>...</div>'\"\n rows=\"10\"\n :class=\"monoTextareaClass\"\n @input=\"\n emit('update', {\n content: ($event.target as HTMLTextAreaElement).value,\n })\n \"\n />\n <p\n class=\"tpl:mt-1.5 tpl:flex tpl:items-start tpl:gap-1.5 tpl:text-[11px] tpl:text-[var(--tpl-text-dim)]\"\n >\n <Info :size=\"12\" class=\"tpl:mt-0.5 tpl:shrink-0\" />\n {{ t.html.sanitizationHint }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ImageBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../composables/useImageDrop\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: ImageBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ImageBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nconst pulseSrc = ref(false);\nconst pulseAlt = ref(false);\n\nconst { start: startPulseSrc } = useTimeoutFn(\n () => {\n pulseSrc.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nconst WIDTH_PRESETS = [300, 400, 500];\nconst DEFAULT_CUSTOM_WIDTH = 350;\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst widthMode = computed(() => {\n const w = props.block.width;\n if (w === \"full\") return \"full\";\n if (WIDTH_PRESETS.includes(w)) return String(w);\n return \"custom\";\n});\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ImageBlock>);\n}\n\nfunction updateWidthMode(value: string): void {\n if (value === \"custom\") {\n const w = props.block.width;\n if (typeof w !== \"number\" || WIDTH_PRESETS.includes(w)) {\n updateField(\"width\", DEFAULT_CUSTOM_WIDTH);\n }\n return;\n }\n updateField(\"width\", value === \"full\" ? \"full\" : Number(value));\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // Same guard as the custom width below: an empty number field yields\n // Number(\"\") === 0, which would collapse the image to nothing. Absent means\n // \"keep the aspect ratio\", so an invalid value must not overwrite the last\n // valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nfunction updateBorderRadius(value: number): void {\n // Negatives only, where the width and height guards below also reject 0:\n // here 0 is a real answer (square), so an emptied field clears the radius\n // rather than keeping the old one. `NumberWithSuffix` withholds the\n // in-progress \"-\" that would otherwise arrive here as a 0.\n if (!Number.isFinite(value) || value < 0) return;\n // Absent, not 0: both render as square corners, but a stored 0 travels in\n // every exported template as though the author had chosen it. Matches\n // `createImageBlock`, and `updateHeightMode` above clears the same way.\n updateField(\"borderRadius\", value > 0 ? value : undefined);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render an\n // invisible image (#259). Ignore invalid values and keep the last valid\n // width; the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\n// A merge-tag src is a deliberate dynamic value — never clobber it via drop.\nconst hasMergeTagSrc = computed(() =>\n containsMergeTag(props.block.src, mergeTagSyntax),\n);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !hasMergeTagSrc.value,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.imageUrl }}</label>\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <MergeTagInput\n :model-value=\"block.src\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n :pulse=\"pulseSrc\"\n @update:model-value=\"updateField('src', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </div>\n <div v-if=\"containsMergeTag(block.src, mergeTagSyntax)\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\"\n >{{ t.image.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.image.placeholderUrlPlaceholder\"\n :title=\"t.image.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.image.altTextPlaceholder\"\n :pulse=\"pulseAlt\"\n :disabled=\"block.decorative === true\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n <ToggleSwitch\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.decorative === true\"\n :label=\"t.image.decorative\"\n @update:model-value=\"updateField('decorative', $event)\"\n >\n <span>\n {{ t.image.decorative }}\n <span class=\"tpl:block tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.decorativeHint }}\n </span>\n </span>\n </ToggleSwitch>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"full\">{{ t.image.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n <option value=\"custom\">{{ t.image.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"image-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.image.heightAuto }}</option>\n <option value=\"custom\">{{ t.image.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"image-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.borderRadius }}</label>\n <!-- No `max`: a circle needs a radius of at least half the rendered size,\n which the block cannot know at edit time. -->\n <NumberWithSuffix\n :model-value=\"block.borderRadius ?? 0\"\n :min=\"0\"\n suffix=\"px\"\n testid=\"image-border-radius-input\"\n @update:model-value=\"updateBorderRadius\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.linkUrl }}</label>\n <MergeTagInput\n :model-value=\"block.linkUrl || ''\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n @update:model-value=\"updateField('linkUrl', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.linkUrl\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.linkOpenInNewTab ?? false\"\n :label=\"t.image.openInNewTab\"\n @update:model-value=\"updateField('linkOpenInNewTab', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ImageBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image, Upload, LoaderCircle } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useImageDrop } from \"../../composables/useImageDrop\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: ImageBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ImageBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\n\nconst pulseSrc = ref(false);\nconst pulseAlt = ref(false);\n\nconst { start: startPulseSrc } = useTimeoutFn(\n () => {\n pulseSrc.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nconst WIDTH_PRESETS = [300, 400, 500];\nconst DEFAULT_CUSTOM_WIDTH = 350;\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst widthMode = computed(() => {\n const w = props.block.width;\n if (w === \"full\") return \"full\";\n if (WIDTH_PRESETS.includes(w)) return String(w);\n return \"custom\";\n});\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<ImageBlock>);\n}\n\nfunction updateWidthMode(value: string): void {\n if (value === \"custom\") {\n const w = props.block.width;\n if (typeof w !== \"number\" || WIDTH_PRESETS.includes(w)) {\n updateField(\"width\", DEFAULT_CUSTOM_WIDTH);\n }\n return;\n }\n updateField(\"width\", value === \"full\" ? \"full\" : Number(value));\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // Same guard as the custom width below: an empty number field yields\n // Number(\"\") === 0, which would collapse the image to nothing. Absent means\n // \"keep the aspect ratio\", so an invalid value must not overwrite the last\n // valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nfunction updateBorderRadius(value: number): void {\n // Negatives only, where the width and height guards below also reject 0:\n // here 0 is a real answer (square), so an emptied field clears the radius\n // rather than keeping the old one. `NumberWithSuffix` withholds the\n // in-progress \"-\" that would otherwise arrive here as a 0.\n if (!Number.isFinite(value) || value < 0) return;\n // Absent, not 0: both render as square corners, but a stored 0 travels in\n // every exported template as though the author had chosen it. Matches\n // `createImageBlock`, and `updateHeightMode` above clears the same way.\n updateField(\"borderRadius\", value > 0 ? value : undefined);\n}\n\nfunction updateCustomWidth(raw: string): void {\n // Guard against empty / NaN / non-positive input. An empty number field\n // yields Number(\"\") === 0, which would emit width: 0 and render an\n // invisible image (#259). Ignore invalid values and keep the last valid\n // width; the canvas still updates live on each valid keystroke.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"width\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n}\n\n// --- Drag-and-drop upload (#229) ---\nconst dropZoneRef = ref<HTMLElement>();\nconst isUploading = ref(false);\n// A merge-tag src is a deliberate dynamic value — never clobber it via drop.\nconst hasMergeTagSrc = computed(() =>\n containsMergeTag(props.block.src, mergeTagSyntax),\n);\nconst dropEnabled = computed(\n () => canBrowseMedia.value && !isUploading.value && !hasMergeTagSrc.value,\n);\n\nasync function uploadDroppedFiles(files: File[]): Promise<void> {\n if (!onRequestMedia) return;\n isUploading.value = true;\n try {\n const result = await onRequestMedia({ accept: [\"images\"], files });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"src\", result.url);\n if (result.alt) {\n updateField(\"alt\", result.alt);\n pulseAlt.value = true;\n }\n pulseSrc.value = true;\n startPulseSrc();\n }\n } finally {\n if (aliveFlag.alive) isUploading.value = false;\n }\n}\n\nconst { isOver } = useImageDrop({\n target: dropZoneRef,\n enabled: dropEnabled,\n onFiles: uploadDroppedFiles,\n});\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.imageUrl }}</label>\n <div ref=\"dropZoneRef\" class=\"tpl:relative\">\n <!-- Drag-over / uploading overlay (#229) -->\n <div\n v-if=\"dropEnabled && (isOver || isUploading)\"\n class=\"tpl:pointer-events-none tpl:absolute tpl:inset-0 tpl:z-10 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded tpl:border-2 tpl:border-dashed tpl:text-xs tpl:font-medium tpl:border-[var(--tpl-primary)] tpl:text-[var(--tpl-primary)]\"\n style=\"\n background-color: color-mix(in srgb, var(--tpl-bg) 90%, transparent);\n \"\n >\n <template v-if=\"isUploading\">\n <LoaderCircle class=\"tpl-spinner\" :size=\"18\" :stroke-width=\"2\" />\n {{ t.image.uploading }}\n </template>\n <template v-else>\n <Upload :size=\"18\" :stroke-width=\"1.5\" />\n {{ t.image.dropToUpload }}\n </template>\n </div>\n <MergeTagInput\n :model-value=\"block.src\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n :pulse=\"pulseSrc\"\n @update:model-value=\"updateField('src', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n </div>\n <div v-if=\"containsMergeTag(block.src, mergeTagSyntax)\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\"\n >{{ t.image.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.image.placeholderUrlPlaceholder\"\n :title=\"t.image.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.image.altTextPlaceholder\"\n :pulse=\"pulseAlt\"\n :disabled=\"block.decorative === true\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n <ToggleSwitch\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.decorative === true\"\n :label=\"t.image.decorative\"\n @update:model-value=\"updateField('decorative', $event)\"\n >\n <span>\n {{ t.image.decorative }}\n <span class=\"tpl:block tpl:text-[var(--tpl-text-dim)]\">\n {{ t.image.decorativeHint }}\n </span>\n </span>\n </ToggleSwitch>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"widthMode\"\n @change=\"updateWidthMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"full\">{{ t.image.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n <option value=\"custom\">{{ t.image.widthCustom }}</option>\n </select>\n <div\n v-if=\"widthMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"\n typeof block.width === 'number' ? block.width : DEFAULT_CUSTOM_WIDTH\n \"\n min=\"20\"\n @input=\"updateCustomWidth(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"image-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.image.heightAuto }}</option>\n <option value=\"custom\">{{ t.image.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"image-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.borderRadius }}</label>\n <!-- No `max`: a circle needs a radius of at least half the rendered size,\n which the block cannot know at edit time. -->\n <NumberWithSuffix\n :model-value=\"block.borderRadius ?? 0\"\n :min=\"0\"\n suffix=\"px\"\n testid=\"image-border-radius-input\"\n @update:model-value=\"updateBorderRadius\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.image.linkUrl }}</label>\n <MergeTagInput\n :model-value=\"block.linkUrl || ''\"\n type=\"url\"\n :placeholder=\"t.image.imageUrlPlaceholder\"\n @update:model-value=\"updateField('linkUrl', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.linkUrl\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.linkOpenInNewTab ?? false\"\n :label=\"t.image.openInNewTab\"\n @update:model-value=\"updateField('linkOpenInNewTab', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: MenuBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<MenuBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst ITEM_TOGGLES = computed(() => [\n { key: \"openInNewTab\" as const, label: t.menu.openInNewTab },\n { key: \"bold\" as const, label: t.menu.bold },\n { key: \"underline\" as const, label: t.menu.underline },\n]);\n\nconst ALIGN_OPTIONS = computed(() => [\n { value: \"left\", label: t.title.alignLeft, icon: AlignLeft },\n { value: \"center\", label: t.title.alignCenter, icon: AlignCenter },\n { value: \"right\", label: t.title.alignRight, icon: AlignRight },\n]);\n\nfunction updateField(field: keyof MenuBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<MenuBlock>);\n}\n\nfunction addMenuItem(): void {\n const newItem: MenuItemData = {\n id: generateId(),\n text: \"\",\n url: \"\",\n openInNewTab: false,\n bold: false,\n underline: false,\n };\n emit(\"update\", { items: [...props.block.items, newItem] });\n}\n\nfunction updateMenuItem(\n itemId: string,\n field: keyof MenuItemData,\n value: unknown,\n): void {\n const updatedItems = props.block.items.map((item) =>\n item.id === itemId ? { ...item, [field]: value } : item,\n );\n emit(\"update\", { items: updatedItems });\n}\n\nfunction removeMenuItem(itemId: string): void {\n emit(\"update\", {\n items: props.block.items.filter((item) => item.id !== itemId),\n });\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.menu.items\">\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"item in block.items\"\n :key=\"item.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"item.text\"\n :placeholder=\"t.menu.text\"\n @input=\"\n updateMenuItem(\n item.id,\n 'text',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.menu.removeItem\"\n @click=\"removeMenuItem(item.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"item.url\"\n type=\"url\"\n :placeholder=\"t.menu.urlPlaceholder\"\n @update:model-value=\"updateMenuItem(item.id, 'url', $event)\"\n />\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:gap-x-3 tpl:gap-y-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <ToggleSwitch\n v-for=\"toggle in ITEM_TOGGLES\"\n :key=\"toggle.key\"\n :model-value=\"item[toggle.key]\"\n :label=\"toggle.label\"\n @update:model-value=\"updateMenuItem(item.id, toggle.key, $event)\"\n />\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <label :class=\"labelClass\" class=\"tpl:!mb-0\">{{\n t.menu.color\n }}</label>\n <ColorPicker\n swatch-only\n :model-value=\"item.color || block.linkColor || block.color || ''\"\n @update:model-value=\"updateMenuItem(item.id, 'color', $event)\"\n />\n </div>\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addMenuItem\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.menu.addItem }}\n </button>\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontSize\">\n <NumberWithSuffix\n :model-value=\"block.fontSize\"\n :min=\"8\"\n :max=\"48\"\n suffix=\"px\"\n @update:model-value=\"updateField('fontSize', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.color\">\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.linkColor\">\n <ColorPicker\n :model-value=\"block.linkColor || block.color || ''\"\n @update:model-value=\"updateField('linkColor', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.textAlign\">\n <SlidingPillSelect\n :options=\"ALIGN_OPTIONS\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separator\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.separator\"\n @input=\"\n updateField('separator', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separatorColor\">\n <ColorPicker\n :model-value=\"block.separatorColor\"\n @update:model-value=\"updateField('separatorColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.spacing\">\n <NumberWithSuffix\n :model-value=\"block.spacing\"\n :min=\"0\"\n :max=\"50\"\n suffix=\"px\"\n @update:model-value=\"updateField('spacing', $event)\"\n />\n </FieldRow>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport FieldRow from \"./FieldRow.vue\";\nimport NumberWithSuffix from \"./NumberWithSuffix.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: MenuBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<MenuBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst ITEM_TOGGLES = computed(() => [\n { key: \"openInNewTab\" as const, label: t.menu.openInNewTab },\n { key: \"bold\" as const, label: t.menu.bold },\n { key: \"underline\" as const, label: t.menu.underline },\n]);\n\nconst ALIGN_OPTIONS = computed(() => [\n { value: \"left\", label: t.title.alignLeft, icon: AlignLeft },\n { value: \"center\", label: t.title.alignCenter, icon: AlignCenter },\n { value: \"right\", label: t.title.alignRight, icon: AlignRight },\n]);\n\nfunction updateField(field: keyof MenuBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<MenuBlock>);\n}\n\nfunction addMenuItem(): void {\n const newItem: MenuItemData = {\n id: generateId(),\n text: \"\",\n url: \"\",\n openInNewTab: false,\n bold: false,\n underline: false,\n };\n emit(\"update\", { items: [...props.block.items, newItem] });\n}\n\nfunction updateMenuItem(\n itemId: string,\n field: keyof MenuItemData,\n value: unknown,\n): void {\n const updatedItems = props.block.items.map((item) =>\n item.id === itemId ? { ...item, [field]: value } : item,\n );\n emit(\"update\", { items: updatedItems });\n}\n\nfunction removeMenuItem(itemId: string): void {\n emit(\"update\", {\n items: props.block.items.filter((item) => item.id !== itemId),\n });\n}\n</script>\n\n<template>\n <FieldRow :label=\"t.menu.items\">\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"item in block.items\"\n :key=\"item.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"item.text\"\n :placeholder=\"t.menu.text\"\n @input=\"\n updateMenuItem(\n item.id,\n 'text',\n ($event.target as HTMLInputElement).value,\n )\n \"\n />\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.menu.removeItem\"\n @click=\"removeMenuItem(item.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"item.url\"\n type=\"url\"\n :placeholder=\"t.menu.urlPlaceholder\"\n @update:model-value=\"updateMenuItem(item.id, 'url', $event)\"\n />\n <div\n class=\"tpl:flex tpl:flex-wrap tpl:items-center tpl:gap-x-3 tpl:gap-y-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <ToggleSwitch\n v-for=\"toggle in ITEM_TOGGLES\"\n :key=\"toggle.key\"\n :model-value=\"item[toggle.key]\"\n :label=\"toggle.label\"\n @update:model-value=\"updateMenuItem(item.id, toggle.key, $event)\"\n />\n </div>\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <label :class=\"labelClass\" class=\"tpl:!mb-0\">{{\n t.menu.color\n }}</label>\n <ColorPicker\n swatch-only\n :model-value=\"item.color || block.linkColor || block.color || ''\"\n @update:model-value=\"updateMenuItem(item.id, 'color', $event)\"\n />\n </div>\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addMenuItem\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.menu.addItem }}\n </button>\n </div>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontFamily\">\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </FieldRow>\n\n <FieldRow :label=\"t.menu.fontSize\">\n <NumberWithSuffix\n :model-value=\"block.fontSize\"\n :min=\"8\"\n :max=\"48\"\n suffix=\"px\"\n @update:model-value=\"updateField('fontSize', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.color\">\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.linkColor\">\n <ColorPicker\n :model-value=\"block.linkColor || block.color || ''\"\n @update:model-value=\"updateField('linkColor', $event || undefined)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.textAlign\">\n <SlidingPillSelect\n :options=\"ALIGN_OPTIONS\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separator\">\n <input\n type=\"text\"\n :class=\"inputClass\"\n :value=\"block.separator\"\n @input=\"\n updateField('separator', ($event.target as HTMLInputElement).value)\n \"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.separatorColor\">\n <ColorPicker\n :model-value=\"block.separatorColor\"\n @update:model-value=\"updateField('separatorColor', $event)\"\n />\n </FieldRow>\n\n <FieldRow :label=\"t.menu.spacing\">\n <NumberWithSuffix\n :model-value=\"block.spacing\"\n :min=\"0\"\n :max=\"50\"\n suffix=\"px\"\n @update:model-value=\"updateField('spacing', $event)\"\n />\n </FieldRow>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ParagraphBlock } from \"@templatical/types\";\nimport { RICH_TEXT_SPACING } from \"@templatical/types\";\n\ndefineProps<{\n block: ParagraphBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ParagraphBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\" data-testid=\"paragraph-spacing\">\n <label :class=\"labelClass\">{{ t.paragraph.paragraphSpacing }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <!-- Shows the built-in gap when the block sets none, so the field always\n reads the gap actually in effect rather than an empty box. -->\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.paragraphSpacing ?? RICH_TEXT_SPACING.paragraphGap\"\n min=\"0\"\n max=\"64\"\n @input=\"\n emit('update', {\n paragraphSpacing: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { ParagraphBlock } from \"@templatical/types\";\nimport { RICH_TEXT_SPACING } from \"@templatical/types\";\n\ndefineProps<{\n block: ParagraphBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<ParagraphBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\" data-testid=\"paragraph-spacing\">\n <label :class=\"labelClass\">{{ t.paragraph.paragraphSpacing }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <!-- Shows the built-in gap when the block sets none, so the field always\n reads the gap actually in effect rather than an empty box. -->\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.paragraphSpacing ?? RICH_TEXT_SPACING.paragraphGap\"\n min=\"0\"\n max=\"64\"\n @input=\"\n emit('update', {\n paragraphSpacing: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n</template>\n","import type { Block, ColumnLayout } from \"@templatical/types\";\n\nfunction expectedColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\n\n/**\n * Resize a section's `children` array to match a new column layout, without\n * dropping any blocks. Growing pads with empty columns; shrinking merges\n * any excess columns' blocks into the last kept column so the user never\n * silently loses content.\n */\nexport function rebalanceColumnChildren(\n current: Block[][],\n layout: ColumnLayout,\n): Block[][] {\n const target = expectedColumnCount(layout);\n\n if (current.length === target) {\n return current;\n }\n\n if (current.length < target) {\n const padding = Array.from(\n { length: target - current.length },\n () => [] as Block[],\n );\n return [...current, ...padding];\n }\n\n const kept = current.slice(0, target);\n const overflow = current.slice(target).flat();\n const merged = [...kept[target - 1], ...overflow];\n return [...kept.slice(0, target - 1), merged];\n}\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport type {\n ColumnLayout,\n SectionBlock,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport { rebalanceColumnChildren } from \"../../utils/rebalanceColumnChildren\";\n\nconst props = defineProps<{\n block: SectionBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SectionBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst columnOptions = computed(() => [\n { value: \"1\" as ColumnLayout, label: t.section.column1 },\n { value: \"2\" as ColumnLayout, label: t.section.column2 },\n { value: \"3\" as ColumnLayout, label: t.section.column3 },\n { value: \"1-2\" as ColumnLayout, label: t.section.ratio12 },\n { value: \"2-1\" as ColumnLayout, label: t.section.ratio21 },\n]);\n\nfunction handleColumnsChange(event: Event): void {\n const columns = (event.target as HTMLSelectElement).value as ColumnLayout;\n const children = rebalanceColumnChildren(props.block.children, columns);\n emit(\"update\", { columns, children });\n}\n\nfunction handleStackOnMobileChange(checked: boolean): void {\n emit(\"update\", { stackOnMobile: checked });\n}\n\nfunction handleBorderRadiusChange(event: Event): void {\n const borderRadius = Number((event.target as HTMLInputElement).value);\n emit(\"update\", { borderRadius });\n}\n\nfunction setWrapperEnabled(enabled: boolean): void {\n emit(\"update\", {\n wrapper: enabled\n ? { padding: { top: 20, right: 20, bottom: 20, left: 20 } }\n : undefined,\n });\n}\n\nfunction updateWrapper(patch: Partial<SectionWrapper>): void {\n emit(\"update\", { wrapper: { ...props.block.wrapper, ...patch } });\n}\n\nfunction handleWrapperPadding(value: SpacingValue): void {\n updateWrapper({ padding: value });\n}\n\nfunction handleWrapperRadius(event: Event): void {\n updateWrapper({\n borderRadius: Number((event.target as HTMLInputElement).value),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.columns }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.columns\"\n @change=\"handleColumnsChange\"\n >\n <option\n v-for=\"option in columnOptions\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </div>\n <div v-if=\"block.columns !== '1'\" class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.stackOnMobile !== false\"\n :label=\"t.section.stackOnMobile\"\n @update:model-value=\"handleStackOnMobileChange($event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleBorderRadiusChange\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"!!block.wrapper\"\n :label=\"t.section.wrapperEnable\"\n @update:model-value=\"setWrapperEnabled($event)\"\n />\n <div\n v-if=\"block.wrapper\"\n class=\"tpl:mt-3 tpl:ml-0.5 tpl:space-y-3 tpl:border-l tpl:border-[var(--tpl-border)] tpl:pl-3\"\n >\n <div>\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.wrapper.backgroundColor ?? ''\"\n @update:model-value=\"updateWrapper({ backgroundColor: $event })\"\n />\n </div>\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"\n block.wrapper.padding ?? { top: 0, right: 0, bottom: 0, left: 0 }\n \"\n @update:model-value=\"handleWrapperPadding\"\n />\n <div>\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.wrapper.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleWrapperRadius\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SpacingControl from \"../SpacingControl.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport type {\n ColumnLayout,\n SectionBlock,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\nimport { rebalanceColumnChildren } from \"../../utils/rebalanceColumnChildren\";\n\nconst props = defineProps<{\n block: SectionBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SectionBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst columnOptions = computed(() => [\n { value: \"1\" as ColumnLayout, label: t.section.column1 },\n { value: \"2\" as ColumnLayout, label: t.section.column2 },\n { value: \"3\" as ColumnLayout, label: t.section.column3 },\n { value: \"1-2\" as ColumnLayout, label: t.section.ratio12 },\n { value: \"2-1\" as ColumnLayout, label: t.section.ratio21 },\n]);\n\nfunction handleColumnsChange(event: Event): void {\n const columns = (event.target as HTMLSelectElement).value as ColumnLayout;\n const children = rebalanceColumnChildren(props.block.children, columns);\n emit(\"update\", { columns, children });\n}\n\nfunction handleStackOnMobileChange(checked: boolean): void {\n emit(\"update\", { stackOnMobile: checked });\n}\n\nfunction handleBorderRadiusChange(event: Event): void {\n const borderRadius = Number((event.target as HTMLInputElement).value);\n emit(\"update\", { borderRadius });\n}\n\nfunction setWrapperEnabled(enabled: boolean): void {\n emit(\"update\", {\n wrapper: enabled\n ? { padding: { top: 20, right: 20, bottom: 20, left: 20 } }\n : undefined,\n });\n}\n\nfunction updateWrapper(patch: Partial<SectionWrapper>): void {\n emit(\"update\", { wrapper: { ...props.block.wrapper, ...patch } });\n}\n\nfunction handleWrapperPadding(value: SpacingValue): void {\n updateWrapper({ padding: value });\n}\n\nfunction handleWrapperRadius(event: Event): void {\n updateWrapper({\n borderRadius: Number((event.target as HTMLInputElement).value),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.columns }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.columns\"\n @change=\"handleColumnsChange\"\n >\n <option\n v-for=\"option in columnOptions\"\n :key=\"option.value\"\n :value=\"option.value\"\n >\n {{ option.label }}\n </option>\n </select>\n </div>\n <div v-if=\"block.columns !== '1'\" class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"block.stackOnMobile !== false\"\n :label=\"t.section.stackOnMobile\"\n @update:model-value=\"handleStackOnMobileChange($event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleBorderRadiusChange\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:text-[var(--tpl-text)]\"\n :model-value=\"!!block.wrapper\"\n :label=\"t.section.wrapperEnable\"\n @update:model-value=\"setWrapperEnabled($event)\"\n />\n <div\n v-if=\"block.wrapper\"\n class=\"tpl:mt-3 tpl:ml-0.5 tpl:space-y-3 tpl:border-l tpl:border-[var(--tpl-border)] tpl:pl-3\"\n >\n <div>\n <label :class=\"labelClass\">{{ t.blockSettings.color }}</label>\n <ColorPicker\n :model-value=\"block.wrapper.backgroundColor ?? ''\"\n @update:model-value=\"updateWrapper({ backgroundColor: $event })\"\n />\n </div>\n <SpacingControl\n :label=\"t.blockSettings.padding\"\n :model-value=\"\n block.wrapper.padding ?? { top: 0, right: 0, bottom: 0, left: 0 }\n \"\n @update:model-value=\"handleWrapperPadding\"\n />\n <div>\n <label :class=\"labelClass\">{{ t.section.borderRadius }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.wrapper.borderRadius ?? 0\"\n min=\"0\"\n max=\"50\"\n @input=\"handleWrapperRadius\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport { socialPlatformOptions } from \"../../constants/socialIcons\";\nimport type {\n SocialIcon,\n SocialIconsBlock,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\n\nconst props = defineProps<{\n block: SocialIconsBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SocialIconsBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<SocialIconsBlock>);\n}\n\nfunction addSocialIcon(): void {\n const newIcon: SocialIcon = {\n id: generateId(),\n platform: \"facebook\",\n url: \"\",\n };\n emit(\"update\", { icons: [...props.block.icons, newIcon] });\n}\n\nfunction updateSocialIcon(\n iconId: string,\n field: keyof SocialIcon,\n value: string,\n): void {\n const updatedIcons = props.block.icons.map((icon) =>\n icon.id === iconId ? { ...icon, [field]: value } : icon,\n );\n emit(\"update\", { icons: updatedIcons });\n}\n\nfunction removeSocialIcon(iconId: string): void {\n emit(\"update\", {\n icons: props.block.icons.filter((icon) => icon.id !== iconId),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.icons }}</label>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"icon in block.icons\"\n :key=\"icon.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <select\n data-testid=\"social-platform-select\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"icon.platform\"\n @change=\"\n updateSocialIcon(\n icon.id,\n 'platform',\n ($event.target as HTMLSelectElement).value as SocialPlatform,\n )\n \"\n >\n <option\n v-for=\"platform in socialPlatformOptions\"\n :key=\"platform\"\n :value=\"platform\"\n >\n {{ t.social.platforms[platform] }}\n </option>\n </select>\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.social.removeIcon\"\n @click=\"removeSocialIcon(icon.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"icon.url\"\n type=\"url\"\n :placeholder=\"t.social.urlPlaceholder\"\n @update:model-value=\"updateSocialIcon(icon.id, 'url', $event)\"\n />\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addSocialIcon\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.social.addIcon }}\n </button>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.style }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.iconStyle\"\n @change=\"\n updateField(\n 'iconStyle',\n ($event.target as HTMLSelectElement).value as SocialIconStyle,\n )\n \"\n >\n <option value=\"solid\">{{ t.social.styleSolid }}</option>\n <option value=\"outlined\">{{ t.social.styleOutlined }}</option>\n <option value=\"rounded\">{{ t.social.styleRounded }}</option>\n <option value=\"square\">{{ t.social.styleSquare }}</option>\n <option value=\"circle\">{{ t.social.styleCircle }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.size }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'small', label: t.social.sizeSmall },\n { value: 'medium', label: t.social.sizeMedium },\n { value: 'large', label: t.social.sizeLarge },\n ]\"\n :model-value=\"block.iconSize\"\n @update:model-value=\"updateField('iconSize', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.spacing }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.spacing\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'spacing',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n removeItemBtnClass,\n addItemBtnClass,\n} from \"../../constants/styleConstants\";\nimport { socialPlatformOptions } from \"../../constants/socialIcons\";\nimport type {\n SocialIcon,\n SocialIconsBlock,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Plus, X } from \"@lucide/vue\";\n\nconst props = defineProps<{\n block: SocialIconsBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SocialIconsBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<SocialIconsBlock>);\n}\n\nfunction addSocialIcon(): void {\n const newIcon: SocialIcon = {\n id: generateId(),\n platform: \"facebook\",\n url: \"\",\n };\n emit(\"update\", { icons: [...props.block.icons, newIcon] });\n}\n\nfunction updateSocialIcon(\n iconId: string,\n field: keyof SocialIcon,\n value: string,\n): void {\n const updatedIcons = props.block.icons.map((icon) =>\n icon.id === iconId ? { ...icon, [field]: value } : icon,\n );\n emit(\"update\", { icons: updatedIcons });\n}\n\nfunction removeSocialIcon(iconId: string): void {\n emit(\"update\", {\n icons: props.block.icons.filter((icon) => icon.id !== iconId),\n });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.icons }}</label>\n <div class=\"tpl:flex tpl:flex-col tpl:gap-2\">\n <div\n v-for=\"icon in block.icons\"\n :key=\"icon.id\"\n class=\"tpl:flex tpl:flex-col tpl:gap-1.5 tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:p-2\"\n >\n <div class=\"tpl:flex tpl:items-center tpl:gap-2\">\n <select\n data-testid=\"social-platform-select\"\n :class=\"inputClass\"\n class=\"tpl:flex-1\"\n :value=\"icon.platform\"\n @change=\"\n updateSocialIcon(\n icon.id,\n 'platform',\n ($event.target as HTMLSelectElement).value as SocialPlatform,\n )\n \"\n >\n <option\n v-for=\"platform in socialPlatformOptions\"\n :key=\"platform\"\n :value=\"platform\"\n >\n {{ t.social.platforms[platform] }}\n </option>\n </select>\n <button\n :class=\"removeItemBtnClass\"\n :title=\"t.social.removeIcon\"\n @click=\"removeSocialIcon(icon.id)\"\n >\n <X :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n <MergeTagInput\n :model-value=\"icon.url\"\n type=\"url\"\n :placeholder=\"t.social.urlPlaceholder\"\n @update:model-value=\"updateSocialIcon(icon.id, 'url', $event)\"\n />\n </div>\n <button :class=\"addItemBtnClass\" @click=\"addSocialIcon\">\n <Plus :size=\"14\" :stroke-width=\"2\" />\n {{ t.social.addIcon }}\n </button>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.style }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.iconStyle\"\n @change=\"\n updateField(\n 'iconStyle',\n ($event.target as HTMLSelectElement).value as SocialIconStyle,\n )\n \"\n >\n <option value=\"solid\">{{ t.social.styleSolid }}</option>\n <option value=\"outlined\">{{ t.social.styleOutlined }}</option>\n <option value=\"rounded\">{{ t.social.styleRounded }}</option>\n <option value=\"square\">{{ t.social.styleSquare }}</option>\n <option value=\"circle\">{{ t.social.styleCircle }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.size }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'small', label: t.social.sizeSmall },\n { value: 'medium', label: t.social.sizeMedium },\n { value: 'large', label: t.social.sizeLarge },\n ]\"\n :model-value=\"block.iconSize\"\n @update:model-value=\"updateField('iconSize', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.spacing }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.spacing\"\n min=\"0\"\n max=\"50\"\n @input=\"\n updateField(\n 'spacing',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.social.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { SpacerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: SpacerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SpacerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.spacer.height }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n <input\n type=\"range\"\n class=\"tpl:mt-2 tpl:w-full tpl:accent-[var(--tpl-primary)]\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { SpacerBlock } from \"@templatical/types\";\n\ndefineProps<{\n block: SpacerBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<SpacerBlock>): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.spacer.height }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n <input\n type=\"range\"\n class=\"tpl:mt-2 tpl:w-full tpl:accent-[var(--tpl-primary)]\"\n :value=\"block.height\"\n min=\"10\"\n max=\"100\"\n @input=\"\n emit('update', {\n height: Number(($event.target as HTMLInputElement).value),\n })\n \"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_TABLE_ROW_BG,\n} from \"../../constants/styleConstants\";\nimport type {\n TableBlock,\n TableCellData,\n TableRowData,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Minus, Plus } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: TableBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TableBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst tableColumnCount = computed(() => {\n return props.block.rows.length > 0 ? props.block.rows[0].cells.length : 0;\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TableBlock>);\n}\n\nfunction addTableRow(): void {\n const columnCount =\n props.block.rows.length > 0 ? props.block.rows[0].cells.length : 3;\n const newRow: TableRowData = {\n id: generateId(),\n cells: Array.from({ length: columnCount }, (): TableCellData => ({\n id: generateId(),\n content: \"\",\n })),\n };\n emit(\"update\", { rows: [...props.block.rows, newRow] });\n}\n\nfunction removeTableRow(rowId: string): void {\n emit(\"update\", {\n rows: props.block.rows.filter((row) => row.id !== rowId),\n });\n}\n\nfunction addTableColumn(): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: [...row.cells, { id: generateId(), content: \"\" } as TableCellData],\n }));\n emit(\"update\", { rows: updatedRows });\n}\n\nfunction removeTableColumn(colIndex: number): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: row.cells.filter((_, i) => i !== colIndex),\n }));\n emit(\"update\", { rows: updatedRows });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.dimensions }}</label>\n <div class=\"tpl:flex tpl:items-center tpl:gap-3\">\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.rows\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"block.rows.length <= 1\"\n @click=\"removeTableRow(block.rows[block.rows.length - 1].id)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ block.rows.length }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableRow\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.columns\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"tableColumnCount <= 1\"\n @click=\"removeTableColumn(tableColumnCount - 1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ tableColumnCount }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableColumn\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.hasHeaderRow\"\n :label=\"t.table.hasHeaderRow\"\n @update:model-value=\"updateField('hasHeaderRow', $event)\"\n />\n </div>\n <div v-if=\"block.hasHeaderRow\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.headerBackgroundColor }}</label>\n <ColorPicker\n :model-value=\"block.headerBackgroundColor ?? ''\"\n :seed-color=\"DEFAULT_TABLE_ROW_BG\"\n :placeholder=\"t.table.noHeaderBg\"\n @update:model-value=\"updateField('headerBackgroundColor', $event || null)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderColor }}</label>\n <ColorPicker\n :model-value=\"block.borderColor\"\n @update:model-value=\"updateField('borderColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderWidth }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderWidth\"\n min=\"0\"\n max=\"10\"\n @input=\"\n updateField(\n 'borderWidth',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.cellPadding }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.cellPadding\"\n min=\"0\"\n max=\"30\"\n @input=\"\n updateField(\n 'cellPadding',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"32\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.textAlign }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n DEFAULT_TABLE_ROW_BG,\n} from \"../../constants/styleConstants\";\nimport type {\n TableBlock,\n TableCellData,\n TableRowData,\n} from \"@templatical/types\";\nimport { generateId } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight, Minus, Plus } from \"@lucide/vue\";\nimport { computed } from \"vue\";\n\nconst props = defineProps<{\n block: TableBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TableBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nconst tableColumnCount = computed(() => {\n return props.block.rows.length > 0 ? props.block.rows[0].cells.length : 0;\n});\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TableBlock>);\n}\n\nfunction addTableRow(): void {\n const columnCount =\n props.block.rows.length > 0 ? props.block.rows[0].cells.length : 3;\n const newRow: TableRowData = {\n id: generateId(),\n cells: Array.from({ length: columnCount }, (): TableCellData => ({\n id: generateId(),\n content: \"\",\n })),\n };\n emit(\"update\", { rows: [...props.block.rows, newRow] });\n}\n\nfunction removeTableRow(rowId: string): void {\n emit(\"update\", {\n rows: props.block.rows.filter((row) => row.id !== rowId),\n });\n}\n\nfunction addTableColumn(): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: [...row.cells, { id: generateId(), content: \"\" } as TableCellData],\n }));\n emit(\"update\", { rows: updatedRows });\n}\n\nfunction removeTableColumn(colIndex: number): void {\n const updatedRows = props.block.rows.map((row) => ({\n ...row,\n cells: row.cells.filter((_, i) => i !== colIndex),\n }));\n emit(\"update\", { rows: updatedRows });\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.dimensions }}</label>\n <div class=\"tpl:flex tpl:items-center tpl:gap-3\">\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.rows\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"block.rows.length <= 1\"\n @click=\"removeTableRow(block.rows[block.rows.length - 1].id)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ block.rows.length }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableRow\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n <div class=\"tpl:flex tpl:flex-1 tpl:items-center tpl:gap-1.5\">\n <span class=\"tpl:text-xs tpl:text-[var(--tpl-text-muted)]\">{{\n t.table.columns\n }}</span>\n <div\n class=\"tpl:flex tpl:items-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)]\"\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)] tpl:disabled:opacity-30\"\n :disabled=\"tableColumnCount <= 1\"\n @click=\"removeTableColumn(tableColumnCount - 1)\"\n >\n <Minus :size=\"12\" :stroke-width=\"2\" />\n </button>\n <span\n class=\"tpl:min-w-[20px] tpl:text-center tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text)]\"\n >{{ tableColumnCount }}</span\n >\n <button\n class=\"tpl:flex tpl:items-center tpl:justify-center tpl:px-1.5 tpl:py-1 tpl:text-[var(--tpl-text-muted)] tpl:transition-colors tpl:hover:text-[var(--tpl-primary)]\"\n @click=\"addTableColumn\"\n >\n <Plus :size=\"12\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <ToggleSwitch\n class=\"tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.hasHeaderRow\"\n :label=\"t.table.hasHeaderRow\"\n @update:model-value=\"updateField('hasHeaderRow', $event)\"\n />\n </div>\n <div v-if=\"block.hasHeaderRow\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.headerBackgroundColor }}</label>\n <ColorPicker\n :model-value=\"block.headerBackgroundColor ?? ''\"\n :seed-color=\"DEFAULT_TABLE_ROW_BG\"\n :placeholder=\"t.table.noHeaderBg\"\n @update:model-value=\"updateField('headerBackgroundColor', $event || null)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderColor }}</label>\n <ColorPicker\n :model-value=\"block.borderColor\"\n @update:model-value=\"updateField('borderColor', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.borderWidth }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.borderWidth\"\n min=\"0\"\n max=\"10\"\n @input=\"\n updateField(\n 'borderWidth',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.cellPadding }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.cellPadding\"\n min=\"0\"\n max=\"30\"\n @input=\"\n updateField(\n 'cellPadding',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.fontSize }}</label>\n <div class=\"tpl:flex tpl:items-stretch\">\n <input\n type=\"number\"\n :class=\"inputGroupInputClass\"\n :value=\"block.fontSize\"\n min=\"10\"\n max=\"32\"\n @input=\"\n updateField(\n 'fontSize',\n Number(($event.target as HTMLInputElement).value),\n )\n \"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.table.textAlign }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass, labelClass } from \"../../constants/styleConstants\";\nimport type { TitleBlock } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight } from \"@lucide/vue\";\n\ndefineProps<{\n block: TitleBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TitleBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TitleBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.level }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.level\"\n @change=\"\n updateField('level', Number(($event.target as HTMLSelectElement).value))\n \"\n >\n <option :value=\"1\">{{ t.title.heading1 }}</option>\n <option :value=\"2\">{{ t.title.heading2 }}</option>\n <option :value=\"3\">{{ t.title.heading3 }}</option>\n <option :value=\"4\">{{ t.title.heading4 }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport ColorPicker from \"../ColorPicker.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport { inputClass, labelClass } from \"../../constants/styleConstants\";\nimport type { TitleBlock } from \"@templatical/types\";\nimport { AlignCenter, AlignLeft, AlignRight } from \"@lucide/vue\";\n\ndefineProps<{\n block: TitleBlock;\n fontFamilies: Array<{ value: string; label: string }>;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<TitleBlock>): void;\n}>();\n\nconst { t } = useI18n();\n\nfunction updateField(field: string, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<TitleBlock>);\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.level }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.level\"\n @change=\"\n updateField('level', Number(($event.target as HTMLSelectElement).value))\n \"\n >\n <option :value=\"1\">{{ t.title.heading1 }}</option>\n <option :value=\"2\">{{ t.title.heading2 }}</option>\n <option :value=\"3\">{{ t.title.heading3 }}</option>\n <option :value=\"4\">{{ t.title.heading4 }}</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.fontFamily }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.fontFamily || ''\"\n @change=\"\n updateField(\n 'fontFamily',\n ($event.target as HTMLSelectElement).value || undefined,\n )\n \"\n >\n <option value=\"\">{{ t.title.inheritFont }}</option>\n <option\n v-for=\"font in fontFamilies\"\n :key=\"font.value\"\n :value=\"font.value\"\n >\n {{ font.label }}\n </option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.color }}</label>\n <ColorPicker\n :model-value=\"block.color ?? ''\"\n @update:model-value=\"updateField('color', $event || undefined)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft, icon: AlignLeft },\n { value: 'center', label: t.title.alignCenter, icon: AlignCenter },\n { value: 'right', label: t.title.alignRight, icon: AlignRight },\n ]\"\n :model-value=\"block.textAlign\"\n @update:model-value=\"updateField('textAlign', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { VideoBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: VideoBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<VideoBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\nconst urlHasMergeTag = computed(() =>\n containsMergeTag(props.block.url, mergeTagSyntax),\n);\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nconst pulseThumbnail = ref(false);\nconst { start: startPulseThumbnail } = useTimeoutFn(\n () => {\n pulseThumbnail.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nfunction updateField(field: keyof VideoBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<VideoBlock>);\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // An empty number field yields Number(\"\") === 0, which would collapse the\n // thumbnail. Absent means \"keep the aspect ratio\", so an invalid value must\n // not overwrite the last valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"thumbnailUrl\", result.url);\n pulseThumbnail.value = true;\n startPulseThumbnail();\n }\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.videoUrl }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.video.videoUrlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.video.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div v-if=\"urlHasMergeTag\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.video.placeholderUrlPlaceholder\"\n :title=\"t.video.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.customThumbnail }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <MergeTagInput\n :model-value=\"block.thumbnailUrl\"\n type=\"url\"\n :placeholder=\"t.video.thumbnailPlaceholder\"\n :pulse=\"pulseThumbnail\"\n @update:model-value=\"updateField('thumbnailUrl', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.video.altTextPlaceholder\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.width\"\n @change=\"\n updateField(\n 'width',\n ($event.target as HTMLSelectElement).value === 'full'\n ? 'full'\n : Number(($event.target as HTMLSelectElement).value),\n )\n \"\n >\n <option value=\"full\">{{ t.video.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"video-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.video.heightAuto }}</option>\n <option value=\"custom\">{{ t.video.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"video-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport MergeTagInput from \"../MergeTagInput.vue\";\nimport SlidingPillSelect from \"../SlidingPillSelect.vue\";\nimport ToggleSwitch from \"../ToggleSwitch.vue\";\nimport { useI18n } from \"../../composables/useI18n\";\nimport {\n inputClass,\n inputGroupInputClass,\n inputSuffixClass,\n labelClass,\n} from \"../../constants/styleConstants\";\nimport type { VideoBlock } from \"@templatical/types\";\nimport { containsMergeTag, SYNTAX_PRESETS } from \"@templatical/types\";\nimport { Image } from \"@lucide/vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { ON_REQUEST_MEDIA_KEY, MERGE_TAG_SYNTAX_KEY } from \"../../keys\";\nimport { useAliveFlag } from \"../../composables/useAliveFlag\";\nimport { useTimeoutFn } from \"@vueuse/core\";\n\nconst props = defineProps<{\n block: VideoBlock;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<VideoBlock>): void;\n}>();\n\nconst { t } = useI18n();\nconst onRequestMedia = inject(ON_REQUEST_MEDIA_KEY, null);\nconst mergeTagSyntax = inject(MERGE_TAG_SYNTAX_KEY, SYNTAX_PRESETS.liquid);\nconst aliveFlag = useAliveFlag();\n\nconst DEFAULT_CUSTOM_HEIGHT = 200;\n\nconst canBrowseMedia = computed(() => !!onRequestMedia);\nconst urlHasMergeTag = computed(() =>\n containsMergeTag(props.block.url, mergeTagSyntax),\n);\n\nconst heightMode = computed(() =>\n typeof props.block.height === \"number\" ? \"custom\" : \"auto\",\n);\n\nconst pulseThumbnail = ref(false);\nconst { start: startPulseThumbnail } = useTimeoutFn(\n () => {\n pulseThumbnail.value = false;\n },\n 1000,\n { immediate: false },\n);\n\nfunction updateField(field: keyof VideoBlock, value: unknown): void {\n emit(\"update\", { [field]: value } as Partial<VideoBlock>);\n}\n\nfunction updateHeightMode(value: string): void {\n updateField(\"height\", value === \"custom\" ? DEFAULT_CUSTOM_HEIGHT : undefined);\n}\n\nfunction updateCustomHeight(raw: string): void {\n // An empty number field yields Number(\"\") === 0, which would collapse the\n // thumbnail. Absent means \"keep the aspect ratio\", so an invalid value must\n // not overwrite the last valid height either.\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return;\n updateField(\"height\", n);\n}\n\nasync function openMediaBrowser(): Promise<void> {\n const result = await onRequestMedia?.({ accept: [\"images\"] });\n if (!aliveFlag.alive) return;\n if (result) {\n updateField(\"thumbnailUrl\", result.url);\n pulseThumbnail.value = true;\n startPulseThumbnail();\n }\n}\n</script>\n\n<template>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.videoUrl }}</label>\n <MergeTagInput\n :model-value=\"block.url\"\n type=\"url\"\n :placeholder=\"t.video.videoUrlPlaceholder\"\n @update:model-value=\"updateField('url', $event)\"\n />\n <ToggleSwitch\n v-if=\"block.url\"\n class=\"tpl:mt-2 tpl:text-[12px] tpl:text-[var(--tpl-text-muted)]\"\n :model-value=\"block.openInNewTab ?? false\"\n :label=\"t.video.openInNewTab\"\n @update:model-value=\"updateField('openInNewTab', $event)\"\n />\n </div>\n <div v-if=\"urlHasMergeTag\" class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.placeholderUrl }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <input\n type=\"url\"\n :class=\"inputClass\"\n :value=\"block.placeholderUrl || ''\"\n :placeholder=\"t.video.placeholderUrlPlaceholder\"\n :title=\"t.video.placeholderUrlTooltip\"\n @input=\"\n updateField('placeholderUrl', ($event.target as HTMLInputElement).value)\n \"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">\n {{ t.video.customThumbnail }}\n <span class=\"tpl:font-normal tpl:text-[var(--tpl-text-dim)]\">\n {{ t.video.optional }}\n </span>\n </label>\n <MergeTagInput\n :model-value=\"block.thumbnailUrl\"\n type=\"url\"\n :placeholder=\"t.video.thumbnailPlaceholder\"\n :pulse=\"pulseThumbnail\"\n @update:model-value=\"updateField('thumbnailUrl', $event)\"\n />\n <button\n v-if=\"canBrowseMedia\"\n class=\"tpl:mt-2 tpl:flex tpl:w-full tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-md tpl:border tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all\"\n style=\"\n border-color: var(--tpl-border);\n color: var(--tpl-primary);\n background-color: var(--tpl-bg);\n \"\n @click=\"openMediaBrowser\"\n >\n <Image :size=\"14\" :stroke-width=\"1.5\" />\n {{ t.image.browseMedia }}\n </button>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.altText }}</label>\n <MergeTagInput\n :model-value=\"block.alt\"\n type=\"text\"\n :placeholder=\"t.video.altTextPlaceholder\"\n @update:model-value=\"updateField('alt', $event)\"\n />\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.width }}</label>\n <select\n :class=\"inputClass\"\n :value=\"block.width\"\n @change=\"\n updateField(\n 'width',\n ($event.target as HTMLSelectElement).value === 'full'\n ? 'full'\n : Number(($event.target as HTMLSelectElement).value),\n )\n \"\n >\n <option value=\"full\">{{ t.video.fullWidth }}</option>\n <option value=\"300\">300px</option>\n <option value=\"400\">400px</option>\n <option value=\"500\">500px</option>\n </select>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.video.height }}</label>\n <select\n :class=\"inputClass\"\n data-testid=\"video-height-mode\"\n :value=\"heightMode\"\n @change=\"updateHeightMode(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"auto\">{{ t.video.heightAuto }}</option>\n <option value=\"custom\">{{ t.video.heightCustom }}</option>\n </select>\n <div\n v-if=\"heightMode === 'custom'\"\n class=\"tpl:mt-2 tpl:flex tpl:items-stretch\"\n >\n <input\n type=\"number\"\n data-testid=\"video-height-input\"\n :class=\"inputGroupInputClass\"\n :value=\"block.height\"\n min=\"20\"\n @input=\"updateCustomHeight(($event.target as HTMLInputElement).value)\"\n />\n <span :class=\"inputSuffixClass\">px</span>\n </div>\n </div>\n <div class=\"tpl:mb-3.5\">\n <label :class=\"labelClass\">{{ t.title.align }}</label>\n <SlidingPillSelect\n :options=\"[\n { value: 'left', label: t.title.alignLeft },\n { value: 'center', label: t.title.alignCenter },\n { value: 'right', label: t.title.alignRight },\n ]\"\n :model-value=\"block.align\"\n @update:model-value=\"updateField('align', $event)\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport ButtonToolbar from \"./toolbar/ButtonToolbar.vue\";\nimport CommonBlockSettings from \"./toolbar/CommonBlockSettings.vue\";\nconst CountdownToolbar = defineAsyncComponent(\n () => import(\"./toolbar/CountdownToolbar.vue\"),\n);\nimport CustomBlockToolbar from \"./toolbar/CustomBlockToolbar.vue\";\nimport DividerToolbar from \"./toolbar/DividerToolbar.vue\";\nimport HtmlToolbar from \"./toolbar/HtmlToolbar.vue\";\nimport ImageToolbar from \"./toolbar/ImageToolbar.vue\";\nimport MenuToolbar from \"./toolbar/MenuToolbar.vue\";\nimport ParagraphToolbar from \"./toolbar/ParagraphToolbar.vue\";\nimport SectionToolbar from \"./toolbar/SectionToolbar.vue\";\nimport SocialToolbar from \"./toolbar/SocialToolbar.vue\";\nimport SpacerToolbar from \"./toolbar/SpacerToolbar.vue\";\nimport TableToolbar from \"./toolbar/TableToolbar.vue\";\nimport TitleToolbar from \"./toolbar/TitleToolbar.vue\";\nimport VideoToolbar from \"./toolbar/VideoToolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type {\n Block,\n ButtonBlock,\n CountdownBlock,\n CustomBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n ParagraphBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n TableBlock,\n TitleBlock,\n VideoBlock,\n} from \"@templatical/types\";\nimport { isCustomBlock } from \"@templatical/types\";\nimport { Code, Copy, Trash2 } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport {\n FONTS_MANAGER_KEY,\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n requireInject,\n} from \"../keys\";\n\nconst props = defineProps<{\n block: Block;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n (e: \"delete\"): void;\n (e: \"duplicate\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"Toolbar\");\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst blockType = computed(() => props.block.type);\n\nconst isCustom = computed(() => isCustomBlock(props.block));\n\nconst customBlockDefinition = computed(() => {\n if (!isCustom.value) {\n return undefined;\n }\n return customBlockDefinitions.find(\n (d) => d.type === (props.block as CustomBlock).customType,\n );\n});\n\nconst blockTypeLabel = computed(() => {\n if (isCustom.value) {\n return (\n customBlockDefinition.value?.name ??\n (props.block as CustomBlock).customType\n );\n }\n\n return getBlockTypeLabel(blockType.value, t);\n});\n\n// Font families from shared fontsManager (provided by Editor.vue)\nconst fontFamilies = fontsManager.fonts;\n\nfunction handleUpdate(updates: Partial<Block>): void {\n emit(\"update\", updates);\n}\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.blockToolbar\"\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3.5\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-[var(--tpl-primary)]\"\n >\n <component\n :is=\"blockTypeIcons[blockType]\"\n v-if=\"blockTypeIcons[blockType]\"\n :size=\"16\"\n :stroke-width=\"1.5\"\n />\n <Code v-else-if=\"isCustom\" :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ blockTypeLabel }}\n </h3>\n </div>\n <div class=\"tpl:flex tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)] tpl:hover:text-[var(--tpl-text)]\"\n :title=\"t.toolbar.duplicate\"\n @click=\"emit('duplicate')\"\n >\n <Copy :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.toolbar.delete\"\n @click=\"emit('delete')\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <template v-if=\"isCustom\">\n <CustomBlockToolbar\n :block=\"block as CustomBlock\"\n @update-field-values=\"emit('update', { fieldValues: $event })\"\n @update-data-source-fetched=\"\n emit('update', { dataSourceFetched: $event })\n \"\n />\n </template>\n\n <SectionToolbar\n v-else-if=\"blockType === 'section'\"\n :block=\"block as SectionBlock\"\n @update=\"handleUpdate\"\n />\n\n <TitleToolbar\n v-else-if=\"blockType === 'title'\"\n :block=\"block as TitleBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Inline formatting lives in the TipTap toolbar; this carries only\n what applies to the block as a whole. -->\n <ParagraphToolbar\n v-else-if=\"blockType === 'paragraph'\"\n :block=\"block as ParagraphBlock\"\n @update=\"handleUpdate\"\n />\n\n <ImageToolbar\n v-else-if=\"blockType === 'image'\"\n :block=\"block as ImageBlock\"\n @update=\"handleUpdate\"\n />\n\n <VideoToolbar\n v-else-if=\"blockType === 'video'\"\n :block=\"block as VideoBlock\"\n @update=\"handleUpdate\"\n />\n\n <ButtonToolbar\n v-else-if=\"blockType === 'button'\"\n :block=\"block as ButtonBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <DividerToolbar\n v-else-if=\"blockType === 'divider'\"\n :block=\"block as DividerBlock\"\n @update=\"handleUpdate\"\n />\n\n <SocialToolbar\n v-else-if=\"blockType === 'social'\"\n :block=\"block as SocialIconsBlock\"\n @update=\"handleUpdate\"\n />\n\n <MenuToolbar\n v-else-if=\"blockType === 'menu'\"\n :block=\"block as MenuBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <TableToolbar\n v-else-if=\"blockType === 'table'\"\n :block=\"block as TableBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <SpacerToolbar\n v-else-if=\"blockType === 'spacer'\"\n :block=\"block as SpacerBlock\"\n @update=\"handleUpdate\"\n />\n\n <HtmlToolbar\n v-else-if=\"blockType === 'html'\"\n :block=\"block as HtmlBlock\"\n @update=\"handleUpdate\"\n />\n\n <CountdownToolbar\n v-else-if=\"blockType === 'countdown'\"\n :block=\"block as CountdownBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Common block settings -->\n <CommonBlockSettings\n :block=\"block\"\n :is-first-section=\"blockType === 'paragraph'\"\n @update=\"handleUpdate\"\n />\n </div>\n </aside>\n</template>\n\n<style scoped>\n.tpl-collapsible {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-collapsible--open {\n grid-template-rows: 1fr;\n}\n\n.tpl-collapsible > div {\n overflow: hidden;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport ButtonToolbar from \"./toolbar/ButtonToolbar.vue\";\nimport CommonBlockSettings from \"./toolbar/CommonBlockSettings.vue\";\nconst CountdownToolbar = defineAsyncComponent(\n () => import(\"./toolbar/CountdownToolbar.vue\"),\n);\nimport CustomBlockToolbar from \"./toolbar/CustomBlockToolbar.vue\";\nimport DividerToolbar from \"./toolbar/DividerToolbar.vue\";\nimport HtmlToolbar from \"./toolbar/HtmlToolbar.vue\";\nimport ImageToolbar from \"./toolbar/ImageToolbar.vue\";\nimport MenuToolbar from \"./toolbar/MenuToolbar.vue\";\nimport ParagraphToolbar from \"./toolbar/ParagraphToolbar.vue\";\nimport SectionToolbar from \"./toolbar/SectionToolbar.vue\";\nimport SocialToolbar from \"./toolbar/SocialToolbar.vue\";\nimport SpacerToolbar from \"./toolbar/SpacerToolbar.vue\";\nimport TableToolbar from \"./toolbar/TableToolbar.vue\";\nimport TitleToolbar from \"./toolbar/TitleToolbar.vue\";\nimport VideoToolbar from \"./toolbar/VideoToolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport type {\n Block,\n ButtonBlock,\n CountdownBlock,\n CustomBlock,\n DividerBlock,\n HtmlBlock,\n ImageBlock,\n MenuBlock,\n ParagraphBlock,\n SectionBlock,\n SocialIconsBlock,\n SpacerBlock,\n TableBlock,\n TitleBlock,\n VideoBlock,\n} from \"@templatical/types\";\nimport { isCustomBlock } from \"@templatical/types\";\nimport { Code, Copy, Trash2 } from \"@lucide/vue\";\nimport { computed, inject } from \"vue\";\nimport { blockTypeIcons } from \"../utils/blockTypeIcons\";\nimport { getBlockTypeLabel } from \"../utils/blockTypeLabels\";\nimport {\n FONTS_MANAGER_KEY,\n CUSTOM_BLOCK_DEFINITIONS_KEY,\n requireInject,\n} from \"../keys\";\n\nconst props = defineProps<{\n block: Block;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update\", updates: Partial<Block>): void;\n (e: \"delete\"): void;\n (e: \"duplicate\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst fontsManager = requireInject(FONTS_MANAGER_KEY, \"Toolbar\");\nconst customBlockDefinitions = inject(CUSTOM_BLOCK_DEFINITIONS_KEY, []);\n\nconst blockType = computed(() => props.block.type);\n\nconst isCustom = computed(() => isCustomBlock(props.block));\n\nconst customBlockDefinition = computed(() => {\n if (!isCustom.value) {\n return undefined;\n }\n return customBlockDefinitions.find(\n (d) => d.type === (props.block as CustomBlock).customType,\n );\n});\n\nconst blockTypeLabel = computed(() => {\n if (isCustom.value) {\n return (\n customBlockDefinition.value?.name ??\n (props.block as CustomBlock).customType\n );\n }\n\n return getBlockTypeLabel(blockType.value, t);\n});\n\n// Font families from shared fontsManager (provided by Editor.vue)\nconst fontFamilies = fontsManager.fonts;\n\nfunction handleUpdate(updates: Partial<Block>): void {\n emit(\"update\", updates);\n}\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.blockToolbar\"\n class=\"tpl:flex tpl:w-full tpl:flex-1 tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)]\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-4 tpl:py-3.5\"\n >\n <div\n class=\"tpl:flex tpl:items-center tpl:gap-2 tpl:text-[var(--tpl-primary)]\"\n >\n <component\n :is=\"blockTypeIcons[blockType]\"\n v-if=\"blockTypeIcons[blockType]\"\n :size=\"16\"\n :stroke-width=\"1.5\"\n />\n <Code v-else-if=\"isCustom\" :size=\"16\" :stroke-width=\"1.5\" />\n <h3\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ blockTypeLabel }}\n </h3>\n </div>\n <div class=\"tpl:flex tpl:gap-1\">\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:bg-[var(--tpl-bg-active)] tpl:hover:text-[var(--tpl-text)]\"\n :title=\"t.toolbar.duplicate\"\n @click=\"emit('duplicate')\"\n >\n <Copy :size=\"14\" :stroke-width=\"2\" />\n </button>\n <button\n class=\"tpl:flex tpl:size-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-md tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-hover)] tpl:text-[var(--tpl-text-muted)] tpl:transition-all tpl:hover:border-[var(--tpl-danger)] tpl:hover:bg-[var(--tpl-danger-light)] tpl:hover:text-[var(--tpl-danger)]\"\n :title=\"t.toolbar.delete\"\n @click=\"emit('delete')\"\n >\n <Trash2 :size=\"14\" :stroke-width=\"2\" />\n </button>\n </div>\n </div>\n\n <div class=\"tpl:flex-1 tpl:overflow-y-auto tpl:p-4\">\n <template v-if=\"isCustom\">\n <CustomBlockToolbar\n :block=\"block as CustomBlock\"\n @update-field-values=\"emit('update', { fieldValues: $event })\"\n @update-data-source-fetched=\"\n emit('update', { dataSourceFetched: $event })\n \"\n />\n </template>\n\n <SectionToolbar\n v-else-if=\"blockType === 'section'\"\n :block=\"block as SectionBlock\"\n @update=\"handleUpdate\"\n />\n\n <TitleToolbar\n v-else-if=\"blockType === 'title'\"\n :block=\"block as TitleBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Inline formatting lives in the TipTap toolbar; this carries only\n what applies to the block as a whole. -->\n <ParagraphToolbar\n v-else-if=\"blockType === 'paragraph'\"\n :block=\"block as ParagraphBlock\"\n @update=\"handleUpdate\"\n />\n\n <ImageToolbar\n v-else-if=\"blockType === 'image'\"\n :block=\"block as ImageBlock\"\n @update=\"handleUpdate\"\n />\n\n <VideoToolbar\n v-else-if=\"blockType === 'video'\"\n :block=\"block as VideoBlock\"\n @update=\"handleUpdate\"\n />\n\n <ButtonToolbar\n v-else-if=\"blockType === 'button'\"\n :block=\"block as ButtonBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <DividerToolbar\n v-else-if=\"blockType === 'divider'\"\n :block=\"block as DividerBlock\"\n @update=\"handleUpdate\"\n />\n\n <SocialToolbar\n v-else-if=\"blockType === 'social'\"\n :block=\"block as SocialIconsBlock\"\n @update=\"handleUpdate\"\n />\n\n <MenuToolbar\n v-else-if=\"blockType === 'menu'\"\n :block=\"block as MenuBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <TableToolbar\n v-else-if=\"blockType === 'table'\"\n :block=\"block as TableBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <SpacerToolbar\n v-else-if=\"blockType === 'spacer'\"\n :block=\"block as SpacerBlock\"\n @update=\"handleUpdate\"\n />\n\n <HtmlToolbar\n v-else-if=\"blockType === 'html'\"\n :block=\"block as HtmlBlock\"\n @update=\"handleUpdate\"\n />\n\n <CountdownToolbar\n v-else-if=\"blockType === 'countdown'\"\n :block=\"block as CountdownBlock\"\n :font-families=\"fontFamilies\"\n @update=\"handleUpdate\"\n />\n\n <!-- Common block settings -->\n <CommonBlockSettings\n :block=\"block\"\n :is-first-section=\"blockType === 'paragraph'\"\n @update=\"handleUpdate\"\n />\n </div>\n </aside>\n</template>\n\n<style scoped>\n.tpl-collapsible {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-collapsible--open {\n grid-template-rows: 1fr;\n}\n\n.tpl-collapsible > div {\n overflow: hidden;\n}\n</style>\n","<script setup lang=\"ts\">\nimport TemplateSettingsPanel from \"./TemplateSettings.vue\";\nimport Toolbar from \"./Toolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { TEMPLATE_LINT_KEY } from \"../keys\";\nimport type { Block, TemplateSettings } from \"@templatical/types\";\nimport { ListChecks, LayoutTemplate, PanelTop, Settings } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, inject, ref, watch } from \"vue\";\n\nconst IssuesPanel = defineAsyncComponent(\n () => import(\"./sidebar/IssuesPanel.vue\"),\n);\n\nconst props = defineProps<{\n selectedBlock: Block | null;\n settings: TemplateSettings;\n shiftedLeft?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update-block\", updates: Partial<Block>): void;\n (e: \"delete-block\"): void;\n (e: \"duplicate-block\"): void;\n (e: \"update-settings\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst { t } = useI18n();\n\ntype Tab = \"content\" | \"settings\" | \"issues\";\nconst activeTab = ref<Tab>(\"content\");\n\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst lintEnabled = computed(() => lint !== null);\nconst issueCount = computed(() => lint?.issues.value.length ?? 0);\n\nfunction tabClass(tab: Tab): string {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return \"tpl:flex-1 tpl:text-[var(--tpl-primary)]\";\n }\n return \"tpl:shrink-0 tpl:text-[var(--tpl-text-muted)] tpl:hover:text-[var(--tpl-text)]\";\n}\n\nfunction tabStyle(tab: Tab): Record<string, string> {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return {\n backgroundColor: \"var(--tpl-bg)\",\n // `-sm` is the step the shadow vocabulary assigns to a lifted control;\n // `-md` is dialog and side-panel depth, which overstates a 32px tab.\n boxShadow: \"var(--tpl-shadow-sm)\",\n };\n }\n return { backgroundColor: \"transparent\" };\n}\n\nwatch(\n () => props.selectedBlock,\n (newBlock) => {\n if (newBlock) {\n activeTab.value = \"content\";\n }\n },\n);\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.rightSidebar\"\n class=\"tpl-right-sidebar tpl:absolute tpl:top-14 tpl:bottom-0 tpl:z-40 tpl:flex tpl:w-[320px] tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)] tpl:transition-all tpl:duration-200 tpl:border-l tpl:border-[var(--tpl-border)]\"\n :class=\"shiftedLeft ? 'tpl:right-[360px]' : 'tpl:right-0'\"\n >\n <div\n role=\"tablist\"\n class=\"tpl:relative tpl:flex tpl:gap-1 tpl:border-b tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-active)] tpl:p-1.5\"\n >\n <button\n id=\"tpl-tab-content\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'content'\"\n aria-controls=\"tpl-tabpanel-content\"\n :aria-label=\"t.sidebar.content\"\n :title=\"t.sidebar.content\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('content')\"\n :style=\"tabStyle('content')\"\n @click=\"activeTab = 'content'\"\n >\n <PanelTop :size=\"14\" :stroke-width=\"2\" />\n <span v-if=\"activeTab === 'content'\">{{ t.sidebar.content }}</span>\n </button>\n <button\n id=\"tpl-tab-settings\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'settings'\"\n aria-controls=\"tpl-tabpanel-settings\"\n :aria-label=\"t.sidebar.settings\"\n :title=\"t.sidebar.settings\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('settings')\"\n :style=\"tabStyle('settings')\"\n @click=\"activeTab = 'settings'\"\n >\n <Settings :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'settings'\">{{ t.sidebar.settings }}</span>\n </button>\n <button\n v-if=\"lintEnabled\"\n id=\"tpl-tab-issues\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'issues'\"\n aria-controls=\"tpl-tabpanel-issues\"\n :aria-label=\"t.issues.panelTabLabel\"\n :title=\"t.issues.panelTabLabel\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('issues')\"\n :style=\"tabStyle('issues')\"\n @click=\"activeTab = 'issues'\"\n >\n <ListChecks :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'issues'\">\n {{ t.issues.panelTabLabel }}\n </span>\n <span\n v-if=\"issueCount > 0\"\n class=\"tpl:ml-1 tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-1.5 tpl:text-[10px]\"\n >\n {{ issueCount }}\n </span>\n </button>\n </div>\n\n <div\n v-if=\"activeTab === 'content'\"\n id=\"tpl-tabpanel-content\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-content\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <Toolbar\n v-if=\"selectedBlock\"\n :block=\"selectedBlock\"\n @update=\"emit('update-block', $event)\"\n @delete=\"emit('delete-block')\"\n @duplicate=\"emit('duplicate-block')\"\n />\n <div\n v-else\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:px-6 tpl:py-10 tpl:text-center tpl:text-[var(--tpl-text-muted)]\"\n >\n <div class=\"tpl:mb-4 tpl:text-[var(--tpl-text-dim)]\">\n <LayoutTemplate :size=\"40\" :stroke-width=\"1.5\" />\n </div>\n <h3\n class=\"tpl:m-0 tpl:mb-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.sidebar.noSelection }}\n </h3>\n <p class=\"tpl:m-0 tpl:text-sm tpl:leading-normal\">\n {{ t.sidebar.noSelectionHint }}\n </p>\n </div>\n </div>\n\n <div\n v-if=\"activeTab === 'settings'\"\n id=\"tpl-tabpanel-settings\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-settings\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <TemplateSettingsPanel\n :settings=\"settings\"\n @update=\"emit('update-settings', $event)\"\n />\n </div>\n\n <div\n v-if=\"activeTab === 'issues' && lintEnabled\"\n id=\"tpl-tabpanel-issues\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-issues\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <IssuesPanel />\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport TemplateSettingsPanel from \"./TemplateSettings.vue\";\nimport Toolbar from \"./Toolbar.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { TEMPLATE_LINT_KEY } from \"../keys\";\nimport type { Block, TemplateSettings } from \"@templatical/types\";\nimport { ListChecks, LayoutTemplate, PanelTop, Settings } from \"@lucide/vue\";\nimport { computed, defineAsyncComponent, inject, ref, watch } from \"vue\";\n\nconst IssuesPanel = defineAsyncComponent(\n () => import(\"./sidebar/IssuesPanel.vue\"),\n);\n\nconst props = defineProps<{\n selectedBlock: Block | null;\n settings: TemplateSettings;\n shiftedLeft?: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"update-block\", updates: Partial<Block>): void;\n (e: \"delete-block\"): void;\n (e: \"duplicate-block\"): void;\n (e: \"update-settings\", settings: Partial<TemplateSettings>): void;\n}>();\n\nconst { t } = useI18n();\n\ntype Tab = \"content\" | \"settings\" | \"issues\";\nconst activeTab = ref<Tab>(\"content\");\n\nconst lint = inject(TEMPLATE_LINT_KEY, null);\nconst lintEnabled = computed(() => lint !== null);\nconst issueCount = computed(() => lint?.issues.value.length ?? 0);\n\nfunction tabClass(tab: Tab): string {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return \"tpl:flex-1 tpl:text-[var(--tpl-primary)]\";\n }\n return \"tpl:shrink-0 tpl:text-[var(--tpl-text-muted)] tpl:hover:text-[var(--tpl-text)]\";\n}\n\nfunction tabStyle(tab: Tab): Record<string, string> {\n const isActive = activeTab.value === tab;\n if (isActive) {\n return {\n backgroundColor: \"var(--tpl-bg)\",\n // `-sm` is the step the shadow vocabulary assigns to a lifted control;\n // `-md` is dialog and side-panel depth, which overstates a 32px tab.\n boxShadow: \"var(--tpl-shadow-sm)\",\n };\n }\n return { backgroundColor: \"transparent\" };\n}\n\nwatch(\n () => props.selectedBlock,\n (newBlock) => {\n if (newBlock) {\n activeTab.value = \"content\";\n }\n },\n);\n</script>\n\n<template>\n <aside\n :aria-label=\"t.landmarks.rightSidebar\"\n class=\"tpl-right-sidebar tpl:absolute tpl:top-14 tpl:bottom-0 tpl:z-40 tpl:flex tpl:w-[320px] tpl:flex-col tpl:bg-[var(--tpl-bg-elevated)] tpl:transition-all tpl:duration-200 tpl:border-l tpl:border-[var(--tpl-border)]\"\n :class=\"shiftedLeft ? 'tpl:right-[360px]' : 'tpl:right-0'\"\n >\n <div\n role=\"tablist\"\n class=\"tpl:relative tpl:flex tpl:gap-1 tpl:border-b tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg-active)] tpl:p-1.5\"\n >\n <button\n id=\"tpl-tab-content\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'content'\"\n aria-controls=\"tpl-tabpanel-content\"\n :aria-label=\"t.sidebar.content\"\n :title=\"t.sidebar.content\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('content')\"\n :style=\"tabStyle('content')\"\n @click=\"activeTab = 'content'\"\n >\n <PanelTop :size=\"14\" :stroke-width=\"2\" />\n <span v-if=\"activeTab === 'content'\">{{ t.sidebar.content }}</span>\n </button>\n <button\n id=\"tpl-tab-settings\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'settings'\"\n aria-controls=\"tpl-tabpanel-settings\"\n :aria-label=\"t.sidebar.settings\"\n :title=\"t.sidebar.settings\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('settings')\"\n :style=\"tabStyle('settings')\"\n @click=\"activeTab = 'settings'\"\n >\n <Settings :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'settings'\">{{ t.sidebar.settings }}</span>\n </button>\n <button\n v-if=\"lintEnabled\"\n id=\"tpl-tab-issues\"\n role=\"tab\"\n :aria-selected=\"activeTab === 'issues'\"\n aria-controls=\"tpl-tabpanel-issues\"\n :aria-label=\"t.issues.panelTabLabel\"\n :title=\"t.issues.panelTabLabel\"\n class=\"tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:gap-1.5 tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:px-3 tpl:py-2 tpl:text-xs tpl:font-medium tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n :class=\"tabClass('issues')\"\n :style=\"tabStyle('issues')\"\n @click=\"activeTab = 'issues'\"\n >\n <ListChecks :size=\"14\" :stroke-width=\"1.5\" />\n <span v-if=\"activeTab === 'issues'\">\n {{ t.issues.panelTabLabel }}\n </span>\n <span\n v-if=\"issueCount > 0\"\n class=\"tpl:ml-1 tpl:rounded-full tpl:bg-[var(--tpl-bg-hover)] tpl:px-1.5 tpl:text-[10px]\"\n >\n {{ issueCount }}\n </span>\n </button>\n </div>\n\n <div\n v-if=\"activeTab === 'content'\"\n id=\"tpl-tabpanel-content\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-content\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <Toolbar\n v-if=\"selectedBlock\"\n :block=\"selectedBlock\"\n @update=\"emit('update-block', $event)\"\n @delete=\"emit('delete-block')\"\n @duplicate=\"emit('duplicate-block')\"\n />\n <div\n v-else\n class=\"tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:px-6 tpl:py-10 tpl:text-center tpl:text-[var(--tpl-text-muted)]\"\n >\n <div class=\"tpl:mb-4 tpl:text-[var(--tpl-text-dim)]\">\n <LayoutTemplate :size=\"40\" :stroke-width=\"1.5\" />\n </div>\n <h3\n class=\"tpl:m-0 tpl:mb-2 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.sidebar.noSelection }}\n </h3>\n <p class=\"tpl:m-0 tpl:text-sm tpl:leading-normal\">\n {{ t.sidebar.noSelectionHint }}\n </p>\n </div>\n </div>\n\n <div\n v-if=\"activeTab === 'settings'\"\n id=\"tpl-tabpanel-settings\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-settings\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <TemplateSettingsPanel\n :settings=\"settings\"\n @update=\"emit('update-settings', $event)\"\n />\n </div>\n\n <div\n v-if=\"activeTab === 'issues' && lintEnabled\"\n id=\"tpl-tabpanel-issues\"\n role=\"tabpanel\"\n aria-labelledby=\"tpl-tab-issues\"\n class=\"tpl:flex tpl:flex-1 tpl:flex-col tpl:overflow-y-auto\"\n >\n <IssuesPanel />\n </div>\n </aside>\n</template>\n","<script setup lang=\"ts\">\nimport { Monitor } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\n// Full-cover gate shown below the usable-width breakpoint (issue #235).\n// Rendered as the last child of `.tpl` and given a literal z-index above all\n// chrome (header z-50, sidebars z-40) AND `.tpl-popover-root` (z 10000), so the\n// opaque overlay covers everything including any open popover/modal.\n//\n// The z-index is a LITERAL, deliberately not `var(--z-modal)` or a `tpl:z-*`\n// utility: the semantic z tokens live on `:root`, unreachable from inside the\n// shadow root, and Tailwind 4 derives z utilities from `--z-index-*` (not the\n// project's `--z-*`), so `tpl:z-modal` compiles to nothing. `.tpl-popover-root`\n// uses the same literal trick for the same reason.\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl-small-screen-notice tpl:absolute tpl:inset-0 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-6 tpl:py-10 tpl:text-center tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)]\"\n style=\"z-index: 10001\"\n role=\"status\"\n data-testid=\"small-screen-notice\"\n >\n <div class=\"tpl:text-[var(--tpl-text-dim)]\">\n <Monitor :size=\"48\" :stroke-width=\"1.5\" />\n </div>\n <h2\n class=\"tpl:m-0 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.smallScreen.title }}\n </h2>\n <p class=\"tpl:m-0 tpl:max-w-sm tpl:text-sm tpl:leading-normal\">\n {{ t.smallScreen.message }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Monitor } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\n// Full-cover gate shown below the usable-width breakpoint (issue #235).\n// Rendered as the last child of `.tpl` and given a literal z-index above all\n// chrome (header z-50, sidebars z-40) AND `.tpl-popover-root` (z 10000), so the\n// opaque overlay covers everything including any open popover/modal.\n//\n// The z-index is a LITERAL, deliberately not `var(--z-modal)` or a `tpl:z-*`\n// utility: the semantic z tokens live on `:root`, unreachable from inside the\n// shadow root, and Tailwind 4 derives z utilities from `--z-index-*` (not the\n// project's `--z-*`), so `tpl:z-modal` compiles to nothing. `.tpl-popover-root`\n// uses the same literal trick for the same reason.\nconst { t } = useI18n();\n</script>\n\n<template>\n <div\n class=\"tpl-small-screen-notice tpl:absolute tpl:inset-0 tpl:flex tpl:flex-col tpl:items-center tpl:justify-center tpl:gap-4 tpl:px-6 tpl:py-10 tpl:text-center tpl:bg-[var(--tpl-bg)] tpl:text-[var(--tpl-text-muted)]\"\n style=\"z-index: 10001\"\n role=\"status\"\n data-testid=\"small-screen-notice\"\n >\n <div class=\"tpl:text-[var(--tpl-text-dim)]\">\n <Monitor :size=\"48\" :stroke-width=\"1.5\" />\n </div>\n <h2\n class=\"tpl:m-0 tpl:text-base tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.smallScreen.title }}\n </h2>\n <p class=\"tpl:m-0 tpl:max-w-sm tpl:text-sm tpl:leading-normal\">\n {{ t.smallScreen.message }}\n </p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n /** Positioning classes for the footer (left/right offsets). */\n positionClass?: string | string[];\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <footer\n class=\"tpl:pointer-events-none tpl:absolute tpl:bottom-0 tpl:z-50 tpl:flex tpl:h-8 tpl:items-center tpl:justify-end tpl:pr-4 tpl:text-[9px] tpl:opacity-90 tpl:transition-all tpl:duration-300 tpl:text-[var(--tpl-text-dim)]\"\n :class=\"positionClass\"\n >\n <div\n class=\"tpl:pointer-events-auto tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-tl-lg tpl:p-1\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-canvas-bg) 85%,\n transparent\n );\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n >\n <span>{{ t.footer.poweredBy }}</span>\n <a\n href=\"https://templatical.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1 tpl:font-medium tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-muted)]\"\n style=\"text-decoration: none\"\n >\n <img\n width=\"14\"\n height=\"14\"\n src=\"https://templatical.com/logo.svg\"\n alt=\"\"\n />\n Templatical\n </a>\n <span class=\"tpl:text-[var(--tpl-border)]\">·</span>\n <a\n href=\"https://github.com/templatical/sdk\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-dim)]\"\n style=\"text-decoration: none\"\n >\n {{ t.footer.openSource }}\n </a>\n </div>\n </footer>\n</template>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n /** Positioning classes for the footer (left/right offsets). */\n positionClass?: string | string[];\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <footer\n class=\"tpl:pointer-events-none tpl:absolute tpl:bottom-0 tpl:z-50 tpl:flex tpl:h-8 tpl:items-center tpl:justify-end tpl:pr-4 tpl:text-[9px] tpl:opacity-90 tpl:transition-all tpl:duration-300 tpl:text-[var(--tpl-text-dim)]\"\n :class=\"positionClass\"\n >\n <div\n class=\"tpl:pointer-events-auto tpl:flex tpl:items-center tpl:gap-1.5 tpl:rounded-tl-lg tpl:p-1\"\n style=\"\n background-color: color-mix(\n in srgb,\n var(--tpl-canvas-bg) 85%,\n transparent\n );\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n \"\n >\n <span>{{ t.footer.poweredBy }}</span>\n <a\n href=\"https://templatical.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:inline-flex tpl:items-center tpl:gap-1 tpl:font-medium tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-muted)]\"\n style=\"text-decoration: none\"\n >\n <img\n width=\"14\"\n height=\"14\"\n src=\"https://templatical.com/logo.svg\"\n alt=\"\"\n />\n Templatical\n </a>\n <span class=\"tpl:text-[var(--tpl-border)]\">·</span>\n <a\n href=\"https://github.com/templatical/sdk\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"tpl:transition-colors tpl:hover:opacity-80 tpl:text-[var(--tpl-text-dim)]\"\n style=\"text-decoration: none\"\n >\n {{ t.footer.openSource }}\n </a>\n </div>\n </footer>\n</template>\n","<!--\n Canvas Dark Mode Preview Toggle\n\n This toggle simulates how the email template will look when the recipient's\n email client uses dark mode (e.g. Gmail, Outlook, Apple Mail in dark theme).\n It applies a CSS filter (invert + hue-rotate) to the canvas area only.\n\n This is NOT the editor UI theme toggle. The editor UI theme (light/dark/auto)\n is controlled externally via the `uiTheme` config option or `editor.setTheme()`.\n-->\n<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Moon, Sun } from \"@lucide/vue\";\n\ndefineProps<{\n darkMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", darkMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-dark-mode-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: darkMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: darkMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :title=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :aria-pressed=\"darkMode\"\n @click=\"emit('change', !darkMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Moon v-if=\"darkMode\" key=\"moon\" :size=\"18\" :stroke-width=\"1.5\" />\n <Sun v-else key=\"sun\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-dark-mode-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-dark-mode-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<!--\n Canvas Dark Mode Preview Toggle\n\n This toggle simulates how the email template will look when the recipient's\n email client uses dark mode (e.g. Gmail, Outlook, Apple Mail in dark theme).\n It applies a CSS filter (invert + hue-rotate) to the canvas area only.\n\n This is NOT the editor UI theme toggle. The editor UI theme (light/dark/auto)\n is controlled externally via the `uiTheme` config option or `editor.setTheme()`.\n-->\n<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Moon, Sun } from \"@lucide/vue\";\n\ndefineProps<{\n darkMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", darkMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-dark-mode-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: darkMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: darkMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :title=\"darkMode ? t.darkMode.disable : t.darkMode.enable\"\n :aria-pressed=\"darkMode\"\n @click=\"emit('change', !darkMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Moon v-if=\"darkMode\" key=\"moon\" :size=\"18\" :stroke-width=\"1.5\" />\n <Sun v-else key=\"sun\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-dark-mode-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-dark-mode-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Eye, EyeOff } from \"@lucide/vue\";\n\ndefineProps<{\n previewMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", previewMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-preview-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: previewMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: previewMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :title=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :aria-pressed=\"previewMode\"\n @click=\"emit('change', !previewMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Eye v-if=\"previewMode\" key=\"eye\" :size=\"18\" :stroke-width=\"1.5\" />\n <EyeOff v-else key=\"eye-off\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-preview-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-preview-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useI18n } from \"../composables/useI18n\";\nimport { Eye, EyeOff } from \"@lucide/vue\";\n\ndefineProps<{\n previewMode: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"change\", previewMode: boolean): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <button\n class=\"tpl-preview-toggle tpl:relative tpl:flex tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:p-2 tpl:transition-all\"\n :style=\"{\n color: previewMode ? 'var(--tpl-primary)' : 'var(--tpl-text-muted)',\n backgroundColor: previewMode ? 'var(--tpl-primary-light)' : 'transparent',\n }\"\n :aria-label=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :title=\"previewMode ? t.previewMode.disable : t.previewMode.enable\"\n :aria-pressed=\"previewMode\"\n @click=\"emit('change', !previewMode)\"\n >\n <Transition\n enter-active-class=\"tpl-icon-enter-active\"\n leave-active-class=\"tpl-icon-leave-active\"\n enter-from-class=\"tpl-icon-enter-from\"\n leave-to-class=\"tpl-icon-leave-to\"\n mode=\"out-in\"\n >\n <Eye v-if=\"previewMode\" key=\"eye\" :size=\"18\" :stroke-width=\"1.5\" />\n <EyeOff v-else key=\"eye-off\" :size=\"18\" :stroke-width=\"1.5\" />\n </Transition>\n </button>\n</template>\n\n<style scoped>\n.tpl-icon-enter-active,\n.tpl-icon-leave-active {\n transition:\n opacity 120ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 120ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-icon-enter-from {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-icon-leave-to {\n opacity: 0;\n transform: scale(0.8);\n}\n\n.tpl-preview-toggle:hover {\n background-color: var(--tpl-bg-hover);\n}\n\n.tpl-preview-toggle:active {\n transform: scale(0.92);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, ref } from \"vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { inputClass } from \"../constants/styleConstants\";\n\nconst props = defineProps<{\n /** The loaded template's name. Undefined renders the \"Untitled\" placeholder. */\n name?: string;\n /**\n * Whether the name may be changed. False when the provider withheld `save`:\n * there would be nowhere for the new name to go, so it renders as plain text.\n */\n editable: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"commit\", name: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst isEditing = ref(false);\nconst draft = ref(\"\");\nconst inputEl = ref<HTMLInputElement | null>(null);\n\nasync function startEditing(): Promise<void> {\n if (!props.editable) return;\n draft.value = props.name ?? \"\";\n isEditing.value = true;\n await nextTick();\n inputEl.value?.focus();\n inputEl.value?.select();\n}\n\nfunction commit(): void {\n // Escape already left edit mode, and removing the input fires `blur` — which\n // would otherwise commit the very draft the user just cancelled.\n if (!isEditing.value) return;\n isEditing.value = false;\n const next = draft.value.trim();\n // An empty name is rejected by reverting: the header would have nothing to\n // render, and a cleared field is far more likely a slip than an intent.\n if (!next || next === props.name) return;\n emit(\"commit\", next);\n}\n\nfunction cancel(): void {\n isEditing.value = false;\n}\n</script>\n\n<template>\n <input\n v-if=\"isEditing\"\n ref=\"inputEl\"\n v-model=\"draft\"\n type=\"text\"\n data-testid=\"template-name-input\"\n :aria-label=\"t.header.templateName\"\n :class=\"[\n inputClass,\n 'tpl:h-8 tpl:max-w-[260px] tpl:text-sm tpl:font-medium',\n ]\"\n @keydown.enter.prevent=\"commit\"\n @keydown.esc.prevent=\"cancel\"\n @blur=\"commit\"\n />\n <button\n v-else-if=\"editable\"\n type=\"button\"\n data-testid=\"template-name\"\n :title=\"t.header.rename\"\n :aria-label=\"t.header.rename\"\n class=\"tpl:max-w-[260px] tpl:cursor-pointer tpl:truncate tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-transparent tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:border-[var(--tpl-border)] tpl:hover:bg-[var(--tpl-bg-hover)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n @click=\"startEditing\"\n >\n {{ name || t.header.untitled }}\n </button>\n <span\n v-else\n data-testid=\"template-name\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n >\n {{ name || t.header.untitled }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { nextTick, ref } from \"vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { inputClass } from \"../constants/styleConstants\";\n\nconst props = defineProps<{\n /** The loaded template's name. Undefined renders the \"Untitled\" placeholder. */\n name?: string;\n /**\n * Whether the name may be changed. False when the provider withheld `save`:\n * there would be nowhere for the new name to go, so it renders as plain text.\n */\n editable: boolean;\n}>();\n\nconst emit = defineEmits<{\n (e: \"commit\", name: string): void;\n}>();\n\nconst { t } = useI18n();\n\nconst isEditing = ref(false);\nconst draft = ref(\"\");\nconst inputEl = ref<HTMLInputElement | null>(null);\n\nasync function startEditing(): Promise<void> {\n if (!props.editable) return;\n draft.value = props.name ?? \"\";\n isEditing.value = true;\n await nextTick();\n inputEl.value?.focus();\n inputEl.value?.select();\n}\n\nfunction commit(): void {\n // Escape already left edit mode, and removing the input fires `blur` — which\n // would otherwise commit the very draft the user just cancelled.\n if (!isEditing.value) return;\n isEditing.value = false;\n const next = draft.value.trim();\n // An empty name is rejected by reverting: the header would have nothing to\n // render, and a cleared field is far more likely a slip than an intent.\n if (!next || next === props.name) return;\n emit(\"commit\", next);\n}\n\nfunction cancel(): void {\n isEditing.value = false;\n}\n</script>\n\n<template>\n <input\n v-if=\"isEditing\"\n ref=\"inputEl\"\n v-model=\"draft\"\n type=\"text\"\n data-testid=\"template-name-input\"\n :aria-label=\"t.header.templateName\"\n :class=\"[\n inputClass,\n 'tpl:h-8 tpl:max-w-[260px] tpl:text-sm tpl:font-medium',\n ]\"\n @keydown.enter.prevent=\"commit\"\n @keydown.esc.prevent=\"cancel\"\n @blur=\"commit\"\n />\n <button\n v-else-if=\"editable\"\n type=\"button\"\n data-testid=\"template-name\"\n :title=\"t.header.rename\"\n :aria-label=\"t.header.rename\"\n class=\"tpl:max-w-[260px] tpl:cursor-pointer tpl:truncate tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-transparent tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)] tpl:transition-all tpl:duration-[120ms] tpl:ease-[cubic-bezier(0.16,1,0.3,1)] tpl:hover:border-[var(--tpl-border)] tpl:hover:bg-[var(--tpl-bg-hover)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n @click=\"startEditing\"\n >\n {{ name || t.header.untitled }}\n </button>\n <span\n v-else\n data-testid=\"template-name\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:py-1 tpl:text-sm tpl:font-medium tpl:text-[var(--tpl-text)]\"\n :class=\"{ 'tpl:text-[var(--tpl-text-dim)]': !name }\"\n >\n {{ name || t.header.untitled }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\n\nconst props = defineProps<{\n /** ISO 8601 timestamp, straight from the store. */\n iso: string;\n /** Which field it came from — decides the \"Updated\"/\"Created\" wording. */\n kind: \"updatedAt\" | \"createdAt\";\n}>();\n\nconst { t, format } = useI18n();\n\n// The header stays open for as long as the session does, so a label computed\n// once would sit at \"Just now\" for hours. A minute is the formatter's smallest\n// bucket, so nothing finer would show anyway.\nconst tick = ref(0);\nuseIntervalFn(() => {\n tick.value += 1;\n}, 60000);\n\nconst label = computed(() => {\n // Depended on for its side effect: `formatRelativeTime` reads the clock\n // itself, so the tick is the only thing that can invalidate this.\n void tick.value;\n const relative = formatRelativeTime(props.iso, t.time, format);\n // Null covers a value that does not parse and one further into the future\n // than clock skew explains. Falling back to an absolute date would put a\n // number where the eye expects chrome, so the line is dropped instead.\n if (relative === null) return \"\";\n // The shared buckets are phrased to stand alone, so the smallest one is\n // capitalised (\"Just now\") and reads wrong inside \"Updated {time}\". Matched\n // against the label rather than re-deriving a one-minute threshold, so what\n // counts as \"just now\" stays `formatRelativeTime`'s decision alone.\n const isJustNow = relative === t.time.justNow;\n if (props.kind === \"updatedAt\") {\n return isJustNow\n ? t.header.updatedJustNow\n : format(t.header.updatedAt, { time: relative });\n }\n return isJustNow\n ? t.header.createdJustNow\n : format(t.header.createdAt, { time: relative });\n});\n\n/** Full date for the tooltip — locale-formatted, so it needs no i18n keys. */\nconst absolute = computed(() => {\n const parsed = new Date(props.iso);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\n});\n</script>\n\n<template>\n <!-- `px-2` aligns the line under the name field's text, which carries the same\n padding for its hover affordance. Present whether or not the name renders,\n so the left column reads the same in both. -->\n <span\n v-if=\"label\"\n data-testid=\"template-timestamp\"\n :title=\"absolute\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:text-xs tpl:leading-tight tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ label }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useIntervalFn } from \"@vueuse/core\";\nimport { useI18n } from \"../composables/useI18n\";\nimport { formatRelativeTime } from \"../utils/formatRelativeTime\";\n\nconst props = defineProps<{\n /** ISO 8601 timestamp, straight from the store. */\n iso: string;\n /** Which field it came from — decides the \"Updated\"/\"Created\" wording. */\n kind: \"updatedAt\" | \"createdAt\";\n}>();\n\nconst { t, format } = useI18n();\n\n// The header stays open for as long as the session does, so a label computed\n// once would sit at \"Just now\" for hours. A minute is the formatter's smallest\n// bucket, so nothing finer would show anyway.\nconst tick = ref(0);\nuseIntervalFn(() => {\n tick.value += 1;\n}, 60000);\n\nconst label = computed(() => {\n // Depended on for its side effect: `formatRelativeTime` reads the clock\n // itself, so the tick is the only thing that can invalidate this.\n void tick.value;\n const relative = formatRelativeTime(props.iso, t.time, format);\n // Null covers a value that does not parse and one further into the future\n // than clock skew explains. Falling back to an absolute date would put a\n // number where the eye expects chrome, so the line is dropped instead.\n if (relative === null) return \"\";\n // The shared buckets are phrased to stand alone, so the smallest one is\n // capitalised (\"Just now\") and reads wrong inside \"Updated {time}\". Matched\n // against the label rather than re-deriving a one-minute threshold, so what\n // counts as \"just now\" stays `formatRelativeTime`'s decision alone.\n const isJustNow = relative === t.time.justNow;\n if (props.kind === \"updatedAt\") {\n return isJustNow\n ? t.header.updatedJustNow\n : format(t.header.updatedAt, { time: relative });\n }\n return isJustNow\n ? t.header.createdJustNow\n : format(t.header.createdAt, { time: relative });\n});\n\n/** Full date for the tooltip — locale-formatted, so it needs no i18n keys. */\nconst absolute = computed(() => {\n const parsed = new Date(props.iso);\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toLocaleString();\n});\n</script>\n\n<template>\n <!-- `px-2` aligns the line under the name field's text, which carries the same\n padding for its hover affordance. Present whether or not the name renders,\n so the left column reads the same in both. -->\n <span\n v-if=\"label\"\n data-testid=\"template-timestamp\"\n :title=\"absolute\"\n class=\"tpl:max-w-[260px] tpl:truncate tpl:px-2 tpl:text-xs tpl:leading-tight tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ label }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { Check, CircleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n status: \"idle\" | \"saved\" | \"error\";\n /** The provider's failure message, shown in a tooltip on the error state. */\n errorMessage: string;\n isDirty: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <!-- Three mutually exclusive states, in priority order: a failure outranks\n everything (it needs acknowledging), a fresh success outranks the dirty\n flag, and \"unsaved\" is the resting state once either decays. Rendered\n only when a templates provider can actually save — without one the editor\n never learns that a save completed, so this would read \"unsaved\" forever. -->\n <div\n v-if=\"status === 'error'\"\n aria-live=\"assertive\"\n data-testid=\"save-status-error\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"errorMessage\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saveFailed }}\n </div>\n <div\n v-else-if=\"status === 'saved'\"\n aria-live=\"polite\"\n data-testid=\"save-status-saved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saved }}\n </div>\n <div\n v-else-if=\"isDirty\"\n aria-live=\"polite\"\n data-testid=\"save-status-unsaved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ t.header.unsaved }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { Check, CircleAlert } from \"@lucide/vue\";\nimport { useI18n } from \"../composables/useI18n\";\n\ndefineProps<{\n status: \"idle\" | \"saved\" | \"error\";\n /** The provider's failure message, shown in a tooltip on the error state. */\n errorMessage: string;\n isDirty: boolean;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n <!-- Three mutually exclusive states, in priority order: a failure outranks\n everything (it needs acknowledging), a fresh success outranks the dirty\n flag, and \"unsaved\" is the resting state once either decays. Rendered\n only when a templates provider can actually save — without one the editor\n never learns that a save completed, so this would read \"unsaved\" forever. -->\n <div\n v-if=\"status === 'error'\"\n aria-live=\"assertive\"\n data-testid=\"save-status-error\"\n class=\"tpl-tooltip tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-danger)]\"\n :data-tooltip=\"errorMessage\"\n >\n <CircleAlert :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saveFailed }}\n </div>\n <div\n v-else-if=\"status === 'saved'\"\n aria-live=\"polite\"\n data-testid=\"save-status-saved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-success)]\"\n >\n <Check :size=\"12\" :stroke-width=\"2.5\" />\n {{ t.header.saved }}\n </div>\n <div\n v-else-if=\"isDirty\"\n aria-live=\"polite\"\n data-testid=\"save-status-unsaved\"\n class=\"tpl-status tpl:flex tpl:items-center tpl:gap-1.5 tpl:text-xs tpl:text-[var(--tpl-text-muted)]\"\n >\n <span\n class=\"tpl-pulse tpl:size-1.5 tpl:rounded-full tpl:bg-[var(--tpl-primary)]\"\n ></span>\n {{ t.header.unsaved }}\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { LoaderCircle, MessageCircle, Save, Send } from \"@lucide/vue\";\n\nimport type {\n BaseEditorReturn,\n UseEditorCoreReturn,\n} from \"../composables/useEditorCore\";\nimport type { UseCommentsFeatureReturn } from \"../composables/useCommentsFeature\";\nimport type { UseTemplatesFeatureReturn } from \"../composables/useTemplatesFeature\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\nimport type { UseVersionHistoryFeatureReturn } from \"../composables/useVersionHistoryFeature\";\nimport {\n primaryBtnCompactClass,\n secondaryBtnCompactClass,\n} from \"../constants/styleConstants\";\n\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport DarkModeToggle from \"./DarkModeToggle.vue\";\nimport PreviewToggle from \"./PreviewToggle.vue\";\nimport TemplateNameField from \"./TemplateNameField.vue\";\nimport TemplateTimestamp from \"./TemplateTimestamp.vue\";\nimport TemplateSaveStatus from \"./TemplateSaveStatus.vue\";\n\n// Lazy: an editor with no `versionHistory` provider never downloads the control.\nconst VersionHistoryMenu = defineAsyncComponent(\n () => import(\"./VersionHistoryMenu.vue\"),\n);\n\n/**\n * The editor's one header. Both entry points render this — `initCloud()` adds its\n * own controls through the `left-extras` / `right-extras` slots rather than\n * through a second header component, which is what keeps the two layouts from\n * drifting apart again (they already had: only Cloud carried `min-w-[200px]`, so\n * the OSS centre column was never actually centred).\n *\n * The two slots are in the anchored columns on purpose. There is deliberately no\n * centre slot: that track's width must stay constant, and a slot is the one\n * thing a guard test cannot stop someone filling. See the centre column's own\n * comment for why width changes there move the Preview button.\n *\n * Everything here is capability-driven, so the same markup covers \"no providers\n * configured at all\" and a fully-wired Cloud session. Nothing in this file knows\n * that Cloud exists.\n */\ndefineProps<{\n editor: BaseEditorReturn;\n core: UseEditorCoreReturn;\n /** Null when no `TemplatesProvider` is configured — no name, save or status. */\n templates: UseTemplatesFeatureReturn | null;\n /**\n * Whether to render the inline name field, from `config.templates?.nameField`.\n * False hides it whether or not the provider can save — a consumer whose\n * store has no name column, or whose own chrome owns the name.\n */\n showTemplateName: boolean;\n testEmail: UseTestEmailFeatureReturn | null;\n versionHistory: UseVersionHistoryFeatureReturn | null;\n /** Null when no `CommentsProvider` (or no `user`) is configured. */\n comments: UseCommentsFeatureReturn | null;\n}>();\n</script>\n\n<template>\n <!-- Opaque, and `--tpl-shadow-sm` because that is the step the shadow\n vocabulary assigns to sticky chrome. `.tpl-body` starts at this header's\n own height, so nothing ever passes beneath it: a translucent fill has an\n identical surface behind it and a backdrop blur has nothing to blur,\n which costs a compositing layer to render exactly `--tpl-bg`. -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <!-- Left: the template's identity — the inline-editable name, and under it\n when the store dates its writes. Stacking the two needs 6px of bottom\n padding: the name is a button, so it carries border + `py-1` + half its\n leading above its text, while the timestamp carries nothing below its\n own. Centring the boxes therefore puts all of that lead at the top and\n reads as a misaligned pair. Only when both render — either alone is\n already centred on its own box. `min-w-[200px]` matches the right column\n so the centre controls are actually centred. Nothing renders before a\n template exists; there is neither a name nor a write time yet. The stack\n is gated as a whole so an empty one cannot add a `gap-2.5` before the\n cloud extras. -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-2.5\"\n >\n <div\n v-if=\"\n templates?.isAvailable.value &&\n templates.hasTemplate.value &&\n (showTemplateName || templates.timestamp.value)\n \"\n class=\"tpl:flex tpl:min-w-0 tpl:flex-col tpl:items-start\"\n :class=\"{\n 'tpl:pb-1.5': showTemplateName && templates.timestamp.value !== null,\n }\"\n >\n <TemplateNameField\n v-if=\"showTemplateName\"\n :name=\"templates.name.value\"\n :editable=\"templates.canSave.value\"\n @commit=\"templates.rename\"\n />\n <TemplateTimestamp\n v-if=\"templates.timestamp.value\"\n :iso=\"templates.timestamp.value.iso\"\n :kind=\"templates.timestamp.value.kind\"\n />\n </div>\n <!-- Version history — the third piece of \"which template, and which\n version of it\", after the name and the write time. It sits here and\n not in the right column because that column's content already\n overflows its track: `justify-end` packs to the right edge, so an\n extra control there spills *leftward* over the centre track and\n swallows clicks meant for the Preview button. Measured — it covered\n it with the menu's own back-arrow. This column packs from the left\n and has the room.\n\n Needs a template as well as a provider: a version belongs to a\n template id, so before one is loaded there is nothing to list. -->\n <VersionHistoryMenu\n v-if=\"\n versionHistory?.isAvailable.value && versionHistory.hasTemplate.value\n \"\n :versions=\"versionHistory.versions.value\"\n :is-loading=\"versionHistory.isLoading.value\"\n :is-restoring=\"versionHistory.isRestoring.value\"\n :previewing-id=\"versionHistory.previewingVersion.value?.id ?? null\"\n @open=\"versionHistory.refresh()\"\n @navigate=\"versionHistory.navigate($event)\"\n />\n <slot name=\"left-extras\" />\n </div>\n\n <!-- Center: the three \"how am I viewing the canvas\" controls, and nothing\n else — ever.\n\n The grid is `1fr auto 1fr`, so this track is exactly max-content wide\n and the equal `fr` columns centre it. Any width change therefore\n redistributes symmetrically about the header's centre and moves every\n sibling by half the delta, whichever side of the change it sits on.\n DOM order buys nothing. So a conditional control here drags the Preview\n button out from under the cursor — 114.5px when the sample/label toggle\n used to live here, and repeatedly in Cloud as collaborators came and\n went (#574).\n\n Hence the rule: nothing in this track may be conditional. No `v-if`, no\n `v-show`, no slot. Conditional controls go in the edge-anchored `fr`\n columns, which grow away from their anchored edge; preview-only ones go\n on the preview surface, where `Editor.vue` floats them over the canvas.\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n </div>\n\n <!-- Right: save status → Comments → [cloud extras: AI] → test email → Save.\n Save is last: it is the primary action. `justify-end` anchors the row to\n the right edge, so a control appearing at its start extends the row\n leftward and moves nothing already in it — but note the row is already\n at the edge of its track at common widths, so anything added here\n overflows onto the centre rather than fitting. -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Gated on a provider that can actually save. Without one the editor\n never learns that a save completed, so the badge could only ever read\n \"unsaved\" — worse than showing nothing. -->\n <TemplateSaveStatus\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n :status=\"templates.status.value\"\n :error-message=\"templates.errorMessage.value\"\n :is-dirty=\"editor.state.isDirty\"\n />\n\n <!-- Comments. Needs a template as well as a provider — a comment is keyed\n to a template id, so before one is loaded there is nothing to attach a\n thread to. Shared chrome, because comments are a shared\n provider-backed feature rather than a Cloud one. -->\n <button\n v-if=\"comments?.isAvailable.value && comments.hasTemplate.value\"\n type=\"button\"\n data-testid=\"comments-trigger\"\n :aria-label=\"\n comments.unresolvedCount.value > 0\n ? `${core.t.comments.button} (${comments.unresolvedCount.value})`\n : core.t.comments.button\n \"\n :aria-expanded=\"comments.isOpen.value\"\n :class=\"secondaryBtnCompactClass\"\n :style=\"\n comments.isOpen.value\n ? {\n backgroundColor: 'var(--tpl-primary-light)',\n borderColor: 'var(--tpl-primary-light)',\n }\n : undefined\n \"\n @click=\"comments.toggle()\"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.comments.button }}\n <span\n v-if=\"comments.unresolvedCount.value > 0 && !comments.isOpen.value\"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </button>\n\n <slot name=\"right-extras\" />\n\n <!-- The shared recipe, like every control in this row. Shared rather than\n hand-rolled so all three keep one height — a bespoke string is how\n this button came to sit 4px shorter than its neighbours. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n :disabled=\"testEmail.isSending.value\"\n :class=\"secondaryBtnCompactClass\"\n @click=\"testEmail.open()\"\n >\n <Send v-if=\"!testEmail.isSending.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.testEmail.button }}\n </button>\n\n <!-- Save is the editor's one primary, and it only takes that treatment\n while there is something to save. Amber announces intent or selection,\n so a Save that looks identical dirty or clean announces nothing — and\n it would be the loudest element in chrome that exists to recede behind\n the canvas. Gated on `hasTemplate` too, or a disabled button would\n light up before there is anywhere to save to.\n\n Amber with the paper colour on it is 2.80:1 in light mode. That is\n accepted rather than overlooked: every amber surface and accent in the\n editor shares this pairing, and correcting this one control alone would\n make it read as a different kind of thing. The Amber-Accent Exception\n in DESIGN.md records the decision and its limits — chief among them\n that amber must never be the only carrier of a state, which is why the\n `TemplateSaveStatus` badge says \"Unsaved\" in words beside it.\n\n The light lift is the same condition, and it is Flat-At-Rest applied\n rather than bent: a surface paints no shadow until something happens to\n it, and unsaved work is that something. It sits inline rather than in\n the recipe because the recipe is shared, and a primary button at rest\n elsewhere should still be flat.\n\n Hidden entirely when the provider withheld `save` — the read-only\n equivalent of the saved-blocks permission discipline: loading a\n template and editing it locally still works, there is simply nothing\n to persist. Disabled (with a reason in the tooltip) until a template\n exists, since `save()` patches an id. -->\n <button\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n type=\"button\"\n data-testid=\"template-save\"\n :class=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? primaryBtnCompactClass\n : secondaryBtnCompactClass\n \"\n :style=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? { boxShadow: 'var(--tpl-shadow-sm)' }\n : undefined\n \"\n :disabled=\"templates.isSaving.value || !templates.hasTemplate.value\"\n :title=\"\n templates.hasTemplate.value\n ? core.t.header.save\n : core.t.header.saveNoTemplate\n \"\n @click=\"templates.requestSave()\"\n >\n <Save v-if=\"!templates.isSaving.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{\n templates.isSaving.value ? core.t.header.saving : core.t.header.save\n }}\n </button>\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport { defineAsyncComponent } from \"vue\";\nimport { LoaderCircle, MessageCircle, Save, Send } from \"@lucide/vue\";\n\nimport type {\n BaseEditorReturn,\n UseEditorCoreReturn,\n} from \"../composables/useEditorCore\";\nimport type { UseCommentsFeatureReturn } from \"../composables/useCommentsFeature\";\nimport type { UseTemplatesFeatureReturn } from \"../composables/useTemplatesFeature\";\nimport type { UseTestEmailFeatureReturn } from \"../composables/useTestEmailFeature\";\nimport type { UseVersionHistoryFeatureReturn } from \"../composables/useVersionHistoryFeature\";\nimport {\n primaryBtnCompactClass,\n secondaryBtnCompactClass,\n} from \"../constants/styleConstants\";\n\nimport ViewportToggle from \"./ViewportToggle.vue\";\nimport DarkModeToggle from \"./DarkModeToggle.vue\";\nimport PreviewToggle from \"./PreviewToggle.vue\";\nimport TemplateNameField from \"./TemplateNameField.vue\";\nimport TemplateTimestamp from \"./TemplateTimestamp.vue\";\nimport TemplateSaveStatus from \"./TemplateSaveStatus.vue\";\n\n// Lazy: an editor with no `versionHistory` provider never downloads the control.\nconst VersionHistoryMenu = defineAsyncComponent(\n () => import(\"./VersionHistoryMenu.vue\"),\n);\n\n/**\n * The editor's one header. Both entry points render this — `initCloud()` adds its\n * own controls through the `left-extras` / `right-extras` slots rather than\n * through a second header component, which is what keeps the two layouts from\n * drifting apart again (they already had: only Cloud carried `min-w-[200px]`, so\n * the OSS centre column was never actually centred).\n *\n * The two slots are in the anchored columns on purpose. There is deliberately no\n * centre slot: that track's width must stay constant, and a slot is the one\n * thing a guard test cannot stop someone filling. See the centre column's own\n * comment for why width changes there move the Preview button.\n *\n * Everything here is capability-driven, so the same markup covers \"no providers\n * configured at all\" and a fully-wired Cloud session. Nothing in this file knows\n * that Cloud exists.\n */\ndefineProps<{\n editor: BaseEditorReturn;\n core: UseEditorCoreReturn;\n /** Null when no `TemplatesProvider` is configured — no name, save or status. */\n templates: UseTemplatesFeatureReturn | null;\n /**\n * Whether to render the inline name field, from `config.templates?.nameField`.\n * False hides it whether or not the provider can save — a consumer whose\n * store has no name column, or whose own chrome owns the name.\n */\n showTemplateName: boolean;\n testEmail: UseTestEmailFeatureReturn | null;\n versionHistory: UseVersionHistoryFeatureReturn | null;\n /** Null when no `CommentsProvider` (or no `user`) is configured. */\n comments: UseCommentsFeatureReturn | null;\n}>();\n</script>\n\n<template>\n <!-- Opaque, and `--tpl-shadow-sm` because that is the step the shadow\n vocabulary assigns to sticky chrome. `.tpl-body` starts at this header's\n own height, so nothing ever passes beneath it: a translucent fill has an\n identical surface behind it and a backdrop blur has nothing to blur,\n which costs a compositing layer to render exactly `--tpl-bg`. -->\n <header\n class=\"tpl-header tpl:absolute tpl:top-0 tpl:right-0 tpl:left-0 tpl:z-50 tpl:grid tpl:h-14 tpl:grid-cols-[1fr_auto_1fr] tpl:items-center tpl:px-4 tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-sm)] tpl:border-b tpl:border-[var(--tpl-border)]\"\n >\n <!-- Left: the template's identity — the inline-editable name, and under it\n when the store dates its writes. Stacking the two needs 6px of bottom\n padding: the name is a button, so it carries border + `py-1` + half its\n leading above its text, while the timestamp carries nothing below its\n own. Centring the boxes therefore puts all of that lead at the top and\n reads as a misaligned pair. Only when both render — either alone is\n already centred on its own box. `min-w-[200px]` matches the right column\n so the centre controls are actually centred. Nothing renders before a\n template exists; there is neither a name nor a write time yet. The stack\n is gated as a whole so an empty one cannot add a `gap-2.5` before the\n cloud extras. -->\n <div\n class=\"tpl-header-left tpl:flex tpl:min-w-[200px] tpl:items-center tpl:gap-2.5\"\n >\n <div\n v-if=\"\n templates?.isAvailable.value &&\n templates.hasTemplate.value &&\n (showTemplateName || templates.timestamp.value)\n \"\n class=\"tpl:flex tpl:min-w-0 tpl:flex-col tpl:items-start\"\n :class=\"{\n 'tpl:pb-1.5': showTemplateName && templates.timestamp.value !== null,\n }\"\n >\n <TemplateNameField\n v-if=\"showTemplateName\"\n :name=\"templates.name.value\"\n :editable=\"templates.canSave.value\"\n @commit=\"templates.rename\"\n />\n <TemplateTimestamp\n v-if=\"templates.timestamp.value\"\n :iso=\"templates.timestamp.value.iso\"\n :kind=\"templates.timestamp.value.kind\"\n />\n </div>\n <!-- Version history — the third piece of \"which template, and which\n version of it\", after the name and the write time. It sits here and\n not in the right column because that column's content already\n overflows its track: `justify-end` packs to the right edge, so an\n extra control there spills *leftward* over the centre track and\n swallows clicks meant for the Preview button. Measured — it covered\n it with the menu's own back-arrow. This column packs from the left\n and has the room.\n\n Needs a template as well as a provider: a version belongs to a\n template id, so before one is loaded there is nothing to list. -->\n <VersionHistoryMenu\n v-if=\"\n versionHistory?.isAvailable.value && versionHistory.hasTemplate.value\n \"\n :versions=\"versionHistory.versions.value\"\n :is-loading=\"versionHistory.isLoading.value\"\n :is-restoring=\"versionHistory.isRestoring.value\"\n :previewing-id=\"versionHistory.previewingVersion.value?.id ?? null\"\n @open=\"versionHistory.refresh()\"\n @navigate=\"versionHistory.navigate($event)\"\n />\n <slot name=\"left-extras\" />\n </div>\n\n <!-- Center: the three \"how am I viewing the canvas\" controls, and nothing\n else — ever.\n\n The grid is `1fr auto 1fr`, so this track is exactly max-content wide\n and the equal `fr` columns centre it. Any width change therefore\n redistributes symmetrically about the header's centre and moves every\n sibling by half the delta, whichever side of the change it sits on.\n DOM order buys nothing. So a conditional control here drags the Preview\n button out from under the cursor — 114.5px when the sample/label toggle\n used to live here, and repeatedly in Cloud as collaborators came and\n went (#574).\n\n Hence the rule: nothing in this track may be conditional. No `v-if`, no\n `v-show`, no slot. Conditional controls go in the edge-anchored `fr`\n columns, which grow away from their anchored edge; preview-only ones go\n on the preview surface, where `Editor.vue` floats them over the canvas.\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div\n class=\"tpl-header-center tpl:flex tpl:items-center tpl:justify-center tpl:gap-10\"\n >\n <ViewportToggle\n :viewport=\"editor.state.viewport\"\n @change=\"editor.setViewport\"\n />\n <DarkModeToggle\n :dark-mode=\"editor.state.darkMode\"\n @change=\"editor.setDarkMode\"\n />\n <PreviewToggle\n :preview-mode=\"editor.state.previewMode\"\n @change=\"editor.setPreviewMode\"\n />\n </div>\n\n <!-- Right: save status → Comments → [cloud extras: AI] → test email → Save.\n Save is last: it is the primary action. `justify-end` anchors the row to\n the right edge, so a control appearing at its start extends the row\n leftward and moves nothing already in it — but note the row is already\n at the edge of its track at common widths, so anything added here\n overflows onto the centre rather than fitting. -->\n <div\n class=\"tpl-header-right tpl:flex tpl:min-w-[200px] tpl:items-center tpl:justify-end tpl:gap-3\"\n >\n <!-- Gated on a provider that can actually save. Without one the editor\n never learns that a save completed, so the badge could only ever read\n \"unsaved\" — worse than showing nothing. -->\n <TemplateSaveStatus\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n :status=\"templates.status.value\"\n :error-message=\"templates.errorMessage.value\"\n :is-dirty=\"editor.state.isDirty\"\n />\n\n <!-- Comments. Needs a template as well as a provider — a comment is keyed\n to a template id, so before one is loaded there is nothing to attach a\n thread to. Shared chrome, because comments are a shared\n provider-backed feature rather than a Cloud one. -->\n <button\n v-if=\"comments?.isAvailable.value && comments.hasTemplate.value\"\n type=\"button\"\n data-testid=\"comments-trigger\"\n :aria-label=\"\n comments.unresolvedCount.value > 0\n ? `${core.t.comments.button} (${comments.unresolvedCount.value})`\n : core.t.comments.button\n \"\n :aria-expanded=\"comments.isOpen.value\"\n :class=\"secondaryBtnCompactClass\"\n :style=\"\n comments.isOpen.value\n ? {\n backgroundColor: 'var(--tpl-primary-light)',\n borderColor: 'var(--tpl-primary-light)',\n }\n : undefined\n \"\n @click=\"comments.toggle()\"\n >\n <MessageCircle :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.comments.button }}\n <span\n v-if=\"comments.unresolvedCount.value > 0 && !comments.isOpen.value\"\n class=\"tpl:inline-flex tpl:size-4.5 tpl:items-center tpl:justify-center tpl:rounded-full tpl:text-[10px] tpl:font-semibold tpl:bg-[var(--tpl-primary)] tpl:text-[var(--tpl-on-primary)]\"\n >\n {{ comments.unresolvedCount.value }}\n </span>\n </button>\n\n <slot name=\"right-extras\" />\n\n <!-- The shared recipe, like every control in this row. Shared rather than\n hand-rolled so all three keep one height — a bespoke string is how\n this button came to sit 4px shorter than its neighbours. -->\n <button\n v-if=\"testEmail?.isAvailable.value\"\n type=\"button\"\n data-testid=\"test-email-trigger\"\n :aria-label=\"core.t.testEmail.title\"\n :title=\"core.t.testEmail.title\"\n :disabled=\"testEmail.isSending.value\"\n :class=\"secondaryBtnCompactClass\"\n @click=\"testEmail.open()\"\n >\n <Send v-if=\"!testEmail.isSending.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.testEmail.button }}\n </button>\n\n <!-- Save is the editor's one primary, and it only takes that treatment\n while there is something to save. Amber announces intent or selection,\n so a Save that looks identical dirty or clean announces nothing — and\n it would be the loudest element in chrome that exists to recede behind\n the canvas. Gated on `hasTemplate` too, or a disabled button would\n light up before there is anywhere to save to.\n\n Amber with the paper colour on it is 2.80:1 in light mode. That is\n accepted rather than overlooked: every amber surface and accent in the\n editor shares this pairing, and correcting this one control alone would\n make it read as a different kind of thing. The Amber-Accent Exception\n in DESIGN.md records the decision and its limits — chief among them\n that amber must never be the only carrier of a state, which is why the\n `TemplateSaveStatus` badge says \"Unsaved\" in words beside it.\n\n The light lift is the same condition, and it is Flat-At-Rest applied\n rather than bent: a surface paints no shadow until something happens to\n it, and unsaved work is that something. It sits inline rather than in\n the recipe because the recipe is shared, and a primary button at rest\n elsewhere should still be flat.\n\n Hidden entirely when the provider withheld `save` — the read-only\n equivalent of the saved-blocks permission discipline: loading a\n template and editing it locally still works, there is simply nothing\n to persist. Disabled (with a reason in the tooltip) until a template\n exists, since `save()` patches an id. -->\n <button\n v-if=\"templates?.isAvailable.value && templates.canSave.value\"\n type=\"button\"\n data-testid=\"template-save\"\n :class=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? primaryBtnCompactClass\n : secondaryBtnCompactClass\n \"\n :style=\"\n editor.state.isDirty && templates.hasTemplate.value\n ? { boxShadow: 'var(--tpl-shadow-sm)' }\n : undefined\n \"\n :disabled=\"templates.isSaving.value || !templates.hasTemplate.value\"\n :title=\"\n templates.hasTemplate.value\n ? core.t.header.save\n : core.t.header.saveNoTemplate\n \"\n @click=\"templates.requestSave()\"\n >\n <Save v-if=\"!templates.isSaving.value\" :size=\"16\" :stroke-width=\"2\" />\n <LoaderCircle v-else class=\"tpl-spinner\" :size=\"16\" :stroke-width=\"2\" />\n {{\n templates.isSaving.value ? core.t.header.saving : core.t.header.save\n }}\n </button>\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport {\n computed,\n inject,\n nextTick,\n ref,\n watch,\n type ComponentPublicInstance,\n} from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { ChevronDown, Search, X } from \"@lucide/vue\";\nimport type { MergeTag } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n MERGE_TAG_PICKER_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\n\n// Picker singleton — provided by useEditorCore. We require it explicitly so\n// the modal is unmountable only in a non-editor context (which we don't\n// support). `requireInject` throws a clear message rather than letting a\n// silent inject default mask the misuse.\nconst picker = requireInject(MERGE_TAG_PICKER_KEY, \"MergeTagPickerModal\");\nconst { t, format } = useI18n();\n\n// Theming — same pattern as every other OSS panel (ParagraphToolbar,\n// RichTextLinkDialog). The panel carries the `tpl` token class, so it must\n// re-establish the theme locally: `data-tpl-theme` selects the dark token\n// block, and the inline `themeStyles` apply the consumer's `theme` config\n// overrides. Without these the base `.tpl` rule shadows both with light\n// defaults and the picker ignores dark mode + custom theme colors.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\n// --- Search ---\nconst rawSearch = ref(\"\");\n// 200ms debounce keeps filtering responsive at 1000+ tags. The raw value\n// drives the input element (so typing feels instant); the filter reads\n// the debounced copy.\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-merge-tag-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\n// --- Highlight (keyboard nav state) ---\nconst highlightedIndex = ref(0);\n\n// --- Grouping mode ---\n// Active only when at least one tag carries `group`. When inactive, the\n// list renders flat with no headers (not even \"Other\"). The plan locks\n// this down in \"Decisions locked in\" #7.\nconst hasGroups = computed(() =>\n picker.tags.value.some((tag) => Boolean(tag.group)),\n);\n\n// --- Collapsed groups (by label) ---\n// Reset on every modal open so each session starts with all groups\n// visible. Persistence across opens would surprise users who expanded\n// in a previous edit; the cost of re-collapsing is one click.\nconst collapsedGroups = ref<Set<string>>(new Set());\nfunction isCollapsed(group: string): boolean {\n return collapsedGroups.value.has(group);\n}\nfunction toggleGroup(group: string): void {\n const next = new Set(collapsedGroups.value);\n if (next.has(group)) next.delete(group);\n else next.add(group);\n collapsedGroups.value = next;\n // Re-anchor highlight to the first visible tag — index of a\n // previously-highlighted item may now point elsewhere after the\n // collapse rearranges visibleTags.\n highlightedIndex.value = 0;\n}\n\n// --- Filtered tags ---\n// Case-insensitive substring match against label, value, description.\n// Insertion order is preserved (per decision #9).\nconst filteredTags = computed<MergeTag[]>(() => {\n const query = search.value.trim().toLowerCase();\n if (!query) return picker.tags.value;\n return picker.tags.value.filter((tag) => {\n const haystack = [tag.label, tag.value, tag.description ?? \"\"]\n .join(\" \")\n .toLowerCase();\n return haystack.includes(query);\n });\n});\n\n// --- Group order (insertion order of first appearance) ---\n// Computed against the *full* tags list (not the filtered one) so the\n// pill row stays stable as the user types. The pills represent the\n// configured groupings; filter just adjusts which items are shown.\nconst groupOrder = computed<string[]>(() => {\n if (!hasGroups.value) return [];\n const otherLabel = t.mergeTag.picker.otherGroup;\n const seen = new Set<string>();\n const order: string[] = [];\n for (const tag of picker.tags.value) {\n const key = tag.group ?? otherLabel;\n if (seen.has(key)) continue;\n seen.add(key);\n order.push(key);\n }\n return order;\n});\n\n// --- Rendered rows (header + tag entries) ---\n// Headers are only emitted in grouped mode AND only when there is no\n// active search query (per decisions #6 + #7). Flat-list mode and\n// search-active mode both render a header-less sequence.\n//\n// Collapsed groups still emit their header row (so the header stays\n// clickable), but skip their tag rows entirely — those tags are then\n// absent from `visibleTags` and the highlight skips over them.\ntype Row =\n | { kind: \"header\"; group: string; count: number }\n | { kind: \"tag\"; tag: MergeTag; index: number };\n\nconst rows = computed<Row[]>(() => {\n const tags = filteredTags.value;\n if (!hasGroups.value || searchActive.value) {\n return tags.map((tag, index) => ({ kind: \"tag\", tag, index }));\n }\n\n // Grouped mode: bucket by `group`, ungrouped → \"Other\".\n const otherLabel = t.mergeTag.picker.otherGroup;\n const buckets = new Map<string, MergeTag[]>();\n // Insertion order over the first appearance of each group label.\n const order: string[] = [];\n for (const tag of tags) {\n const key = tag.group ?? otherLabel;\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(tag);\n }\n\n const result: Row[] = [];\n let tagIndex = 0;\n for (const groupKey of order) {\n const items = buckets.get(groupKey)!;\n result.push({ kind: \"header\", group: groupKey, count: items.length });\n if (collapsedGroups.value.has(groupKey)) continue;\n for (const tag of items) {\n result.push({ kind: \"tag\", tag, index: tagIndex });\n tagIndex++;\n }\n }\n return result;\n});\n\n// Flat list of tag-rows only — used for highlight bookkeeping. Keyboard\n// nav skips headers, so the highlight index addresses this list directly.\nconst visibleTags = computed(() =>\n rows.value.flatMap((row) => (row.kind === \"tag\" ? [row.tag] : [])),\n);\n\n// --- Lifecycle: focus search + reset state when modal opens ---\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n collapsedGroups.value = new Set();\n // Search input is mounted as part of the same render cycle the\n // modal opens; nextTick guarantees the ref is bound before focus.\n nextTick(() => {\n searchInputRef.value?.focus();\n });\n }\n },\n);\n\n// Keep the highlight inside the visible-tags bounds whenever the\n// filtered list shrinks (e.g. user typed a more specific query).\nwatch(visibleTags, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\n// --- Actions ---\nfunction selectTag(tag: MergeTag): void {\n picker.resolve(tag);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleTags.value.length === 0) return;\n const next = highlightedIndex.value + delta;\n // Stop at edges (don't wrap) — feels more predictable in a long list.\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleTags.value.length - 1, next),\n );\n scrollHighlightIntoView();\n}\n\nconst listRef = ref<HTMLElement | null>(null);\n// One ref per group header so the pill row can `scrollIntoView` precisely.\n// Bound by callback-ref in the template; cleared (set to null) when Vue\n// unmounts the header during re-render.\nconst headerRefs = ref<Record<string, HTMLElement | null>>({});\n// Vue's callback-ref type widens to Element | ComponentPublicInstance | null\n// because the same ref shape works for both DOM elements and components.\n// Headers are always native buttons, so coerce to HTMLElement once here.\nfunction setHeaderRef(\n group: string,\n el: Element | ComponentPublicInstance | null,\n): void {\n headerRefs.value[group] = (el as HTMLElement | null) ?? null;\n}\n\nfunction scrollHighlightIntoView(): void {\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n const el = container.querySelector<HTMLElement>(\n `[data-merge-tag-index=\"${highlightedIndex.value}\"]`,\n );\n el?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction jumpToGroup(group: string): void {\n // Expand the group if it was collapsed — otherwise scrolling reveals\n // only the header, which is confusing for a \"jump to\" affordance.\n if (collapsedGroups.value.has(group)) {\n const next = new Set(collapsedGroups.value);\n next.delete(group);\n collapsedGroups.value = next;\n }\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n // Sum the height of every child rendered BEFORE the target header.\n // Avoids two browser quirks:\n // - `scrollIntoView({ block: \"start\" })` short-circuits when the\n // element is currently sticky-pinned at top.\n // - `offsetTop` historically misreported on sticky elements in some\n // Chromium versions; offsetHeight on each in-flow child is stable.\n // Each child is a top-level DOM row (header button or tag button),\n // and there are at most a few dozen, so the linear scan is cheap.\n let targetScrollTop = 0;\n let found = false;\n for (const child of Array.from(container.children) as HTMLElement[]) {\n if (\n child.dataset.testid === \"merge-tag-picker-group-header\" &&\n child.dataset.groupName === group\n ) {\n found = true;\n break;\n }\n targetScrollTop += child.offsetHeight;\n }\n if (!found) return;\n container.scrollTo({ top: targetScrollTop, behavior: \"smooth\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const tag = visibleTags.value[highlightedIndex.value];\n if (tag) selectTag(tag);\n }\n // Esc is forwarded by TplModal via its own keydown→close path.\n}\n\nfunction activeOptionId(): string | undefined {\n if (visibleTags.value.length === 0) return undefined;\n return `${listId}-opt-${highlightedIndex.value}`;\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\n\n// --- Empty states ---\n// Distinguish two cases so the messaging is honest:\n// - the consumer configured zero tags (defensive — button shouldn't even\n// render in that case but a programmatic open() should still degrade)\n// - the active search matched nothing\n//\n// `filteredTags` (NOT `visibleTags`) is the correct signal for \"no\n// results\" — visibleTags also excludes tags inside collapsed groups, so\n// collapsing every group would otherwise falsely trigger the empty\n// state and hide the headers the user wants to see.\nconst isEmptyConfig = computed(() => picker.tags.value.length === 0);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && filteredTags.value.length === 0,\n);\n\n// --- Pill row visibility ---\n// Pills jump-link to group headers; they only make sense when there are\n// at least 2 groups AND the user isn't currently searching (search\n// collapses the grouped view to a flat list, so the pills' targets\n// disappear).\nconst showPillRow = computed(\n () => hasGroups.value && groupOrder.value.length > 1 && !searchActive.value,\n);\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"merge-tag-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <!-- Header -->\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.mergeTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.mergeTag.picker.close\"\n data-testid=\"merge-tag-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <!-- Search -->\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.mergeTag.picker.searchPlaceholder\"\n :aria-label=\"t.mergeTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"merge-tag-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <!-- Group pills — visible only when grouping is active and the\n user isn't searching. Click a pill to jump (and expand) the\n matching group. Horizontally scrollable so a long group list\n degrades gracefully. -->\n <div\n v-if=\"showPillRow\"\n class=\"tpl:flex tpl:gap-1.5 tpl:overflow-x-auto tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2\"\n data-testid=\"merge-tag-picker-group-pills\"\n >\n <button\n v-for=\"group in groupOrder\"\n :key=\"group\"\n type=\"button\"\n class=\"tpl:flex tpl:flex-shrink-0 tpl:cursor-pointer tpl:items-center tpl:rounded-full tpl:border tpl:border-[var(--tpl-border)] tpl:bg-transparent tpl:px-2.5 tpl:py-0.5 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n data-testid=\"merge-tag-picker-group-pill\"\n :data-group-name=\"group\"\n @click=\"jumpToGroup(group)\"\n >\n {{ group }}\n </button>\n </div>\n\n <!-- Body / list -->\n <!-- `position: relative` makes this the offsetParent for the sticky\n group headers below, so `header.offsetTop` reliably reports each\n header's natural in-flow position. jumpToGroup() depends on this\n — see the comment in jumpToGroup() for why scrollIntoView isn't\n sufficient. -->\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.mergeTag.picker.title\"\n data-testid=\"merge-tag-picker-list\"\n >\n <template v-if=\"isEmptyConfig\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.empty }}\n </div>\n </template>\n <template v-else-if=\"isNoResults\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.noResults }}\n </div>\n </template>\n <template v-else>\n <template v-for=\"(row, rowIndex) in rows\" :key=\"rowIndex\">\n <button\n v-if=\"row.kind === 'header'\"\n :ref=\"(el) => setHeaderRef(row.group, el)\"\n type=\"button\"\n :aria-expanded=\"!isCollapsed(row.group)\"\n :aria-controls=\"listId\"\n class=\"tpl:sticky tpl:top-0 tpl:z-10 tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-[var(--tpl-bg-elevated)] tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-left tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)]\"\n data-testid=\"merge-tag-picker-group-header\"\n :data-group-name=\"row.group\"\n :data-group-collapsed=\"isCollapsed(row.group) ? 'true' : 'false'\"\n @click=\"toggleGroup(row.group)\"\n >\n <ChevronDown\n :size=\"12\"\n :stroke-width=\"2\"\n class=\"tpl:transition-transform\"\n :class=\"isCollapsed(row.group) ? 'tpl:-rotate-90' : ''\"\n />\n <span>{{ row.group }}</span>\n <span class=\"tpl:ml-1 tpl:font-normal\">\n ({{\n format(t.mergeTag.picker.groupCount, { count: row.count })\n }})\n </span>\n </button>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-merge-tag-index=\"row.index\"\n :data-merge-tag-value=\"row.tag.value\"\n :title=\"\n row.tag.description\n ? `${row.tag.value} — ${row.tag.description}`\n : row.tag.value\n \"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"merge-tag-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectTag(row.tag)\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.tag.label\n }}</span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.value }}\n </span>\n <span\n v-if=\"row.tag.description\"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.description }}\n </span>\n <!-- What the previews will render in Sample mode, so an author\n knows before inserting. Shown only when configured. -->\n <span\n v-if=\"row.tag.sample !== undefined\"\n data-testid=\"merge-tag-picker-sample\"\n class=\"tpl:line-clamp-1 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.mergeTag.picker.sample, { sample: row.tag.sample })\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport {\n computed,\n inject,\n nextTick,\n ref,\n watch,\n type ComponentPublicInstance,\n} from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { ChevronDown, Search, X } from \"@lucide/vue\";\nimport type { MergeTag } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n MERGE_TAG_PICKER_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\n\n// Picker singleton — provided by useEditorCore. We require it explicitly so\n// the modal is unmountable only in a non-editor context (which we don't\n// support). `requireInject` throws a clear message rather than letting a\n// silent inject default mask the misuse.\nconst picker = requireInject(MERGE_TAG_PICKER_KEY, \"MergeTagPickerModal\");\nconst { t, format } = useI18n();\n\n// Theming — same pattern as every other OSS panel (ParagraphToolbar,\n// RichTextLinkDialog). The panel carries the `tpl` token class, so it must\n// re-establish the theme locally: `data-tpl-theme` selects the dark token\n// block, and the inline `themeStyles` apply the consumer's `theme` config\n// overrides. Without these the base `.tpl` rule shadows both with light\n// defaults and the picker ignores dark mode + custom theme colors.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\n// --- Search ---\nconst rawSearch = ref(\"\");\n// 200ms debounce keeps filtering responsive at 1000+ tags. The raw value\n// drives the input element (so typing feels instant); the filter reads\n// the debounced copy.\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-merge-tag-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\n// --- Highlight (keyboard nav state) ---\nconst highlightedIndex = ref(0);\n\n// --- Grouping mode ---\n// Active only when at least one tag carries `group`. When inactive, the\n// list renders flat with no headers (not even \"Other\"). The plan locks\n// this down in \"Decisions locked in\" #7.\nconst hasGroups = computed(() =>\n picker.tags.value.some((tag) => Boolean(tag.group)),\n);\n\n// --- Collapsed groups (by label) ---\n// Reset on every modal open so each session starts with all groups\n// visible. Persistence across opens would surprise users who expanded\n// in a previous edit; the cost of re-collapsing is one click.\nconst collapsedGroups = ref<Set<string>>(new Set());\nfunction isCollapsed(group: string): boolean {\n return collapsedGroups.value.has(group);\n}\nfunction toggleGroup(group: string): void {\n const next = new Set(collapsedGroups.value);\n if (next.has(group)) next.delete(group);\n else next.add(group);\n collapsedGroups.value = next;\n // Re-anchor highlight to the first visible tag — index of a\n // previously-highlighted item may now point elsewhere after the\n // collapse rearranges visibleTags.\n highlightedIndex.value = 0;\n}\n\n// --- Filtered tags ---\n// Case-insensitive substring match against label, value, description.\n// Insertion order is preserved (per decision #9).\nconst filteredTags = computed<MergeTag[]>(() => {\n const query = search.value.trim().toLowerCase();\n if (!query) return picker.tags.value;\n return picker.tags.value.filter((tag) => {\n const haystack = [tag.label, tag.value, tag.description ?? \"\"]\n .join(\" \")\n .toLowerCase();\n return haystack.includes(query);\n });\n});\n\n// --- Group order (insertion order of first appearance) ---\n// Computed against the *full* tags list (not the filtered one) so the\n// pill row stays stable as the user types. The pills represent the\n// configured groupings; filter just adjusts which items are shown.\nconst groupOrder = computed<string[]>(() => {\n if (!hasGroups.value) return [];\n const otherLabel = t.mergeTag.picker.otherGroup;\n const seen = new Set<string>();\n const order: string[] = [];\n for (const tag of picker.tags.value) {\n const key = tag.group ?? otherLabel;\n if (seen.has(key)) continue;\n seen.add(key);\n order.push(key);\n }\n return order;\n});\n\n// --- Rendered rows (header + tag entries) ---\n// Headers are only emitted in grouped mode AND only when there is no\n// active search query (per decisions #6 + #7). Flat-list mode and\n// search-active mode both render a header-less sequence.\n//\n// Collapsed groups still emit their header row (so the header stays\n// clickable), but skip their tag rows entirely — those tags are then\n// absent from `visibleTags` and the highlight skips over them.\ntype Row =\n | { kind: \"header\"; group: string; count: number }\n | { kind: \"tag\"; tag: MergeTag; index: number };\n\nconst rows = computed<Row[]>(() => {\n const tags = filteredTags.value;\n if (!hasGroups.value || searchActive.value) {\n return tags.map((tag, index) => ({ kind: \"tag\", tag, index }));\n }\n\n // Grouped mode: bucket by `group`, ungrouped → \"Other\".\n const otherLabel = t.mergeTag.picker.otherGroup;\n const buckets = new Map<string, MergeTag[]>();\n // Insertion order over the first appearance of each group label.\n const order: string[] = [];\n for (const tag of tags) {\n const key = tag.group ?? otherLabel;\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(tag);\n }\n\n const result: Row[] = [];\n let tagIndex = 0;\n for (const groupKey of order) {\n const items = buckets.get(groupKey)!;\n result.push({ kind: \"header\", group: groupKey, count: items.length });\n if (collapsedGroups.value.has(groupKey)) continue;\n for (const tag of items) {\n result.push({ kind: \"tag\", tag, index: tagIndex });\n tagIndex++;\n }\n }\n return result;\n});\n\n// Flat list of tag-rows only — used for highlight bookkeeping. Keyboard\n// nav skips headers, so the highlight index addresses this list directly.\nconst visibleTags = computed(() =>\n rows.value.flatMap((row) => (row.kind === \"tag\" ? [row.tag] : [])),\n);\n\n// --- Lifecycle: focus search + reset state when modal opens ---\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n collapsedGroups.value = new Set();\n // Search input is mounted as part of the same render cycle the\n // modal opens; nextTick guarantees the ref is bound before focus.\n nextTick(() => {\n searchInputRef.value?.focus();\n });\n }\n },\n);\n\n// Keep the highlight inside the visible-tags bounds whenever the\n// filtered list shrinks (e.g. user typed a more specific query).\nwatch(visibleTags, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\n// --- Actions ---\nfunction selectTag(tag: MergeTag): void {\n picker.resolve(tag);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleTags.value.length === 0) return;\n const next = highlightedIndex.value + delta;\n // Stop at edges (don't wrap) — feels more predictable in a long list.\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleTags.value.length - 1, next),\n );\n scrollHighlightIntoView();\n}\n\nconst listRef = ref<HTMLElement | null>(null);\n// One ref per group header so the pill row can `scrollIntoView` precisely.\n// Bound by callback-ref in the template; cleared (set to null) when Vue\n// unmounts the header during re-render.\nconst headerRefs = ref<Record<string, HTMLElement | null>>({});\n// Vue's callback-ref type widens to Element | ComponentPublicInstance | null\n// because the same ref shape works for both DOM elements and components.\n// Headers are always native buttons, so coerce to HTMLElement once here.\nfunction setHeaderRef(\n group: string,\n el: Element | ComponentPublicInstance | null,\n): void {\n headerRefs.value[group] = (el as HTMLElement | null) ?? null;\n}\n\nfunction scrollHighlightIntoView(): void {\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n const el = container.querySelector<HTMLElement>(\n `[data-merge-tag-index=\"${highlightedIndex.value}\"]`,\n );\n el?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction jumpToGroup(group: string): void {\n // Expand the group if it was collapsed — otherwise scrolling reveals\n // only the header, which is confusing for a \"jump to\" affordance.\n if (collapsedGroups.value.has(group)) {\n const next = new Set(collapsedGroups.value);\n next.delete(group);\n collapsedGroups.value = next;\n }\n nextTick(() => {\n const container = listRef.value;\n if (!container) return;\n // Sum the height of every child rendered BEFORE the target header.\n // Avoids two browser quirks:\n // - `scrollIntoView({ block: \"start\" })` short-circuits when the\n // element is currently sticky-pinned at top.\n // - `offsetTop` historically misreported on sticky elements in some\n // Chromium versions; offsetHeight on each in-flow child is stable.\n // Each child is a top-level DOM row (header button or tag button),\n // and there are at most a few dozen, so the linear scan is cheap.\n let targetScrollTop = 0;\n let found = false;\n for (const child of Array.from(container.children) as HTMLElement[]) {\n if (\n child.dataset.testid === \"merge-tag-picker-group-header\" &&\n child.dataset.groupName === group\n ) {\n found = true;\n break;\n }\n targetScrollTop += child.offsetHeight;\n }\n if (!found) return;\n container.scrollTo({ top: targetScrollTop, behavior: \"smooth\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const tag = visibleTags.value[highlightedIndex.value];\n if (tag) selectTag(tag);\n }\n // Esc is forwarded by TplModal via its own keydown→close path.\n}\n\nfunction activeOptionId(): string | undefined {\n if (visibleTags.value.length === 0) return undefined;\n return `${listId}-opt-${highlightedIndex.value}`;\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\n\n// --- Empty states ---\n// Distinguish two cases so the messaging is honest:\n// - the consumer configured zero tags (defensive — button shouldn't even\n// render in that case but a programmatic open() should still degrade)\n// - the active search matched nothing\n//\n// `filteredTags` (NOT `visibleTags`) is the correct signal for \"no\n// results\" — visibleTags also excludes tags inside collapsed groups, so\n// collapsing every group would otherwise falsely trigger the empty\n// state and hide the headers the user wants to see.\nconst isEmptyConfig = computed(() => picker.tags.value.length === 0);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && filteredTags.value.length === 0,\n);\n\n// --- Pill row visibility ---\n// Pills jump-link to group headers; they only make sense when there are\n// at least 2 groups AND the user isn't currently searching (search\n// collapses the grouped view to a flat list, so the pills' targets\n// disappear).\nconst showPillRow = computed(\n () => hasGroups.value && groupOrder.value.length > 1 && !searchActive.value,\n);\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"merge-tag-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <!-- Header -->\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.mergeTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.mergeTag.picker.close\"\n data-testid=\"merge-tag-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <!-- Search -->\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.mergeTag.picker.searchPlaceholder\"\n :aria-label=\"t.mergeTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"merge-tag-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <!-- Group pills — visible only when grouping is active and the\n user isn't searching. Click a pill to jump (and expand) the\n matching group. Horizontally scrollable so a long group list\n degrades gracefully. -->\n <div\n v-if=\"showPillRow\"\n class=\"tpl:flex tpl:gap-1.5 tpl:overflow-x-auto tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2\"\n data-testid=\"merge-tag-picker-group-pills\"\n >\n <button\n v-for=\"group in groupOrder\"\n :key=\"group\"\n type=\"button\"\n class=\"tpl:flex tpl:flex-shrink-0 tpl:cursor-pointer tpl:items-center tpl:rounded-full tpl:border tpl:border-[var(--tpl-border)] tpl:bg-transparent tpl:px-2.5 tpl:py-0.5 tpl:text-[11px] tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n data-testid=\"merge-tag-picker-group-pill\"\n :data-group-name=\"group\"\n @click=\"jumpToGroup(group)\"\n >\n {{ group }}\n </button>\n </div>\n\n <!-- Body / list -->\n <!-- `position: relative` makes this the offsetParent for the sticky\n group headers below, so `header.offsetTop` reliably reports each\n header's natural in-flow position. jumpToGroup() depends on this\n — see the comment in jumpToGroup() for why scrollIntoView isn't\n sufficient. -->\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.mergeTag.picker.title\"\n data-testid=\"merge-tag-picker-list\"\n >\n <template v-if=\"isEmptyConfig\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.empty }}\n </div>\n </template>\n <template v-else-if=\"isNoResults\">\n <div\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"merge-tag-picker-empty\"\n >\n {{ t.mergeTag.picker.noResults }}\n </div>\n </template>\n <template v-else>\n <template v-for=\"(row, rowIndex) in rows\" :key=\"rowIndex\">\n <button\n v-if=\"row.kind === 'header'\"\n :ref=\"(el) => setHeaderRef(row.group, el)\"\n type=\"button\"\n :aria-expanded=\"!isCollapsed(row.group)\"\n :aria-controls=\"listId\"\n class=\"tpl:sticky tpl:top-0 tpl:z-10 tpl:flex tpl:w-full tpl:cursor-pointer tpl:items-center tpl:gap-1.5 tpl:border-none tpl:bg-[var(--tpl-bg-elevated)] tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-left tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)]\"\n data-testid=\"merge-tag-picker-group-header\"\n :data-group-name=\"row.group\"\n :data-group-collapsed=\"isCollapsed(row.group) ? 'true' : 'false'\"\n @click=\"toggleGroup(row.group)\"\n >\n <ChevronDown\n :size=\"12\"\n :stroke-width=\"2\"\n class=\"tpl:transition-transform\"\n :class=\"isCollapsed(row.group) ? 'tpl:-rotate-90' : ''\"\n />\n <span>{{ row.group }}</span>\n <span class=\"tpl:ml-1 tpl:font-normal\">\n ({{\n format(t.mergeTag.picker.groupCount, { count: row.count })\n }})\n </span>\n </button>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-merge-tag-index=\"row.index\"\n :data-merge-tag-value=\"row.tag.value\"\n :title=\"\n row.tag.description\n ? `${row.tag.value} — ${row.tag.description}`\n : row.tag.value\n \"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"merge-tag-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectTag(row.tag)\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.tag.label\n }}</span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.value }}\n </span>\n <span\n v-if=\"row.tag.description\"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{ row.tag.description }}\n </span>\n <!-- What the previews will render in Sample mode, so an author\n knows before inserting. Shown only when configured. -->\n <span\n v-if=\"row.tag.sample !== undefined\"\n data-testid=\"merge-tag-picker-sample\"\n class=\"tpl:line-clamp-1 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n format(t.mergeTag.picker.sample, { sample: row.tag.sample })\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n type SyntaxPreset,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\n// Small outlined keyword chip (IF, ENDIF, FOR…) shown next to a tag in the\n// merge tag picker and the typing-autocomplete list. It marks a configured\n// tag whose `value` is a logic tag rather than a data tag, so authors can\n// tell at a glance which entries insert control flow. The visual mirrors the\n// on-canvas logic badge (outlined, primary color) for consistency. Renders\n// nothing when the value isn't logic-shaped, so callers can drop it into any\n// row unconditionally.\nconst props = defineProps<{ value: string; syntax: SyntaxPreset }>();\n\nconst isLogic = computed(() => isLogicMergeTagValue(props.value, props.syntax));\nconst keyword = computed(() =>\n getLogicMergeTagKeyword(props.value, props.syntax),\n);\n</script>\n\n<template>\n <span\n v-if=\"isLogic\"\n class=\"tpl:inline-flex tpl:flex-shrink-0 tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:leading-none tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n style=\"\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n data-testid=\"merge-tag-logic-badge\"\n :data-logic-keyword=\"keyword\"\n >\n {{ keyword }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport {\n getLogicMergeTagKeyword,\n isLogicMergeTagValue,\n type SyntaxPreset,\n} from \"@templatical/types\";\nimport { computed } from \"vue\";\n\n// Small outlined keyword chip (IF, ENDIF, FOR…) shown next to a tag in the\n// merge tag picker and the typing-autocomplete list. It marks a configured\n// tag whose `value` is a logic tag rather than a data tag, so authors can\n// tell at a glance which entries insert control flow. The visual mirrors the\n// on-canvas logic badge (outlined, primary color) for consistency. Renders\n// nothing when the value isn't logic-shaped, so callers can drop it into any\n// row unconditionally.\nconst props = defineProps<{ value: string; syntax: SyntaxPreset }>();\n\nconst isLogic = computed(() => isLogicMergeTagValue(props.value, props.syntax));\nconst keyword = computed(() =>\n getLogicMergeTagKeyword(props.value, props.syntax),\n);\n</script>\n\n<template>\n <span\n v-if=\"isLogic\"\n class=\"tpl:inline-flex tpl:flex-shrink-0 tpl:items-center tpl:rounded tpl:px-1.5 tpl:py-0.5 tpl:text-[10px] tpl:leading-none tpl:font-bold tpl:tracking-wide tpl:uppercase\"\n style=\"\n border: 1.5px solid\n color-mix(in srgb, var(--tpl-primary) 50%, transparent);\n color: var(--tpl-primary);\n \"\n data-testid=\"merge-tag-logic-badge\"\n :data-logic-keyword=\"keyword\"\n >\n {{ keyword }}\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject, nextTick, ref, watch } from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { Search, X } from \"@lucide/vue\";\nimport type { LogicPair, LogicTag, SyntaxPreset } from \"@templatical/types\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport LogicTagBadge from \"./LogicTagBadge.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n LOGIC_TAG_PICKER_KEY,\n MERGE_TAG_SYNTAX_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\nimport type { LogicTagPickerResult } from \"../composables/useLogicTagPicker\";\n\n// Picker singleton — provided by useEditorCore. Required explicitly so the\n// modal only functions inside an editor context.\nconst picker = requireInject(LOGIC_TAG_PICKER_KEY, \"LogicTagPickerModal\");\nconst { t } = useI18n();\nconst syntax = inject<SyntaxPreset>(\n MERGE_TAG_SYNTAX_KEY,\n SYNTAX_PRESETS.liquid,\n);\n\n// Theming — same pattern as MergeTagPickerModal: re-establish the theme\n// locally so the `.tpl` token root resolves dark mode + consumer overrides.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\nconst rawSearch = ref(\"\");\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-logic-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\nconst highlightedIndex = ref(0);\n\nfunction matches(haystackParts: (string | undefined)[]): boolean {\n const query = search.value.trim().toLowerCase();\n if (!query) return true;\n return haystackParts.filter(Boolean).join(\" \").toLowerCase().includes(query);\n}\n\nconst filteredTags = computed<LogicTag[]>(() =>\n picker.tags.value.filter((tag) =>\n matches([tag.label, tag.value, tag.description]),\n ),\n);\nconst filteredPairs = computed<LogicPair[]>(() =>\n picker.pairs.value.filter((pair) =>\n matches([pair.label, pair.before, pair.after, pair.description]),\n ),\n);\n\n// Tags and pairs share one grouped list: a `group` section holds both its\n// standalone tags and its open/close pairs, so a group used by both (e.g.\n// \"Conditions\") renders once instead of appearing separately for each kind.\n// The badge shape — one pill for a tag, two for a pair — signals\n// insert-vs-wrap per row.\ntype Entry =\n | { kind: \"tag\"; tag: LogicTag; group?: string }\n | { kind: \"pair\"; pair: LogicPair; group?: string };\n\n// Pairs first, then standalone tags, so within a shared group the wrapping\n// constructs (e.g. If VIP) sort ahead of single tokens (e.g. Else). Group\n// order follows first appearance across this combined sequence.\nconst filteredEntries = computed<Entry[]>(() => [\n ...filteredPairs.value.map((pair): Entry => ({\n kind: \"pair\",\n pair,\n group: pair.group,\n })),\n ...filteredTags.value.map((tag): Entry => ({\n kind: \"tag\",\n tag,\n group: tag.group,\n })),\n]);\n\nconst hasGroups = computed(\n () =>\n picker.tags.value.some((tag) => Boolean(tag.group)) ||\n picker.pairs.value.some((pair) => Boolean(pair.group)),\n);\n\n// Rows drive rendering; `items` is the parallel flat list of selectable\n// entries in render order, so `highlightedIndex` (keyboard nav) maps 1:1.\ntype Row =\n | { kind: \"header\"; label: string }\n | { kind: \"tag\"; tag: LogicTag; index: number }\n | { kind: \"pair\"; pair: LogicPair; index: number };\n\nfunction bucket(entries: Entry[]): [string, Entry[]][] {\n const otherLabel = t.logicTag.picker.otherGroup;\n const map = new Map<string, Entry[]>();\n const order: string[] = [];\n for (const entry of entries) {\n const key = entry.group ?? otherLabel;\n if (!map.has(key)) {\n map.set(key, []);\n order.push(key);\n }\n map.get(key)!.push(entry);\n }\n return order.map((key) => [key, map.get(key)!]);\n}\n\nconst built = computed(() => {\n const rows: Row[] = [];\n const items: LogicTagPickerResult[] = [];\n let idx = 0;\n\n const pushEntry = (entry: Entry): void => {\n if (entry.kind === \"tag\") {\n rows.push({ kind: \"tag\", tag: entry.tag, index: idx });\n items.push(entry.tag);\n } else {\n rows.push({ kind: \"pair\", pair: entry.pair, index: idx });\n items.push(entry.pair);\n }\n idx++;\n };\n\n const entries = filteredEntries.value;\n if (searchActive.value || !hasGroups.value) {\n // Flat list: search flattens groups (matches the merge-tag picker), and\n // an ungrouped config has no headers to render.\n for (const entry of entries) pushEntry(entry);\n } else {\n for (const [group, groupEntries] of bucket(entries)) {\n rows.push({ kind: \"header\", label: group });\n for (const entry of groupEntries) pushEntry(entry);\n }\n }\n\n return { rows, items };\n});\n\nconst visibleItems = computed(() => built.value.items);\n\nconst isEmptyConfig = computed(\n () => picker.tags.value.length === 0 && picker.pairs.value.length === 0,\n);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && visibleItems.value.length === 0,\n);\n\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n nextTick(() => searchInputRef.value?.focus());\n }\n },\n);\n\nwatch(visibleItems, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\nconst listRef = ref<HTMLElement | null>(null);\n\nfunction selectItem(item: LogicTagPickerResult): void {\n picker.resolve(item);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleItems.value.length === 0) return;\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleItems.value.length - 1, highlightedIndex.value + delta),\n );\n nextTick(() => {\n listRef.value\n ?.querySelector<HTMLElement>(\n `[data-logic-index=\"${highlightedIndex.value}\"]`,\n )\n ?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const item = visibleItems.value[highlightedIndex.value];\n if (item) selectItem(item);\n }\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\nfunction activeOptionId(): string | undefined {\n return visibleItems.value.length === 0\n ? undefined\n : `${listId}-opt-${highlightedIndex.value}`;\n}\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"logic-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.logicTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.logicTag.picker.close\"\n data-testid=\"logic-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.logicTag.picker.searchPlaceholder\"\n :aria-label=\"t.logicTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"logic-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.logicTag.picker.title\"\n data-testid=\"logic-picker-list\"\n >\n <div\n v-if=\"isEmptyConfig\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.empty }}\n </div>\n <div\n v-else-if=\"isNoResults\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.noResults }}\n </div>\n <template v-else>\n <template v-for=\"(row, rowIndex) in built.rows\" :key=\"rowIndex\">\n <div\n v-if=\"row.kind === 'header'\"\n class=\"tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-header\"\n :data-group-name=\"row.label\"\n >\n {{ row.label }}\n </div>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-logic-index=\"row.index\"\n :data-logic-kind=\"row.kind\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"logic-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectItem(row.kind === 'tag' ? row.tag : row.pair)\"\n >\n <span\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.kind === \"tag\" ? row.tag.label : row.pair.label\n }}</span>\n <span\n v-if=\"row.kind === 'tag'\"\n class=\"tpl:flex tpl:items-center\"\n >\n <LogicTagBadge :value=\"row.tag.value\" :syntax=\"syntax\" />\n </span>\n <span v-else class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <LogicTagBadge :value=\"row.pair.before\" :syntax=\"syntax\" />\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">…</span>\n <LogicTagBadge :value=\"row.pair.after\" :syntax=\"syntax\" />\n </span>\n </span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.value\n : `${row.pair.before} … ${row.pair.after}`\n }}\n </span>\n <span\n v-if=\"\n (row.kind === 'tag'\n ? row.tag.description\n : row.pair.description) as string | undefined\n \"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.description\n : row.pair.description\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject, nextTick, ref, watch } from \"vue\";\nimport { refDebounced } from \"@vueuse/core\";\nimport { Search, X } from \"@lucide/vue\";\nimport type { LogicPair, LogicTag, SyntaxPreset } from \"@templatical/types\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport TplModal from \"./TplModal.vue\";\nimport LogicTagBadge from \"./LogicTagBadge.vue\";\nimport { useI18n } from \"../composables/useI18n\";\nimport {\n LOGIC_TAG_PICKER_KEY,\n MERGE_TAG_SYNTAX_KEY,\n THEME_STYLES_KEY,\n UI_THEME_KEY,\n requireInject,\n} from \"../keys\";\nimport type { LogicTagPickerResult } from \"../composables/useLogicTagPicker\";\n\n// Picker singleton — provided by useEditorCore. Required explicitly so the\n// modal only functions inside an editor context.\nconst picker = requireInject(LOGIC_TAG_PICKER_KEY, \"LogicTagPickerModal\");\nconst { t } = useI18n();\nconst syntax = inject<SyntaxPreset>(\n MERGE_TAG_SYNTAX_KEY,\n SYNTAX_PRESETS.liquid,\n);\n\n// Theming — same pattern as MergeTagPickerModal: re-establish the theme\n// locally so the `.tpl` token root resolves dark mode + consumer overrides.\nconst themeStyles = inject(THEME_STYLES_KEY, null);\nconst tplUiTheme = inject(UI_THEME_KEY, null);\n\nconst rawSearch = ref(\"\");\nconst search = refDebounced(rawSearch, 200);\nconst searchInputRef = ref<HTMLInputElement | null>(null);\nconst listId = \"tpl-logic-picker-list\";\nconst searchActive = computed(() => search.value.trim().length > 0);\n\nconst highlightedIndex = ref(0);\n\nfunction matches(haystackParts: (string | undefined)[]): boolean {\n const query = search.value.trim().toLowerCase();\n if (!query) return true;\n return haystackParts.filter(Boolean).join(\" \").toLowerCase().includes(query);\n}\n\nconst filteredTags = computed<LogicTag[]>(() =>\n picker.tags.value.filter((tag) =>\n matches([tag.label, tag.value, tag.description]),\n ),\n);\nconst filteredPairs = computed<LogicPair[]>(() =>\n picker.pairs.value.filter((pair) =>\n matches([pair.label, pair.before, pair.after, pair.description]),\n ),\n);\n\n// Tags and pairs share one grouped list: a `group` section holds both its\n// standalone tags and its open/close pairs, so a group used by both (e.g.\n// \"Conditions\") renders once instead of appearing separately for each kind.\n// The badge shape — one pill for a tag, two for a pair — signals\n// insert-vs-wrap per row.\ntype Entry =\n | { kind: \"tag\"; tag: LogicTag; group?: string }\n | { kind: \"pair\"; pair: LogicPair; group?: string };\n\n// Pairs first, then standalone tags, so within a shared group the wrapping\n// constructs (e.g. If VIP) sort ahead of single tokens (e.g. Else). Group\n// order follows first appearance across this combined sequence.\nconst filteredEntries = computed<Entry[]>(() => [\n ...filteredPairs.value.map((pair): Entry => ({\n kind: \"pair\",\n pair,\n group: pair.group,\n })),\n ...filteredTags.value.map((tag): Entry => ({\n kind: \"tag\",\n tag,\n group: tag.group,\n })),\n]);\n\nconst hasGroups = computed(\n () =>\n picker.tags.value.some((tag) => Boolean(tag.group)) ||\n picker.pairs.value.some((pair) => Boolean(pair.group)),\n);\n\n// Rows drive rendering; `items` is the parallel flat list of selectable\n// entries in render order, so `highlightedIndex` (keyboard nav) maps 1:1.\ntype Row =\n | { kind: \"header\"; label: string }\n | { kind: \"tag\"; tag: LogicTag; index: number }\n | { kind: \"pair\"; pair: LogicPair; index: number };\n\nfunction bucket(entries: Entry[]): [string, Entry[]][] {\n const otherLabel = t.logicTag.picker.otherGroup;\n const map = new Map<string, Entry[]>();\n const order: string[] = [];\n for (const entry of entries) {\n const key = entry.group ?? otherLabel;\n if (!map.has(key)) {\n map.set(key, []);\n order.push(key);\n }\n map.get(key)!.push(entry);\n }\n return order.map((key) => [key, map.get(key)!]);\n}\n\nconst built = computed(() => {\n const rows: Row[] = [];\n const items: LogicTagPickerResult[] = [];\n let idx = 0;\n\n const pushEntry = (entry: Entry): void => {\n if (entry.kind === \"tag\") {\n rows.push({ kind: \"tag\", tag: entry.tag, index: idx });\n items.push(entry.tag);\n } else {\n rows.push({ kind: \"pair\", pair: entry.pair, index: idx });\n items.push(entry.pair);\n }\n idx++;\n };\n\n const entries = filteredEntries.value;\n if (searchActive.value || !hasGroups.value) {\n // Flat list: search flattens groups (matches the merge-tag picker), and\n // an ungrouped config has no headers to render.\n for (const entry of entries) pushEntry(entry);\n } else {\n for (const [group, groupEntries] of bucket(entries)) {\n rows.push({ kind: \"header\", label: group });\n for (const entry of groupEntries) pushEntry(entry);\n }\n }\n\n return { rows, items };\n});\n\nconst visibleItems = computed(() => built.value.items);\n\nconst isEmptyConfig = computed(\n () => picker.tags.value.length === 0 && picker.pairs.value.length === 0,\n);\nconst isNoResults = computed(\n () => !isEmptyConfig.value && visibleItems.value.length === 0,\n);\n\nwatch(\n () => picker.isOpen.value,\n (isOpen) => {\n if (isOpen) {\n rawSearch.value = \"\";\n highlightedIndex.value = 0;\n nextTick(() => searchInputRef.value?.focus());\n }\n },\n);\n\nwatch(visibleItems, (next) => {\n if (highlightedIndex.value >= next.length) {\n highlightedIndex.value = Math.max(0, next.length - 1);\n }\n});\n\nconst listRef = ref<HTMLElement | null>(null);\n\nfunction selectItem(item: LogicTagPickerResult): void {\n picker.resolve(item);\n}\n\nfunction cancel(): void {\n picker.resolve(null);\n}\n\nfunction moveHighlight(delta: number): void {\n if (visibleItems.value.length === 0) return;\n highlightedIndex.value = Math.max(\n 0,\n Math.min(visibleItems.value.length - 1, highlightedIndex.value + delta),\n );\n nextTick(() => {\n listRef.value\n ?.querySelector<HTMLElement>(\n `[data-logic-index=\"${highlightedIndex.value}\"]`,\n )\n ?.scrollIntoView({ block: \"nearest\" });\n });\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n moveHighlight(1);\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n moveHighlight(-1);\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const item = visibleItems.value[highlightedIndex.value];\n if (item) selectItem(item);\n }\n}\n\nfunction optionId(index: number): string {\n return `${listId}-opt-${index}`;\n}\nfunction activeOptionId(): string | undefined {\n return visibleItems.value.length === 0\n ? undefined\n : `${listId}-opt-${highlightedIndex.value}`;\n}\n</script>\n\n<template>\n <TplModal :visible=\"picker.isOpen.value\" @close=\"cancel\" @keydown=\"onKeydown\">\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"`${listId}-title`\"\n :data-tpl-theme=\"tplUiTheme\"\n data-testid=\"logic-picker-modal\"\n class=\"tpl tpl:flex tpl:max-h-[80%] tpl:w-[420px] tpl:max-w-full tpl:flex-col tpl:rounded-[var(--tpl-radius)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:shadow-[var(--tpl-shadow-lg)]\"\n :style=\"themeStyles\"\n >\n <header\n class=\"tpl:flex tpl:items-center tpl:justify-between tpl:gap-3 tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <h2\n :id=\"`${listId}-title`\"\n class=\"tpl:m-0 tpl:text-sm tpl:font-semibold tpl:text-[var(--tpl-text)]\"\n >\n {{ t.logicTag.picker.title }}\n </h2>\n <button\n type=\"button\"\n class=\"tpl:flex tpl:h-7 tpl:w-7 tpl:cursor-pointer tpl:items-center tpl:justify-center tpl:rounded-[var(--tpl-radius-sm)] tpl:border-none tpl:bg-transparent tpl:text-[var(--tpl-text-dim)] tpl:transition-colors tpl:hover:bg-[var(--tpl-bg-hover)] tpl:hover:text-[var(--tpl-text)]\"\n :aria-label=\"t.logicTag.picker.close\"\n data-testid=\"logic-picker-close\"\n @click=\"cancel\"\n >\n <X :size=\"16\" :stroke-width=\"2\" />\n </button>\n </header>\n\n <div\n class=\"tpl:relative tpl:border-b tpl:border-[var(--tpl-border)] tpl:px-3 tpl:py-2.5\"\n >\n <Search\n class=\"tpl:pointer-events-none tpl:absolute tpl:top-1/2 tpl:left-5 tpl:-translate-y-1/2 tpl:text-[var(--tpl-text-dim)]\"\n :size=\"14\"\n :stroke-width=\"2\"\n />\n <input\n :id=\"`${listId}-search`\"\n ref=\"searchInputRef\"\n v-model=\"rawSearch\"\n type=\"text\"\n class=\"tpl:w-full tpl:rounded-[var(--tpl-radius-sm)] tpl:border tpl:border-[var(--tpl-border)] tpl:bg-[var(--tpl-bg)] tpl:py-1.5 tpl:pr-3 tpl:pl-8 tpl:text-sm tpl:text-[var(--tpl-text)] tpl:outline-none tpl:focus:border-[var(--tpl-primary)]\"\n :placeholder=\"t.logicTag.picker.searchPlaceholder\"\n :aria-label=\"t.logicTag.picker.searchAriaLabel\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeOptionId()\"\n data-testid=\"logic-picker-search\"\n @keydown.enter.prevent\n />\n </div>\n\n <div\n :id=\"listId\"\n ref=\"listRef\"\n class=\"tpl:relative tpl:max-h-[60vh] tpl:flex-1 tpl:overflow-y-auto\"\n role=\"listbox\"\n :aria-label=\"t.logicTag.picker.title\"\n data-testid=\"logic-picker-list\"\n >\n <div\n v-if=\"isEmptyConfig\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.empty }}\n </div>\n <div\n v-else-if=\"isNoResults\"\n class=\"tpl:px-3 tpl:py-6 tpl:text-center tpl:text-xs tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-empty\"\n >\n {{ t.logicTag.picker.noResults }}\n </div>\n <template v-else>\n <template v-for=\"(row, rowIndex) in built.rows\" :key=\"rowIndex\">\n <div\n v-if=\"row.kind === 'header'\"\n class=\"tpl:px-3 tpl:pt-2.5 tpl:pb-1 tpl:text-xs tpl:font-medium tpl:text-[var(--tpl-text-dim)]\"\n data-testid=\"logic-picker-header\"\n :data-group-name=\"row.label\"\n >\n {{ row.label }}\n </div>\n <button\n v-else\n :id=\"optionId(row.index)\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"row.index === highlightedIndex\"\n :data-selected=\"row.index === highlightedIndex ? 'true' : 'false'\"\n :data-logic-index=\"row.index\"\n :data-logic-kind=\"row.kind\"\n class=\"tpl:flex tpl:w-full tpl:cursor-pointer tpl:flex-col tpl:items-start tpl:gap-0.5 tpl:border-none tpl:px-3 tpl:py-1.5 tpl:text-left tpl:transition-colors\"\n :class=\"\n row.index === highlightedIndex\n ? 'tpl:bg-[var(--tpl-primary-light)] tpl:text-[var(--tpl-primary)]'\n : 'tpl:bg-transparent tpl:text-[var(--tpl-text)] tpl:hover:bg-[var(--tpl-bg-hover)]'\n \"\n data-testid=\"logic-picker-item\"\n @mousemove=\"highlightedIndex = row.index\"\n @click=\"selectItem(row.kind === 'tag' ? row.tag : row.pair)\"\n >\n <span\n class=\"tpl:flex tpl:w-full tpl:items-center tpl:justify-between tpl:gap-2\"\n >\n <span class=\"tpl:text-sm tpl:font-medium\">{{\n row.kind === \"tag\" ? row.tag.label : row.pair.label\n }}</span>\n <span\n v-if=\"row.kind === 'tag'\"\n class=\"tpl:flex tpl:items-center\"\n >\n <LogicTagBadge :value=\"row.tag.value\" :syntax=\"syntax\" />\n </span>\n <span v-else class=\"tpl:flex tpl:items-center tpl:gap-1\">\n <LogicTagBadge :value=\"row.pair.before\" :syntax=\"syntax\" />\n <span class=\"tpl:text-[var(--tpl-text-dim)]\">…</span>\n <LogicTagBadge :value=\"row.pair.after\" :syntax=\"syntax\" />\n </span>\n </span>\n <span\n class=\"tpl:line-clamp-2 tpl:font-mono tpl:text-xs tpl:text-ellipsis tpl:break-all tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.value\n : `${row.pair.before} … ${row.pair.after}`\n }}\n </span>\n <span\n v-if=\"\n (row.kind === 'tag'\n ? row.tag.description\n : row.pair.description) as string | undefined\n \"\n class=\"tpl:line-clamp-2 tpl:text-xs tpl:text-ellipsis tpl:text-[var(--tpl-text-dim)]\"\n >\n {{\n row.kind === \"tag\"\n ? row.tag.description\n : row.pair.description\n }}\n </span>\n </button>\n </template>\n </template>\n </div>\n </div>\n </TplModal>\n</template>\n","<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onMounted,\n onUnmounted,\n ref,\n watch,\n} from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\n// Type-only, so no cloud module is statically reachable from the OSS entry —\n// see `cloud/runtime.ts` for why the seam is shaped this way.\nimport type { CloudRuntime } from \"./cloud/runtime\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useCommentsFeature } from \"./composables/useCommentsFeature\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTemplatesFeature } from \"./composables/useTemplatesFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useVersionHistoryFeature } from \"./composables/useVersionHistoryFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveAutoSave } from \"./types/auto-save\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { logger } from \"./utils/logger\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport { resolveRenderFonts } from \"./utils/renderProvider\";\nimport {\n withNormalizedContentWrites,\n withNormalizedTemplateLoads,\n} from \"./utils/normalizeMergeTagMarkup\";\nimport type { Translations } from \"./i18n\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw } from \"@lucide/vue\";\nimport { warningBtnCompactClass } from \"./constants/styleConstants\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport EditorHeader from \"./components/EditorHeader.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n /**\n * Cloud's adapter wiring, supplied only by `initCloud()`.\n *\n * There is **one** editor component. Cloud is a set of providers on\n * `config` plus this runtime, which exists purely for the handful of\n * composables that must run at a specific point inside this `setup()` —\n * collaboration before the history interceptor, the lint save-gate after\n * `useEditorCore`. Everything else it contributes arrives through the\n * ordinary config keys an OSS consumer would fill in themselves.\n */\n cloud?: CloudRuntime;\n}>();\n\n// The fourth place consumer content enters (the other three are the entry\n// points in `index.ts`: the mount seed, `setContent` and `create`). A template\n// fetched from the store never passes through the public API — core assigns it\n// to state itself — so the provider is wrapped here, on its way in. Wrapping\n// rather than normalizing after the load resolves is what keeps the load from\n// registering as an edit: core is handed content that is already correct.\nconst templatesProvider = props.config.templates\n ? withNormalizedTemplateLoads(props.config.templates, props.config.mergeTags)\n : undefined;\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n // Seeds a *new* template's body font when no `content` was supplied. Only the\n // deleted Cloud core passed this, so `init({ fonts: { defaultFont } })` never\n // reached a blank template.\n defaultFontFamily: props.config.fonts?.defaultFont,\n templateDefaults: props.config.templateDefaults,\n templates: templatesProvider,\n onError: props.config.onError,\n // Cloud's collaborators lock the blocks they are editing. The map is\n // forward-declared by the runtime because `useCollaboration` — which fills\n // it — can only be built after this call.\n lockedBlocks: props.cloud?.lockedBlocks,\n});\n\n// Collaboration wraps the editor's mutators so they broadcast; `useEditorCore`'s\n// history interceptor wraps them again below. Reversing that order would push\n// local history entries for remote operations and drift the peers apart, which\n// is the whole reason this runs here rather than alongside the rest.\nconst cloudAttachment = props.cloud ? props.cloud.attach({ editor }) : null;\n\n// --- Templates (opt-in: only when a storage provider is configured) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header's name field, status indicator and save button — and what\n// makes Cmd+S mean \"persist now\".\nconst templates = templatesProvider\n ? useTemplatesFeature({\n provider: templatesProvider,\n editor,\n guardUnsavedChanges: props.config.templates?.unsavedChangesGuard,\n // Cloud's lint save-gate, when there is one. Read through a getter\n // because the gate needs `core.templateLint`, which does not exist yet.\n // Without this the collapse would have silently dropped the server's\n // `accessibility.blockOnError` policy.\n getSaveGate: props.cloud ? () => props.cloud!.getSaveGate() : undefined,\n })\n : null;\n\n// Reported to the consumer regardless of whether a provider is configured: a\n// `beforeunload` guard cannot cover SPA route changes, so an embedder needs this\n// to guard their own router — including when they persist via `onChange`.\nif (props.config.onDirtyChange) {\n watch(\n () => editor.state.isDirty,\n (isDirty) => props.config.onDirtyChange!(isDirty),\n );\n}\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n // Cloud's store is plan-gated; a consumer's own store never is.\n isAvailable: props.cloud\n ? () => props.cloud!.isSavedBlocksAvailable()\n : undefined,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n // Cloud folds in the plan feature and \"the template must be saved\", both\n // constraints of *its* sending path rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isTestEmailAvailable()\n : undefined,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Comments (opt-in: only with a storage provider *and* a `user`) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header trigger and the per-block comment indicators.\n//\n// No `user` means no provider call at all — an unattributable comment is worse\n// than no comment feature, so `isAvailable` stays false and nothing renders.\nconst comments = props.config.comments\n ? useCommentsFeature({\n provider: props.config.comments,\n editor,\n user: props.config.user,\n // Cloud keeps comments mutually exclusive with its AI and scoring sidebars:\n // they share one 360px gutter, and two of them open at once would overlap.\n isOpen: cloudAttachment?.panelState.commentsOpen,\n onError: props.config.onError,\n // Cloud folds in the `commenting` plan feature and \"the template must be\n // saved\", both constraints of *its* store rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isCommentsAvailable()\n : undefined,\n // Cloud anchors a comment to a block in the saved template; a consumer's\n // store accepts whatever anchor it is given.\n isBlockSaved: props.cloud\n ? (blockId: string) => props.cloud!.isBlockSaved(blockId)\n : undefined,\n })\n : null;\n\nconst CommentsPanel = defineAsyncComponent(\n () => import(\"./components/CommentsPanel.vue\"),\n);\n\n// --- Cloud chrome (opt-in: only under `initCloud()`) ---\n// One lazy wrapper for every panel, overlay and modal Cloud adds, plus one for\n// its header controls. Same discipline as `SavedBlocksPanels` / `TestEmailPanel`:\n// an OSS consumer never downloads any of it.\nconst CloudPanels = defineAsyncComponent(\n () => import(\"./cloud/components/CloudPanels.vue\"),\n);\nconst CloudHeaderExtras = defineAsyncComponent(\n () => import(\"./cloud/components/CloudHeaderExtras.vue\"),\n);\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// The header control lives in `EditorHeader`; this is the preview banner, which\n// renders outside the header. Both are lazy.\nconst VersionHistoryPanels = defineAsyncComponent(\n () => import(\"./components/VersionHistoryPanels.vue\"),\n);\n\n// --- Auto-save ---\n// One debounced tick drives everything a consumer can ask for on a content\n// change: the `onChange` notification, and, when `templates.autoSave` is on,\n// the save itself. Sharing a single `useAutoSave` instance keeps the two in\n// step and inherits the pause-during-undo/redo behaviour `useEditorCore` wires\n// up around it. `config.changeDebounce` sets the cadence for both.\nconst autoSaveEnabled = resolveAutoSave(\n props.config.templates?.autoSave,\n false,\n);\nconst isAutoSaving = autoSaveEnabled && templates !== null;\n// The one way autosave can still be inert once a provider exists is its\n// `save` being `false` — a read-only store — which `requestAutoSave()` below\n// already no-ops per tick for; this warning is the only signal the consumer\n// gets. `templates !== null` is redundant with `autoSaveEnabled` at runtime\n// (the provider that enables autosave is the same provider that makes\n// `templates` non-null) but stays as an explicit conjunct so TypeScript can\n// narrow `templates.canSave.value` below without a non-null assertion.\nif (autoSaveEnabled && templates !== null && !templates.canSave.value) {\n logger.warn(\n \"config.templates.autoSave is on but this provider's save is false — \" +\n \"there is nothing to persist to. Give it a real save function, or \" +\n \"use `onChange` to persist the content yourself.\",\n );\n}\n// `changeDebounce` alone does not enable autosave — only `templates.autoSave`\n// does. A config with `changeDebounce` set, a provider present, `autoSave`\n// left unset, and no `onChange` has nothing consuming the timer at all,\n// which reads as intentional `onChange`-only pacing but is easy to reach by\n// mistake. Warn on exactly that combination; `autoSave: false` is a stated\n// decision and must stay silent.\nif (\n props.config.changeDebounce !== undefined &&\n templates !== null &&\n props.config.templates?.autoSave === undefined &&\n !props.config.onChange\n) {\n logger.warn(\n \"config.changeDebounce is set but nothing will use it — \" +\n \"config.templates.autoSave is unset and there is no onChange either, \" +\n \"so the timer has no consumer. Set config.templates.autoSave, or add \" +\n \"onChange, so the debounce takes effect.\",\n );\n}\n\nconst autoSaveOptions =\n props.config.onChange || isAutoSaving\n ? {\n onChange: () => {\n props.config.onChange?.(\n JSON.parse(JSON.stringify(editor.state.content)),\n );\n // `requestAutoSave()` already no-ops without a saveable template, so a\n // read-only provider costs one predicate per tick and nothing else.\n // It is the *ungated* request only in the sense that it never raises a\n // prompt: Cloud's lint gate still refuses it, which is what keeps\n // `accessibility.blockOnError` a policy rather than a manual-save\n // speed bump.\n if (isAutoSaving) templates!.requestAutoSave();\n },\n ...(props.config.changeDebounce !== undefined\n ? { debounce: props.config.changeDebounce }\n : {}),\n }\n : null;\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\n// A named object rather than an inline literal: `useVersionHistoryFeature` can\n// only be built after core (it needs history/conditionPreview/autoSave), so its\n// capability is assigned in below, into the very object core provided.\nconst capabilities: EditorCapabilities = {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(templates ? { templates: templates.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n ...(comments ? { comments: comments.capability } : {}),\n};\n\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n // Cloud swaps in its own media browser. Not plan-gated: an entitlement here\n // would fire when a consumer is *not* using Cloud storage, i.e. backwards.\n onRequestMedia: props.cloud?.onRequestMedia ?? props.config.onRequestMedia,\n resolvePreview: props.config.resolvePreview,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions,\n capabilities,\n editorRoot: props.shadowRoot,\n historyOptions: props.cloud\n ? { isRemoteOperation: () => props.cloud!.isRemoteOperation() }\n : undefined,\n keyboardOptions: props.cloud\n ? { onBeforeUndo: () => props.cloud!.onBeforeUndo() }\n : undefined,\n});\n\n// Cloud's setup-time wiring that needs `core`: the lint save-gate (reads\n// `core.templateLint`), the collab undo warning (reads `core.history.canUndo`),\n// and the capability entries Cloud contributes — mutated into the very object\n// `useEditorCore` provided, so injecting components see them on first render.\nconst cloudReady =\n props.cloud && cloudAttachment\n ? props.cloud.ready({ core, capabilities })\n : null;\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// Constructed *after* `useEditorCore` because it drives `history` /\n// `conditionPreview` / `autoSave`, which core owns. Its capability is therefore\n// assigned into the object above rather than passed in — same object reference\n// core provided, mutated during setup, so injecting components see it on their\n// first render.\nconst versionHistory = props.config.versionHistory\n ? useVersionHistoryFeature({\n provider: props.config.versionHistory,\n // The fifth content-in path. A stored version holds whatever was written\n // to it, so it can carry bare tokens like any other loaded content, and\n // previewing one would put them on a canvas where every other tag is a\n // chip. Wrapped at `setContent` rather than at the provider, because\n // version content also arrives off the hydrated list and out of the\n // `fetched` cache without touching `get()`.\n editor: withNormalizedContentWrites(editor, props.config.mergeTags),\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n // Lets the restore confirmation offer to persist unsaved work instead of\n // only warning about it. Without a templates provider — or with one whose\n // `save` is `false` — there is nowhere to put that work, and the\n // confirmation says so rather than offering an action that can't happen.\n // Cloud additionally refuses while the lint gate would block: stacking a\n // second modal on the confirmation would be worse than saying there is\n // currently nowhere to put the work.\n saveBeforeRestore: templates\n ? {\n canSave: () =>\n templates.canSave.value &&\n !(props.cloud?.getSaveGate()?.shouldBlock.value ?? false),\n save: () => templates.save(\"restore\"),\n }\n : null,\n onError: props.config.onError,\n })\n : null;\n\nif (versionHistory) capabilities.versionHistory = versionHistory.capability;\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n getFonts: () => resolveRenderFonts(props.fontsManager),\n });\n}\n\n/**\n * Left/right insets for the canvas body and the footer, which must always agree.\n *\n * Cloud's sidebars widen the right gutter when one is open; without a cloud\n * runtime `rightPanelOpen` is simply never true.\n */\n/**\n * Whether a 360px feature panel occupies the right-hand gutter.\n *\n * The comments panel is the one an OSS session can open, so it counts on its own.\n * In Cloud it also drives `rightPanelOpen` (they share the panel state, which is\n * what keeps comments mutually exclusive with the AI and scoring sidebars), so the\n * `||` is redundant there and load-bearing here.\n *\n * Two things read this, and **both must**: the canvas/footer insets below, and\n * `RightSidebar`'s `shifted-left` — the properties panel sits at `right-0` too, so\n * a panel opening over it would swallow every click meant for the panel.\n */\nconst rightPanelOpen = computed(\n () =>\n comments?.isOpen.value === true ||\n cloudAttachment?.panelState.rightPanelOpen.value === true,\n);\n\nconst bodyInsetClass = computed(() => {\n if (editor.state.previewMode) return \"tpl:left-0 tpl:right-0\";\n return rightPanelOpen.value\n ? \"tpl:left-12 tpl:right-[680px]\"\n : \"tpl:left-12 tpl:right-[320px]\";\n});\n\n/** Canvas affordances that open a Cloud panel. No-ops without a runtime. */\nfunction openCloudPanel(panel: \"ai-chat\" | \"design-reference\"): void {\n if (!cloudAttachment) return;\n if (panel === \"ai-chat\") cloudAttachment.panelState.aiChatOpen.value = true;\n else cloudAttachment.panelState.designReferenceOpen.value = true;\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n props.cloud?.destroy();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\n// The lifecycle trio comes from the templates feature when one exists, so a\n// programmatic save also drives the header's status. Without a provider it comes\n// straight from core, whose own methods reject with the actionable error.\nconst templateLifecycle = templates ?? editor;\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n isDirty: () => editor.state.isDirty,\n create: templateLifecycle.create,\n load: templateLifecycle.load,\n save: templateLifecycle.save,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Chrome wrapper — the clip belongs here, never on the root and never\n around `.tpl-popover-root`. Safari paints a `position: fixed`\n descendant clipped to an ancestor's `overflow: hidden` box while still\n resolving its layout against the viewport, so a dialog teleported\n under a clipping ancestor is cut off wherever it extends past the\n container, and its backdrop dims only the container's own area\n (#633). Keeping the clip on this element bounds the header, rails,\n canvas and footer without reaching the popover root, and this box is\n identical to the root's, so everything anchored `absolute` inside\n lands exactly where it would against the root. Locked by\n `tests/popover-root-clip-scope.test.ts`. -->\n <div class=\"tpl:absolute tpl:inset-0 tpl:overflow-hidden\">\n <!-- One header for both entry points. Cloud's own controls arrive through\n the three slots; everything else here is capability-gated, so the same\n markup covers \"no providers at all\" and a fully-wired Cloud session. -->\n <EditorHeader\n :editor=\"editor\"\n :core=\"core\"\n :templates=\"templates\"\n :show-template-name=\"config.templates?.nameField !== false\"\n :test-email=\"testEmail\"\n :version-history=\"versionHistory\"\n :comments=\"comments\"\n >\n <template v-if=\"cloudAttachment\" #left-extras>\n <CloudHeaderExtras part=\"left\" :cloud=\"cloudAttachment\" />\n </template>\n <template v-if=\"cloudAttachment\" #right-extras>\n <CloudHeaderExtras part=\"right\" :cloud=\"cloudAttachment\" />\n </template>\n </EditorHeader>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n bodyInsetClass,\n versionHistory?.isPreviewing.value ? 'tpl:top-[104px]' : 'tpl:top-14',\n ]\"\n >\n <!-- Preview chrome, floated over the canvas.\n\n A zero-height sticky layer holding one absolutely-positioned,\n canvas-centred **column**. Zero height means the layer contributes no\n space of its own, so nothing here can push the canvas around — which\n is what lets the merge-tag toggle live over the preview instead of in\n the header, whose centre track re-centres on every width change and\n so dragged the Preview button 114.5px sideways each time the toggle\n appeared (#574).\n\n A column rather than two fixed offsets. Both pills are reachable at\n once — the restore pill needs hidden blocks, the toggle needs preview\n mode without a `resolvePreview`, and nothing makes those exclusive —\n so they have to stack. Doing that with a hardcoded second offset put\n the restore pill 48px down even when it rendered alone, which in\n editing mode (where the toggle never shows) dropped it onto the first\n block's content. Flow solves what the offset got wrong: whichever\n pills render sit tight under the header, in order.\n\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div class=\"tpl-preview-overlay tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <div\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:flex tpl:-translate-x-1/2 tpl:flex-col tpl:items-center tpl:gap-2\"\n >\n <!-- Sample / Label. Preview mode only: merge tags are never\n substituted on the editing canvas, so the choice would have no\n effect there. A configured `resolvePreview` supersedes samples\n entirely.\n\n The wrapper carries only the shadow that lifts the control off\n the canvas — no `backdrop-filter`, because the toggle's own\n `--tpl-bg-hover` fill is opaque and covers this box exactly, so a\n blur would composite a layer to show nothing. -->\n <Transition name=\"tpl-preview-pill\">\n <div\n v-if=\"\n editor.state.previewMode &&\n !core.previewResolution.supersedesSamples.value\n \"\n data-testid=\"merge-tag-mode-toggle-anchor\"\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:shadow-[var(--tpl-shadow-md)]\"\n >\n <MergeTagModeToggle\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n </Transition>\n <!-- Show all hidden blocks. The shared warning recipe, not a bespoke\n string: it has to read as the Sample/Label switch's sibling,\n since the two stack here, and a hand-rolled pill is how this one\n came to sit 8px shorter with a different radius and a 1.85:1\n label. `warningBtnCompactClass` carries why the amber is on the\n border rather than the text. -->\n <Transition name=\"tpl-preview-pill\">\n <button\n v-if=\"\n core.conditionPreview.hasHiddenBlocks.value &&\n core.appliesConditionFilter.value\n \"\n type=\"button\"\n :class=\"warningBtnCompactClass\"\n class=\"tpl:shadow-[var(--tpl-shadow-md)]\"\n data-testid=\"restore-hidden-blocks\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"core.previewResolution.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"cloudAttachment?.collaboration?.lockedBlocks.value\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"openCloudPanel('ai-chat')\"\n @open-design-reference=\"openCloudPanel('design-reference')\"\n />\n </main>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[bodyInsetClass]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"rightPanelOpen\"\n @update-block=\"\n (updates) =>\n editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n </div>\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modals.\n Every popup mounts here rather than at the page's `<body>`, so it\n renders inside the editor's effective DOM root and keeps the shadow\n root's adopted stylesheet; a body-level teleport leaves the shadow\n tree and renders unstyled. It also has to stay a child of `.tpl`, which\n declares `--tpl-base-size` and the colour tokens — `@theme inline`\n rebases the whole Tailwind length scale onto that variable, so a\n popup hoisted out would lose the sizing scale as well as the palette.\n Enforced by the `no-teleport-to-body` ESLint rule and\n `tests/global-refs-audit.test.ts`. -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- The comments sidebar. Only mounted when a provider and a `user` are\n configured; the sidebar's own chunk loads the first time it opens. -->\n <CommentsPanel v-if=\"comments?.isAvailable.value\" :feature=\"comments\" />\n\n <!-- Version-history chrome outside the header (the preview banner). Only\n mounted when a provider is configured; the banner's chunk loads the\n first time a version is previewed. -->\n <VersionHistoryPanels\n v-if=\"versionHistory?.isAvailable.value\"\n :feature=\"versionHistory\"\n />\n\n <!-- Cloud sidebars, modals and overlays. One lazy wrapper, mounted only\n under `initCloud()`; an OSS consumer downloads none of it. -->\n <CloudPanels\n v-if=\"cloud && cloudAttachment && cloudReady\"\n :editor=\"editor\"\n :core=\"core\"\n :runtime=\"cloud\"\n :cloud=\"cloudAttachment\"\n :ready=\"cloudReady\"\n :locale=\"config.locale\"\n />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-preview-pill-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-preview-pill-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-preview-pill-enter-from,\n.tpl-preview-pill-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-preview-pill-enter-to,\n.tpl-preview-pill-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onMounted,\n onUnmounted,\n ref,\n watch,\n} from \"vue\";\nimport type { TemplaticalEditorConfig } from \"./index\";\nimport { useEditor } from \"@templatical/core\";\nimport type { TemplateContent, UiTheme } from \"@templatical/types\";\n// Type-only, so no cloud module is statically reachable from the OSS entry —\n// see `cloud/runtime.ts` for why the seam is shaped this way.\nimport type { CloudRuntime } from \"./cloud/runtime\";\nimport { useEditorCore } from \"./composables/useEditorCore\";\nimport { useCommentsFeature } from \"./composables/useCommentsFeature\";\nimport { useSavedBlocksFeature } from \"./composables/useSavedBlocksFeature\";\nimport { useTemplatesFeature } from \"./composables/useTemplatesFeature\";\nimport { useTestEmailFeature } from \"./composables/useTestEmailFeature\";\nimport { useVersionHistoryFeature } from \"./composables/useVersionHistoryFeature\";\nimport { useSmallScreenNotice } from \"./composables/useSmallScreenNotice\";\nimport { resolveAutoSave } from \"./types/auto-save\";\nimport { resolveLintOptions } from \"./utils/resolveLintOptions\";\nimport { logger } from \"./utils/logger\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport { resolveRenderFonts } from \"./utils/renderProvider\";\nimport {\n withNormalizedContentWrites,\n withNormalizedTemplateLoads,\n} from \"./utils/normalizeMergeTagMarkup\";\nimport type { Translations } from \"./i18n\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\n\nimport { RotateCcw } from \"@lucide/vue\";\nimport { warningBtnCompactClass } from \"./constants/styleConstants\";\nimport Canvas from \"./components/Canvas.vue\";\nimport CustomBlockStylesheets from \"./components/CustomBlockStylesheets.vue\";\nimport Sidebar from \"./components/Sidebar.vue\";\nimport RightSidebar from \"./components/RightSidebar.vue\";\nimport SmallScreenNotice from \"./components/SmallScreenNotice.vue\";\nimport EditorFooter from \"./components/EditorFooter.vue\";\nimport EditorHeader from \"./components/EditorHeader.vue\";\nimport MergeTagModeToggle from \"./components/MergeTagModeToggle.vue\";\nimport MergeTagPickerModal from \"./components/MergeTagPickerModal.vue\";\nimport LogicTagPickerModal from \"./components/LogicTagPickerModal.vue\";\nimport \"./styles/index.css\";\n\nconst props = defineProps<{\n config: TemplaticalEditorConfig;\n translations: Translations;\n fontsManager: UseFontsReturn;\n /**\n * Shadow root the editor is mounted into. Supplied by `init()` when\n * `shadowDom: true`; undefined in light-DOM mode. Passed through to\n * `useEditorCore` so shadow-DOM-aware composables can resolve the\n * effective root via `EDITOR_ROOT_KEY`.\n */\n shadowRoot?: ShadowRoot;\n /**\n * Cloud's adapter wiring, supplied only by `initCloud()`.\n *\n * There is **one** editor component. Cloud is a set of providers on\n * `config` plus this runtime, which exists purely for the handful of\n * composables that must run at a specific point inside this `setup()` —\n * collaboration before the history interceptor, the lint save-gate after\n * `useEditorCore`. Everything else it contributes arrives through the\n * ordinary config keys an OSS consumer would fill in themselves.\n */\n cloud?: CloudRuntime;\n}>();\n\n// The fourth place consumer content enters (the other three are the entry\n// points in `index.ts`: the mount seed, `setContent` and `create`). A template\n// fetched from the store never passes through the public API — core assigns it\n// to state itself — so the provider is wrapped here, on its way in. Wrapping\n// rather than normalizing after the load resolves is what keeps the load from\n// registering as an edit: core is handed content that is already correct.\nconst templatesProvider = props.config.templates\n ? withNormalizedTemplateLoads(props.config.templates, props.config.mergeTags)\n : undefined;\n\n// --- Core editor state ---\nconst editor = useEditor({\n content: props.config.content!,\n // Seeds a *new* template's body font when no `content` was supplied. Only the\n // deleted Cloud core passed this, so `init({ fonts: { defaultFont } })` never\n // reached a blank template.\n defaultFontFamily: props.config.fonts?.defaultFont,\n templateDefaults: props.config.templateDefaults,\n templates: templatesProvider,\n onError: props.config.onError,\n // Cloud's collaborators lock the blocks they are editing. The map is\n // forward-declared by the runtime because `useCollaboration` — which fills\n // it — can only be built after this call.\n lockedBlocks: props.cloud?.lockedBlocks,\n});\n\n// Collaboration wraps the editor's mutators so they broadcast; `useEditorCore`'s\n// history interceptor wraps them again below. Reversing that order would push\n// local history entries for remote operations and drift the peers apart, which\n// is the whole reason this runs here rather than alongside the rest.\nconst cloudAttachment = props.cloud ? props.cloud.attach({ editor }) : null;\n\n// --- Templates (opt-in: only when a storage provider is configured) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header's name field, status indicator and save button — and what\n// makes Cmd+S mean \"persist now\".\nconst templates = templatesProvider\n ? useTemplatesFeature({\n provider: templatesProvider,\n editor,\n guardUnsavedChanges: props.config.templates?.unsavedChangesGuard,\n // Cloud's lint save-gate, when there is one. Read through a getter\n // because the gate needs `core.templateLint`, which does not exist yet.\n // Without this the collapse would have silently dropped the server's\n // `accessibility.blockOnError` policy.\n getSaveGate: props.cloud ? () => props.cloud!.getSaveGate() : undefined,\n })\n : null;\n\n// Reported to the consumer regardless of whether a provider is configured: a\n// `beforeunload` guard cannot cover SPA route changes, so an embedder needs this\n// to guard their own router — including when they persist via `onChange`.\nif (props.config.onDirtyChange) {\n watch(\n () => editor.state.isDirty,\n (isDirty) => props.config.onDirtyChange!(isDirty),\n );\n}\n\n// Outer `.tpl` ref — passed to `useEditorCore` so the active-editor\n// tracker can route keyboard shortcuts when two editors share a page.\nconst rootEl = ref<HTMLElement | null>(null);\n\n// --- Saved blocks (opt-in: only when a storage provider is configured) ---\n// Instantiated before `useEditorCore` so its capability can be passed in,\n// which is what lights up the shared save button and sidebar rail.\nconst savedBlocks = props.config.savedBlocks\n ? useSavedBlocksFeature({\n provider: props.config.savedBlocks,\n editor,\n onError: props.config.onError,\n // Cloud's store is plan-gated; a consumer's own store never is.\n isAvailable: props.cloud\n ? () => props.cloud!.isSavedBlocksAvailable()\n : undefined,\n })\n : null;\n\n// Lazily loaded so a consumer without a provider downloads none of the\n// saved-blocks UI. Rendered only when the feature is active.\nconst SavedBlocksPanels = defineAsyncComponent(\n () => import(\"./components/SavedBlocksPanels.vue\"),\n);\n\n// --- Test email (opt-in: only when a sending provider is configured) ---\n// Created before `useEditorCore` so its capability can be passed in, same as\n// saved blocks. `renderCurrentMjml` is a hoisted function declaration, so it can\n// be referenced here and still read `core` — which is declared below — because\n// it isn't called until the user sends.\nconst testEmail = props.config.testEmail\n ? useTestEmailFeature({\n provider: props.config.testEmail,\n getContent: () => editor.content.value,\n renderMjml: renderCurrentMjml,\n onError: props.config.onError,\n // Cloud folds in the plan feature and \"the template must be saved\", both\n // constraints of *its* sending path rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isTestEmailAvailable()\n : undefined,\n })\n : null;\n\nconst TestEmailPanel = defineAsyncComponent(\n () => import(\"./components/TestEmailPanel.vue\"),\n);\n\n// --- Comments (opt-in: only with a storage provider *and* a `user`) ---\n// Built before `useEditorCore` so its capability can be passed in, which is what\n// lights up the header trigger and the per-block comment indicators.\n//\n// No `user` means no provider call at all — an unattributable comment is worse\n// than no comment feature, so `isAvailable` stays false and nothing renders.\nconst comments = props.config.comments\n ? useCommentsFeature({\n provider: props.config.comments,\n editor,\n user: props.config.user,\n // Cloud keeps comments mutually exclusive with its AI and scoring sidebars:\n // they share one 360px gutter, and two of them open at once would overlap.\n isOpen: cloudAttachment?.panelState.commentsOpen,\n onError: props.config.onError,\n // Cloud folds in the `commenting` plan feature and \"the template must be\n // saved\", both constraints of *its* store rather than of the contract.\n isAvailable: props.cloud\n ? () => props.cloud!.isCommentsAvailable()\n : undefined,\n // Cloud anchors a comment to a block in the saved template; a consumer's\n // store accepts whatever anchor it is given.\n isBlockSaved: props.cloud\n ? (blockId: string) => props.cloud!.isBlockSaved(blockId)\n : undefined,\n })\n : null;\n\nconst CommentsPanel = defineAsyncComponent(\n () => import(\"./components/CommentsPanel.vue\"),\n);\n\n// --- Cloud chrome (opt-in: only under `initCloud()`) ---\n// One lazy wrapper for every panel, overlay and modal Cloud adds, plus one for\n// its header controls. Same discipline as `SavedBlocksPanels` / `TestEmailPanel`:\n// an OSS consumer never downloads any of it.\nconst CloudPanels = defineAsyncComponent(\n () => import(\"./cloud/components/CloudPanels.vue\"),\n);\nconst CloudHeaderExtras = defineAsyncComponent(\n () => import(\"./cloud/components/CloudHeaderExtras.vue\"),\n);\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// The header control lives in `EditorHeader`; this is the preview banner, which\n// renders outside the header. Both are lazy.\nconst VersionHistoryPanels = defineAsyncComponent(\n () => import(\"./components/VersionHistoryPanels.vue\"),\n);\n\n// --- Auto-save ---\n// One debounced tick drives everything a consumer can ask for on a content\n// change: the `onChange` notification, and, when `templates.autoSave` is on,\n// the save itself. Sharing a single `useAutoSave` instance keeps the two in\n// step and inherits the pause-during-undo/redo behaviour `useEditorCore` wires\n// up around it. `config.changeDebounce` sets the cadence for both.\nconst autoSaveEnabled = resolveAutoSave(\n props.config.templates?.autoSave,\n false,\n);\nconst isAutoSaving = autoSaveEnabled && templates !== null;\n// The one way autosave can still be inert once a provider exists is its\n// `save` being `false` — a read-only store — which `requestAutoSave()` below\n// already no-ops per tick for; this warning is the only signal the consumer\n// gets. `templates !== null` is redundant with `autoSaveEnabled` at runtime\n// (the provider that enables autosave is the same provider that makes\n// `templates` non-null) but stays as an explicit conjunct so TypeScript can\n// narrow `templates.canSave.value` below without a non-null assertion.\nif (autoSaveEnabled && templates !== null && !templates.canSave.value) {\n logger.warn(\n \"config.templates.autoSave is on but this provider's save is false — \" +\n \"there is nothing to persist to. Give it a real save function, or \" +\n \"use `onChange` to persist the content yourself.\",\n );\n}\n// `changeDebounce` alone does not enable autosave — only `templates.autoSave`\n// does. A config with `changeDebounce` set, a provider present, `autoSave`\n// left unset, and no `onChange` has nothing consuming the timer at all,\n// which reads as intentional `onChange`-only pacing but is easy to reach by\n// mistake. Warn on exactly that combination; `autoSave: false` is a stated\n// decision and must stay silent.\nif (\n props.config.changeDebounce !== undefined &&\n templates !== null &&\n props.config.templates?.autoSave === undefined &&\n !props.config.onChange\n) {\n logger.warn(\n \"config.changeDebounce is set but nothing will use it — \" +\n \"config.templates.autoSave is unset and there is no onChange either, \" +\n \"so the timer has no consumer. Set config.templates.autoSave, or add \" +\n \"onChange, so the debounce takes effect.\",\n );\n}\n\nconst autoSaveOptions =\n props.config.onChange || isAutoSaving\n ? {\n onChange: () => {\n props.config.onChange?.(\n JSON.parse(JSON.stringify(editor.state.content)),\n );\n // `requestAutoSave()` already no-ops without a saveable template, so a\n // read-only provider costs one predicate per tick and nothing else.\n // It is the *ungated* request only in the sense that it never raises a\n // prompt: Cloud's lint gate still refuses it, which is what keeps\n // `accessibility.blockOnError` a policy rather than a manual-save\n // speed bump.\n if (isAutoSaving) templates!.requestAutoSave();\n },\n ...(props.config.changeDebounce !== undefined\n ? { debounce: props.config.changeDebounce }\n : {}),\n }\n : null;\n\n// --- Shared editor core (composables, provides, plugins, keyboard) ---\n// A named object rather than an inline literal: `useVersionHistoryFeature` can\n// only be built after core (it needs history/conditionPreview/autoSave), so its\n// capability is assigned in below, into the very object core provided.\nconst capabilities: EditorCapabilities = {\n ...(savedBlocks ? { savedBlocks: savedBlocks.capability } : {}),\n ...(templates ? { templates: templates.capability } : {}),\n ...(testEmail ? { testEmail: testEmail.capability } : {}),\n ...(comments ? { comments: comments.capability } : {}),\n};\n\nconst core = useEditorCore({\n editor,\n containerEl: rootEl,\n config: {\n uiTheme: props.config.uiTheme,\n theme: props.config.theme,\n blockDefaults: props.config.blockDefaults,\n templateDefaults: props.config.templateDefaults,\n customBlocks: props.config.customBlocks,\n paletteBlocks: props.config.paletteBlocks,\n htmlBlockPreview: props.config.htmlBlockPreview,\n colors: props.config.colors,\n mergeTags: props.config.mergeTags,\n logicTags: props.config.logicTags,\n displayConditions: props.config.displayConditions,\n // Cloud swaps in its own media browser. Not plan-gated: an entitlement here\n // would fire when a consumer is *not* using Cloud storage, i.e. backwards.\n onRequestMedia: props.cloud?.onRequestMedia ?? props.config.onRequestMedia,\n resolvePreview: props.config.resolvePreview,\n resolveImageUrl: props.config.resolveImageUrl,\n lint: resolveLintOptions(props.config),\n },\n translations: props.translations,\n fontsManager: props.fontsManager,\n autoSaveOptions,\n capabilities,\n editorRoot: props.shadowRoot,\n historyOptions: props.cloud\n ? { isRemoteOperation: () => props.cloud!.isRemoteOperation() }\n : undefined,\n keyboardOptions: props.cloud\n ? { onBeforeUndo: () => props.cloud!.onBeforeUndo() }\n : undefined,\n});\n\n// Cloud's setup-time wiring that needs `core`: the lint save-gate (reads\n// `core.templateLint`), the collab undo warning (reads `core.history.canUndo`),\n// and the capability entries Cloud contributes — mutated into the very object\n// `useEditorCore` provided, so injecting components see them on first render.\nconst cloudReady =\n props.cloud && cloudAttachment\n ? props.cloud.ready({ core, capabilities })\n : null;\n\n// --- Version history (opt-in: only when a storage provider is configured) ---\n// Constructed *after* `useEditorCore` because it drives `history` /\n// `conditionPreview` / `autoSave`, which core owns. Its capability is therefore\n// assigned into the object above rather than passed in — same object reference\n// core provided, mutated during setup, so injecting components see it on their\n// first render.\nconst versionHistory = props.config.versionHistory\n ? useVersionHistoryFeature({\n provider: props.config.versionHistory,\n // The fifth content-in path. A stored version holds whatever was written\n // to it, so it can carry bare tokens like any other loaded content, and\n // previewing one would put them on a canvas where every other tag is a\n // chip. Wrapped at `setContent` rather than at the provider, because\n // version content also arrives off the hydrated list and out of the\n // `fetched` cache without touching `get()`.\n editor: withNormalizedContentWrites(editor, props.config.mergeTags),\n history: core.history,\n conditionPreview: core.conditionPreview,\n autoSave: core.autoSave,\n // Lets the restore confirmation offer to persist unsaved work instead of\n // only warning about it. Without a templates provider — or with one whose\n // `save` is `false` — there is nowhere to put that work, and the\n // confirmation says so rather than offering an action that can't happen.\n // Cloud additionally refuses while the lint gate would block: stacking a\n // second modal on the confirmation would be worse than saying there is\n // currently nowhere to put the work.\n saveBeforeRestore: templates\n ? {\n canSave: () =>\n templates.canSave.value &&\n !(props.cloud?.getSaveGate()?.shouldBlock.value ?? false),\n save: () => templates.save(\"restore\"),\n }\n : null,\n onError: props.config.onError,\n })\n : null;\n\nif (versionHistory) capabilities.versionHistory = versionHistory.capability;\n\n/**\n * Render the current template to MJML for `testEmail`'s `includeMjml` option.\n *\n * Assembles the same three members `defineExpose` hands to the public instance,\n * so a test email carries byte-identical MJML to `editor.toMjml()`. Only reached\n * when a provider opted in; the dynamic renderer import inside\n * `toMjmlForInstance` means an OSS consumer who didn't opt in never loads it.\n */\nfunction renderCurrentMjml(): Promise<string> {\n return toMjmlForInstance({\n getContent: () => editor.content.value,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n getFonts: () => resolveRenderFonts(props.fontsManager),\n });\n}\n\n/**\n * Left/right insets for the canvas body and the footer, which must always agree.\n *\n * Cloud's sidebars widen the right gutter when one is open; without a cloud\n * runtime `rightPanelOpen` is simply never true.\n */\n/**\n * Whether a 360px feature panel occupies the right-hand gutter.\n *\n * The comments panel is the one an OSS session can open, so it counts on its own.\n * In Cloud it also drives `rightPanelOpen` (they share the panel state, which is\n * what keeps comments mutually exclusive with the AI and scoring sidebars), so the\n * `||` is redundant there and load-bearing here.\n *\n * Two things read this, and **both must**: the canvas/footer insets below, and\n * `RightSidebar`'s `shifted-left` — the properties panel sits at `right-0` too, so\n * a panel opening over it would swallow every click meant for the panel.\n */\nconst rightPanelOpen = computed(\n () =>\n comments?.isOpen.value === true ||\n cloudAttachment?.panelState.rightPanelOpen.value === true,\n);\n\nconst bodyInsetClass = computed(() => {\n if (editor.state.previewMode) return \"tpl:left-0 tpl:right-0\";\n return rightPanelOpen.value\n ? \"tpl:left-12 tpl:right-[680px]\"\n : \"tpl:left-12 tpl:right-[320px]\";\n});\n\n/** Canvas affordances that open a Cloud panel. No-ops without a runtime. */\nfunction openCloudPanel(panel: \"ai-chat\" | \"design-reference\"): void {\n if (!cloudAttachment) return;\n if (panel === \"ai-chat\") cloudAttachment.panelState.aiChatOpen.value = true;\n else cloudAttachment.panelState.designReferenceOpen.value = true;\n}\n\n// --- Small-screen gate (#235) ---\n// Below ~768px the three-pane chrome can't lay out usably; show a notice\n// instead. Opt out with `config.smallScreenNotice: false`.\nconst { showNotice: showSmallScreenNotice } = useSmallScreenNotice(\n () => props.config.smallScreenNotice,\n);\n\n// --- Lifecycle ---\nonMounted(async () => {\n await props.fontsManager.loadCustomFonts();\n});\n\nonUnmounted(() => {\n props.fontsManager.cleanupFontLinks();\n props.cloud?.destroy();\n core.destroy();\n});\n\n// --- Public API (accessed via template ref from init()) ---\n// The lifecycle trio comes from the templates feature when one exists, so a\n// programmatic save also drives the header's status. Without a provider it comes\n// straight from core, whose own methods reject with the actionable error.\nconst templateLifecycle = templates ?? editor;\n\ndefineExpose({\n getContent: () => editor.content.value,\n setContent: (content: TemplateContent) => editor.setContent(content),\n setTheme: (theme: UiTheme) => editor.setUiTheme(theme),\n isDirty: () => editor.state.isDirty,\n create: templateLifecycle.create,\n load: templateLifecycle.load,\n save: templateLifecycle.save,\n renderCustomBlock: core.registry.renderCustomBlock,\n getCustomBlockStylesheet: (customType: string) =>\n core.registry.getDefinition(customType)?.stylesheet,\n});\n</script>\n\n<template>\n <!-- @dragover/@drop.prevent: a file dropped outside an image drop zone would\n otherwise make the browser navigate to the file:// URL and destroy the\n editor session. Image zones (useImageDrop) handle their own drops; this\n only neutralizes the default action everywhere else (#229). -->\n <div\n ref=\"rootEl\"\n class=\"tpl tpl:relative tpl:h-full\"\n :class=\"{ 'tpl:dark': editor.state.darkMode }\"\n :data-tpl-theme=\"core.resolvedTheme.value\"\n :style=\"core.themeStyles.value\"\n @dragover.prevent\n @drop.prevent\n >\n <!-- Reactive `<style>` tags for custom-block definition stylesheets in\n use. Sits at the top so its rules apply to the canvas below. -->\n <CustomBlockStylesheets />\n <!-- Chrome wrapper — the clip belongs here, never on the root and never\n around `.tpl-popover-root`. Safari paints a `position: fixed`\n descendant clipped to an ancestor's `overflow: hidden` box while still\n resolving its layout against the viewport, so a dialog teleported\n under a clipping ancestor is cut off wherever it extends past the\n container, and its backdrop dims only the container's own area\n (#633). Keeping the clip on this element bounds the header, rails,\n canvas and footer without reaching the popover root, and this box is\n identical to the root's, so everything anchored `absolute` inside\n lands exactly where it would against the root. Locked by\n `tests/popover-root-clip-scope.test.ts`. -->\n <div class=\"tpl:absolute tpl:inset-0 tpl:overflow-hidden\">\n <!-- One header for both entry points. Cloud's own controls arrive through\n the three slots; everything else here is capability-gated, so the same\n markup covers \"no providers at all\" and a fully-wired Cloud session. -->\n <EditorHeader\n :editor=\"editor\"\n :core=\"core\"\n :templates=\"templates\"\n :show-template-name=\"config.templates?.nameField !== false\"\n :test-email=\"testEmail\"\n :version-history=\"versionHistory\"\n :comments=\"comments\"\n >\n <template v-if=\"cloudAttachment\" #left-extras>\n <CloudHeaderExtras part=\"left\" :cloud=\"cloudAttachment\" />\n </template>\n <template v-if=\"cloudAttachment\" #right-extras>\n <CloudHeaderExtras part=\"right\" :cloud=\"cloudAttachment\" />\n </template>\n </EditorHeader>\n\n <!-- Left sidebar — absolute, below header -->\n <Sidebar v-show=\"!editor.state.previewMode\" />\n\n <!-- Canvas area — absolute, fills remaining space -->\n <div\n class=\"tpl-body tpl:absolute tpl:bottom-0 tpl:overflow-auto tpl:bg-[var(--tpl-canvas-bg)]\"\n style=\"transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1)\"\n :class=\"[\n bodyInsetClass,\n versionHistory?.isPreviewing.value ? 'tpl:top-[104px]' : 'tpl:top-14',\n ]\"\n >\n <!-- Preview chrome, floated over the canvas.\n\n A zero-height sticky layer holding one absolutely-positioned,\n canvas-centred **column**. Zero height means the layer contributes no\n space of its own, so nothing here can push the canvas around — which\n is what lets the merge-tag toggle live over the preview instead of in\n the header, whose centre track re-centres on every width change and\n so dragged the Preview button 114.5px sideways each time the toggle\n appeared (#574).\n\n A column rather than two fixed offsets. Both pills are reachable at\n once — the restore pill needs hidden blocks, the toggle needs preview\n mode without a `resolvePreview`, and nothing makes those exclusive —\n so they have to stack. Doing that with a hardcoded second offset put\n the restore pill 48px down even when it rendered alone, which in\n editing mode (where the toggle never shows) dropped it onto the first\n block's content. Flow solves what the offset got wrong: whichever\n pills render sit tight under the header, in order.\n\n Locked by `tests/headerCenterStability.test.ts`. -->\n <div class=\"tpl-preview-overlay tpl:sticky tpl:top-0 tpl:z-40 tpl:h-0\">\n <div\n class=\"tpl:absolute tpl:left-1/2 tpl:top-2 tpl:flex tpl:-translate-x-1/2 tpl:flex-col tpl:items-center tpl:gap-2\"\n >\n <!-- Sample / Label. Preview mode only: merge tags are never\n substituted on the editing canvas, so the choice would have no\n effect there. A configured `resolvePreview` supersedes samples\n entirely.\n\n The wrapper carries only the shadow that lifts the control off\n the canvas — no `backdrop-filter`, because the toggle's own\n `--tpl-bg-hover` fill is opaque and covers this box exactly, so a\n blur would composite a layer to show nothing. -->\n <Transition name=\"tpl-preview-pill\">\n <div\n v-if=\"\n editor.state.previewMode &&\n !core.previewResolution.supersedesSamples.value\n \"\n data-testid=\"merge-tag-mode-toggle-anchor\"\n class=\"tpl:rounded-[var(--tpl-radius-sm)] tpl:shadow-[var(--tpl-shadow-md)]\"\n >\n <MergeTagModeToggle\n :sample-mode=\"core.mergeTagSampleMode.value\"\n @change=\"core.mergeTagSampleMode.value = $event\"\n />\n </div>\n </Transition>\n <!-- Show all hidden blocks. The shared warning recipe, not a bespoke\n string: it has to read as the Sample/Label switch's sibling,\n since the two stack here, and a hand-rolled pill is how this one\n came to sit 8px shorter with a different radius and a 1.85:1\n label. `warningBtnCompactClass` carries why the amber is on the\n border rather than the text. -->\n <Transition name=\"tpl-preview-pill\">\n <button\n v-if=\"\n core.conditionPreview.hasHiddenBlocks.value &&\n core.appliesConditionFilter.value\n \"\n type=\"button\"\n :class=\"warningBtnCompactClass\"\n class=\"tpl:shadow-[var(--tpl-shadow-md)]\"\n data-testid=\"restore-hidden-blocks\"\n @click=\"core.conditionPreview.reset()\"\n >\n <RotateCcw :size=\"16\" :stroke-width=\"2\" />\n {{ core.t.blockSettings.restoreHiddenBlocks }}\n </button>\n </Transition>\n </div>\n </div>\n <main class=\"tpl-main tpl:flex tpl:justify-center tpl:p-8\">\n <Canvas\n :viewport=\"editor.state.viewport\"\n :content=\"core.previewResolution.content.value\"\n :selected-block-id=\"editor.state.selectedBlockId\"\n :dark-mode=\"editor.state.darkMode\"\n :preview-mode=\"editor.state.previewMode\"\n :locked-blocks=\"cloudAttachment?.collaboration?.lockedBlocks.value\"\n @select-block=\"editor.selectBlock\"\n @open-ai-chat=\"openCloudPanel('ai-chat')\"\n @open-design-reference=\"openCloudPanel('design-reference')\"\n />\n </main>\n </div>\n\n <EditorFooter\n v-if=\"config.branding !== false\"\n :position-class=\"[bodyInsetClass]\"\n />\n\n <!-- Keyboard reorder announcement region (visually hidden, screen-reader live) -->\n <div\n class=\"tpl-sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n :aria-label=\"core.t.landmarks.reorderAnnouncements\"\n >\n {{ core.keyboardReorder.announcement.value }}\n </div>\n\n <!-- Right sidebar — persisted with v-show -->\n <RightSidebar\n v-show=\"!editor.state.previewMode\"\n :selected-block=\"editor.selectedBlock.value\"\n :settings=\"editor.content.value.settings\"\n :shifted-left=\"rightPanelOpen\"\n @update-block=\"\n (updates) =>\n editor.updateBlock(editor.state.selectedBlockId!, updates)\n \"\n @delete-block=\"\n () => {\n if (editor.state.selectedBlockId) {\n core.blockActions.deleteBlock(editor.state.selectedBlockId);\n }\n }\n \"\n @duplicate-block=\"\n () => {\n if (editor.selectedBlock.value) {\n core.blockActions.duplicateBlock(editor.selectedBlock.value);\n }\n }\n \"\n @update-settings=\"(updates) => editor.updateSettings(updates)\"\n />\n </div>\n\n <!-- Popover mount — Teleport target for toolbars, link dialog, modals.\n Every popup mounts here rather than at the page's `<body>`, so it\n renders inside the editor's effective DOM root and keeps the shadow\n root's adopted stylesheet; a body-level teleport leaves the shadow\n tree and renders unstyled. It also has to stay a child of `.tpl`, which\n declares `--tpl-base-size` and the colour tokens — `@theme inline`\n rebases the whole Tailwind length scale onto that variable, so a\n popup hoisted out would lose the sizing scale as well as the palette.\n Enforced by the `no-teleport-to-body` ESLint rule and\n `tests/global-refs-audit.test.ts`. -->\n <div\n :ref=\"(el) => (core.popoverRoot.value = el as HTMLElement | null)\"\n class=\"tpl-popover-root\"\n />\n\n <!-- Built-in merge tag picker modal. Reads picker state via injection;\n renders nothing until `picker.isOpen` flips true. -->\n <MergeTagPickerModal />\n\n <!-- Built-in logic picker modal (standalone logic feature). -->\n <LogicTagPickerModal />\n\n <!-- Saved blocks dialogs. Only mounted when a provider is configured;\n each dialog's chunk loads on first open. -->\n <SavedBlocksPanels\n v-if=\"savedBlocks?.isAvailable.value\"\n :feature=\"savedBlocks\"\n />\n\n <TestEmailPanel v-if=\"testEmail?.isAvailable.value\" :feature=\"testEmail\" />\n\n <!-- The comments sidebar. Only mounted when a provider and a `user` are\n configured; the sidebar's own chunk loads the first time it opens. -->\n <CommentsPanel v-if=\"comments?.isAvailable.value\" :feature=\"comments\" />\n\n <!-- Version-history chrome outside the header (the preview banner). Only\n mounted when a provider is configured; the banner's chunk loads the\n first time a version is previewed. -->\n <VersionHistoryPanels\n v-if=\"versionHistory?.isAvailable.value\"\n :feature=\"versionHistory\"\n />\n\n <!-- Cloud sidebars, modals and overlays. One lazy wrapper, mounted only\n under `initCloud()`; an OSS consumer downloads none of it. -->\n <CloudPanels\n v-if=\"cloud && cloudAttachment && cloudReady\"\n :editor=\"editor\"\n :core=\"core\"\n :runtime=\"cloud\"\n :cloud=\"cloudAttachment\"\n :ready=\"cloudReady\"\n :locale=\"config.locale\"\n />\n\n <!-- Small-screen gate (#235). Last child + a literal z-index above the\n chrome and `.tpl-popover-root`, so the opaque notice covers everything\n below the breakpoint. -->\n <SmallScreenNotice v-if=\"showSmallScreenNotice\" />\n </div>\n</template>\n\n<style scoped>\n.tpl-preview-pill-enter-active {\n transition:\n opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),\n transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.tpl-preview-pill-leave-active {\n transition:\n opacity 150ms ease-in,\n transform 150ms ease-in;\n}\n\n.tpl-preview-pill-enter-from,\n.tpl-preview-pill-leave-to {\n opacity: 0;\n transform: translateY(-8px) scale(0.9);\n}\n\n.tpl-preview-pill-enter-to,\n.tpl-preview-pill-leave-from {\n opacity: 1;\n transform: translateY(0) scale(1);\n}\n</style>\n","/// <reference types=\"vite/client\" />\n\nimport type en from \"./locales/en\";\nimport type cloudEn from \"./locales/cloud/en\";\n\nexport type Translations = typeof en;\nexport type CloudTranslations = typeof cloudEn;\nexport type TranslationKey = keyof Translations;\n\n// Vite resolves these globs at build time. Adding a new locale file\n// automatically registers it — no array to keep in sync.\nconst ossModules = import.meta.glob<{ default: Translations }>(\n \"./locales/*.ts\",\n);\nconst cloudModules = import.meta.glob<{ default: CloudTranslations }>(\n \"./locales/cloud/*.ts\",\n);\n\nfunction localesFromGlob(modules: Record<string, unknown>): string[] {\n return Object.keys(modules)\n .map((path) => path.match(/\\/([^/]+)\\.ts$/)?.[1])\n .filter((locale): locale is string => Boolean(locale));\n}\n\nconst supportedLocales = localesFromGlob(ossModules);\nconst supportedCloudLocales = localesFromGlob(cloudModules);\n\nfunction canonicalize(locale: string): string {\n return locale.trim().replace(/_/g, \"-\").toLowerCase();\n}\n\nfunction findSupportedLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n const canonical = canonicalize(locale);\n return supported.find((s) => canonicalize(s) === canonical);\n}\n\n/**\n * Get the base language code from a locale string.\n * e.g., 'en-GB' -> 'en', 'de_DE' -> 'de'\n */\nexport function getBaseLocale(locale: string): string {\n return canonicalize(locale).split(\"-\")[0];\n}\n\nfunction tryResolveLocale(\n locale: string,\n supported: string[],\n): string | undefined {\n return (\n findSupportedLocale(locale, supported) ??\n findSupportedLocale(getBaseLocale(locale), supported)\n );\n}\n\nfunction resolveLocale(locale: string, supported: string[]): string {\n return tryResolveLocale(locale, supported) ?? \"en\";\n}\n\n/**\n * Load OSS translations for a given locale.\n * Falls back to English if the locale is not supported.\n */\nexport async function loadTranslations(locale: string): Promise<Translations> {\n const target = resolveLocale(locale, supportedLocales);\n const loader = ossModules[`./locales/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/**\n * Load cloud translations for a given locale.\n * Cloud translations cover features available only via initCloud() —\n * AI, comments, collaboration, scoring, snapshots, plan limits, etc.\n * OSS contributors don't need to translate these; cloud locales fall back\n * to English independently of the OSS locale.\n */\nexport async function loadCloudTranslations(\n locale: string,\n): Promise<CloudTranslations> {\n const target = resolveLocale(locale, supportedCloudLocales);\n const loader = cloudModules[`./locales/cloud/${target}.ts`];\n const mod = await loader();\n return mod.default;\n}\n\n/** Check if a locale has OSS translations (matched by exact locale, then base). */\nexport function isLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedLocales) !== undefined;\n}\n\n/** Check if a locale has cloud translations (matched by exact locale, then base). */\nexport function isCloudLocaleSupported(locale: string): boolean {\n return tryResolveLocale(locale, supportedCloudLocales) !== undefined;\n}\n\n/** List of OSS-supported locales. */\nexport function getSupportedLocales(): string[] {\n return [...supportedLocales];\n}\n\n/** List of cloud-supported locales. May be a subset of OSS locales. */\nexport function getSupportedCloudLocales(): string[] {\n return [...supportedCloudLocales];\n}\n","// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- ambient-module declaration must be loaded for cross-package typecheck (workspace path alias resolves to source)\n/// <reference path=\"./virtual-modules.d.ts\" />\nimport { createApp, h, ref, type App, type Ref } from \"vue\";\nimport { DEFAULT_AUTO_SAVE_DEBOUNCE_MS } from \"@templatical/core\";\nimport type {\n BlockDefaults,\n ColorsConfig,\n CommentsProvider,\n CustomBlock,\n CustomBlockDefinition,\n DisplayConditionsConfig,\n EditorUser,\n FontsConfig,\n LogicTagsConfig,\n MediaResult,\n MergeTagsConfig,\n RenderProvider,\n SavedBlocksProvider,\n TestEmailProvider,\n Template,\n TemplateContent,\n TemplateDefaults,\n TemplatesProvider,\n ThemeOverrides,\n VersionHistoryProvider,\n UiTheme,\n ResolvePreview,\n} from \"@templatical/types\";\nimport { createDefaultTemplateContent, safeClone } from \"@templatical/types\";\nimport type { MediaRequestContext } from \"@templatical/media-library\";\n\nimport Editor from \"./Editor.vue\";\nimport type { CloudRuntime } from \"./cloud/runtime\";\nimport type { TemplaticalCloudEditorConfig } from \"./cloud/cloudConfig\";\nimport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nimport { loadTranslations, loadCloudTranslations } from \"./i18n\";\nimport { useFonts } from \"./composables\";\nimport { toMjmlForInstance } from \"./utils/toMjml\";\nimport { normalizeContentForConfig } from \"./utils/normalizeMergeTagMarkup\";\nimport {\n buildRenderPayload,\n createRenderMethods,\n resolveRenderFonts,\n} from \"./utils/renderProvider\";\nimport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n// Compiled-CSS-as-string for shadow root adoption. The `virtual:editor-css`\n// module is owned by `scripts/inline-style-css-plugin.ts` — at build time it\n// captures every emitted CSS asset (Tailwind utilities + every `.vue` SFC\n// `<style>` block + `styles/index.css` rules) and replaces this import's\n// runtime value with the full library CSS string. In dev/test the plugin\n// returns `styles/index.css` source as a fallback.\n//\n// Separate concern from the side-effecting `import \"./styles/index.css\"` in\n// `Editor.vue`, which injects styles into `document.head` for light-DOM mode and\n// survives untouched.\n//\n// Ambient declaration for `virtual:editor-css` lives in `virtual-modules.d.ts`\n// referenced at the top of this file via triple-slash so it's visible to any\n// consumer typechecking through the workspace path alias.\nimport editorStylesInline from \"virtual:editor-css\";\n\n// ---------------------------------------------------------------------------\n// OSS config + return types\n// ---------------------------------------------------------------------------\n\nexport interface TemplaticalEditorConfig {\n /**\n * Where to mount the editor — a CSS selector or an `HTMLElement`.\n *\n * Layout caveat: do NOT apply `transform`, `filter`, `perspective`, or\n * `will-change` to an ancestor of this element. Each establishes a CSS\n * containing block for `position: fixed`, which offsets the editor's\n * floating UI (color pickers, rich-text toolbars) and its drag ghost away\n * from their anchor — even when the transform's computed value is `none`\n * (an active/animated transform still promotes the element). For a\n * scroll/entrance effect on a wrapper, animate `opacity` instead of\n * `transform`.\n */\n container: string | HTMLElement;\n content?: TemplateContent;\n\n /**\n * Mount the editor inside a Shadow DOM (open mode) for CSS isolation\n * from the host page. Defaults to `true` — host stylesheets cannot\n * cascade past the shadow boundary into editor elements (`p`, `a`,\n * `input`, etc.), and editor utility classes never collide with host\n * class names.\n *\n * Set to `false` to mount in light DOM. Opt out when:\n * - Your host integration uses `document.querySelector` to reach\n * editor internals (with shadow DOM, use `container.shadowRoot\n * .querySelector(...)` instead).\n * - You need to support Firefox <101 or Safari <16.4, which lack the\n * `adoptedStyleSheets` API the shadow path relies on.\n *\n * Light-mode consumers should keep this set to `false` explicitly so\n * future SDK changes don't silently flip the default again.\n *\n * @default true\n */\n shadowDom?: boolean;\n\n onChange?: (content: TemplateContent) => void;\n onError?: (error: Error) => void;\n\n /**\n * Called whenever the editor's unsaved-changes state flips — `true` on the\n * first edit after a save, `false` once a save completes.\n *\n * Works with or without a `templates` provider, because the editor's own\n * `beforeunload` guard cannot cover SPA route changes: an embedded editor's\n * consumer has to guard their router themselves, and this is what they guard\n * it with.\n */\n onDirtyChange?: (isDirty: boolean) => void;\n\n /**\n * Storage backend for **the template itself** — the save/load lifecycle around\n * whatever is on the canvas.\n *\n * The editor owns the header's name field, save button and save-status\n * indicator, the Cmd+S shortcut, autosave and the unsaved-changes guard; you\n * own persistence. Three methods:\n *\n * ```ts\n * const editor = await init({\n * container,\n * templates: {\n * load: (id) => fetch(`/api/templates/${id}`).then((r) => r.json()),\n * create: (input) =>\n * fetch(\"/api/templates\", {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(input),\n * }).then((r) => r.json()),\n * save: (id, patch) =>\n * fetch(`/api/templates/${id}`, {\n * method: \"PATCH\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(patch),\n * }).then((r) => r.json()),\n * },\n * });\n *\n * await editor.load(\"tpl_123\");\n * ```\n *\n * Autosave (`templates.autoSave`), the unsaved-changes guard\n * (`templates.unsavedChangesGuard`) and the header's inline name field\n * (`templates.nameField`) are configured on this same object — grouped here\n * because none of the three mean anything without somewhere to save to.\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * name field, no save button, no status indicator — and `create()` / `load()` /\n * `save()` reject with an explanatory error. Persist the content yourself from\n * `onChange` instead; Cmd+S then flushes that notification immediately rather\n * than waiting out the debounce.\n *\n * Opening a template is deliberately imperative: there is no `templateId`\n * config key and no in-editor template browser, because choosing which\n * template to open belongs to your application.\n */\n templates?: TemplatesProvider;\n\n /**\n * Rendering backend — how the template becomes MJML, and how MJML becomes the\n * HTML you send.\n *\n * Separate from `templates` on purpose: saving and rendering run at different\n * frequencies (autosave would compile MJML on every debounce tick) and fail in\n * different ways. Every method is optional, and `editor.toMjml()` /\n * `editor.toHtml()` resolve each one independently:\n *\n * | Call | Order |\n * |---|---|\n * | `toMjml()` | `render.toMjml` → the bundled `@templatical/renderer` → throw |\n * | `toHtml()` | `render.toHtml` → `toMjml()`'s result + `render.compileMjml` → throw |\n *\n * **Omitted by default**, in which case `toMjml()` renders locally and\n * `toHtml()` rejects — the SDK deliberately bundles no MJML compiler, so there\n * is no local HTML path. One endpoint is enough to close that gap:\n *\n * ```ts\n * init({\n * container,\n * render: {\n * compileMjml: (mjml) =>\n * fetch(\"/api/mjml\", { method: \"POST\", body: mjml }).then((r) => r.text()),\n * },\n * });\n * ```\n *\n * Provider methods receive a **render-complete** payload: custom blocks already\n * resolved to `renderedHtml`, and the editor's effective fonts. Both are things\n * a backend cannot reconstruct from the template JSON alone.\n */\n render?: RenderProvider;\n\n /**\n * Storage backend for the template's **version history** — the past states a\n * user can browse, preview and restore.\n *\n * The editor owns the header's history control, the preview banner and the\n * restore flow; you own the storage. `list` and `get` are the operations;\n * `create` and `restore` can each be turned off by passing `false` instead of\n * a function.\n *\n * ```ts\n * init({\n * container,\n * versionHistory: {\n * list: (templateId) =>\n * fetch(`/api/templates/${templateId}/versions`).then((r) => r.json()),\n * get: (templateId, versionId) =>\n * fetch(`/api/templates/${templateId}/versions/${versionId}`)\n * .then((r) => r.json())\n * .then((v) => v.content),\n * create: false,\n * restore: (templateId, versionId) =>\n * fetch(`/api/templates/${templateId}/versions/${versionId}/restore`, {\n * method: \"POST\",\n * }).then((r) => r.json()),\n * },\n * });\n * ```\n *\n * **Omitted by default** — the control does not render and none of its UI is\n * downloaded. It also stays hidden until a template is loaded, since a version\n * belongs to a template id.\n *\n * **The editor never records a version by itself.** Whichever\n * `TemplatesProvider.save` you supply decides whether a save also records one,\n * so throttling and retention stay with the side that pays for the storage.\n */\n versionHistory?: VersionHistoryProvider;\n\n /**\n * How long the editor waits after the last edit before it fires `onChange`\n * and, when `templates.autoSave` is on, saves.\n *\n * One timer drives both, so they cannot drift apart. Set it with no templates\n * provider to pace `onChange` alone.\n *\n * @default 2000\n */\n changeDebounce?: number;\n\n onRequestMedia?: OnRequestMedia;\n\n /**\n * Resolves the template for preview surfaces — typically evaluating logic\n * tags (`{% if %}` … `{% endif %}`) against real data on your backend.\n *\n * Called when a preview opens and when the test-email recipient changes,\n * debounced. Display-only: the result never reaches `getContent()`, a send or\n * an export. Rejecting is safe — the preview falls back to the unresolved\n * template and says so.\n *\n * Supersedes `MergeTag.sample` when configured: real data beats example data,\n * so the Sample/Label switch stops rendering.\n */\n resolvePreview?: ResolvePreview;\n\n /**\n * Display-only resolver for image `src` values. The canvas calls this to\n * obtain a preview URL for a src the user entered; the content model (and\n * `toMjml()` output) always keeps the canonical value. Return `null` — or\n * the input value — to use the src as-is.\n *\n * Use this when templates reference images by a value that isn't directly\n * displayable, e.g. plain file names resolved to ephemeral `blob:` URLs\n * from local storage:\n *\n * ```ts\n * resolveImageUrl: async (src) => {\n * const file = await fileStore.lookup(src);\n * return file ? URL.createObjectURL(file) : null;\n * }\n * ```\n *\n * The resolver is called once per committed src value (typing in the src\n * input is debounced, so partial values never reach it) and results are\n * cached per src for the editor's lifetime — including failures, which\n * fall back to displaying the src verbatim. Note the caching applies to\n * transient failures too: a src that failed to resolve stays unresolved\n * until the editor is re-initialized (a re-resolve hook may be added\n * later). Applies to image srcs, design-time placeholder previews, and\n * explicit video thumbnails; auto-derived provider thumbnails (YouTube/\n * Vimeo) are already real URLs and are never resolved.\n */\n resolveImageUrl?: ResolveImageUrl;\n\n mergeTags?: MergeTagsConfig;\n /**\n * Standalone logic tags — control-flow tokens (`{% else %}`) and open/close\n * pairs (`{% if %}` … `{% endif %}`) inserted from a dedicated \"Insert logic\"\n * button. Separate from `mergeTags`; typed/pasted logic is highlighted\n * regardless of this config.\n */\n logicTags?: LogicTagsConfig;\n displayConditions?: DisplayConditionsConfig;\n customBlocks?: CustomBlockDefinition[];\n\n /**\n * Allowlist + order for the block palette. When set, only the listed block\n * types appear in the sidebar palette, in exactly this order — unlisted\n * built-ins (e.g. `video`, `table`) are hidden. When omitted, the full\n * default palette is shown.\n *\n * Reference built-in blocks by their bare type (`'section'`, `'image'`, …)\n * and custom blocks by their `custom:`-prefixed type (`'custom:qrcode'`),\n * so they can be interleaved freely:\n *\n * ```ts\n * paletteBlocks: ['section', 'title', 'image', 'custom:qrcode', 'button']\n * ```\n *\n * Unknown entries — a typo, an unregistered custom block, or `countdown`\n * outside a Cloud plan — are logged with a warning and skipped. Filtering\n * the palette never affects rendering: existing content using a hidden\n * block type still renders correctly.\n */\n paletteBlocks?: string[];\n\n /**\n * Render each HTML block's raw content as a live preview in the editor\n * canvas instead of the static placeholder card. **Off by default.**\n *\n * ```ts\n * htmlBlockPreview: true // shorthand for { enabled: true }\n * htmlBlockPreview: { enabled: true }\n * ```\n *\n * The content is rendered verbatim inside a sandboxed `<iframe>`\n * (`sandbox=\"allow-same-origin\"`, **no** `allow-scripts`): scripts and\n * inline event handlers never execute and styles can't bleed into the rest\n * of the editor. This is a preview-only setting — the MJML/HTML export path\n * renders HTML blocks regardless of it.\n *\n * @default false\n */\n htmlBlockPreview?: HtmlBlockPreviewConfig;\n\n fonts?: FontsConfig;\n\n /**\n * Editor-wide color-picker palette. `presets` appear as a clickable grid in\n * every color picker popover (block toolbars, template settings, rich-text\n * color, custom-block color fields) — clicking one applies it, and the preset\n * matching the current value is marked selected.\n *\n * ```ts\n * colors: {\n * presets: ['#0b5cff', '#111827', '#6b7280', '#ffffff'],\n * allowCustom: false, // lock authors to the presets above\n * }\n * ```\n *\n * `allowCustom` defaults to `true`. Set it to `false` (together with\n * `presets`) to hide the wheel and hex input so authors can only pick from the\n * palette — useful when embedding the editor as a white-label / brand-kit\n * tool. `allowCustom: false` is ignored, with a warning, when no `presets` are\n * configured, since that would leave the picker with no way to set a color.\n */\n colors?: ColorsConfig;\n\n /**\n * Storage backend for **saved blocks** — reusable groups of blocks a user\n * saves from the canvas and re-inserts into other templates.\n *\n * The editor owns the UI (a save action on each block, a searchable browser\n * with preview, insert-at-position, rename, delete); you own persistence.\n * Implement the four methods of `SavedBlocksProvider` against your own API,\n * or use the bundled browser-local provider for demos and prototypes:\n *\n * ```ts\n * import { init, createLocalStorageSavedBlocksProvider } from \"@templatical/editor\";\n *\n * init({ container, savedBlocks: createLocalStorageSavedBlocksProvider() });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * save action, no sidebar entry, and none of its UI code is downloaded.\n *\n * Not to be confused with `customBlocks`, which registers developer-defined\n * block *types* with their own templates and field schemas.\n */\n savedBlocks?: SavedBlocksProvider;\n\n /**\n * Sending backend for **test emails** — letting a user mail themselves the\n * template they're editing.\n *\n * The editor owns the trigger, the dialog, recipient validation and the\n * sending/success/error states; you own delivery. One method is enough:\n *\n * ```ts\n * init({\n * container,\n * testEmail: {\n * send: ({ recipient, content }) =>\n * fetch(\"/api/test-email\", {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify({ recipient, content }),\n * }).then((r) => {\n * if (!r.ok) throw new Error(\"Could not send the test email\");\n * }),\n * },\n * });\n * ```\n *\n * **Omitted by default.** With no provider the feature is entirely off — no\n * button, and none of its UI code is downloaded.\n *\n * `allowedRecipients` restricts the picker but is **not** a security boundary:\n * it lives in the user's browser. Validate the recipient on your server.\n */\n testEmail?: TestEmailProvider;\n\n /**\n * Storage backend for **comments** — the review conversation on a template.\n *\n * The editor owns the panel, the threading, the resolve flow, the per-block\n * indicators and the jump-to-block affordance; you own persistence. Five\n * methods, each mutation turn-off-able by passing `false`:\n *\n * ```ts\n * init({\n * container,\n * user: { id: \"u_7\", name: \"Ada\" },\n * comments: {\n * list: (templateId) =>\n * fetch(`/api/templates/${templateId}/comments`).then((r) => r.json()),\n * create: (templateId, input) =>\n * fetch(`/api/templates/${templateId}/comments`, {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(input),\n * }).then((r) => r.json()),\n * update: (templateId, commentId, patch) =>\n * fetch(`/api/templates/${templateId}/comments/${commentId}`, {\n * method: \"PATCH\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify(patch),\n * }).then((r) => r.json()),\n * delete: async (templateId, commentId) => {\n * await fetch(`/api/templates/${templateId}/comments/${commentId}`, {\n * method: \"DELETE\",\n * });\n * },\n * setResolved: (templateId, commentId, resolved) =>\n * fetch(`/api/templates/${templateId}/comments/${commentId}/resolve`, {\n * method: \"POST\",\n * headers: { \"Content-Type\": \"application/json\" },\n * body: JSON.stringify({ resolved }),\n * }).then((r) => r.json()),\n * },\n * });\n * ```\n *\n * **Requires {@link user}** — with no identity the feature reports itself\n * unavailable and nothing renders, rather than writing anonymous comments.\n *\n * **Omitted by default**, and the panel also stays hidden until a template is\n * loaded, since a comment belongs to a template id.\n *\n * The same object also carries outward events — `onCreated`, `onUpdated`,\n * `onDeleted`, `onResolved`, `onUnresolved` — each called with the comment and\n * whether the change was made locally or arrived through `subscribe`. That's\n * the hook for a \"3 new comments\" badge outside the editor.\n *\n * Realtime is an optional `subscribe` on the provider, not a prerequisite:\n * without it comments work identically, you simply see a colleague's on the next\n * read rather than immediately.\n */\n comments?: CommentsProvider;\n\n /**\n * Who is using the editor — needed by any feature that attributes work to a\n * person. Today that is {@link comments}; collaboration presence will want the\n * same answer, which is why this is a top-level key rather than part of the\n * comments provider.\n *\n * ```ts\n * init({ container, user: { id: \"u_7\", name: \"Ada Lovelace\" } });\n * ```\n *\n * Not a security boundary — it identifies the user to the editor's UI, in the\n * user's own browser. Attribute writes server-side from the session your backend\n * already trusts.\n */\n user?: EditorUser;\n\n blockDefaults?: BlockDefaults;\n templateDefaults?: TemplateDefaults;\n\n theme?: ThemeOverrides;\n uiTheme?: UiTheme;\n locale?: string;\n\n /**\n * Show the \"Powered by Templatical\" footer. Defaults to `true`.\n * Set to `false` to hide the footer (no attribution required by the license).\n */\n branding?: boolean;\n\n /**\n * Show a \"use a larger screen\" notice instead of the editor chrome on\n * viewports narrower than ~768px. Defaults to `true`.\n *\n * The drag-and-drop editor is a desktop-class tool — the block palette,\n * canvas, and properties panel can't lay out usably on a phone, and touch\n * dragging is impractical. Rather than render a broken, cramped layout, the\n * editor shows a clear message below the breakpoint.\n *\n * Set to `false` if you handle small screens yourself (e.g. you embed the\n * editor in a deliberately narrow desktop pane). The check is viewport-based,\n * so it targets actual small devices, not narrow containers on a wide screen.\n *\n * @default true\n */\n smallScreenNotice?: boolean;\n\n /**\n * Template linter (`@templatical/quality`) configuration. Runs every\n * linter exported by the package (accessibility + structure).\n *\n * - When unset, the linter loads on demand once the user opens the panel.\n * - When `disabled: true`, the optional peer is never imported (saves the\n * chunk download) and the sidebar tab + inline badges are suppressed.\n * - `rules`/`thresholds` follow the shape exported by `@templatical/quality`.\n */\n lint?: import(\"@templatical/quality\").LintOptions;\n}\n\n/** Function type for media browser requests, used by both OSS and Cloud editors. */\nexport type OnRequestMedia = (\n context?: MediaRequestContext,\n) => Promise<MediaResult | null>;\n\ninterface TemplaticalEditorBase {\n getContent(): TemplateContent;\n setContent(content: TemplateContent): void;\n setTheme(theme: UiTheme): void;\n unmount(): void;\n /**\n * Render the current template to MJML.\n *\n * Resolves `render.toMjml` first when a `render` provider supplies it, then the\n * bundled `@templatical/renderer` (which resolves custom blocks through the\n * editor's own registry). Rejects with a clear error when neither is available —\n * i.e. no provider method *and* the optional `@templatical/renderer` peer isn't\n * installed.\n */\n toMjml(): Promise<string>;\n /**\n * Render the current template to sending-ready HTML.\n *\n * Resolves `render.toHtml` first, then `toMjml()`'s output through\n * `render.compileMjml`. **Requires one of the two** — the SDK bundles no MJML\n * compiler, so with no `render` provider this always rejects, and the error says\n * which method to add.\n */\n toHtml(): Promise<string>;\n}\n\nexport interface TemplaticalEditor extends TemplaticalEditorBase {\n /**\n * Persist the current content as a new template through the configured\n * `templates` provider, and make the result the editor's template.\n *\n * `input.content`, when given, replaces the editor's content first. Always\n * present on the type; rejects with an explanatory error when no provider is\n * configured or when the provider set `create: false` — the same convention as\n * {@link toMjml}.\n */\n create(input?: {\n name?: string;\n content?: TemplateContent;\n }): Promise<Template>;\n /** Load a template by id and make it the editor's content. */\n load(templateId: string): Promise<Template>;\n /**\n * Persist the loaded template — name and content, as one patch. Rejects when\n * no provider is configured, when the provider set `save: false`, or when no\n * template has been created or loaded yet.\n */\n save(): Promise<Template>;\n /**\n * Whether there are edits the editor knows aren't persisted. Cleared by a\n * successful `save()` / `create()` / `load()`.\n *\n * Read it in a router guard; `onDirtyChange` is the push-based counterpart.\n */\n isDirty(): boolean;\n /**\n * Render a single custom block to its HTML representation, using the\n * registered custom block definition's template and the block's current\n * field values. Exposed for headless callers that want to reuse the\n * editor's renderer (e.g., to drive `@templatical/renderer`'s\n * `renderCustomBlock` option from outside the editor instance).\n */\n renderCustomBlock(block: CustomBlock): Promise<string>;\n /**\n * Look up the definition-level `stylesheet` for a registered custom block\n * type. Returns the raw CSS string, or `undefined` when the type is unknown\n * or the definition has no stylesheet. Exposed for headless callers that\n * want to drive `@templatical/renderer`'s `getCustomBlockStylesheet` option\n * from outside the editor instance.\n */\n getCustomBlockStylesheet(customType: string): string | undefined;\n}\n\n/**\n * `initCloud()` returns the **same** editor `init()` does.\n *\n * That is the point rather than a tidy-up: Cloud is a set of provider\n * implementations behind the same interfaces a consumer would implement, so if\n * the two entry points still returned different shapes, the claim would be\n * false. It also means moving between them is a config change and never a\n * rewrite of the calling code.\n *\n * Three cloud-only members went with the convergence: `create()` takes\n * `init()`'s `{ name?, content? }` input object now, rather than a bare\n * `TemplateContent`; `setThemeOverrides` is gone (`config.theme` is applied\n * at init on both entry points, and the entitlement that gated changing it\n * later is gone); and `sendTestEmail` is gone (the shared dialog is the\n * supported path).\n */\nexport type TemplaticalCloudEditor = TemplaticalEditor;\n\n// ---------------------------------------------------------------------------\n// Shadow root helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Module-cached `CSSStyleSheet` built once from the inline editor CSS string.\n * `adoptedStyleSheets` accepts the same sheet object across multiple shadow\n * roots — sharing one sheet costs zero per-instance memory, regardless of\n * how many editors mount.\n */\nlet cachedEditorStyleSheet: CSSStyleSheet | null = null;\nfunction getEditorStyleSheet(): CSSStyleSheet {\n if (cachedEditorStyleSheet === null) {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(editorStylesInline);\n cachedEditorStyleSheet = sheet;\n }\n return cachedEditorStyleSheet;\n}\n\ninterface MountTarget {\n target: Element;\n shadowRoot: ShadowRoot | null;\n /**\n * Disposer for any dev-mode side effects attached to the shadow root\n * (currently: the document.head `<style>` mirror's MutationObserver).\n * Always safe to call — no-op when no side effects were registered.\n */\n cleanup: () => void;\n}\n\n/**\n * Dev-only: mirror every `<style>` tag in `document.head` into the\n * shadow root's `adoptedStyleSheets`, and observe `document.head` so\n * Vite's HMR-injected style updates flow through to the shadow root\n * automatically.\n *\n * Background: Vite dev injects each `.vue` `<style scoped>` block as a\n * separate `<style>` element in `document.head` via HMR. Those don't\n * cross the shadow boundary, so a shadow-mounted editor in dev would\n * render with only the bundled `styles/index.css` rules — every SFC\n * scoped style (block selection outlines, sidebar layout, etc.) missing.\n *\n * In production builds, `inline-style-css-plugin.ts` captures every\n * emitted CSS asset at `generateBundle` time and inlines the full\n * library CSS as a single string adopted by the shadow root. This\n * dev-only mirror does the same thing at runtime by observing whatever\n * Vite ends up injecting.\n *\n * The dead-code-elimination on `import.meta.env.DEV` ensures the\n * observer + filter logic is stripped from production bundles entirely.\n *\n * Caveats:\n * - In a real consumer's Vite-dev environment, this would also adopt\n * the consumer's page-level styles (whatever they put in\n * `document.head`). That's harmless when consumers install the\n * editor from npm dist (the dev branch is dead-coded out). It only\n * matters when a consumer source-resolves this package, which is\n * unusual outside this repo's own playground.\n * - `replaceSync` strips `@import` rules per the CSSOM spec. Styles\n * containing `@import` are skipped silently (the catch below). The\n * primary bundled sheet covers Tailwind imports already, so this\n * should never matter in practice.\n */\nfunction attachDevStyleMirror(shadowRoot: ShadowRoot): () => void {\n if (!import.meta.env?.DEV) return () => {};\n\n function buildSheets(): CSSStyleSheet[] {\n const sheets: CSSStyleSheet[] = [];\n // shadow-ok: dev-only HMR style mirror (production path is plugin-driven via adoptedStyleSheets)\n document.head.querySelectorAll(\"style\").forEach((el) => {\n const text = el.textContent;\n if (!text) return;\n try {\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(text);\n sheets.push(sheet);\n } catch {\n // Skip styles that contain disallowed constructs (e.g. `@import`).\n }\n });\n return sheets;\n }\n\n function refresh(): void {\n // Keep the editor's primary (bundled) sheet first so its declarations\n // are the cascade fallback; dev sheets adopted after may override them\n // — same precedence model as production where everything ends up in\n // one stylesheet anyway.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet(), ...buildSheets()];\n }\n\n refresh();\n\n const observer = new MutationObserver(() => refresh());\n // childList catches new/removed <style> tags; characterData + subtree\n // catches Vite HMR mutating an existing style's textContent in place.\n // shadow-ok: dev-only HMR style mirror observer\n observer.observe(document.head, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n\n return () => observer.disconnect();\n}\n\n/**\n * Resolve where Vue should mount: directly on the consumer's container\n * (light-DOM mode) or on a fresh `<div>` inside a newly-attached open\n * shadow root (shadow mode). In shadow mode, also attaches the editor's\n * cached `CSSStyleSheet` to the root so chrome renders with the right\n * styles inside the boundary, plus a dev-only mirror that observes\n * `document.head` (see `attachDevStyleMirror`).\n *\n * Idempotent: a second call on the same container reuses any existing\n * shadow root, clearing its contents so a prior mount's stale DOM doesn't\n * accumulate.\n */\nfunction resolveMountTarget(\n container: Element,\n shadowDom: boolean | undefined,\n): MountTarget {\n if (!shadowDom) {\n return { target: container, shadowRoot: null, cleanup: () => {} };\n }\n\n const shadowRoot =\n container.shadowRoot ?? container.attachShadow({ mode: \"open\" });\n\n // Adopt the editor stylesheet. Idempotent — repeated assignment of the\n // same sheet is fine. Dev mirror (below) will overwrite this with the\n // primary sheet + mirrored sheets, then keep them in sync.\n shadowRoot.adoptedStyleSheets = [getEditorStyleSheet()];\n\n // Clear stale content from a prior mount (re-init on same container).\n while (shadowRoot.firstChild) {\n shadowRoot.removeChild(shadowRoot.firstChild);\n }\n\n const host = document.createElement(\"div\");\n host.className = \"tpl-editor-host\";\n // Match the container's layout. Without an explicit size the host is a\n // default block element (height: auto) and the editor's `tpl:h-full`\n // template root collapses to content height because the height-100% chain\n // breaks at the shadow boundary. `width: 100%` is redundant for block\n // elements but harmless and explicit.\n host.style.cssText = \"display:block;height:100%;width:100%;\";\n shadowRoot.appendChild(host);\n\n const cleanup = attachDevStyleMirror(shadowRoot);\n\n return { target: host, shadowRoot, cleanup };\n}\n\n// ---------------------------------------------------------------------------\n// OSS init — sync\n// ---------------------------------------------------------------------------\n\ninterface OssEntry {\n app: App;\n editorRef: Ref<InstanceType<typeof Editor> | null>;\n /** Tear down dev-mode side effects (style mirror observer, etc.). */\n cleanup: () => void;\n}\n\n// Per-container registry so two `init()` calls with different containers\n// produce independent editor instances (multi-instance support). Re-init\n// on the same container still auto-unmounts the previous instance.\nconst ossEntries = new Map<Element, OssEntry>();\n\n// \"Last init'd\" container preserves the legacy single-instance behavior\n// of the top-level `unmount()` export — the playground (and other one-\n// editor consumers) calls bare `unmount()` and expects it to tear down\n// whatever was most recently mounted.\nlet lastOssContainer: Element | null = null;\n\nfunction unmountOssContainer(container: Element): void {\n const entry = ossEntries.get(container);\n if (!entry) return;\n entry.cleanup();\n entry.app.unmount();\n ossEntries.delete(container);\n if (lastOssContainer === container) {\n lastOssContainer = null;\n }\n}\n\nexport async function init(\n config: TemplaticalEditorConfig,\n): Promise<TemplaticalEditor> {\n return mountEditor(config);\n}\n\n/**\n * The one mount path. `init()` calls it with no runtime; `initCloud()` calls it\n * with Cloud's, having already resolved auth, the plan and every adapter.\n */\nasync function mountEditor(\n config: TemplaticalEditorConfig,\n cloud?: CloudRuntime,\n): Promise<TemplaticalEditor> {\n const container =\n typeof config.container === \"string\"\n ? document.querySelector(config.container)\n : config.container;\n\n if (!container) {\n throw new Error(\n `[Templatical] Container element not found: ${config.container}`,\n );\n }\n\n // Load translations before mounting so child components can use useI18n synchronously\n const translations = await loadTranslations(config.locale ?? \"en\");\n\n // Create fonts manager to pass to Editor\n const fontsManager = useFonts(config.fonts);\n\n // Auto-unmount any prior instance on the SAME container *after* awaits\n // — checking before the await would let two concurrent init() calls\n // both pass the guard and orphan the first-mounted app on this\n // container.\n unmountOssContainer(container);\n\n const mount = resolveMountTarget(container, config.shadowDom ?? true);\n const editorRef: Ref<InstanceType<typeof Editor> | null> = ref(null);\n\n // Merge tags reach us in one of two shapes, and only one of them behaves like\n // a tag. Anything typed or pasted is already a `<span data-merge-tag>`, since\n // `MergeTagNode`'s input rules convert it on the spot; content that arrived\n // any other way — a consumer's stored JSON, an `@templatical/import-*`\n // conversion — still holds bare `{{tokens}}` that render as plain text.\n // Normalizing here, before the seed reaches `Editor.vue`, means core is\n // handed content that is already correct and never observes a mutation, so\n // nothing is marked dirty and no autosave tick fires for a load.\n //\n // This is one of four places content enters. The other three:\n // `instance.setContent` and `instance.create` below, and the `templates`\n // provider's `load`, wrapped in `Editor.vue` where it reaches core.\n if (config.content) {\n config.content = normalizeContentForConfig(\n config.content,\n config.mergeTags,\n );\n }\n\n const app = createApp({\n setup() {\n return () =>\n h(Editor, {\n config,\n translations,\n fontsManager,\n shadowRoot: mount.shadowRoot ?? undefined,\n cloud,\n ref: editorRef,\n });\n },\n });\n\n app.mount(mount.target);\n\n ossEntries.set(container, { app, editorRef, cleanup: mount.cleanup });\n lastOssContainer = container;\n\n const instance: TemplaticalEditor = {\n getContent() {\n // safeClone (not a naked JSON.stringify): a drag inside a section can\n // leave a Sortable expando cycle reachable from live content, which a\n // plain stringify chokes on (#203). The clone drops the back-ref.\n if (editorRef.value) {\n return safeClone(editorRef.value.getContent());\n }\n return safeClone(config.content ?? createDefaultTemplateContent());\n },\n setContent(content: TemplateContent) {\n // Normalized once and used for both writes: `getContent()` falls back to\n // `config.content` before mount, so a raw write-back here would hand back\n // different content than the mounted editor holds.\n const normalized = normalizeContentForConfig(content, config.mergeTags);\n if (editorRef.value) {\n editorRef.value.setContent(normalized);\n }\n config.content = normalized;\n },\n setTheme(theme: UiTheme) {\n if (editorRef.value) {\n editorRef.value.setTheme(theme);\n }\n },\n unmount: () => unmountOssContainer(container),\n create(input?: { name?: string; content?: TemplateContent }) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n // Supplied content becomes editor state before the provider is called,\n // so it is consumer content arriving in and gets the same treatment as\n // `setContent`. A contentless create is forwarded as-is — core then\n // persists the state it already holds, which is normalized already.\n return editorRef.value.create(\n input?.content\n ? {\n ...input,\n content: normalizeContentForConfig(\n input.content,\n config.mergeTags,\n ),\n }\n : input,\n );\n },\n load(templateId: string) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.load(templateId);\n },\n save() {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.save();\n },\n isDirty() {\n // Pre-mount there is nothing the editor could have changed, so the honest\n // answer is \"no unsaved edits\" rather than a throw.\n return editorRef.value?.isDirty() ?? false;\n },\n renderCustomBlock(block: CustomBlock) {\n if (!editorRef.value) {\n return Promise.reject(new Error(\"[Templatical] Editor not ready\"));\n }\n return editorRef.value.renderCustomBlock(block);\n },\n getCustomBlockStylesheet(customType: string) {\n // Pre-mount: registry is empty, so no definition has a stylesheet to\n // resolve. Returning undefined here matches the \"unknown type\" branch\n // and keeps the renderer's `getCustomBlockStylesheet` resolver total.\n if (!editorRef.value) {\n return undefined;\n }\n return editorRef.value.getCustomBlockStylesheet(customType);\n },\n toMjml: () => render.toMjml(),\n toHtml: () => render.toHtml(),\n };\n\n // Both methods resolve per call, so a provider added later in the config — or a\n // provider that implements only one method — behaves the same as one that\n // implements all three. `instance` is captured lazily, which is what lets the\n // payload builder read the editor's live content.\n const render = createRenderMethods({\n provider: config.render,\n buildPayload: () =>\n buildRenderPayload({\n getContent: () => instance.getContent(),\n renderCustomBlock: (block: CustomBlock) =>\n instance.renderCustomBlock(block),\n getFonts: () => resolveRenderFonts(fontsManager),\n }),\n renderLocalMjml: () =>\n toMjmlForInstance({\n getContent: () => instance.getContent(),\n renderCustomBlock: (block: CustomBlock) =>\n instance.renderCustomBlock(block),\n getCustomBlockStylesheet: (customType: string) =>\n instance.getCustomBlockStylesheet(customType),\n getFonts: () => resolveRenderFonts(fontsManager),\n }),\n });\n\n return instance;\n}\n\n// ---------------------------------------------------------------------------\n// Cloud init — a thin adapter-wiring wrapper over init()\n// ---------------------------------------------------------------------------\n\n/**\n * Mount the editor against Templatical Cloud.\n *\n * **This is `init()` with Cloud's adapters filled in**, and deliberately nothing\n * more. Templates, rendering and version history are providers, so Cloud's entry\n * point is adapter wiring rather than a second editor:\n *\n * 1. Build the auth manager and complete the handshake.\n * 2. Health-check the API, and fetch the plan config.\n * 3. Build Cloud's adapters over that auth manager.\n * 4. Delegate to `init()`.\n *\n * A failure in steps 1–2 **rejects**, rather than mounting an editor that shows\n * an error overlay. That is the one place the wrapper is genuinely not `init()`:\n * `init()` cannot fail after it mounts, and OSS should not grow the ability. A\n * session that dies *later* — an auth refresh that cannot renew the token — does\n * still surface as an overlay, because by then there is an editor to cover.\n *\n * `templates`, `comments` and `versionHistory` are all keyed to a template\n * id Cloud issued, which also anchors collaboration, AI rewrite, scoring and\n * the server-side export — a store Cloud never issued ids for would degrade\n * all of them silently. Cloud therefore keeps each key's storage, but each\n * key is still accepted, for its configuration and events:\n * `templates.load`/`create`/`save`,\n * `comments.list`/`create`/`update`/`delete`/`setResolved`, and\n * `versionHistory.list`/`get`/`create`/`restore` are ignored with a warning\n * naming them, while `templates`' `autoSave`, `unsavedChangesGuard`,\n * `nameField`, `onSaved`, `onCreated`, `onLoaded`, `comments`' `onCreated`,\n * `onUpdated`, `onDeleted`, `onResolved`, `onUnresolved`, and\n * `versionHistory`'s `onCreated`, `onRestored` are all honoured.\n *\n * `render` is not a key here at all, unlike `init()` — Cloud renders\n * server-side for test email, sends and exports, so a supplied renderer\n * would change only what you preview and export, never what Cloud delivers.\n * `resolvePreview` is the same key with the same type on both entry points,\n * so upgrading an OSS integration is a deletion. `savedBlocks` and\n * `testEmail` are the same key on both entry points too, but Cloud widens\n * each type to also accept an events-only shape — `boolean |\n * SavedBlocksOptions | SavedBlocksProvider` and `Pick<TestEmailOptions,\n * \"onSent\" | \"defaultRecipient\"> | TestEmailProvider` — so upgrading is\n * still a deletion: drop the key to adopt Cloud's store or sender, or leave\n * it exactly as it is to keep your own.\n *\n * `user` is not a key either: Cloud signs comment writes against the auth token's\n * `user` claim, so it fills `init({ user })` from there rather than letting a\n * browser name someone else.\n */\nexport async function initCloud(\n config: TemplaticalCloudEditorConfig,\n): Promise<TemplaticalCloudEditor> {\n // Dynamic imports — every cloud module is tree-shaken from the OSS bundle, and\n // an OSS consumer who never calls this downloads none of it.\n const [{ bootstrapCloud }, cloudTranslations] = await Promise.all([\n import(\"./cloud/createCloudRuntime\"),\n loadCloudTranslations(config.locale ?? \"en\"),\n ]);\n\n const { runtime, providers, user } = await bootstrapCloud({\n config,\n cloudTranslations,\n });\n\n return mountEditor(\n {\n container: config.container,\n content: config.content,\n shadowDom: config.shadowDom,\n locale: config.locale,\n uiTheme: config.uiTheme,\n theme: config.theme,\n branding: config.branding,\n smallScreenNotice: config.smallScreenNotice,\n blockDefaults: config.blockDefaults,\n templateDefaults: config.templateDefaults,\n customBlocks: config.customBlocks,\n paletteBlocks: config.paletteBlocks,\n htmlBlockPreview: config.htmlBlockPreview,\n colors: config.colors,\n fonts: config.fonts,\n mergeTags: config.mergeTags,\n logicTags: config.logicTags,\n displayConditions: config.displayConditions,\n resolvePreview: config.resolvePreview,\n lint: config.lint,\n onChange: config.onChange,\n onError: config.onError,\n onDirtyChange: config.onDirtyChange,\n // Cloud's adapters, behind the very keys a consumer would fill in\n // themselves. `savedBlocks` is absent when the consumer passed `false`.\n // `templates` spreads Cloud's own provider first so its load/create/save\n // always win over a consumer's config. Autosave defaults on here, unlike\n // `init()`, because Cloud always has a store to save to; the debounce\n // falls back to the same shared constant `useAutoSave` already defaults\n // to, made explicit so a consumer's own `changeDebounce` reaches it.\n templates: {\n ...providers.templates,\n autoSave: config.templates?.autoSave ?? true,\n unsavedChangesGuard: config.templates?.unsavedChangesGuard,\n nameField: config.templates?.nameField,\n },\n changeDebounce: config.changeDebounce ?? DEFAULT_AUTO_SAVE_DEBOUNCE_MS,\n render: providers.render,\n versionHistory: providers.versionHistory,\n savedBlocks: providers.savedBlocks,\n testEmail: providers.testEmail,\n comments: providers.comments,\n // From the JWT. Undefined when the project's token carries no `user` claim,\n // which leaves comments unavailable rather than anonymous.\n user,\n },\n runtime,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Unmount helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Unmount the most-recently-created OSS editor. Single-instance legacy\n * API — callers managing multiple editors should use `instance.unmount()`\n * from each returned object, which targets the specific container.\n */\nexport function unmount(): void {\n if (lastOssContainer) {\n unmountOssContainer(lastOssContainer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TemplaticalCloudEditorConfig } from \"./cloud/cloudConfig\";\nexport type {\n BlockDefaults,\n TemplateContent,\n TemplateDefaults,\n ThemeOverrides,\n UiTheme,\n MergeTagsConfig,\n LogicTagsConfig,\n LogicTag,\n LogicPair,\n DisplayConditionsConfig,\n CustomBlockDefinition,\n ViewportSize,\n ColorsConfig,\n CustomFont,\n FontsConfig,\n RenderPayload,\n RenderProvider,\n SavedBlock,\n SavedBlocksListParams,\n SavedBlocksOptions,\n SavedBlocksProvider,\n Template,\n TemplatePatch,\n TemplatesOptions,\n TemplateSaveTrigger,\n TemplatesProvider,\n TestEmailOptions,\n TestEmailPayload,\n TestEmailProvider,\n VersionHistoryOptions,\n CommentsOptions,\n CommentEventMeta,\n} from \"@templatical/types\";\n\n// Bundled browser-local saved-blocks provider. Re-exported here (rather than\n// leaving it to `@templatical/core`) because consumers install only this\n// package — core is bundled inline and isn't resolvable on their side.\nexport { createLocalStorageSavedBlocksProvider } from \"@templatical/core\";\nexport type { LocalStorageSavedBlocksOptions } from \"@templatical/core\";\n\nexport type { ResolveImageUrl } from \"./composables/useImageUrlResolver\";\nexport type { UseFontsReturn, FontOption } from \"./composables/useFonts\";\nexport { useFonts } from \"./composables/useFonts\";\nexport type { EditorCapabilities } from \"./types/editor-capabilities\";\nexport type { HtmlBlockPreviewConfig } from \"./utils/resolveHtmlBlockPreview\";\n\nexport {\n getSupportedLocales,\n getSupportedCloudLocales,\n isLocaleSupported,\n isCloudLocaleSupported,\n getBaseLocale,\n} from \"./i18n\";\n"],"mappings":"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"}
|