best-email-builder 0.5.0 → 0.6.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 (54) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +50 -10
  3. package/dist/ai-server.js +6 -4
  4. package/dist/ai.js +167 -39
  5. package/dist/ai.js.map +1 -1
  6. package/dist/builder.js +9 -9
  7. package/dist/{chunk-ECHGJZ2W.js → chunk-4U7L2NYQ.js} +32 -3
  8. package/dist/chunk-4U7L2NYQ.js.map +1 -0
  9. package/dist/{chunk-6D3SLT5W.js → chunk-5W4TUUPD.js} +4 -4
  10. package/dist/{chunk-LYMJ2TBX.js → chunk-CNJZLZRH.js} +2 -2
  11. package/dist/{chunk-DFNHICTF.js → chunk-JI36FNWE.js} +3 -1
  12. package/dist/chunk-JI36FNWE.js.map +1 -0
  13. package/dist/{chunk-LXR4QTGP.js → chunk-JU3OUO6X.js} +3 -3
  14. package/dist/{chunk-PB7YZ3WP.js → chunk-NC2YDZSP.js} +19 -65
  15. package/dist/chunk-NC2YDZSP.js.map +1 -0
  16. package/dist/{chunk-MPX5CWTZ.js → chunk-NZIQF7IW.js} +172 -29
  17. package/dist/chunk-NZIQF7IW.js.map +1 -0
  18. package/dist/{chunk-2RZHGAH5.js → chunk-OWVXKMLI.js} +59 -2
  19. package/dist/chunk-OWVXKMLI.js.map +1 -0
  20. package/dist/{chunk-7U6BAKM7.js → chunk-QPAZLCPO.js} +3 -3
  21. package/dist/{chunk-7U6BAKM7.js.map → chunk-QPAZLCPO.js.map} +1 -1
  22. package/dist/{chunk-KXJJSVN5.js → chunk-SAIW46IC.js} +2 -2
  23. package/dist/{chunk-5SASKCUC.js → chunk-W424KPCR.js} +2 -2
  24. package/dist/{chunk-3LKF4QBL.js → chunk-XEFBPP5Y.js} +4 -4
  25. package/dist/index.js +11 -11
  26. package/dist/library.js +5 -5
  27. package/dist/server.js +12 -29
  28. package/dist/server.js.map +1 -1
  29. package/dist/styles.css +1 -1
  30. package/dist/types/ai/ConnectAi.d.ts +4 -0
  31. package/dist/types/ai/claude.d.ts +30 -5
  32. package/dist/types/ai/createAi.d.ts +5 -3
  33. package/dist/types/ai/draft.d.ts +12 -0
  34. package/dist/types/ai/index.d.ts +2 -2
  35. package/dist/types/ai/keys.d.ts +15 -26
  36. package/dist/types/ai/providers.d.ts +24 -2
  37. package/dist/types/ai/server.d.ts +1 -1
  38. package/dist/types/core/constants.d.ts +1 -1
  39. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  40. package/dist/types/renderer/prepareMjml.d.ts +8 -2
  41. package/dist/types/server/index.d.ts +1 -1
  42. package/dist/types/server/render.d.ts +5 -2
  43. package/package.json +1 -1
  44. package/dist/chunk-2RZHGAH5.js.map +0 -1
  45. package/dist/chunk-DFNHICTF.js.map +0 -1
  46. package/dist/chunk-ECHGJZ2W.js.map +0 -1
  47. package/dist/chunk-MPX5CWTZ.js.map +0 -1
  48. package/dist/chunk-PB7YZ3WP.js.map +0 -1
  49. /package/dist/{chunk-6D3SLT5W.js.map → chunk-5W4TUUPD.js.map} +0 -0
  50. /package/dist/{chunk-LYMJ2TBX.js.map → chunk-CNJZLZRH.js.map} +0 -0
  51. /package/dist/{chunk-LXR4QTGP.js.map → chunk-JU3OUO6X.js.map} +0 -0
  52. /package/dist/{chunk-KXJJSVN5.js.map → chunk-SAIW46IC.js.map} +0 -0
  53. /package/dist/{chunk-5SASKCUC.js.map → chunk-W424KPCR.js.map} +0 -0
  54. /package/dist/{chunk-3LKF4QBL.js.map → chunk-XEFBPP5Y.js.map} +0 -0
