@ideare/kit 0.0.0-stage → 0.1.0-next.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/LICENSE +21 -0
- package/README.md +148 -2
- package/THIRD_PARTY.md +52 -0
- package/dist/brand/schema.d.ts +265 -0
- package/dist/brand/schema.js +271 -0
- package/dist/check/copy.d.ts +27 -0
- package/dist/check/copy.js +139 -0
- package/dist/check/index.d.ts +54 -0
- package/dist/check/index.js +492 -0
- package/dist/color/contrast.d.ts +41 -0
- package/dist/color/contrast.js +84 -0
- package/dist/color/palette.d.ts +27 -0
- package/dist/color/palette.js +114 -0
- package/dist/color/wcag.d.ts +22 -0
- package/dist/color/wcag.js +50 -0
- package/dist/draft/draft.d.ts +61 -0
- package/dist/draft/draft.js +383 -0
- package/dist/editor/history.d.ts +78 -0
- package/dist/editor/history.js +221 -0
- package/dist/editor/operations.d.ts +112 -0
- package/dist/editor/operations.js +621 -0
- package/dist/files/docs.d.ts +49 -0
- package/dist/files/docs.js +392 -0
- package/dist/files/generate.d.ts +47 -0
- package/dist/files/generate.js +254 -0
- package/dist/files/png.d.ts +40 -0
- package/dist/files/png.js +168 -0
- package/dist/files/raster.d.ts +14 -0
- package/dist/files/raster.js +0 -0
- package/dist/files/templates.d.ts +162 -0
- package/dist/files/templates.js +390 -0
- package/dist/files/tokens.d.ts +22 -0
- package/dist/files/tokens.js +204 -0
- package/dist/fonts/catalog.d.ts +3 -0
- package/dist/fonts/catalog.js +345 -0
- package/dist/fonts/ofl.d.ts +5 -0
- package/dist/fonts/ofl.js +5 -0
- package/dist/fonts/pick.d.ts +33 -0
- package/dist/fonts/pick.js +73 -0
- package/dist/imports/folder.d.ts +80 -0
- package/dist/imports/folder.js +124 -0
- package/dist/imports/unzip.d.ts +32 -0
- package/dist/imports/unzip.js +128 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +34 -0
- package/dist/marks/draw.d.ts +48 -0
- package/dist/marks/draw.js +149 -0
- package/dist/marks/icons.data.d.ts +5 -0
- package/dist/marks/icons.data.js +600 -0
- package/dist/marks/svg.d.ts +46 -0
- package/dist/marks/svg.js +341 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +41 -0
- package/dist/posts/pix.d.ts +19 -0
- package/dist/posts/pix.js +49 -0
- package/dist/posts/templates.d.ts +45 -0
- package/dist/posts/templates.js +868 -0
- package/dist/resvg/index.d.ts +2 -0
- package/dist/resvg/index.js +22 -0
- package/dist/scene/brand-art.d.ts +49 -0
- package/dist/scene/brand-art.js +154 -0
- package/dist/scene/frame.d.ts +89 -0
- package/dist/scene/frame.js +213 -0
- package/dist/scene/grain.d.ts +7 -0
- package/dist/scene/grain.js +7 -0
- package/dist/scene/qr.d.ts +10 -0
- package/dist/scene/qr.js +359 -0
- package/dist/scene/render.d.ts +76 -0
- package/dist/scene/render.js +557 -0
- package/dist/scene/schema.d.ts +271 -0
- package/dist/scene/schema.js +257 -0
- package/dist/sign.d.ts +47 -0
- package/dist/sign.js +88 -0
- package/dist/text/engine.d.ts +128 -0
- package/dist/text/engine.js +245 -0
- package/dist/text/fonts.d.ts +56 -0
- package/dist/text/fonts.js +65 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/wasm/index.d.ts +15 -0
- package/dist/wasm/index.js +46 -0
- package/package.json +78 -5
- package/schema/brand.v1.json +442 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { FORMATS } from "../scene/schema.js";
|
|
2
|
+
import { pixCode } from "./pix.js";
|
|
3
|
+
/**
|
|
4
|
+
* The launch templates: a starting design for each job an owner posts for, in every size they
|
|
5
|
+
* post at. Each is plain scene data the editor opens and the API draws. Paints are brand tokens
|
|
6
|
+
* only, in the pairs every brand's palette keeps readable (ink and muted on ground and surface,
|
|
7
|
+
* onPrimary on primary, primary as large type on ground), and type uses the brand's two font
|
|
8
|
+
* roles, so a template looks like the brand it is made for. Photo spots are art layers named
|
|
9
|
+
* `photo`, which the editor fills with an upload or AI art; text the owner changes is a slot,
|
|
10
|
+
* and slots marked live can be filled by a live image link (`t.<slot>`).
|
|
11
|
+
*/
|
|
12
|
+
export const POST_TEMPLATES = [
|
|
13
|
+
"promotion",
|
|
14
|
+
"hours",
|
|
15
|
+
"menu",
|
|
16
|
+
"pix",
|
|
17
|
+
"new-product",
|
|
18
|
+
"status-offer",
|
|
19
|
+
"carousel",
|
|
20
|
+
];
|
|
21
|
+
export const POST_FORMATS = [
|
|
22
|
+
"ig-post",
|
|
23
|
+
"ig-square",
|
|
24
|
+
"ig-story",
|
|
25
|
+
"wa-status",
|
|
26
|
+
"share-card",
|
|
27
|
+
];
|
|
28
|
+
/** What the template picker shows, and the size each template starts at. */
|
|
29
|
+
export const POST_TEMPLATE_INFO = {
|
|
30
|
+
promotion: {
|
|
31
|
+
format: "ig-post",
|
|
32
|
+
text: {
|
|
33
|
+
"pt-BR": { name: "Promoção", description: "Um desconto ou uma oferta, com foto." },
|
|
34
|
+
en: { name: "Promotion", description: "A discount or a deal, with a photo." },
|
|
35
|
+
es: { name: "Promoción", description: "Un descuento o una oferta, con foto." },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
hours: {
|
|
39
|
+
format: "ig-post",
|
|
40
|
+
text: {
|
|
41
|
+
"pt-BR": { name: "Horário", description: "Os dias e os horários em que você abre." },
|
|
42
|
+
en: { name: "Opening hours", description: "The days and times you're open." },
|
|
43
|
+
es: { name: "Horario", description: "Los días y las horas en que abres." },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
menu: {
|
|
47
|
+
format: "ig-post",
|
|
48
|
+
text: {
|
|
49
|
+
"pt-BR": {
|
|
50
|
+
name: "Cardápio ou preços",
|
|
51
|
+
description: "Seus itens e preços, em uma lista.",
|
|
52
|
+
},
|
|
53
|
+
en: { name: "Menu or price list", description: "Your items and prices, in one list." },
|
|
54
|
+
es: { name: "Menú o precios", description: "Tus productos y precios, en una lista." },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
pix: {
|
|
58
|
+
format: "ig-post",
|
|
59
|
+
text: {
|
|
60
|
+
"pt-BR": { name: "Cartão de Pix", description: "Um QR code para pagarem com Pix." },
|
|
61
|
+
en: { name: "Pix card", description: "A QR code people scan to pay you with Pix." },
|
|
62
|
+
es: { name: "Tarjeta de Pix", description: "Un código QR para que te paguen con Pix." },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"new-product": {
|
|
66
|
+
format: "ig-post",
|
|
67
|
+
text: {
|
|
68
|
+
"pt-BR": { name: "Novidade", description: "Um produto novo, com foto e preço." },
|
|
69
|
+
en: { name: "New product", description: "Something new, with a photo and a price." },
|
|
70
|
+
es: { name: "Novedad", description: "Un producto nuevo, con foto y precio." },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"status-offer": {
|
|
74
|
+
format: "wa-status",
|
|
75
|
+
text: {
|
|
76
|
+
"pt-BR": {
|
|
77
|
+
name: "Oferta no status",
|
|
78
|
+
description: "Uma oferta para o status do WhatsApp.",
|
|
79
|
+
},
|
|
80
|
+
en: { name: "Status offer", description: "A deal for your WhatsApp status." },
|
|
81
|
+
es: {
|
|
82
|
+
name: "Oferta en estados",
|
|
83
|
+
description: "Una oferta para tu estado de WhatsApp.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
carousel: {
|
|
88
|
+
format: "ig-post",
|
|
89
|
+
text: {
|
|
90
|
+
"pt-BR": { name: "Carrossel", description: "Cinco páginas para arrastar para o lado." },
|
|
91
|
+
en: { name: "Carousel", description: "Five pages people swipe through." },
|
|
92
|
+
es: { name: "Carrusel", description: "Cinco páginas para deslizar." },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/** The sample words in each language. Owners replace them; they only show the layout. */
|
|
97
|
+
const COPY = {
|
|
98
|
+
"pt-BR": {
|
|
99
|
+
promotion: {
|
|
100
|
+
badge: "-20%",
|
|
101
|
+
title: "Promoção da semana",
|
|
102
|
+
detail: "20% de desconto em tudo, até sábado.",
|
|
103
|
+
cta: "Chame no WhatsApp",
|
|
104
|
+
prompt: "O produto em oferta, em um fundo simples",
|
|
105
|
+
},
|
|
106
|
+
hours: {
|
|
107
|
+
title: "Nosso horário",
|
|
108
|
+
days: ["Segunda a sexta", "Sábado", "Domingo"],
|
|
109
|
+
times: ["7h às 19h", "7h às 13h", "Fechado"],
|
|
110
|
+
},
|
|
111
|
+
menu: {
|
|
112
|
+
title: "Cardápio do dia",
|
|
113
|
+
items: ["Pão de queijo", "Café coado", "Bolo do dia", "Suco natural"],
|
|
114
|
+
prices: ["R$ 6,00", "R$ 5,00", "R$ 8,00", "R$ 9,00"],
|
|
115
|
+
cta: "Peça pelo WhatsApp",
|
|
116
|
+
},
|
|
117
|
+
pix: {
|
|
118
|
+
title: "Pague com Pix",
|
|
119
|
+
detail: "Abra o app do seu banco e leia o código.",
|
|
120
|
+
key: "Chave Pix",
|
|
121
|
+
},
|
|
122
|
+
newProduct: {
|
|
123
|
+
tag: "Novidade",
|
|
124
|
+
title: "Bolo de cenoura",
|
|
125
|
+
detail: "Feito todo dia, com a receita da casa.",
|
|
126
|
+
price: "R$ 12,00",
|
|
127
|
+
prompt: "O produto novo de perto, com luz natural",
|
|
128
|
+
},
|
|
129
|
+
statusOffer: {
|
|
130
|
+
eyebrow: "Só hoje",
|
|
131
|
+
title: "Leve 3, pague 2",
|
|
132
|
+
detail: "Válido até as 18h.",
|
|
133
|
+
cta: "Responda este status para pedir",
|
|
134
|
+
},
|
|
135
|
+
carousel: {
|
|
136
|
+
title: "3 motivos para nos visitar",
|
|
137
|
+
hint: "Arraste para o lado",
|
|
138
|
+
tips: [
|
|
139
|
+
{ title: "Feito com cuidado", detail: "Cada detalhe é pensado para você." },
|
|
140
|
+
{
|
|
141
|
+
title: "Atendimento de verdade",
|
|
142
|
+
detail: "A gente conhece cada cliente pelo nome.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
title: "Perto de você",
|
|
146
|
+
detail: "Fácil de chegar, e também atendemos pelo WhatsApp.",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
end: "Venha nos ver",
|
|
150
|
+
cta: "Chame no WhatsApp",
|
|
151
|
+
},
|
|
152
|
+
slots: {
|
|
153
|
+
title: "Título",
|
|
154
|
+
detail: "Detalhe",
|
|
155
|
+
badge: "Desconto",
|
|
156
|
+
cta: "Como pedir",
|
|
157
|
+
contact: "Contato",
|
|
158
|
+
photo: "Foto",
|
|
159
|
+
days: "Dias",
|
|
160
|
+
times: "Horários",
|
|
161
|
+
items: "Itens",
|
|
162
|
+
prices: "Preços",
|
|
163
|
+
tag: "Etiqueta",
|
|
164
|
+
name: "Nome do produto",
|
|
165
|
+
price: "Preço",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
en: {
|
|
169
|
+
promotion: {
|
|
170
|
+
badge: "-20%",
|
|
171
|
+
title: "This week's deal",
|
|
172
|
+
detail: "20% off everything, until Saturday.",
|
|
173
|
+
cta: "Message us on WhatsApp",
|
|
174
|
+
prompt: "The product on sale, on a simple background",
|
|
175
|
+
},
|
|
176
|
+
hours: {
|
|
177
|
+
title: "Opening hours",
|
|
178
|
+
days: ["Monday to Friday", "Saturday", "Sunday"],
|
|
179
|
+
times: ["7 am to 7 pm", "7 am to 1 pm", "Closed"],
|
|
180
|
+
},
|
|
181
|
+
menu: {
|
|
182
|
+
title: "Today's menu",
|
|
183
|
+
items: ["Cheese bread", "Drip coffee", "Cake of the day", "Fresh juice"],
|
|
184
|
+
prices: ["$4.00", "$3.50", "$5.00", "$6.00"],
|
|
185
|
+
cta: "Order on WhatsApp",
|
|
186
|
+
},
|
|
187
|
+
pix: {
|
|
188
|
+
title: "Pay with Pix",
|
|
189
|
+
detail: "Open your bank app and scan the code.",
|
|
190
|
+
key: "Pix key",
|
|
191
|
+
},
|
|
192
|
+
newProduct: {
|
|
193
|
+
tag: "New",
|
|
194
|
+
title: "Carrot cake",
|
|
195
|
+
detail: "Made fresh every day, from our own recipe.",
|
|
196
|
+
price: "$9.00",
|
|
197
|
+
prompt: "The new product up close, in natural light",
|
|
198
|
+
},
|
|
199
|
+
statusOffer: {
|
|
200
|
+
eyebrow: "Today only",
|
|
201
|
+
title: "Buy 2, get 1 free",
|
|
202
|
+
detail: "Until 6 pm.",
|
|
203
|
+
cta: "Reply to this status to order",
|
|
204
|
+
},
|
|
205
|
+
carousel: {
|
|
206
|
+
title: "3 reasons to visit us",
|
|
207
|
+
hint: "Swipe to see more",
|
|
208
|
+
tips: [
|
|
209
|
+
{ title: "Made with care", detail: "We think through every detail for you." },
|
|
210
|
+
{ title: "Real service", detail: "We know our customers by name." },
|
|
211
|
+
{ title: "Close to you", detail: "Easy to get to, and we answer on WhatsApp too." },
|
|
212
|
+
],
|
|
213
|
+
end: "Come and see us",
|
|
214
|
+
cta: "Message us on WhatsApp",
|
|
215
|
+
},
|
|
216
|
+
slots: {
|
|
217
|
+
title: "Title",
|
|
218
|
+
detail: "Details",
|
|
219
|
+
badge: "Discount",
|
|
220
|
+
cta: "How to order",
|
|
221
|
+
contact: "Contact",
|
|
222
|
+
photo: "Photo",
|
|
223
|
+
days: "Days",
|
|
224
|
+
times: "Hours",
|
|
225
|
+
items: "Items",
|
|
226
|
+
prices: "Prices",
|
|
227
|
+
tag: "Label",
|
|
228
|
+
name: "Product name",
|
|
229
|
+
price: "Price",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
es: {
|
|
233
|
+
promotion: {
|
|
234
|
+
badge: "-20%",
|
|
235
|
+
title: "Oferta de la semana",
|
|
236
|
+
detail: "20% de descuento en todo, hasta el sábado.",
|
|
237
|
+
cta: "Escríbenos por WhatsApp",
|
|
238
|
+
prompt: "El producto en oferta, sobre un fondo simple",
|
|
239
|
+
},
|
|
240
|
+
hours: {
|
|
241
|
+
title: "Nuestro horario",
|
|
242
|
+
days: ["Lunes a viernes", "Sábado", "Domingo"],
|
|
243
|
+
times: ["7:00 a 19:00", "7:00 a 13:00", "Cerrado"],
|
|
244
|
+
},
|
|
245
|
+
menu: {
|
|
246
|
+
title: "Menú del día",
|
|
247
|
+
items: ["Pan de queso", "Café de filtro", "Postre del día", "Jugo natural"],
|
|
248
|
+
prices: ["$60", "$45", "$70", "$80"],
|
|
249
|
+
cta: "Pide por WhatsApp",
|
|
250
|
+
},
|
|
251
|
+
pix: {
|
|
252
|
+
title: "Paga con Pix",
|
|
253
|
+
detail: "Abre la app de tu banco y escanea el código.",
|
|
254
|
+
key: "Clave Pix",
|
|
255
|
+
},
|
|
256
|
+
newProduct: {
|
|
257
|
+
tag: "Novedad",
|
|
258
|
+
title: "Pastel de zanahoria",
|
|
259
|
+
detail: "Hecho todos los días, con la receta de la casa.",
|
|
260
|
+
price: "$90",
|
|
261
|
+
prompt: "El producto nuevo de cerca, con luz natural",
|
|
262
|
+
},
|
|
263
|
+
statusOffer: {
|
|
264
|
+
eyebrow: "Solo hoy",
|
|
265
|
+
title: "Lleva 3, paga 2",
|
|
266
|
+
detail: "Hasta las 18:00.",
|
|
267
|
+
cta: "Responde este estado para pedir",
|
|
268
|
+
},
|
|
269
|
+
carousel: {
|
|
270
|
+
title: "3 razones para visitarnos",
|
|
271
|
+
hint: "Desliza para ver más",
|
|
272
|
+
tips: [
|
|
273
|
+
{ title: "Hecho con cuidado", detail: "Pensamos cada detalle para ti." },
|
|
274
|
+
{ title: "Atención de verdad", detail: "Conocemos a cada cliente por su nombre." },
|
|
275
|
+
{
|
|
276
|
+
title: "Cerca de ti",
|
|
277
|
+
detail: "Fácil de llegar, y también atendemos por WhatsApp.",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
end: "Ven a visitarnos",
|
|
281
|
+
cta: "Escríbenos por WhatsApp",
|
|
282
|
+
},
|
|
283
|
+
slots: {
|
|
284
|
+
title: "Título",
|
|
285
|
+
detail: "Detalle",
|
|
286
|
+
badge: "Descuento",
|
|
287
|
+
cta: "Cómo pedir",
|
|
288
|
+
contact: "Contacto",
|
|
289
|
+
photo: "Foto",
|
|
290
|
+
days: "Días",
|
|
291
|
+
times: "Horarios",
|
|
292
|
+
items: "Productos",
|
|
293
|
+
prices: "Precios",
|
|
294
|
+
tag: "Etiqueta",
|
|
295
|
+
name: "Nombre del producto",
|
|
296
|
+
price: "Precio",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
/** A sample Pix key from Banco Central's manual, so the card shows a code until the owner's
|
|
301
|
+
* own key replaces it. It pays no one. */
|
|
302
|
+
const SAMPLE_PIX_KEY = "123e4567-e12b-12d1-a456-426655440000";
|
|
303
|
+
/** The margins of each size. Stories and status keep clear of the strips their app draws over
|
|
304
|
+
* the top (the name) and the bottom (the reply box). */
|
|
305
|
+
const MARGINS = {
|
|
306
|
+
"ig-post": { side: 80, top: 80, bottom: 80 },
|
|
307
|
+
"ig-square": { side: 72, top: 72, bottom: 72 },
|
|
308
|
+
"ig-story": { side: 80, top: 250, bottom: 320 },
|
|
309
|
+
"wa-status": { side: 80, top: 220, bottom: 300 },
|
|
310
|
+
"share-card": { side: 64, top: 64, bottom: 64 },
|
|
311
|
+
};
|
|
312
|
+
function canvasOf(format) {
|
|
313
|
+
const { w, h } = FORMATS[format];
|
|
314
|
+
const margin = MARGINS[format];
|
|
315
|
+
const safe = {
|
|
316
|
+
x: margin.side,
|
|
317
|
+
y: margin.top,
|
|
318
|
+
w: w - 2 * margin.side,
|
|
319
|
+
h: h - margin.top - margin.bottom,
|
|
320
|
+
};
|
|
321
|
+
return { w, h, safe, wide: w > h, k: Math.min(safe.w / 920, safe.h / 1190) };
|
|
322
|
+
}
|
|
323
|
+
const r = Math.round;
|
|
324
|
+
const rounded = (box) => ({ x: r(box.x), y: r(box.y), w: r(box.w), h: r(box.h) });
|
|
325
|
+
/** A text box `lines` tall, or `box.h` tall when given. */
|
|
326
|
+
function words(id, text, box, spec, slot) {
|
|
327
|
+
const lineHeight = lineHeightOf(spec);
|
|
328
|
+
const lines = spec.lines ?? 1;
|
|
329
|
+
const size = r(spec.size);
|
|
330
|
+
return {
|
|
331
|
+
id,
|
|
332
|
+
type: "text",
|
|
333
|
+
text,
|
|
334
|
+
...(slot ? { slot } : {}),
|
|
335
|
+
frame: rounded({ ...box, h: box.h ?? tall(spec) }),
|
|
336
|
+
style: {
|
|
337
|
+
font: spec.weight ? { role: spec.font, weight: spec.weight } : { role: spec.font },
|
|
338
|
+
size,
|
|
339
|
+
lineHeight,
|
|
340
|
+
...(spec.align ? { align: spec.align } : {}),
|
|
341
|
+
...(spec.valign ? { valign: spec.valign } : {}),
|
|
342
|
+
...(spec.caps ? { uppercase: true, tracking: 0.08 } : {}),
|
|
343
|
+
fill: { token: spec.fill ?? "ink" },
|
|
344
|
+
},
|
|
345
|
+
...(spec.fixed
|
|
346
|
+
? {}
|
|
347
|
+
: { fit: { mode: "shrink", min: r(size * 0.6), maxLines: lines } }),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const lineHeightOf = (spec) => spec.lineHeight ?? (spec.font === "display" ? 1.06 : 1.3);
|
|
351
|
+
/** The height a `words` box takes. */
|
|
352
|
+
function tall(spec) {
|
|
353
|
+
return Math.ceil((spec.lines ?? 1) * r(spec.size) * lineHeightOf(spec));
|
|
354
|
+
}
|
|
355
|
+
function rect(id, box, fill, radius = 0) {
|
|
356
|
+
return {
|
|
357
|
+
id,
|
|
358
|
+
type: "shape",
|
|
359
|
+
shape: "rect",
|
|
360
|
+
frame: rounded(box),
|
|
361
|
+
...(radius ? { radius: r(radius) } : {}),
|
|
362
|
+
fill: { token: fill },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function ellipse(id, box, fill) {
|
|
366
|
+
return { id, type: "shape", shape: "ellipse", frame: rounded(box), fill: { token: fill } };
|
|
367
|
+
}
|
|
368
|
+
function clock(box) {
|
|
369
|
+
return {
|
|
370
|
+
id: "clock",
|
|
371
|
+
type: "icon",
|
|
372
|
+
icon: "clock",
|
|
373
|
+
stroke: { token: "primary" },
|
|
374
|
+
frame: rounded(box),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function photo(box, radius, prompt) {
|
|
378
|
+
return {
|
|
379
|
+
id: "photo",
|
|
380
|
+
type: "art",
|
|
381
|
+
slot: "photo",
|
|
382
|
+
prompt,
|
|
383
|
+
radius: r(radius),
|
|
384
|
+
frame: rounded(box),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function logo(id, box, align = "start") {
|
|
388
|
+
return { id, type: "logo", variant: "horizontal", tone: "color", align, frame: rounded(box) };
|
|
389
|
+
}
|
|
390
|
+
/** A group whose layers are given in its own space. */
|
|
391
|
+
function group(id, box, layers, rotate) {
|
|
392
|
+
const frame = rounded(box);
|
|
393
|
+
return { id, type: "group", frame: rotate ? { ...frame, rotate } : frame, layers };
|
|
394
|
+
}
|
|
395
|
+
/** Words on a rounded bar of a color, as one group. */
|
|
396
|
+
function pill(id, text, box, fill, ink, size, slot, caps = false) {
|
|
397
|
+
const inset = box.h * 0.45;
|
|
398
|
+
return group(id, box, [
|
|
399
|
+
rect(`${id}-bar`, { x: 0, y: 0, w: box.w, h: box.h }, fill, box.h / 2),
|
|
400
|
+
words(`${id}-text`, text, { x: inset, y: 0, w: box.w - 2 * inset, h: box.h }, { font: "text", size, weight: 600, fill: ink, align: "center", valign: "middle", caps }, slot),
|
|
401
|
+
]);
|
|
402
|
+
}
|
|
403
|
+
/** The line a post ends with: the brand's public Instagram, its site, or its WhatsApp. */
|
|
404
|
+
function contactLine(brand) {
|
|
405
|
+
const { instagram, site, whatsapp } = brand.links;
|
|
406
|
+
if (instagram?.public)
|
|
407
|
+
return `@${instagram.value.replace(/^@/, "").replace(/^.*\//, "")}`;
|
|
408
|
+
if (site?.public)
|
|
409
|
+
return site.value.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
410
|
+
if (brand.domain)
|
|
411
|
+
return brand.domain;
|
|
412
|
+
if (whatsapp?.public)
|
|
413
|
+
return whatsapp.value;
|
|
414
|
+
return brand.name;
|
|
415
|
+
}
|
|
416
|
+
/** The logo on the left of a row and a line of words on the right. */
|
|
417
|
+
function footer(box, text, size, fill, slot) {
|
|
418
|
+
return [
|
|
419
|
+
logo("logo", { ...box, w: box.w * 0.46 }),
|
|
420
|
+
words("contact", text, { x: box.x + box.w * 0.5, y: box.y, w: box.w * 0.5, h: box.h }, { font: "text", size, weight: 600, fill, align: "end", valign: "middle" }, slot),
|
|
421
|
+
];
|
|
422
|
+
}
|
|
423
|
+
const ROW_LINE = 2;
|
|
424
|
+
/** A card for `rows` rows of `size` type, with room for one more before the words overflow. */
|
|
425
|
+
function tableHeight(rows, size, pad) {
|
|
426
|
+
return Math.ceil((rows + 1) * r(size) * ROW_LINE + 2 * pad);
|
|
427
|
+
}
|
|
428
|
+
/** Two columns of rows on a card: names on the left, values on the right, lined up. */
|
|
429
|
+
function table(card, pad, left, right, size, slots) {
|
|
430
|
+
const inner = { x: card.x + pad, y: card.y + pad, w: card.w - 2 * pad, h: card.h - 2 * pad };
|
|
431
|
+
const row = { font: "text", size, lineHeight: ROW_LINE, valign: "middle", fixed: true };
|
|
432
|
+
return [
|
|
433
|
+
rect("card", card, "surface", pad * 0.6),
|
|
434
|
+
words("names", left.join("\n"), { ...inner, w: inner.w * 0.6 }, row, slots[0]),
|
|
435
|
+
words("values", right.join("\n"), { ...inner, x: inner.x + inner.w * 0.6, w: inner.w * 0.4 }, { ...row, weight: 600, align: "end" }, slots[1]),
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
function page(id, layers, background = "ground") {
|
|
439
|
+
return { id, background: { token: background }, layers };
|
|
440
|
+
}
|
|
441
|
+
function promotion(c, copy) {
|
|
442
|
+
const t = copy.promotion;
|
|
443
|
+
const s = c.safe;
|
|
444
|
+
const badge = (box) => group("badge", box, [
|
|
445
|
+
ellipse("badge-disc", { ...box, x: 0, y: 0 }, "primary"),
|
|
446
|
+
words("badge-text", t.badge, { x: box.w * 0.12, y: 0, w: box.w * 0.76, h: box.h }, {
|
|
447
|
+
font: "display",
|
|
448
|
+
size: box.w * 0.3,
|
|
449
|
+
fill: "onPrimary",
|
|
450
|
+
align: "center",
|
|
451
|
+
valign: "middle",
|
|
452
|
+
}, "badge"),
|
|
453
|
+
], -8);
|
|
454
|
+
if (c.wide) {
|
|
455
|
+
const photoW = 470;
|
|
456
|
+
const textW = s.w - photoW - 48;
|
|
457
|
+
const title = { font: "display", size: 72, lines: 2 };
|
|
458
|
+
return [
|
|
459
|
+
page("p", [
|
|
460
|
+
photo({ x: s.x + s.w - photoW, y: s.y, w: photoW, h: s.h }, 28, t.prompt),
|
|
461
|
+
badge({ x: s.x + s.w - photoW + 24, y: s.y + 24, w: 150, h: 150 }),
|
|
462
|
+
words("title", t.title, { x: s.x, y: s.y, w: textW }, title, "title"),
|
|
463
|
+
words("detail", t.detail, { x: s.x, y: s.y + tall(title) + 20, w: textW }, { font: "text", size: 34, lines: 2, fill: "muted" }, "detail"),
|
|
464
|
+
...footer({ x: s.x, y: s.y + s.h - 56, w: textW, h: 56 }, t.cta, 28, "primary", "cta"),
|
|
465
|
+
]),
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
const k = c.k;
|
|
469
|
+
const gap = 40 * k;
|
|
470
|
+
const footerH = 72 * k;
|
|
471
|
+
const title = { font: "display", size: 104 * k, lines: 2 };
|
|
472
|
+
const detail = { font: "text", size: 48 * k, lines: 2, fill: "muted" };
|
|
473
|
+
const photoH = s.h - footerH - tall(title) - tall(detail) - 2.5 * gap;
|
|
474
|
+
const titleY = s.y + photoH + gap;
|
|
475
|
+
const size = 230 * k;
|
|
476
|
+
return [
|
|
477
|
+
page("p", [
|
|
478
|
+
photo({ x: s.x, y: s.y, w: s.w, h: photoH }, 36 * k, t.prompt),
|
|
479
|
+
badge({ x: s.x + s.w - size - 28 * k, y: s.y + 28 * k, w: size, h: size }),
|
|
480
|
+
words("title", t.title, { x: s.x, y: titleY, w: s.w }, title, "title"),
|
|
481
|
+
words("detail", t.detail, { x: s.x, y: titleY + tall(title) + gap / 2, w: s.w }, detail, "detail"),
|
|
482
|
+
...footer({ x: s.x, y: s.y + s.h - footerH, w: s.w, h: footerH }, t.cta, 38 * k, "primary", "cta"),
|
|
483
|
+
]),
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
function hours(c, copy, brand) {
|
|
487
|
+
const t = copy.hours;
|
|
488
|
+
const s = c.safe;
|
|
489
|
+
const slots = ["days", "times"];
|
|
490
|
+
if (c.wide) {
|
|
491
|
+
const left = 400;
|
|
492
|
+
const title = { font: "display", size: 64, lines: 2 };
|
|
493
|
+
return [
|
|
494
|
+
page("p", [
|
|
495
|
+
clock({ x: s.x, y: s.y, w: 72, h: 72 }),
|
|
496
|
+
words("title", t.title, { x: s.x, y: s.y + 96, w: left }, title, "title"),
|
|
497
|
+
logo("logo", { x: s.x, y: s.y + s.h - 56, w: left, h: 56 }),
|
|
498
|
+
...table({ x: s.x + left + 48, y: s.y, w: s.w - left - 48, h: s.h }, 40, t.days, t.times, 32, slots),
|
|
499
|
+
]),
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
const k = c.k;
|
|
503
|
+
const gap = 40 * k;
|
|
504
|
+
const icon = 96 * k;
|
|
505
|
+
const footerH = 72 * k;
|
|
506
|
+
const pad = 56 * k;
|
|
507
|
+
const row = 52 * k;
|
|
508
|
+
const title = { font: "display", size: 104 * k, lines: 2 };
|
|
509
|
+
const room = s.h - footerH - gap;
|
|
510
|
+
const head = icon + 24 * k + tall(title) + gap;
|
|
511
|
+
const cardH = Math.min(tableHeight(t.days.length, row, pad), room - head);
|
|
512
|
+
const top = s.y + (room - head - cardH) / 2;
|
|
513
|
+
return [
|
|
514
|
+
page("p", [
|
|
515
|
+
clock({ x: s.x, y: top, w: icon, h: icon }),
|
|
516
|
+
words("title", t.title, { x: s.x, y: top + icon + 24 * k, w: s.w }, title, "title"),
|
|
517
|
+
...table({ x: s.x, y: top + head, w: s.w, h: cardH }, pad, t.days, t.times, row, slots),
|
|
518
|
+
...footer({ x: s.x, y: s.y + s.h - footerH, w: s.w, h: footerH }, contactLine(brand), 38 * k, "muted", "contact"),
|
|
519
|
+
]),
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
function menu(c, copy) {
|
|
523
|
+
const t = copy.menu;
|
|
524
|
+
const s = c.safe;
|
|
525
|
+
const slots = ["items", "prices"];
|
|
526
|
+
if (c.wide) {
|
|
527
|
+
const left = 380;
|
|
528
|
+
const title = { font: "display", size: 64, lines: 3 };
|
|
529
|
+
return [
|
|
530
|
+
page("p", [
|
|
531
|
+
words("title", t.title, { x: s.x, y: s.y, w: left }, title, "title"),
|
|
532
|
+
words("cta", t.cta, { x: s.x, y: s.y + tall(title) + 24, w: left }, { font: "text", size: 28, weight: 600, fill: "primary" }, "cta"),
|
|
533
|
+
logo("logo", { x: s.x, y: s.y + s.h - 56, w: left, h: 56 }),
|
|
534
|
+
...table({ x: s.x + left + 48, y: s.y, w: s.w - left - 48, h: s.h }, 40, t.items, t.prices, 32, slots),
|
|
535
|
+
]),
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
const k = c.k;
|
|
539
|
+
const gap = 40 * k;
|
|
540
|
+
const footerH = 72 * k;
|
|
541
|
+
const pad = 56 * k;
|
|
542
|
+
const row = 52 * k;
|
|
543
|
+
const title = { font: "display", size: 104 * k, lines: 2 };
|
|
544
|
+
const room = s.h - footerH - gap;
|
|
545
|
+
const head = tall(title) + gap;
|
|
546
|
+
const cardH = Math.min(tableHeight(t.items.length, row, pad), room - head);
|
|
547
|
+
const top = s.y + (room - head - cardH) / 2;
|
|
548
|
+
return [
|
|
549
|
+
page("p", [
|
|
550
|
+
words("title", t.title, { x: s.x, y: top, w: s.w }, title, "title"),
|
|
551
|
+
...table({ x: s.x, y: top + head, w: s.w, h: cardH }, pad, t.items, t.prices, row, slots),
|
|
552
|
+
...footer({ x: s.x, y: s.y + s.h - footerH, w: s.w, h: footerH }, t.cta, 38 * k, "primary", "cta"),
|
|
553
|
+
]),
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
function pix(c, copy, brand, options) {
|
|
557
|
+
const t = copy.pix;
|
|
558
|
+
const s = c.safe;
|
|
559
|
+
const details = {
|
|
560
|
+
key: options.pix?.key ?? SAMPLE_PIX_KEY,
|
|
561
|
+
name: brand.name,
|
|
562
|
+
city: options.pix?.city ?? "BRASIL",
|
|
563
|
+
amount: options.pix?.amount,
|
|
564
|
+
};
|
|
565
|
+
const qr = (box) => ({
|
|
566
|
+
id: "pix-qr",
|
|
567
|
+
type: "qr",
|
|
568
|
+
value: pixCode(details),
|
|
569
|
+
fill: { token: "ink" },
|
|
570
|
+
background: { token: "ground" },
|
|
571
|
+
ecc: "M",
|
|
572
|
+
frame: rounded(box),
|
|
573
|
+
});
|
|
574
|
+
const keyLine = `${t.key}: ${details.key}`;
|
|
575
|
+
if (c.wide) {
|
|
576
|
+
const side = s.h;
|
|
577
|
+
const textX = s.x + side + 48;
|
|
578
|
+
const textW = s.w - side - 48;
|
|
579
|
+
const title = { font: "display", size: 68, lines: 2 };
|
|
580
|
+
const detail = { font: "text", size: 34, lines: 2, fill: "muted" };
|
|
581
|
+
return [
|
|
582
|
+
page("p", [
|
|
583
|
+
rect("card", { x: s.x, y: s.y, w: side, h: side }, "surface", 28),
|
|
584
|
+
qr({ x: s.x + 32, y: s.y + 32, w: side - 64, h: side - 64 }),
|
|
585
|
+
words("title", t.title, { x: textX, y: s.y, w: textW }, title, "title"),
|
|
586
|
+
words("detail", t.detail, { x: textX, y: s.y + tall(title) + 16, w: textW }, detail, "detail"),
|
|
587
|
+
words("pix-key", keyLine, { x: textX, y: s.y + tall(title) + tall(detail) + 40, w: textW }, { font: "text", size: 28, lines: 2, weight: 600 }),
|
|
588
|
+
logo("logo", { x: textX, y: s.y + s.h - 56, w: textW, h: 56 }),
|
|
589
|
+
]),
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
const k = c.k;
|
|
593
|
+
const gap = 40 * k;
|
|
594
|
+
const footerH = 72 * k;
|
|
595
|
+
const pad = 48 * k;
|
|
596
|
+
const title = { font: "display", size: 104 * k, lines: 1 };
|
|
597
|
+
const detail = { font: "text", size: 44 * k, lines: 2, fill: "muted" };
|
|
598
|
+
const key = { font: "text", size: 36 * k, lines: 2, weight: 600, align: "center" };
|
|
599
|
+
const cardY = s.y + tall(title) + 20 * k + tall(detail) + gap;
|
|
600
|
+
const card = { x: s.x, y: cardY, w: s.w, h: s.y + s.h - footerH - gap - cardY };
|
|
601
|
+
const side = Math.min(card.w - 2 * pad, card.h - 2 * pad - tall(key) - 24 * k);
|
|
602
|
+
return [
|
|
603
|
+
page("p", [
|
|
604
|
+
words("title", t.title, { x: s.x, y: s.y, w: s.w }, title, "title"),
|
|
605
|
+
words("detail", t.detail, { x: s.x, y: s.y + tall(title) + 20 * k, w: s.w }, detail, "detail"),
|
|
606
|
+
rect("card", card, "surface", 36 * k),
|
|
607
|
+
qr({ x: card.x + (card.w - side) / 2, y: card.y + pad, w: side, h: side }),
|
|
608
|
+
words("pix-key", keyLine, { x: card.x + pad, y: card.y + pad + side + 24 * k, w: card.w - 2 * pad }, key),
|
|
609
|
+
...footer({ x: s.x, y: s.y + s.h - footerH, w: s.w, h: footerH }, brand.name, 38 * k, "muted"),
|
|
610
|
+
]),
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
function newProduct(c, copy) {
|
|
614
|
+
const t = copy.newProduct;
|
|
615
|
+
const s = c.safe;
|
|
616
|
+
if (c.wide) {
|
|
617
|
+
const photoW = 470;
|
|
618
|
+
const textX = s.x + photoW + 48;
|
|
619
|
+
const textW = s.w - photoW - 48;
|
|
620
|
+
const name = { font: "display", size: 60, lines: 2 };
|
|
621
|
+
const detail = { font: "text", size: 32, lines: 2, fill: "muted" };
|
|
622
|
+
return [
|
|
623
|
+
page("p", [
|
|
624
|
+
photo({ x: s.x, y: s.y, w: photoW, h: s.h }, 28, t.prompt),
|
|
625
|
+
pill("tag", t.tag, { x: textX, y: s.y, w: 220, h: 52 }, "primary", "onPrimary", 24, "tag", true),
|
|
626
|
+
words("title", t.title, { x: textX, y: s.y + 76, w: textW }, name, "name"),
|
|
627
|
+
words("detail", t.detail, { x: textX, y: s.y + 76 + tall(name) + 16, w: textW }, detail, "detail"),
|
|
628
|
+
words("price", t.price, { x: textX, y: s.y + s.h - 64, w: textW * 0.5, h: 64 }, { font: "display", size: 52, fill: "primary", valign: "middle" }, "price"),
|
|
629
|
+
logo("logo", { x: textX + textW * 0.5, y: s.y + s.h - 56, w: textW * 0.5, h: 56 }, "end"),
|
|
630
|
+
]),
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
const k = c.k;
|
|
634
|
+
const gap = 40 * k;
|
|
635
|
+
const tagH = 68 * k;
|
|
636
|
+
const rowH = 88 * k;
|
|
637
|
+
const name = { font: "display", size: 96 * k, lines: 2 };
|
|
638
|
+
const detail = { font: "text", size: 44 * k, lines: 2, fill: "muted" };
|
|
639
|
+
const photoY = s.y + tagH + 28 * k;
|
|
640
|
+
const photoH = s.y + s.h - rowH - gap - tall(detail) - 16 * k - tall(name) - gap - photoY;
|
|
641
|
+
const nameY = photoY + photoH + gap;
|
|
642
|
+
const rowY = s.y + s.h - rowH;
|
|
643
|
+
return [
|
|
644
|
+
page("p", [
|
|
645
|
+
pill("tag", t.tag, { x: s.x, y: s.y, w: 300 * k, h: tagH }, "primary", "onPrimary", 32 * k, "tag", true),
|
|
646
|
+
photo({ x: s.x, y: photoY, w: s.w, h: photoH }, 36 * k, t.prompt),
|
|
647
|
+
words("title", t.title, { x: s.x, y: nameY, w: s.w }, name, "name"),
|
|
648
|
+
words("detail", t.detail, { x: s.x, y: nameY + tall(name) + 16 * k, w: s.w }, detail, "detail"),
|
|
649
|
+
words("price", t.price, { x: s.x, y: rowY, w: s.w * 0.55, h: rowH }, { font: "display", size: 80 * k, fill: "primary", valign: "middle" }, "price"),
|
|
650
|
+
logo("logo", { x: s.x + s.w * 0.55, y: rowY + (rowH - 72 * k) / 2, w: s.w * 0.45, h: 72 * k }, "end"),
|
|
651
|
+
]),
|
|
652
|
+
];
|
|
653
|
+
}
|
|
654
|
+
function statusOffer(c, copy) {
|
|
655
|
+
const t = copy.statusOffer;
|
|
656
|
+
const s = c.safe;
|
|
657
|
+
/** The logo on a ground chip, since the brand color behind it may be close to the mark's. */
|
|
658
|
+
const chip = (box) => group("logo-chip", box, [
|
|
659
|
+
rect("logo-chip-bar", { x: 0, y: 0, w: box.w, h: box.h }, "ground", box.h / 2),
|
|
660
|
+
logo("logo", { x: box.h * 0.45, y: box.h * 0.2, w: box.w - box.h * 0.9, h: box.h * 0.6 }, "center"),
|
|
661
|
+
]);
|
|
662
|
+
const reply = (box, size) => group("reply", box, [
|
|
663
|
+
rect("reply-bar", { x: 0, y: 0, w: box.w, h: box.h }, "ground", box.h / 2),
|
|
664
|
+
{
|
|
665
|
+
id: "reply-icon",
|
|
666
|
+
type: "icon",
|
|
667
|
+
icon: "message-circle",
|
|
668
|
+
stroke: { token: "primary" },
|
|
669
|
+
frame: rounded({
|
|
670
|
+
x: box.h * 0.35,
|
|
671
|
+
y: box.h * 0.25,
|
|
672
|
+
w: box.h * 0.5,
|
|
673
|
+
h: box.h * 0.5,
|
|
674
|
+
}),
|
|
675
|
+
},
|
|
676
|
+
words("cta", t.cta, { x: box.h, y: 0, w: box.w - box.h * 1.35, h: box.h }, { font: "text", size, weight: 600, valign: "middle" }, "cta"),
|
|
677
|
+
]);
|
|
678
|
+
/** The offer, centered between `top` and `bottom`: a line above, the deal, a line below. */
|
|
679
|
+
const middle = (top, bottom, x, w, sizes, align) => {
|
|
680
|
+
const eyebrow = {
|
|
681
|
+
font: "text",
|
|
682
|
+
size: sizes.eyebrow,
|
|
683
|
+
weight: 600,
|
|
684
|
+
fill: "onPrimary",
|
|
685
|
+
align,
|
|
686
|
+
caps: true,
|
|
687
|
+
};
|
|
688
|
+
const title = {
|
|
689
|
+
font: "display",
|
|
690
|
+
size: sizes.title,
|
|
691
|
+
lines: sizes.titleLines,
|
|
692
|
+
lineHeight: 1,
|
|
693
|
+
fill: "onPrimary",
|
|
694
|
+
align,
|
|
695
|
+
valign: "middle",
|
|
696
|
+
};
|
|
697
|
+
const detail = { font: "text", size: sizes.detail, fill: "onPrimary", align };
|
|
698
|
+
const block = tall(eyebrow) + tall(title) + tall(detail) + 2 * sizes.gap;
|
|
699
|
+
const y = top + (bottom - top - block) / 2;
|
|
700
|
+
const titleY = y + tall(eyebrow) + sizes.gap;
|
|
701
|
+
return [
|
|
702
|
+
words("eyebrow", t.eyebrow, { x, y, w }, eyebrow),
|
|
703
|
+
words("title", t.title, { x, y: titleY, w }, title, "title"),
|
|
704
|
+
words("detail", t.detail, { x, y: titleY + tall(title) + sizes.gap, w }, detail, "detail"),
|
|
705
|
+
];
|
|
706
|
+
};
|
|
707
|
+
if (c.wide) {
|
|
708
|
+
return [
|
|
709
|
+
page("p", [
|
|
710
|
+
chip({ x: s.x, y: s.y, w: 300, h: 84 }),
|
|
711
|
+
...middle(s.y + 84, s.y + s.h - 84, s.x, s.w * 0.8, { eyebrow: 28, title: 100, titleLines: 2, detail: 32, gap: 12 }, "start"),
|
|
712
|
+
reply({ x: s.x, y: s.y + s.h - 84, w: 640, h: 84 }, 28),
|
|
713
|
+
], "primary"),
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
const k = c.k;
|
|
717
|
+
const chipH = 104 * k;
|
|
718
|
+
const replyH = 120 * k;
|
|
719
|
+
return [
|
|
720
|
+
page("p", [
|
|
721
|
+
chip({ x: s.x + (s.w - 380 * k) / 2, y: s.y, w: 380 * k, h: chipH }),
|
|
722
|
+
...middle(s.y + chipH, s.y + s.h - replyH, s.x, s.w, { eyebrow: 40 * k, title: 150 * k, titleLines: 3, detail: 48 * k, gap: 24 * k }, "center"),
|
|
723
|
+
reply({ x: s.x, y: s.y + s.h - replyH, w: s.w, h: replyH }, 40 * k),
|
|
724
|
+
], "primary"),
|
|
725
|
+
];
|
|
726
|
+
}
|
|
727
|
+
/** An arrow pointing right, drawn as a path in a 56 by 40 box. */
|
|
728
|
+
function arrow(id, box) {
|
|
729
|
+
return {
|
|
730
|
+
id,
|
|
731
|
+
type: "shape",
|
|
732
|
+
shape: "path",
|
|
733
|
+
d: "M4 20H50M32 4L50 20L32 36",
|
|
734
|
+
stroke: { paint: { token: "primary" }, width: 6 },
|
|
735
|
+
frame: rounded(box),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function carousel(c, copy, brand) {
|
|
739
|
+
const t = copy.carousel;
|
|
740
|
+
const s = c.safe;
|
|
741
|
+
// The share card holds the portrait layout at a smaller scale.
|
|
742
|
+
const k = c.wide ? 0.6 : c.k;
|
|
743
|
+
const count = t.tips.length + 2;
|
|
744
|
+
const gap = 40 * k;
|
|
745
|
+
const footerH = 72 * k;
|
|
746
|
+
const footerY = s.y + s.h - footerH;
|
|
747
|
+
const cover = { font: "display", size: 128 * k, lines: 4, valign: "middle" };
|
|
748
|
+
const coverY = s.y + 80 * k + (footerY - gap - s.y - 80 * k - tall(cover)) / 2;
|
|
749
|
+
const hint = {
|
|
750
|
+
font: "text",
|
|
751
|
+
size: 38 * k,
|
|
752
|
+
fill: "muted",
|
|
753
|
+
align: "end",
|
|
754
|
+
valign: "middle",
|
|
755
|
+
};
|
|
756
|
+
const pages = [
|
|
757
|
+
page("cover", [
|
|
758
|
+
words("brand", brand.name, { x: s.x, y: s.y, w: s.w }, { font: "text", size: 36 * k, weight: 600, fill: "primary", caps: true }),
|
|
759
|
+
words("title", t.title, { x: s.x, y: coverY, w: s.w }, cover, "title"),
|
|
760
|
+
logo("logo", { x: s.x, y: footerY, w: s.w * 0.4, h: footerH }),
|
|
761
|
+
words("hint", t.hint, { x: s.x + s.w * 0.42, y: footerY, w: s.w * 0.58 - 84 * k, h: footerH }, hint),
|
|
762
|
+
arrow("hint-arrow", {
|
|
763
|
+
x: s.x + s.w - 56 * k,
|
|
764
|
+
y: footerY + (footerH - 40 * k) / 2,
|
|
765
|
+
w: 56 * k,
|
|
766
|
+
h: 40 * k,
|
|
767
|
+
}),
|
|
768
|
+
]),
|
|
769
|
+
];
|
|
770
|
+
t.tips.forEach((tip, index) => {
|
|
771
|
+
const circle = 150 * k;
|
|
772
|
+
const head = { font: "display", size: 96 * k, lines: 2 };
|
|
773
|
+
const text = { font: "text", size: 50 * k, lines: 4, fill: "muted" };
|
|
774
|
+
const block = circle + gap + tall(head) + 24 * k + tall(text);
|
|
775
|
+
const y = s.y + (footerY - gap - s.y - block) / 2;
|
|
776
|
+
const n = index + 1;
|
|
777
|
+
pages.push(page(`tip-${n}`, [
|
|
778
|
+
words("count", `${n + 1}/${count}`, { x: s.x + s.w - 200 * k, y: s.y, w: 200 * k }, { font: "text", size: 36 * k, fill: "muted", align: "end" }),
|
|
779
|
+
group("number", { x: s.x, y, w: circle, h: circle }, [
|
|
780
|
+
ellipse("number-disc", { x: 0, y: 0, w: circle, h: circle }, "primary"),
|
|
781
|
+
words("number-text", String(n), { x: 0, y: 0, w: circle, h: circle }, {
|
|
782
|
+
font: "display",
|
|
783
|
+
size: circle * 0.5,
|
|
784
|
+
fill: "onPrimary",
|
|
785
|
+
align: "center",
|
|
786
|
+
valign: "middle",
|
|
787
|
+
}),
|
|
788
|
+
]),
|
|
789
|
+
words("title", tip.title, { x: s.x, y: y + circle + gap, w: s.w }, head, `tip-${n}-title`),
|
|
790
|
+
words("detail", tip.detail, { x: s.x, y: y + circle + gap + tall(head) + 24 * k, w: s.w }, text, `tip-${n}-detail`),
|
|
791
|
+
{
|
|
792
|
+
id: "mark",
|
|
793
|
+
type: "logo",
|
|
794
|
+
variant: "mark",
|
|
795
|
+
tone: "color",
|
|
796
|
+
frame: rounded({ x: s.x, y: footerY, w: footerH, h: footerH }),
|
|
797
|
+
},
|
|
798
|
+
]));
|
|
799
|
+
});
|
|
800
|
+
const end = { font: "display", size: 120 * k, lines: 2 };
|
|
801
|
+
const contact = { font: "text", size: 48 * k, weight: 600 };
|
|
802
|
+
const ctaH = 112 * k;
|
|
803
|
+
const block = tall(end) + gap + tall(contact) + gap + ctaH;
|
|
804
|
+
const y = s.y + (footerY - gap - s.y - block) / 2;
|
|
805
|
+
pages.push(page("end", [
|
|
806
|
+
words("title", t.end, { x: s.x, y, w: s.w }, end, "end-title"),
|
|
807
|
+
words("contact", contactLine(brand), { x: s.x, y: y + tall(end) + gap, w: s.w }, contact, "contact"),
|
|
808
|
+
pill("cta", t.cta, { x: s.x, y: y + tall(end) + tall(contact) + 2 * gap, w: s.w * 0.8, h: ctaH }, "primary", "onPrimary", 40 * k, "cta"),
|
|
809
|
+
logo("logo", { x: s.x, y: footerY, w: s.w * 0.4, h: footerH }),
|
|
810
|
+
]));
|
|
811
|
+
return pages;
|
|
812
|
+
}
|
|
813
|
+
const BUILDERS = {
|
|
814
|
+
promotion,
|
|
815
|
+
hours,
|
|
816
|
+
menu,
|
|
817
|
+
pix,
|
|
818
|
+
"new-product": newProduct,
|
|
819
|
+
"status-offer": statusOffer,
|
|
820
|
+
carousel,
|
|
821
|
+
};
|
|
822
|
+
/** The text slots of a scene's pages, labeled in its language, and its photo slot. */
|
|
823
|
+
function slotsOf(pages, labels) {
|
|
824
|
+
const slots = {};
|
|
825
|
+
// A carousel numbers its slots by page (tip-2-title); the label is the one for "title".
|
|
826
|
+
const label = (slot) => {
|
|
827
|
+
const base = slot.replace(/^(tip-\d+|end)-/, "");
|
|
828
|
+
return labels[base] ?? base;
|
|
829
|
+
};
|
|
830
|
+
const walk = (layers) => {
|
|
831
|
+
for (const layer of layers) {
|
|
832
|
+
if (layer.type === "group")
|
|
833
|
+
walk(layer.layers);
|
|
834
|
+
else if (layer.type === "text" && layer.slot)
|
|
835
|
+
slots[layer.slot] = { kind: "text", label: label(layer.slot), live: true };
|
|
836
|
+
else if (layer.type === "art" && layer.slot)
|
|
837
|
+
slots[layer.slot] = { kind: "image", label: label(layer.slot) };
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
for (const each of pages)
|
|
841
|
+
walk(each.layers);
|
|
842
|
+
return slots;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* A starting design for one of the owner's jobs, in one size and language, for one brand.
|
|
846
|
+
*
|
|
847
|
+
* ```ts
|
|
848
|
+
* const scene = postTemplate("promotion", brand); // an Instagram post, in the brand's language
|
|
849
|
+
* ```
|
|
850
|
+
*/
|
|
851
|
+
export function postTemplate(id, brand, options = {}) {
|
|
852
|
+
const info = POST_TEMPLATE_INFO[id];
|
|
853
|
+
const format = options.format ?? info.format;
|
|
854
|
+
const locale = options.locale ?? brand.defaultLocale;
|
|
855
|
+
const copy = COPY[locale];
|
|
856
|
+
const canvas = canvasOf(format);
|
|
857
|
+
const pages = BUILDERS[id](canvas, copy, brand, options);
|
|
858
|
+
return {
|
|
859
|
+
v: 1,
|
|
860
|
+
format,
|
|
861
|
+
size: { w: canvas.w, h: canvas.h },
|
|
862
|
+
theme: "light",
|
|
863
|
+
locale,
|
|
864
|
+
pages,
|
|
865
|
+
slots: slotsOf(pages, copy.slots),
|
|
866
|
+
meta: { name: info.text[locale].name, template: id, source: "template" },
|
|
867
|
+
};
|
|
868
|
+
}
|