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.
- package/CHANGELOG.md +190 -0
- package/README.md +290 -20
- package/dist/ai-server.js +68 -0
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.js +852 -0
- package/dist/ai.js.map +1 -0
- package/dist/builder.js +16 -8
- package/dist/chunk-2RZHGAH5.js +64 -0
- package/dist/chunk-2RZHGAH5.js.map +1 -0
- package/dist/chunk-3LKF4QBL.js +595 -0
- package/dist/chunk-3LKF4QBL.js.map +1 -0
- package/dist/chunk-5SASKCUC.js +251 -0
- package/dist/chunk-5SASKCUC.js.map +1 -0
- package/dist/{chunk-JOC752I3.js → chunk-6D3SLT5W.js} +27 -29
- package/dist/chunk-6D3SLT5W.js.map +1 -0
- package/dist/chunk-7U6BAKM7.js +119 -0
- package/dist/chunk-7U6BAKM7.js.map +1 -0
- package/dist/chunk-DFNHICTF.js +1359 -0
- package/dist/chunk-DFNHICTF.js.map +1 -0
- package/dist/chunk-ECHGJZ2W.js +327 -0
- package/dist/chunk-ECHGJZ2W.js.map +1 -0
- package/dist/chunk-KXJJSVN5.js +98 -0
- package/dist/chunk-KXJJSVN5.js.map +1 -0
- package/dist/{chunk-ZSUHOCCE.js → chunk-LXR4QTGP.js} +123 -579
- package/dist/chunk-LXR4QTGP.js.map +1 -0
- package/dist/{chunk-BVOVAEEJ.js → chunk-LYMJ2TBX.js} +2 -2
- package/dist/chunk-MPX5CWTZ.js +1687 -0
- package/dist/chunk-MPX5CWTZ.js.map +1 -0
- package/dist/chunk-PB7YZ3WP.js +5635 -0
- package/dist/chunk-PB7YZ3WP.js.map +1 -0
- package/dist/chunk-UFKCLU2D.js +255 -0
- package/dist/chunk-UFKCLU2D.js.map +1 -0
- package/dist/index.js +51 -20
- package/dist/index.js.map +1 -1
- package/dist/library.js +12 -7
- package/dist/server.js +513 -214
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +12 -0
- package/dist/types/ai/GenerateButton.d.ts +4 -0
- package/dist/types/ai/RewriteText.d.ts +5 -0
- package/dist/types/ai/SubjectIdeas.d.ts +25 -0
- package/dist/types/ai/chip.d.ts +1 -0
- package/dist/types/ai/claude.d.ts +21 -0
- package/dist/types/ai/createAi.d.ts +66 -0
- package/dist/types/ai/draft.d.ts +163 -0
- package/dist/types/ai/generator.d.ts +123 -0
- package/dist/types/ai/i18n/fr.d.ts +2 -0
- package/dist/types/ai/i18n/index.d.ts +2 -0
- package/dist/types/ai/index.d.ts +9 -0
- package/dist/types/ai/keys.d.ts +46 -0
- package/dist/types/ai/prompt.d.ts +76 -0
- package/dist/types/ai/providers.d.ts +67 -0
- package/dist/types/ai/purposes.d.ts +48 -0
- package/dist/types/ai/rewrite.d.ts +83 -0
- package/dist/types/ai/server.d.ts +9 -0
- package/dist/types/ai/subjects.d.ts +97 -0
- package/dist/types/blocks/shared.d.ts +20 -0
- package/dist/types/core/EmailEditor.d.ts +72 -19
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/mjmlDefaults.d.ts +13 -0
- package/dist/types/core/scrollbars.d.ts +1 -1
- package/dist/types/defaultEditor.d.ts +4 -1
- package/dist/types/engine/NativeAdapter.d.ts +28 -0
- package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
- package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
- package/dist/types/engine/canvas/icons.d.ts +14 -0
- package/dist/types/engine/canvas/morph.d.ts +21 -0
- package/dist/types/engine/canvasMjml.d.ts +30 -0
- package/dist/types/engine/mjml.d.ts +31 -0
- package/dist/types/engine/model.d.ts +130 -0
- package/dist/types/engine/placement.d.ts +69 -0
- package/dist/types/engine/richText.d.ts +15 -0
- package/dist/types/engine/unwrapModule.d.ts +29 -0
- package/dist/types/features/brand/BrandButton.d.ts +4 -0
- package/dist/types/features/brand/brand.d.ts +108 -0
- package/dist/types/features/brand/extract.d.ts +61 -0
- package/dist/types/features/brand/index.d.ts +2 -0
- package/dist/types/features/brand/messages.d.ts +13 -0
- package/dist/types/features/brand/useBrand.d.ts +22 -0
- package/dist/types/features/preview/PreviewButton.d.ts +1 -1
- package/dist/types/features/preview/darkMode.d.ts +65 -0
- package/dist/types/features/variables/VariablePicker.d.ts +3 -2
- package/dist/types/i18n/index.d.ts +8 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/server/brand.d.ts +32 -0
- package/dist/types/server/brandImport.d.ts +93 -0
- package/dist/types/server/compileMjml.d.ts +2 -0
- package/dist/types/server/handler.d.ts +26 -0
- package/dist/types/server/index.d.ts +5 -1
- package/dist/types/storage/client.d.ts +57 -3
- package/dist/types/ui/EmailBuilder.d.ts +5 -1
- package/dist/types/ui/EmailStudio.d.ts +6 -1
- package/dist/types/ui/PropertiesPanel.d.ts +2 -1
- package/dist/types/ui/fields.d.ts +2 -1
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/dist/types/ui/primitives/color-picker.d.ts +2 -1
- package/dist/types/ui/primitives/searchShortcut.d.ts +16 -0
- package/dist/types/ui/useLayoutWidth.d.ts +1 -1
- package/package.json +27 -7
- package/dist/chunk-3M36VVXS.js +0 -665
- package/dist/chunk-3M36VVXS.js.map +0 -1
- package/dist/chunk-57CTQNTK.js +0 -56
- package/dist/chunk-57CTQNTK.js.map +0 -1
- package/dist/chunk-ILRAQTHS.js +0 -3677
- package/dist/chunk-ILRAQTHS.js.map +0 -1
- package/dist/chunk-JOC752I3.js.map +0 -1
- package/dist/chunk-ZSUHOCCE.js.map +0 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
- package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
- /package/dist/{chunk-BVOVAEEJ.js.map → chunk-LYMJ2TBX.js.map} +0 -0
package/dist/ai.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ai/keys.js","../src/ai/ConnectAi.jsx","../src/ai/GenerateButton.jsx","../src/ai/chip.js","../src/ai/RewriteText.jsx","../src/ai/SubjectIdeas.jsx","../src/ai/createAi.js"],"sourcesContent":["// @ts-check\n/**\n * THE USER'S KEY — when the AI runs in the page with `askKey: true`, each\n * user connects their own provider key, once.\n *\n * The first AI action asks for it (the Connect dialog, `ConnectAi.jsx`); it is\n * kept in this browser (localStorage, one entry per provider) and sent only to\n * the provider. A key the provider refuses is forgotten, and asked again.\n *\n * ai.key.get() the key in use ('' when none)\n * ai.key.set(key) connect a key without the dialog\n * ai.key.forget() disconnect: the next AI action asks again\n * ai.key.request() ask the user now → the key, or '' when they cancel\n * ai.key.subscribe(fn) called on every change; returns the unsubscribe\n * ai.key.provider the provider's name, where to get a key, what is free\n */\n\n/** @typedef {import('./providers.js').AiProvider} AiProvider */\n\nconst STORAGE_PREFIX = 'best-email-builder:ai-key:';\n\n/** localStorage when there is one that works (not in private modes that throw, not on the server). */\nfunction browserStorage() {\n try {\n const storage = globalThis.localStorage;\n return storage && typeof storage.getItem === 'function' ? storage : null;\n } catch {\n return null;\n }\n}\n\n/**\n * @typedef {{\n * provider: Readonly<{id: string, name: string, keyUrl: string|null, keyHint: string, free: string|null}>,\n * get: () => string,\n * set: (key: string) => void,\n * forget: () => void,\n * request: () => Promise<string>,\n * subscribe: (listener: () => void) => () => void,\n * state: () => Readonly<{asking: boolean, refused: boolean, connected: boolean}>,\n * answer: (key: string|null) => void,\n * refuse: () => void,\n * }} AiKey\n * `state`, `answer` and `refuse` serve the Connect dialog and the module:\n * whether the user is being asked, whether the last key was refused.\n */\n\n/**\n * @param {AiProvider} provider\n * @returns {AiKey}\n */\nexport function createKeyStore(provider) {\n const slot = STORAGE_PREFIX + provider.id;\n /** Kept here too: a browser that does not store still works for the page's life. */\n let memory = '';\n let refused = false;\n /** @type {{promise: Promise<string>, resolve: (key: string) => void}|null} */\n let pending = null;\n /** @type {Set<() => void>} */\n const listeners = new Set();\n /** @type {ReturnType<AiKey['state']>} */\n let snapshot;\n\n const read = () => {\n try {\n return browserStorage()?.getItem(slot) || '';\n } catch {\n return '';\n }\n };\n /** @param {string} value */\n const write = (value) => {\n try {\n const storage = browserStorage();\n if (value) storage?.setItem(slot, value);\n else storage?.removeItem(slot);\n } catch {\n // Full, or blocked: the key stays in memory.\n }\n };\n const get = () => memory || read();\n const changed = () => {\n snapshot = Object.freeze({ asking: Boolean(pending), refused, connected: Boolean(get()) });\n for (const listener of [...listeners]) listener();\n };\n snapshot = Object.freeze({ asking: false, refused: false, connected: Boolean(get()) });\n\n /** @param {string} key */\n const answer = (key) => {\n const value = String(key || '').trim();\n if (value) {\n memory = value;\n refused = false;\n write(value);\n }\n const waiting = pending;\n pending = null;\n changed();\n waiting?.resolve(value);\n };\n\n return Object.freeze({\n provider: Object.freeze({ id: provider.id, name: provider.name, keyUrl: provider.keyUrl, keyHint: provider.keyHint, free: provider.free }),\n get,\n set: (/** @type {string} */ key) => {\n if (typeof key !== 'string' || !key.trim()) throw new TypeError('ai.key.set: a non-empty key');\n answer(key);\n },\n forget: () => {\n memory = '';\n write('');\n changed();\n },\n request: () => {\n if (!pending) {\n /** @type {(key: string) => void} */\n let resolve = () => {};\n const promise = new Promise((done) => (resolve = done));\n pending = { promise, resolve };\n changed();\n }\n return pending.promise;\n },\n subscribe: (/** @type {() => void} */ listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n state: () => snapshot,\n answer: (/** @type {string|null} */ key) => answer(key || ''),\n refuse: () => {\n memory = '';\n write('');\n refused = true;\n changed();\n },\n });\n}\n","import { useEffect, useId, useRef, useState, useSyncExternalStore } from 'react';\nimport { ArrowUpRight, Eye, EyeOff, Gift, KeyRound, ShieldCheck, TriangleAlert } from 'lucide-react';\nimport { useUi } from '../ui/context.jsx';\nimport { Button } from '../ui/primitives/button.jsx';\nimport { Input, Label } from '../ui/primitives/fields.jsx';\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/primitives/overlays.jsx';\n\n/**\n * CONNECT — the dialog that asks the user for their provider key, when the AI\n * runs in the page with `createAi({ provider, askKey: true })`.\n *\n * It opens by itself when an AI action needs a key and there is none, or the\n * provider refused the last one; the action goes on once the key is given.\n * Cancel ends the action (\"Connect an AI provider first\"). The key is kept in\n * this browser and sent only to the provider (see keys.js).\n */\nexport default function ConnectAi({ aiKey }) {\n const { t } = useUi();\n const state = useSyncExternalStore(aiKey.subscribe, aiKey.state, aiKey.state);\n const [value, setValue] = useState('');\n const [shown, setShown] = useState(false);\n const inputRef = useRef(/** @type {HTMLInputElement|null} */ (null));\n const inputId = useId();\n const hintId = useId();\n const { name, keyUrl, keyHint, free } = aiKey.provider;\n\n // Each time it opens: an empty field, the key hidden.\n useEffect(() => {\n if (!state.asking) return;\n setValue('');\n setShown(false);\n }, [state.asking]);\n\n const key = value.trim();\n // A key from elsewhere (another provider's) is the usual mistake: said, not blocked.\n const unusual = Boolean(key && keyHint && !key.startsWith(keyHint));\n\n const submit = (/** @type {React.FormEvent} */ event) => {\n event.preventDefault();\n if (key) aiKey.answer(key);\n };\n\n return (\n <Dialog open={state.asking} onOpenChange={(open) => !open && aiKey.answer(null)}>\n <DialogContent\n className=\"eb:gap-0 eb:overflow-hidden eb:p-0 eb:sm:max-w-md\"\n data-testid=\"eb-ai-connect\"\n onOpenAutoFocus={(event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }}\n >\n <form onSubmit={submit}>\n <div className=\"eb:flex eb:flex-col eb:gap-5 eb:px-6 eb:pt-6 eb:pb-5\">\n <DialogHeader className=\"eb:flex-row eb:items-start eb:gap-3.5 eb:pr-6\">\n <span className=\"eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-full eb:bg-primary/10 eb:text-primary\">\n <KeyRound className=\"eb:size-5\" aria-hidden=\"true\" />\n </span>\n <div className=\"eb:flex eb:flex-col eb:gap-1.5\">\n <DialogTitle>{t('Connect {provider}', { provider: name })}</DialogTitle>\n <DialogDescription className=\"eb:text-[13px] eb:leading-relaxed eb:text-foreground-lighter\">\n {t('The AI writes with your own {provider} key. Connect it once: this browser remembers it.', { provider: name })}\n </DialogDescription>\n </div>\n </DialogHeader>\n\n {state.refused && (\n <p role=\"alert\" className=\"eb:flex eb:items-start eb:gap-2 eb:rounded-md eb:border eb:border-destructive/30 eb:bg-destructive/10 eb:px-3 eb:py-2 eb:text-[12.5px] eb:text-destructive\">\n <TriangleAlert className=\"eb:mt-px eb:size-3.5 eb:shrink-0\" aria-hidden=\"true\" />\n {t('{provider} refused the last key. Paste another one.', { provider: name })}\n </p>\n )}\n\n {(keyUrl || free) && (\n <div className=\"eb:flex eb:flex-wrap eb:items-center eb:gap-x-3 eb:gap-y-2\">\n {keyUrl && (\n <Button asChild variant=\"outline\" size=\"sm-compact\">\n <a href={keyUrl} target=\"_blank\" rel=\"noopener noreferrer\" data-testid=\"eb-ai-connect-get-key\">\n {t('Get a key')}\n <ArrowUpRight aria-hidden=\"true\" />\n </a>\n </Button>\n )}\n {free && (\n <span className=\"eb:inline-flex eb:items-center eb:gap-1.5 eb:rounded-full eb:bg-success/10 eb:px-2.5 eb:py-1 eb:text-[11.5px] eb:font-medium eb:text-success\">\n <Gift className=\"eb:size-3.5\" aria-hidden=\"true\" />\n {t(free)}\n </span>\n )}\n </div>\n )}\n\n <div className=\"eb:flex eb:flex-col eb:gap-1.5\">\n <Label htmlFor={inputId}>{t('API key')}</Label>\n <div className=\"eb:relative\">\n <Input\n ref={inputRef}\n id={inputId}\n type={shown ? 'text' : 'password'}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n placeholder={keyHint ? `${keyHint}…` : t('Paste the key')}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n aria-describedby={hintId}\n className=\"eb:h-9 eb:pr-10 eb:font-mono eb:text-[12.5px]\"\n data-testid=\"eb-ai-connect-key\"\n />\n <button\n type=\"button\"\n className=\"eb:absolute eb:top-1/2 eb:right-1 eb:flex eb:size-7 eb:-translate-y-1/2 eb:items-center eb:justify-center eb:rounded eb:text-foreground-muted eb:transition-colors eb:outline-none eb:hover:bg-muted eb:hover:text-foreground eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50\"\n onClick={() => setShown((current) => !current)}\n aria-label={shown ? t('Hide the key') : t('Show the key')}\n aria-pressed={shown}\n >\n {shown ? <EyeOff className=\"eb:size-4\" aria-hidden=\"true\" /> : <Eye className=\"eb:size-4\" aria-hidden=\"true\" />}\n </button>\n </div>\n <p id={hintId} className=\"eb:min-h-4 eb:text-[11.5px] eb:text-foreground-muted\">\n {unusual ? (\n <span className=\"eb:text-warning\">{t('{provider} keys usually start with {prefix}.', { provider: name, prefix: keyHint })}</span>\n ) : keyHint ? (\n t('{provider} keys start with {prefix}.', { provider: name, prefix: keyHint })\n ) : null}\n </p>\n </div>\n </div>\n\n <div className=\"eb:flex eb:items-start eb:gap-2 eb:border-t eb:border-border eb:bg-surface-75 eb:px-6 eb:py-3 eb:text-[11.5px] eb:leading-relaxed eb:text-foreground-lighter\">\n <ShieldCheck className=\"eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted\" aria-hidden=\"true\" />\n <span>{t('The key stays in this browser and is sent only to {provider}.', { provider: name })}</span>\n </div>\n\n <DialogFooter className=\"eb:border-t eb:border-border eb:px-6 eb:py-4\">\n <Button type=\"button\" variant=\"ghost\" onClick={() => aiKey.answer(null)}>\n {t('Cancel')}\n </Button>\n <Button type=\"submit\" disabled={!key} data-testid=\"eb-ai-connect-submit\">\n {t('Connect')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n );\n}\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport { Check, Loader2, Search, Sparkles, TriangleAlert } from 'lucide-react';\nimport { SEARCH_KEYS, SearchKeyHint, useSearchShortcut } from '../ui/primitives/searchShortcut.jsx';\nimport { useUi } from '../ui/context.jsx';\nimport { Button } from '../ui/primitives/button.jsx';\nimport { Input, Textarea } from '../ui/primitives/fields.jsx';\nimport { SectionLabel } from '../ui/primitives/misc.jsx';\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/primitives/overlays.jsx';\nimport { chipClassName } from './chip.js';\nimport { MAX_DETAILS_LENGTH } from './generator.js';\nimport { purposesByCategory } from './purposes.js';\nimport { brandTemplate, hasBrand } from '../features/brand/brand.js';\n\n/**\n * GENERATE — toolbar button and dialog: an email written from scratch.\n *\n * One decision for the user: what the email is for (a badge). Details are\n * optional; the language is the interface's, the sender and the variables\n * are the builder's own. The dialog says up front that the email in the\n * editor will be replaced — no confirmation after a minute of waiting.\n *\n * The result REPLACES the email in the editor, subject and preview text\n * included (they belong to the new email); the name is filled when empty.\n * A saved template counts as modified: the next save keeps it.\n */\n\n/** Lowercase, without accents: \"Été\" matches \"ete\". @param {string} text */\nconst fold = (text) =>\n text\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase();\n\nexport default function GenerateButton({ context, generate: generateEmail }) {\n const { editor, ready, isDirty, markDirty, template, setMeta, variables, sender, brand } = context;\n const { t, locale, notify } = useUi();\n const [open, setOpen] = useState(false);\n const [purpose, setPurpose] = useState(/** @type {string|null} */ (null));\n const [details, setDetails] = useState('');\n const [query, setQuery] = useState('');\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState('');\n const [seconds, setSeconds] = useState(0);\n const [dirty, setDirty] = useState(false);\n const abortRef = useRef(/** @type {AbortController|null} */ (null));\n const searchRef = useRef(/** @type {HTMLInputElement|null} */ (null));\n useSearchShortcut(searchRef);\n\n useEffect(() => () => abortRef.current?.abort(), []);\n useEffect(() => {\n if (!busy) return undefined;\n setSeconds(0);\n const timer = setInterval(() => setSeconds((s) => s + 1), 1000);\n return () => clearInterval(timer);\n }, [busy]);\n\n const groups = useMemo(() => {\n const words = fold(query.trim()).split(/\\s+/).filter(Boolean);\n return purposesByCategory()\n .map((group) => ({\n ...group,\n purposes: group.purposes.filter((entry) => {\n const label = fold(`${t(entry.label)} ${entry.label} ${t(group.label)}`);\n return words.every((word) => label.includes(word));\n }),\n }))\n .filter((group) => group.purposes.length > 0);\n }, [query, t]);\n\n const openDialog = () => {\n setError('');\n setDirty(isDirty());\n setOpen(true);\n };\n\n const close = (next) => {\n if (next) return;\n abortRef.current?.abort();\n setOpen(false);\n };\n\n const generate = async () => {\n if (busy || (!purpose && !details.trim())) return;\n setError('');\n setBusy(true);\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n const email = await generateEmail(\n {\n purpose,\n details: details.trim(),\n language: String(locale || 'en').slice(0, 2),\n sender: sender || {},\n ...(hasBrand(brand) ? { brand: { primary: brand.primary, heading: brand.heading, font: brand.font } } : {}),\n variables: (variables || []).map((variable) => ({ key: variable.key, label: variable.label })),\n },\n { signal: controller.signal }\n );\n if (controller.signal.aborted) return;\n // The brand's logo where the model wrote the name (an uploaded logo\n // is not an https address the model may be given).\n editor.loadMjml(hasBrand(brand) ? brandTemplate(email.mjml, brand, { sender, t }) : email.mjml);\n setMeta((meta) => ({\n ...meta,\n name: meta.name || email.name,\n subject: email.subject || meta.subject,\n preheader: email.preheader || meta.preheader,\n }));\n if (template.id) markDirty();\n setOpen(false);\n notify('success', t('Your email is ready. Edit anything, then save.'));\n } catch (err) {\n if (!controller.signal.aborted) setError(err?.message ? t(err.message) : t('The email could not be generated.'));\n } finally {\n if (abortRef.current === controller) abortRef.current = null;\n setBusy(false);\n }\n };\n\n const cancel = () => {\n abortRef.current?.abort();\n setBusy(false);\n };\n\n const canGenerate = Boolean(purpose || details.trim());\n\n return (\n <>\n <Button variant=\"outline\" size=\"sm-compact\" disabled={!ready} onClick={openDialog}>\n <Sparkles />\n {t('Generate')}\n </Button>\n <Dialog open={open} onOpenChange={close}>\n <DialogContent\n className=\"eb:flex eb:max-h-[calc(100dvh-2rem)] eb:flex-col eb:gap-0 eb:p-0 eb:sm:max-w-2xl\"\n data-testid=\"eb-generate\"\n onOpenAutoFocus={(event) => {\n // The search field rather than the first badge.\n event.preventDefault();\n searchRef.current?.focus();\n }}\n >\n <DialogHeader className=\"eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-3\">\n <DialogTitle>{t('Generate an email')}</DialogTitle>\n <DialogDescription>{t('Pick what it is for. AI writes a complete email that opens here, ready to edit.')}</DialogDescription>\n </DialogHeader>\n\n <div className=\"eb:px-6 eb:pb-3\">\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\" aria-hidden=\"true\" />\n <Input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={t('Search: sale, password, birthday…')}\n aria-label={t('Search')}\n aria-keyshortcuts={SEARCH_KEYS}\n className=\"eb:pr-16 eb:pl-8 eb:[&::-webkit-search-cancel-button]:hidden\"\n disabled={busy}\n />\n <SearchKeyHint className=\"eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2\" />\n </div>\n </div>\n\n {/* The scroll box is a div: a fieldset does not clip its overflow\n as a flex item, and the badges would run under the footer. */}\n <div className=\"eb:min-h-0 eb:flex-1 eb:overflow-y-auto eb:border-y eb:border-border eb:px-6 eb:py-4\" data-testid=\"eb-generate-purposes\">\n <fieldset disabled={busy} className=\"eb:m-0 eb:min-w-0 eb:border-0 eb:p-0\">\n <legend className=\"eb:sr-only\">{t('What is the email for?')}</legend>\n {groups.length === 0 && (\n <p className=\"eb:py-6 eb:text-center eb:text-[13px] eb:text-foreground-muted\">\n {t('Nothing matches. Describe your email in the details below.')}\n </p>\n )}\n <div className=\"eb:flex eb:flex-col eb:gap-4\">\n {groups.map((group) => (\n <section key={group.id} className=\"eb:flex eb:flex-col eb:gap-2\" aria-label={t(group.label)}>\n <SectionLabel>{t(group.label)}</SectionLabel>\n <div className=\"eb:flex eb:flex-wrap eb:gap-1.5\">\n {group.purposes.map((entry) => {\n const selected = purpose === entry.id;\n return (\n <button\n key={entry.id}\n type=\"button\"\n aria-pressed={selected}\n data-purpose={entry.id}\n onClick={() => setPurpose(selected ? null : entry.id)}\n className={chipClassName(selected)}\n >\n {selected && <Check className=\"eb:size-3\" aria-hidden=\"true\" />}\n {t(entry.label)}\n </button>\n );\n })}\n </div>\n </section>\n ))}\n </div>\n </fieldset>\n </div>\n\n <div className=\"eb:flex eb:flex-col eb:gap-3 eb:px-6 eb:pt-3 eb:pb-6\">\n <Textarea\n value={details}\n onChange={(event) => setDetails(event.target.value.slice(0, MAX_DETAILS_LENGTH))}\n placeholder={t('Details (optional): product, dates, offer, tone, language…')}\n aria-label={t('Details (optional)')}\n rows={2}\n className=\"eb:min-h-14\"\n disabled={busy}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) generate();\n }}\n />\n {error ? (\n <p role=\"alert\" className=\"eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive\">\n <TriangleAlert className=\"eb:mt-px eb:size-3.5 eb:shrink-0\" aria-hidden=\"true\" />\n {error}\n </p>\n ) : (\n dirty &&\n !busy && (\n <p className=\"eb:text-[12px] eb:text-foreground-muted\">{t('The email being edited will be replaced, unsaved changes included.')}</p>\n )\n )}\n <div className=\"eb:flex eb:items-center eb:justify-end eb:gap-2\">\n {busy && (\n <p className=\"eb:mr-auto eb:text-[12px] eb:text-foreground-muted\" role=\"status\" aria-live=\"polite\">\n {t('Writing your email… {seconds} s', { seconds })}\n </p>\n )}\n {busy && (\n <Button variant=\"outline\" size=\"sm\" onClick={cancel}>\n {t('Cancel')}\n </Button>\n )}\n <Button size=\"sm\" onClick={generate} disabled={!canGenerate || busy} data-testid=\"eb-generate-submit\">\n {busy ? <Loader2 className=\"eb:animate-spin\" /> : <Sparkles />}\n {t('Generate')}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n","import { cn } from '../ui/primitives/cn.js';\n\n/**\n * A rounded badge button: the purposes of \"Generate\", the rewrite actions.\n * @param {boolean} [selected]\n */\nexport const chipClassName = (selected = false) =>\n cn(\n 'eb:inline-flex eb:h-7 eb:items-center eb:gap-1 eb:rounded-full eb:border eb:px-3 eb:text-[12px] eb:font-medium eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:disabled:opacity-50',\n selected\n ? 'eb:border-primary eb:bg-primary eb:text-primary-foreground'\n : 'eb:border-border eb:bg-background eb:text-foreground eb:hover:border-primary/50 eb:hover:bg-primary/5'\n );\n","import { useEffect, useRef, useState } from 'react';\nimport { ChevronDown, Languages, Loader2, Sparkles, TriangleAlert } from 'lucide-react';\nimport { useUi } from '../ui/context.jsx';\nimport { Button } from '../ui/primitives/button.jsx';\nimport { Input } from '../ui/primitives/fields.jsx';\nimport { SectionLabel } from '../ui/primitives/misc.jsx';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/primitives/overlays.jsx';\nimport { cleanRichText } from '../engine/richText.js';\nimport { chipClassName } from './chip.js';\nimport { MAX_REWRITE_INSTRUCTION, REWRITE_ACTIONS, REWRITE_LANGUAGES } from './rewrite.js';\n\n/**\n * REWRITE WITH AI — in the properties of a selected text or button.\n *\n * One click: shorter, longer, fixed, friendlier, more formal, translated —\n * or a change in the user's words. The result replaces the text at once, in\n * one step of the history: Ctrl+Z brings the previous text back, so there is\n * nothing to accept or compare.\n *\n * Mounted per element (keyed by its id): selecting another element cancels a\n * rewrite in progress. If the text is edited while the AI works, the user's\n * words win and the answer is dropped.\n */\n\n/** A language under its own name: \"Deutsch\", \"Українська\". @param {string} code */\nfunction nativeName(code) {\n try {\n const name = new Intl.DisplayNames([code], { type: 'language' }).of(code) || code;\n return name.charAt(0).toLocaleUpperCase(code) + name.slice(1);\n } catch {\n return code;\n }\n}\n\nexport default function RewriteText({ editor, selection, rewrite }) {\n const { t, notify } = useUi();\n const [running, setRunning] = useState(/** @type {string|null} */ (null));\n const [error, setError] = useState('');\n const [instruction, setInstruction] = useState('');\n const abortRef = useRef(/** @type {AbortController|null} */ (null));\n const label = selection.tagName === 'mj-button';\n\n useEffect(() => () => abortRef.current?.abort(), []);\n\n /** The text as stored: a button's label, a text block's HTML. */\n const currentText = () => {\n const element = editor.getElement(selection.id);\n return (label ? element?.content : element?.html) ?? '';\n };\n\n const run = async (action, extra = {}) => {\n if (running) return;\n const text = currentText();\n setError('');\n setRunning(action);\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n const result = await rewrite({ action, text, format: label ? 'plain' : 'html', ...extra }, { signal: controller.signal });\n if (controller.signal.aborted) return;\n if (currentText() !== text) {\n setError(t('The text changed in the meantime, so it was left as it is.'));\n return;\n }\n editor.setContent(label ? result.text : cleanRichText(result.text, document), selection.id);\n if (action === 'custom') setInstruction('');\n notify('success', t('Text rewritten. Ctrl+Z brings back the previous one.'));\n } catch (err) {\n if (!controller.signal.aborted) setError(err?.message ? t(err.message) : t('The text could not be rewritten.'));\n } finally {\n if (abortRef.current === controller) abortRef.current = null;\n setRunning(null);\n }\n };\n\n const cancel = () => {\n abortRef.current?.abort();\n setRunning(null);\n };\n\n const busy = Boolean(running);\n const spinner = <Loader2 className=\"eb:size-3 eb:animate-spin\" aria-hidden=\"true\" />;\n\n return (\n <section className=\"eb:space-y-2.5\" aria-label={t('Rewrite with AI')} data-testid=\"eb-rewrite\">\n <SectionLabel className=\"eb:flex eb:items-center eb:gap-1.5\">\n <Sparkles className=\"eb:size-3\" aria-hidden=\"true\" />\n {t('Rewrite with AI')}\n </SectionLabel>\n <div className=\"eb:flex eb:flex-wrap eb:gap-1.5\">\n {REWRITE_ACTIONS.map((action) => (\n <button key={action.id} type=\"button\" className={chipClassName()} disabled={busy} onClick={() => run(action.id)}>\n {running === action.id && spinner}\n {t(action.label)}\n </button>\n ))}\n <DropdownMenu>\n <DropdownMenuTrigger asChild disabled={busy}>\n <button type=\"button\" className={chipClassName()}>\n {running === 'translate' ? spinner : <Languages className=\"eb:size-3\" aria-hidden=\"true\" />}\n {t('Translate')}\n <ChevronDown className=\"eb:size-3\" aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" aria-label={t('Translate into')}>\n {REWRITE_LANGUAGES.map((code) => (\n <DropdownMenuItem key={code} lang={code} onSelect={() => run('translate', { targetLanguage: code })}>\n {nativeName(code)}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n <form\n className=\"eb:flex eb:gap-1.5\"\n onSubmit={(event) => {\n event.preventDefault();\n if (instruction.trim()) run('custom', { instruction: instruction.trim() });\n }}\n >\n <Input\n value={instruction}\n onChange={(event) => setInstruction(event.target.value)}\n maxLength={MAX_REWRITE_INSTRUCTION}\n placeholder={t('Or say what to change…')}\n aria-label={t('What to change')}\n disabled={busy}\n />\n <Button type=\"submit\" variant=\"outline\" size=\"icon-sm\" aria-label={t('Rewrite')} disabled={busy || !instruction.trim()}>\n {running === 'custom' ? <Loader2 className=\"eb:animate-spin\" /> : <Sparkles />}\n </Button>\n </form>\n {running && (\n <p className=\"eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted\" role=\"status\" aria-live=\"polite\">\n {t('Rewriting…')}\n <Button type=\"button\" variant=\"link\" size=\"xs\" className=\"eb:h-auto eb:px-0\" onClick={cancel}>\n {t('Cancel')}\n </Button>\n </p>\n )}\n {error && (\n <p role=\"alert\" className=\"eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive\">\n <TriangleAlert className=\"eb:mt-px eb:size-3.5 eb:shrink-0\" aria-hidden=\"true\" />\n {error}\n </p>\n )}\n </section>\n );\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { Check, Loader2, Sparkles, TriangleAlert } from 'lucide-react';\nimport { useUi } from '../ui/context.jsx';\nimport { Button } from '../ui/primitives/button.jsx';\nimport { cn } from '../ui/primitives/cn.js';\nimport { chipClassName } from './chip.js';\nimport { emailWords } from './subjects.js';\n\n/** Ideas already shown that \"More ideas\" asks the model not to repeat (the server keeps 12). */\nconst MAX_AVOID = 12;\n\n/**\n * SUGGEST SUBJECTS — under the subject of the email.\n *\n * One click: three other subject lines, drawn from the words of the email.\n * They are a list to pick from, the user's own subject first: a click puts\n * one in the field, another click another one, the first row brings the\n * user's subject back. \"More ideas\" asks for three new ones (none of those\n * already shown; the one in the field stays in the list); \"Done\" closes the\n * list and keeps the subject in the field.\n *\n * \"Yours\" is what the user wrote: typed in the field while the list is open\n * (or while ideas are on their way), it becomes the new \"yours\". When the\n * email itself is replaced (Generate, a template), the list closes.\n *\n * Keyboard: the list is a radio group — one Tab stop, the arrows move and\n * pick. Focus goes to the list when it opens, back to the button after Done or\n * Cancel.\n */\nexport default function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {\n const { t, locale } = useUi();\n const [running, setRunning] = useState(false);\n const [error, setError] = useState('');\n const [ideas, setIdeas] = useState(/** @type {string[]|null} */ (null));\n const [original, setOriginal] = useState('');\n const shownRef = useRef(/** @type {string[]} */ ([]));\n const abortRef = useRef(/** @type {AbortController|null} */ (null));\n const subjectRef = useRef(meta.subject);\n subjectRef.current = meta.subject;\n const rootRef = useRef(/** @type {HTMLDivElement|null} */ (null));\n /** Where focus goes after the next render: the list, or the button that opened it. */\n const focusRef = useRef(/** @type {'list'|'button'|null} */ (null));\n\n useEffect(() => () => abortRef.current?.abort(), []);\n\n // Another email loaded in the editor: these ideas were for the previous one.\n useEffect(\n () =>\n editor.on('load', () => {\n abortRef.current?.abort();\n abortRef.current = null;\n shownRef.current = [];\n setRunning(false);\n setIdeas(null);\n setError('');\n setOriginal('');\n }),\n [editor]\n );\n\n // Typed in the field while the list is open: that is the user's subject now.\n const typed = meta.subject.trim();\n useEffect(() => {\n if (ideas && typed && !ideas.includes(meta.subject) && typed !== original) setOriginal(typed);\n }, [ideas, meta.subject, typed, original]);\n\n useLayoutEffect(() => {\n const target = focusRef.current;\n if (!target || running) return;\n focusRef.current = null;\n const root = rootRef.current;\n const element =\n target === 'list'\n ? root?.querySelector('[role=\"radio\"][tabindex=\"0\"]')\n : root?.querySelector('[data-subject-ideas-button]');\n /** @type {HTMLElement|null|undefined} */ (element)?.focus();\n });\n\n const ready = editor.status === 'ready';\n\n const run = async () => {\n if (running || editor.status !== 'ready') return;\n const first = !ideas;\n // The user's subject: as it was before the first idea was picked, or typed since.\n const yours = first ? subjectRef.current.trim() : original;\n const controller = new AbortController();\n abortRef.current = controller;\n setError('');\n setRunning(true);\n try {\n const result = await suggest(\n {\n subject: yours,\n preheader: meta.preheader,\n text: emailWords(editor.exportMjml()),\n language: locale,\n variables: variables?.map(({ key, label, kind }) => ({ key, label, kind })),\n avoid: shownRef.current.slice(-MAX_AVOID),\n },\n { signal: controller.signal }\n );\n if (controller.signal.aborted) return;\n const current = subjectRef.current;\n // The idea in the field stays in the list; the others give way to the new ones.\n const kept = ideas?.includes(current) ? [current] : [];\n const next = [...kept, ...result.subjects.filter((subject) => subject !== current)];\n shownRef.current = [...shownRef.current, ...result.subjects];\n // Typed during the wait: that is \"yours\" now.\n setOriginal(current.trim() && !ideas?.includes(current) ? current.trim() : yours);\n setIdeas(next);\n if (first) focusRef.current = 'list';\n } catch (err) {\n if (!controller.signal.aborted) setError(err?.message ? t(err.message) : t('No subject could be suggested.'));\n } finally {\n // A request cancelled earlier must not end the one that replaced it.\n if (abortRef.current === controller) {\n abortRef.current = null;\n setRunning(false);\n }\n }\n };\n\n const cancel = () => {\n abortRef.current?.abort();\n abortRef.current = null;\n setRunning(false);\n focusRef.current = 'button';\n };\n\n const done = () => {\n setIdeas(null);\n setError('');\n shownRef.current = [];\n focusRef.current = 'button';\n };\n\n const options = ideas ? [...(original ? [{ text: original, yours: true }] : []), ...ideas.map((text) => ({ text, yours: false }))] : [];\n const checked = options.findIndex((option) => option.text === meta.subject);\n const tabStop = checked === -1 ? 0 : checked;\n\n /** Arrows move through the ideas and pick, as in any radio group. */\n const onKeyDown = (/** @type {React.KeyboardEvent} */ event) => {\n const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];\n const to = step ? (tabStop + step + options.length) % options.length : event.key === 'Home' ? 0 : event.key === 'End' ? options.length - 1 : -1;\n if (to === -1 || !options.length) return;\n event.preventDefault();\n onSubject(options[to].text);\n /** @type {HTMLElement|undefined} */ (rootRef.current?.querySelectorAll('[role=\"radio\"]')[to])?.focus();\n };\n\n const spinner = <Loader2 className=\"eb:size-3 eb:animate-spin\" aria-hidden=\"true\" />;\n\n return (\n <div className=\"eb:space-y-2\" data-testid=\"eb-subject-ideas\" ref={rootRef}>\n {!ideas && (\n <button type=\"button\" className={chipClassName()} disabled={running || !ready} onClick={run} data-subject-ideas-button>\n {running ? spinner : <Sparkles className=\"eb:size-3\" aria-hidden=\"true\" />}\n {t('Suggest subjects')}\n </button>\n )}\n {ideas && (\n <div className=\"eb:space-y-2 eb:rounded-md eb:border eb:border-border eb:p-1.5\">\n <div role=\"radiogroup\" aria-label={t('Subject ideas')} className=\"eb:space-y-0.5\" onKeyDown={onKeyDown}>\n {options.map((option, index) => {\n const chosen = index === checked;\n return (\n <button\n key={`${option.yours ? 'yours' : 'idea'}:${option.text}`}\n type=\"button\"\n role=\"radio\"\n aria-checked={chosen}\n tabIndex={index === tabStop ? 0 : -1}\n className={cn(\n 'eb:flex eb:w-full eb:items-start eb:gap-2 eb:rounded eb:px-2 eb:py-1.5 eb:text-left eb:text-[12.5px] eb:leading-snug eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50',\n chosen ? 'eb:bg-primary/10 eb:text-foreground' : 'eb:text-foreground-lighter eb:hover:bg-muted'\n )}\n onClick={() => onSubject(option.text)}\n >\n <Check className={cn('eb:mt-0.5 eb:size-3.5 eb:shrink-0', chosen ? 'eb:text-primary' : 'eb:invisible')} aria-hidden=\"true\" />\n <span className=\"eb:min-w-0 eb:flex-1 eb:break-words\">{option.text}</span>\n {option.yours && <span className=\"eb:shrink-0 eb:text-[11px] eb:text-foreground-muted\">{t('yours')}</span>}\n </button>\n );\n })}\n </div>\n <div className=\"eb:flex eb:items-center eb:gap-1.5 eb:px-1 eb:pb-0.5\">\n <button type=\"button\" className={chipClassName()} disabled={running || !ready} onClick={run} data-subject-ideas-button>\n {running ? spinner : <Sparkles className=\"eb:size-3\" aria-hidden=\"true\" />}\n {t('More ideas')}\n </button>\n <Button type=\"button\" variant=\"ghost\" size=\"xs\" className=\"eb:ml-auto\" disabled={running} onClick={done}>\n {t('Done')}\n </Button>\n </div>\n </div>\n )}\n {running && (\n <p className=\"eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted\" role=\"status\" aria-live=\"polite\">\n {t('Reading the email…')}\n <Button type=\"button\" variant=\"link\" size=\"xs\" className=\"eb:h-auto eb:px-0\" onClick={cancel}>\n {t('Cancel')}\n </Button>\n </p>\n )}\n {error && (\n <p role=\"alert\" className=\"eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive\">\n <TriangleAlert className=\"eb:mt-px eb:size-3.5 eb:shrink-0\" aria-hidden=\"true\" />\n {error}\n </p>\n )}\n </div>\n );\n}\n","// @ts-check\n/**\n * THE AI MODULE — `createAi()`, given to the builder as `ai`.\n *\n * import { createAi } from 'best-email-builder/ai';\n * <EmailBuilder storage={storage} ai={createAi()} />\n *\n * Without it, the builder has no AI button, and none of the AI's code or\n * texts is in the application's bundle: the core never imports this folder.\n * With it, the builder gets what the module can do:\n *\n * generate the Generate button (an email written from a purpose)\n * rewrite \"Rewrite with AI\" in the properties of a text or a button\n * suggestSubjects \"Suggest subjects\" under the subject\n *\n * Where the model runs:\n *\n * createAi() the routes of createEmailBuilderHandler({ generator })\n * createAi({ baseUrl, headers }) the same routes elsewhere, or with the user's credentials\n * createAi({ provider, askKey: true }) the AI runs in the page with each user's own key\n * (OpenRouter, Gemini, Groq… — see providers.js);\n * the builder asks for it once (the Connect dialog)\n * createAi({ provider, apiKey }) the same with one key for all (prototypes: the key\n * is then in the page)\n * createAi({ model }) the generator runs in the page with this model\n * createAi({ generate, rewrite, … }) functions of your own: only those given are on\n *\n * Each of `generate`, `rewrite` and `suggestSubjects` can also be `false`,\n * to leave that part out — its button with it.\n */\n\nimport { createHttpCaller } from '../storage/client.js';\nimport { createEmailGenerator } from './generator.js';\nimport { AI_MESSAGES } from './i18n/index.js';\nimport { createKeyStore } from './keys.js';\nimport { providerEmailModel, resolveProvider } from './providers.js';\nimport ConnectAi from './ConnectAi.jsx';\nimport GenerateButton from './GenerateButton.jsx';\nimport RewriteText from './RewriteText.jsx';\nimport SubjectIdeas from './SubjectIdeas.jsx';\n\n/**\n * @typedef {import('./generator.js').GenerateInput} GenerateInput\n * @typedef {import('./generator.js').GeneratedEmail} GeneratedEmail\n * @typedef {import('./rewrite.js').RewriteInput} RewriteInput\n * @typedef {import('./subjects.js').SubjectsInput} SubjectsInput\n * @typedef {{signal?: AbortSignal}} CallOptions\n * @typedef {(input: GenerateInput, options?: CallOptions) => Promise<GeneratedEmail | Omit<GeneratedEmail, 'warnings'|'attempts'>>} GenerateFn\n * @typedef {(input: RewriteInput, options?: CallOptions) => Promise<{text: string}>} RewriteFn\n * @typedef {(input: SubjectsInput, options?: CallOptions) => Promise<{subjects: string[]}>} SuggestSubjectsFn\n *\n * @typedef {{\n * baseUrl?: string,\n * headers?: Record<string, string> | (() => Record<string, string>),\n * fetch?: typeof fetch,\n * model?: import('./generator.js').EmailModel,\n * provider?: string | Partial<import('./providers.js').AiProvider>,\n * apiKey?: string,\n * askKey?: boolean,\n * compile?: (mjml: string) => Promise<{warnings?: {message: string}[]}>,\n * maxAttempts?: number,\n * generate?: GenerateFn | boolean,\n * rewrite?: RewriteFn | boolean,\n * suggestSubjects?: SuggestSubjectsFn | boolean,\n * }} AiOptions\n * `provider`: an id of `AI_PROVIDERS` ('openrouter', 'gemini', 'groq'…), or\n * `{ id, model, baseUrl, headers }` for another model or an endpoint of your\n * own. `apiKey`: one key for every user. `askKey`: each user connects their\n * own (off by default). `compile`: with `provider` or `model`, MJML's own\n * warnings fed back to the model (see createEmailGenerator).\n *\n * @typedef {Readonly<{\n * generate: GenerateFn | null,\n * rewrite: RewriteFn | null,\n * suggestSubjects: SuggestSubjectsFn | null,\n * key: import('./keys.js').AiKey | null,\n * messages: Record<string, Record<string, string>>,\n * components: Readonly<{Generate: typeof GenerateButton, Rewrite: typeof RewriteText, Subjects: typeof SubjectIdeas, Connect: typeof ConnectAi | null}>,\n * }>} EmailBuilderAi\n * What the builder reads. `key`: the user's key, with `askKey` (keys.js).\n * `messages`: the module's own translations, by locale, merged under the\n * host's `messages`. `components.Connect`: the dialog that asks for the key.\n */\n\nconst PARTS = /** @type {const} */ (['generate', 'rewrite', 'suggestSubjects']);\nconst ROUTES = { generate: '/generate', rewrite: '/rewrite', suggestSubjects: '/subjects' };\n\n/**\n * @param {AiOptions} [options]\n * @returns {EmailBuilderAi}\n */\nexport function createAi(options = {}) {\n if (!options || typeof options !== 'object') throw new TypeError('createAi: options must be an object');\n const { model, maxAttempts, compile, baseUrl, headers, fetch: fetchImpl, provider: providerOption, apiKey, askKey = false } = options;\n const usesProvider = providerOption !== undefined;\n const http = baseUrl !== undefined || headers !== undefined || (fetchImpl !== undefined && !usesProvider);\n if (model !== undefined && typeof model !== 'function') throw new TypeError('createAi: `model` must be a function');\n if (model && http) throw new TypeError('createAi: pass `model` (the AI runs in the page) or `baseUrl` (on the server), not both');\n if (usesProvider && (model || http)) {\n throw new TypeError('createAi: `provider` runs the AI in the page — not with `model`, `baseUrl` or `headers` (give the provider `{ id, model, baseUrl, headers }`)');\n }\n if (!usesProvider && (apiKey !== undefined || options.askKey !== undefined)) {\n throw new TypeError('createAi: `apiKey` and `askKey` go with a `provider` (e.g. provider: \"openrouter\")');\n }\n if (apiKey !== undefined && (typeof apiKey !== 'string' || !apiKey)) throw new TypeError('createAi: `apiKey` must be a non-empty string');\n if (compile !== undefined && (typeof compile !== 'function' || (!model && !usesProvider))) {\n throw new TypeError('createAi: `compile` must be a function, and goes with `provider` or `model` (the AI in the page)');\n }\n if (typeof askKey !== 'boolean') throw new TypeError('createAi: `askKey` must be true or false');\n if (apiKey && askKey) throw new TypeError('createAi: `apiKey` (one key for all) or `askKey` (each user\\'s own), not both');\n const provider = usesProvider ? resolveProvider(providerOption) : null;\n if (provider && !apiKey && !askKey && !provider.keyless) {\n throw new TypeError(`createAi: ${provider.name} needs a key — \\`apiKey\\`, or \\`askKey: true\\` to let each user connect their own`);\n }\n for (const part of PARTS) {\n const value = options[part];\n if (value !== undefined && typeof value !== 'boolean' && typeof value !== 'function') {\n throw new TypeError(`createAi: \\`${part}\\` must be a function, true or false`);\n }\n }\n\n // Functions of the host's own and nothing else: only those are on.\n const ownOnly = !model && !http && PARTS.some((part) => typeof options[part] === 'function');\n /** @type {Partial<Record<typeof PARTS[number], Function>>} */\n let transport = {};\n const key = provider && askKey ? createKeyStore(provider) : null;\n if (provider) {\n const generator = createEmailGenerator({\n model: providerEmailModel({\n provider,\n fetch: fetchImpl,\n // The user's key, asked when there is none ('' when they cancel: KEY_MISSING).\n apiKey: key ? async () => key.get() || (await key.request()) : apiKey,\n }),\n maxAttempts,\n compile,\n });\n for (const part of PARTS) transport[part] = key ? askedAgain(generator[part], key) : generator[part];\n } else if (model) {\n transport = createEmailGenerator({ model, maxAttempts, compile });\n } else if (!ownOnly) {\n const { call } = createHttpCaller({ baseUrl, headers, fetch: fetchImpl });\n for (const part of PARTS) {\n transport[part] = (/** @type {object} */ input, /** @type {CallOptions} */ { signal } = {}) =>\n call(ROUTES[part], { method: 'POST', body: JSON.stringify(input), signal });\n }\n }\n\n /** @param {typeof PARTS[number]} part */\n const resolve = (part) => {\n const value = options[part];\n if (typeof value === 'function') return value;\n if (value === false) return null;\n return transport[part] || null;\n };\n\n return Object.freeze({\n generate: /** @type {GenerateFn|null} */ (resolve('generate')),\n rewrite: /** @type {RewriteFn|null} */ (resolve('rewrite')),\n suggestSubjects: /** @type {SuggestSubjectsFn|null} */ (resolve('suggestSubjects')),\n key,\n messages: AI_MESSAGES,\n components: Object.freeze({ Generate: GenerateButton, Rewrite: RewriteText, Subjects: SubjectIdeas, Connect: key ? ConnectAi : null }),\n });\n}\n\n/**\n * A key the provider refuses is forgotten and asked again, once, in the same\n * action: the user pastes a good one and the email still comes.\n * @param {Function} run\n * @param {import('./keys.js').AiKey} key\n */\nfunction askedAgain(run, key) {\n return async (/** @type {any} */ input, /** @type {any} */ options) => {\n try {\n return await run(input, options);\n } catch (error) {\n if (/** @type {any} */ (error)?.code !== 'KEY_REFUSED' || options?.signal?.aborted) throw error;\n key.refuse();\n try {\n return await run(input, options);\n } catch (again) {\n if (/** @type {any} */ (again)?.code === 'KEY_REFUSED') key.refuse();\n throw again;\n }\n }\n };\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","TriangleAlert","jsx","jsxs","useState","useRef","useEffect","TriangleAlert","useEffect","useRef","useState","Loader2","Sparkles","TriangleAlert","jsx","jsxs","useState","useRef","useEffect","Loader2","Sparkles","TriangleAlert","useEffect","useRef","useState","Check","Loader2","Sparkles","TriangleAlert","jsx","jsxs","useState","useRef","useEffect","Loader2","Sparkles","Check","TriangleAlert"]}
|
package/dist/builder.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EmailBuilder,
|
|
3
3
|
EmailBuilderBoundary
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PB7YZ3WP.js";
|
|
5
5
|
import {
|
|
6
6
|
STARTER_MJML,
|
|
7
|
-
StorageError,
|
|
8
7
|
TEMPLATE_GALLERY,
|
|
9
8
|
buildGallery,
|
|
10
|
-
createHttpStorage,
|
|
11
|
-
createMemoryStorage,
|
|
12
9
|
createStarterMjml
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-3LKF4QBL.js";
|
|
11
|
+
import "./chunk-ECHGJZ2W.js";
|
|
12
|
+
import "./chunk-UFKCLU2D.js";
|
|
13
|
+
import "./chunk-KXJJSVN5.js";
|
|
14
|
+
import {
|
|
15
|
+
StorageError,
|
|
16
|
+
createHttpStorage,
|
|
17
|
+
createMemoryStorage
|
|
18
|
+
} from "./chunk-LXR4QTGP.js";
|
|
19
|
+
import {
|
|
20
|
+
EMAIL_BUILDER_VERSION
|
|
21
|
+
} from "./chunk-7U6BAKM7.js";
|
|
22
|
+
import "./chunk-2RZHGAH5.js";
|
|
23
|
+
import "./chunk-5SASKCUC.js";
|
|
15
24
|
import {
|
|
16
25
|
CUSTOM_GROUP,
|
|
17
26
|
DEFAULT_VARIABLES,
|
|
18
|
-
EMAIL_BUILDER_VERSION,
|
|
19
27
|
LOCALES,
|
|
20
28
|
createTranslator,
|
|
21
29
|
defineVariables,
|
|
22
30
|
mergeVariables,
|
|
23
31
|
translatorFor
|
|
24
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-DFNHICTF.js";
|
|
25
33
|
export {
|
|
26
34
|
CUSTOM_GROUP,
|
|
27
35
|
DEFAULT_VARIABLES,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/core/mjmlAttributes.js
|
|
2
|
+
var LONGHAND_SPACING = /* @__PURE__ */ new Set(["padding"]);
|
|
3
|
+
var SIDES = (
|
|
4
|
+
/** @type {const} */
|
|
5
|
+
["top", "right", "bottom", "left"]
|
|
6
|
+
);
|
|
7
|
+
function parseSpacing(value) {
|
|
8
|
+
const parts = String(value || "").trim().split(/\s+/).filter(Boolean);
|
|
9
|
+
const [a = "0px", b = a, c = a, d = b] = parts;
|
|
10
|
+
return [a, b, c, d];
|
|
11
|
+
}
|
|
12
|
+
function formatSpacing(sides) {
|
|
13
|
+
const [t, r, b, l] = sides.map((s) => String(s || "0px"));
|
|
14
|
+
if (t === b && r === l) return t === r ? t : `${t} ${r}`;
|
|
15
|
+
if (r === l) return `${t} ${r} ${b}`;
|
|
16
|
+
return `${t} ${r} ${b} ${l}`;
|
|
17
|
+
}
|
|
18
|
+
function cleanAttributeValue(value) {
|
|
19
|
+
return String(value ?? "").replace(/["<>\r\n]/g, "").trim();
|
|
20
|
+
}
|
|
21
|
+
function toAttributes(values) {
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [key, raw] of Object.entries(values || {})) {
|
|
24
|
+
const value = raw === null || raw === void 0 ? "" : cleanAttributeValue(raw);
|
|
25
|
+
if (LONGHAND_SPACING.has(key)) {
|
|
26
|
+
const sides = value ? parseSpacing(value) : null;
|
|
27
|
+
SIDES.forEach((side, i) => {
|
|
28
|
+
out[`${key}-${side}`] = sides ? sides[i] : null;
|
|
29
|
+
});
|
|
30
|
+
out[key] = null;
|
|
31
|
+
} else {
|
|
32
|
+
out[key] = value || null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function toValues(attributes, properties) {
|
|
38
|
+
const values = {};
|
|
39
|
+
for (const { key } of properties) {
|
|
40
|
+
if (LONGHAND_SPACING.has(key)) {
|
|
41
|
+
const base = parseSpacing(String(attributes[key] ?? ""));
|
|
42
|
+
const sides = SIDES.map((side, i) => {
|
|
43
|
+
const v = attributes[`${key}-${side}`];
|
|
44
|
+
return v === void 0 || v === null || v === "" ? base[i] : String(v);
|
|
45
|
+
});
|
|
46
|
+
const hasAny = attributes[key] || SIDES.some((side) => attributes[`${key}-${side}`]);
|
|
47
|
+
values[key] = hasAny ? formatSpacing(sides) : "";
|
|
48
|
+
} else {
|
|
49
|
+
const v = attributes[key];
|
|
50
|
+
values[key] = v === void 0 || v === null ? "" : String(v);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
LONGHAND_SPACING,
|
|
58
|
+
SIDES,
|
|
59
|
+
parseSpacing,
|
|
60
|
+
formatSpacing,
|
|
61
|
+
toAttributes,
|
|
62
|
+
toValues
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=chunk-2RZHGAH5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/mjmlAttributes.js"],"sourcesContent":["// @ts-check\n/**\n * MJML attributes — reading and writing properties panel values.\n *\n * This is knowledge of the LANGUAGE (MJML), not of the engine: it lives in\n * the core, and every adapter benefits from it.\n *\n * ── PADDING IS WRITTEN IN LONGHAND ─────────────────────────────────────────\n * An MJML element carries both `padding` and `padding-top/-right/-bottom/\n * -left`, and the longhand wins. The canvas, starting from the defaults\n * (`padding-top=\"10px\"`…), ignored a `padding=\"40px 60px\"` that the sent\n * email did apply: the screen lied. So `padding` is always written as four\n * attributes, and read back the same way.\n *\n * `inner-padding` (button) has no longhand in MJML: it stays as is.\n *\n * ── NO QUOTES IN A VALUE ───────────────────────────────────────────────────\n * The engine export writes attributes unescaped: a `\"` in a value would break\n * the MJML. Double quotes, angle brackets and line breaks are stripped on\n * write.\n */\n\n/** Spacing attributes that have a longhand form in MJML. */\nexport const LONGHAND_SPACING = new Set(['padding']);\n\nexport const SIDES = /** @type {const} */ (['top', 'right', 'bottom', 'left']);\n\n/**\n * \"10px 25px\" → ['10px', '25px', '10px', '25px'] (CSS 1-to-4 value rule).\n * @param {string|null|undefined} value\n * @returns {[string, string, string, string]}\n */\nexport function parseSpacing(value) {\n const parts = String(value || '')\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n const [a = '0px', b = a, c = a, d = b] = parts;\n return [a, b, c, d];\n}\n\n/**\n * Shortest form of a four-sided spacing.\n * @param {string[]} sides\n */\nexport function formatSpacing(sides) {\n const [t, r, b, l] = sides.map((s) => String(s || '0px'));\n if (t === b && r === l) return t === r ? t : `${t} ${r}`;\n if (r === l) return `${t} ${r} ${b}`;\n return `${t} ${r} ${b} ${l}`;\n}\n\n/** Attribute value safe for an unescaped export. @param {unknown} value */\nexport function cleanAttributeValue(value) {\n return String(value ?? '')\n .replace(/[\"<>\\r\\n]/g, '')\n .trim();\n}\n\n/**\n * Entered values → MJML attributes to set (`null` = attribute to remove).\n *\n * @param {Record<string, string|null|undefined>} values\n * @returns {Record<string, string|null>}\n */\nexport function toAttributes(values) {\n /** @type {Record<string, string|null>} */\n const out = {};\n for (const [key, raw] of Object.entries(values || {})) {\n const value = raw === null || raw === undefined ? '' : cleanAttributeValue(raw);\n if (LONGHAND_SPACING.has(key)) {\n const sides = value ? parseSpacing(value) : null;\n SIDES.forEach((side, i) => {\n out[`${key}-${side}`] = sides ? sides[i] : null;\n });\n out[key] = null;\n } else {\n out[key] = value || null;\n }\n }\n return out;\n}\n\n/**\n * Element attributes → a block's property values.\n *\n * @param {Record<string, unknown>} attributes\n * @param {{key: string}[]} properties\n * @returns {Record<string, string>}\n */\nexport function toValues(attributes, properties) {\n /** @type {Record<string, string>} */\n const values = {};\n for (const { key } of properties) {\n if (LONGHAND_SPACING.has(key)) {\n const base = parseSpacing(String(attributes[key] ?? ''));\n const sides = SIDES.map((side, i) => {\n const v = attributes[`${key}-${side}`];\n return v === undefined || v === null || v === '' ? base[i] : String(v);\n });\n const hasAny = attributes[key] || SIDES.some((side) => attributes[`${key}-${side}`]);\n values[key] = hasAny ? formatSpacing(sides) : '';\n } else {\n const v = attributes[key];\n values[key] = v === undefined || v === null ? '' : String(v);\n }\n }\n return values;\n}\n"],"mappings":";AAuBO,IAAM,mBAAmB,oBAAI,IAAI,CAAC,SAAS,CAAC;AAE5C,IAAM;AAAA;AAAA,EAA8B,CAAC,OAAO,SAAS,UAAU,MAAM;AAAA;AAOrE,SAAS,aAAa,OAAO;AAClC,QAAM,QAAQ,OAAO,SAAS,EAAE,EAC7B,KAAK,EACL,MAAM,KAAK,EACX,OAAO,OAAO;AACjB,QAAM,CAAC,IAAI,OAAO,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI;AACzC,SAAO,CAAC,GAAG,GAAG,GAAG,CAAC;AACpB;AAMO,SAAS,cAAc,OAAO;AACnC,QAAM,CAAC,GAAG,GAAG,GAAG,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,OAAO,KAAK,KAAK,CAAC;AACxD,MAAI,MAAM,KAAK,MAAM,EAAG,QAAO,MAAM,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC;AACtD,MAAI,MAAM,EAAG,QAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;AAClC,SAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AAC5B;AAGO,SAAS,oBAAoB,OAAO;AACzC,SAAO,OAAO,SAAS,EAAE,EACtB,QAAQ,cAAc,EAAE,EACxB,KAAK;AACV;AAQO,SAAS,aAAa,QAAQ;AAEnC,QAAM,MAAM,CAAC;AACb,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,UAAU,CAAC,CAAC,GAAG;AACrD,UAAM,QAAQ,QAAQ,QAAQ,QAAQ,SAAY,KAAK,oBAAoB,GAAG;AAC9E,QAAI,iBAAiB,IAAI,GAAG,GAAG;AAC7B,YAAM,QAAQ,QAAQ,aAAa,KAAK,IAAI;AAC5C,YAAM,QAAQ,CAAC,MAAM,MAAM;AACzB,YAAI,GAAG,GAAG,IAAI,IAAI,EAAE,IAAI,QAAQ,MAAM,CAAC,IAAI;AAAA,MAC7C,CAAC;AACD,UAAI,GAAG,IAAI;AAAA,IACb,OAAO;AACL,UAAI,GAAG,IAAI,SAAS;AAAA,IACtB;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,SAAS,YAAY,YAAY;AAE/C,QAAM,SAAS,CAAC;AAChB,aAAW,EAAE,IAAI,KAAK,YAAY;AAChC,QAAI,iBAAiB,IAAI,GAAG,GAAG;AAC7B,YAAM,OAAO,aAAa,OAAO,WAAW,GAAG,KAAK,EAAE,CAAC;AACvD,YAAM,QAAQ,MAAM,IAAI,CAAC,MAAM,MAAM;AACnC,cAAM,IAAI,WAAW,GAAG,GAAG,IAAI,IAAI,EAAE;AACrC,eAAO,MAAM,UAAa,MAAM,QAAQ,MAAM,KAAK,KAAK,CAAC,IAAI,OAAO,CAAC;AAAA,MACvE,CAAC;AACD,YAAM,SAAS,WAAW,GAAG,KAAK,MAAM,KAAK,CAAC,SAAS,WAAW,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;AACnF,aAAO,GAAG,IAAI,SAAS,cAAc,KAAK,IAAI;AAAA,IAChD,OAAO;AACL,YAAM,IAAI,WAAW,GAAG;AACxB,aAAO,GAAG,IAAI,MAAM,UAAa,MAAM,OAAO,KAAK,OAAO,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|