create-email-renderer 0.1.1 → 0.2.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/README.md +106 -2
- package/SKILL.md +189 -0
- package/dist/build.d.ts +52 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +169 -0
- package/dist/build.js.map +1 -0
- package/dist/default-blocks.d.ts.map +1 -1
- package/dist/default-blocks.js +357 -5
- package/dist/default-blocks.js.map +1 -1
- package/dist/html-render.d.ts +4 -2
- package/dist/html-render.d.ts.map +1 -1
- package/dist/html-render.js +694 -54
- package/dist/html-render.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/normalize.d.ts +3 -1
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +120 -40
- package/dist/normalize.js.map +1 -1
- package/dist/records.d.ts +8 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +81 -0
- package/dist/records.js.map +1 -0
- package/dist/registry.d.ts +29 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +31 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +4 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/tracking.d.ts +12 -0
- package/dist/tracking.d.ts.map +1 -0
- package/dist/tracking.js +78 -0
- package/dist/tracking.js.map +1 -0
- package/dist/types.d.ts +354 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +82 -0
- package/dist/types.js.map +1 -1
- package/docs/BLOCKS.md +467 -0
- package/docs/MCP.md +186 -0
- package/llms.txt +15 -0
- package/package.json +17 -4
package/dist/types.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { newId } from "./id.js";
|
|
2
|
+
/**
|
|
3
|
+
* Profundidad máxima de anidamiento. 1 = un bloque puede contener hijos, pero
|
|
4
|
+
* esos hijos no pueden volver a contener bloques (sin recursión infinita).
|
|
5
|
+
*/
|
|
6
|
+
export const MAX_BLOCK_DEPTH = 1;
|
|
7
|
+
/** Tipos que pueden contener bloques hijos. */
|
|
8
|
+
export const CONTAINER_BLOCK_TYPES = [
|
|
9
|
+
"container",
|
|
10
|
+
"columns",
|
|
11
|
+
"grid",
|
|
12
|
+
"footer",
|
|
13
|
+
];
|
|
14
|
+
export const isContainerType = (type) => type === "container" ||
|
|
15
|
+
type === "columns" ||
|
|
16
|
+
type === "grid" ||
|
|
17
|
+
type === "footer";
|
|
2
18
|
export const LIST_ICONS = [
|
|
3
19
|
"✓",
|
|
4
20
|
"•",
|
|
@@ -11,6 +27,60 @@ export const LIST_ICONS = [
|
|
|
11
27
|
"»",
|
|
12
28
|
"–",
|
|
13
29
|
];
|
|
30
|
+
/** Glifos sugeridos para el modo logo (editables o sustituibles por una URL). */
|
|
31
|
+
export const SOCIAL_ICONS = [
|
|
32
|
+
"◎",
|
|
33
|
+
"f",
|
|
34
|
+
"X",
|
|
35
|
+
"in",
|
|
36
|
+
"▶",
|
|
37
|
+
"♪",
|
|
38
|
+
"✆",
|
|
39
|
+
"✉",
|
|
40
|
+
"@",
|
|
41
|
+
"★",
|
|
42
|
+
"●",
|
|
43
|
+
"◆",
|
|
44
|
+
];
|
|
45
|
+
/** Fuentes seguras (de sistema) para el correo, agrupadas por estilo. */
|
|
46
|
+
export const FONT_STACKS = [
|
|
47
|
+
{
|
|
48
|
+
value: "system",
|
|
49
|
+
label: "Sistema",
|
|
50
|
+
stack: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: "arial",
|
|
54
|
+
label: "Arial / Helvetica",
|
|
55
|
+
stack: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: "verdana",
|
|
59
|
+
label: "Verdana / Tahoma",
|
|
60
|
+
stack: "Verdana, Tahoma, sans-serif",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: "trebuchet",
|
|
64
|
+
label: "Trebuchet MS",
|
|
65
|
+
stack: "'Trebuchet MS', Verdana, sans-serif",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: "georgia",
|
|
69
|
+
label: "Georgia (serif)",
|
|
70
|
+
stack: "Georgia, 'Times New Roman', serif",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: "times",
|
|
74
|
+
label: "Times New Roman (serif)",
|
|
75
|
+
stack: "'Times New Roman', Times, serif",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
value: "mono",
|
|
79
|
+
label: "Monoespaciada",
|
|
80
|
+
stack: "'Courier New', Courier, monospace",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
export const DEFAULT_FONT_STACK = FONT_STACKS[0].stack;
|
|
14
84
|
export const EMAIL_BLOCK_TYPES = [
|
|
15
85
|
"header",
|
|
16
86
|
"hero",
|
|
@@ -21,9 +91,21 @@ export const EMAIL_BLOCK_TYPES = [
|
|
|
21
91
|
"image",
|
|
22
92
|
"quote",
|
|
23
93
|
"columns",
|
|
94
|
+
"container",
|
|
95
|
+
"grid",
|
|
24
96
|
"divider",
|
|
25
97
|
"spacer",
|
|
26
98
|
"footer",
|
|
99
|
+
"social",
|
|
100
|
+
"gallery",
|
|
101
|
+
"stats",
|
|
102
|
+
"pricing",
|
|
103
|
+
"product",
|
|
104
|
+
"testimonial",
|
|
105
|
+
"features",
|
|
106
|
+
"avatar",
|
|
107
|
+
"code",
|
|
108
|
+
"link",
|
|
27
109
|
];
|
|
28
110
|
export const buildBlockMap = (library) => Object.fromEntries(library.map((def) => [def.type, def]));
|
|
29
111
|
export const createBlock = (type, library) => {
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AA6BhC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AAEjC,+CAA+C;AAC/C,MAAM,CAAC,MAAM,qBAAqB,GAAqB;IACrD,WAAW;IACX,SAAS;IACT,MAAM;IACN,QAAQ;CACT,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAW,EAAE,CACvD,IAAI,KAAK,WAAW;IACpB,IAAI,KAAK,SAAS;IAClB,IAAI,KAAK,MAAM;IACf,IAAI,KAAK,QAAQ,CAAC;AA+EpB,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;CACJ,CAAC;AAyFF,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,GAAG;IACH,GAAG;IACH,GAAG;IACH,IAAI;IACJ,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;CACJ,CAAC;AAqTF,yEAAyE;AACzE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB;QACE,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,SAAS;QAChB,KAAK,EACH,4FAA4F;KAC/F;IACD;QACE,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,gDAAgD;KACxD;IACD;QACE,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,kBAAkB;QACzB,KAAK,EAAE,6BAA6B;KACrC;IACD;QACE,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,qCAAqC;KAC7C;IACD;QACE,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,iBAAiB;QACxB,KAAK,EAAE,mCAAmC;KAC3C;IACD;QACE,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,yBAAyB;QAChC,KAAK,EAAE,iCAAiC;KACzC;IACD;QACE,KAAK,EAAE,MAAM;QACb,KAAK,EAAE,eAAe;QACtB,KAAK,EAAE,mCAAmC;KAC3C;CACO,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AASvD,MAAM,CAAC,MAAM,iBAAiB,GAAqB;IACjD,QAAQ;IACR,MAAM;IACN,SAAS;IACT,MAAM;IACN,MAAM;IACN,QAAQ;IACR,OAAO;IACP,OAAO;IACP,SAAS;IACT,WAAW;IACX,MAAM;IACN,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO;IACP,SAAS;IACT,SAAS;IACT,aAAa;IACb,UAAU;IACV,QAAQ;IACR,MAAM;IACN,MAAM;CACP,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAA0B,EACe,EAAE,CAC3C,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAGvD,CAAC;AAEJ,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,IAAoB,EACpB,OAA0B,EACd,EAAE;IACd,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACnC,OAAO;QACL,EAAE,EAAE,KAAK,EAAE;QACX,IAAI;QACJ,KAAK,EAAE,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE;KACrC,CAAC;AACJ,CAAC,CAAC"}
|
package/docs/BLOCKS.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Catálogo de bloques — `create-email-renderer`
|
|
2
|
+
|
|
3
|
+
Referencia completa de los **25 bloques built-in**: qué hace cada uno, sus props con el default
|
|
4
|
+
real del esquema, sus variantes y un ejemplo copiable con `blockJson`. Si eres una IA leyendo
|
|
5
|
+
esto para generar un correo, empieza por [El payload](#el-payload) y luego baja al bloque que
|
|
6
|
+
necesites.
|
|
7
|
+
|
|
8
|
+
> Los defaults de abajo salen de `DEFAULT_BLOCK_LIBRARY` (`src/default-blocks.ts`), la misma
|
|
9
|
+
> fuente que usa el panel del editor. Lo que no pases queda con su default.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## El payload
|
|
14
|
+
|
|
15
|
+
Lo que se guarda y lo que se renderiza es:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface EmailTemplatePayload {
|
|
19
|
+
content: EmailBlock[]; // los bloques, en orden
|
|
20
|
+
settings: Partial<EmailSettings>; // ajustes de la tarjeta (opcional)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EmailBlock {
|
|
24
|
+
id: string; // generado por `blockJson` / normalizado si falta
|
|
25
|
+
type: EmailBlockType; // uno de los 25 de este documento
|
|
26
|
+
props: Record<string, unknown>; // props del bloque (ver cada sección)
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Para construirlo desde el back-end **no escribas el JSON a mano**: usa `blockJson` y
|
|
31
|
+
`templateJson` (rellenan defaults, coaccionan tipos y generan ids).
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { blockJson, templateJson } from "create-email-renderer";
|
|
35
|
+
import { renderTemplateEmail } from "create-email-renderer/server";
|
|
36
|
+
|
|
37
|
+
const payload = templateJson(
|
|
38
|
+
[
|
|
39
|
+
blockJson("heading", { text: "Hola {firstName}" }),
|
|
40
|
+
blockJson("text", { text: "Tu pedido está listo." }),
|
|
41
|
+
blockJson("checkout", {
|
|
42
|
+
lines: [{ name: "Camiseta", quantity: "1", price: "$20" }],
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
{ cardBorderWidth: 0, fontFamily: "Arial, 'Helvetica Neue', Helvetica, sans-serif" },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const { html, subject } = await renderTemplateEmail({
|
|
49
|
+
subject: "Tu pedido {orderId}",
|
|
50
|
+
payload,
|
|
51
|
+
context: { firstName: "Ana", orderId: "A-123" },
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Props comunes a (casi) todos los bloques
|
|
56
|
+
|
|
57
|
+
Todos heredan `BlockCommonProps`:
|
|
58
|
+
|
|
59
|
+
| Prop | Tipo | Default | Notas |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `align` | `"left" \| "center" \| "right"` | según bloque | Alineación del contenido. |
|
|
62
|
+
| `paddingY` / `paddingX` | `number` (px) | según bloque | Padding vertical / horizontal del bloque. |
|
|
63
|
+
| `paddingTop` / `paddingRight` / `paddingBottom` / `paddingLeft` | `number` (px) | `undefined` | Overrides por lado; **ganan** sobre el shorthand. |
|
|
64
|
+
| `marginTop` / `marginBottom` | `number` (px) | `undefined` | Separación externa (se aplica a la tabla contenedora, no al `<td>`). |
|
|
65
|
+
| `gap` | `number` (px) | según bloque | Separación interna (gutter de columnas, filas de listas, celdas de galería). |
|
|
66
|
+
|
|
67
|
+
Excepciones: `spacer` tiene layout fijo y `footer` lo resuelve por variante.
|
|
68
|
+
|
|
69
|
+
### Variables `{key}`
|
|
70
|
+
|
|
71
|
+
Cualquier campo de texto (y los `src`/`href`) acepta `{clave}` y se resuelve con el `context`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await renderTemplateEmail({
|
|
75
|
+
subject: "Hola {firstName}",
|
|
76
|
+
payload,
|
|
77
|
+
context: { firstName: "Ana", unsubscribeUrl: "https://…/baja" },
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Claves de ejemplo ya presentes en `SAMPLE_CONTEXT`: `name`, `firstName`, `lastName`, `email`,
|
|
82
|
+
`phone`, `role`, `companyName`, `unsubscribeUrl`, `link`, `date`, `year`, `siteUrl`,
|
|
83
|
+
`supportEmail`, `webinarName`, `webinarDate`, `slotsLeft`, `productName`, `amountVes`,
|
|
84
|
+
`paymentUrl`… Una `{clave}` desconocida **se deja tal cual** (no se vacía).
|
|
85
|
+
|
|
86
|
+
### Ajustes (`settings`)
|
|
87
|
+
|
|
88
|
+
| Prop | Tipo | Default | Notas |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `pageBackground` | `string` | `#f5f1e8` | Fondo de la página (fuera de la tarjeta). |
|
|
91
|
+
| `cardBorderWidth` | `number` (px) | `1` | Borde de la tarjeta del correo (0 = sin marco). |
|
|
92
|
+
| `cardBorderRadius` | `number` (px) | `4` | Redondeo de la tarjeta. |
|
|
93
|
+
| `fontFamily` | `string` | stack de sistema | Tipografía del correo; `""` = no emitir (hereda la del cliente). |
|
|
94
|
+
|
|
95
|
+
### Anidamiento
|
|
96
|
+
|
|
97
|
+
`container`, `columns`, `grid` y `footer` son **contenedores**: guardan hijos en
|
|
98
|
+
`blocks: EmailBlock[]` o `columns: ColumnDef[]` (`{ id, blocks, width? }`).
|
|
99
|
+
**Profundidad máxima = 1** (`MAX_BLOCK_DEPTH`): un bloque puede contener hijos, pero esos hijos
|
|
100
|
+
no pueden volver a contener bloques (se descartan al normalizar). Un `footer` **no** se puede
|
|
101
|
+
anidar.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
blockJson("grid", {
|
|
105
|
+
layout: "50-50",
|
|
106
|
+
columns: [
|
|
107
|
+
{ width: 50, blocks: [blockJson("heading", { text: "Izquierda" })] },
|
|
108
|
+
{ width: 50, blocks: [["image", { src: "https://…/foto.jpg" }]] },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 1. `header` — Header / Marca
|
|
116
|
+
|
|
117
|
+
Cabecera con logo o nombre de marca y una línea de apoyo.
|
|
118
|
+
|
|
119
|
+
| Prop | Tipo | Default |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `brandName` | `string` | `"Tu Marca"` |
|
|
122
|
+
| `tagline` | `string` | `"Breve descripción de tu marca"` |
|
|
123
|
+
| `logoUrl` | `string` | `""` (si falta, pinta el `brandName` en texto) |
|
|
124
|
+
| `backgroundColor` | `string` | `#0d0b08` |
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
blockJson("header", {
|
|
128
|
+
brandName: "Tu Marca",
|
|
129
|
+
tagline: "Novedades de la semana",
|
|
130
|
+
logoUrl: "https://…/logo.png",
|
|
131
|
+
backgroundColor: "#0d0b08",
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 2. `hero` — Hero
|
|
136
|
+
|
|
137
|
+
Titular grande + subtítulo (y opcionalmente una imagen), centrado por defecto.
|
|
138
|
+
|
|
139
|
+
| Prop | Tipo | Default |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `title` | `string` | `"¡Un gran titular aquí!"` |
|
|
142
|
+
| `subtitle` | `string` | `"Un subtítulo de apoyo para tu mensaje."` |
|
|
143
|
+
| `imageUrl` | `string` | `""` |
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
blockJson("hero", { title: "Todo lo que llega este mes", subtitle: "Novedades y ofertas" });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 3. `heading` — Título
|
|
150
|
+
|
|
151
|
+
Título de sección.
|
|
152
|
+
|
|
153
|
+
| Prop | Tipo | Default |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `text` | `string` (richText) | `"Título de sección"` |
|
|
156
|
+
| `size` | `number` (px) | `22` |
|
|
157
|
+
| `color` | `string` | `#0d0b08` |
|
|
158
|
+
|
|
159
|
+
## 4. `text` — Texto
|
|
160
|
+
|
|
161
|
+
Párrafo de texto (admite richText: `<strong>`, `<em>`, `<a>`… saneado con `sanitize-html`).
|
|
162
|
+
|
|
163
|
+
| Prop | Tipo | Default |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `text` | `string` (richText) | `"Escribe aquí tu mensaje…"` |
|
|
166
|
+
| `size` | `number` (px) | `15` |
|
|
167
|
+
| `color` | `string` | `#221d15` |
|
|
168
|
+
|
|
169
|
+
## 5. `list` — Lista (3 estilos)
|
|
170
|
+
|
|
171
|
+
| `variant` | Qué pinta |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `bullets` (default) | viñetas con `icon` + `items: string[]` |
|
|
174
|
+
| `numbered` | badge circular numerado + `title` + `description` |
|
|
175
|
+
| `with-image` | imagen 40% + título + descripción + enlace (sin badge) |
|
|
176
|
+
|
|
177
|
+
| Prop | Tipo | Default | Aplica a |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| `items` | `string[]` | 3 beneficios | `bullets` |
|
|
180
|
+
| `icon` | `string` | `"✓"` | `bullets` |
|
|
181
|
+
| `entries` | `ListEntry[]` (`title`, `description`, `number`, `image`, `href`, `linkLabel`) | 3 filas | `numbered`, `with-image` |
|
|
182
|
+
| `accentColor` | `string` | `#d7b227` | badge y enlace |
|
|
183
|
+
| `radius` / `imageHeight` | `number` (px) | `4` / `168` | `with-image` |
|
|
184
|
+
| `gap` | `number` (px) | `8` (bullets) / `24` | separación entre filas |
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
blockJson("list", {
|
|
188
|
+
variant: "numbered",
|
|
189
|
+
items: [],
|
|
190
|
+
entries: [
|
|
191
|
+
{ title: "Envíos en 24 h", description: "Entrega exprés." },
|
|
192
|
+
{ title: "Soporte ampliado", description: "También los domingos." },
|
|
193
|
+
],
|
|
194
|
+
accentColor: "#4f46e5",
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 6. `button` — Botón CTA
|
|
199
|
+
|
|
200
|
+
| Prop | Tipo | Default |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `label` | `string` | `"¡Quiero mi cupo!"` |
|
|
203
|
+
| `href` | `string` | `""` |
|
|
204
|
+
| `backgroundColor` | `string` | `#d7b227` |
|
|
205
|
+
| `color` | `string` | `#0d0b08` |
|
|
206
|
+
| `blockBackgroundColor` | `string` | `undefined` (fondo del bloque) |
|
|
207
|
+
|
|
208
|
+
> Si `href` está vacío el botón se pinta como texto (sin `<a>`), no navega.
|
|
209
|
+
|
|
210
|
+
## 7. `image` — Imagen
|
|
211
|
+
|
|
212
|
+
| Prop | Tipo | Default |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| `src` | `string` | `""` (placeholder "(Selecciona una imagen)") |
|
|
215
|
+
| `alt` | `string` | `"Imagen"` |
|
|
216
|
+
| `width` | `number` (px) | `480` |
|
|
217
|
+
| `href` | `string` | `""` (opcional: envuelve la imagen en un enlace) |
|
|
218
|
+
|
|
219
|
+
## 8. `quote` — Cita / Testimonio
|
|
220
|
+
|
|
221
|
+
| Prop | Tipo | Default |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `text` | `string` (richText) | `"Esto cambió por completo mi manera de pensar."` |
|
|
224
|
+
| `author` | `string` | `"Nombre del participante"` |
|
|
225
|
+
| `borderColor` | `string` | `#d7b227` |
|
|
226
|
+
|
|
227
|
+
## 9. `columns` — Columnas (contenedor)
|
|
228
|
+
|
|
229
|
+
Dos celdas con bloques hijos.
|
|
230
|
+
|
|
231
|
+
| Prop | Tipo | Default |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `columns` | `ColumnDef[]` = `{ id, blocks: EmailBlock[], width? }` | 2 celdas con un `text` |
|
|
234
|
+
| `gap` | `number` (px) | `8` |
|
|
235
|
+
|
|
236
|
+
## 10. `container` — Contenedor (contenedor)
|
|
237
|
+
|
|
238
|
+
Agrupa bloques en una columna (útil para mover un bloque como unidad).
|
|
239
|
+
|
|
240
|
+
| Prop | Tipo | Default |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `blocks` | `EmailBlock[]` | `[]` |
|
|
243
|
+
|
|
244
|
+
## 11. `grid` — Cuadrícula (contenedor)
|
|
245
|
+
|
|
246
|
+
Una **fila** de celdas con anchos configurables.
|
|
247
|
+
|
|
248
|
+
| Prop | Tipo | Default |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `columns` | `ColumnDef[]` con `width` en % | 2 celdas |
|
|
251
|
+
| `layout` | `string` (preset: `1-2`, `1-3-2-3`, `2-3-1-3`, `1-3-1-3-1-3`, `1-4-1-4-1-4-1-4`) | `"1-2"` |
|
|
252
|
+
| `gap` | `number` (px) | `12` |
|
|
253
|
+
|
|
254
|
+
## 12. `divider` — Divisor
|
|
255
|
+
|
|
256
|
+
| Prop | Tipo | Default |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `color` | `string` | `#e3dccb` |
|
|
259
|
+
|
|
260
|
+
## 13. `spacer` — Espaciador
|
|
261
|
+
|
|
262
|
+
| Prop | Tipo | Default |
|
|
263
|
+
|---|---|---|
|
|
264
|
+
| `height` | `number` (px) | `24` |
|
|
265
|
+
|
|
266
|
+
## 14. `footer` — Footer (3 estilos, contenedor)
|
|
267
|
+
|
|
268
|
+
| `variant` | Qué pinta |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `classic` (default) | barra oscura con `text` + `brandName` |
|
|
271
|
+
| `one-column` | una celda con bloques hijos (`columns[0]`) |
|
|
272
|
+
| `two-columns` | dos celdas con bloques hijos |
|
|
273
|
+
|
|
274
|
+
| Prop | Tipo | Default | Aplica a |
|
|
275
|
+
|---|---|---|---|
|
|
276
|
+
| `text` | `string` | `"Recibes este correo por estar registrado…"` | `classic` |
|
|
277
|
+
| `brandName` | `string` | `"Tu Marca"` | `classic` |
|
|
278
|
+
| `columns` | `ColumnDef[]` | `[]` | `one-column`, `two-columns` |
|
|
279
|
+
| `backgroundColor` | `string` | `#0d0b08` | todas |
|
|
280
|
+
| `gap` | `number` (px) | `12` | columnas |
|
|
281
|
+
|
|
282
|
+
> No puede anidarse dentro de otro contenedor.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
blockJson("footer", {
|
|
286
|
+
variant: "one-column",
|
|
287
|
+
columns: [{ blocks: [blockJson("text", { text: "Recibes este correo por…" })] }],
|
|
288
|
+
backgroundColor: "#f9fafb",
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 15. `social` — Redes sociales (2 modos)
|
|
293
|
+
|
|
294
|
+
| Prop | Tipo | Default |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `mode` | `"text" \| "logo"` | `"text"` |
|
|
297
|
+
| `links` | `{ label, href, icon }[]` | 3 redes |
|
|
298
|
+
| `iconSize` | `number` (px) | `32` |
|
|
299
|
+
| `gap` | `number` (px) | `6` |
|
|
300
|
+
| `color` | `string` | `#221d15` (texto en modo `text`) |
|
|
301
|
+
| `accentColor` | `string` | `#d7b227` (medallón en modo `logo`) |
|
|
302
|
+
|
|
303
|
+
En modo `logo`, `icon` acepta un glifo (`SOCIAL_ICONS`) **o** una URL de imagen (`https:` o `data:`).
|
|
304
|
+
|
|
305
|
+
## 16. `gallery` — Galería (4 diseños)
|
|
306
|
+
|
|
307
|
+
| `variant` | Qué pinta |
|
|
308
|
+
|---|---|
|
|
309
|
+
| `grid` (default) | cuadrícula de 2/3 columnas (`columns`) |
|
|
310
|
+
| `three-columns` | una fila de tres |
|
|
311
|
+
| `horizontal` | dos apiladas + una alta (cubre las dos + gutter) |
|
|
312
|
+
| `vertical` | una ancha arriba + dos abajo |
|
|
313
|
+
|
|
314
|
+
| Prop | Tipo | Default | Notas |
|
|
315
|
+
|---|---|---|---|
|
|
316
|
+
| `images` | `{ src, alt, href }[]` | 2 | |
|
|
317
|
+
| `columns` | `number` (2 \| 3) | `2` | solo `grid` |
|
|
318
|
+
| `radius` | `number` (px) | `6` | |
|
|
319
|
+
| `imageHeight` | `number` (px) | `0` = alto automático | con alto fijo añade `object-fit:cover` |
|
|
320
|
+
| `gap` | `number` (px) | `12` | gutter entre celdas |
|
|
321
|
+
|
|
322
|
+
> `object-fit:cover` no lo soporta Outlook escritorio: ahí las imágenes se estiran. Con
|
|
323
|
+
> `imageHeight: 0` el correo es "a prueba de balas".
|
|
324
|
+
|
|
325
|
+
## 17. `stats` — Estadísticas
|
|
326
|
+
|
|
327
|
+
| Prop | Tipo | Default |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `stats` | `{ value, label }[]` | 3 cifras |
|
|
330
|
+
| `accentColor` | `string` | `#d7b227` |
|
|
331
|
+
|
|
332
|
+
## 18. `pricing` — Precio / Plan (3 estilos)
|
|
333
|
+
|
|
334
|
+
| `variant` | Qué pinta | Props que usa |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| `card` (default) | tarjeta de un plan | `title`, `price`, `period`, `bullets`, `ctaLabel`, `ctaHref` |
|
|
337
|
+
| `offer` | tabla de oferta con nota | + `eyebrow`, `description`, `note`, `note2` (botón full-width) |
|
|
338
|
+
| `two-tiers` | 2+ planes con uno destacado | `heading`, `subtitle`, `plans[]`, `footnote` |
|
|
339
|
+
|
|
340
|
+
`PricingPlan` = `{ title, price, period, description, features: string[], ctaLabel, ctaHref, highlighted }`.
|
|
341
|
+
|
|
342
|
+
| Prop | Tipo | Default |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| `plans` | `PricingPlan[]` | `[]` |
|
|
345
|
+
| `accentColor` | `string` | `#d7b227` |
|
|
346
|
+
| `textColor` | `string` | `#221d15` |
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
blockJson("pricing", {
|
|
350
|
+
variant: "two-tiers",
|
|
351
|
+
heading: "Elige tu plan",
|
|
352
|
+
subtitle: "Sin permanencia.",
|
|
353
|
+
plans: [
|
|
354
|
+
{ title: "Hobby", price: "$29", period: "/ mes", features: ["25 productos"], ctaLabel: "Empezar" },
|
|
355
|
+
{ title: "Enterprise", price: "$99", period: "/ mes", features: ["Todo incluido"], ctaLabel: "Empezar", highlighted: true },
|
|
356
|
+
],
|
|
357
|
+
footnote: "Precios sin impuestos.",
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## 19. `product` — Producto (4 estilos)
|
|
362
|
+
|
|
363
|
+
| `variant` | Qué pinta |
|
|
364
|
+
|---|---|
|
|
365
|
+
| `card` (default) | imagen centrada + nombre + descripción + precio + CTA |
|
|
366
|
+
| `hero` | imagen ancha arriba (320) + `eyebrow` + título 36px + … |
|
|
367
|
+
| `image-left` | tabla 50/50: imagen izquierda, ficha derecha (botón 75%) |
|
|
368
|
+
| `grid` | encabezado opcional + tarjetas de `products[]` (`columns` 2/3/4; con 4 → 2×2 + `Hr`) |
|
|
369
|
+
|
|
370
|
+
| Prop | Tipo | Default | Notas |
|
|
371
|
+
|---|---|---|---|
|
|
372
|
+
| `imageUrl`, `name`, `description`, `price` | `string` | ver esquema | variantes de un producto |
|
|
373
|
+
| `eyebrow` | `string` | `""` | `hero` |
|
|
374
|
+
| `heading` / `subheading` | `string` | `""` | `grid` |
|
|
375
|
+
| `products` | `ProductItem[]` (`imageUrl`, `name`, `description`, `price`, `ctaLabel`, `ctaHref`) | 2 tarjetas | `grid` |
|
|
376
|
+
| `columns` | `number` (2 \| 3 \| 4) | `2` | `grid` |
|
|
377
|
+
| `radius` | `number` (px) | `8` (hero 12) | |
|
|
378
|
+
| `imageHeight` | `number` (px) | `0` = default del layout (hero 320 · grid 180/250) | |
|
|
379
|
+
| `accentColor` | `string` | `#d7b227` | eyebrow, precio y botón |
|
|
380
|
+
|
|
381
|
+
## 20. `checkout` — Resumen de pedido
|
|
382
|
+
|
|
383
|
+
Carrito con líneas de producto y botón de compra full-width.
|
|
384
|
+
|
|
385
|
+
| Prop | Tipo | Default |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| `heading` | `string` | `"Tu carrito te espera"` |
|
|
388
|
+
| `lines` | `CheckoutLine[]` = `{ imageUrl, name, quantity, price }` | 2 líneas |
|
|
389
|
+
| `ctaLabel` / `ctaHref` | `string` | `"Finalizar compra"` / `""` |
|
|
390
|
+
| `imageHeight` | `number` (px) | `110` (miniatura cuadrada) |
|
|
391
|
+
| `radius` | `number` (px) | `8` |
|
|
392
|
+
| `accentColor` | `string` | `#4f46e5` (botón) |
|
|
393
|
+
| `borderColor` | `string` | `#e5e7eb` (caja y líneas de la tabla) |
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
blockJson("checkout", {
|
|
397
|
+
heading: "Tu carrito te espera",
|
|
398
|
+
lines: [
|
|
399
|
+
{ name: "Reloj clásico", imageUrl: "https://…/reloj.jpg", quantity: "1", price: "$210.00" },
|
|
400
|
+
{ name: "Reloj de pared", quantity: "2", price: "$90.00" },
|
|
401
|
+
],
|
|
402
|
+
ctaLabel: "Volver a mi carrito",
|
|
403
|
+
ctaHref: "https://tienda.com/carrito",
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## 21. `testimonial` — Testimonio
|
|
408
|
+
|
|
409
|
+
| Prop | Tipo | Default |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| `avatarUrl` | `string` | `""` |
|
|
412
|
+
| `quote` | `string` (richText) | `"Cambió por completo la forma en que trabajamos."` |
|
|
413
|
+
| `name` / `role` | `string` | `"Nombre Apellido"` / `"Cargo en la empresa"` |
|
|
414
|
+
| `accentColor` | `string` | `#d7b227` |
|
|
415
|
+
|
|
416
|
+
## 22. `features` — Características
|
|
417
|
+
|
|
418
|
+
| Prop | Tipo | Default |
|
|
419
|
+
|---|---|---|
|
|
420
|
+
| `features` | `{ icon, title, description }[]` | 2 filas |
|
|
421
|
+
| `columns` | `number` (2 \| 3) | `2` |
|
|
422
|
+
| `accentColor` | `string` | `#d7b227` (ícono) |
|
|
423
|
+
|
|
424
|
+
## 23. `avatar` — Avatar
|
|
425
|
+
|
|
426
|
+
| Prop | Tipo | Default |
|
|
427
|
+
|---|---|---|
|
|
428
|
+
| `imageUrl` | `string` | `""` |
|
|
429
|
+
| `name` / `role` | `string` | `"Nombre Apellido"` / `"Cargo"` |
|
|
430
|
+
| `size` | `number` (px) | `96` |
|
|
431
|
+
|
|
432
|
+
## 24. `code` — Código
|
|
433
|
+
|
|
434
|
+
| Prop | Tipo | Default |
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| `code` | `string` | `"npm install create-email-template"` |
|
|
437
|
+
| `language` | `string` | `"bash"` |
|
|
438
|
+
| `backgroundColor` / `color` | `string` | `#0d0b08` / `#f5f1e8` |
|
|
439
|
+
|
|
440
|
+
Usa un stack monoespaciado propio (gana sobre el `fontFamily` del correo).
|
|
441
|
+
|
|
442
|
+
## 25. `link` — Enlace
|
|
443
|
+
|
|
444
|
+
| Prop | Tipo | Default |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| `label` | `string` | `"Ver más"` |
|
|
447
|
+
| `href` | `string` | `""` |
|
|
448
|
+
| `color` | `string` | `#a98a1e` |
|
|
449
|
+
|
|
450
|
+
> Sin `href` se pinta como texto (sin `<a>`).
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Reglas prácticas al generar correos
|
|
455
|
+
|
|
456
|
+
1. **Usa `blockJson` / `templateJson`**, no JSON a mano: los defaults y los ids salen solos.
|
|
457
|
+
2. Los arrays de registros se llaman `images`, `links`, `stats`, `features`, `entries`, `plans`,
|
|
458
|
+
`products` y `lines`. **Nunca `items`** para registros: `items` es la lista de strings de
|
|
459
|
+
`list`.
|
|
460
|
+
3. **Imágenes**: mejor URLs `https:` propias. Las `data:` (SVG inline) las descarta Gmail.
|
|
461
|
+
4. **Alto fijo + `object-fit`** se ve bien en Gmail/Apple Mail/Outlook web, pero **no** en Outlook
|
|
462
|
+
escritorio (se estira). Para máxima compatibilidad: `imageHeight: 0`.
|
|
463
|
+
5. **Enlaces**: si dejas `href: ""` el CTA se pinta como texto; nunca genera `href=""`.
|
|
464
|
+
6. **Un solo nivel de anidamiento** y un `footer` por correo.
|
|
465
|
+
7. **Peso**: mantén el HTML por debajo de ~102 KB o Gmail lo recortará ("Ver todo el mensaje").
|
|
466
|
+
8. **Variables**: úsalas en textos y enlaces; no inventes claves fuera del `context` (se quedan
|
|
467
|
+
visibles como `{clave}`).
|