best-email-builder 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/README.md +309 -12
- package/dist/builder.js +44 -0
- package/dist/builder.js.map +1 -0
- package/dist/chunk-AT73Y7JA.js +1427 -0
- package/dist/chunk-AT73Y7JA.js.map +1 -0
- package/dist/chunk-HHLOE6OZ.js +1682 -0
- package/dist/chunk-HHLOE6OZ.js.map +1 -0
- package/dist/chunk-JNHFZXDL.js +1776 -0
- package/dist/chunk-JNHFZXDL.js.map +1 -0
- package/dist/chunk-MY6WPDT4.js +6110 -0
- package/dist/chunk-MY6WPDT4.js.map +1 -0
- package/dist/chunk-NAEGU6PR.js +538 -0
- package/dist/chunk-NAEGU6PR.js.map +1 -0
- package/dist/chunk-TGDZ4NFI.js +114 -0
- package/dist/chunk-TGDZ4NFI.js.map +1 -0
- package/dist/index.js +124 -4866
- package/dist/index.js.map +1 -1
- package/dist/library.js +31 -0
- package/dist/library.js.map +1 -0
- package/dist/server.js +554 -210
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/blocks/shared.d.ts +20 -0
- package/dist/types/builder.d.ts +8 -0
- package/dist/types/core/EmailEditor.d.ts +72 -19
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/mjmlDefaults.d.ts +13 -0
- package/dist/types/core/scrollbars.d.ts +13 -0
- package/dist/types/defaultEditor.d.ts +11 -0
- package/dist/types/engine/NativeAdapter.d.ts +28 -0
- package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
- package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
- package/dist/types/engine/canvas/icons.d.ts +14 -0
- package/dist/types/engine/canvas/morph.d.ts +21 -0
- package/dist/types/engine/canvasMjml.d.ts +30 -0
- package/dist/types/engine/mjml.d.ts +31 -0
- package/dist/types/engine/model.d.ts +130 -0
- package/dist/types/engine/placement.d.ts +69 -0
- package/dist/types/engine/richText.d.ts +15 -0
- package/dist/types/engine/unwrapModule.d.ts +29 -0
- package/dist/types/features/brand/BrandButton.d.ts +4 -0
- package/dist/types/features/brand/brand.d.ts +108 -0
- package/dist/types/features/brand/extract.d.ts +61 -0
- package/dist/types/features/brand/index.d.ts +2 -0
- package/dist/types/features/brand/messages.d.ts +13 -0
- package/dist/types/features/brand/useBrand.d.ts +22 -0
- package/dist/types/features/generate/GenerateButton.d.ts +3 -0
- package/dist/types/features/generate/RewriteText.d.ts +5 -0
- package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
- package/dist/types/features/generate/chip.d.ts +1 -0
- package/dist/types/features/generate/claude.d.ts +21 -0
- package/dist/types/features/generate/draft.d.ts +122 -0
- package/dist/types/features/generate/generator.d.ts +118 -0
- package/dist/types/features/generate/index.d.ts +2 -0
- package/dist/types/features/generate/prompt.d.ts +76 -0
- package/dist/types/features/generate/purposes.d.ts +48 -0
- package/dist/types/features/generate/rewrite.d.ts +83 -0
- package/dist/types/features/generate/subjects.d.ts +83 -0
- package/dist/types/features/preview/PreviewButton.d.ts +1 -1
- package/dist/types/features/preview/darkMode.d.ts +48 -0
- package/dist/types/features/templates/gallery.d.ts +7 -0
- package/dist/types/index.d.ts +14 -10
- package/dist/types/library.d.ts +5 -0
- package/dist/types/server/brand.d.ts +32 -0
- package/dist/types/server/brandImport.d.ts +93 -0
- package/dist/types/server/compileMjml.d.ts +2 -0
- package/dist/types/server/handler.d.ts +25 -0
- package/dist/types/server/index.d.ts +11 -0
- package/dist/types/storage/client.d.ts +79 -3
- package/dist/types/ui/EmailStudio.d.ts +42 -0
- package/dist/types/ui/fields.d.ts +2 -1
- package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
- package/dist/types/ui/primitives/button.d.ts +2 -2
- package/dist/types/ui/primitives/color-picker.d.ts +2 -1
- package/dist/types/ui/useLayoutWidth.d.ts +1 -1
- package/package.json +27 -4
- package/dist/chunk-MTALS5GC.js +0 -747
- package/dist/chunk-MTALS5GC.js.map +0 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -162
- package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
|
@@ -0,0 +1,1427 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DARK_PAGE,
|
|
3
|
+
DEFAULT_ASSETS_PATH,
|
|
4
|
+
DEFAULT_BUTTON_URL,
|
|
5
|
+
DEFAULT_VARIABLES,
|
|
6
|
+
EMAIL_MOBILE_WIDTH,
|
|
7
|
+
FONT_STACK,
|
|
8
|
+
GROUPS,
|
|
9
|
+
IMAGE_TYPES,
|
|
10
|
+
PALETTE,
|
|
11
|
+
cleanBrand,
|
|
12
|
+
identity,
|
|
13
|
+
imageUploadError,
|
|
14
|
+
inWhiteSection,
|
|
15
|
+
prop,
|
|
16
|
+
readDocument,
|
|
17
|
+
simulateDarkMode,
|
|
18
|
+
translatorFor,
|
|
19
|
+
validateCustomVariable
|
|
20
|
+
} from "./chunk-HHLOE6OZ.js";
|
|
21
|
+
|
|
22
|
+
// src/ui/context.jsx
|
|
23
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
24
|
+
import { AlertCircle, CheckCircle2, Info, TriangleAlert, X } from "lucide-react";
|
|
25
|
+
|
|
26
|
+
// src/ui/primitives/overlays.jsx
|
|
27
|
+
import {
|
|
28
|
+
AlertDialog as AlertDialogPrimitive,
|
|
29
|
+
Dialog as DialogPrimitive,
|
|
30
|
+
DropdownMenu as DropdownMenuPrimitive,
|
|
31
|
+
Popover as PopoverPrimitive,
|
|
32
|
+
Select as SelectPrimitive
|
|
33
|
+
} from "radix-ui";
|
|
34
|
+
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
|
35
|
+
|
|
36
|
+
// src/ui/primitives/cn.js
|
|
37
|
+
import { clsx } from "clsx";
|
|
38
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
39
|
+
var twMerge = extendTailwindMerge({ prefix: "eb" });
|
|
40
|
+
function cn(...inputs) {
|
|
41
|
+
return twMerge(clsx(inputs));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/ui/primitives/overlays.jsx
|
|
45
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
46
|
+
var FADE_ZOOM = "eb:data-[state=open]:animate-in eb:data-[state=open]:fade-in-0 eb:data-[state=open]:zoom-in-95 eb:data-[state=closed]:animate-out eb:data-[state=closed]:fade-out-0 eb:data-[state=closed]:zoom-out-95";
|
|
47
|
+
function Dialog(props) {
|
|
48
|
+
return /* @__PURE__ */ jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
|
|
49
|
+
}
|
|
50
|
+
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
51
|
+
const t = useT();
|
|
52
|
+
return /* @__PURE__ */ jsxs(DialogPrimitive.Portal, { children: [
|
|
53
|
+
/* @__PURE__ */ jsx(DialogPrimitive.Overlay, { className: "eb-scope eb:fixed eb:inset-0 eb:z-50 eb:bg-black/50 eb:data-[state=open]:animate-in eb:data-[state=open]:fade-in-0 eb:data-[state=closed]:animate-out eb:data-[state=closed]:fade-out-0" }),
|
|
54
|
+
/* @__PURE__ */ jsxs(
|
|
55
|
+
DialogPrimitive.Content,
|
|
56
|
+
{
|
|
57
|
+
"data-slot": "dialog-content",
|
|
58
|
+
className: cn(
|
|
59
|
+
"eb-scope eb:fixed eb:top-[50%] eb:left-[50%] eb:z-50 eb:grid eb:w-full eb:max-w-[calc(100%-2rem)] eb:translate-x-[-50%] eb:translate-y-[-50%] eb:gap-4 eb:rounded-lg2 eb:border eb:border-border eb:bg-background eb:p-6 eb:text-foreground eb:shadow-lg eb:duration-200 eb:outline-none eb:sm:max-w-lg",
|
|
60
|
+
FADE_ZOOM,
|
|
61
|
+
className
|
|
62
|
+
),
|
|
63
|
+
...props,
|
|
64
|
+
children: [
|
|
65
|
+
children,
|
|
66
|
+
showCloseButton && /* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "eb:absolute eb:top-4 eb:right-4 eb:rounded-xs eb:opacity-70 eb:transition-opacity eb:hover:opacity-100 eb:focus:ring-2 eb:focus:ring-ring eb:focus:ring-offset-2 eb:focus:outline-hidden eb:disabled:pointer-events-none eb:[&_svg]:pointer-events-none eb:[&_svg]:shrink-0 eb:[&_svg:not([class*='size-'])]:size-4", children: [
|
|
67
|
+
/* @__PURE__ */ jsx(XIcon, {}),
|
|
68
|
+
/* @__PURE__ */ jsx("span", { className: "eb:sr-only", children: t("Close") })
|
|
69
|
+
] })
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
function DialogHeader({ className, ...props }) {
|
|
76
|
+
return /* @__PURE__ */ jsx("div", { className: cn("eb:flex eb:flex-col eb:gap-2 eb:text-left", className), ...props });
|
|
77
|
+
}
|
|
78
|
+
function DialogTitle({ className, ...props }) {
|
|
79
|
+
return /* @__PURE__ */ jsx(DialogPrimitive.Title, { className: cn("eb:text-lg eb:leading-none eb:font-semibold", className), ...props });
|
|
80
|
+
}
|
|
81
|
+
function DialogDescription({ className, ...props }) {
|
|
82
|
+
return /* @__PURE__ */ jsx(DialogPrimitive.Description, { className: cn("eb:text-sm eb:text-muted-foreground", className), ...props });
|
|
83
|
+
}
|
|
84
|
+
function AlertDialog(props) {
|
|
85
|
+
return /* @__PURE__ */ jsx(AlertDialogPrimitive.Root, { ...props });
|
|
86
|
+
}
|
|
87
|
+
function AlertDialogContent({ className, ...props }) {
|
|
88
|
+
return /* @__PURE__ */ jsxs(AlertDialogPrimitive.Portal, { children: [
|
|
89
|
+
/* @__PURE__ */ jsx(AlertDialogPrimitive.Overlay, { className: "eb-scope eb:fixed eb:inset-0 eb:z-50 eb:bg-black/50 eb:data-[state=open]:animate-in eb:data-[state=open]:fade-in-0 eb:data-[state=closed]:animate-out eb:data-[state=closed]:fade-out-0" }),
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
AlertDialogPrimitive.Content,
|
|
92
|
+
{
|
|
93
|
+
className: cn(
|
|
94
|
+
"eb-scope eb:fixed eb:top-[50%] eb:left-[50%] eb:z-50 eb:grid eb:w-full eb:max-w-[calc(100%-2rem)] eb:translate-x-[-50%] eb:translate-y-[-50%] eb:gap-4 eb:rounded-lg2 eb:border eb:border-border eb:bg-background eb:p-6 eb:text-foreground eb:shadow-lg eb:duration-200 eb:sm:max-w-lg",
|
|
95
|
+
FADE_ZOOM,
|
|
96
|
+
className
|
|
97
|
+
),
|
|
98
|
+
...props
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
var AlertDialogTitle = ({ className, ...props }) => /* @__PURE__ */ jsx(AlertDialogPrimitive.Title, { className: cn("eb:text-lg eb:font-semibold", className), ...props });
|
|
104
|
+
var AlertDialogDescription = ({ className, ...props }) => /* @__PURE__ */ jsx(AlertDialogPrimitive.Description, { className: cn("eb:text-sm eb:text-muted-foreground", className), ...props });
|
|
105
|
+
var AlertDialogAction = AlertDialogPrimitive.Action;
|
|
106
|
+
var AlertDialogCancel = AlertDialogPrimitive.Cancel;
|
|
107
|
+
function Popover(props) {
|
|
108
|
+
return /* @__PURE__ */ jsx(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
|
|
109
|
+
}
|
|
110
|
+
function PopoverTrigger(props) {
|
|
111
|
+
return /* @__PURE__ */ jsx(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
|
|
112
|
+
}
|
|
113
|
+
function PopoverContent({ className, align = "center", sideOffset = 4, ...props }) {
|
|
114
|
+
return /* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
115
|
+
PopoverPrimitive.Content,
|
|
116
|
+
{
|
|
117
|
+
"data-slot": "popover-content",
|
|
118
|
+
align,
|
|
119
|
+
sideOffset,
|
|
120
|
+
className: cn(
|
|
121
|
+
"eb-scope eb:z-50 eb:w-72 eb:origin-(--radix-popover-content-transform-origin) eb:rounded-lg2 eb:border eb:border-border eb:bg-popover eb:p-4 eb:text-popover-foreground eb:shadow-md eb:outline-hidden",
|
|
122
|
+
FADE_ZOOM,
|
|
123
|
+
className
|
|
124
|
+
),
|
|
125
|
+
...props
|
|
126
|
+
}
|
|
127
|
+
) });
|
|
128
|
+
}
|
|
129
|
+
function DropdownMenu(props) {
|
|
130
|
+
return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
131
|
+
}
|
|
132
|
+
function DropdownMenuTrigger(props) {
|
|
133
|
+
return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
|
|
134
|
+
}
|
|
135
|
+
function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|
136
|
+
return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
137
|
+
DropdownMenuPrimitive.Content,
|
|
138
|
+
{
|
|
139
|
+
"data-slot": "dropdown-menu-content",
|
|
140
|
+
sideOffset,
|
|
141
|
+
className: cn(
|
|
142
|
+
"eb-scope eb:z-50 eb:max-h-(--radix-dropdown-menu-content-available-height) eb:min-w-[8rem] eb:origin-(--radix-dropdown-menu-content-transform-origin) eb:overflow-x-hidden eb:overflow-y-auto eb:rounded-lg2 eb:border eb:border-border eb:bg-popover eb:p-1 eb:text-popover-foreground eb:shadow-md",
|
|
143
|
+
FADE_ZOOM,
|
|
144
|
+
className
|
|
145
|
+
),
|
|
146
|
+
...props
|
|
147
|
+
}
|
|
148
|
+
) });
|
|
149
|
+
}
|
|
150
|
+
function DropdownMenuItem({ className, ...props }) {
|
|
151
|
+
return /* @__PURE__ */ jsx(
|
|
152
|
+
DropdownMenuPrimitive.Item,
|
|
153
|
+
{
|
|
154
|
+
"data-slot": "dropdown-menu-item",
|
|
155
|
+
className: cn(
|
|
156
|
+
"eb:relative eb:flex eb:cursor-default eb:items-center eb:gap-2 eb:rounded-sm eb:px-2 eb:py-1.5 eb:text-[13px] eb:outline-hidden eb:select-none eb:focus:bg-accent eb:focus:text-accent-foreground eb:data-[disabled]:pointer-events-none eb:data-[disabled]:opacity-50 eb:[&_svg]:pointer-events-none eb:[&_svg]:shrink-0 eb:[&_svg:not([class*='size-'])]:size-4",
|
|
157
|
+
className
|
|
158
|
+
),
|
|
159
|
+
...props
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function Select(props) {
|
|
164
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
|
|
165
|
+
}
|
|
166
|
+
function SelectValue(props) {
|
|
167
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
|
|
168
|
+
}
|
|
169
|
+
function SelectTrigger({ className, children, ...props }) {
|
|
170
|
+
return /* @__PURE__ */ jsxs(
|
|
171
|
+
SelectPrimitive.Trigger,
|
|
172
|
+
{
|
|
173
|
+
"data-slot": "select-trigger",
|
|
174
|
+
className: cn(
|
|
175
|
+
"eb:flex eb:h-8 eb:w-full eb:items-center eb:justify-between eb:gap-2 eb:rounded-lg2 eb:border eb:border-border-strong eb:bg-surface-75 eb:px-3 eb:py-1 eb:text-[13px] eb:whitespace-nowrap eb:text-foreground eb:shadow-xs eb:transition-[color,box-shadow] eb:outline-none eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:disabled:cursor-not-allowed eb:disabled:opacity-50 eb:data-[placeholder]:text-foreground-muted eb:*:data-[slot=select-value]:line-clamp-1 eb:*:data-[slot=select-value]:flex eb:*:data-[slot=select-value]:items-center eb:*:data-[slot=select-value]:gap-2 eb:[&_svg]:pointer-events-none eb:[&_svg]:shrink-0 eb:[&_svg:not([class*='size-'])]:size-4",
|
|
176
|
+
className
|
|
177
|
+
),
|
|
178
|
+
...props,
|
|
179
|
+
children: [
|
|
180
|
+
children,
|
|
181
|
+
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDownIcon, { className: "eb:size-4 eb:opacity-50" }) })
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
function SelectContent({ className, children, position = "popper", ...props }) {
|
|
187
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
188
|
+
SelectPrimitive.Content,
|
|
189
|
+
{
|
|
190
|
+
"data-slot": "select-content",
|
|
191
|
+
position,
|
|
192
|
+
className: cn(
|
|
193
|
+
"eb-scope eb:relative eb:z-50 eb:max-h-(--radix-select-content-available-height) eb:min-w-[8rem] eb:origin-(--radix-select-content-transform-origin) eb:overflow-x-hidden eb:overflow-y-auto eb:rounded-lg2 eb:border eb:border-border eb:bg-popover eb:text-popover-foreground eb:shadow-md",
|
|
194
|
+
FADE_ZOOM,
|
|
195
|
+
position === "popper" && "eb:data-[side=bottom]:translate-y-1 eb:data-[side=top]:-translate-y-1",
|
|
196
|
+
className
|
|
197
|
+
),
|
|
198
|
+
...props,
|
|
199
|
+
children: /* @__PURE__ */ jsx(
|
|
200
|
+
SelectPrimitive.Viewport,
|
|
201
|
+
{
|
|
202
|
+
className: cn(
|
|
203
|
+
"eb:p-1",
|
|
204
|
+
position === "popper" && "eb:h-[var(--radix-select-trigger-height)] eb:w-full eb:min-w-[var(--radix-select-trigger-width)] eb:scroll-my-1"
|
|
205
|
+
),
|
|
206
|
+
children
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
) });
|
|
211
|
+
}
|
|
212
|
+
function SelectItem({ className, children, ...props }) {
|
|
213
|
+
return /* @__PURE__ */ jsxs(
|
|
214
|
+
SelectPrimitive.Item,
|
|
215
|
+
{
|
|
216
|
+
"data-slot": "select-item",
|
|
217
|
+
className: cn(
|
|
218
|
+
"eb:relative eb:flex eb:w-full eb:cursor-default eb:items-center eb:gap-2 eb:rounded-sm eb:py-1.5 eb:pr-8 eb:pl-2 eb:text-[13px] eb:outline-hidden eb:select-none eb:focus:bg-accent eb:focus:text-accent-foreground eb:data-[disabled]:pointer-events-none eb:data-[disabled]:opacity-50",
|
|
219
|
+
className
|
|
220
|
+
),
|
|
221
|
+
...props,
|
|
222
|
+
children: [
|
|
223
|
+
/* @__PURE__ */ jsx("span", { className: "eb:absolute eb:right-2 eb:flex eb:size-3.5 eb:items-center eb:justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(CheckIcon, { className: "eb:size-4" }) }) }),
|
|
224
|
+
/* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/ui/primitives/button.jsx
|
|
231
|
+
import { Slot } from "radix-ui";
|
|
232
|
+
import { cva } from "class-variance-authority";
|
|
233
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
234
|
+
var buttonVariants = cva(
|
|
235
|
+
"eb:inline-flex eb:shrink-0 eb:items-center eb:justify-center eb:gap-2 eb:whitespace-nowrap eb:rounded-lg2 eb:text-sm eb:font-medium eb:transition-all eb:outline-none eb:disabled:pointer-events-none eb:disabled:opacity-50 eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:aria-invalid:border-destructive eb:aria-invalid:ring-destructive/20 eb:[&_svg]:pointer-events-none eb:[&_svg]:shrink-0 eb:[&_svg:not([class*='size-'])]:size-4",
|
|
236
|
+
{
|
|
237
|
+
variants: {
|
|
238
|
+
variant: {
|
|
239
|
+
default: "eb:bg-primary eb:text-primary-foreground eb:hover:bg-primary/90",
|
|
240
|
+
destructive: "eb:bg-destructive eb:text-white eb:hover:bg-destructive/90 eb:focus-visible:ring-destructive/20",
|
|
241
|
+
outline: "eb:border eb:border-border eb:bg-background eb:text-foreground eb:shadow-xs eb:hover:bg-accent eb:hover:text-accent-foreground",
|
|
242
|
+
secondary: "eb:bg-secondary eb:text-secondary-foreground eb:hover:bg-secondary/80",
|
|
243
|
+
ghost: "eb:text-foreground eb:hover:bg-accent eb:hover:text-accent-foreground",
|
|
244
|
+
link: "eb:text-primary eb:underline-offset-4 eb:hover:underline"
|
|
245
|
+
},
|
|
246
|
+
size: {
|
|
247
|
+
default: "eb:h-8 eb:gap-1.5 eb:px-3 eb:text-[13px] eb:has-[>svg]:px-2.5",
|
|
248
|
+
xs: "eb:h-6 eb:gap-1 eb:px-2 eb:text-xs eb:has-[>svg]:px-1.5 eb:[&_svg:not([class*='size-'])]:size-3",
|
|
249
|
+
"sm-compact": "eb:h-7 eb:gap-1.5 eb:px-3 eb:text-xs eb:has-[>svg]:px-2.5 eb:[&_svg:not([class*='size-'])]:size-3",
|
|
250
|
+
sm: "eb:h-8 eb:gap-1.5 eb:px-3 eb:text-[13px] eb:has-[>svg]:px-2.5",
|
|
251
|
+
lg: "eb:h-10 eb:px-6 eb:has-[>svg]:px-4",
|
|
252
|
+
icon: "eb:size-8",
|
|
253
|
+
"icon-xs": "eb:size-6 eb:[&_svg:not([class*='size-'])]:size-3",
|
|
254
|
+
"icon-sm-compact": "eb:size-7 eb:[&_svg:not([class*='size-'])]:size-3",
|
|
255
|
+
"icon-sm": "eb:size-8"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
defaultVariants: { variant: "default", size: "default" }
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
function Button({ className, variant = "default", size = "default", asChild = false, type, ...props }) {
|
|
262
|
+
const Comp = asChild ? Slot.Root : "button";
|
|
263
|
+
return /* @__PURE__ */ jsx2(
|
|
264
|
+
Comp,
|
|
265
|
+
{
|
|
266
|
+
"data-slot": "button",
|
|
267
|
+
type: asChild ? type : type || "button",
|
|
268
|
+
className: cn(buttonVariants({ variant, size }), className),
|
|
269
|
+
...props
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/ui/primitives/misc.jsx
|
|
275
|
+
import { Separator as SeparatorPrimitive, Tabs as TabsPrimitive, Tooltip as TooltipPrimitive } from "radix-ui";
|
|
276
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
277
|
+
function Separator({ className, orientation = "horizontal", decorative = true, ...props }) {
|
|
278
|
+
return /* @__PURE__ */ jsx3(
|
|
279
|
+
SeparatorPrimitive.Root,
|
|
280
|
+
{
|
|
281
|
+
"data-slot": "separator",
|
|
282
|
+
decorative,
|
|
283
|
+
orientation,
|
|
284
|
+
className: cn(
|
|
285
|
+
"eb:shrink-0 eb:bg-border eb:data-[orientation=horizontal]:h-px eb:data-[orientation=horizontal]:w-full eb:data-[orientation=vertical]:h-full eb:data-[orientation=vertical]:w-px",
|
|
286
|
+
className
|
|
287
|
+
),
|
|
288
|
+
...props
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
function Skeleton({ className, as: Comp = "div", ...props }) {
|
|
293
|
+
return /* @__PURE__ */ jsx3(Comp, { "data-slot": "skeleton", className: cn("eb:animate-pulse eb:rounded-lg2 eb:bg-accent", className), ...props });
|
|
294
|
+
}
|
|
295
|
+
function SectionLabel({ className, ...props }) {
|
|
296
|
+
return /* @__PURE__ */ jsx3(
|
|
297
|
+
"span",
|
|
298
|
+
{
|
|
299
|
+
"data-slot": "section-label",
|
|
300
|
+
className: cn("eb:font-mono eb:text-[10px] eb:uppercase eb:tracking-[0.14em] eb:text-foreground-muted", className),
|
|
301
|
+
...props
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
function TruncatedText({ children, className, as: Comp = "span", ...props }) {
|
|
306
|
+
return /* @__PURE__ */ jsx3(
|
|
307
|
+
Comp,
|
|
308
|
+
{
|
|
309
|
+
className: cn("eb:block eb:truncate", className),
|
|
310
|
+
title: typeof children === "string" ? children : void 0,
|
|
311
|
+
...props,
|
|
312
|
+
children
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
function Tabs({ className, ...props }) {
|
|
317
|
+
return /* @__PURE__ */ jsx3(TabsPrimitive.Root, { "data-slot": "tabs", className: cn("eb:flex eb:flex-col eb:gap-2", className), ...props });
|
|
318
|
+
}
|
|
319
|
+
function TabsList({ className, ...props }) {
|
|
320
|
+
return /* @__PURE__ */ jsx3(
|
|
321
|
+
TabsPrimitive.List,
|
|
322
|
+
{
|
|
323
|
+
"data-slot": "tabs-list",
|
|
324
|
+
className: cn(
|
|
325
|
+
"eb:inline-flex eb:h-8 eb:w-fit eb:items-center eb:justify-center eb:rounded-lg2 eb:bg-muted eb:p-[3px] eb:text-muted-foreground",
|
|
326
|
+
className
|
|
327
|
+
),
|
|
328
|
+
...props
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function TabsTrigger({ className, ...props }) {
|
|
333
|
+
return /* @__PURE__ */ jsx3(
|
|
334
|
+
TabsPrimitive.Trigger,
|
|
335
|
+
{
|
|
336
|
+
"data-slot": "tabs-trigger",
|
|
337
|
+
className: cn(
|
|
338
|
+
"eb:inline-flex eb:h-[calc(100%-1px)] eb:flex-1 eb:items-center eb:justify-center eb:gap-1.5 eb:rounded-lg2 eb:border eb:border-transparent eb:px-2 eb:py-1 eb:text-[13px] eb:font-medium eb:whitespace-nowrap eb:text-foreground/60 eb:transition-all eb:hover:text-foreground eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:focus-visible:outline-1 eb:disabled:pointer-events-none eb:disabled:opacity-50 eb:data-[state=active]:bg-background eb:data-[state=active]:text-foreground eb:data-[state=active]:shadow-sm eb:[&_svg]:pointer-events-none eb:[&_svg]:shrink-0 eb:[&_svg:not([class*='size-'])]:size-4",
|
|
339
|
+
className
|
|
340
|
+
),
|
|
341
|
+
...props
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
function TooltipProvider({ delayDuration = 0, ...props }) {
|
|
346
|
+
return /* @__PURE__ */ jsx3(TooltipPrimitive.Provider, { delayDuration, ...props });
|
|
347
|
+
}
|
|
348
|
+
function Tooltip(props) {
|
|
349
|
+
return /* @__PURE__ */ jsx3(TooltipPrimitive.Root, { ...props });
|
|
350
|
+
}
|
|
351
|
+
function TooltipTrigger(props) {
|
|
352
|
+
return /* @__PURE__ */ jsx3(TooltipPrimitive.Trigger, { ...props });
|
|
353
|
+
}
|
|
354
|
+
function TooltipContent({ className, sideOffset = 4, children, ...props }) {
|
|
355
|
+
return /* @__PURE__ */ jsx3(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx3(
|
|
356
|
+
TooltipPrimitive.Content,
|
|
357
|
+
{
|
|
358
|
+
"data-slot": "tooltip-content",
|
|
359
|
+
sideOffset,
|
|
360
|
+
className: cn(
|
|
361
|
+
"eb-scope eb:z-50 eb:w-fit eb:origin-(--radix-tooltip-content-transform-origin) eb:animate-in eb:rounded-lg2 eb:border eb:border-border eb:bg-popover eb:px-3 eb:py-1.5 eb:text-xs eb:text-balance eb:text-popover-foreground eb:shadow-md eb:fade-in-0 eb:zoom-in-95 eb:data-[state=closed]:animate-out eb:data-[state=closed]:fade-out-0 eb:data-[state=closed]:zoom-out-95",
|
|
362
|
+
className
|
|
363
|
+
),
|
|
364
|
+
...props,
|
|
365
|
+
children
|
|
366
|
+
}
|
|
367
|
+
) });
|
|
368
|
+
}
|
|
369
|
+
function TooltipWrap({ label, side = "top", children, asChild = true, delayDuration }) {
|
|
370
|
+
if (!label) return children;
|
|
371
|
+
return /* @__PURE__ */ jsxs2(Tooltip, { delayDuration, children: [
|
|
372
|
+
/* @__PURE__ */ jsx3(TooltipTrigger, { asChild, children }),
|
|
373
|
+
/* @__PURE__ */ jsx3(TooltipContent, { side, className: "eb:px-2 eb:py-1 eb:text-[11px]", children: label })
|
|
374
|
+
] });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/ui/context.jsx
|
|
378
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
379
|
+
var NoticeContext = createContext({ notices: [], dismiss: () => {
|
|
380
|
+
} });
|
|
381
|
+
var UiContext = createContext({
|
|
382
|
+
t: identity,
|
|
383
|
+
locale: "en",
|
|
384
|
+
/** @type {(level: NotifyLevel, message: string) => void} */
|
|
385
|
+
notify: () => {
|
|
386
|
+
},
|
|
387
|
+
/** @type {(options: ConfirmOptions) => Promise<boolean>} */
|
|
388
|
+
confirm: async () => false
|
|
389
|
+
});
|
|
390
|
+
var useUi = () => useContext(UiContext);
|
|
391
|
+
var useT = () => useContext(UiContext).t;
|
|
392
|
+
var NOTICE_ICON = { success: CheckCircle2, info: Info, warning: TriangleAlert, error: AlertCircle };
|
|
393
|
+
var NOTICE_TTL = { success: 3500, info: 4e3, warning: 6e3, error: 7e3 };
|
|
394
|
+
function NoticeArea() {
|
|
395
|
+
const t = useT();
|
|
396
|
+
const { notices, dismiss: onDismiss } = useContext(NoticeContext);
|
|
397
|
+
return /* @__PURE__ */ jsx4("div", { className: "eb:pointer-events-none eb:absolute eb:right-3 eb:bottom-3 eb:z-40 eb:flex eb:w-80 eb:max-w-[calc(100%-1.5rem)] eb:flex-col eb:gap-2", children: notices.map((notice) => {
|
|
398
|
+
const Icon = NOTICE_ICON[notice.level] || Info;
|
|
399
|
+
return /* @__PURE__ */ jsxs3(
|
|
400
|
+
"div",
|
|
401
|
+
{
|
|
402
|
+
role: notice.level === "error" ? "alert" : "status",
|
|
403
|
+
"data-level": notice.level,
|
|
404
|
+
className: "eb:pointer-events-auto eb:flex eb:items-start eb:gap-2 eb:rounded-lg2 eb:border eb:border-border eb:bg-popover eb:px-3 eb:py-2.5 eb:text-[13px] eb:text-popover-foreground eb:shadow-lg",
|
|
405
|
+
children: [
|
|
406
|
+
/* @__PURE__ */ jsx4(
|
|
407
|
+
Icon,
|
|
408
|
+
{
|
|
409
|
+
"aria-hidden": "true",
|
|
410
|
+
className: cn(
|
|
411
|
+
"eb:mt-0.5 eb:size-4 eb:shrink-0",
|
|
412
|
+
notice.level === "error" && "eb:text-destructive",
|
|
413
|
+
notice.level === "warning" && "eb:text-warning",
|
|
414
|
+
notice.level === "success" && "eb:text-success",
|
|
415
|
+
notice.level === "info" && "eb:text-foreground-muted"
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
),
|
|
419
|
+
/* @__PURE__ */ jsx4("p", { className: "eb:min-w-0 eb:flex-1", children: notice.message }),
|
|
420
|
+
/* @__PURE__ */ jsx4(
|
|
421
|
+
"button",
|
|
422
|
+
{
|
|
423
|
+
type: "button",
|
|
424
|
+
"aria-label": t("Dismiss"),
|
|
425
|
+
className: "eb:shrink-0 eb:rounded-sm eb:text-foreground-muted eb:hover:text-foreground",
|
|
426
|
+
onClick: () => onDismiss(notice.id),
|
|
427
|
+
children: /* @__PURE__ */ jsx4(X, { className: "eb:size-3.5" })
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
notice.id
|
|
433
|
+
);
|
|
434
|
+
}) });
|
|
435
|
+
}
|
|
436
|
+
function UiProvider({ locale = "en", messages, onNotify, children }) {
|
|
437
|
+
const t = useMemo(() => translatorFor(locale, messages), [locale, messages]);
|
|
438
|
+
const [notices, setNotices] = useState([]);
|
|
439
|
+
const timers = useRef(/* @__PURE__ */ new Map());
|
|
440
|
+
const dismiss = useCallback((id) => {
|
|
441
|
+
clearTimeout(timers.current.get(id));
|
|
442
|
+
timers.current.delete(id);
|
|
443
|
+
setNotices((list) => list.filter((n) => n.id !== id));
|
|
444
|
+
}, []);
|
|
445
|
+
const hostNotify = useRef(onNotify);
|
|
446
|
+
hostNotify.current = onNotify;
|
|
447
|
+
const nextId = useRef(0);
|
|
448
|
+
const notify = useCallback(
|
|
449
|
+
(level, message) => {
|
|
450
|
+
if (hostNotify.current) {
|
|
451
|
+
hostNotify.current(level, message);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const id = nextId.current += 1;
|
|
455
|
+
setNotices((list) => {
|
|
456
|
+
const same = list.filter((n) => n.level === level && n.message === message);
|
|
457
|
+
for (const notice of same) {
|
|
458
|
+
clearTimeout(timers.current.get(notice.id));
|
|
459
|
+
timers.current.delete(notice.id);
|
|
460
|
+
}
|
|
461
|
+
return [...list.filter((n) => !same.includes(n)).slice(-3), { id, level, message }];
|
|
462
|
+
});
|
|
463
|
+
timers.current.set(
|
|
464
|
+
id,
|
|
465
|
+
setTimeout(() => dismiss(id), NOTICE_TTL[level] || 4e3)
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
[dismiss]
|
|
469
|
+
);
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
const pending = timers.current;
|
|
472
|
+
return () => pending.forEach((timer) => clearTimeout(timer));
|
|
473
|
+
}, []);
|
|
474
|
+
const [request, setRequest] = useState(null);
|
|
475
|
+
const confirm = useCallback(
|
|
476
|
+
(options2 = {}) => new Promise((resolve) => {
|
|
477
|
+
setRequest((current) => {
|
|
478
|
+
current?.resolve(false);
|
|
479
|
+
return { options: options2, resolve };
|
|
480
|
+
});
|
|
481
|
+
}),
|
|
482
|
+
[]
|
|
483
|
+
);
|
|
484
|
+
const settle = (result) => {
|
|
485
|
+
request?.resolve(result);
|
|
486
|
+
setRequest(null);
|
|
487
|
+
};
|
|
488
|
+
const value = useMemo(() => ({ t, locale, notify, confirm }), [t, locale, notify, confirm]);
|
|
489
|
+
const noticeValue = useMemo(() => ({ notices, dismiss }), [notices, dismiss]);
|
|
490
|
+
const options = request?.options || {};
|
|
491
|
+
return /* @__PURE__ */ jsx4(UiContext.Provider, { value, children: /* @__PURE__ */ jsxs3(TooltipProvider, { delayDuration: 300, children: [
|
|
492
|
+
/* @__PURE__ */ jsx4(NoticeContext.Provider, { value: noticeValue, children }),
|
|
493
|
+
/* @__PURE__ */ jsx4(AlertDialog, { open: Boolean(request), onOpenChange: (open) => !open && settle(false), children: /* @__PURE__ */ jsxs3(AlertDialogContent, { children: [
|
|
494
|
+
/* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:items-center eb:gap-3", children: [
|
|
495
|
+
/* @__PURE__ */ jsx4(
|
|
496
|
+
"span",
|
|
497
|
+
{
|
|
498
|
+
className: cn(
|
|
499
|
+
"eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-lg2",
|
|
500
|
+
options.destructive ? "eb:bg-destructive/10 eb:text-destructive" : "eb:bg-primary/10 eb:text-primary"
|
|
501
|
+
),
|
|
502
|
+
children: /* @__PURE__ */ jsx4(AlertCircle, { className: "eb:size-5" })
|
|
503
|
+
}
|
|
504
|
+
),
|
|
505
|
+
/* @__PURE__ */ jsx4(AlertDialogTitle, { className: "eb:text-base", children: options.title || t("Confirm") })
|
|
506
|
+
] }),
|
|
507
|
+
options.description && /* @__PURE__ */ jsx4(AlertDialogDescription, { className: "eb:text-[13px] eb:leading-relaxed", children: options.description }),
|
|
508
|
+
/* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:flex-col-reverse eb:gap-2 eb:sm:flex-row eb:sm:justify-end", children: [
|
|
509
|
+
/* @__PURE__ */ jsx4(AlertDialogCancel, { className: buttonVariants({ variant: "outline" }), onClick: () => settle(false), children: options.cancelLabel || t("Cancel") }),
|
|
510
|
+
/* @__PURE__ */ jsx4(
|
|
511
|
+
AlertDialogAction,
|
|
512
|
+
{
|
|
513
|
+
className: buttonVariants({ variant: options.destructive ? "destructive" : "default" }),
|
|
514
|
+
onClick: () => settle(true),
|
|
515
|
+
children: options.confirmLabel || t("Confirm")
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
] })
|
|
519
|
+
] }) })
|
|
520
|
+
] }) });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/features/templates/starter.js
|
|
524
|
+
function escapeXml(text) {
|
|
525
|
+
return String(text ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
526
|
+
}
|
|
527
|
+
var SIDE = 'padding-left="32px" padding-right="32px"';
|
|
528
|
+
var TEXT = `font-family="${FONT_STACK}" color="${PALETTE.ink}"`;
|
|
529
|
+
function senderHeaderMjml(sender, t) {
|
|
530
|
+
const name = escapeXml(sender.name || t("Your company"));
|
|
531
|
+
return sender.logoUrl ? `<mj-image src="${escapeXml(sender.logoUrl)}" alt="${name}" width="${Number(sender.logoWidth) || 140}px" align="left" ${SIDE}></mj-image>` : `<mj-text ${TEXT} font-size="18px" line-height="24px" font-weight="bold" ${SIDE}>${name}</mj-text>`;
|
|
532
|
+
}
|
|
533
|
+
function footerSectionMjml(sender, t) {
|
|
534
|
+
const name = escapeXml(sender.name || t("Your company"));
|
|
535
|
+
const footer = [name, sender.address ? escapeXml(sender.address) : ""].filter(Boolean).join(" \xB7 ");
|
|
536
|
+
return `<mj-section padding-top="20px" padding-bottom="28px"><mj-column><mj-text font-family="${FONT_STACK}" font-size="12px" line-height="18px" color="#6d7484" align="center" ${SIDE}>${footer}<br /><a href="{{ unsubscribe }}" style="color:#6d7484;text-decoration:underline;">${escapeXml(t("Unsubscribe"))}</a></mj-text></mj-column></mj-section>`;
|
|
537
|
+
}
|
|
538
|
+
function createStarterMjml({ sender = {}, buttonUrl = DEFAULT_BUTTON_URL, t = identity } = {}) {
|
|
539
|
+
return [
|
|
540
|
+
"<mjml>",
|
|
541
|
+
`<mj-body background-color="${PALETTE.canvas}" width="600px">`,
|
|
542
|
+
// Header: logo or sender name.
|
|
543
|
+
`<mj-section background-color="${PALETTE.surface}" padding-top="28px" padding-bottom="8px">`,
|
|
544
|
+
"<mj-column>",
|
|
545
|
+
senderHeaderMjml(sender, t),
|
|
546
|
+
"</mj-column>",
|
|
547
|
+
"</mj-section>",
|
|
548
|
+
// Body: headline, text, button.
|
|
549
|
+
`<mj-section background-color="${PALETTE.surface}" padding-top="8px" padding-bottom="28px">`,
|
|
550
|
+
"<mj-column>",
|
|
551
|
+
`<mj-text ${TEXT} font-size="24px" line-height="32px" font-weight="bold" ${SIDE}>${escapeXml(t("Your headline"))}</mj-text>`,
|
|
552
|
+
`<mj-text ${TEXT} font-size="15px" line-height="24px" ${SIDE}>${escapeXml(t("Hello {name},", { name: "{{contact.firstname}}" }))}<br /><br />${escapeXml(t("Your text."))}</mj-text>`,
|
|
553
|
+
`<mj-button href="${escapeXml(buttonUrl)}" background-color="${PALETTE.navy}" color="${PALETTE.white}" font-family="${FONT_STACK}" font-size="15px" font-weight="bold" border-radius="6px" inner-padding="12px 28px" align="left" ${SIDE}>${escapeXml(t("Learn more"))}</mj-button>`,
|
|
554
|
+
"</mj-column>",
|
|
555
|
+
"</mj-section>",
|
|
556
|
+
// Footer: sender and unsubscribe link.
|
|
557
|
+
footerSectionMjml(sender, t),
|
|
558
|
+
"</mj-body>",
|
|
559
|
+
"</mjml>"
|
|
560
|
+
].join("");
|
|
561
|
+
}
|
|
562
|
+
var STARTER_MJML = createStarterMjml();
|
|
563
|
+
|
|
564
|
+
// src/blocks/social/index.js
|
|
565
|
+
var SOCIAL_NETWORKS = [
|
|
566
|
+
{ value: "facebook-noshare", label: "Facebook", href: "https://www.facebook.com/" },
|
|
567
|
+
{ value: "instagram-noshare", label: "Instagram", href: "https://www.instagram.com/" },
|
|
568
|
+
{ value: "linkedin-noshare", label: "LinkedIn", href: "https://www.linkedin.com/" },
|
|
569
|
+
{ value: "x-noshare", label: "X", href: "https://x.com/" },
|
|
570
|
+
{ value: "youtube-noshare", label: "YouTube", href: "https://www.youtube.com/" },
|
|
571
|
+
{ value: "github-noshare", label: "GitHub", href: "https://github.com/" }
|
|
572
|
+
];
|
|
573
|
+
var DEFAULT_NETWORKS = ["facebook-noshare", "instagram-noshare", "linkedin-noshare", "x-noshare"];
|
|
574
|
+
var MJML = '<mj-social mode="horizontal" align="center" icon-size="28px" border-radius="14px" inner-padding="6px">' + DEFAULT_NETWORKS.map((name) => {
|
|
575
|
+
const network = SOCIAL_NETWORKS.find((n) => n.value === name);
|
|
576
|
+
return `<mj-social-element name="${name}" href="${network?.href}"></mj-social-element>`;
|
|
577
|
+
}).join("") + "</mj-social>";
|
|
578
|
+
var socialBlock = {
|
|
579
|
+
id: "social",
|
|
580
|
+
label: "Social links",
|
|
581
|
+
description: "Icons linking to your profiles",
|
|
582
|
+
icon: "Share2",
|
|
583
|
+
kind: "content",
|
|
584
|
+
tagName: "mj-social",
|
|
585
|
+
order: 60,
|
|
586
|
+
mjml: MJML,
|
|
587
|
+
sectionMjml: inWhiteSection(MJML),
|
|
588
|
+
properties: [
|
|
589
|
+
{ key: "icon-size", label: "Icon size", type: "size", group: GROUPS.layout, min: 16, max: 48, unit: "px" },
|
|
590
|
+
prop.radius(24),
|
|
591
|
+
prop.align(),
|
|
592
|
+
{
|
|
593
|
+
key: "mode",
|
|
594
|
+
label: "Direction",
|
|
595
|
+
type: "select",
|
|
596
|
+
group: GROUPS.layout,
|
|
597
|
+
options: [
|
|
598
|
+
{ value: "horizontal", label: "Horizontal" },
|
|
599
|
+
{ value: "vertical", label: "Vertical" }
|
|
600
|
+
]
|
|
601
|
+
},
|
|
602
|
+
prop.padding("inner-padding", "Space around each icon"),
|
|
603
|
+
prop.padding()
|
|
604
|
+
]
|
|
605
|
+
};
|
|
606
|
+
var socialElementBlock = {
|
|
607
|
+
id: "social-element",
|
|
608
|
+
label: "Social link",
|
|
609
|
+
icon: "Link",
|
|
610
|
+
kind: "structure",
|
|
611
|
+
tagName: "mj-social-element",
|
|
612
|
+
order: 903,
|
|
613
|
+
properties: [
|
|
614
|
+
{
|
|
615
|
+
key: "name",
|
|
616
|
+
label: "Network",
|
|
617
|
+
type: "select",
|
|
618
|
+
group: GROUPS.content,
|
|
619
|
+
options: SOCIAL_NETWORKS.map(({ value, label }) => ({ value, label }))
|
|
620
|
+
},
|
|
621
|
+
prop.href("Profile URL"),
|
|
622
|
+
prop.background("background-color", "Icon background")
|
|
623
|
+
]
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/features/templates/gallery.js
|
|
627
|
+
var WHITE_SECTION = `background-color="${PALETTE.surface}"`;
|
|
628
|
+
function section(inner, { top = 8, bottom = 8, x = 0 } = {}) {
|
|
629
|
+
const sides = x ? ` padding-left="${x}px" padding-right="${x}px"` : "";
|
|
630
|
+
return `<mj-section ${WHITE_SECTION} padding-top="${top}px" padding-bottom="${bottom}px"${sides}>${inner}</mj-section>`;
|
|
631
|
+
}
|
|
632
|
+
function column(inner, attrs = "") {
|
|
633
|
+
return `<mj-column${attrs ? ` ${attrs}` : ""}>${inner}</mj-column>`;
|
|
634
|
+
}
|
|
635
|
+
function heading(text, { size = 24, color = PALETTE.navy, align = "left", side = true } = {}) {
|
|
636
|
+
return `<mj-text font-family="${FONT_STACK}" font-size="${size}px" line-height="${Math.round(size * 1.3)}px" font-weight="bold" color="${color}" align="${align}"${side ? ` ${SIDE}` : ""}>${text}</mj-text>`;
|
|
637
|
+
}
|
|
638
|
+
function paragraph(html, { size = 15, color = PALETTE.ink, align = "left", side = true } = {}) {
|
|
639
|
+
return `<mj-text font-family="${FONT_STACK}" font-size="${size}px" line-height="${Math.round(size * 1.6)}px" color="${color}" align="${align}"${side ? ` ${SIDE}` : ""}>${html}</mj-text>`;
|
|
640
|
+
}
|
|
641
|
+
function button(label, { align = "left", background = PALETTE.navy, color = PALETTE.white, side = true } = {}) {
|
|
642
|
+
return `<mj-button href="${DEFAULT_BUTTON_URL}" background-color="${background}" color="${color}" font-family="${FONT_STACK}" font-size="15px" font-weight="bold" border-radius="6px" inner-padding="12px 28px" align="${align}"${side ? ` ${SIDE}` : ""}>${label}</mj-button>`;
|
|
643
|
+
}
|
|
644
|
+
var IMAGE = '<mj-image align="center"></mj-image>';
|
|
645
|
+
function header(sender, t) {
|
|
646
|
+
return `<mj-section ${WHITE_SECTION} padding-top="28px" padding-bottom="8px">${column(senderHeaderMjml(sender, t))}</mj-section>`;
|
|
647
|
+
}
|
|
648
|
+
function banner({ title, text, cta, background = PALETTE.navy }) {
|
|
649
|
+
return `<mj-section background-color="${background}" background-size="cover" background-position="center center" background-repeat="no-repeat" padding-top="48px" padding-bottom="48px">` + column(
|
|
650
|
+
heading(title, { size: 30, color: PALETTE.white, align: "center" }) + paragraph(text, { size: 16, color: PALETTE.border, align: "center" }) + (cta ? button(cta, { align: "center", background: PALETTE.white, color: PALETTE.navy }) : "")
|
|
651
|
+
) + "</mj-section>";
|
|
652
|
+
}
|
|
653
|
+
function social() {
|
|
654
|
+
const elements = ["facebook-noshare", "instagram-noshare", "linkedin-noshare"].map((name) => `<mj-social-element name="${name}" href="${SOCIAL_NETWORKS.find((n) => n.value === name)?.href}"></mj-social-element>`).join("");
|
|
655
|
+
return section(
|
|
656
|
+
column(
|
|
657
|
+
`<mj-social mode="horizontal" align="center" icon-size="28px" border-radius="14px" inner-padding="6px">${elements}</mj-social>`
|
|
658
|
+
),
|
|
659
|
+
{ top: 16, bottom: 16 }
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
function email(body) {
|
|
663
|
+
return `<mjml><mj-body background-color="${PALETTE.canvas}" width="600px">${body}</mj-body></mjml>`;
|
|
664
|
+
}
|
|
665
|
+
var esc = escapeXml;
|
|
666
|
+
var TEMPLATE_GALLERY = [
|
|
667
|
+
{
|
|
668
|
+
id: "blank",
|
|
669
|
+
name: "Blank",
|
|
670
|
+
description: "An empty page to build from scratch",
|
|
671
|
+
create: () => email("")
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
id: "simple",
|
|
675
|
+
name: "Simple",
|
|
676
|
+
description: "Logo, headline, text and a button",
|
|
677
|
+
create: ({ sender, t }) => createStarterMjml({ sender, t })
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
id: "newsletter",
|
|
681
|
+
name: "Newsletter",
|
|
682
|
+
description: "Monthly news: banner, two articles, highlights",
|
|
683
|
+
meta: (t) => ({
|
|
684
|
+
subject: t("The news of the month"),
|
|
685
|
+
preheader: t("What changed, what is coming, and two stories worth your time.")
|
|
686
|
+
}),
|
|
687
|
+
create: ({ sender, t }) => email(
|
|
688
|
+
header(sender, t) + banner({
|
|
689
|
+
title: esc(t("The news of the month")),
|
|
690
|
+
text: esc(t("What changed, what is coming, and two stories worth your time.")),
|
|
691
|
+
cta: null
|
|
692
|
+
}) + [1, 2].map(
|
|
693
|
+
(n) => section(
|
|
694
|
+
column(IMAGE, 'vertical-align="middle"') + column(
|
|
695
|
+
heading(esc(t("Article {n}", { n })), { size: 20, side: false }) + paragraph(esc(t("Two or three sentences that sum up the story and make people want the rest.")), {
|
|
696
|
+
side: false
|
|
697
|
+
}) + button(esc(t("Read more")), { side: false }),
|
|
698
|
+
'vertical-align="middle"'
|
|
699
|
+
),
|
|
700
|
+
{ top: 16, bottom: 16, x: 16 }
|
|
701
|
+
)
|
|
702
|
+
).join("") + section(column(heading(esc(t("In brief")), { size: 20 })), { top: 20, bottom: 0 }) + section(
|
|
703
|
+
[t("A new feature"), t("An event"), t("A tip")].map(
|
|
704
|
+
(title) => column(
|
|
705
|
+
heading(esc(title), { size: 16, side: false }) + paragraph(esc(t("One sentence about it.")), { size: 14, side: false })
|
|
706
|
+
)
|
|
707
|
+
).join(""),
|
|
708
|
+
{ bottom: 20, x: 16 }
|
|
709
|
+
) + social() + footerSectionMjml(sender, t)
|
|
710
|
+
)
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
id: "welcome",
|
|
714
|
+
name: "Welcome",
|
|
715
|
+
description: "Greets a new customer and lists the first steps",
|
|
716
|
+
meta: (t) => ({ subject: t("Welcome aboard!"), preheader: t("Three steps to get started.") }),
|
|
717
|
+
create: ({ sender, t }) => email(
|
|
718
|
+
header(sender, t) + section(
|
|
719
|
+
column(
|
|
720
|
+
heading(esc(t("Welcome, {name}!", { name: "{{contact.firstname}}" })), { size: 28 }) + paragraph(esc(t("Thank you for joining us. Here is how to get the most out of your account.")))
|
|
721
|
+
),
|
|
722
|
+
{ top: 16, bottom: 8 }
|
|
723
|
+
) + section(
|
|
724
|
+
[
|
|
725
|
+
[t("1. Complete your profile"), t("So we can tailor what we send you.")],
|
|
726
|
+
[t("2. Explore"), t("Take a tour of what you can do.")],
|
|
727
|
+
[t("3. Ask us anything"), t("Reply to this email: a person reads it.")]
|
|
728
|
+
].map(
|
|
729
|
+
([title, text]) => column(
|
|
730
|
+
heading(esc(title), { size: 16, side: false }) + paragraph(esc(text), { size: 14, side: false }),
|
|
731
|
+
`background-color="${PALETTE.canvas}" padding-top="12px" padding-bottom="12px" padding-left="12px" padding-right="12px"`
|
|
732
|
+
)
|
|
733
|
+
).join(""),
|
|
734
|
+
{ x: 24 }
|
|
735
|
+
) + section(column(button(esc(t("Get started")), { align: "center" })), { top: 16, bottom: 28 }) + footerSectionMjml(sender, t)
|
|
736
|
+
)
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "promotion",
|
|
740
|
+
name: "Promotion",
|
|
741
|
+
description: "A sale with a discount code and two products",
|
|
742
|
+
meta: (t) => ({ subject: t("30% off this weekend only"), preheader: t("With the code SPRING30, until Sunday midnight.") }),
|
|
743
|
+
create: ({ sender, t }) => email(
|
|
744
|
+
header(sender, t) + banner({
|
|
745
|
+
title: esc(t("30% off everything")),
|
|
746
|
+
text: esc(t("This weekend only, with the code SPRING30.")),
|
|
747
|
+
cta: esc(t("Shop the sale")),
|
|
748
|
+
background: PALETTE.brand
|
|
749
|
+
}) + section(
|
|
750
|
+
[t("Product one"), t("Product two")].map(
|
|
751
|
+
(name) => column(
|
|
752
|
+
IMAGE + heading(esc(name), { size: 18, side: false }) + paragraph(esc(t("\u20AC49 instead of \u20AC70")), { size: 15, side: false }) + button(esc(t("Buy")), { side: false })
|
|
753
|
+
)
|
|
754
|
+
).join(""),
|
|
755
|
+
{ top: 20, bottom: 20, x: 16 }
|
|
756
|
+
) + section(
|
|
757
|
+
column(
|
|
758
|
+
paragraph(esc(t("Offer valid until Sunday midnight, while stocks last.")), {
|
|
759
|
+
size: 12,
|
|
760
|
+
color: "#6d7484",
|
|
761
|
+
align: "center"
|
|
762
|
+
})
|
|
763
|
+
)
|
|
764
|
+
) + footerSectionMjml(sender, t)
|
|
765
|
+
)
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
id: "event",
|
|
769
|
+
name: "Event invitation",
|
|
770
|
+
description: "Date, place and a button to book a seat",
|
|
771
|
+
meta: (t) => ({ subject: t("You're invited"), preheader: t("Save the date \u2014 seats are limited.") }),
|
|
772
|
+
create: ({ sender, t }) => email(
|
|
773
|
+
header(sender, t) + banner({
|
|
774
|
+
title: esc(t("You're invited")),
|
|
775
|
+
text: esc(t("An evening of talks and drinks with the team and our customers.")),
|
|
776
|
+
cta: null
|
|
777
|
+
}) + section(
|
|
778
|
+
column(heading(esc(t("When")), { size: 16, side: false }) + paragraph(esc(t("Thursday, 7 pm")), { side: false })) + column(heading(esc(t("Where")), { size: 16, side: false }) + paragraph(esc(t("Our offices, 1 Main Street")), { side: false })),
|
|
779
|
+
{ top: 24, x: 16 }
|
|
780
|
+
) + section(
|
|
781
|
+
column(
|
|
782
|
+
paragraph(esc(t("Seats are limited: book yours now, it takes ten seconds."))) + button(esc(t("Book my seat")), { align: "left" })
|
|
783
|
+
),
|
|
784
|
+
{ bottom: 28 }
|
|
785
|
+
) + footerSectionMjml(sender, t)
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
];
|
|
789
|
+
function buildGallery({ templates = TEMPLATE_GALLERY, sender = {}, t = identity } = {}) {
|
|
790
|
+
return templates.map((template) => {
|
|
791
|
+
const meta = template.meta ? template.meta(t) : {};
|
|
792
|
+
return {
|
|
793
|
+
id: template.id,
|
|
794
|
+
name: t(template.name),
|
|
795
|
+
description: t(template.description),
|
|
796
|
+
mjml: template.create({ sender, t }),
|
|
797
|
+
subject: meta.subject || "",
|
|
798
|
+
preheader: meta.preheader || ""
|
|
799
|
+
};
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
var THUMBNAIL_IMAGE = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 300'%3E%3Crect width='600' height='300' fill='%23dfe6f0'/%3E%3C/svg%3E";
|
|
803
|
+
function withThumbnailImages(mjml) {
|
|
804
|
+
return String(mjml || "").replace(/<mj-image(?![^>]*\ssrc=)([^>]*)>/g, `<mj-image src="${THUMBNAIL_IMAGE}"$1>`);
|
|
805
|
+
}
|
|
806
|
+
function withPlaceholderImages(html) {
|
|
807
|
+
return String(html || "").replace(/<img(?![^>]*\ssrc=)(\s[^>]*)?>/g, (_m, attrs = "") => `<img src="${THUMBNAIL_IMAGE}"${attrs}>`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/storage/client.js
|
|
811
|
+
var StorageError = class extends Error {
|
|
812
|
+
/** @param {string} message @param {number} status @param {string} [code] */
|
|
813
|
+
constructor(message, status, code) {
|
|
814
|
+
super(message);
|
|
815
|
+
this.name = "StorageError";
|
|
816
|
+
this.status = status;
|
|
817
|
+
this.code = code;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
function assertUploadable(file) {
|
|
821
|
+
const error = imageUploadError(file);
|
|
822
|
+
if (error) throw new StorageError(error, 415, "UNSUPPORTED_IMAGE");
|
|
823
|
+
}
|
|
824
|
+
function withDocument(template) {
|
|
825
|
+
if (!template || !template.document) return template;
|
|
826
|
+
return { ...template, document: readDocument(template.document) };
|
|
827
|
+
}
|
|
828
|
+
function createHttpStorage({ baseUrl = "/api/email-builder", headers = {}, fetch: fetchImpl, generate = false, brand = false } = {}) {
|
|
829
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
830
|
+
const doFetch = fetchImpl || ((input, init) => fetch(input, init));
|
|
831
|
+
const extraHeaders = () => typeof headers === "function" ? headers() : headers;
|
|
832
|
+
async function unwrap(res) {
|
|
833
|
+
const json = await res.json().catch(() => ({}));
|
|
834
|
+
if (!res.ok || json.error) {
|
|
835
|
+
throw new StorageError(json.error || `Request failed (HTTP ${res.status})`, res.status, json.code);
|
|
836
|
+
}
|
|
837
|
+
return json.data;
|
|
838
|
+
}
|
|
839
|
+
async function call(path, init) {
|
|
840
|
+
const res = await doFetch(`${root}${path}`, {
|
|
841
|
+
...init,
|
|
842
|
+
headers: { "Content-Type": "application/json", ...extraHeaders(), ...init?.headers || {} }
|
|
843
|
+
});
|
|
844
|
+
return unwrap(res);
|
|
845
|
+
}
|
|
846
|
+
function body(payload) {
|
|
847
|
+
return JSON.stringify({ ...payload, document: readDocument(payload.document) });
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
list: async () => call("/templates"),
|
|
851
|
+
load: async (id) => withDocument(await call(`/templates/${encodeURIComponent(id)}`)),
|
|
852
|
+
remove: async (id) => {
|
|
853
|
+
await call(`/templates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
854
|
+
},
|
|
855
|
+
create: async (payload) => call("/templates", { method: "POST", body: body(payload) }),
|
|
856
|
+
duplicate: async (id) => call("/templates", { method: "POST", body: JSON.stringify({ duplicateOf: id }) }),
|
|
857
|
+
save: async (id, payload) => call(`/templates/${encodeURIComponent(id)}`, { method: "PUT", body: body(payload) }),
|
|
858
|
+
listVersions: async (id) => call(`/templates/${encodeURIComponent(id)}/versions`),
|
|
859
|
+
getVersion: async (id, versionId) => withDocument(await call(`/templates/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}`)),
|
|
860
|
+
render: async (input) => call("/render", { method: "POST", body: JSON.stringify(input) }),
|
|
861
|
+
sendTest: async (input) => call("/test", { method: "POST", body: JSON.stringify(input) }),
|
|
862
|
+
listVariables: async () => call("/variables"),
|
|
863
|
+
saveVariable: async (variable, { create = false } = {}) => create ? call("/variables", { method: "POST", body: JSON.stringify(variable) }) : call(`/variables/${encodeURIComponent(String(variable.key))}`, { method: "PUT", body: JSON.stringify(variable) }),
|
|
864
|
+
deleteVariable: async (key) => {
|
|
865
|
+
await call(`/variables/${encodeURIComponent(key)}`, { method: "DELETE" });
|
|
866
|
+
},
|
|
867
|
+
...generate ? {
|
|
868
|
+
generate: async (input, { signal } = {}) => call("/generate", { method: "POST", body: JSON.stringify(input), signal }),
|
|
869
|
+
rewrite: async (input, { signal } = {}) => call("/rewrite", { method: "POST", body: JSON.stringify(input), signal }),
|
|
870
|
+
suggestSubjects: async (input, { signal } = {}) => call("/subjects", { method: "POST", body: JSON.stringify(input), signal })
|
|
871
|
+
} : {},
|
|
872
|
+
...brand ? {
|
|
873
|
+
loadBrand: async () => call("/brand"),
|
|
874
|
+
saveBrand: async (value) => call("/brand", { method: "PUT", body: JSON.stringify(value) }),
|
|
875
|
+
...brand === true || brand.import !== false ? {
|
|
876
|
+
importBrand: async (url, { signal } = {}) => call("/brand/import", { method: "POST", body: JSON.stringify({ url }), signal })
|
|
877
|
+
} : {}
|
|
878
|
+
} : {},
|
|
879
|
+
async uploadImage(file) {
|
|
880
|
+
assertUploadable(file);
|
|
881
|
+
const form = new FormData();
|
|
882
|
+
form.append("file", file, file.name || "image");
|
|
883
|
+
const res = await doFetch(`${root}/assets`, { method: "POST", body: form, headers: extraHeaders() });
|
|
884
|
+
const data = await unwrap(res);
|
|
885
|
+
return { path: data.path };
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function createMemoryStorage({
|
|
890
|
+
render,
|
|
891
|
+
sendTest,
|
|
892
|
+
generate,
|
|
893
|
+
rewrite,
|
|
894
|
+
suggestSubjects,
|
|
895
|
+
importBrand,
|
|
896
|
+
brand = null,
|
|
897
|
+
assetsPath = DEFAULT_ASSETS_PATH,
|
|
898
|
+
systemVariables = DEFAULT_VARIABLES
|
|
899
|
+
} = {}) {
|
|
900
|
+
const rows = /* @__PURE__ */ new Map();
|
|
901
|
+
const versions = /* @__PURE__ */ new Map();
|
|
902
|
+
const images = /* @__PURE__ */ new Map();
|
|
903
|
+
const customVariables = /* @__PURE__ */ new Map();
|
|
904
|
+
let savedBrand = cleanBrand(brand);
|
|
905
|
+
let nextId = 1;
|
|
906
|
+
let tick = Date.parse("2026-01-01T00:00:00.000Z");
|
|
907
|
+
const now = () => new Date(tick += 1e3).toISOString();
|
|
908
|
+
function toTemplate(row) {
|
|
909
|
+
return {
|
|
910
|
+
id: row.id,
|
|
911
|
+
name: row.name,
|
|
912
|
+
description: row.description,
|
|
913
|
+
subject: row.subject,
|
|
914
|
+
preheader: row.preheader,
|
|
915
|
+
revision: row.revision,
|
|
916
|
+
document: readDocument(row.documentJson),
|
|
917
|
+
mjml: row.mjml,
|
|
918
|
+
createdAt: row.createdAt,
|
|
919
|
+
updatedAt: row.updatedAt
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function snapshot(row) {
|
|
923
|
+
const list = versions.get(String(row.id)) || [];
|
|
924
|
+
const entry = {
|
|
925
|
+
id: list.length + 1,
|
|
926
|
+
versionNumber: list.length + 1,
|
|
927
|
+
name: row.name,
|
|
928
|
+
subject: row.subject,
|
|
929
|
+
createdBy: null,
|
|
930
|
+
createdAt: row.revision,
|
|
931
|
+
documentJson: row.documentJson,
|
|
932
|
+
mjml: row.mjml
|
|
933
|
+
};
|
|
934
|
+
versions.set(String(row.id), [...list, entry]);
|
|
935
|
+
return { id: entry.id, versionNumber: entry.versionNumber };
|
|
936
|
+
}
|
|
937
|
+
function rowOf(id) {
|
|
938
|
+
const row = rows.get(String(id));
|
|
939
|
+
if (!row) throw new StorageError("Template not found", 404, "NOT_FOUND");
|
|
940
|
+
return row;
|
|
941
|
+
}
|
|
942
|
+
const storage = {
|
|
943
|
+
async list() {
|
|
944
|
+
return [...rows.values()].sort((a, b) => (b.updatedAt || b.createdAt).localeCompare(a.updatedAt || a.createdAt)).map((row) => {
|
|
945
|
+
const { document: _d, mjml: _m, ...summary } = toTemplate(row);
|
|
946
|
+
return summary;
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
async load(id) {
|
|
950
|
+
return toTemplate(rowOf(id));
|
|
951
|
+
},
|
|
952
|
+
async remove(id) {
|
|
953
|
+
rowOf(id);
|
|
954
|
+
rows.delete(String(id));
|
|
955
|
+
versions.delete(String(id));
|
|
956
|
+
},
|
|
957
|
+
async create(payload) {
|
|
958
|
+
if (!payload.name?.trim()) throw new StorageError("Name required", 400, "NAME_REQUIRED");
|
|
959
|
+
const at = now();
|
|
960
|
+
const row = {
|
|
961
|
+
id: nextId++,
|
|
962
|
+
name: payload.name.trim(),
|
|
963
|
+
description: payload.description ?? null,
|
|
964
|
+
subject: payload.subject ?? null,
|
|
965
|
+
preheader: payload.preheader ?? null,
|
|
966
|
+
documentJson: JSON.stringify(readDocument(payload.document)),
|
|
967
|
+
mjml: payload.mjml,
|
|
968
|
+
createdAt: at,
|
|
969
|
+
updatedAt: null,
|
|
970
|
+
revision: at
|
|
971
|
+
};
|
|
972
|
+
rows.set(String(row.id), row);
|
|
973
|
+
snapshot(row);
|
|
974
|
+
return { template: toTemplate(row), warnings: [] };
|
|
975
|
+
},
|
|
976
|
+
async duplicate(id) {
|
|
977
|
+
const source = rowOf(id);
|
|
978
|
+
return storage.create({
|
|
979
|
+
name: `${source.name} (copy)`,
|
|
980
|
+
description: source.description,
|
|
981
|
+
subject: source.subject,
|
|
982
|
+
preheader: source.preheader,
|
|
983
|
+
document: readDocument(source.documentJson),
|
|
984
|
+
mjml: source.mjml
|
|
985
|
+
});
|
|
986
|
+
},
|
|
987
|
+
async save(id, payload) {
|
|
988
|
+
const row = rowOf(id);
|
|
989
|
+
if (payload.revision && payload.revision !== row.revision) {
|
|
990
|
+
throw new StorageError("Template changed in the meantime \u2014 reload it before saving", 409, "CONFLICT");
|
|
991
|
+
}
|
|
992
|
+
const at = now();
|
|
993
|
+
Object.assign(row, {
|
|
994
|
+
...payload.name !== void 0 ? { name: payload.name } : {},
|
|
995
|
+
...payload.description !== void 0 ? { description: payload.description } : {},
|
|
996
|
+
...payload.subject !== void 0 ? { subject: payload.subject } : {},
|
|
997
|
+
...payload.preheader !== void 0 ? { preheader: payload.preheader } : {},
|
|
998
|
+
documentJson: JSON.stringify(readDocument(payload.document)),
|
|
999
|
+
mjml: payload.mjml,
|
|
1000
|
+
updatedAt: at,
|
|
1001
|
+
revision: at
|
|
1002
|
+
});
|
|
1003
|
+
const version = payload.createVersion ? snapshot(row) : null;
|
|
1004
|
+
const { document: _omit, ...template } = toTemplate(row);
|
|
1005
|
+
return { template, warnings: [], version };
|
|
1006
|
+
},
|
|
1007
|
+
async listVersions(id) {
|
|
1008
|
+
rowOf(id);
|
|
1009
|
+
return [...versions.get(String(id)) || []].reverse().map(({ documentJson: _d, mjml: _m, ...v }) => v);
|
|
1010
|
+
},
|
|
1011
|
+
async getVersion(id, versionId) {
|
|
1012
|
+
const entry = (versions.get(String(id)) || []).find((v) => String(v.id) === String(versionId));
|
|
1013
|
+
if (!entry) throw new StorageError("Version not found", 404, "NOT_FOUND");
|
|
1014
|
+
const { documentJson, ...version } = entry;
|
|
1015
|
+
return { ...version, document: readDocument(documentJson) };
|
|
1016
|
+
},
|
|
1017
|
+
async uploadImage(file) {
|
|
1018
|
+
assertUploadable(file);
|
|
1019
|
+
const digest = await crypto.subtle.digest("SHA-256", await file.arrayBuffer());
|
|
1020
|
+
const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1021
|
+
const path = `${assetsPath}${hex.slice(0, 32)}.${IMAGE_TYPES[file.type]}`;
|
|
1022
|
+
images.set(path, file);
|
|
1023
|
+
return { path };
|
|
1024
|
+
},
|
|
1025
|
+
async listVariables() {
|
|
1026
|
+
return [...customVariables.values()].map((v) => ({ ...v }));
|
|
1027
|
+
},
|
|
1028
|
+
async saveVariable(input, { create = false } = {}) {
|
|
1029
|
+
const existing = [...systemVariables, ...customVariables.values()];
|
|
1030
|
+
if (!create && !customVariables.has(String(input?.key))) {
|
|
1031
|
+
throw new StorageError("Variable not found", 404, "NOT_FOUND");
|
|
1032
|
+
}
|
|
1033
|
+
const base = create ? {} : customVariables.get(String(input.key));
|
|
1034
|
+
const { variable, errors } = validateCustomVariable({ ...base, ...input }, { existing, editing: create ? null : input.key });
|
|
1035
|
+
if (errors) throw new StorageError(Object.values(errors)[0], 400, "INVALID_VARIABLE");
|
|
1036
|
+
customVariables.set(variable.key, variable);
|
|
1037
|
+
return { ...variable };
|
|
1038
|
+
},
|
|
1039
|
+
async deleteVariable(key) {
|
|
1040
|
+
if (!customVariables.delete(String(key))) throw new StorageError("Variable not found", 404, "NOT_FOUND");
|
|
1041
|
+
},
|
|
1042
|
+
async loadBrand() {
|
|
1043
|
+
return { ...savedBrand };
|
|
1044
|
+
},
|
|
1045
|
+
async saveBrand(value) {
|
|
1046
|
+
savedBrand = cleanBrand(value);
|
|
1047
|
+
return { ...savedBrand };
|
|
1048
|
+
},
|
|
1049
|
+
...importBrand ? { importBrand } : {},
|
|
1050
|
+
...render ? { render } : {},
|
|
1051
|
+
...sendTest ? { sendTest } : {},
|
|
1052
|
+
...generate ? { generate } : {},
|
|
1053
|
+
...rewrite ? { rewrite } : {},
|
|
1054
|
+
...suggestSubjects ? { suggestSubjects } : {},
|
|
1055
|
+
dump: () => ({
|
|
1056
|
+
rows: [...rows.values()],
|
|
1057
|
+
versions: Object.fromEntries(versions),
|
|
1058
|
+
images: [...images.keys()],
|
|
1059
|
+
variables: [...customVariables.values()],
|
|
1060
|
+
brand: savedBrand
|
|
1061
|
+
})
|
|
1062
|
+
};
|
|
1063
|
+
return storage;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/ui/primitives/fields.jsx
|
|
1067
|
+
import { forwardRef } from "react";
|
|
1068
|
+
import { Label as LabelPrimitive } from "radix-ui";
|
|
1069
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1070
|
+
var Input = forwardRef(function Input2({ className, type, ...props }, ref) {
|
|
1071
|
+
return /* @__PURE__ */ jsx5(
|
|
1072
|
+
"input",
|
|
1073
|
+
{
|
|
1074
|
+
ref,
|
|
1075
|
+
type,
|
|
1076
|
+
"data-slot": "input",
|
|
1077
|
+
className: cn(
|
|
1078
|
+
"eb:h-8 eb:w-full eb:min-w-0 eb:rounded-lg2 eb:border eb:border-border-strong eb:bg-surface-75 eb:px-3 eb:py-1 eb:text-[13px] eb:text-foreground eb:shadow-xs eb:transition-[color,box-shadow] eb:outline-none eb:placeholder:text-foreground-muted eb:selection:bg-primary eb:selection:text-primary-foreground eb:disabled:pointer-events-none eb:disabled:cursor-not-allowed eb:disabled:opacity-50",
|
|
1079
|
+
"eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
|
|
1080
|
+
"eb:aria-invalid:border-destructive eb:aria-invalid:ring-destructive/20",
|
|
1081
|
+
className
|
|
1082
|
+
),
|
|
1083
|
+
...props
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1086
|
+
});
|
|
1087
|
+
var Textarea = forwardRef(function Textarea2({ className, ...props }, ref) {
|
|
1088
|
+
return /* @__PURE__ */ jsx5(
|
|
1089
|
+
"textarea",
|
|
1090
|
+
{
|
|
1091
|
+
ref,
|
|
1092
|
+
"data-slot": "textarea",
|
|
1093
|
+
className: cn(
|
|
1094
|
+
"eb:flex eb:field-sizing-content eb:min-h-16 eb:w-full eb:rounded-lg2 eb:border eb:border-border-strong eb:bg-surface-75 eb:px-3 eb:py-2 eb:text-[13px] eb:text-foreground eb:shadow-xs eb:transition-[color,box-shadow] eb:outline-none eb:placeholder:text-foreground-muted eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50 eb:aria-invalid:border-destructive eb:aria-invalid:ring-destructive/20 eb:disabled:cursor-not-allowed eb:disabled:opacity-50",
|
|
1095
|
+
className
|
|
1096
|
+
),
|
|
1097
|
+
...props
|
|
1098
|
+
}
|
|
1099
|
+
);
|
|
1100
|
+
});
|
|
1101
|
+
function Label({ className, ...props }) {
|
|
1102
|
+
return /* @__PURE__ */ jsx5(
|
|
1103
|
+
LabelPrimitive.Root,
|
|
1104
|
+
{
|
|
1105
|
+
"data-slot": "label",
|
|
1106
|
+
className: cn(
|
|
1107
|
+
"eb:flex eb:items-center eb:gap-2 eb:text-[13px] eb:leading-none eb:font-medium eb:text-foreground eb:select-none eb:peer-disabled:cursor-not-allowed eb:peer-disabled:opacity-50",
|
|
1108
|
+
className
|
|
1109
|
+
),
|
|
1110
|
+
...props
|
|
1111
|
+
}
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/features/preview/PreviewButton.jsx
|
|
1116
|
+
import { useMemo as useMemo2, useState as useState2 } from "react";
|
|
1117
|
+
import { AlertTriangle, CheckCircle2 as CheckCircle22, Eye, Info as Info2, Loader2, Monitor, Moon, RefreshCw, Smartphone, Sun } from "lucide-react";
|
|
1118
|
+
|
|
1119
|
+
// src/core/scrollbars.js
|
|
1120
|
+
var FRAME_SCROLLBAR_CSS = `
|
|
1121
|
+
::-webkit-scrollbar{width:5.6px;height:5.6px;background:transparent}
|
|
1122
|
+
::-webkit-scrollbar-button{display:none;width:0;height:0}
|
|
1123
|
+
::-webkit-scrollbar-track,::-webkit-scrollbar-track-piece,::-webkit-scrollbar-corner{background:transparent;border:0;box-shadow:none}
|
|
1124
|
+
::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45);border:1.4px solid transparent;background-clip:content-box;border-radius:9999px;min-height:2rem;min-width:2rem}
|
|
1125
|
+
::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}
|
|
1126
|
+
@supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.45) transparent}}
|
|
1127
|
+
@media (hover:hover) and (pointer:fine){
|
|
1128
|
+
::-webkit-scrollbar-thumb{background-color:transparent}
|
|
1129
|
+
*:hover::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45)}
|
|
1130
|
+
*:hover::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}
|
|
1131
|
+
@supports not selector(::-webkit-scrollbar){*{scrollbar-color:transparent transparent}*:hover{scrollbar-color:rgba(156,163,175,.45) transparent}}
|
|
1132
|
+
}`;
|
|
1133
|
+
|
|
1134
|
+
// src/features/preview/PreviewButton.jsx
|
|
1135
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1136
|
+
var LEVEL_ICON = { warning: AlertTriangle, info: Info2 };
|
|
1137
|
+
function QualityList({ quality = [], warnings = [] }) {
|
|
1138
|
+
const { t } = useUi();
|
|
1139
|
+
if (!quality.length && !warnings.length) {
|
|
1140
|
+
return /* @__PURE__ */ jsxs4("p", { className: "eb:flex eb:items-center eb:gap-1.5 eb:text-[12px] eb:text-foreground-lighter", children: [
|
|
1141
|
+
/* @__PURE__ */ jsx6(CheckCircle22, { className: "eb:size-3.5 eb:text-success", "aria-hidden": "true" }),
|
|
1142
|
+
t("Nothing to fix.")
|
|
1143
|
+
] });
|
|
1144
|
+
}
|
|
1145
|
+
return /* @__PURE__ */ jsxs4("ul", { className: "eb:space-y-1.5", "aria-label": t("Points to check"), children: [
|
|
1146
|
+
quality.map((issue) => {
|
|
1147
|
+
const Icon = LEVEL_ICON[issue.level] || Info2;
|
|
1148
|
+
return /* @__PURE__ */ jsxs4("li", { className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-foreground", children: [
|
|
1149
|
+
/* @__PURE__ */ jsx6(
|
|
1150
|
+
Icon,
|
|
1151
|
+
{
|
|
1152
|
+
className: cn(
|
|
1153
|
+
"eb:mt-0.5 eb:size-3.5 eb:shrink-0",
|
|
1154
|
+
issue.level === "warning" ? "eb:text-warning" : "eb:text-foreground-muted"
|
|
1155
|
+
),
|
|
1156
|
+
"aria-hidden": "true"
|
|
1157
|
+
}
|
|
1158
|
+
),
|
|
1159
|
+
issue.message
|
|
1160
|
+
] }, issue.code);
|
|
1161
|
+
}),
|
|
1162
|
+
warnings.slice(0, 5).map((w, i) => /* @__PURE__ */ jsxs4("li", { className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-foreground-lighter", children: [
|
|
1163
|
+
/* @__PURE__ */ jsx6(Info2, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted", "aria-hidden": "true" }),
|
|
1164
|
+
"MJML: ",
|
|
1165
|
+
w.message
|
|
1166
|
+
] }, `mjml-${i}`))
|
|
1167
|
+
] });
|
|
1168
|
+
}
|
|
1169
|
+
var PREVIEW_SCROLLBAR_CSS = `<style data-preview-scrollbar>${FRAME_SCROLLBAR_CSS}</style>`;
|
|
1170
|
+
var withPreviewScrollbars = (html) => !html ? "" : /<\/head>/i.test(html) ? html.replace(/<\/head>/i, `${PREVIEW_SCROLLBAR_CSS}</head>`) : PREVIEW_SCROLLBAR_CSS + html;
|
|
1171
|
+
function EmailPreview({ html, loading = false, error = null, device = "desktop", onDeviceChange, onRefresh, hint = true }) {
|
|
1172
|
+
const { t } = useUi();
|
|
1173
|
+
const isMobile = device === "mobile";
|
|
1174
|
+
const [scheme, setScheme] = useState2("light");
|
|
1175
|
+
const dark = scheme === "dark";
|
|
1176
|
+
const srcDoc = useMemo2(() => withPreviewScrollbars(dark ? simulateDarkMode(html) : html), [html, dark]);
|
|
1177
|
+
return /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:h-full eb:min-h-0 eb:flex-col", children: [
|
|
1178
|
+
/* @__PURE__ */ jsxs4("div", { className: "eb:mb-2 eb:flex eb:shrink-0 eb:flex-wrap eb:items-center eb:gap-2", children: [
|
|
1179
|
+
/* @__PURE__ */ jsx6(Tabs, { value: device, onValueChange: (value) => onDeviceChange?.(value), children: /* @__PURE__ */ jsxs4(TabsList, { "aria-label": t("Device"), children: [
|
|
1180
|
+
/* @__PURE__ */ jsxs4(TabsTrigger, { value: "desktop", children: [
|
|
1181
|
+
/* @__PURE__ */ jsx6(Monitor, {}),
|
|
1182
|
+
t("Desktop")
|
|
1183
|
+
] }),
|
|
1184
|
+
/* @__PURE__ */ jsxs4(TabsTrigger, { value: "mobile", children: [
|
|
1185
|
+
/* @__PURE__ */ jsx6(Smartphone, {}),
|
|
1186
|
+
t("Mobile")
|
|
1187
|
+
] })
|
|
1188
|
+
] }) }),
|
|
1189
|
+
/* @__PURE__ */ jsx6(Tabs, { value: scheme, onValueChange: setScheme, children: /* @__PURE__ */ jsxs4(TabsList, { "aria-label": t("Color scheme"), children: [
|
|
1190
|
+
/* @__PURE__ */ jsx6(TabsTrigger, { value: "light", "aria-label": t("Light mode"), title: t("Light mode"), children: /* @__PURE__ */ jsx6(Sun, {}) }),
|
|
1191
|
+
/* @__PURE__ */ jsx6(TabsTrigger, { value: "dark", "aria-label": t("Dark mode"), title: t("Dark mode"), children: /* @__PURE__ */ jsx6(Moon, {}) })
|
|
1192
|
+
] }) }),
|
|
1193
|
+
/* @__PURE__ */ jsx6("span", { className: "eb:flex-1" }),
|
|
1194
|
+
onRefresh && /* @__PURE__ */ jsxs4(Button, { variant: "outline", size: "sm-compact", onClick: onRefresh, disabled: loading, children: [
|
|
1195
|
+
loading ? /* @__PURE__ */ jsx6(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx6(RefreshCw, {}),
|
|
1196
|
+
t("Refresh")
|
|
1197
|
+
] })
|
|
1198
|
+
] }),
|
|
1199
|
+
/* @__PURE__ */ jsx6("div", { className: "eb:min-h-0 eb:flex-1 eb:overflow-auto eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:p-3", children: error ? /* @__PURE__ */ jsx6("div", { className: "eb:flex eb:h-full eb:items-center eb:justify-center eb:p-6 eb:text-center eb:text-[13px] eb:text-destructive", children: error }) : !srcDoc ? /* @__PURE__ */ jsx6("div", { className: "eb:flex eb:h-full eb:items-center eb:justify-center eb:p-6 eb:text-center eb:text-[13px] eb:text-foreground-lighter", children: loading ? t("Rendering\u2026") : t("Nothing to show yet.") }) : (
|
|
1200
|
+
// Narrows and widens like the editor's canvas (width .35s ease).
|
|
1201
|
+
/* @__PURE__ */ jsx6(
|
|
1202
|
+
"div",
|
|
1203
|
+
{
|
|
1204
|
+
className: cn(
|
|
1205
|
+
"eb:mx-auto eb:max-w-full eb:transition-[width,box-shadow] eb:duration-[350ms] eb:ease-in-out eb:motion-reduce:transition-none",
|
|
1206
|
+
isMobile && "eb:shadow-sm"
|
|
1207
|
+
),
|
|
1208
|
+
style: { width: isMobile ? EMAIL_MOBILE_WIDTH : "100%", background: dark ? DARK_PAGE : "#ffffff" },
|
|
1209
|
+
"data-device": device,
|
|
1210
|
+
"data-scheme": scheme,
|
|
1211
|
+
children: /* @__PURE__ */ jsx6(
|
|
1212
|
+
"iframe",
|
|
1213
|
+
{
|
|
1214
|
+
sandbox: "",
|
|
1215
|
+
srcDoc,
|
|
1216
|
+
title: t("Email preview"),
|
|
1217
|
+
className: "eb:block eb:w-full eb:border-0 eb:transition-[height] eb:duration-[350ms] eb:ease-in-out eb:motion-reduce:transition-none",
|
|
1218
|
+
style: { height: isMobile ? 760 : 900, background: dark ? DARK_PAGE : "#ffffff" }
|
|
1219
|
+
}
|
|
1220
|
+
)
|
|
1221
|
+
}
|
|
1222
|
+
)
|
|
1223
|
+
) }),
|
|
1224
|
+
dark ? /* @__PURE__ */ jsx6("p", { className: "eb:mt-2 eb:shrink-0 eb:text-[11px] eb:leading-relaxed eb:text-foreground-lighter", "data-testid": "eb-dark-hint", children: t("Dark mode as Gmail and Outlook apps show it: light backgrounds turn dark, dark text turns light, images stay as they are.") }) : hint && /* @__PURE__ */ jsx6("p", { className: "eb:mt-2 eb:shrink-0 eb:text-[11px] eb:leading-relaxed eb:text-foreground-lighter", children: t("Indicative preview \u2014 check it with \u201CSend test\u201D.") })
|
|
1225
|
+
] });
|
|
1226
|
+
}
|
|
1227
|
+
function PreviewButton({ context }) {
|
|
1228
|
+
const { editor, ready, meta, storage } = context;
|
|
1229
|
+
const { t } = useUi();
|
|
1230
|
+
const [open, setOpen] = useState2(false);
|
|
1231
|
+
const [device, setDevice] = useState2("desktop");
|
|
1232
|
+
const [state, setState] = useState2({ loading: false, html: "", subject: null, preheader: null, warnings: [], quality: [], error: null });
|
|
1233
|
+
const refresh = async () => {
|
|
1234
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1235
|
+
try {
|
|
1236
|
+
const data = await storage.render({ mjml: editor.exportMjml(), subject: meta.subject, preheader: meta.preheader });
|
|
1237
|
+
setState({
|
|
1238
|
+
loading: false,
|
|
1239
|
+
html: data.html,
|
|
1240
|
+
// Subject and preview text with their variables filled, when the backend returns them.
|
|
1241
|
+
subject: data.subject ?? null,
|
|
1242
|
+
preheader: data.preheader ?? null,
|
|
1243
|
+
warnings: data.warnings || [],
|
|
1244
|
+
quality: data.quality || [],
|
|
1245
|
+
error: null
|
|
1246
|
+
});
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
setState({ loading: false, html: "", subject: null, preheader: null, warnings: [], quality: [], error: error.message });
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
1252
|
+
/* @__PURE__ */ jsxs4(
|
|
1253
|
+
Button,
|
|
1254
|
+
{
|
|
1255
|
+
variant: "outline",
|
|
1256
|
+
size: "sm-compact",
|
|
1257
|
+
disabled: !ready,
|
|
1258
|
+
onClick: () => {
|
|
1259
|
+
setOpen(true);
|
|
1260
|
+
refresh();
|
|
1261
|
+
},
|
|
1262
|
+
children: [
|
|
1263
|
+
/* @__PURE__ */ jsx6(Eye, {}),
|
|
1264
|
+
t("Preview")
|
|
1265
|
+
]
|
|
1266
|
+
}
|
|
1267
|
+
),
|
|
1268
|
+
/* @__PURE__ */ jsx6(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs4(DialogContent, { className: "eb:flex eb:h-[85vh] eb:max-h-[85vh] eb:flex-col eb:gap-4 eb:sm:max-w-[67.2rem]", children: [
|
|
1269
|
+
/* @__PURE__ */ jsx6(DialogHeader, { children: /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:items-center eb:gap-3", children: [
|
|
1270
|
+
/* @__PURE__ */ jsx6("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-lg2 eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx6(Eye, { className: "eb:size-5" }) }),
|
|
1271
|
+
/* @__PURE__ */ jsxs4("div", { children: [
|
|
1272
|
+
/* @__PURE__ */ jsx6(DialogTitle, { className: "eb:text-base eb:font-normal", children: t("Email preview") }),
|
|
1273
|
+
/* @__PURE__ */ jsx6(DialogDescription, { className: "eb:text-[13px]", children: t("Final rendering, variables filled with samples.") })
|
|
1274
|
+
] })
|
|
1275
|
+
] }) }),
|
|
1276
|
+
/* @__PURE__ */ jsxs4("div", { className: "eb:grid eb:min-h-0 eb:flex-1 eb:grid-cols-1 eb:gap-4 eb:md:grid-cols-[minmax(0,1fr)_260px]", children: [
|
|
1277
|
+
/* @__PURE__ */ jsx6(
|
|
1278
|
+
EmailPreview,
|
|
1279
|
+
{
|
|
1280
|
+
html: state.html,
|
|
1281
|
+
loading: state.loading,
|
|
1282
|
+
error: state.error,
|
|
1283
|
+
device,
|
|
1284
|
+
onDeviceChange: setDevice,
|
|
1285
|
+
onRefresh: refresh,
|
|
1286
|
+
hint: typeof storage.sendTest === "function"
|
|
1287
|
+
}
|
|
1288
|
+
),
|
|
1289
|
+
/* @__PURE__ */ jsxs4("aside", { className: "eb:min-h-0 eb:space-y-5 eb:overflow-y-auto", "aria-busy": state.loading, children: [
|
|
1290
|
+
/* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2", children: [
|
|
1291
|
+
/* @__PURE__ */ jsx6(SectionLabel, { children: t("Inbox") }),
|
|
1292
|
+
/* @__PURE__ */ jsxs4("div", { className: "eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:px-3 eb:py-2.5", children: [
|
|
1293
|
+
/* @__PURE__ */ jsx6("p", { className: "eb:text-[13px] eb:font-medium eb:text-foreground", children: (state.subject ?? meta.subject) || t("No subject") }),
|
|
1294
|
+
/* @__PURE__ */ jsx6("p", { className: "eb:text-[12px] eb:text-foreground-muted", children: (state.preheader ?? meta.preheader) || t("No preview text") })
|
|
1295
|
+
] })
|
|
1296
|
+
] }),
|
|
1297
|
+
/* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2", children: [
|
|
1298
|
+
/* @__PURE__ */ jsx6(SectionLabel, { children: t("To check") }),
|
|
1299
|
+
!state.loading && !state.error && /* @__PURE__ */ jsx6(QualityList, { quality: state.quality, warnings: state.warnings })
|
|
1300
|
+
] })
|
|
1301
|
+
] })
|
|
1302
|
+
] })
|
|
1303
|
+
] }) })
|
|
1304
|
+
] });
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/features/brand/useBrand.js
|
|
1308
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useState as useState3 } from "react";
|
|
1309
|
+
var entries = /* @__PURE__ */ new WeakMap();
|
|
1310
|
+
function brandOf(storage) {
|
|
1311
|
+
let entry = entries.get(storage);
|
|
1312
|
+
if (entry) return entry;
|
|
1313
|
+
const created = { value: {}, loaded: Promise.resolve({}), listeners: /* @__PURE__ */ new Set() };
|
|
1314
|
+
created.loaded = typeof storage?.loadBrand === "function" ? Promise.resolve().then(() => storage.loadBrand()).then(
|
|
1315
|
+
(brand) => cleanBrand(brand),
|
|
1316
|
+
() => (
|
|
1317
|
+
/** @type {Brand} */
|
|
1318
|
+
{}
|
|
1319
|
+
)
|
|
1320
|
+
).then((brand) => {
|
|
1321
|
+
created.value = brand;
|
|
1322
|
+
created.listeners.forEach((listener) => listener(brand));
|
|
1323
|
+
return brand;
|
|
1324
|
+
}) : created.loaded;
|
|
1325
|
+
if (storage && typeof storage === "object") entries.set(storage, created);
|
|
1326
|
+
return created;
|
|
1327
|
+
}
|
|
1328
|
+
function useBrand(storage) {
|
|
1329
|
+
const [brand, setState] = useState3(() => brandOf(storage).value);
|
|
1330
|
+
useEffect2(() => {
|
|
1331
|
+
const entry = brandOf(storage);
|
|
1332
|
+
setState(entry.value);
|
|
1333
|
+
entry.listeners.add(setState);
|
|
1334
|
+
return () => {
|
|
1335
|
+
entry.listeners.delete(setState);
|
|
1336
|
+
};
|
|
1337
|
+
}, [storage]);
|
|
1338
|
+
const setBrand = useCallback2(
|
|
1339
|
+
(value) => {
|
|
1340
|
+
const entry = brandOf(storage);
|
|
1341
|
+
entry.value = cleanBrand(value);
|
|
1342
|
+
entry.listeners.forEach((listener) => listener(entry.value));
|
|
1343
|
+
},
|
|
1344
|
+
[storage]
|
|
1345
|
+
);
|
|
1346
|
+
return { brand, setBrand };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/ui/useLayoutWidth.js
|
|
1350
|
+
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
1351
|
+
var LAYOUT_BREAKPOINTS = { wide: 1180, medium: 760 };
|
|
1352
|
+
function layoutFor(width) {
|
|
1353
|
+
if (width >= LAYOUT_BREAKPOINTS.wide) return "wide";
|
|
1354
|
+
if (width >= LAYOUT_BREAKPOINTS.medium) return "medium";
|
|
1355
|
+
return "narrow";
|
|
1356
|
+
}
|
|
1357
|
+
function useLayoutWidth(ref) {
|
|
1358
|
+
const [layout, setLayout] = useState4(
|
|
1359
|
+
/** @type {'wide'|'medium'|'narrow'} */
|
|
1360
|
+
"wide"
|
|
1361
|
+
);
|
|
1362
|
+
useEffect3(() => {
|
|
1363
|
+
const el = ref.current;
|
|
1364
|
+
if (!el || typeof ResizeObserver === "undefined") return void 0;
|
|
1365
|
+
const measure = () => setLayout(layoutFor(el.getBoundingClientRect().width));
|
|
1366
|
+
measure();
|
|
1367
|
+
const observer = new ResizeObserver(measure);
|
|
1368
|
+
observer.observe(el);
|
|
1369
|
+
return () => observer.disconnect();
|
|
1370
|
+
}, [ref]);
|
|
1371
|
+
return layout;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
export {
|
|
1375
|
+
socialBlock,
|
|
1376
|
+
socialElementBlock,
|
|
1377
|
+
cn,
|
|
1378
|
+
Button,
|
|
1379
|
+
Separator,
|
|
1380
|
+
Skeleton,
|
|
1381
|
+
SectionLabel,
|
|
1382
|
+
TruncatedText,
|
|
1383
|
+
Tabs,
|
|
1384
|
+
TabsList,
|
|
1385
|
+
TabsTrigger,
|
|
1386
|
+
TooltipWrap,
|
|
1387
|
+
useUi,
|
|
1388
|
+
useT,
|
|
1389
|
+
NoticeArea,
|
|
1390
|
+
UiProvider,
|
|
1391
|
+
Dialog,
|
|
1392
|
+
DialogContent,
|
|
1393
|
+
DialogHeader,
|
|
1394
|
+
DialogTitle,
|
|
1395
|
+
DialogDescription,
|
|
1396
|
+
Popover,
|
|
1397
|
+
PopoverTrigger,
|
|
1398
|
+
PopoverContent,
|
|
1399
|
+
DropdownMenu,
|
|
1400
|
+
DropdownMenuTrigger,
|
|
1401
|
+
DropdownMenuContent,
|
|
1402
|
+
DropdownMenuItem,
|
|
1403
|
+
Select,
|
|
1404
|
+
SelectValue,
|
|
1405
|
+
SelectTrigger,
|
|
1406
|
+
SelectContent,
|
|
1407
|
+
SelectItem,
|
|
1408
|
+
Input,
|
|
1409
|
+
Textarea,
|
|
1410
|
+
Label,
|
|
1411
|
+
FRAME_SCROLLBAR_CSS,
|
|
1412
|
+
EmailPreview,
|
|
1413
|
+
PreviewButton,
|
|
1414
|
+
createStarterMjml,
|
|
1415
|
+
STARTER_MJML,
|
|
1416
|
+
TEMPLATE_GALLERY,
|
|
1417
|
+
buildGallery,
|
|
1418
|
+
withThumbnailImages,
|
|
1419
|
+
withPlaceholderImages,
|
|
1420
|
+
brandOf,
|
|
1421
|
+
useBrand,
|
|
1422
|
+
StorageError,
|
|
1423
|
+
createHttpStorage,
|
|
1424
|
+
createMemoryStorage,
|
|
1425
|
+
useLayoutWidth
|
|
1426
|
+
};
|
|
1427
|
+
//# sourceMappingURL=chunk-AT73Y7JA.js.map
|