package/dist/ai.js.map CHANGED
@@ -1 +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"]}
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 * Given several providers, the user chooses one in the same dialog; the\n * choice is kept too.\n *\n * ai.key.get(id?) the key in use, or the one kept for that provider ('' 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.choose(id) use another of the providers (its kept key, if any)\n * ai.key.subscribe(fn) called on every change; returns the unsubscribe\n * ai.key.provider the provider in use: name, model, where to get a key, what is free\n * ai.key.providers all those the user can choose from\n */\n\n/** @typedef {import('./providers.js').AiProvider} AiProvider */\n\nconst STORAGE_PREFIX = 'best-email-builder:ai-key:';\nconst CHOICE_SLOT = 'best-email-builder:ai-provider';\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 {Readonly<{id: string, name: string, model: string|null, keyUrl: string|null, keyHint: string, free: string|null}>} ProviderChoice\n * @typedef {{\n * provider: ProviderChoice,\n * providers: ReadonlyArray<ProviderChoice>,\n * get: (id?: string) => string,\n * set: (key: string) => void,\n * forget: () => void,\n * request: () => Promise<string>,\n * choose: (id: string) => void,\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/** @param {AiProvider} provider @returns {ProviderChoice} */\nconst choiceOf = (provider) =>\n Object.freeze({\n id: provider.id,\n name: provider.name,\n model: provider.model ?? null,\n keyUrl: provider.keyUrl,\n keyHint: provider.keyHint,\n free: provider.free,\n });\n\n/**\n * @param {AiProvider | AiProvider[]} providerOrList one provider, or those the user chooses from\n * @returns {AiKey}\n */\nexport function createKeyStore(providerOrList) {\n const providers = Object.freeze((Array.isArray(providerOrList) ? providerOrList : [providerOrList]).map(choiceOf));\n if (!providers.length) throw new TypeError('createKeyStore: at least one provider');\n const known = (/** @type {unknown} */ id) => providers.some((provider) => provider.id === id);\n /** Kept here too: a browser that does not store still works for the page's life. */\n const memory = new Map();\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 /** @param {string} slot */\n const read = (slot) => {\n try {\n return browserStorage()?.getItem(slot) || '';\n } catch {\n return '';\n }\n };\n /** @param {string} slot @param {string} value */\n const write = (slot, 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 /** @param {string} id */\n const keyOf = (id) => (memory.has(id) ? memory.get(id) : read(STORAGE_PREFIX + id));\n /** @param {string} id @param {string} value */\n const keep = (id, value) => {\n memory.set(id, value);\n write(STORAGE_PREFIX + id, value);\n };\n // The provider chosen last time; else the first one with a key; else the first.\n const kept = read(CHOICE_SLOT);\n let current = known(kept) ? kept : (providers.find((provider) => keyOf(provider.id)) || providers[0]).id;\n /** @param {string} [id] */\n const get = (id) => keyOf(id === undefined ? current : String(id));\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 keep(current, value);\n refused = false;\n }\n const waiting = pending;\n pending = null;\n changed();\n waiting?.resolve(value);\n };\n\n return Object.freeze({\n get provider() {\n return /** @type {ProviderChoice} */ (providers.find((provider) => provider.id === current));\n },\n providers,\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 keep(current, '');\n changed();\n },\n choose: (/** @type {string} */ id) => {\n if (!known(id)) throw new TypeError(`ai.key.choose: one of ${providers.map((provider) => provider.id).join(', ')}`);\n if (id === current) return;\n current = id;\n refused = false;\n write(CHOICE_SLOT, id);\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 keep(current, '');\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 { cn } from '../ui/primitives/cn.js';\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 *\n * Given several providers (`createAi({ provider: [...] })`), the user chooses\n * one at the top; a provider whose key this browser already keeps needs no\n * new one.\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 [chosenId, setChosenId] = useState(aiKey.provider.id);\n const inputRef = useRef(/** @type {HTMLInputElement|null} */ (null));\n const choicesRef = useRef(/** @type {HTMLDivElement|null} */ (null));\n const inputId = useId();\n const hintId = useId();\n const choices = aiKey.providers;\n const chosen = choices.find((provider) => provider.id === chosenId) || aiKey.provider;\n const { name, keyUrl, keyHint, free } = chosen;\n\n // Each time it opens: the provider in use, an empty field, the key hidden.\n useEffect(() => {\n if (!state.asking) return;\n setChosenId(aiKey.provider.id);\n setValue('');\n setShown(false);\n }, [state.asking, aiKey]);\n\n const key = value.trim();\n // The key this browser keeps for the provider chosen (none for one refused).\n const saved = aiKey.get(chosen.id);\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 const refused = state.refused && chosen.id === aiKey.provider.id;\n\n const submit = (/** @type {React.FormEvent} */ event) => {\n event.preventDefault();\n const answer = key || saved;\n if (!answer) return;\n if (chosen.id !== aiKey.provider.id) aiKey.choose(chosen.id);\n aiKey.answer(answer);\n };\n\n /** @param {string} id */\n const choose = (id) => {\n setChosenId(id);\n setValue('');\n };\n\n /** Arrows move through the providers and choose, as in any radio group. */\n const onChoicesKeyDown = (/** @type {React.KeyboardEvent} */ event) => {\n const at = choices.findIndex((provider) => provider.id === chosen.id);\n const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];\n const to = step ? (at + step + choices.length) % choices.length : event.key === 'Home' ? 0 : event.key === 'End' ? choices.length - 1 : -1;\n if (to === -1) return;\n event.preventDefault();\n choose(choices[to].id);\n /** @type {HTMLElement|undefined} */ (choicesRef.current?.querySelectorAll('[role=\"radio\"]')[to])?.focus();\n };\n\n return (\n <Dialog open={state.asking} onOpenChange={(open) => !open && aiKey.answer(null)}>\n <DialogContent\n className={cn('eb:gap-0 eb:overflow-hidden eb:p-0', choices.length > 1 ? 'eb:sm:max-w-xl' : '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 {choices.length > 1 && (\n <div\n ref={choicesRef}\n role=\"radiogroup\"\n aria-label={t('AI provider')}\n className=\"eb:grid eb:grid-cols-2 eb:gap-1.5 eb:sm:grid-cols-3\"\n onKeyDown={onChoicesKeyDown}\n data-testid=\"eb-ai-connect-providers\"\n >\n {choices.map((provider) => {\n const checked = provider.id === chosen.id;\n return (\n <button\n key={provider.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={checked}\n tabIndex={checked ? 0 : -1}\n onClick={() => choose(provider.id)}\n className={cn(\n 'eb:flex eb:min-w-0 eb:flex-col eb:gap-0.5 eb:rounded-md eb:border eb:px-2.5 eb:py-2 eb:text-left eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50',\n checked ? 'eb:border-primary eb:bg-primary/10' : 'eb:border-border eb:hover:bg-muted'\n )}\n >\n <span className=\"eb:flex eb:items-center eb:gap-1.5 eb:text-[13px] eb:leading-tight eb:font-medium eb:text-foreground\">\n <span className=\"eb:truncate\">{provider.name}</span>\n {provider.free && <Gift className=\"eb:size-3 eb:shrink-0 eb:text-success\" aria-label={t(provider.free)} />}\n {aiKey.get(provider.id) && (\n <span className=\"eb:ml-auto eb:size-1.5 eb:shrink-0 eb:rounded-full eb:bg-success\" title={t('Key saved')} aria-label={t('Key saved')} />\n )}\n </span>\n {provider.model && (\n <span className=\"eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted\" title={provider.model}>\n {provider.model}\n </span>\n )}\n </button>\n );\n })}\n </div>\n )}\n\n {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 ) : saved && !key ? (\n t('A key for {provider} is saved in this browser: Connect uses it, or paste another one.', { provider: name })\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 && !saved} 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: [...], askKey: true })\n * the same, the user choosing the provider in that\n * dialog (Claude too: claudeProvider in claude.js)\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?: ProviderOption | ProviderOption[],\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 — or a list of them, with `askKey`: the user chooses (the first is\n * offered first). `apiKey`: one key for every user. `askKey`: each user\n * connects their 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\n/** @typedef {string | Partial<import('./providers.js').AiProvider>} ProviderOption */\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 choice = Array.isArray(providerOption);\n if (choice && (!providerOption.length || !askKey)) {\n throw new TypeError('createAi: a list of providers is for `askKey: true` — each user chooses one and connects their own key');\n }\n /** @type {import('./providers.js').AiProvider[]} */\n const providers = usesProvider ? (choice ? providerOption : [providerOption]).map(providerOf) : [];\n if (new Set(providers.map((provider) => provider.id)).size !== providers.length) throw new TypeError('createAi: each provider once');\n const [provider = null] = providers;\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(providers) : null;\n if (provider) {\n /** @type {Map<string, import('./generator.js').EmailModel>} */\n const models = new Map();\n const keyFor = () => (key ? key.get() : apiKey || '');\n /** The model of the provider in use, made once. @param {string} id */\n const modelOf = (id) => {\n if (!models.has(id)) {\n const chosen = /** @type {import('./providers.js').AiProvider} */ (providers.find((item) => item.id === id));\n models.set(\n id,\n chosen.createModel\n ? chosen.createModel({ apiKey: keyFor, fetch: fetchImpl })\n : providerEmailModel({ provider: chosen, fetch: fetchImpl, apiKey: keyFor })\n );\n }\n return /** @type {import('./generator.js').EmailModel} */ (models.get(id));\n };\n /** @type {import('./generator.js').EmailModel} */\n const model = async (request) => {\n // The user's key, asked when there is none — they may choose another\n // provider there ('' when they cancel: KEY_MISSING).\n if (key && !key.get() && !providers.find((item) => item.id === key.provider.id)?.keyless) await key.request();\n return modelOf(key ? key.provider.id : provider.id)(request);\n };\n const generator = createEmailGenerator({ model, maxAttempts, compile });\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 provider of the list: an id, `{ id, model, … }`, or one with its own\n * `createModel` (claudeProvider) taken as it is.\n * @param {ProviderOption} option\n * @returns {import('./providers.js').AiProvider}\n */\nfunction providerOf(option) {\n if (option && typeof option === 'object' && typeof option.createModel === 'function') {\n if (!option.id || !option.name) throw new TypeError('createAi: a provider with `createModel` needs an `id` and a `name`');\n return /** @type {import('./providers.js').AiProvider} */ (option);\n }\n return resolveProvider(/** @type {any} */ (option));\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","provider","model"]}
package/dist/builder.js CHANGED
@@ -1,26 +1,26 @@
1
1
  import {
2
2
  EmailBuilder,
3
3
  EmailBuilderBoundary
4
- } from "./chunk-PB7YZ3WP.js";
4
+ } from "./chunk-NC2YDZSP.js";
5
5
  import {
6
6
  STARTER_MJML,
7
7
  TEMPLATE_GALLERY,
8
8
  buildGallery,
9
9
  createStarterMjml
10
- } from "./chunk-3LKF4QBL.js";
11
- import "./chunk-ECHGJZ2W.js";
10
+ } from "./chunk-XEFBPP5Y.js";
11
+ import "./chunk-4U7L2NYQ.js";
12
12
  import "./chunk-UFKCLU2D.js";
13
- import "./chunk-KXJJSVN5.js";
13
+ import "./chunk-SAIW46IC.js";
14
14
  import {
15
15
  StorageError,
16
16
  createHttpStorage,
17
17
  createMemoryStorage
18
- } from "./chunk-LXR4QTGP.js";
18
+ } from "./chunk-JU3OUO6X.js";
19
19
  import {
20
20
  EMAIL_BUILDER_VERSION
21
- } from "./chunk-7U6BAKM7.js";
22
- import "./chunk-2RZHGAH5.js";
23
- import "./chunk-5SASKCUC.js";
21
+ } from "./chunk-QPAZLCPO.js";
22
+ import "./chunk-OWVXKMLI.js";
23
+ import "./chunk-W424KPCR.js";
24
24
  import {
25
25
  CUSTOM_GROUP,
26
26
  DEFAULT_VARIABLES,
@@ -29,7 +29,7 @@ import {
29
29
  defineVariables,
30
30
  mergeVariables,
31
31
  translatorFor
32
- } from "./chunk-DFNHICTF.js";
32
+ } from "./chunk-JI36FNWE.js";
33
33
  export {
34
34
  CUSTOM_GROUP,
35
35
  DEFAULT_VARIABLES,
@@ -5,7 +5,34 @@ import {
5
5
  parseColor,
6
6
  toHex,
7
7
  toHsl
8
- } from "./chunk-DFNHICTF.js";
8
+ } from "./chunk-JI36FNWE.js";
9
+
10
+ // src/renderer/prepareMjml.js
11
+ var MJ_IMAGE = /<mj-image\b((?:[^>"']|"[^"]*"|'[^']*')*?)\s*(?:\/>|>\s*<\/mj-image\s*>)/gi;
12
+ var HAS_SRC = /\ssrc\s*=\s*(?:"\s*[^"\s][^"]*"|'\s*[^'\s][^']*')/i;
13
+ function countImagesWithoutPicture(mjml) {
14
+ return [...String(mjml || "").matchAll(MJ_IMAGE)].filter(([, attrs]) => !HAS_SRC.test(attrs)).length;
15
+ }
16
+ function escapeXml(text) {
17
+ return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18
+ }
19
+ function prepareMjml(mjml, { title = null, preheader = null, lang = "en", emptyImages = "drop" } = {}) {
20
+ let out = String(mjml || "").replace(/<mj-title(?:\s[^>]*)?>[\s\S]*?<\/mj-title>/gi, "").replace(/<mj-preview(?:\s[^>]*)?>[\s\S]*?<\/mj-preview>/gi, "");
21
+ if (emptyImages !== "keep") out = out.replace(MJ_IMAGE, (element, attrs) => HAS_SRC.test(attrs) ? element : "");
22
+ out = out.replace(
23
+ /<mjml(\s[^>]*)?>/i,
24
+ (tag, attrs = "") => /\slang\s*=/.test(attrs) ? tag : `<mjml lang="${escapeXml(lang)}"${attrs}>`
25
+ );
26
+ const head = [
27
+ title && String(title).trim() ? `<mj-title>${escapeXml(String(title).trim())}</mj-title>` : "",
28
+ preheader && String(preheader).trim() ? `<mj-preview>${escapeXml(String(preheader).trim())}</mj-preview>` : ""
29
+ ].join("");
30
+ if (!head) return out;
31
+ if (/<mj-head(\s[^>]*)?>/i.test(out)) {
32
+ return out.replace(/<mj-head(\s[^>]*)?>/i, (tag) => `${tag}${head}`);
33
+ }
34
+ return out.replace(/<mjml(\s[^>]*)?>/i, (tag) => `${tag}<mj-head>${head}</mj-head>`);
35
+ }
9
36
 
10
37
  // src/features/brand/extract.js
11
38
  function decodeEntities(text) {
@@ -322,6 +349,8 @@ export {
322
349
  imageSize,
323
350
  logoWidthFor,
324
351
  BRAND_IMPORT_ERRORS,
325
- BRAND_NOTES
352
+ BRAND_NOTES,
353
+ countImagesWithoutPicture,
354
+ prepareMjml
326
355
  };
327
- //# sourceMappingURL=chunk-ECHGJZ2W.js.map
356
+ //# sourceMappingURL=chunk-4U7L2NYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/renderer/prepareMjml.js","../src/features/brand/extract.js","../src/features/brand/messages.js"],"sourcesContent":["// @ts-check\n/**\n * Prepares MJML before compilation — shared by the server preview, the test\n * send and saving.\n *\n * The subject and the preview text (preheader) are METADATA of the template,\n * typed outside the canvas: they do not live in the engine project. They are\n * written here, into `<mj-head>`, at compile time:\n *\n * `<mj-title>` document title (browser tab, some webmails);\n * `<mj-preview>` the grey text that follows the subject in the inbox.\n *\n * `lang` is set when missing: otherwise screen readers read the message with\n * the voice of the system language.\n *\n * An image with no picture yet (`src` empty or missing — the canvas shows a\n * grey frame, AI drafts leave them for the user) is left out: sent, it would\n * be a broken image in every inbox (a red cross in Outlook). A preview keeps\n * it (`emptyImages: 'keep'`) to draw the frame that is still to fill.\n */\n\n/** An `<mj-image>` element, self-closing or not; its attributes in group 1. */\nconst MJ_IMAGE = /<mj-image\\b((?:[^>\"']|\"[^\"]*\"|'[^']*')*?)\\s*(?:\\/>|>\\s*<\\/mj-image\\s*>)/gi;\nconst HAS_SRC = /\\ssrc\\s*=\\s*(?:\"\\s*[^\"\\s][^\"]*\"|'\\s*[^'\\s][^']*')/i;\n\n/**\n * How many images have no picture yet.\n * @param {string} mjml\n */\nexport function countImagesWithoutPicture(mjml) {\n return [...String(mjml || '').matchAll(MJ_IMAGE)].filter(([, attrs]) => !HAS_SRC.test(attrs)).length;\n}\n\nfunction escapeXml(text) {\n return String(text ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/**\n * @param {string} mjml\n * @param {{title?: string|null, preheader?: string|null, lang?: string, emptyImages?: 'drop'|'keep'}} [meta]\n * @returns {string}\n */\nexport function prepareMjml(mjml, { title = null, preheader = null, lang = 'en', emptyImages = 'drop' } = {}) {\n let out = String(mjml || '')\n .replace(/<mj-title(?:\\s[^>]*)?>[\\s\\S]*?<\\/mj-title>/gi, '')\n .replace(/<mj-preview(?:\\s[^>]*)?>[\\s\\S]*?<\\/mj-preview>/gi, '');\n if (emptyImages !== 'keep') out = out.replace(MJ_IMAGE, (element, attrs) => (HAS_SRC.test(attrs) ? element : ''));\n\n out = out.replace(/<mjml(\\s[^>]*)?>/i, (tag, attrs = '') =>\n /\\slang\\s*=/.test(attrs) ? tag : `<mjml lang=\"${escapeXml(lang)}\"${attrs}>`\n );\n\n const head = [\n title && String(title).trim() ? `<mj-title>${escapeXml(String(title).trim())}</mj-title>` : '',\n preheader && String(preheader).trim() ? `<mj-preview>${escapeXml(String(preheader).trim())}</mj-preview>` : '',\n ].join('');\n if (!head) return out;\n\n if (/<mj-head(\\s[^>]*)?>/i.test(out)) {\n return out.replace(/<mj-head(\\s[^>]*)?>/i, (tag) => `${tag}${head}`);\n }\n return out.replace(/<mjml(\\s[^>]*)?>/i, (tag) => `${tag}<mj-head>${head}</mj-head>`);\n}\n","// @ts-check\n/**\n * BRAND FROM A WEBSITE — what a company's home page says about its look:\n * its name, where its logo is, its main color, the color of its headings\n * and its font.\n *\n * Pure text work on what the server downloaded (src/server/brandImport.js\n * does the downloading, with its safety rules): no network, no DOM, so it\n * runs anywhere and is tested on fixtures.\n *\n * name og:site_name → JSON-LD Organization → application-name → the\n * part of <title> that matches the domain → the domain;\n * logo JSON-LD Organization logo → an <img> called \"logo\" (header\n * first) → apple-touch-icon; SVG and WebP are skipped later\n * (most email apps do not show them);\n * colors counted in the CSS, weighted by where they are used (buttons,\n * links, custom properties named primary/brand/accent, the\n * theme-color meta); grays, alert and social colors set aside;\n * font the body's, mapped to the nearest web-safe stack.\n */\n\nimport { FONT_OPTIONS, PALETTE } from '../../blocks/shared.js';\nimport { contrastRatio, parseColor, toHex, toHsl } from './brand.js';\n\n/**\n * @typedef {{\n * name: string,\n * logos: {url: string, from: 'schema'|'img'|'icon'}[],\n * stylesheets: string[],\n * css: string,\n * themeColor: string,\n * }} PageReading\n */\n\n/* ── HTML ───────────────────────────────────────────────────────────────── */\n\n/** @param {string} text */\nfunction decodeEntities(text) {\n return String(text ?? '')\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))\n .replace(/&quot;/g, '\"')\n .replace(/&#39;|&apos;/g, \"'\")\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&');\n}\n\n/**\n * The attributes of one start tag, names in lower case.\n * @param {string} tag `<img src=\"…\" class=logo>`\n * @returns {Record<string, string>}\n */\nexport function tagAttributes(tag) {\n /** @type {Record<string, string>} */\n const attributes = {};\n const inner = tag.replace(/^<[^\\s>/]+/, '').replace(/\\/?>$/, '');\n const pattern = /([^\\s\"'=<>/]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g;\n let match;\n while ((match = pattern.exec(inner))) {\n const name = match[1].toLowerCase();\n if (!(name in attributes)) attributes[name] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? '');\n }\n return attributes;\n}\n\n/** @param {string} html @param {string} name */\nconst startTags = (html, name) => html.match(new RegExp(`<${name}\\\\b[^>]*>`, 'gi')) || [];\n\n/** An http(s) address, resolved against the page; '' otherwise. @param {string|undefined} href @param {string} base */\nfunction resolve(href, base) {\n const value = String(href ?? '').trim();\n if (!value || /^(data|javascript|blob):/i.test(value)) return '';\n try {\n const url = new URL(value, base);\n return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : '';\n } catch {\n return '';\n }\n}\n\n/** The largest image of a srcset (\"a.png 1x, a@2x.png 2x\"). @param {string} srcset */\nfunction largestOf(srcset) {\n let best = { url: '', size: 0 };\n for (const part of srcset.split(/,\\s+/)) {\n const [url, descriptor = '1x'] = part.trim().split(/\\s+/);\n const size = parseFloat(descriptor) * (descriptor.endsWith('w') ? 1 : 1000);\n if (url && size > best.size) best = { url, size };\n }\n return best.url;\n}\n\n/** Objects of a JSON-LD script, flattened (@graph, arrays). @param {unknown} value @returns {any[]} */\nfunction jsonLdNodes(value) {\n if (Array.isArray(value)) return value.flatMap(jsonLdNodes);\n if (!value || typeof value !== 'object') return [];\n const node = /** @type {any} */ (value);\n return [node, ...jsonLdNodes(node['@graph']), ...jsonLdNodes(node.publisher), ...jsonLdNodes(node.brand)];\n}\n\nconst ORGANIZATION = /organization|corporation|business|brand|store|restaurant|company|ngo|school|airline|bank|agency/i;\n\n/** The words of a host name that name the company: \"www.acme-corp.co.uk\" → \"acme corp\". @param {string} host */\nfunction hostWords(host) {\n const labels = host.toLowerCase().replace(/^www\\d?\\./, '').split('.');\n const main = labels.length > 2 && labels[labels.length - 2].length <= 3 ? labels[labels.length - 3] : labels[0];\n return main.replace(/[-_]+/g, ' ');\n}\n\n/** @param {string} text */\nconst squash = (text) => text.toLowerCase().replace(/[^a-z0-9À-￿]+/g, '');\n\n/** @param {unknown} value */\nconst cleanName = (value) =>\n decodeEntities(String(value ?? ''))\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 80);\n\n/**\n * What a home page says about its brand.\n * @param {string} html\n * @param {string} pageUrl the address it was read from (after redirects)\n * @returns {PageReading}\n */\nexport function readPage(html, pageUrl) {\n const source = String(html ?? '');\n const head = source.slice(0, 400_000);\n const baseTag = startTags(head, 'base').map(tagAttributes).find((a) => a.href);\n const base = resolve(baseTag?.href, pageUrl) || pageUrl;\n const host = (() => {\n try {\n return new URL(pageUrl).hostname;\n } catch {\n return '';\n }\n })();\n\n /** @type {Record<string, string>} */\n const meta = {};\n for (const attributes of startTags(head, 'meta').map(tagAttributes)) {\n const key = (attributes.property || attributes.name || '').toLowerCase();\n if (key && attributes.content && !(key in meta)) meta[key] = attributes.content;\n }\n\n // JSON-LD: the organization's own name and logo, when the site states them.\n let schemaName = '';\n let schemaLogo = '';\n for (const [, body] of source.matchAll(/<script\\b[^>]*type\\s*=\\s*[\"']?application\\/ld\\+json[^>]*>([\\s\\S]*?)<\\/script>/gi)) {\n let data;\n try {\n data = JSON.parse(body.trim());\n } catch {\n continue;\n }\n for (const node of jsonLdNodes(data)) {\n const types = [].concat(node['@type'] || []).join(' ');\n if (!ORGANIZATION.test(types)) continue;\n if (!schemaName && typeof node.name === 'string') schemaName = node.name;\n const logo = typeof node.logo === 'string' ? node.logo : node.logo?.url || node.logo?.contentUrl || '';\n if (!schemaLogo && typeof logo === 'string') schemaLogo = resolve(logo, base);\n }\n }\n\n /* Name */\n const title = cleanName(head.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1] || '');\n const words = hostWords(host);\n const fromTitle =\n title\n .split(/\\s+[|–—:·•»-]\\s+/)\n .map((part) => part.trim())\n .find((part) => part && squash(part).length <= 40 && (squash(part).includes(squash(words)) || squash(words).includes(squash(part)))) || '';\n const fromHost = words.replace(/\\b\\p{L}/gu, (letter) => letter.toUpperCase());\n const name = cleanName(meta['og:site_name'] || schemaName || meta['application-name'] || fromTitle || fromHost);\n\n /* Logo candidates, best first */\n /** @type {PageReading['logos']} */\n const logos = [];\n const add = (/** @type {string} */ url, /** @type {PageReading['logos'][number]['from']} */ from) => {\n if (url && !logos.some((logo) => logo.url === url)) logos.push({ url, from });\n };\n add(schemaLogo, 'schema');\n /** @type {{url: string, score: number}[]} */\n const images = [];\n const headerEnd = (() => {\n const at = source.search(/<\\/header>/i);\n return at > 0 ? at : Math.min(source.length, 20_000);\n })();\n for (const match of source.matchAll(/<img\\b[^>]*>/gi)) {\n const attributes = tagAttributes(match[0]);\n const own = [attributes.class, attributes.id, attributes.alt, attributes.src, attributes['data-src']].join(' ').toLowerCase();\n const before = source.slice(Math.max(0, (match.index ?? 0) - 400), match.index).toLowerCase();\n const parent = /<(a|div|span|figure|picture)\\b[^>]*(class|id)\\s*=\\s*[\"'][^\"']*logo[^\"']*[\"'][^>]*>[^<]*(<source\\b[^>]*>\\s*)*$/i.test(before);\n if (!own.includes('logo') && !parent) continue;\n // Partner, client and payment logos are someone else's.\n if (/partner|client|customer|sponsor|award|payment|badge|trust|press|app-?store|google-?play/.test(own)) continue;\n const url = resolve(attributes.srcset ? largestOf(attributes.srcset) : attributes.src || attributes['data-src'] || '', base);\n if (!url) continue;\n let score = own.includes('logo') ? 3 : 2;\n if ((match.index ?? 0) < headerEnd) score += 3;\n if (name && squash(attributes.alt || '').includes(squash(name))) score += 2;\n if (/footer/.test(own)) score -= 2;\n images.push({ url, score: score - images.length * 0.01 });\n }\n images.sort((a, b) => b.score - a.score).forEach((image) => add(image.url, 'img'));\n /** @type {{url: string, size: number}[]} */\n const icons = [];\n for (const attributes of startTags(head, 'link').map(tagAttributes)) {\n const rel = (attributes.rel || '').toLowerCase();\n if (!/apple-touch-icon|icon/.test(rel)) continue;\n const size = parseInt(attributes.sizes || (rel.includes('apple') ? '180' : '0'), 10) || 0;\n if (size >= 96) icons.push({ url: resolve(attributes.href, base), size });\n }\n icons.sort((a, b) => b.size - a.size).forEach((icon) => add(icon.url, 'icon'));\n\n /* Styles */\n const stylesheets = startTags(head, 'link')\n .map(tagAttributes)\n .filter((a) => /(^|\\s)stylesheet(\\s|$)/i.test(a.rel || '') && !/^print$/i.test((a.media || '').trim()))\n .map((a) => resolve(a.href, base))\n .filter(Boolean)\n // Fonts and third-party widgets say nothing about the brand's colors.\n .filter((href) => !/fonts\\.googleapis|use\\.typekit|fontawesome|cookie|consent|onetrust/i.test(href));\n const css = [\n ...[...source.matchAll(/<style\\b[^>]*>([\\s\\S]*?)<\\/style>/gi)].map((m) => m[1]),\n // Inline styles of buttons and links count too: `<a class=\"btn\" style=\"…\">`.\n ...[...source.matchAll(/<(a|button)\\b[^>]*>/gi)]\n .map((m) => tagAttributes(m[0]))\n .filter((a) => a.style)\n .map((a) => `${/btn|button|cta/i.test(a.class || '') ? '.btn' : 'a'}{${a.style}}`),\n ].join('\\n');\n\n return {\n name,\n logos,\n stylesheets: [...new Set(stylesheets)],\n css,\n themeColor: meta['theme-color'] || meta['msapplication-tilecolor'] || '',\n };\n}\n\n/* ── CSS ────────────────────────────────────────────────────────────────── */\n\nconst PRIMARY_NAME = /primary|brand|accent|main|theme|highlight|cta/i;\nconst NOT_A_BRAND_NAME = /text|foreground|fg|border|muted|gr[ae]y|neutral|surface|shadow|overlay|success|error|danger|warning|info|disabled|placeholder/i;\nconst NOISE = /error|danger|success|warning|alert|invalid|valid\\b|notice|toast|badge|disabled|facebook|twitter|linkedin|instagram|youtube|whatsapp|pinterest|tiktok|social|cookie|consent|onetrust|ot-|cmp|gdpr|wp-block|has-[\\w-]+-(background-)?color|syntax|hljs|code/i;\nconst COLOR_IN_VALUE = /#[0-9a-f]{3,8}\\b|rgba?\\([^)]*\\)|hsla?\\([^)]*\\)/gi;\nconst COLOR_PROPERTY = /^(color|background|background-color|border|border-color|border-(top|bottom|left|right)(-color)?|fill|stroke|outline(-color)?|text-decoration-color|accent-color|caret-color)$/;\n\n/** @param {string} css */\nconst stripComments = (css) => css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n\n/** Rules as [selector, declarations], @media flattened. @param {string} css */\nfunction rulesOf(css) {\n /** @type {[string, [string, string][]][]} */\n const rules = [];\n for (const [, selector, body] of stripComments(css).matchAll(/([^{}]+)\\{([^{}]*)\\}/g)) {\n const declarations = body\n .split(';')\n .map((part) => {\n const colon = part.indexOf(':');\n return /** @type {[string, string]} */ ([part.slice(0, colon).trim().toLowerCase(), part.slice(colon + 1).trim()]);\n })\n .filter(([property, value]) => property && value);\n if (declarations.length) rules.push([selector.trim(), declarations]);\n }\n return rules;\n}\n\n/**\n * A custom property's value as a color: `#2977bc`, `rgb(…)`, and the bare\n * channels design systems keep there (`41 119 188`, `210 64% 45%`).\n * @param {string} value\n */\nfunction customColor(value) {\n const direct = parseColor(value.replace(/\\s*!important$/, ''));\n if (direct) return direct;\n if (/^\\d{1,3}(\\.\\d+)?\\s+\\d{1,3}(\\.\\d+)?\\s+\\d{1,3}(\\.\\d+)?$/.test(value)) return parseColor(`rgb(${value})`);\n if (/^\\d{1,3}(\\.\\d+)?(deg)?\\s+\\d{1,3}(\\.\\d+)?%\\s+\\d{1,3}(\\.\\d+)?%$/.test(value)) return parseColor(`hsl(${value.replace('deg', '')})`);\n return null;\n}\n\n/**\n * `var(--x, fallback)` replaced by what --x holds, a few levels deep.\n * @param {string} value @param {Map<string, string>} custom\n */\nfunction resolveVars(value, custom) {\n let out = value;\n for (let depth = 0; depth < 4 && out.includes('var('); depth += 1) {\n out = out.replace(/var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\))?[^()]*))?\\)/g, (_, name, fallback) => custom.get(name) ?? fallback ?? '');\n }\n return out;\n}\n\n/** @param {{r: number, g: number, b: number}} a @param {{r: number, g: number, b: number}} b */\nconst distance = (a, b) => Math.hypot(a.r - b.r, a.g - b.g, a.b - b.b);\n\n/** Colors of one value, as found (not resolved through var()). @param {string} value */\nconst colorsIn = (value) => (value.match(COLOR_IN_VALUE) || []).map((text) => parseColor(text)).filter((c) => c && c.a >= 0.6);\n\n/**\n * The web-safe stack nearest to a CSS font-family.\n * @param {string} family\n * @returns {string|undefined}\n */\nexport function nearestFont(family) {\n const stack = String(family ?? '')\n .toLowerCase()\n .replace(/[\"']/g, '');\n if (!stack.trim() || /^(inherit|initial|unset|revert)$/.test(stack.trim())) return undefined;\n const first = stack.split(',')[0].trim();\n const known = FONT_OPTIONS.find((option) => first.startsWith(option.label.toLowerCase()));\n if (known) return known.value;\n if (/^helvetica/.test(first)) return FONT_OPTIONS[0].value;\n if (/^times/.test(first)) return FONT_OPTIONS.find((o) => o.label === 'Times')?.value;\n if (/monospace|mono\\b|courier|consolas/.test(stack)) return FONT_OPTIONS.find((o) => o.label === 'Courier')?.value;\n // Web fonts: their family decides (serif, else sans-serif).\n if (/(^|,)\\s*serif\\s*(,|$)|garamond|playfair|merriweather|lora|baskerville|caslon|didot|bodoni|crimson|libre baskerville|source serif|noto serif|pt serif/.test(stack)) {\n return FONT_OPTIONS.find((o) => o.label === 'Georgia')?.value;\n }\n return FONT_OPTIONS[0].value;\n}\n\n/**\n * Main color, headings color and font of a site, from its CSS.\n * @param {{css: string, themeColor?: string}} input\n * @returns {{primary?: string, heading?: string, font?: string}}\n */\nexport function brandFromCss({ css, themeColor = '' }) {\n const rules = rulesOf(String(css ?? ''));\n\n /** @type {Map<string, string>} */\n const custom = new Map();\n for (const [selector, declarations] of rules) {\n // :root first: a theme's dark variant must not replace it.\n const root = /^(:root|html|body|\\*)(\\s*,|$)/i.test(selector);\n for (const [property, value] of declarations) {\n if (property.startsWith('--') && (root || !custom.has(property))) custom.set(property, value.replace(/\\s*!important$/, ''));\n }\n }\n\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const found = [];\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const darkText = [];\n /** @type {{font: string, weight: number}[]} */\n const fonts = [];\n const count = (/** @type {{r: number, g: number, b: number}} */ color, /** @type {number} */ weight) => found.push({ color, weight });\n\n for (const [selector, declarations] of rules) {\n if (selector.startsWith('@')) continue;\n const noise = NOISE.test(selector);\n const emphasis = /btn|button|cta|primary|brand|accent|hero|highlight/i.test(selector) ? 4 : /(^|[\\s,>+~])a([:.[\\s,]|$)|link/i.test(selector) ? 2 : 1;\n const isHeading = /(^|[\\s,>])h[1-3]\\b|title|heading|headline/i.test(selector);\n const isBody = /^(html|body|:root)(\\s*,|$)|(^|,)\\s*(html|body)\\s*(,|$)/i.test(selector);\n for (const [property, raw] of declarations) {\n if (property.startsWith('--')) {\n const color = customColor(resolveVars(raw, custom));\n if (color && color.a >= 0.6 && PRIMARY_NAME.test(property) && !NOT_A_BRAND_NAME.test(property)) count(color, 10);\n continue;\n }\n const value = resolveVars(raw, custom);\n if (property === 'font-family' || property === 'font') {\n const family = property === 'font' ? value.replace(/^.*?\\d[\\w%.]*(\\/[\\w%.]+)?\\s+/, '') : value;\n if (isBody || isHeading) fonts.push({ font: family, weight: isBody ? 2 : 1 });\n continue;\n }\n if (!COLOR_PROPERTY.test(property) || noise) continue;\n for (const color of colorsIn(value)) {\n if (!color) continue;\n count(color, emphasis * (property.startsWith('background') ? 1.5 : 1));\n if (property === 'color' && (isHeading || isBody)) darkText.push({ color, weight: isHeading ? 2 : 1 });\n }\n }\n }\n const theme = parseColor(themeColor);\n if (theme && theme.a >= 0.6) count(theme, 15);\n\n // Near colors are one color (hover shades, rounding).\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const clusters = [];\n for (const entry of found) {\n const near = clusters.find((cluster) => distance(cluster.color, entry.color) < 24);\n if (near) near.weight += entry.weight;\n else clusters.push({ ...entry });\n }\n clusters.sort((a, b) => b.weight - a.weight);\n const primary = clusters.find(({ color }) => {\n const { s, l } = toHsl(color);\n return s >= 0.25 && l >= 0.15 && l <= 0.85;\n });\n\n /** @type {{primary?: string, heading?: string, font?: string}} */\n const result = {};\n if (primary) result.primary = toHex(primary.color);\n\n const dark = darkText\n .filter(({ color }) => toHsl(color).l < 0.3)\n .reduce((/** @type {Map<string, number>} */ map, { color, weight }) => map.set(toHex(color), (map.get(toHex(color)) || 0) + weight), new Map());\n const heading = [...dark.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];\n if (heading) result.heading = heading;\n else if (result.primary && contrastRatio(result.primary, PALETTE.white) >= 7) result.heading = result.primary;\n else result.heading = PALETTE.ink;\n\n const font = fonts.sort((a, b) => b.weight - a.weight).map(({ font }) => nearestFont(font)).find(Boolean);\n if (font) result.font = font;\n return result;\n}\n\n/* ── Images ─────────────────────────────────────────────────────────────── */\n\n/**\n * Width and height of a PNG, GIF or JPEG, from its header; null otherwise.\n * @param {Uint8Array} bytes\n * @returns {{width: number, height: number}|null}\n */\nexport function imageSize(bytes) {\n if (!bytes || bytes.length < 24) return null;\n const at = (/** @type {number} */ i) => bytes[i];\n // PNG: IHDR right after the signature.\n if (at(0) === 0x89 && at(1) === 0x50 && at(2) === 0x4e && at(3) === 0x47) {\n const read32 = (/** @type {number} */ i) => ((at(i) << 24) | (at(i + 1) << 16) | (at(i + 2) << 8) | at(i + 3)) >>> 0;\n return { width: read32(16), height: read32(20) };\n }\n // GIF: logical screen size.\n if (at(0) === 0x47 && at(1) === 0x49 && at(2) === 0x46) {\n return { width: at(6) | (at(7) << 8), height: at(8) | (at(9) << 8) };\n }\n // JPEG: the first start-of-frame marker.\n if (at(0) === 0xff && at(1) === 0xd8) {\n let i = 2;\n while (i + 9 < bytes.length) {\n if (at(i) !== 0xff) {\n i += 1;\n continue;\n }\n const marker = at(i + 1);\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0xff) {\n i += marker === 0xff ? 1 : 2;\n continue;\n }\n const length = (at(i + 2) << 8) | at(i + 3);\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n return { height: (at(i + 5) << 8) | at(i + 6), width: (at(i + 7) << 8) | at(i + 8) };\n }\n i += 2 + length;\n }\n }\n return null;\n}\n\n/**\n * The width a logo is shown at in an email: a wordmark wider, a square mark\n * smaller; never wider than the image itself (a blurry logo is worse than a\n * small one). Retina images (twice the size shown) are common: halved first.\n * @param {{width: number, height: number}} size\n */\nexport function logoWidthFor({ width, height }) {\n if (!width || !height) return 140;\n const ratio = width / height;\n const shown = width >= 240 ? width / 2 : width;\n const target = ratio >= 2.5 ? 180 : ratio >= 1.3 ? 140 : 72;\n return Math.max(24, Math.round(Math.min(target, shown)));\n}\n","// @ts-check\n/**\n * What the user is told about reading a brand from a website: why it failed\n * (the server's answer) and what it could not find (notes under the field).\n * English source strings, translated where they are shown.\n */\n\n/**\n * Why a website could not be read: [HTTP status, message], by\n * BrandImportError code (src/server/brandImport.js).\n * @type {Record<string, [number, string]>}\n */\nexport const BRAND_IMPORT_ERRORS = {\n BAD_URL: [400, 'Enter the address of a website, like acme.com'],\n PRIVATE_ADDRESS: [400, 'This address is not a public website'],\n UNREACHABLE: [422, 'The website could not be reached. Check the address.'],\n TIMEOUT: [422, 'The website took too long to answer'],\n HTTP_ERROR: [422, 'The website does not let the builder read it. Fill in your brand below.'],\n NOT_HTML: [422, 'This address is not a web page'],\n TOO_MANY_REDIRECTS: [422, 'The website redirects too many times'],\n TOO_LARGE: [422, 'The website sent too much data'],\n};\n\n/** What could not be read, by note code. @type {Record<string, string>} */\nexport const BRAND_NOTES = {\n LOGO_NOT_FOUND: 'No logo was found on the website. Upload it below.',\n LOGO_UNSUPPORTED: 'The logo on the website is an SVG or WebP image, which most email apps do not show. Upload a PNG or JPEG version.',\n LOGO_ICON: 'The website icon is used as the logo. Upload your full logo if you have one.',\n COLORS_NOT_FOUND: 'No main color was found on the website. Pick one below.',\n};\n"],"mappings":"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","names":["font"]}