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/ui/library/TemplateLibrary.jsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n AlertCircle,\n ArrowLeft,\n Copy,\n Download,\n LayoutTemplate,\n Loader2,\n PencilRuler,\n Plus,\n RefreshCw,\n Search,\n Star,\n Trash2,\n X,\n} from 'lucide-react';\nimport { TEMPLATE_GALLERY, buildGallery, withPlaceholderImages } from '../../features/templates/gallery.js';\nimport { brandTemplate, brandedSender } from '../../features/brand/brand.js';\nimport { useBrand } from '../../features/brand/useBrand.js';\nimport { EmailPreview } from '../../features/preview/PreviewButton.jsx';\nimport { createHttpStorage } from '../../storage/client.js';\nimport { NoticeArea, UiProvider, useUi } from '../context.jsx';\nimport { Button } from '../primitives/button.jsx';\nimport { cn } from '../primitives/cn.js';\nimport { Input } from '../primitives/fields.jsx';\nimport { SectionLabel } from '../primitives/misc.jsx';\nimport { SEARCH_KEYS, SearchKeyHint, useSearchShortcut } from '../primitives/searchShortcut.jsx';\nimport { useLayoutWidth } from '../useLayoutWidth.js';\n\n/**\n * TEMPLATE LIBRARY — the first screen: pick a template, see it, open it.\n *\n * ┌──────────────────────────────────────────────────────────────────┐\n * │ ▣ Email template library × │\n * ├──────────────┬──────────────────────────┬────────────────────────┤\n * │ New template │ Name ☆ │ Desktop | Mobile │\n * │ 🔍 Ctrl K │ code │ ┌────────────────────┐ │\n * │ FAVORITES │ Origin, subject, dates… │ │ rendered email │ │\n * │ TEMPLATES │ │ │ │ │\n * │ BASES │ [ Edit ] [ Duplicate ] │ └────────────────────┘ │\n * └──────────────┴──────────────────────────┴────────────────────────┘\n *\n * Standalone: it never mounts the editor. \"Edit\", \"New template\" and \"Start\n * from this base\" only call `onEdit(template)` / `onCreate(start)`, and the\n * host decides what opens — `EmailBuilder` (see `EmailStudio`, which wires\n * the two), its own screen, a route. Likewise `EmailBuilder` works without it.\n *\n * Templates `storage.list()`; preview through `storage.render()` when the\n * storage has it, else the HTML compiled at the last save.\n * Bases the gallery (`templates`, `[]` hides them): read-only, \"Start\n * from this base\" calls `onCreate({ baseId, mjml, meta })`.\n * Favorites the star of the details. A favorite leaves its section for the\n * one at the top. Kept in this browser (`favoritesKey`, `null`\n * turns favorites off).\n *\n * Layout follows the library's own width, like the builder: three columns,\n * then the preview under the details, then (phone) list and details as two\n * screens.\n */\n\nconst DATE_FORMAT = { day: '2-digit', month: '2-digit', year: 'numeric' };\nconst DEFAULT_FAVORITES_KEY = 'best-email-builder:favorites';\n\nconst matches = (query, ...fields) => {\n const q = query.trim().toLowerCase();\n return !q || fields.some((f) => String(f || '').toLowerCase().includes(q));\n};\n\n/** `October newsletter` → `october_newsletter`: the code line under a name. */\nconst codeOf = (name, fallback) =>\n String(name || '')\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 48) || fallback;\n\n/** Keys of the starred entries: `template:<id>` or `base:<id>`. */\nfunction useFavorites(storageKey) {\n const [favorites, setFavorites] = useState(() => {\n if (!storageKey) return new Set();\n try {\n return new Set(JSON.parse(localStorage.getItem(storageKey) || '[]'));\n } catch {\n return new Set();\n }\n });\n const toggle = useCallback(\n (key) =>\n setFavorites((current) => {\n const next = new Set(current);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n try {\n localStorage.setItem(storageKey, JSON.stringify([...next]));\n } catch {\n // Private window, blocked storage: the star holds until the page closes.\n }\n return next;\n }),\n [storageKey]\n );\n return { enabled: Boolean(storageKey), favorites, toggle };\n}\n\nfunction download(name, html) {\n const blob = new Blob([html], { type: 'text/html;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `${codeOf(name, 'email')}.html`;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction Badge({ children }) {\n return (\n <span className=\"eb:inline-flex eb:w-fit eb:shrink-0 eb:items-center eb:rounded-md eb:border eb:border-border eb:px-1.5 eb:py-0 eb:text-[10px] eb:whitespace-nowrap eb:text-foreground\">\n {children}\n </span>\n );\n}\n\nfunction Row({ entry, selected, favorite, onSelect, testId }) {\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n aria-current={selected ? 'true' : undefined}\n data-testid={testId}\n className={cn(\n 'eb:w-full eb:rounded-lg2 eb:border eb:px-2.5 eb:py-2 eb:text-left eb:transition-colors eb:outline-none',\n 'eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50',\n selected ? 'eb:border-primary/50 eb:bg-primary/5' : 'eb:border-transparent eb:hover:bg-surface-75'\n )}\n >\n <span className=\"eb:flex eb:items-center eb:gap-1.5\">\n <span className=\"eb:min-w-0 eb:flex-1 eb:truncate eb:text-[13px] eb:text-foreground\">{entry.name}</span>\n {favorite && <Star className=\"eb:size-3.5 eb:shrink-0 eb:fill-current eb:text-warning\" aria-hidden=\"true\" />}\n </span>\n <span className=\"eb:mt-0.5 eb:block eb:truncate eb:font-mono eb:text-[11px] eb:text-foreground-lighter\">{entry.code}</span>\n <span className=\"eb:mt-1 eb:flex eb:flex-wrap eb:items-center eb:gap-1\">\n <Badge>{entry.badge}</Badge>\n {entry.date && <span className=\"eb:ml-auto eb:text-[10.5px] eb:text-foreground-muted\">{entry.date}</span>}\n </span>\n </button>\n );\n}\n\nfunction Section({ label, children, ...props }) {\n return (\n <div className=\"eb:flex eb:flex-col eb:gap-1.5\" {...props}>\n <SectionLabel>{label}</SectionLabel>\n {children}\n </div>\n );\n}\n\nfunction Library({\n storage: injectedStorage,\n sender,\n templates: galleryProp,\n title,\n description,\n selectedId = null,\n favoritesKey = DEFAULT_FAVORITES_KEY,\n onEdit,\n onCreate,\n onClose,\n className,\n}) {\n const { t, locale, notify, confirm } = useUi();\n const storage = useMemo(() => injectedStorage || createHttpStorage(), [injectedStorage]);\n const { brand } = useBrand(storage);\n const bases = useMemo(\n () =>\n buildGallery({ templates: galleryProp ?? TEMPLATE_GALLERY, sender: brandedSender(sender, brand), t }).map((base) => ({\n ...base,\n mjml: brandTemplate(base.mjml, brand, { sender, t }),\n })),\n [galleryProp, sender, brand, t]\n );\n const dateFormat = useMemo(() => new Intl.DateTimeFormat(locale, DATE_FORMAT), [locale]);\n const formatDate = useCallback((at) => (at ? dateFormat.format(new Date(at)) : ''), [dateFormat]);\n const stars = useFavorites(favoritesKey);\n\n const rootRef = useRef(null);\n const searchRef = useRef(null);\n const layout = useLayoutWidth(rootRef);\n\n const [items, setItems] = useState({ status: 'loading', list: [], error: null });\n const [query, setQuery] = useState('');\n /** `template:<id>` or `base:<id>` */\n const [selection, setSelection] = useState(selectedId != null ? `template:${selectedId}` : null);\n /** Phone layout: the details screen is shown over the list. */\n const [detailsOpen, setDetailsOpen] = useState(false);\n const [detail, setDetail] = useState({ status: 'idle', template: null, error: null });\n const [preview, setPreview] = useState({ html: '', loading: false, error: null });\n const [device, setDevice] = useState('desktop');\n const [busy, setBusy] = useState(null);\n\n // Only the LAST read counts: a slow answer arriving after a newer one\n // (opening, then a duplicate) would show a stale list.\n const lastRead = useRef(0);\n const reload = useCallback(async () => {\n const ticket = ++lastRead.current;\n if (typeof storage.list !== 'function') {\n setItems({ status: 'ready', list: [], error: null });\n return;\n }\n setItems((s) => ({ ...s, status: s.list.length ? 'ready' : 'loading', error: null }));\n try {\n const list = await storage.list();\n if (ticket === lastRead.current) setItems({ status: 'ready', list, error: null });\n } catch (error) {\n if (ticket === lastRead.current) setItems({ status: 'error', list: [], error: error.message });\n }\n }, [storage]);\n\n useEffect(() => {\n reload();\n }, [reload]);\n\n /* ── entries: one shape for templates and bases ────────────────────────── */\n\n const templateEntries = useMemo(\n () =>\n items.list.map((item) => ({\n key: `template:${item.id}`,\n kind: 'template',\n id: item.id,\n name: item.name,\n code: codeOf(item.name, `#${item.id}`),\n badge: t('Builder'),\n date: formatDate(item.updatedAt || item.createdAt),\n subject: item.subject,\n item,\n })),\n [items.list, t, formatDate]\n );\n const baseEntries = useMemo(\n () =>\n bases.map((base) => ({\n key: `base:${base.id}`,\n kind: 'base',\n id: base.id,\n name: base.name,\n code: `base_${base.id}`,\n badge: t('Base'),\n date: '',\n subject: base.subject,\n base,\n })),\n [bases, t]\n );\n const shown = (entries) => entries.filter((e) => matches(query, e.name, e.code, e.subject, e.base?.description));\n const favoriteEntries = shown([...templateEntries, ...baseEntries]).filter((e) => stars.favorites.has(e.key));\n const otherTemplates = shown(templateEntries).filter((e) => !stars.favorites.has(e.key));\n const otherBases = shown(baseEntries).filter((e) => !stars.favorites.has(e.key));\n\n // First selection: the requested template, else the most recent one, else\n // the first base with something to show (a blank page previews as nothing).\n useEffect(() => {\n if (items.status !== 'ready' || selection) return;\n const base = bases.find((b) => b.mjml.includes('<mj-section')) || bases[0];\n if (items.list.length) setSelection(`template:${items.list[0].id}`);\n else if (base) setSelection(`base:${base.id}`);\n }, [items, selection, bases]);\n\n // Ctrl/Cmd+K focuses the search.\n useSearchShortcut(searchRef);\n\n const selected =\n templateEntries.find((e) => e.key === selection) || baseEntries.find((e) => e.key === selection) || null;\n const base = selected?.kind === 'base' ? selected.base : null;\n const templateId = selection?.startsWith('template:') ? selection.slice('template:'.length) : null;\n\n // Details of a saved template (MJML and compiled HTML are not in the list).\n useEffect(() => {\n if (templateId == null) {\n setDetail({ status: 'idle', template: null, error: null });\n return undefined;\n }\n let live = true;\n setDetail((d) => ({ status: 'loading', template: String(d.template?.id) === templateId ? d.template : null, error: null }));\n storage.load(templateId).then(\n (template) => live && setDetail({ status: 'ready', template, error: null }),\n (error) => live && setDetail({ status: 'error', template: null, error: error.message })\n );\n return () => {\n live = false;\n };\n }, [templateId, storage]);\n\n // Preview: rendered by the storage (server) when it can, else the saved HTML.\n const template = detail.template;\n const source = base\n ? { mjml: base.mjml, subject: base.subject, preheader: base.preheader, html: '' }\n : template\n ? { mjml: template.mjml, subject: template.subject, preheader: template.preheader, html: template.html || '' }\n : null;\n const sourceKey = base ? `base:${base.id}:${locale}` : template ? `template:${template.id}:${template.revision}` : '';\n useEffect(() => {\n if (!source) {\n setPreview({ html: '', loading: templateId != null && detail.status === 'loading', error: null });\n return undefined;\n }\n if (typeof storage.render !== 'function' || !source.mjml) {\n setPreview({ html: source.html, loading: false, error: source.html ? null : t('No preview: this storage cannot render emails.') });\n return undefined;\n }\n let live = true;\n setPreview((p) => ({ ...p, loading: true, error: null }));\n storage.render({ mjml: source.mjml, subject: source.subject, preheader: source.preheader }).then(\n (result) => live && setPreview({ html: withPlaceholderImages(result.html), loading: false, error: null }),\n (error) => live && setPreview({ html: source.html, loading: false, error: source.html ? null : error.message })\n );\n return () => {\n live = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sourceKey, storage]);\n\n /* ── actions ───────────────────────────────────────────────────────────── */\n\n const select = (key) => {\n setSelection(key);\n setDetailsOpen(true);\n };\n\n const remove = async () => {\n const ok = await confirm({\n title: t('Delete “{name}”?', { name: template.name }),\n description: t('The template and its versions will be deleted.'),\n confirmLabel: t('Delete'),\n destructive: true,\n });\n if (!ok) return;\n setBusy('delete');\n try {\n await storage.remove(template.id);\n setSelection(null);\n setDetailsOpen(false);\n await reload();\n notify('success', t('Template deleted.'));\n } catch (error) {\n notify('error', error.message);\n } finally {\n setBusy(null);\n }\n };\n\n const duplicate = async () => {\n setBusy('duplicate');\n try {\n const { template: copy } = await storage.duplicate(template.id);\n await reload();\n setSelection(`template:${copy.id}`);\n notify('success', t('Template duplicated.'));\n } catch (error) {\n notify('error', error.message);\n } finally {\n setBusy(null);\n }\n };\n\n const copyHtml = async () => {\n try {\n await navigator.clipboard.writeText(template.html);\n notify('success', t('HTML copied.'));\n } catch {\n notify('error', t('The browser refused to copy.'));\n }\n };\n\n const canList = typeof storage.list === 'function';\n const rowProps = (entry) => ({\n entry,\n testId: entry.kind === 'base' ? 'eb-library-base' : 'eb-library-template',\n selected: entry.key === selection,\n favorite: stars.favorites.has(entry.key),\n onSelect: () => select(entry.key),\n });\n\n /* ── left column ───────────────────────────────────────────────────────── */\n\n const listColumn = (\n <div key=\"list\" className=\"eb:flex eb:min-h-0 eb:min-w-0 eb:flex-col\" data-testid=\"eb-library-list\">\n <div className=\"eb:flex eb:shrink-0 eb:flex-col eb:gap-2 eb:border-b eb:border-border eb:p-3\">\n {onCreate && (\n <Button className=\"eb:w-full\" onClick={() => onCreate(null)}>\n <PencilRuler />\n {t('New template')}\n </Button>\n )}\n <div className=\"eb:relative\">\n <Search className=\"eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2.5 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted\" />\n <Input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape' && query) {\n e.preventDefault();\n setQuery('');\n }\n }}\n placeholder={t('Search templates')}\n aria-label={t('Search templates')}\n aria-keyshortcuts={SEARCH_KEYS}\n className=\"eb:pr-16 eb:pl-8 eb:[&::-webkit-search-cancel-button]:hidden\"\n />\n <div className=\"eb:absolute eb:top-1/2 eb:right-2 eb:flex eb:-translate-y-1/2 eb:items-center eb:gap-1\">\n {query && (\n <button type=\"button\" onClick={() => setQuery('')} aria-label={t('Clear the search')} className=\"eb:cursor-pointer\">\n <X className=\"eb:size-3.5 eb:text-foreground-muted eb:hover:text-foreground\" />\n </button>\n )}\n <SearchKeyHint>{query ? 'Esc' : null}</SearchKeyHint>\n </div>\n </div>\n </div>\n\n <div className=\"eb:flex eb:min-h-0 eb:flex-1 eb:flex-col eb:gap-4 eb:overflow-y-auto eb:p-3\">\n {stars.enabled && favoriteEntries.length > 0 && (\n <Section label={t('Favorites')} role=\"group\" aria-label={t('Favorites')} data-testid=\"eb-library-favorites\">\n {favoriteEntries.map((entry) => (\n <Row key={entry.key} {...rowProps(entry)} />\n ))}\n </Section>\n )}\n {canList && (\n <Section label={`${t('Templates')} (${otherTemplates.length})`} data-testid=\"eb-library-templates\">\n {items.status === 'loading' ? (\n <p className=\"eb:m-0 eb:flex eb:items-center eb:gap-2 eb:px-1 eb:text-[12px] eb:text-foreground-lighter\">\n <Loader2 className=\"eb:size-3.5 eb:animate-spin\" />\n {t('Loading…')}\n </p>\n ) : items.status === 'error' ? (\n <div className=\"eb:px-1 eb:text-[12px] eb:text-destructive\">\n <p className=\"eb:m-0\">{t('Could not load the templates.')}</p>\n <p className=\"eb:m-0 eb:mt-0.5 eb:text-foreground-muted\">{items.error}</p>\n <Button variant=\"outline\" size=\"sm-compact\" className=\"eb:mt-2\" onClick={reload}>\n <RefreshCw />\n {t('Retry')}\n </Button>\n </div>\n ) : otherTemplates.length === 0 ? (\n <p className=\"eb:m-0 eb:px-1 eb:text-[12px] eb:leading-relaxed eb:text-foreground-lighter\">\n {query\n ? t('Nothing matches “{query}”.', { query })\n : templateEntries.length\n ? t('All your templates are in the favorites.')\n : t('No template yet. Start from a base, or create a new one.')}\n </p>\n ) : (\n otherTemplates.map((entry) => <Row key={entry.key} {...rowProps(entry)} />)\n )}\n </Section>\n )}\n {otherBases.length > 0 && (\n <Section label={`${t('Bases')} (${otherBases.length})`} data-testid=\"eb-library-bases\">\n {otherBases.map((entry) => (\n <Row key={entry.key} {...rowProps(entry)} />\n ))}\n </Section>\n )}\n </div>\n </div>\n );\n\n /* ── details ───────────────────────────────────────────────────────────── */\n\n const meta = base\n ? { name: base.name, code: `base_${base.id}`, description: base.description, origin: t('Base'), subject: base.subject, preheader: base.preheader }\n : selected || template\n ? {\n name: template?.name ?? selected.name,\n code: selected?.code ?? codeOf(template?.name, `#${templateId}`),\n description: template?.description ?? selected?.item?.description,\n origin: t('Builder'),\n subject: (template ?? selected?.item)?.subject,\n preheader: (template ?? selected?.item)?.preheader,\n createdAt: (template ?? selected?.item)?.createdAt,\n createdBy: (template ?? selected?.item)?.createdBy,\n updatedAt: (template ?? selected?.item)?.updatedAt,\n updatedBy: (template ?? selected?.item)?.updatedBy,\n }\n : null;\n const favorite = selection ? stars.favorites.has(selection) : false;\n\n const detailsBody =\n templateId != null && detail.status === 'error' ? (\n <p className=\"eb:m-0 eb:flex eb:items-start eb:gap-2 eb:text-[13px] eb:text-destructive\">\n <AlertCircle className=\"eb:mt-0.5 eb:size-4 eb:shrink-0\" />\n {detail.error}\n </p>\n ) : meta ? (\n <>\n <div className=\"eb:flex eb:flex-col eb:gap-1\">\n <div className=\"eb:flex eb:items-center eb:gap-2\">\n <h3 className=\"eb:m-0 eb:min-w-0 eb:flex-1 eb:text-[15px] eb:font-medium eb:text-foreground\">{meta.name}</h3>\n {stars.enabled && (\n <Button\n variant=\"ghost\"\n size=\"icon-sm-compact\"\n className={cn('eb:shrink-0', favorite ? 'eb:text-warning' : 'eb:text-foreground-muted eb:hover:text-warning')}\n onClick={() => stars.toggle(selection)}\n aria-pressed={favorite}\n aria-label={favorite ? t('Remove from favorites') : t('Add to favorites')}\n title={favorite ? t('Remove from favorites') : t('Add to favorites')}\n >\n <Star className={cn('eb:size-4', favorite && 'eb:fill-current')} />\n </Button>\n )}\n </div>\n <p className=\"eb:m-0 eb:font-mono eb:text-[11.5px] eb:text-foreground-lighter\">{meta.code}</p>\n {meta.description && (\n <p className=\"eb:m-0 eb:text-[12.5px] eb:leading-relaxed eb:text-foreground-lighter\">{meta.description}</p>\n )}\n </div>\n\n <dl className=\"eb:m-0 eb:grid eb:grid-cols-[110px_minmax(0,1fr)] eb:gap-x-3 eb:gap-y-1.5 eb:text-[12px]\">\n <dt className=\"eb:text-foreground-muted\">{t('Origin')}</dt>\n <dd className=\"eb:m-0 eb:text-foreground\">{meta.origin}</dd>\n <dt className=\"eb:text-foreground-muted\">{t('Subject')}</dt>\n <dd className=\"eb:m-0 eb:break-words eb:text-foreground\">{meta.subject || '—'}</dd>\n <dt className=\"eb:text-foreground-muted\">{t('Preview text')}</dt>\n <dd className=\"eb:m-0 eb:break-words eb:text-foreground-lighter\">{meta.preheader || '—'}</dd>\n {!base && (\n <>\n <dt className=\"eb:text-foreground-muted\">{t('Created')}</dt>\n <dd className=\"eb:m-0 eb:text-foreground\">\n {formatDate(meta.createdAt) || '—'}\n {meta.createdBy ? ` ${t('by {name}', { name: meta.createdBy })}` : ''}\n </dd>\n {meta.updatedAt && (\n <>\n <dt className=\"eb:text-foreground-muted\">{t('Updated')}</dt>\n <dd className=\"eb:m-0 eb:text-foreground\">\n {formatDate(meta.updatedAt)}\n {meta.updatedBy ? ` ${t('by {name}', { name: meta.updatedBy })}` : ''}\n </dd>\n </>\n )}\n </>\n )}\n </dl>\n\n {base && (\n <p className=\"eb:m-0 eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:p-2.5 eb:text-[12px] eb:leading-relaxed eb:text-foreground-lighter\">\n {t('Base — not editable: start from it to create your template.')}\n </p>\n )}\n </>\n ) : (\n <div className=\"eb:flex eb:flex-1 eb:items-center eb:justify-center\">\n <div className=\"eb:flex eb:w-full eb:flex-col eb:items-center eb:justify-center eb:gap-2 eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:px-6 eb:py-12 eb:text-center\">\n <span className=\"eb:flex eb:size-12 eb:items-center eb:justify-center eb:rounded-full eb:bg-surface-200 eb:text-foreground-muted\">\n <LayoutTemplate className=\"eb:size-6\" />\n </span>\n <p className=\"eb:m-0 eb:text-[13px] eb:font-medium eb:text-foreground\">{t('Choose a template')}</p>\n <p className=\"eb:m-0 eb:max-w-sm eb:text-[13px] eb:leading-relaxed eb:text-foreground-muted\">\n {t('Select a template on the left.')}\n </p>\n </div>\n </div>\n );\n\n const actions = base ? (\n onCreate && (\n <Button\n className=\"eb:w-full\"\n onClick={() => onCreate({ baseId: base.id, mjml: base.mjml, meta: { subject: base.subject, preheader: base.preheader } })}\n >\n <Plus />\n {t('Start from this base')}\n </Button>\n )\n ) : template ? (\n <>\n <div className=\"eb:flex eb:gap-2\">\n {onEdit && (\n <Button className=\"eb:flex-1\" onClick={() => onEdit(template)}>\n <PencilRuler />\n {t('Edit')}\n </Button>\n )}\n <Button variant=\"outline\" className=\"eb:flex-1\" disabled={!!busy} onClick={duplicate}>\n {busy === 'duplicate' ? <Loader2 className=\"eb:animate-spin\" /> : <Copy />}\n {t('Duplicate')}\n </Button>\n </div>\n {(template.html || typeof storage.remove === 'function') && (\n <div className=\"eb:flex eb:gap-2\">\n {template.html && (\n <>\n <Button variant=\"outline\" size=\"sm-compact\" className=\"eb:flex-1\" onClick={copyHtml}>\n <Copy />\n {t('Copy HTML')}\n </Button>\n <Button variant=\"outline\" size=\"sm-compact\" className=\"eb:flex-1\" onClick={() => download(template.name, template.html)}>\n <Download />\n {t('Download')}\n </Button>\n </>\n )}\n {typeof storage.remove === 'function' && (\n <Button\n variant=\"outline\"\n size=\"sm-compact\"\n className={cn('eb:text-destructive', !template.html && 'eb:ml-auto')}\n disabled={!!busy}\n onClick={remove}\n aria-label={t('Delete')}\n title={t('Delete')}\n >\n {busy === 'delete' ? <Loader2 className=\"eb:animate-spin\" /> : <Trash2 />}\n {!template.html && t('Delete')}\n </Button>\n )}\n </div>\n )}\n </>\n ) : null;\n\n const previewPane =\n preview.html || preview.loading || preview.error ? (\n <EmailPreview html={preview.html} loading={preview.loading} error={preview.error} device={device} onDeviceChange={setDevice} />\n ) : (\n <div className=\"eb:flex eb:h-full eb:min-h-40 eb:items-center eb:justify-center eb:rounded-lg2 eb:border eb:border-dashed eb:border-border eb:p-6 eb:text-center eb:text-[13px] eb:text-foreground-lighter\">\n {t('The template preview appears here.')}\n </div>\n );\n\n const detailsColumn = (\n <div key=\"details\" className=\"eb:flex eb:min-h-0 eb:min-w-0 eb:flex-col\" data-testid=\"eb-library-details\">\n {layout === 'narrow' && (\n <div className=\"eb:shrink-0 eb:border-b eb:border-border eb:px-2 eb:py-1.5\">\n <Button variant=\"ghost\" size=\"sm-compact\" onClick={() => setDetailsOpen(false)}>\n <ArrowLeft />\n {t('All templates')}\n </Button>\n </div>\n )}\n <div className=\"eb:flex eb:min-h-0 eb:flex-1 eb:flex-col eb:gap-4 eb:overflow-y-auto eb:p-4\">\n {detailsBody}\n {layout !== 'wide' && meta && <div className=\"eb:h-[560px] eb:shrink-0\">{previewPane}</div>}\n </div>\n {actions && <div className=\"eb:flex eb:shrink-0 eb:flex-col eb:gap-2 eb:border-t eb:border-border eb:p-3\">{actions}</div>}\n </div>\n );\n\n return (\n <div\n ref={rootRef}\n className={cn('eb-root eb:relative eb:flex eb:h-full eb:min-h-0 eb:flex-col eb:bg-background eb:text-foreground', className)}\n data-testid=\"template-library\"\n data-layout={layout}\n >\n <header className=\"eb:flex eb:shrink-0 eb:items-center eb:gap-3 eb:border-b eb:border-border eb:px-5 eb:py-3.5\">\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 <LayoutTemplate className=\"eb:size-5\" />\n </span>\n <div className=\"eb:min-w-0 eb:flex-1\">\n <h2 className=\"eb:m-0 eb:truncate eb:text-base eb:font-normal eb:text-foreground\">{title ?? t('Email template library')}</h2>\n <p className=\"eb:m-0 eb:truncate eb:text-[13px] eb:text-foreground-muted\">\n {description ?? t('Ready-made templates to copy into your campaigns.')}\n </p>\n </div>\n {onClose && (\n <Button variant=\"ghost\" size=\"icon-sm\" onClick={onClose} aria-label={t('Close')}>\n <X />\n </Button>\n )}\n </header>\n\n {layout === 'narrow' ? (\n <div className=\"eb:grid eb:min-h-0 eb:flex-1 eb:grid-cols-[minmax(0,1fr)]\">\n {detailsOpen && selection ? detailsColumn : listColumn}\n </div>\n ) : (\n <div\n className={cn(\n 'eb:grid eb:min-h-0 eb:flex-1 eb:grid-rows-1 eb:divide-x eb:divide-border eb:overflow-hidden',\n layout === 'wide' ? 'eb:grid-cols-[290px_minmax(0,1fr)_minmax(0,1fr)]' : 'eb:grid-cols-[260px_minmax(0,1fr)]'\n )}\n >\n {listColumn}\n {detailsColumn}\n {layout === 'wide' && (\n <div className=\"eb:min-h-0 eb:min-w-0 eb:p-3\" data-testid=\"eb-library-preview\">\n {previewPane}\n </div>\n )}\n </div>\n )}\n <NoticeArea />\n </div>\n );\n}\n\n/**\n * @param {{\n * storage?: import('../../storage/client.js').TemplateStorage,\n * locale?: string,\n * messages?: Record<string, string>,\n * onNotify?: (level: 'success'|'info'|'warning'|'error', message: string) => void,\n * sender?: import('../../features/templates/starter.js').StarterSender,\n * templates?: import('../../features/templates/gallery.js').GalleryTemplate[],\n * title?: string,\n * description?: string,\n * selectedId?: string|number|null,\n * favoritesKey?: string|null,\n * onEdit?: (template: import('../../storage/client.js').StoredTemplate) => void,\n * onCreate?: (start: null | {baseId: string, mjml: string, meta: {subject?: string, preheader?: string}}) => void,\n * onClose?: () => void,\n * className?: string,\n * }} props\n */\nexport default function TemplateLibrary({ locale = 'en', messages, onNotify, ...props }) {\n return (\n <UiProvider locale={locale} messages={messages} onNotify={onNotify}>\n <Library {...props} />\n </UiProvider>\n );\n}\n"],"mappings":"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","names":["base","template"]}
@@ -0,0 +1,119 @@
1
+ import {
2
+ identity
3
+ } from "./chunk-DFNHICTF.js";
4
+
5
+ // src/core/constants.js
6
+ var EMAIL_BUILDER_VERSION = "0.5.0";
7
+ var EMAIL_DOCUMENT_SCHEMA_VERSION = 1;
8
+ var EMAIL_DEVICES = ["desktop", "mobile"];
9
+ var EMAIL_MOBILE_WIDTH = 375;
10
+
11
+ // src/core/document.js
12
+ var MIGRATIONS = {};
13
+ var EmailDocumentError = class extends Error {
14
+ /** @param {string} message */
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "EmailDocumentError";
18
+ this.status = 422;
19
+ }
20
+ };
21
+ function isPlainObject(value) {
22
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+ function createDocument({ engine, engineVersion, project }) {
25
+ if (!engine) throw new EmailDocumentError("Document: missing engine");
26
+ if (!isPlainObject(project)) throw new EmailDocumentError("Document: invalid engine project");
27
+ return {
28
+ schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,
29
+ engine,
30
+ engineVersion: String(engineVersion || ""),
31
+ builderVersion: EMAIL_BUILDER_VERSION,
32
+ project: (
33
+ /** @type {Record<string, any>} */
34
+ project
35
+ )
36
+ };
37
+ }
38
+ function readDocument(raw) {
39
+ let value = raw;
40
+ if (typeof value === "string") {
41
+ try {
42
+ value = JSON.parse(value);
43
+ } catch {
44
+ throw new EmailDocumentError("Unreadable document: invalid JSON");
45
+ }
46
+ }
47
+ if (!isPlainObject(value)) throw new EmailDocumentError("Unreadable document: object expected");
48
+ let document = { .../** @type {object} */
49
+ value };
50
+ const version = Number(document.schemaVersion);
51
+ if (!Number.isInteger(version) || version < 1) {
52
+ throw new EmailDocumentError("Unreadable document: missing schema version");
53
+ }
54
+ if (version > EMAIL_DOCUMENT_SCHEMA_VERSION) {
55
+ throw new EmailDocumentError(
56
+ `Document written by a newer version of the builder (schema ${version}) \u2014 reload the page`
57
+ );
58
+ }
59
+ for (let v = version; v < EMAIL_DOCUMENT_SCHEMA_VERSION; v += 1) {
60
+ const migrate = MIGRATIONS[v];
61
+ if (!migrate) throw new EmailDocumentError(`Document: no migration from schema ${v}`);
62
+ document = migrate(document);
63
+ }
64
+ if (typeof document.engine !== "string" || !document.engine) {
65
+ throw new EmailDocumentError("Unreadable document: missing engine");
66
+ }
67
+ if (!isPlainObject(document.project)) {
68
+ throw new EmailDocumentError("Unreadable document: missing engine project");
69
+ }
70
+ return {
71
+ schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,
72
+ engine: document.engine,
73
+ engineVersion: String(document.engineVersion || ""),
74
+ builderVersion: String(document.builderVersion || ""),
75
+ project: document.project
76
+ };
77
+ }
78
+ function serializeDocument(document) {
79
+ return JSON.stringify(readDocument(document));
80
+ }
81
+
82
+ // src/validation/images.js
83
+ var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
84
+ var IMAGE_TYPES = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif" };
85
+ var ASSET_KEY_PATTERN = /^[a-f0-9]{32}\.(png|jpg|gif)$/;
86
+ var SIGNATURES = [
87
+ { type: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
88
+ { type: "image/jpeg", bytes: [255, 216, 255] },
89
+ { type: "image/gif", bytes: [71, 73, 70, 56] }
90
+ ];
91
+ function detectImage(bytes) {
92
+ if (!bytes || bytes.length < 8) return null;
93
+ const found = SIGNATURES.find((s) => s.bytes.every((b, i) => bytes[i] === b));
94
+ return found ? { type: found.type, ext: IMAGE_TYPES[found.type] } : null;
95
+ }
96
+ function imageUploadError(file, { t = identity } = {}) {
97
+ if (!(String(file?.type || "") in IMAGE_TYPES)) return t("Format rejected: PNG, JPEG or GIF expected");
98
+ if (Number(file?.size || 0) > MAX_IMAGE_BYTES) {
99
+ return t("Image too large: {max} MB maximum", { max: MAX_IMAGE_BYTES / (1024 * 1024) });
100
+ }
101
+ return null;
102
+ }
103
+
104
+ export {
105
+ EMAIL_BUILDER_VERSION,
106
+ EMAIL_DOCUMENT_SCHEMA_VERSION,
107
+ EMAIL_DEVICES,
108
+ EMAIL_MOBILE_WIDTH,
109
+ EmailDocumentError,
110
+ createDocument,
111
+ readDocument,
112
+ serializeDocument,
113
+ MAX_IMAGE_BYTES,
114
+ IMAGE_TYPES,
115
+ ASSET_KEY_PATTERN,
116
+ detectImage,
117
+ imageUploadError
118
+ };
119
+ //# sourceMappingURL=chunk-7U6BAKM7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/constants.js","../src/core/document.js","../src/validation/images.js"],"sourcesContent":["// @ts-check\n/**\n * Email builder constants.\n *\n * `EMAIL_BUILDER_VERSION` versions THIS package (UI, blocks, rendering);\n * `EMAIL_DOCUMENT_SCHEMA_VERSION` versions the stored JSON envelope (the\n * project JSON). Both are written with every template, so a document read by\n * a newer version knows whether it must be migrated (see `core/document.js`).\n */\n\nexport const EMAIL_BUILDER_VERSION = '0.5.0';\n\nexport const EMAIL_DOCUMENT_SCHEMA_VERSION = 1;\n\n/** @typedef {'desktop'|'mobile'} EmailDevice */\n\n/** @type {EmailDevice[]} */\nexport const EMAIL_DEVICES = ['desktop', 'mobile'];\n\n/** Content column width — the email client standard. */\nexport const EMAIL_CONTENT_WIDTH = 600;\n\n/** Mobile preview frame width (standard iPhone, the common worst case). */\nexport const EMAIL_MOBILE_WIDTH = 375;\n","// @ts-check\n/**\n * DOCUMENT envelope — what gets stored (the project JSON).\n *\n * HTML is only a compilation output; what gets edited is the engine's\n * project. It is not stored bare but wrapped with:\n *\n * schemaVersion the shape of THIS envelope (migrations, see MIGRATIONS);\n * engine the engine that can read `project` back;\n * engineVersion its version at write time;\n * builderVersion the builder version that wrote it;\n * project the engine data, opaque to the rest of the code.\n *\n * A document from another engine, a future schema or a malformed one is\n * REJECTED on read rather than half-loaded: a template silently emptied and\n * then saved again would be lost.\n *\n * Dependency-free module: testable outside the browser.\n */\n\nimport { EMAIL_BUILDER_VERSION, EMAIL_DOCUMENT_SCHEMA_VERSION } from './constants.js';\n\n/**\n * @typedef {{\n * schemaVersion: number,\n * engine: string,\n * engineVersion: string,\n * builderVersion: string,\n * project: Record<string, any>,\n * }} EmailDocument\n */\n\n/**\n * Envelope migrations, from N to N+1. Empty while only one schema exists; the\n * first entry will be `1: (doc) => ({ ...doc, schemaVersion: 2 })`.\n *\n * @type {Record<number, (document: any) => any>}\n */\nconst MIGRATIONS = {};\n\nexport class EmailDocumentError extends Error {\n /** @param {string} message */\n constructor(message) {\n super(message);\n this.name = 'EmailDocumentError';\n this.status = 422;\n }\n}\n\nfunction isPlainObject(value) {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * @param {{engine: string, engineVersion: string, project: object}} parts\n * @returns {EmailDocument}\n */\nexport function createDocument({ engine, engineVersion, project }) {\n if (!engine) throw new EmailDocumentError('Document: missing engine');\n if (!isPlainObject(project)) throw new EmailDocumentError('Document: invalid engine project');\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine,\n engineVersion: String(engineVersion || ''),\n builderVersion: EMAIL_BUILDER_VERSION,\n project: /** @type {Record<string, any>} */ (project),\n };\n}\n\n/**\n * Reads a document (object or JSON string), migrates it to the current schema\n * and validates it.\n *\n * @param {unknown} raw\n * @returns {EmailDocument}\n * @throws {EmailDocumentError}\n */\nexport function readDocument(raw) {\n let value = raw;\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value);\n } catch {\n throw new EmailDocumentError('Unreadable document: invalid JSON');\n }\n }\n if (!isPlainObject(value)) throw new EmailDocumentError('Unreadable document: object expected');\n\n /** @type {any} */\n let document = { ...(/** @type {object} */ (value)) };\n const version = Number(document.schemaVersion);\n if (!Number.isInteger(version) || version < 1) {\n throw new EmailDocumentError('Unreadable document: missing schema version');\n }\n if (version > EMAIL_DOCUMENT_SCHEMA_VERSION) {\n throw new EmailDocumentError(\n `Document written by a newer version of the builder (schema ${version}) — ` +\n 'reload the page'\n );\n }\n for (let v = version; v < EMAIL_DOCUMENT_SCHEMA_VERSION; v += 1) {\n const migrate = MIGRATIONS[v];\n if (!migrate) throw new EmailDocumentError(`Document: no migration from schema ${v}`);\n document = migrate(document);\n }\n\n if (typeof document.engine !== 'string' || !document.engine) {\n throw new EmailDocumentError('Unreadable document: missing engine');\n }\n if (!isPlainObject(document.project)) {\n throw new EmailDocumentError('Unreadable document: missing engine project');\n }\n\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine: document.engine,\n engineVersion: String(document.engineVersion || ''),\n builderVersion: String(document.builderVersion || ''),\n project: document.project,\n };\n}\n\n/** @param {EmailDocument} document */\nexport function serializeDocument(document) {\n return JSON.stringify(readDocument(document));\n}\n","// @ts-check\n/**\n * Images UPLOADED in the builder — one rule shared by the browser (immediate\n * rejection, storage/client.js) and the server (storage/server/assetFile.js).\n *\n * PNG, JPEG, GIF: the three formats every email client displays. WebP and SVG\n * do not display in Outlook, and an SVG can carry script.\n * The server reads the type from the file BYTES; the browser, from `file.type`.\n */\n\nimport { identity } from '../i18n/index.js';\n\n/** Maximum image size: beyond it, the email becomes slow to open. */\nexport const MAX_IMAGE_BYTES = 2 * 1024 * 1024;\n\n/** Accepted types → extension of the public key. */\nexport const IMAGE_TYPES = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif' };\n\n/** Public key: 32 hex characters of the hash + extension. */\nexport const ASSET_KEY_PATTERN = /^[a-f0-9]{32}\\.(png|jpg|gif)$/;\n\nconst SIGNATURES = [\n { type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },\n { type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },\n { type: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },\n];\n\n/**\n * Image type detected from the file signature, or `null`.\n * @param {Uint8Array} bytes\n * @returns {{type: string, ext: string}|null}\n */\nexport function detectImage(bytes) {\n if (!bytes || bytes.length < 8) return null;\n const found = SIGNATURES.find((s) => s.bytes.every((b, i) => bytes[i] === b));\n return found ? { type: found.type, ext: IMAGE_TYPES[found.type] } : null;\n}\n\n/**\n * Immediate rejection of a file the server would refuse, or `null`.\n * @param {{type?: string, size?: number}} file\n * @param {{t?: import('../i18n/index.js').Translate}} [options]\n */\nexport function imageUploadError(file, { t = identity } = {}) {\n if (!(String(file?.type || '') in IMAGE_TYPES)) return t('Format rejected: PNG, JPEG or GIF expected');\n if (Number(file?.size || 0) > MAX_IMAGE_BYTES) {\n return t('Image too large: {max} MB maximum', { max: MAX_IMAGE_BYTES / (1024 * 1024) });\n }\n return null;\n}\n"],"mappings":"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","names":[]}