best-email-builder 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +290 -20
  3. package/dist/ai-server.js +68 -0
  4. package/dist/ai-server.js.map +1 -0
  5. package/dist/ai.js +852 -0
  6. package/dist/ai.js.map +1 -0
  7. package/dist/builder.js +16 -8
  8. package/dist/chunk-2RZHGAH5.js +64 -0
  9. package/dist/chunk-2RZHGAH5.js.map +1 -0
  10. package/dist/chunk-3LKF4QBL.js +595 -0
  11. package/dist/chunk-3LKF4QBL.js.map +1 -0
  12. package/dist/chunk-5SASKCUC.js +251 -0
  13. package/dist/chunk-5SASKCUC.js.map +1 -0
  14. package/dist/{chunk-JOC752I3.js → chunk-6D3SLT5W.js} +27 -29
  15. package/dist/chunk-6D3SLT5W.js.map +1 -0
  16. package/dist/chunk-7U6BAKM7.js +119 -0
  17. package/dist/chunk-7U6BAKM7.js.map +1 -0
  18. package/dist/chunk-DFNHICTF.js +1359 -0
  19. package/dist/chunk-DFNHICTF.js.map +1 -0
  20. package/dist/chunk-ECHGJZ2W.js +327 -0
  21. package/dist/chunk-ECHGJZ2W.js.map +1 -0
  22. package/dist/chunk-KXJJSVN5.js +98 -0
  23. package/dist/chunk-KXJJSVN5.js.map +1 -0
  24. package/dist/{chunk-ZSUHOCCE.js → chunk-LXR4QTGP.js} +123 -579
  25. package/dist/chunk-LXR4QTGP.js.map +1 -0
  26. package/dist/{chunk-BVOVAEEJ.js → chunk-LYMJ2TBX.js} +2 -2
  27. package/dist/chunk-MPX5CWTZ.js +1687 -0
  28. package/dist/chunk-MPX5CWTZ.js.map +1 -0
  29. package/dist/chunk-PB7YZ3WP.js +5635 -0
  30. package/dist/chunk-PB7YZ3WP.js.map +1 -0
  31. package/dist/chunk-UFKCLU2D.js +255 -0
  32. package/dist/chunk-UFKCLU2D.js.map +1 -0
  33. package/dist/index.js +51 -20
  34. package/dist/index.js.map +1 -1
  35. package/dist/library.js +12 -7
  36. package/dist/server.js +513 -214
  37. package/dist/server.js.map +1 -1
  38. package/dist/styles.css +1 -1
  39. package/dist/types/ai/ConnectAi.d.ts +12 -0
  40. package/dist/types/ai/GenerateButton.d.ts +4 -0
  41. package/dist/types/ai/RewriteText.d.ts +5 -0
  42. package/dist/types/ai/SubjectIdeas.d.ts +25 -0
  43. package/dist/types/ai/chip.d.ts +1 -0
  44. package/dist/types/ai/claude.d.ts +21 -0
  45. package/dist/types/ai/createAi.d.ts +66 -0
  46. package/dist/types/ai/draft.d.ts +163 -0
  47. package/dist/types/ai/generator.d.ts +123 -0
  48. package/dist/types/ai/i18n/fr.d.ts +2 -0
  49. package/dist/types/ai/i18n/index.d.ts +2 -0
  50. package/dist/types/ai/index.d.ts +9 -0
  51. package/dist/types/ai/keys.d.ts +46 -0
  52. package/dist/types/ai/prompt.d.ts +76 -0
  53. package/dist/types/ai/providers.d.ts +67 -0
  54. package/dist/types/ai/purposes.d.ts +48 -0
  55. package/dist/types/ai/rewrite.d.ts +83 -0
  56. package/dist/types/ai/server.d.ts +9 -0
  57. package/dist/types/ai/subjects.d.ts +97 -0
  58. package/dist/types/blocks/shared.d.ts +20 -0
  59. package/dist/types/core/EmailEditor.d.ts +72 -19
  60. package/dist/types/core/constants.d.ts +1 -1
  61. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  62. package/dist/types/core/scrollbars.d.ts +1 -1
  63. package/dist/types/defaultEditor.d.ts +4 -1
  64. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  65. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  66. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  67. package/dist/types/engine/canvas/icons.d.ts +14 -0
  68. package/dist/types/engine/canvas/morph.d.ts +21 -0
  69. package/dist/types/engine/canvasMjml.d.ts +30 -0
  70. package/dist/types/engine/mjml.d.ts +31 -0
  71. package/dist/types/engine/model.d.ts +130 -0
  72. package/dist/types/engine/placement.d.ts +69 -0
  73. package/dist/types/engine/richText.d.ts +15 -0
  74. package/dist/types/engine/unwrapModule.d.ts +29 -0
  75. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  76. package/dist/types/features/brand/brand.d.ts +108 -0
  77. package/dist/types/features/brand/extract.d.ts +61 -0
  78. package/dist/types/features/brand/index.d.ts +2 -0
  79. package/dist/types/features/brand/messages.d.ts +13 -0
  80. package/dist/types/features/brand/useBrand.d.ts +22 -0
  81. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  82. package/dist/types/features/preview/darkMode.d.ts +65 -0
  83. package/dist/types/features/variables/VariablePicker.d.ts +3 -2
  84. package/dist/types/i18n/index.d.ts +8 -0
  85. package/dist/types/index.d.ts +5 -2
  86. package/dist/types/server/brand.d.ts +32 -0
  87. package/dist/types/server/brandImport.d.ts +93 -0
  88. package/dist/types/server/compileMjml.d.ts +2 -0
  89. package/dist/types/server/handler.d.ts +26 -0
  90. package/dist/types/server/index.d.ts +5 -1
  91. package/dist/types/storage/client.d.ts +57 -3
  92. package/dist/types/ui/EmailBuilder.d.ts +5 -1
  93. package/dist/types/ui/EmailStudio.d.ts +6 -1
  94. package/dist/types/ui/PropertiesPanel.d.ts +2 -1
  95. package/dist/types/ui/fields.d.ts +2 -1
  96. package/dist/types/ui/primitives/button.d.ts +1 -1
  97. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  98. package/dist/types/ui/primitives/searchShortcut.d.ts +16 -0
  99. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  100. package/package.json +27 -7
  101. package/dist/chunk-3M36VVXS.js +0 -665
  102. package/dist/chunk-3M36VVXS.js.map +0 -1
  103. package/dist/chunk-57CTQNTK.js +0 -56
  104. package/dist/chunk-57CTQNTK.js.map +0 -1
  105. package/dist/chunk-ILRAQTHS.js +0 -3677
  106. package/dist/chunk-ILRAQTHS.js.map +0 -1
  107. package/dist/chunk-JOC752I3.js.map +0 -1
  108. package/dist/chunk-ZSUHOCCE.js.map +0 -1
  109. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  110. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
  111. package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
  112. /package/dist/{chunk-BVOVAEEJ.js.map → chunk-LYMJ2TBX.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/features/templates/starter.js","../src/blocks/social/index.js","../src/features/templates/gallery.js","../src/features/preview/PreviewButton.jsx","../src/core/scrollbars.js","../src/features/brand/useBrand.js","../src/ui/useLayoutWidth.js"],"sourcesContent":["// @ts-check\n/**\n * Starter template of a new email.\n *\n * A complete email rather than a blank page: header (logo or sender name),\n * headline, text, button, and a FOOTER with the sender's postal address and the\n * unsubscribe link — the two mentions a bulk email must carry (CAN-SPAM, GDPR)\n * and that get forgotten when starting from nothing.\n *\n * Written with the same settings as the blocks (`blocks/`), with longhand\n * paddings (see core/mjmlAttributes): what the canvas shows is what gets sent.\n */\n\nimport { identity } from '../../i18n/index.js';\nimport { DEFAULT_BUTTON_URL, FONT_STACK, PALETTE } from '../../blocks/shared.js';\n\n/**\n * @typedef {{\n * name?: string,\n * address?: string,\n * logoUrl?: string,\n * logoWidth?: number,\n * }} StarterSender\n */\n\nexport function escapeXml(text) {\n return String(text ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nexport const SIDE = 'padding-left=\"32px\" padding-right=\"32px\"';\nexport const TEXT = `font-family=\"${FONT_STACK}\" color=\"${PALETTE.ink}\"`;\n\n/**\n * Header row: the sender's logo, or its name in bold.\n * @param {StarterSender} sender @param {import('../../i18n/index.js').Translate} t\n */\nexport function senderHeaderMjml(sender, t) {\n const name = escapeXml(sender.name || t('Your company'));\n return sender.logoUrl\n ? `<mj-image src=\"${escapeXml(sender.logoUrl)}\" alt=\"${name}\" width=\"${Number(sender.logoWidth) || 140}px\" align=\"left\" ${SIDE}></mj-image>`\n : `<mj-text ${TEXT} font-size=\"18px\" line-height=\"24px\" font-weight=\"bold\" ${SIDE}>${name}</mj-text>`;\n}\n\n/**\n * Footer section: sender name and postal address, unsubscribe link — the two\n * mentions a bulk email must carry (CAN-SPAM, GDPR).\n * @param {StarterSender} sender @param {import('../../i18n/index.js').Translate} t\n */\nexport function footerSectionMjml(sender, t) {\n const name = escapeXml(sender.name || t('Your company'));\n const footer = [name, sender.address ? escapeXml(sender.address) : ''].filter(Boolean).join(' · ');\n return (\n '<mj-section padding-top=\"20px\" padding-bottom=\"28px\">' +\n '<mj-column>' +\n `<mj-text font-family=\"${FONT_STACK}\" font-size=\"12px\" line-height=\"18px\" color=\"#6d7484\" align=\"center\" ${SIDE}>` +\n `${footer}<br />` +\n `<a href=\"{{ unsubscribe }}\" style=\"color:#6d7484;text-decoration:underline;\">${escapeXml(t('Unsubscribe'))}</a>` +\n '</mj-text>' +\n '</mj-column>' +\n '</mj-section>'\n );\n}\n\n/**\n * @param {{\n * sender?: StarterSender,\n * buttonUrl?: string,\n * t?: import('../../i18n/index.js').Translate,\n * }} [options]\n */\nexport function createStarterMjml({ sender = {}, buttonUrl = DEFAULT_BUTTON_URL, t = identity } = {}) {\n return [\n '<mjml>',\n `<mj-body background-color=\"${PALETTE.canvas}\" width=\"600px\">`,\n // Header: logo or sender name.\n `<mj-section background-color=\"${PALETTE.surface}\" padding-top=\"28px\" padding-bottom=\"8px\">`,\n '<mj-column>',\n senderHeaderMjml(sender, t),\n '</mj-column>',\n '</mj-section>',\n // Body: headline, text, button.\n `<mj-section background-color=\"${PALETTE.surface}\" padding-top=\"8px\" padding-bottom=\"28px\">`,\n '<mj-column>',\n `<mj-text ${TEXT} font-size=\"24px\" line-height=\"32px\" font-weight=\"bold\" ${SIDE}>${escapeXml(t('Your headline'))}</mj-text>`,\n `<mj-text ${TEXT} font-size=\"15px\" line-height=\"24px\" ${SIDE}>` +\n `${escapeXml(t('Hello {name},', { name: '{{contact.firstname}}' }))}<br /><br />${escapeXml(t('Your text.'))}</mj-text>`,\n `<mj-button href=\"${escapeXml(buttonUrl)}\" background-color=\"${PALETTE.navy}\" color=\"${PALETTE.white}\" ` +\n `font-family=\"${FONT_STACK}\" font-size=\"15px\" font-weight=\"bold\" border-radius=\"6px\" ` +\n `inner-padding=\"12px 28px\" align=\"left\" ${SIDE}>${escapeXml(t('Learn more'))}</mj-button>`,\n '</mj-column>',\n '</mj-section>',\n // Footer: sender and unsubscribe link.\n footerSectionMjml(sender, t),\n '</mj-body>',\n '</mjml>',\n ].join('');\n}\n\n/** The starter template with default (English) texts. */\nexport const STARTER_MJML = createStarterMjml();\n","// @ts-check\n/**\n * SOCIAL LINKS - `mj-social` and its `mj-social-element` icons.\n *\n * MJML draws each icon as an image in a table cell, so the row holds in\n * Outlook. The networks use their `-noshare` variant: the link goes to the\n * page given (the company's profile), not to a \"share this\" dialog.\n *\n * The icons are MJML's own, served over https by Mailjet; a network can be\n * changed, a link duplicated (one more network) or deleted from the panel.\n * No text next to the icons: the row stays readable on a phone.\n */\n\nimport { GROUPS, inWhiteSection, prop } from '../shared.js';\n\n/** @typedef {import('../../core/EmailEditor.js').EmailBlock} EmailBlock */\n\n/** Networks MJML ships an icon for (server and canvas). */\nexport const SOCIAL_NETWORKS = [\n { value: 'facebook-noshare', label: 'Facebook', href: 'https://www.facebook.com/' },\n { value: 'instagram-noshare', label: 'Instagram', href: 'https://www.instagram.com/' },\n { value: 'linkedin-noshare', label: 'LinkedIn', href: 'https://www.linkedin.com/' },\n { value: 'x-noshare', label: 'X', href: 'https://x.com/' },\n { value: 'youtube-noshare', label: 'YouTube', href: 'https://www.youtube.com/' },\n { value: 'github-noshare', label: 'GitHub', href: 'https://github.com/' },\n];\n\nconst DEFAULT_NETWORKS = ['facebook-noshare', 'instagram-noshare', 'linkedin-noshare', 'x-noshare'];\n\nconst MJML =\n '<mj-social mode=\"horizontal\" align=\"center\" icon-size=\"28px\" border-radius=\"14px\" inner-padding=\"6px\">' +\n DEFAULT_NETWORKS.map((name) => {\n const network = SOCIAL_NETWORKS.find((n) => n.value === name);\n return `<mj-social-element name=\"${name}\" href=\"${network?.href}\"></mj-social-element>`;\n }).join('') +\n '</mj-social>';\n\n/** @type {EmailBlock} */\nexport const socialBlock = {\n id: 'social',\n label: 'Social links',\n description: 'Icons linking to your profiles',\n icon: 'Share2',\n kind: 'content',\n tagName: 'mj-social',\n order: 60,\n mjml: MJML,\n sectionMjml: inWhiteSection(MJML),\n properties: [\n { key: 'icon-size', label: 'Icon size', type: 'size', group: GROUPS.layout, min: 16, max: 48, unit: 'px' },\n prop.radius(24),\n prop.align(),\n {\n key: 'mode',\n label: 'Direction',\n type: 'select',\n group: GROUPS.layout,\n options: [\n { value: 'horizontal', label: 'Horizontal' },\n { value: 'vertical', label: 'Vertical' },\n ],\n },\n prop.padding('inner-padding', 'Space around each icon'),\n prop.padding(),\n ],\n};\n\n/** One icon of the row: selectable, not addable (duplicate it instead). */\n/** @type {EmailBlock} */\nexport const socialElementBlock = {\n id: 'social-element',\n label: 'Social link',\n icon: 'Link',\n kind: 'structure',\n tagName: 'mj-social-element',\n order: 903,\n properties: [\n {\n key: 'name',\n label: 'Network',\n type: 'select',\n group: GROUPS.content,\n options: SOCIAL_NETWORKS.map(({ value, label }) => ({ value, label })),\n },\n prop.href('Profile URL'),\n prop.background('background-color', 'Icon background'),\n ],\n};\n","// @ts-check\n/**\n * TEMPLATE GALLERY — ready-made emails to start from.\n *\n * Each entry builds its MJML from the same pieces as the blocks (palette,\n * web-safe fonts, longhand paddings) and ends with the sender's footer and the\n * unsubscribe link: a template picked here passes the server's quality checks\n * as it is. Texts go through `t()`, so a French interface starts from French\n * emails.\n *\n * Images are left EMPTY on purpose (the canvas shows a frame to fill): a\n * sample photo from a third-party service would, one day, end up in a real\n * send. Thumbnails replace them with a grey placeholder (`withThumbnailImages`).\n *\n * The list is plain data: pass your own to `<EmailBuilder templates>`, or `[]`\n * to hide the gallery.\n */\n\nimport { identity } from '../../i18n/index.js';\nimport { DEFAULT_BUTTON_URL, FONT_STACK, PALETTE } from '../../blocks/shared.js';\nimport { SOCIAL_NETWORKS } from '../../blocks/social/index.js';\nimport { SIDE, createStarterMjml, escapeXml, footerSectionMjml, senderHeaderMjml } from './starter.js';\n\n/**\n * @typedef {import('./starter.js').StarterSender} StarterSender\n * @typedef {import('../../i18n/index.js').Translate} Translate\n *\n * @typedef {{\n * id: string,\n * name: string,\n * description: string,\n * create: (options: {sender: StarterSender, t: Translate}) => string,\n * meta?: (t: Translate) => {subject?: string, preheader?: string},\n * }} GalleryTemplate\n */\n\nconst WHITE_SECTION = `background-color=\"${PALETTE.surface}\"`;\n\n/**\n * White section, paddings in longhand (see core/mjmlAttributes).\n * @param {string} inner @param {{top?: number, bottom?: number, x?: number}} [padding]\n */\nfunction section(inner, { top = 8, bottom = 8, x = 0 } = {}) {\n const sides = x ? ` padding-left=\"${x}px\" padding-right=\"${x}px\"` : '';\n return `<mj-section ${WHITE_SECTION} padding-top=\"${top}px\" padding-bottom=\"${bottom}px\"${sides}>${inner}</mj-section>`;\n}\n\n/** @param {string} inner @param {string} [attrs] */\nfunction column(inner, attrs = '') {\n return `<mj-column${attrs ? ` ${attrs}` : ''}>${inner}</mj-column>`;\n}\n\n/** @param {string} text @param {{size?: number, color?: string, align?: string, side?: boolean}} [o] */\nfunction heading(text, { size = 24, color = PALETTE.navy, align = 'left', side = true } = {}) {\n return (\n `<mj-text font-family=\"${FONT_STACK}\" font-size=\"${size}px\" line-height=\"${Math.round(size * 1.3)}px\" ` +\n `font-weight=\"bold\" color=\"${color}\" align=\"${align}\"${side ? ` ${SIDE}` : ''}>${text}</mj-text>`\n );\n}\n\n/** @param {string} html @param {{size?: number, color?: string, align?: string, side?: boolean}} [o] */\nfunction paragraph(html, { size = 15, color = PALETTE.ink, align = 'left', side = true } = {}) {\n return (\n `<mj-text font-family=\"${FONT_STACK}\" font-size=\"${size}px\" line-height=\"${Math.round(size * 1.6)}px\" ` +\n `color=\"${color}\" align=\"${align}\"${side ? ` ${SIDE}` : ''}>${html}</mj-text>`\n );\n}\n\n/** @param {string} label @param {{align?: string, background?: string, color?: string, side?: boolean}} [o] */\nfunction button(label, { align = 'left', background = PALETTE.navy, color = PALETTE.white, side = true } = {}) {\n return (\n `<mj-button href=\"${DEFAULT_BUTTON_URL}\" background-color=\"${background}\" color=\"${color}\" ` +\n `font-family=\"${FONT_STACK}\" font-size=\"15px\" font-weight=\"bold\" border-radius=\"6px\" ` +\n `inner-padding=\"12px 28px\" align=\"${align}\"${side ? ` ${SIDE}` : ''}>${label}</mj-button>`\n );\n}\n\nconst IMAGE = '<mj-image align=\"center\"></mj-image>';\n\n/** @param {StarterSender} sender @param {Translate} t */\nfunction header(sender, t) {\n return `<mj-section ${WHITE_SECTION} padding-top=\"28px\" padding-bottom=\"8px\">${column(senderHeaderMjml(sender, t))}</mj-section>`;\n}\n\n/** A coloured banner: title, sentence, button — the Hero banner block. */\nfunction banner({ title, text, cta, background = PALETTE.navy }) {\n return (\n `<mj-section background-color=\"${background}\" background-size=\"cover\" background-position=\"center center\" ` +\n 'background-repeat=\"no-repeat\" padding-top=\"48px\" padding-bottom=\"48px\">' +\n column(\n heading(title, { size: 30, color: PALETTE.white, align: 'center' }) +\n paragraph(text, { size: 16, color: PALETTE.border, align: 'center' }) +\n (cta ? button(cta, { align: 'center', background: PALETTE.white, color: PALETTE.navy }) : '')\n ) +\n '</mj-section>'\n );\n}\n\nfunction social() {\n const elements = ['facebook-noshare', 'instagram-noshare', 'linkedin-noshare']\n .map((name) => `<mj-social-element name=\"${name}\" href=\"${SOCIAL_NETWORKS.find((n) => n.value === name)?.href}\"></mj-social-element>`)\n .join('');\n return section(\n column(\n `<mj-social mode=\"horizontal\" align=\"center\" icon-size=\"28px\" border-radius=\"14px\" inner-padding=\"6px\">${elements}</mj-social>`\n ),\n { top: 16, bottom: 16 }\n );\n}\n\n/** @param {string} body */\nfunction email(body) {\n return `<mjml><mj-body background-color=\"${PALETTE.canvas}\" width=\"600px\">${body}</mj-body></mjml>`;\n}\n\nconst esc = escapeXml;\n\n/** @type {GalleryTemplate[]} */\nexport const TEMPLATE_GALLERY = [\n {\n id: 'blank',\n name: 'Blank',\n description: 'An empty page to build from scratch',\n create: () => email(''),\n },\n {\n id: 'simple',\n name: 'Simple',\n description: 'Logo, headline, text and a button',\n create: ({ sender, t }) => createStarterMjml({ sender, t }),\n },\n {\n id: 'newsletter',\n name: 'Newsletter',\n description: 'Monthly news: banner, two articles, highlights',\n meta: (t) => ({\n subject: t('The news of the month'),\n preheader: t('What changed, what is coming, and two stories worth your time.'),\n }),\n create: ({ sender, t }) =>\n email(\n header(sender, t) +\n banner({\n title: esc(t('The news of the month')),\n text: esc(t('What changed, what is coming, and two stories worth your time.')),\n cta: null,\n }) +\n [1, 2]\n .map((n) =>\n section(\n column(IMAGE, 'vertical-align=\"middle\"') +\n column(\n heading(esc(t('Article {n}', { n })), { size: 20, side: false }) +\n paragraph(esc(t('Two or three sentences that sum up the story and make people want the rest.')), {\n side: false,\n }) +\n button(esc(t('Read more')), { side: false }),\n 'vertical-align=\"middle\"'\n ),\n { top: 16, bottom: 16, x: 16 }\n )\n )\n .join('') +\n section(column(heading(esc(t('In brief')), { size: 20 })), { top: 20, bottom: 0 }) +\n section(\n [t('A new feature'), t('An event'), t('A tip')]\n .map((title) =>\n column(\n heading(esc(title), { size: 16, side: false }) +\n paragraph(esc(t('One sentence about it.')), { size: 14, side: false })\n )\n )\n .join(''),\n { bottom: 20, x: 16 }\n ) +\n social() +\n footerSectionMjml(sender, t)\n ),\n },\n {\n id: 'welcome',\n name: 'Welcome',\n description: 'Greets a new customer and lists the first steps',\n meta: (t) => ({ subject: t('Welcome aboard!'), preheader: t('Three steps to get started.') }),\n create: ({ sender, t }) =>\n email(\n header(sender, t) +\n section(\n column(\n heading(esc(t('Welcome, {name}!', { name: '{{contact.firstname}}' })), { size: 28 }) +\n paragraph(esc(t('Thank you for joining us. Here is how to get the most out of your account.')))\n ),\n { top: 16, bottom: 8 }\n ) +\n section(\n [\n [t('1. Complete your profile'), t('So we can tailor what we send you.')],\n [t('2. Explore'), t('Take a tour of what you can do.')],\n [t('3. Ask us anything'), t('Reply to this email: a person reads it.')],\n ]\n .map(([title, text]) =>\n column(\n heading(esc(title), { size: 16, side: false }) + paragraph(esc(text), { size: 14, side: false }),\n `background-color=\"${PALETTE.canvas}\" padding-top=\"12px\" padding-bottom=\"12px\" padding-left=\"12px\" padding-right=\"12px\"`\n )\n )\n .join(''),\n { x: 24 }\n ) +\n section(column(button(esc(t('Get started')), { align: 'center' })), { top: 16, bottom: 28 }) +\n footerSectionMjml(sender, t)\n ),\n },\n {\n id: 'promotion',\n name: 'Promotion',\n description: 'A sale with a discount code and two products',\n meta: (t) => ({ subject: t('30% off this weekend only'), preheader: t('With the code SPRING30, until Sunday midnight.') }),\n create: ({ sender, t }) =>\n email(\n header(sender, t) +\n banner({\n title: esc(t('30% off everything')),\n text: esc(t('This weekend only, with the code SPRING30.')),\n cta: esc(t('Shop the sale')),\n background: PALETTE.brand,\n }) +\n section(\n [t('Product one'), t('Product two')]\n .map((name) =>\n column(\n IMAGE +\n heading(esc(name), { size: 18, side: false }) +\n paragraph(esc(t('€49 instead of €70')), { size: 15, side: false }) +\n button(esc(t('Buy')), { side: false })\n )\n )\n .join(''),\n { top: 20, bottom: 20, x: 16 }\n ) +\n section(\n column(\n paragraph(esc(t('Offer valid until Sunday midnight, while stocks last.')), {\n size: 12,\n color: '#6d7484',\n align: 'center',\n })\n )\n ) +\n footerSectionMjml(sender, t)\n ),\n },\n {\n id: 'event',\n name: 'Event invitation',\n description: 'Date, place and a button to book a seat',\n meta: (t) => ({ subject: t(\"You're invited\"), preheader: t('Save the date — seats are limited.') }),\n create: ({ sender, t }) =>\n email(\n header(sender, t) +\n banner({\n title: esc(t(\"You're invited\")),\n text: esc(t('An evening of talks and drinks with the team and our customers.')),\n cta: null,\n }) +\n section(\n column(heading(esc(t('When')), { size: 16, side: false }) + paragraph(esc(t('Thursday, 7 pm')), { side: false })) +\n column(heading(esc(t('Where')), { size: 16, side: false }) + paragraph(esc(t('Our offices, 1 Main Street')), { side: false })),\n { top: 24, x: 16 }\n ) +\n section(\n column(\n paragraph(esc(t('Seats are limited: book yours now, it takes ten seconds.'))) +\n button(esc(t('Book my seat')), { align: 'left' })\n ),\n { bottom: 28 }\n ) +\n footerSectionMjml(sender, t)\n ),\n },\n];\n\n/**\n * The gallery with its texts translated and the sender applied.\n *\n * @param {{templates?: GalleryTemplate[], sender?: StarterSender, t?: Translate}} [options]\n * @returns {{id: string, name: string, description: string, mjml: string, subject: string, preheader: string}[]}\n */\nexport function buildGallery({ templates = TEMPLATE_GALLERY, sender = {}, t = identity } = {}) {\n return templates.map((template) => {\n const meta = template.meta ? template.meta(t) : {};\n return {\n id: template.id,\n name: t(template.name),\n description: t(template.description),\n mjml: template.create({ sender, t }),\n subject: meta.subject || '',\n preheader: meta.preheader || '',\n };\n });\n}\n\n/** Grey frame drawn in place of an image not chosen yet, in thumbnails only. */\nconst THUMBNAIL_IMAGE =\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 300'%3E\" +\n \"%3Crect width='600' height='300' fill='%23dfe6f0'/%3E%3C/svg%3E\";\n\n/**\n * MJML for a thumbnail: empty images get a grey frame instead of a broken\n * image icon. Never saved: the template itself keeps its empty images.\n * @param {string} mjml\n */\nexport function withThumbnailImages(mjml) {\n return String(mjml || '').replace(/<mj-image(?![^>]*\\ssrc=)([^>]*)>/g, `<mj-image src=\"${THUMBNAIL_IMAGE}\"$1>`);\n}\n\n/**\n * Compiled HTML for a thumbnail or a preview: an image not chosen yet (an\n * `<img>` with no `src`) gets the same grey frame. For HTML that went through\n * the server's validator, which rightly refuses `data:` images in MJML.\n * @param {string} html\n */\nexport function withPlaceholderImages(html) {\n return String(html || '').replace(/<img(?![^>]*\\ssrc=)(\\s[^>]*)?>/g, (_m, attrs = '') => `<img src=\"${THUMBNAIL_IMAGE}\"${attrs}>`);\n}\n","import { useMemo, useState } from 'react';\nimport { AlertTriangle, CheckCircle2, Eye, Info, Loader2, Monitor, Moon, RefreshCw, Smartphone, Sun } from 'lucide-react';\nimport { EMAIL_MOBILE_WIDTH } from '../../core/constants.js';\nimport { FRAME_SCROLLBAR_CSS } from '../../core/scrollbars.js';\nimport { useUi } from '../../ui/context.jsx';\nimport { Button } from '../../ui/primitives/button.jsx';\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/primitives/overlays.jsx';\nimport { SectionLabel, Tabs, TabsList, TabsTrigger } from '../../ui/primitives/misc.jsx';\nimport { cn } from '../../ui/primitives/cn.js';\nimport { DARK_PAGE, simulateDarkMode } from './darkMode.js';\n\n/**\n * PREVIEW of the FINAL email — rendered by the server, as it will be stored.\n *\n * The canvas already compiles in the browser; the preview goes through the\n * server compilation (the only one that counts), with sample values for the\n * variables, and lists the quality checks (unsubscribe link, alt texts, links,\n * Gmail clipping size) and the MJML warnings.\n *\n * The HTML is shown in an iframe with an EMPTY sandbox: no script, form,\n * navigation, nor same origin. An email is a complete HTML document: injected\n * into the page it would inherit the application's styles and lie.\n *\n * Sun / moon: the email in light mode, or as the apps that repaint emails in\n * dark mode show it (see `darkMode.js`).\n */\n\nconst LEVEL_ICON = { warning: AlertTriangle, info: Info };\n\nexport function QualityList({ quality = [], warnings = [] }) {\n const { t } = useUi();\n if (!quality.length && !warnings.length) {\n return (\n <p className=\"eb:flex eb:items-center eb:gap-1.5 eb:text-[12px] eb:text-foreground-lighter\">\n <CheckCircle2 className=\"eb:size-3.5 eb:text-success\" aria-hidden=\"true\" />\n {t('Nothing to fix.')}\n </p>\n );\n }\n return (\n <ul className=\"eb:space-y-1.5\" aria-label={t('Points to check')}>\n {quality.map((issue) => {\n const Icon = LEVEL_ICON[issue.level] || Info;\n return (\n <li key={issue.code} className=\"eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-foreground\">\n <Icon\n className={cn(\n 'eb:mt-0.5 eb:size-3.5 eb:shrink-0',\n issue.level === 'warning' ? 'eb:text-warning' : 'eb:text-foreground-muted'\n )}\n aria-hidden=\"true\"\n />\n {issue.message}\n </li>\n );\n })}\n {warnings.slice(0, 5).map((w, i) => (\n <li key={`mjml-${i}`} className=\"eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-foreground-lighter\">\n <Info className=\"eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted\" aria-hidden=\"true\" />\n MJML: {w.message}\n </li>\n ))}\n </ul>\n );\n}\n\n/** The builder's thin scrollbars inside the frame (see `core/scrollbars.js`). */\nconst PREVIEW_SCROLLBAR_CSS = `<style data-preview-scrollbar>${FRAME_SCROLLBAR_CSS}</style>`;\n\nconst withPreviewScrollbars = (html) =>\n !html ? '' : /<\\/head>/i.test(html) ? html.replace(/<\\/head>/i, `${PREVIEW_SCROLLBAR_CSS}</head>`) : PREVIEW_SCROLLBAR_CSS + html;\n\n/** Sandboxed preview frame, desktop or mobile width, light or dark. */\nexport function EmailPreview({ html, loading = false, error = null, device = 'desktop', onDeviceChange, onRefresh, hint = true }) {\n const { t } = useUi();\n const isMobile = device === 'mobile';\n const [scheme, setScheme] = useState('light');\n const dark = scheme === 'dark';\n // `srcDoc`, not a blob URL: nothing to revoke when the HTML changes.\n const srcDoc = useMemo(() => withPreviewScrollbars(dark ? simulateDarkMode(html) : html), [html, dark]);\n\n return (\n <div className=\"eb:flex eb:h-full eb:min-h-0 eb:flex-col\">\n <div className=\"eb:mb-2 eb:flex eb:shrink-0 eb:flex-wrap eb:items-center eb:gap-2\">\n {/* The same switch as the editor's toolbar. */}\n <Tabs value={device} onValueChange={(value) => onDeviceChange?.(value)}>\n <TabsList aria-label={t('Device')}>\n <TabsTrigger value=\"desktop\">\n <Monitor />\n {t('Desktop')}\n </TabsTrigger>\n <TabsTrigger value=\"mobile\">\n <Smartphone />\n {t('Mobile')}\n </TabsTrigger>\n </TabsList>\n </Tabs>\n <Tabs value={scheme} onValueChange={setScheme}>\n <TabsList aria-label={t('Color scheme')}>\n <TabsTrigger value=\"light\" aria-label={t('Light mode')} title={t('Light mode')}>\n <Sun />\n </TabsTrigger>\n <TabsTrigger value=\"dark\" aria-label={t('Dark mode')} title={t('Dark mode')}>\n <Moon />\n </TabsTrigger>\n </TabsList>\n </Tabs>\n <span className=\"eb:flex-1\" />\n {onRefresh && (\n <Button variant=\"outline\" size=\"sm-compact\" onClick={onRefresh} disabled={loading}>\n {loading ? <Loader2 className=\"eb:animate-spin\" /> : <RefreshCw />}\n {t('Refresh')}\n </Button>\n )}\n </div>\n\n <div className=\"eb:min-h-0 eb:flex-1 eb:overflow-auto eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:p-3\">\n {error ? (\n <div className=\"eb:flex eb:h-full eb:items-center eb:justify-center eb:p-6 eb:text-center eb:text-[13px] eb:text-destructive\">{error}</div>\n ) : !srcDoc ? (\n <div className=\"eb:flex eb:h-full eb:items-center eb:justify-center eb:p-6 eb:text-center eb:text-[13px] eb:text-foreground-lighter\">\n {loading ? t('Rendering…') : t('Nothing to show yet.')}\n </div>\n ) : (\n // Narrows and widens like the editor's canvas (width .35s ease).\n <div\n className={cn(\n 'eb:mx-auto eb:max-w-full eb:transition-[width,box-shadow] eb:duration-[350ms] eb:ease-in-out eb:motion-reduce:transition-none',\n isMobile && 'eb:shadow-sm'\n )}\n style={{ width: isMobile ? EMAIL_MOBILE_WIDTH : '100%', background: dark ? DARK_PAGE : '#ffffff' }}\n data-device={device}\n data-scheme={scheme}\n >\n <iframe\n sandbox=\"\"\n srcDoc={srcDoc}\n title={t('Email preview')}\n className=\"eb:block eb:w-full eb:border-0 eb:transition-[height] eb:duration-[350ms] eb:ease-in-out eb:motion-reduce:transition-none\"\n style={{ height: isMobile ? 760 : 900, background: dark ? DARK_PAGE : '#ffffff' }}\n />\n </div>\n )}\n </div>\n\n {dark ? (\n <p className=\"eb:mt-2 eb:shrink-0 eb:text-[11px] eb:leading-relaxed eb:text-foreground-lighter\" data-testid=\"eb-dark-hint\">\n {t('Dark mode as Gmail and Outlook apps show it: light backgrounds turn dark, dark text turns light, images stay as they are.')}\n </p>\n ) : (\n hint && (\n <p className=\"eb:mt-2 eb:shrink-0 eb:text-[11px] eb:leading-relaxed eb:text-foreground-lighter\">\n {t('Indicative preview — check it with “Send test”.')}\n </p>\n )\n )}\n </div>\n );\n}\n\nexport default function PreviewButton({ context }) {\n const { editor, ready, meta, storage } = context;\n const { t } = useUi();\n const [open, setOpen] = useState(false);\n const [device, setDevice] = useState('desktop');\n const [state, setState] = useState({ loading: false, html: '', subject: null, preheader: null, warnings: [], quality: [], error: null });\n\n const refresh = async () => {\n setState((s) => ({ ...s, loading: true, error: null }));\n try {\n const data = await storage.render({ mjml: editor.exportMjml(), subject: meta.subject, preheader: meta.preheader });\n setState({\n loading: false,\n html: data.html,\n // Subject and preview text with their variables filled, when the backend returns them.\n subject: data.subject ?? null,\n preheader: data.preheader ?? null,\n warnings: data.warnings || [],\n quality: data.quality || [],\n error: null,\n });\n } catch (error) {\n setState({ loading: false, html: '', subject: null, preheader: null, warnings: [], quality: [], error: error.message });\n }\n };\n\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm-compact\"\n disabled={!ready}\n onClick={() => {\n setOpen(true);\n refresh();\n }}\n >\n <Eye />\n {t('Preview')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"eb:flex eb:h-[85vh] eb:max-h-[85vh] eb:flex-col eb:gap-4 eb:sm:max-w-[67.2rem]\">\n <DialogHeader>\n <div className=\"eb:flex eb:items-center eb:gap-3\">\n <span className=\"eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-lg2 eb:bg-primary/10 eb:text-primary\">\n <Eye className=\"eb:size-5\" />\n </span>\n <div>\n <DialogTitle className=\"eb:text-base eb:font-normal\">{t('Email preview')}</DialogTitle>\n <DialogDescription className=\"eb:text-[13px]\">{t('Final rendering, variables filled with samples.')}</DialogDescription>\n </div>\n </div>\n </DialogHeader>\n <div className=\"eb:grid eb:min-h-0 eb:flex-1 eb:grid-cols-1 eb:gap-4 eb:md:grid-cols-[minmax(0,1fr)_260px]\">\n <EmailPreview\n html={state.html}\n loading={state.loading}\n error={state.error}\n device={device}\n onDeviceChange={setDevice}\n onRefresh={refresh}\n // The reminder points to \"Send test\": only when it exists.\n hint={typeof storage.sendTest === 'function'}\n />\n <aside className=\"eb:min-h-0 eb:space-y-5 eb:overflow-y-auto\" aria-busy={state.loading}>\n <section className=\"eb:space-y-2\">\n <SectionLabel>{t('Inbox')}</SectionLabel>\n <div className=\"eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:px-3 eb:py-2.5\">\n <p className=\"eb:text-[13px] eb:font-medium eb:text-foreground\">{(state.subject ?? meta.subject) || t('No subject')}</p>\n <p className=\"eb:text-[12px] eb:text-foreground-muted\">{(state.preheader ?? meta.preheader) || t('No preview text')}</p>\n </div>\n </section>\n <section className=\"eb:space-y-2\">\n <SectionLabel>{t('To check')}</SectionLabel>\n {!state.loading && !state.error && <QualityList quality={state.quality} warnings={state.warnings} />}\n </section>\n </aside>\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n","// @ts-check\n/**\n * Scrollbars inside the builder's frames (the canvas, the preview),\n * the same as in the rest of the builder: a 5.6 px gutter, a 2.8 px thumb,\n * no arrows, shown only while the pointer is over the scrolling block. A frame is a\n * document of its own, so `styles.css` does not reach it — the same rules\n * are injected into it. Added to the canvas and the preview only, never to\n * the HTML that is saved or sent.\n *\n * Chromium ≥ 121 drops `::-webkit-scrollbar` (and brings back the arrows)\n * as soon as `scrollbar-width` or `scrollbar-color` is set, so those two are\n * kept for Firefox alone.\n */\nexport const FRAME_SCROLLBAR_CSS = `\n::-webkit-scrollbar{width:5.6px;height:5.6px;background:transparent}\n::-webkit-scrollbar-button{display:none;width:0;height:0}\n::-webkit-scrollbar-track,::-webkit-scrollbar-track-piece,::-webkit-scrollbar-corner{background:transparent;border:0;box-shadow:none}\n::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45);border:1.4px solid transparent;background-clip:content-box;border-radius:9999px;min-height:2rem;min-width:2rem}\n::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.45) transparent}}\n@media (hover:hover) and (pointer:fine){\n::-webkit-scrollbar-thumb{background-color:transparent}\n*:hover::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45)}\n*:hover::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-color:transparent transparent}*:hover{scrollbar-color:rgba(156,163,175,.45) transparent}}\n}`;\n","// @ts-check\n/**\n * The workspace's brand, read once per storage and shared by every screen\n * using that storage (the library and the builder show the same brand, and\n * a brand saved in one is seen by the other).\n */\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { cleanBrand } from './brand.js';\n\n/**\n * @typedef {import('./brand.js').Brand} Brand\n * @typedef {{value: Brand, loaded: Promise<Brand>, listeners: Set<(brand: Brand) => void>}} BrandEntry\n */\n\n/** @type {WeakMap<object, BrandEntry>} */\nconst entries = new WeakMap();\n\n/**\n * The brand of a storage: `loaded` resolves once it is known ({} when the\n * storage keeps none, or could not be read — the builder then uses its own look).\n * @param {any} storage\n * @returns {BrandEntry}\n */\nexport function brandOf(storage) {\n let entry = entries.get(storage);\n if (entry) return entry;\n /** @type {BrandEntry} */\n const created = { value: {}, loaded: Promise.resolve({}), listeners: new Set() };\n created.loaded =\n typeof storage?.loadBrand === 'function'\n ? Promise.resolve()\n .then(() => storage.loadBrand())\n .then(\n (brand) => cleanBrand(brand),\n () => /** @type {Brand} */ ({})\n )\n .then((brand) => {\n created.value = brand;\n created.listeners.forEach((listener) => listener(brand));\n return brand;\n })\n : created.loaded;\n if (storage && typeof storage === 'object') entries.set(storage, created);\n return created;\n}\n\n/**\n * @param {any} storage\n * @returns {{brand: Brand, setBrand: (brand: Brand) => void}}\n * `setBrand`: tells every screen of this storage about a brand just saved.\n */\nexport function useBrand(storage) {\n const [brand, setState] = useState(() => brandOf(storage).value);\n useEffect(() => {\n const entry = brandOf(storage);\n setState(entry.value);\n entry.listeners.add(setState);\n return () => {\n entry.listeners.delete(setState);\n };\n }, [storage]);\n const setBrand = useCallback(\n (/** @type {Brand} */ value) => {\n const entry = brandOf(storage);\n entry.value = cleanBrand(value);\n entry.listeners.forEach((listener) => listener(entry.value));\n },\n [storage]\n );\n return { brand, setBrand };\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\n/**\n * Layout of the builder from the width of ITS OWN box, not of the window: the\n * builder is embedded in host pages (a side panel, a modal, a phone), and its\n * room is what counts.\n *\n * wide ≥ 1180 px blocks, canvas and properties side by side;\n * medium ≥ 760 px blocks beside the canvas, properties in a drawer;\n * narrow < 760 px the canvas alone, blocks and properties in drawers.\n *\n * Before the first measure (server rendering, first paint) the layout is\n * `wide`, the desktop one.\n */\n\nexport const LAYOUT_BREAKPOINTS = { wide: 1180, medium: 760 };\n\n/** @param {number} width @returns {'wide'|'medium'|'narrow'} */\nexport function layoutFor(width) {\n if (width >= LAYOUT_BREAKPOINTS.wide) return 'wide';\n if (width >= LAYOUT_BREAKPOINTS.medium) return 'medium';\n return 'narrow';\n}\n\n/** @param {{current: HTMLElement|null}} ref */\nexport function useLayoutWidth(ref) {\n const [layout, setLayout] = useState(/** @type {'wide'|'medium'|'narrow'} */ ('wide'));\n useEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === 'undefined') return undefined;\n const measure = () => setLayout(layoutFor(el.getBoundingClientRect().width));\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(el);\n return () => observer.disconnect();\n }, [ref]);\n return layout;\n}\n"],"mappings":"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","names":["useState","useState","useEffect","useState"]}
@@ -0,0 +1,251 @@
1
+ import {
2
+ identity,
3
+ isAssetPath
4
+ } from "./chunk-DFNHICTF.js";
5
+
6
+ // src/validation/urls.js
7
+ var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "action", "formaction", "xlink:href"]);
8
+ var IMAGE_ATTRIBUTES = /* @__PURE__ */ new Set([
9
+ "src",
10
+ "srcset",
11
+ "background-url",
12
+ "background",
13
+ "poster",
14
+ "icon-unwrapped-url",
15
+ "icon-wrapped-url"
16
+ ]);
17
+ var NAMED_ENTITIES = {
18
+ colon: ":",
19
+ tab: " ",
20
+ newline: "\n",
21
+ lpar: "(",
22
+ rpar: ")",
23
+ amp: "&",
24
+ quot: '"',
25
+ apos: "'",
26
+ lt: "<",
27
+ gt: ">",
28
+ nbsp: " "
29
+ };
30
+ function decodeEntities(value) {
31
+ return String(value ?? "").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);?/gi, (whole, body) => {
32
+ const key = String(body).toLowerCase();
33
+ if (key.startsWith("#x")) return String.fromCodePoint(Number.parseInt(key.slice(2), 16) || 0);
34
+ if (key.startsWith("#")) return String.fromCodePoint(Number.parseInt(key.slice(1), 10) || 0);
35
+ return NAMED_ENTITIES[key] ?? whole;
36
+ });
37
+ }
38
+ function isMergeTag(value) {
39
+ return /^\{\{\s*[a-zA-Z][\w.]*\s*\}\}$/.test(String(value || "").trim());
40
+ }
41
+ function isSafeUrl(raw, kind) {
42
+ const value = decodeEntities(raw).trim();
43
+ if (!value) return true;
44
+ if (isMergeTag(value)) return true;
45
+ const compact = value.replace(/[\s\u0000-\u001f\u007f]+/g, "").toLowerCase();
46
+ if (kind === "image") return compact.startsWith("https://") || isAssetPath(value);
47
+ if (compact.startsWith("#")) return true;
48
+ return /^(https?:\/\/|mailto:|tel:)/.test(compact);
49
+ }
50
+
51
+ // src/validation/mjmlValidator.js
52
+ var MJML_MAX_LENGTH = 512 * 1024;
53
+ var MJML_TAGS = /* @__PURE__ */ new Set([
54
+ "mjml",
55
+ "mj-head",
56
+ "mj-body",
57
+ "mj-attributes",
58
+ "mj-all",
59
+ "mj-class",
60
+ "mj-preview",
61
+ "mj-title",
62
+ "mj-breakpoint",
63
+ "mj-wrapper",
64
+ "mj-section",
65
+ "mj-group",
66
+ "mj-column",
67
+ "mj-text",
68
+ "mj-image",
69
+ "mj-button",
70
+ "mj-divider",
71
+ "mj-spacer",
72
+ "mj-table",
73
+ "mj-social",
74
+ "mj-social-element",
75
+ "mj-navbar",
76
+ "mj-navbar-link",
77
+ "mj-hero"
78
+ ]);
79
+ var INLINE_TAGS = /* @__PURE__ */ new Set([
80
+ "a",
81
+ "b",
82
+ "strong",
83
+ "i",
84
+ "em",
85
+ "u",
86
+ "s",
87
+ "strike",
88
+ "del",
89
+ "br",
90
+ "span",
91
+ "p",
92
+ "div",
93
+ "font",
94
+ "sup",
95
+ "sub",
96
+ "small",
97
+ "big",
98
+ "ul",
99
+ "ol",
100
+ "li",
101
+ "h1",
102
+ "h2",
103
+ "h3",
104
+ "h4",
105
+ "h5",
106
+ "h6",
107
+ "blockquote",
108
+ "code",
109
+ "pre",
110
+ "hr",
111
+ "img",
112
+ "table",
113
+ "tbody",
114
+ "thead",
115
+ "tr",
116
+ "td",
117
+ "th"
118
+ ]);
119
+ var FORBIDDEN_TAGS = /* @__PURE__ */ new Map([
120
+ ["mj-include", "mj-include would read a file from the server"],
121
+ ["mj-raw", "mj-raw (free HTML) is not allowed"],
122
+ ["mj-style", "mj-style (free CSS) is outside the builder"],
123
+ ["mj-font", "mj-font (external font) is outside the builder"],
124
+ ["script", "script not allowed"],
125
+ ["iframe", "iframe not allowed"],
126
+ ["frame", "frame not allowed"],
127
+ ["object", "object not allowed"],
128
+ ["embed", "embed not allowed"],
129
+ ["form", "form not allowed"],
130
+ ["input", "form field not allowed"],
131
+ ["button", "HTML button not allowed (use the Button block)"],
132
+ ["textarea", "form field not allowed"],
133
+ ["select", "form field not allowed"],
134
+ ["style", "style sheet not allowed"],
135
+ ["link", "external resource not allowed"],
136
+ ["meta", "meta tag not allowed"],
137
+ ["base", "base tag not allowed"],
138
+ ["svg", "SVG not allowed"],
139
+ ["math", "MathML not allowed"]
140
+ ]);
141
+ var TAG_RE = /<\s*(\/?)\s*([a-zA-Z][\w:-]*)([^>]*)>/g;
142
+ var ATTR_RE = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
143
+ function* attributesOf(source) {
144
+ ATTR_RE.lastIndex = 0;
145
+ let match;
146
+ while (match = ATTR_RE.exec(source)) {
147
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
148
+ yield { name: match[1].toLowerCase(), value };
149
+ }
150
+ }
151
+ function styleIsSafe(style) {
152
+ const css = decodeEntities(style).toLowerCase().replace(/\s+/g, "");
153
+ if (css.includes("expression(") || css.includes("javascript:") || css.includes("behavior:")) return false;
154
+ const urls = css.match(/url\(([^)]*)\)/g) || [];
155
+ return urls.every((u) => isSafeUrl(u.slice(4, -1).replace(/^['"]|['"]$/g, ""), "image"));
156
+ }
157
+ function validateMjml(mjml, { t = identity } = {}) {
158
+ const issues = [];
159
+ const source = typeof mjml === "string" ? mjml : "";
160
+ if (!source.trim()) {
161
+ return { ok: false, issues: [{ code: "EMPTY", message: t("Empty MJML") }] };
162
+ }
163
+ if (source.length > MJML_MAX_LENGTH) {
164
+ issues.push({
165
+ code: "TOO_LARGE",
166
+ message: t("MJML too large ({size} KB, maximum {max} KB)", {
167
+ size: Math.round(source.length / 1024),
168
+ max: MJML_MAX_LENGTH / 1024
169
+ })
170
+ });
171
+ }
172
+ const withoutComments = source.replace(/<!--([\s\S]*?)-->/g, (whole, body) => {
173
+ if (/\[if\s/i.test(body)) {
174
+ issues.push({ code: "CONDITIONAL_COMMENT", message: t("conditional comment not allowed") });
175
+ }
176
+ return "";
177
+ });
178
+ if (/<!\[CDATA\[/i.test(withoutComments)) {
179
+ issues.push({ code: "CDATA", message: t("CDATA section not allowed") });
180
+ }
181
+ const trimmed = withoutComments.trim();
182
+ if (!/^<mjml[\s>]/i.test(trimmed)) {
183
+ issues.push({ code: "ROOT", message: t("the document must start with <mjml>") });
184
+ }
185
+ if (!/<mj-body[\s>]/i.test(trimmed)) {
186
+ issues.push({ code: "BODY", message: t("missing <mj-body>") });
187
+ }
188
+ TAG_RE.lastIndex = 0;
189
+ let match;
190
+ while (match = TAG_RE.exec(withoutComments)) {
191
+ const closing = match[1] === "/";
192
+ const tag = match[2].toLowerCase();
193
+ if (FORBIDDEN_TAGS.has(tag)) {
194
+ const template = FORBIDDEN_TAGS.get(tag);
195
+ issues.push({ code: "FORBIDDEN_TAG", message: template ? t(template) : tag, tag });
196
+ continue;
197
+ }
198
+ if (!MJML_TAGS.has(tag) && !INLINE_TAGS.has(tag)) {
199
+ issues.push({ code: "UNKNOWN_TAG", message: t("tag not allowed: <{tag}>", { tag }), tag });
200
+ continue;
201
+ }
202
+ if (closing) continue;
203
+ for (const { name, value } of attributesOf(match[3])) {
204
+ if (name.startsWith("on")) {
205
+ issues.push({
206
+ code: "EVENT_ATTRIBUTE",
207
+ message: t("attribute {name} not allowed", { name }),
208
+ tag,
209
+ attribute: name
210
+ });
211
+ } else if (LINK_ATTRIBUTES.has(name) && !isSafeUrl(value, "link")) {
212
+ issues.push({ code: "UNSAFE_URL", message: t("URL rejected ({name})", { name }), tag, attribute: name });
213
+ } else if (IMAGE_ATTRIBUTES.has(name) && !isSafeUrl(value, "image")) {
214
+ issues.push({
215
+ code: "UNSAFE_IMAGE",
216
+ message: t("image rejected ({name}): https only", { name }),
217
+ tag,
218
+ attribute: name
219
+ });
220
+ } else if (name === "style" && !styleIsSafe(value)) {
221
+ issues.push({ code: "UNSAFE_STYLE", message: t("inline style rejected"), tag, attribute: name });
222
+ }
223
+ }
224
+ }
225
+ return { ok: issues.length === 0, issues };
226
+ }
227
+ function assertValidMjml(mjml, { t = identity } = {}) {
228
+ const { ok, issues } = validateMjml(mjml, { t });
229
+ if (ok) return;
230
+ const shown = issues.slice(0, 3).map((i) => i.message).join("; ");
231
+ const more = issues.length - 3;
232
+ const error = (
233
+ /** @type {Error & {status?: number, issues?: ValidationIssue[]}} */
234
+ new Error(
235
+ more > 0 ? t("Template rejected: {issues} (+{more})", { issues: shown, more }) : t("Template rejected: {issues}", { issues: shown })
236
+ )
237
+ );
238
+ error.status = 422;
239
+ error.issues = issues;
240
+ throw error;
241
+ }
242
+
243
+ export {
244
+ LINK_ATTRIBUTES,
245
+ IMAGE_ATTRIBUTES,
246
+ isSafeUrl,
247
+ INLINE_TAGS,
248
+ validateMjml,
249
+ assertValidMjml
250
+ };
251
+ //# sourceMappingURL=chunk-5SASKCUC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/validation/urls.js","../src/validation/mjmlValidator.js"],"sourcesContent":["// @ts-check\n/**\n * URLs accepted in an email template.\n *\n * link (`href`) https, http, mailto:, tel:, `#…` anchor,\n * or a `{{ … }}` merge tag (unsubscribe,\n * variable) replaced at render time;\n * image (`src`, `background-url`…) https only: an http image is blocked or\n * flagged by most email clients, and\n * `data:` bloats the message until Gmail\n * clips it. Or an UPLOADED image (asset\n * path, see `isAssetPath`), made absolute\n * at render time (renderer/links.js).\n *\n * Everything else (`javascript:`, `data:`, `vbscript:`, `file:`…) is rejected.\n * The scheme is read AFTER decoding entities and stripping whitespace and\n * control characters: `java&#x09;script:` and `&#106;avascript:` are classics.\n */\nimport { isAssetPath } from '../renderer/links.js';\n\n/** Attributes carrying a LINK URL. */\nexport const LINK_ATTRIBUTES = new Set(['href', 'action', 'formaction', 'xlink:href']);\n\n/** Attributes carrying an IMAGE URL (MJML and HTML). */\nexport const IMAGE_ATTRIBUTES = new Set([\n 'src',\n 'srcset',\n 'background-url',\n 'background',\n 'poster',\n 'icon-unwrapped-url',\n 'icon-wrapped-url',\n]);\n\nconst NAMED_ENTITIES = {\n colon: ':',\n tab: '\\t',\n newline: '\\n',\n lpar: '(',\n rpar: ')',\n amp: '&',\n quot: '\"',\n apos: \"'\",\n lt: '<',\n gt: '>',\n nbsp: ' ',\n};\n\n/** Decodes the HTML entities of an attribute value. @param {string} value */\nexport function decodeEntities(value) {\n return String(value ?? '').replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);?/gi, (whole, body) => {\n const key = String(body).toLowerCase();\n if (key.startsWith('#x')) return String.fromCodePoint(Number.parseInt(key.slice(2), 16) || 0);\n if (key.startsWith('#')) return String.fromCodePoint(Number.parseInt(key.slice(1), 10) || 0);\n return NAMED_ENTITIES[key] ?? whole;\n });\n}\n\n/** A `{{ … }}` merge tag (variable, unsubscribe, web version). @param {string} value */\nexport function isMergeTag(value) {\n return /^\\{\\{\\s*[a-zA-Z][\\w.]*\\s*\\}\\}$/.test(String(value || '').trim());\n}\n\n/**\n * @param {string} raw\n * @param {'link'|'image'} kind\n * @returns {boolean}\n */\nexport function isSafeUrl(raw, kind) {\n const value = decodeEntities(raw).trim();\n if (!value) return true;\n if (isMergeTag(value)) return true;\n // Scheme read without whitespace or control characters (\"java\\tscript:\").\n // eslint-disable-next-line no-control-regex -- control characters are exactly what is stripped\n const compact = value.replace(/[\\s\\u0000-\\u001f\\u007f]+/g, '').toLowerCase();\n if (kind === 'image') return compact.startsWith('https://') || isAssetPath(value);\n if (compact.startsWith('#')) return true;\n return /^(https?:\\/\\/|mailto:|tel:)/.test(compact);\n}\n","// @ts-check\n/**\n * What an MJML document may contain before the server compiles it.\n *\n * The MJML comes from the browser (editor export). The server does not take it\n * at face value: it compiles it, stores it and will send it to recipients.\n * Three families of rejection:\n *\n * 1. tags that would take the template out of bounds: `mj-include` (would\n * read a server FILE at compile time), `mj-raw` (free HTML, without any of\n * MJML's guarantees), `mj-style` / `mj-font` (external CSS and fonts,\n * outside the builder's scope);\n * 2. anything that executes or embeds: `<script>`, `<iframe>`, `<object>`,\n * `<form>`, `on*` attributes, CSS `expression()`;\n * 3. dangerous URLs (see `urls.js`).\n *\n * Only tags from two closed lists pass: the MJML components produced by the\n * blocks, and the formatting HTML written by the rich text editor inside an\n * `mj-text` or `mj-button`.\n *\n * Dependency-free module (no parser): it can be tested outside a browser, and\n * runs before compilation, which alone truly parses the MJML.\n */\n\nimport { identity } from '../i18n/index.js';\nimport { IMAGE_ATTRIBUTES, LINK_ATTRIBUTES, decodeEntities, isSafeUrl } from './urls.js';\n\n/** Maximum MJML size (UTF-16 units, approximated by the string length). */\nexport const MJML_MAX_LENGTH = 512 * 1024;\n\n/** Accepted MJML components. */\nexport const MJML_TAGS = new Set([\n 'mjml',\n 'mj-head',\n 'mj-body',\n 'mj-attributes',\n 'mj-all',\n 'mj-class',\n 'mj-preview',\n 'mj-title',\n 'mj-breakpoint',\n 'mj-wrapper',\n 'mj-section',\n 'mj-group',\n 'mj-column',\n 'mj-text',\n 'mj-image',\n 'mj-button',\n 'mj-divider',\n 'mj-spacer',\n 'mj-table',\n 'mj-social',\n 'mj-social-element',\n 'mj-navbar',\n 'mj-navbar-link',\n 'mj-hero',\n]);\n\n/** Formatting HTML accepted inside a text or a button. */\nexport const INLINE_TAGS = new Set([\n 'a',\n 'b',\n 'strong',\n 'i',\n 'em',\n 'u',\n 's',\n 'strike',\n 'del',\n 'br',\n 'span',\n 'p',\n 'div',\n 'font',\n 'sup',\n 'sub',\n 'small',\n 'big',\n 'ul',\n 'ol',\n 'li',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'blockquote',\n 'code',\n 'pre',\n 'hr',\n 'img',\n 'table',\n 'tbody',\n 'thead',\n 'tr',\n 'td',\n 'th',\n]);\n\n/**\n * Tags rejected by name (explicit message rather than \"tag not allowed\").\n * Values are English message templates, translated when the issue is built.\n */\nconst FORBIDDEN_TAGS = new Map([\n ['mj-include', 'mj-include would read a file from the server'],\n ['mj-raw', 'mj-raw (free HTML) is not allowed'],\n ['mj-style', 'mj-style (free CSS) is outside the builder'],\n ['mj-font', 'mj-font (external font) is outside the builder'],\n ['script', 'script not allowed'],\n ['iframe', 'iframe not allowed'],\n ['frame', 'frame not allowed'],\n ['object', 'object not allowed'],\n ['embed', 'embed not allowed'],\n ['form', 'form not allowed'],\n ['input', 'form field not allowed'],\n ['button', 'HTML button not allowed (use the Button block)'],\n ['textarea', 'form field not allowed'],\n ['select', 'form field not allowed'],\n ['style', 'style sheet not allowed'],\n ['link', 'external resource not allowed'],\n ['meta', 'meta tag not allowed'],\n ['base', 'base tag not allowed'],\n ['svg', 'SVG not allowed'],\n ['math', 'MathML not allowed'],\n]);\n\n/**\n * @typedef {{code: string, message: string, tag?: string, attribute?: string}} ValidationIssue\n */\n\nconst TAG_RE = /<\\s*(\\/?)\\s*([a-zA-Z][\\w:-]*)([^>]*)>/g;\nconst ATTR_RE = /([^\\s=/>\"']+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'>]+)))?/g;\n\n/** @param {string} source */\nfunction* attributesOf(source) {\n ATTR_RE.lastIndex = 0;\n let match;\n while ((match = ATTR_RE.exec(source))) {\n const value = match[2] ?? match[3] ?? match[4] ?? '';\n yield { name: match[1].toLowerCase(), value };\n }\n}\n\n/**\n * Inline style check: no `expression()`, no non-https `url()`.\n * @param {string} style\n */\nfunction styleIsSafe(style) {\n const css = decodeEntities(style).toLowerCase().replace(/\\s+/g, '');\n if (css.includes('expression(') || css.includes('javascript:') || css.includes('behavior:')) return false;\n const urls = css.match(/url\\(([^)]*)\\)/g) || [];\n return urls.every((u) => isSafeUrl(u.slice(4, -1).replace(/^['\"]|['\"]$/g, ''), 'image'));\n}\n\n/**\n * Validates an MJML document. Does not throw: returns the list of issues.\n *\n * @param {string} mjml\n * @param {{t?: import('../i18n/index.js').Translate}} [options]\n * @returns {{ok: boolean, issues: ValidationIssue[]}}\n */\nexport function validateMjml(mjml, { t = identity } = {}) {\n /** @type {ValidationIssue[]} */\n const issues = [];\n const source = typeof mjml === 'string' ? mjml : '';\n\n if (!source.trim()) {\n return { ok: false, issues: [{ code: 'EMPTY', message: t('Empty MJML') }] };\n }\n if (source.length > MJML_MAX_LENGTH) {\n issues.push({\n code: 'TOO_LARGE',\n message: t('MJML too large ({size} KB, maximum {max} KB)', {\n size: Math.round(source.length / 1024),\n max: MJML_MAX_LENGTH / 1024,\n }),\n });\n }\n\n // Comments are not parsed as tags, but an Outlook conditional comment\n // (`<!--[if mso]>`) has no business in MJML written by the builder.\n const withoutComments = source.replace(/<!--([\\s\\S]*?)-->/g, (whole, body) => {\n if (/\\[if\\s/i.test(body)) {\n issues.push({ code: 'CONDITIONAL_COMMENT', message: t('conditional comment not allowed') });\n }\n return '';\n });\n if (/<!\\[CDATA\\[/i.test(withoutComments)) {\n issues.push({ code: 'CDATA', message: t('CDATA section not allowed') });\n }\n\n const trimmed = withoutComments.trim();\n if (!/^<mjml[\\s>]/i.test(trimmed)) {\n issues.push({ code: 'ROOT', message: t('the document must start with <mjml>') });\n }\n if (!/<mj-body[\\s>]/i.test(trimmed)) {\n issues.push({ code: 'BODY', message: t('missing <mj-body>') });\n }\n\n TAG_RE.lastIndex = 0;\n let match;\n while ((match = TAG_RE.exec(withoutComments))) {\n const closing = match[1] === '/';\n const tag = match[2].toLowerCase();\n if (FORBIDDEN_TAGS.has(tag)) {\n const template = FORBIDDEN_TAGS.get(tag);\n issues.push({ code: 'FORBIDDEN_TAG', message: template ? t(template) : tag, tag });\n continue;\n }\n if (!MJML_TAGS.has(tag) && !INLINE_TAGS.has(tag)) {\n issues.push({ code: 'UNKNOWN_TAG', message: t('tag not allowed: <{tag}>', { tag }), tag });\n continue;\n }\n if (closing) continue;\n\n for (const { name, value } of attributesOf(match[3])) {\n if (name.startsWith('on')) {\n issues.push({\n code: 'EVENT_ATTRIBUTE',\n message: t('attribute {name} not allowed', { name }),\n tag,\n attribute: name,\n });\n } else if (LINK_ATTRIBUTES.has(name) && !isSafeUrl(value, 'link')) {\n issues.push({ code: 'UNSAFE_URL', message: t('URL rejected ({name})', { name }), tag, attribute: name });\n } else if (IMAGE_ATTRIBUTES.has(name) && !isSafeUrl(value, 'image')) {\n issues.push({\n code: 'UNSAFE_IMAGE',\n message: t('image rejected ({name}): https only', { name }),\n tag,\n attribute: name,\n });\n } else if (name === 'style' && !styleIsSafe(value)) {\n issues.push({ code: 'UNSAFE_STYLE', message: t('inline style rejected'), tag, attribute: name });\n }\n }\n }\n\n return { ok: issues.length === 0, issues };\n}\n\n/**\n * Validates and throws a readable 422 error listing the first rejections.\n * @param {string} mjml\n * @param {{t?: import('../i18n/index.js').Translate}} [options]\n */\nexport function assertValidMjml(mjml, { t = identity } = {}) {\n const { ok, issues } = validateMjml(mjml, { t });\n if (ok) return;\n const shown = issues.slice(0, 3).map((i) => i.message).join('; ');\n const more = issues.length - 3;\n const error = /** @type {Error & {status?: number, issues?: ValidationIssue[]}} */ (\n new Error(\n more > 0\n ? t('Template rejected: {issues} (+{more})', { issues: shown, more })\n : t('Template rejected: {issues}', { issues: shown })\n )\n );\n error.status = 422;\n error.issues = issues;\n throw error;\n}\n"],"mappings":"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","names":[]}
@@ -1,18 +1,28 @@
1
1
  import {
2
- Button,
3
2
  EmailPreview,
3
+ TEMPLATE_GALLERY,
4
+ buildGallery,
5
+ useBrand,
6
+ useLayoutWidth,
7
+ withPlaceholderImages
8
+ } from "./chunk-3LKF4QBL.js";
9
+ import {
10
+ Button,
4
11
  Input,
5
12
  NoticeArea,
13
+ SEARCH_KEYS,
14
+ SearchKeyHint,
6
15
  SectionLabel,
7
- TEMPLATE_GALLERY,
8
16
  UiProvider,
9
- buildGallery,
10
17
  cn,
11
18
  createHttpStorage,
12
- useLayoutWidth,
13
- useUi,
14
- withPlaceholderImages
15
- } from "./chunk-ZSUHOCCE.js";
19
+ useSearchShortcut,
20
+ useUi
21
+ } from "./chunk-LXR4QTGP.js";
22
+ import {
23
+ brandTemplate,
24
+ brandedSender
25
+ } from "./chunk-DFNHICTF.js";
16
26
 
17
27
  // src/ui/library/TemplateLibrary.jsx
18
28
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -123,9 +133,13 @@ function Library({
123
133
  }) {
124
134
  const { t, locale, notify, confirm } = useUi();
125
135
  const storage = useMemo(() => injectedStorage || createHttpStorage(), [injectedStorage]);
136
+ const { brand } = useBrand(storage);
126
137
  const bases = useMemo(
127
- () => buildGallery({ templates: galleryProp ?? TEMPLATE_GALLERY, sender: sender || {}, t }),
128
- [galleryProp, sender, t]
138
+ () => buildGallery({ templates: galleryProp ?? TEMPLATE_GALLERY, sender: brandedSender(sender, brand), t }).map((base2) => ({
139
+ ...base2,
140
+ mjml: brandTemplate(base2.mjml, brand, { sender, t })
141
+ })),
142
+ [galleryProp, sender, brand, t]
129
143
  );
130
144
  const dateFormat = useMemo(() => new Intl.DateTimeFormat(locale, DATE_FORMAT), [locale]);
131
145
  const formatDate = useCallback((at) => at ? dateFormat.format(new Date(at)) : "", [dateFormat]);
@@ -197,16 +211,7 @@ function Library({
197
211
  if (items.list.length) setSelection(`template:${items.list[0].id}`);
198
212
  else if (base2) setSelection(`base:${base2.id}`);
199
213
  }, [items, selection, bases]);
200
- useEffect(() => {
201
- const onKey = (event) => {
202
- if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k" && rootRef.current?.isConnected) {
203
- event.preventDefault();
204
- searchRef.current?.focus();
205
- }
206
- };
207
- window.addEventListener("keydown", onKey);
208
- return () => window.removeEventListener("keydown", onKey);
209
- }, []);
214
+ useSearchShortcut(searchRef);
210
215
  const selected = templateEntries.find((e) => e.key === selection) || baseEntries.find((e) => e.key === selection) || null;
211
216
  const base = selected?.kind === "base" ? selected.base : null;
212
217
  const templateId = selection?.startsWith("template:") ? selection.slice("template:".length) : null;
@@ -324,20 +329,13 @@ function Library({
324
329
  },
325
330
  placeholder: t("Search templates"),
326
331
  "aria-label": t("Search templates"),
327
- "aria-keyshortcuts": "Control+K",
332
+ "aria-keyshortcuts": SEARCH_KEYS,
328
333
  className: "eb:pr-16 eb:pl-8 eb:[&::-webkit-search-cancel-button]:hidden"
329
334
  }
330
335
  ),
331
336
  /* @__PURE__ */ jsxs("div", { className: "eb:absolute eb:top-1/2 eb:right-2 eb:flex eb:-translate-y-1/2 eb:items-center eb:gap-1", children: [
332
337
  query && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setQuery(""), "aria-label": t("Clear the search"), className: "eb:cursor-pointer", children: /* @__PURE__ */ jsx(X, { className: "eb:size-3.5 eb:text-foreground-muted eb:hover:text-foreground" }) }),
333
- /* @__PURE__ */ jsx(
334
- "kbd",
335
- {
336
- className: "eb:inline-flex eb:h-5 eb:items-center eb:rounded eb:border eb:border-border eb:bg-surface-100 eb:px-1.5 eb:font-mono eb:text-[10px] eb:text-foreground-muted",
337
- "aria-hidden": "true",
338
- children: query ? "Esc" : "Ctrl K"
339
- }
340
- )
338
+ /* @__PURE__ */ jsx(SearchKeyHint, { children: query ? "Esc" : null })
341
339
  ] })
342
340
  ] })
343
341
  ] }),
@@ -526,4 +524,4 @@ function TemplateLibrary({ locale = "en", messages, onNotify, ...props }) {
526
524
  export {
527
525
  TemplateLibrary
528
526
  };
529
- //# sourceMappingURL=chunk-JOC752I3.js.map
527
+ //# sourceMappingURL=chunk-6D3SLT5W.js.map