@appmockup/core 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/dist/index.js ADDED
@@ -0,0 +1,1079 @@
1
+ // src/schema.ts
2
+ import { z } from "zod";
3
+ var fontWeightSchema = z.enum([
4
+ "regular",
5
+ "medium",
6
+ "semibold",
7
+ "bold",
8
+ "heavy",
9
+ "black"
10
+ ]);
11
+ var outputSizeSchema = z.object({
12
+ name: z.string(),
13
+ width: z.number().int().positive(),
14
+ height: z.number().int().positive()
15
+ });
16
+ var shadowSchema = z.object({
17
+ color: z.string().optional(),
18
+ blur: z.number().min(0).optional(),
19
+ offsetX: z.number().optional(),
20
+ offsetY: z.number().optional()
21
+ });
22
+ var decorationSchema = z.object({
23
+ type: z.enum(["glow", "circle", "dots"]),
24
+ /** Center x as a fraction of canvas width (glow/circle; ignored by dots). */
25
+ x: z.number().optional(),
26
+ /** Center y as a fraction of canvas height (glow/circle; ignored by dots). */
27
+ y: z.number().optional(),
28
+ /** glow/circle: radius as a fraction of canvas width; dots: dot radius. */
29
+ size: z.number().positive().optional(),
30
+ /** dots only: grid pitch as a fraction of canvas width. */
31
+ spacing: z.number().positive().optional(),
32
+ color: z.string().optional(),
33
+ opacity: z.number().min(0).max(1).optional()
34
+ });
35
+ var backgroundSchema = z.object({
36
+ type: z.enum(["solid", "gradient", "radial"]),
37
+ colors: z.array(z.string()).min(1),
38
+ direction: z.enum([
39
+ "topToBottom",
40
+ "leftToRight",
41
+ "topLeftToBottomRight",
42
+ "bottomToTop",
43
+ "rightToLeft",
44
+ "bottomLeftToTopRight"
45
+ ]).optional(),
46
+ /** Decorative shapes drawn on top of the fill, behind text and device. */
47
+ decorations: z.array(decorationSchema).optional()
48
+ });
49
+ var headlineSchema = z.object({
50
+ fontFamily: z.string().optional(),
51
+ fontWeight: fontWeightSchema.optional(),
52
+ color: z.string(),
53
+ relativeSize: z.number().positive(),
54
+ /** Optional text shadow; a bright color at zero offsets reads as a glow. */
55
+ shadow: shadowSchema.optional()
56
+ });
57
+ var subtitleSchema = z.object({
58
+ text: z.string(),
59
+ fontFamily: z.string().optional(),
60
+ fontWeight: fontWeightSchema.optional(),
61
+ color: z.string(),
62
+ relativeSize: z.number().positive(),
63
+ /** Optional text shadow; a bright color at zero offsets reads as a glow. */
64
+ shadow: shadowSchema.optional()
65
+ });
66
+ var deviceFrameSchema = z.object({
67
+ style: z.enum(["iPhone", "iPad"]),
68
+ bezelColor: z.string(),
69
+ cornerRadius: z.number(),
70
+ bezelWidth: z.number(),
71
+ screenRelativeWidth: z.number(),
72
+ verticalOffset: z.number(),
73
+ /** Device left edge as a fraction of canvas width. Omit/undefined = horizontally centered. */
74
+ horizontalOffset: z.number().optional(),
75
+ /** Clockwise rotation about the device center, in degrees. Omit/undefined = 0. */
76
+ rotation: z.number().optional(),
77
+ /**
78
+ * How the screenshot fills the device screen. "cover" (default) scales to fill and crops
79
+ * the overflow; "contain" scales to fit entirely, letterboxing with the bezel color.
80
+ */
81
+ fit: z.enum(["cover", "contain"]).optional(),
82
+ /** Drop shadow under the device. A bare `{}` uses the default soft look. */
83
+ shadow: shadowSchema.optional(),
84
+ /** Horizontal shear in degrees about the device center — a pseudo-3D lean. */
85
+ tilt: z.number().optional()
86
+ });
87
+ var screenshotEntrySchema = z.object({
88
+ id: z.string(),
89
+ file: z.string(),
90
+ headlines: z.record(z.string(), z.string()),
91
+ /** Overrides the global background for this screenshot (incl. its decorations). */
92
+ background: backgroundSchema.optional()
93
+ });
94
+ var mockupConfigSchema = z.object({
95
+ appName: z.string(),
96
+ outputPrefix: z.string(),
97
+ languages: z.array(z.string()).min(1),
98
+ outputSizes: z.array(outputSizeSchema).min(1),
99
+ background: backgroundSchema,
100
+ headline: headlineSchema,
101
+ subtitle: subtitleSchema.optional(),
102
+ deviceFrame: deviceFrameSchema,
103
+ screenshots: z.array(screenshotEntrySchema).min(1)
104
+ });
105
+ function parseConfig(input) {
106
+ return mockupConfigSchema.parse(input);
107
+ }
108
+ function validateHeadlines(config) {
109
+ const errors = [];
110
+ for (const entry of config.screenshots) {
111
+ for (const lang of config.languages) {
112
+ if (entry.headlines[lang] === void 0) {
113
+ errors.push(
114
+ `Missing headline for screenshot '${entry.id}' in language '${lang}'`
115
+ );
116
+ }
117
+ }
118
+ }
119
+ return errors;
120
+ }
121
+
122
+ // src/color.ts
123
+ function parseHexParts(hex) {
124
+ let h = hex.trim();
125
+ if (h.startsWith("#")) h = h.slice(1);
126
+ let r = 0;
127
+ let g = 0;
128
+ let b = 0;
129
+ let a = 1;
130
+ if (h.length === 3 && /^[0-9a-fA-F]{3}$/.test(h)) {
131
+ r = parseInt(h[0] + h[0], 16);
132
+ g = parseInt(h[1] + h[1], 16);
133
+ b = parseInt(h[2] + h[2], 16);
134
+ } else if (h.length === 6 && /^[0-9a-fA-F]{6}$/.test(h)) {
135
+ r = parseInt(h.slice(0, 2), 16);
136
+ g = parseInt(h.slice(2, 4), 16);
137
+ b = parseInt(h.slice(4, 6), 16);
138
+ } else if (h.length === 8 && /^[0-9a-fA-F]{8}$/.test(h)) {
139
+ r = parseInt(h.slice(0, 2), 16);
140
+ g = parseInt(h.slice(2, 4), 16);
141
+ b = parseInt(h.slice(4, 6), 16);
142
+ a = parseInt(h.slice(6, 8), 16) / 255;
143
+ }
144
+ return { r, g, b, a };
145
+ }
146
+ function parseHexColor(hex) {
147
+ const { r, g, b, a } = parseHexParts(hex);
148
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
149
+ }
150
+ function parseHexColorWithAlpha(hex, alphaScale) {
151
+ const { r, g, b, a } = parseHexParts(hex);
152
+ return `rgba(${r}, ${g}, ${b}, ${a * alphaScale})`;
153
+ }
154
+
155
+ // src/fonts.ts
156
+ var DEFAULT_FONT_FAMILY = "Inter";
157
+ var FALLBACK_FONT_STACK = '"Noto Sans", "Noto Sans Arabic", "Noto Sans KR", sans-serif';
158
+ function cssFontWeight(weight) {
159
+ switch (weight) {
160
+ case "regular":
161
+ return 400;
162
+ case "medium":
163
+ return 500;
164
+ case "semibold":
165
+ return 600;
166
+ case "bold":
167
+ return 700;
168
+ case "heavy":
169
+ return 800;
170
+ case "black":
171
+ return 900;
172
+ }
173
+ }
174
+ function cssFont(family, weight, sizePx) {
175
+ const fam = family && !family.startsWith(".") ? family : DEFAULT_FONT_FAMILY;
176
+ return `${cssFontWeight(weight)} ${sizePx}px "${fam}", ${FALLBACK_FONT_STACK}`;
177
+ }
178
+
179
+ // src/starter.ts
180
+ function createStarterConfig(appName) {
181
+ return {
182
+ appName,
183
+ outputPrefix: appName.toLowerCase().replace(/\s+/g, "-") || "app",
184
+ languages: ["en"],
185
+ outputSizes: [
186
+ { name: "iPhone_6.7", width: 1290, height: 2796 },
187
+ { name: "iPhone_6.5", width: 1242, height: 2688 },
188
+ { name: "iPad_12.9", width: 2048, height: 2732 }
189
+ ],
190
+ background: {
191
+ type: "gradient",
192
+ colors: ["#FBF8F2", "#F0E4CF"],
193
+ direction: "topToBottom"
194
+ },
195
+ headline: { fontWeight: "bold", color: "#1D2939", relativeSize: 0.05 },
196
+ subtitle: { text: appName, fontWeight: "medium", color: "#C9975B", relativeSize: 0.025 },
197
+ deviceFrame: {
198
+ style: "iPhone",
199
+ bezelColor: "#1D2939",
200
+ cornerRadius: 55,
201
+ bezelWidth: 12,
202
+ screenRelativeWidth: 0.75,
203
+ verticalOffset: 0.25
204
+ },
205
+ screenshots: [
206
+ {
207
+ id: "01",
208
+ file: "screenshots/01.png",
209
+ headlines: { en: "Your headline\ngoes here" }
210
+ }
211
+ ]
212
+ };
213
+ }
214
+ var STANDARD_OUTPUT_SIZES = [
215
+ { name: "iPhone_6.9", width: 1320, height: 2868, note: "iPhone 16 Pro Max etc." },
216
+ { name: "iPhone_6.7", width: 1290, height: 2796, note: "iPhone 15/16 Pro Max, 14 Plus" },
217
+ { name: "iPhone_6.5", width: 1242, height: 2688, note: "iPhone 11 Pro Max, XS Max" },
218
+ { name: "iPad_13", width: 2064, height: 2752, note: "iPad Pro 13-inch (M4)" },
219
+ { name: "iPad_12.9", width: 2048, height: 2732, note: "iPad Pro 12.9-inch" },
220
+ { name: "Android_Phone", width: 1080, height: 1920, note: "Google Play phone (portrait)" }
221
+ ];
222
+
223
+ // src/render/decorations.ts
224
+ var DEFAULTS = {
225
+ glow: { x: 0.5, y: 0.3, size: 0.35, color: "#FFFFFF", opacity: 0.5 },
226
+ circle: { x: 0.5, y: 0.3, size: 0.18, color: "#FFFFFF", opacity: 0.25 },
227
+ dots: { size: 6e-3, spacing: 0.06, color: "#FFFFFF", opacity: 0.18 }
228
+ };
229
+ function drawDecorations(ctx, width, height, decorations) {
230
+ if (!decorations || decorations.length === 0) return;
231
+ for (const deco of decorations) {
232
+ ctx.save();
233
+ if (deco.type === "glow") {
234
+ const d = DEFAULTS.glow;
235
+ const cx = (deco.x ?? d.x) * width;
236
+ const cy = (deco.y ?? d.y) * height;
237
+ const r = (deco.size ?? d.size) * width;
238
+ const color = deco.color ?? d.color;
239
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
240
+ gradient.addColorStop(0, parseHexColor(color));
241
+ gradient.addColorStop(1, parseHexColorWithAlpha(color, 0));
242
+ ctx.globalAlpha = deco.opacity ?? d.opacity;
243
+ ctx.fillStyle = gradient;
244
+ ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
245
+ } else if (deco.type === "circle") {
246
+ const d = DEFAULTS.circle;
247
+ const cx = (deco.x ?? d.x) * width;
248
+ const cy = (deco.y ?? d.y) * height;
249
+ const r = (deco.size ?? d.size) * width;
250
+ ctx.globalAlpha = deco.opacity ?? d.opacity;
251
+ ctx.fillStyle = parseHexColor(deco.color ?? d.color);
252
+ ctx.beginPath();
253
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
254
+ ctx.fill();
255
+ } else {
256
+ const d = DEFAULTS.dots;
257
+ const r = (deco.size ?? d.size) * width;
258
+ const pitch = (deco.spacing ?? d.spacing) * width;
259
+ ctx.globalAlpha = deco.opacity ?? d.opacity;
260
+ ctx.fillStyle = parseHexColor(deco.color ?? d.color);
261
+ for (let py = pitch / 2; py < height; py += pitch) {
262
+ for (let px = pitch / 2; px < width; px += pitch) {
263
+ ctx.beginPath();
264
+ ctx.arc(px, py, r, 0, Math.PI * 2);
265
+ ctx.fill();
266
+ }
267
+ }
268
+ }
269
+ ctx.restore();
270
+ }
271
+ }
272
+
273
+ // src/render/background.ts
274
+ function drawBackground(ctx, width, height, config) {
275
+ if (config.type === "solid") {
276
+ ctx.fillStyle = parseHexColor(config.colors[0] ?? "#FFFFFF");
277
+ ctx.fillRect(0, 0, width, height);
278
+ } else if (config.type === "radial") {
279
+ const cx = width / 2;
280
+ const cy = height / 2;
281
+ const radius = Math.hypot(cx, cy);
282
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
283
+ addStops(gradient, config.colors);
284
+ ctx.fillStyle = gradient;
285
+ ctx.fillRect(0, 0, width, height);
286
+ } else {
287
+ const direction = config.direction ?? "topToBottom";
288
+ let x0 = 0;
289
+ let y0 = 0;
290
+ let x1 = 0;
291
+ let y1 = 0;
292
+ switch (direction) {
293
+ case "topToBottom":
294
+ x0 = width / 2;
295
+ y0 = 0;
296
+ x1 = width / 2;
297
+ y1 = height;
298
+ break;
299
+ case "bottomToTop":
300
+ x0 = width / 2;
301
+ y0 = height;
302
+ x1 = width / 2;
303
+ y1 = 0;
304
+ break;
305
+ case "leftToRight":
306
+ x0 = 0;
307
+ y0 = height / 2;
308
+ x1 = width;
309
+ y1 = height / 2;
310
+ break;
311
+ case "rightToLeft":
312
+ x0 = width;
313
+ y0 = height / 2;
314
+ x1 = 0;
315
+ y1 = height / 2;
316
+ break;
317
+ case "topLeftToBottomRight":
318
+ x0 = 0;
319
+ y0 = 0;
320
+ x1 = width;
321
+ y1 = height;
322
+ break;
323
+ case "bottomLeftToTopRight":
324
+ x0 = 0;
325
+ y0 = height;
326
+ x1 = width;
327
+ y1 = 0;
328
+ break;
329
+ }
330
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
331
+ addStops(gradient, config.colors);
332
+ ctx.fillStyle = gradient;
333
+ ctx.fillRect(0, 0, width, height);
334
+ }
335
+ drawDecorations(ctx, width, height, config.decorations);
336
+ }
337
+ function addStops(gradient, colors) {
338
+ if (colors.length === 1) {
339
+ const only = parseHexColor(colors[0]);
340
+ gradient.addColorStop(0, only);
341
+ gradient.addColorStop(1, only);
342
+ } else {
343
+ colors.forEach((c, i) => {
344
+ gradient.addColorStop(i / (colors.length - 1), parseHexColor(c));
345
+ });
346
+ }
347
+ }
348
+
349
+ // src/render/device-frame.ts
350
+ var DEVICE_SHADOW_DEFAULTS = {
351
+ color: "#00000055",
352
+ blur: 0.035,
353
+ offsetX: 0,
354
+ offsetY: 0.015
355
+ };
356
+ function deviceFrameRect(canvasWidth, canvasHeight, config) {
357
+ const cw = canvasWidth;
358
+ const ch = canvasHeight;
359
+ const width = config.screenRelativeWidth * cw;
360
+ const aspectRatio = config.style === "iPhone" ? 2.167 : 1.4;
361
+ const height = width * aspectRatio;
362
+ const x = config.horizontalOffset != null ? config.horizontalOffset * cw : (cw - width) / 2;
363
+ const y = config.verticalOffset * ch;
364
+ return {
365
+ x,
366
+ y,
367
+ width,
368
+ height,
369
+ centerX: x + width / 2,
370
+ centerY: y + height / 2,
371
+ rotationDeg: config.rotation ?? 0
372
+ };
373
+ }
374
+ function drawDeviceFrame(ctx, screenshot, canvasWidth, canvasHeight, config) {
375
+ const rect = deviceFrameRect(canvasWidth, canvasHeight, config);
376
+ const deviceX = rect.x;
377
+ const deviceTop = rect.y;
378
+ const deviceWidth = rect.width;
379
+ const deviceHeight = rect.height;
380
+ const scaledCornerRadius = config.cornerRadius * (deviceWidth / 400);
381
+ const scaledBezelWidth = config.bezelWidth * (deviceWidth / 400);
382
+ const rotationRad = rect.rotationDeg * Math.PI / 180;
383
+ const tiltRad = (config.tilt ?? 0) * Math.PI / 180;
384
+ const rotated = rotationRad !== 0;
385
+ const tilted = tiltRad !== 0;
386
+ ctx.save();
387
+ if (rotated || tilted) {
388
+ ctx.translate(rect.centerX, rect.centerY);
389
+ if (rotated) ctx.rotate(rotationRad);
390
+ if (tilted) ctx.transform(1, 0, Math.tan(tiltRad), 1, 0, 0);
391
+ ctx.translate(-rect.centerX, -rect.centerY);
392
+ }
393
+ ctx.save();
394
+ if (config.shadow) {
395
+ ctx.shadowColor = parseHexColor(config.shadow.color ?? DEVICE_SHADOW_DEFAULTS.color);
396
+ ctx.shadowBlur = (config.shadow.blur ?? DEVICE_SHADOW_DEFAULTS.blur) * canvasWidth;
397
+ ctx.shadowOffsetX = (config.shadow.offsetX ?? DEVICE_SHADOW_DEFAULTS.offsetX) * canvasWidth;
398
+ ctx.shadowOffsetY = (config.shadow.offsetY ?? DEVICE_SHADOW_DEFAULTS.offsetY) * canvasWidth;
399
+ }
400
+ ctx.fillStyle = parseHexColor(config.bezelColor);
401
+ ctx.beginPath();
402
+ ctx.roundRect(deviceX, deviceTop, deviceWidth, deviceHeight, scaledCornerRadius);
403
+ ctx.fill();
404
+ ctx.restore();
405
+ const innerX = deviceX + scaledBezelWidth;
406
+ const innerY = deviceTop + scaledBezelWidth;
407
+ const innerW = deviceWidth - scaledBezelWidth * 2;
408
+ const innerH = deviceHeight - scaledBezelWidth * 2;
409
+ const innerCornerRadius = Math.max(scaledCornerRadius - scaledBezelWidth, 0);
410
+ ctx.save();
411
+ ctx.beginPath();
412
+ ctx.roundRect(innerX, innerY, innerW, innerH, innerCornerRadius);
413
+ ctx.clip();
414
+ if (screenshot) {
415
+ const imgW = screenshot.width;
416
+ const imgH = screenshot.height;
417
+ const scale = config.fit === "contain" ? Math.min(innerW / imgW, innerH / imgH) : Math.max(innerW / imgW, innerH / imgH);
418
+ const drawW = imgW * scale;
419
+ const drawH = imgH * scale;
420
+ const drawX = innerX + (innerW - drawW) / 2;
421
+ const drawY = innerY + (innerH - drawH) / 2;
422
+ ctx.drawImage(screenshot.source, drawX, drawY, drawW, drawH);
423
+ }
424
+ ctx.restore();
425
+ if (config.style === "iPhone") {
426
+ const pillWidth = deviceWidth * 0.27;
427
+ const pillHeight = deviceWidth * 0.075;
428
+ const pillX = innerX + innerW / 2 - pillWidth / 2;
429
+ const pillY = innerY + innerH * 0.015;
430
+ ctx.save();
431
+ ctx.fillStyle = "rgba(0, 0, 0, 1)";
432
+ ctx.beginPath();
433
+ ctx.roundRect(pillX, pillY, pillWidth, pillHeight, pillHeight / 2);
434
+ ctx.fill();
435
+ ctx.restore();
436
+ }
437
+ const barWidth = deviceWidth * 0.33;
438
+ const barHeight = deviceWidth * 0.012;
439
+ const barX = innerX + innerW / 2 - barWidth / 2;
440
+ const barY = innerY + innerH - innerH * 0.01 - barHeight;
441
+ ctx.save();
442
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
443
+ ctx.beginPath();
444
+ ctx.roundRect(barX, barY, barWidth, barHeight, barHeight / 2);
445
+ ctx.fill();
446
+ ctx.restore();
447
+ ctx.restore();
448
+ }
449
+
450
+ // src/render/text.ts
451
+ var LINE_HEIGHT_MULTIPLE = 1.1;
452
+ var TEXT_SHADOW_DEFAULTS = {
453
+ color: "#00000066",
454
+ blur: 0.012,
455
+ offsetX: 0,
456
+ offsetY: 0
457
+ };
458
+ function drawHeadline(ctx, text, canvasWidth, canvasHeight, config, topMarginFraction = 0.035) {
459
+ const fontSize = config.relativeSize * canvasHeight;
460
+ const topY = topMarginFraction * canvasHeight;
461
+ const maxWidth = canvasWidth * 0.88;
462
+ return drawText(ctx, {
463
+ text,
464
+ fontFamily: config.fontFamily,
465
+ fontWeight: config.fontWeight ?? "bold",
466
+ color: config.color,
467
+ fontSize,
468
+ x: (canvasWidth - maxWidth) / 2,
469
+ y: topY,
470
+ maxWidth,
471
+ shadow: config.shadow,
472
+ canvasWidth
473
+ });
474
+ }
475
+ function drawSubtitle(ctx, text, canvasWidth, canvasHeight, config, belowY) {
476
+ const fontSize = config.relativeSize * canvasHeight;
477
+ const gap = canvasHeight * 8e-3;
478
+ const maxWidth = canvasWidth * 0.88;
479
+ return drawText(ctx, {
480
+ text,
481
+ fontFamily: config.fontFamily,
482
+ fontWeight: config.fontWeight ?? "medium",
483
+ color: config.color,
484
+ fontSize,
485
+ x: (canvasWidth - maxWidth) / 2,
486
+ y: belowY + gap,
487
+ maxWidth,
488
+ shadow: config.shadow,
489
+ canvasWidth
490
+ });
491
+ }
492
+ function layoutLines(ctx, text, maxWidth) {
493
+ const out = [];
494
+ for (const paragraph of text.split("\n")) {
495
+ const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
496
+ if (words.length === 0) {
497
+ out.push("");
498
+ continue;
499
+ }
500
+ let line = "";
501
+ for (const word of words) {
502
+ const candidate = line ? `${line} ${word}` : word;
503
+ if (line === "" || ctx.measureText(candidate).width <= maxWidth) {
504
+ line = candidate;
505
+ } else {
506
+ out.push(line);
507
+ line = word;
508
+ }
509
+ }
510
+ out.push(line);
511
+ }
512
+ return out;
513
+ }
514
+ function drawText(ctx, opts) {
515
+ const fontSize = opts.fontSize;
516
+ ctx.font = cssFont(opts.fontFamily, opts.fontWeight, fontSize);
517
+ const lines = layoutLines(ctx, opts.text, opts.maxWidth);
518
+ const metrics = ctx.measureText("Mg");
519
+ const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent ?? fontSize * 0.8;
520
+ const descent = metrics.fontBoundingBoxDescent ?? metrics.actualBoundingBoxDescent ?? fontSize * 0.2;
521
+ const naturalLineHeight = ascent + descent;
522
+ const lineHeight = naturalLineHeight * LINE_HEIGHT_MULTIPLE;
523
+ ctx.save();
524
+ if (opts.shadow) {
525
+ ctx.shadowColor = parseHexColor(opts.shadow.color ?? TEXT_SHADOW_DEFAULTS.color);
526
+ ctx.shadowBlur = (opts.shadow.blur ?? TEXT_SHADOW_DEFAULTS.blur) * opts.canvasWidth;
527
+ ctx.shadowOffsetX = (opts.shadow.offsetX ?? TEXT_SHADOW_DEFAULTS.offsetX) * opts.canvasWidth;
528
+ ctx.shadowOffsetY = (opts.shadow.offsetY ?? TEXT_SHADOW_DEFAULTS.offsetY) * opts.canvasWidth;
529
+ }
530
+ ctx.fillStyle = parseHexColor(opts.color);
531
+ ctx.textAlign = "center";
532
+ ctx.textBaseline = "alphabetic";
533
+ const centerX = opts.x + opts.maxWidth / 2;
534
+ lines.forEach((line, i) => {
535
+ const baseline = opts.y + ascent + i * lineHeight;
536
+ ctx.fillText(line, centerX, baseline);
537
+ });
538
+ ctx.restore();
539
+ return { bottomY: opts.y + lines.length * lineHeight };
540
+ }
541
+
542
+ // src/render/index.ts
543
+ function renderMockup(ctx, scene) {
544
+ drawBackground(ctx, scene.width, scene.height, scene.background);
545
+ const headlineResult = drawHeadline(
546
+ ctx,
547
+ scene.headlineText,
548
+ scene.width,
549
+ scene.height,
550
+ scene.headline
551
+ );
552
+ if (scene.subtitle) {
553
+ drawSubtitle(
554
+ ctx,
555
+ scene.subtitle.text,
556
+ scene.width,
557
+ scene.height,
558
+ scene.subtitle,
559
+ headlineResult.bottomY
560
+ );
561
+ }
562
+ if (scene.screenshot) {
563
+ drawDeviceFrame(ctx, scene.screenshot, scene.width, scene.height, scene.deviceFrame);
564
+ }
565
+ }
566
+ function resolveScene(config, entry, language, size, screenshot) {
567
+ return {
568
+ width: size.width,
569
+ height: size.height,
570
+ // A screenshot may carry its own background (incl. decorations), so a set
571
+ // can alternate looks across the store listing.
572
+ background: entry.background ?? config.background,
573
+ headlineText: entry.headlines[language] ?? "",
574
+ headline: config.headline,
575
+ subtitle: config.subtitle,
576
+ deviceFrame: config.deviceFrame,
577
+ screenshot
578
+ };
579
+ }
580
+
581
+ // src/templates.ts
582
+ var STANDARD_SIZES = [
583
+ { name: "iPhone_6.7", width: 1290, height: 2796 },
584
+ { name: "iPhone_6.5", width: 1242, height: 2688 },
585
+ { name: "iPad_12.9", width: 2048, height: 2732 }
586
+ ];
587
+ var TEMPLATES = [
588
+ {
589
+ id: "sunrise",
590
+ name: "Sunrise",
591
+ description: "Warm cream gradient, navy headline, centered device.",
592
+ swatch: "linear-gradient(180deg, #FBF8F2, #F0E4CF)",
593
+ config: {
594
+ appName: "Sunrise",
595
+ outputPrefix: "sunrise",
596
+ languages: ["en", "de", "tr"],
597
+ outputSizes: STANDARD_SIZES,
598
+ background: { type: "gradient", colors: ["#FBF8F2", "#F0E4CF"], direction: "topToBottom" },
599
+ headline: { fontWeight: "bold", color: "#1D2939", relativeSize: 0.05 },
600
+ subtitle: { text: "Sunrise", fontWeight: "medium", color: "#C9975B", relativeSize: 0.025 },
601
+ deviceFrame: {
602
+ style: "iPhone",
603
+ bezelColor: "#1D2939",
604
+ cornerRadius: 55,
605
+ bezelWidth: 12,
606
+ screenRelativeWidth: 0.75,
607
+ verticalOffset: 0.25
608
+ },
609
+ screenshots: [
610
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Start every day\nwith intention", de: "Beginne jeden Tag\nmit Absicht", tr: "Her g\xFCne\nbir niyetle ba\u015Fla" } },
611
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Build habits\nthat stick", de: "Gewohnheiten,\ndie bleiben", tr: "Kal\u0131c\u0131\nal\u0131\u015Fkanl\u0131klar kur" } },
612
+ { id: "03", file: "screenshots/03.png", headlines: { en: "Watch your\nprogress grow", de: "Sieh deinen\nFortschritt wachsen", tr: "\u0130lerlemeni\nb\xFCy\xFCrken g\xF6r" } }
613
+ ]
614
+ }
615
+ },
616
+ {
617
+ id: "aurora",
618
+ name: "Aurora",
619
+ description: "Dark gradient, white headline, slight tilt.",
620
+ swatch: "linear-gradient(180deg, #0B1220, #1E293B)",
621
+ config: {
622
+ appName: "Aurora",
623
+ outputPrefix: "aurora",
624
+ languages: ["en", "de", "tr"],
625
+ outputSizes: STANDARD_SIZES,
626
+ background: { type: "gradient", colors: ["#0B1220", "#1E293B"], direction: "topToBottom" },
627
+ headline: { fontWeight: "heavy", color: "#F8FAFC", relativeSize: 0.052 },
628
+ subtitle: { text: "Aurora", fontWeight: "medium", color: "#7DD3FC", relativeSize: 0.025 },
629
+ deviceFrame: {
630
+ style: "iPhone",
631
+ bezelColor: "#0F172A",
632
+ cornerRadius: 55,
633
+ bezelWidth: 12,
634
+ screenRelativeWidth: 0.72,
635
+ verticalOffset: 0.27,
636
+ rotation: -4
637
+ },
638
+ screenshots: [
639
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Your night,\nbeautifully tracked", de: "Deine Nacht,\nsch\xF6n erfasst", tr: "Gecen,\nzarif\xE7e takip edilir" } },
640
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Insights that\nactually help", de: "Einblicke, die\nwirklich helfen", tr: "Ger\xE7ekten yard\u0131mc\u0131\nolan i\xE7g\xF6r\xFCler" } }
641
+ ]
642
+ }
643
+ },
644
+ {
645
+ id: "ocean",
646
+ name: "Ocean",
647
+ description: "Blue diagonal gradient, device shifted right and rotated.",
648
+ swatch: "linear-gradient(135deg, #0EA5E9, #2563EB)",
649
+ config: {
650
+ appName: "Wavelength",
651
+ outputPrefix: "wavelength",
652
+ languages: ["en", "de", "tr"],
653
+ outputSizes: STANDARD_SIZES,
654
+ background: { type: "gradient", colors: ["#0EA5E9", "#2563EB"], direction: "topLeftToBottomRight" },
655
+ headline: { fontWeight: "bold", color: "#FFFFFF", relativeSize: 0.05 },
656
+ subtitle: { text: "Wavelength", fontWeight: "medium", color: "#E0F2FE", relativeSize: 0.024 },
657
+ deviceFrame: {
658
+ style: "iPhone",
659
+ bezelColor: "#0C4A6E",
660
+ cornerRadius: 55,
661
+ bezelWidth: 12,
662
+ screenRelativeWidth: 0.66,
663
+ verticalOffset: 0.3,
664
+ horizontalOffset: 0.3,
665
+ rotation: 6
666
+ },
667
+ screenshots: [
668
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Ride your\nfocus flow", de: "Reite deinen\nFokus-Flow", tr: "Odak ak\u0131\u015F\u0131na\nbin" } },
669
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Deep work,\nmade simple", de: "Tiefe Arbeit,\neinfach gemacht", tr: "Derin \xE7al\u0131\u015Fma,\nbasitle\u015Ftirildi" } }
670
+ ]
671
+ }
672
+ },
673
+ {
674
+ id: "minimal",
675
+ name: "Minimal",
676
+ description: "Clean white, large centered device, no subtitle.",
677
+ swatch: "#FFFFFF",
678
+ config: {
679
+ appName: "Focus",
680
+ outputPrefix: "focus",
681
+ languages: ["en", "de", "tr"],
682
+ outputSizes: STANDARD_SIZES,
683
+ background: { type: "solid", colors: ["#FFFFFF"] },
684
+ headline: { fontWeight: "bold", color: "#111827", relativeSize: 0.052 },
685
+ deviceFrame: {
686
+ style: "iPhone",
687
+ bezelColor: "#111827",
688
+ cornerRadius: 55,
689
+ bezelWidth: 11,
690
+ screenRelativeWidth: 0.82,
691
+ verticalOffset: 0.22
692
+ },
693
+ screenshots: [
694
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Less noise.\nMore done.", de: "Weniger L\xE4rm.\nMehr geschafft.", tr: "Daha az g\xFCr\xFClt\xFC.\nDaha \xE7ok i\u015F." } },
695
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Everything\nin one place", de: "Alles an\neinem Ort", tr: "Her \u015Fey\ntek yerde" } }
696
+ ]
697
+ }
698
+ },
699
+ {
700
+ id: "sunset",
701
+ name: "Sunset",
702
+ description: "Pink-to-amber diagonal, device shifted left and tilted.",
703
+ swatch: "linear-gradient(135deg, #FB7185, #F59E0B)",
704
+ config: {
705
+ appName: "Ember",
706
+ outputPrefix: "ember",
707
+ languages: ["en", "de", "tr"],
708
+ outputSizes: STANDARD_SIZES,
709
+ background: { type: "gradient", colors: ["#FB7185", "#F59E0B"], direction: "topLeftToBottomRight" },
710
+ headline: { fontWeight: "heavy", color: "#FFFFFF", relativeSize: 0.05 },
711
+ subtitle: { text: "Ember", fontWeight: "semibold", color: "#FFE4E6", relativeSize: 0.024 },
712
+ deviceFrame: {
713
+ style: "iPhone",
714
+ bezelColor: "#7F1D1D",
715
+ cornerRadius: 55,
716
+ bezelWidth: 12,
717
+ screenRelativeWidth: 0.64,
718
+ verticalOffset: 0.3,
719
+ horizontalOffset: 0.05,
720
+ rotation: -8
721
+ },
722
+ screenshots: [
723
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Make every\nmoment count", de: "Lass jeden\nMoment z\xE4hlen", tr: "Her an\u0131\nde\u011Ferli k\u0131l" } },
724
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Glow up\nyour routine", de: "Bring Glanz in\ndeine Routine", tr: "Rutinine\nparlakl\u0131k kat" } }
725
+ ]
726
+ }
727
+ },
728
+ {
729
+ id: "neon",
730
+ name: "Neon Glow",
731
+ description: "Dark radial background, neon glows, glowing text and device.",
732
+ swatch: "radial-gradient(circle, #2E1065, #0F0A1F)",
733
+ config: {
734
+ appName: "Neon",
735
+ outputPrefix: "neon",
736
+ languages: ["en", "de", "tr"],
737
+ outputSizes: STANDARD_SIZES,
738
+ background: {
739
+ type: "radial",
740
+ colors: ["#2E1065", "#0F0A1F"],
741
+ decorations: [
742
+ { type: "glow", x: 0.22, y: 0.18, size: 0.4, color: "#8B5CF6", opacity: 0.55 },
743
+ { type: "glow", x: 0.85, y: 0.65, size: 0.45, color: "#06B6D4", opacity: 0.4 }
744
+ ]
745
+ },
746
+ headline: {
747
+ fontWeight: "heavy",
748
+ color: "#F5F3FF",
749
+ relativeSize: 0.052,
750
+ shadow: { color: "#A78BFAAA", blur: 0.02 }
751
+ },
752
+ subtitle: {
753
+ text: "Neon",
754
+ fontWeight: "medium",
755
+ color: "#C4B5FD",
756
+ relativeSize: 0.025,
757
+ shadow: { color: "#A78BFA88", blur: 0.015 }
758
+ },
759
+ deviceFrame: {
760
+ style: "iPhone",
761
+ bezelColor: "#1E1145",
762
+ cornerRadius: 55,
763
+ bezelWidth: 12,
764
+ screenRelativeWidth: 0.7,
765
+ verticalOffset: 0.27,
766
+ shadow: { color: "#8B5CF680", blur: 0.05, offsetY: 0.01 }
767
+ },
768
+ screenshots: [
769
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Turn the lights\nup on your data", de: "Bring Licht\nin deine Daten", tr: "Verilerinin\n\u0131\u015F\u0131\u011F\u0131n\u0131 a\xE7" } },
770
+ { id: "02", file: "screenshots/02.png", headlines: { en: "After dark,\nstill in control", de: "Nach Einbruch der Nacht\nweiter im Griff", tr: "Gece bile\nkontrol sende" } }
771
+ ]
772
+ }
773
+ },
774
+ {
775
+ id: "glass",
776
+ name: "Soft Glass",
777
+ description: "Airy light gradient, soft circles, hero drop shadow.",
778
+ swatch: "linear-gradient(0deg, #EEF2F7, #DDE5F0)",
779
+ config: {
780
+ appName: "Glass",
781
+ outputPrefix: "glass",
782
+ languages: ["en", "de", "tr"],
783
+ outputSizes: STANDARD_SIZES,
784
+ background: {
785
+ type: "gradient",
786
+ colors: ["#EEF2F7", "#DDE5F0"],
787
+ direction: "bottomToTop",
788
+ decorations: [
789
+ { type: "circle", x: 0.15, y: 0.12, size: 0.22, color: "#FFFFFF", opacity: 0.5 },
790
+ { type: "circle", x: 0.9, y: 0.5, size: 0.3, color: "#BFD3EE", opacity: 0.35 }
791
+ ]
792
+ },
793
+ headline: { fontWeight: "bold", color: "#1E293B", relativeSize: 0.05 },
794
+ subtitle: { text: "Glass", fontWeight: "medium", color: "#3B82F6", relativeSize: 0.024 },
795
+ deviceFrame: {
796
+ style: "iPhone",
797
+ bezelColor: "#334155",
798
+ cornerRadius: 55,
799
+ bezelWidth: 12,
800
+ screenRelativeWidth: 0.74,
801
+ verticalOffset: 0.25,
802
+ shadow: { color: "#33415566", blur: 0.045, offsetY: 0.02 }
803
+ },
804
+ screenshots: [
805
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Light as air,\nclear as glass", de: "Leicht wie Luft,\nklar wie Glas", tr: "Hava kadar hafif,\ncam kadar net" } },
806
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Calm design,\nsharp results", de: "Ruhiges Design,\nklare Ergebnisse", tr: "Sakin tasar\u0131m,\nkeskin sonu\xE7lar" } }
807
+ ]
808
+ }
809
+ },
810
+ {
811
+ id: "pulse",
812
+ name: "Bold Pulse",
813
+ description: "Three-stop radial burst, black-weight headline, default shadow.",
814
+ swatch: "radial-gradient(circle, #F59E0B, #DC2626, #7F1D1D)",
815
+ config: {
816
+ appName: "Pulse",
817
+ outputPrefix: "pulse",
818
+ languages: ["en", "de", "tr"],
819
+ outputSizes: STANDARD_SIZES,
820
+ background: { type: "radial", colors: ["#F59E0B", "#DC2626", "#7F1D1D"] },
821
+ headline: {
822
+ fontWeight: "black",
823
+ color: "#FFFFFF",
824
+ relativeSize: 0.055,
825
+ shadow: { color: "#00000066", blur: 0.015, offsetY: 4e-3 }
826
+ },
827
+ subtitle: { text: "Pulse", fontWeight: "semibold", color: "#FDE68A", relativeSize: 0.024 },
828
+ deviceFrame: {
829
+ style: "iPhone",
830
+ bezelColor: "#450A0A",
831
+ cornerRadius: 55,
832
+ bezelWidth: 12,
833
+ screenRelativeWidth: 0.7,
834
+ verticalOffset: 0.28,
835
+ shadow: {}
836
+ },
837
+ screenshots: [
838
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Feel the beat\nof your goals", de: "Sp\xFCr den Takt\ndeiner Ziele", tr: "Hedeflerinin\nritmini hisset" } },
839
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Push harder.\nGo further.", de: "Gib mehr.\nKomm weiter.", tr: "Daha s\u0131k\u0131 \xE7al\u0131\u015F.\nDaha ileri git." } }
840
+ ]
841
+ }
842
+ },
843
+ {
844
+ id: "dotgrid",
845
+ name: "Dot Grid",
846
+ description: "Minimal paper white with a dot pattern and subtle shadow.",
847
+ swatch: "radial-gradient(circle at 30% 30%, #E7E5E4 8%, #FAFAF9 9%)",
848
+ config: {
849
+ appName: "Grid",
850
+ outputPrefix: "grid",
851
+ languages: ["en", "de", "tr"],
852
+ outputSizes: STANDARD_SIZES,
853
+ background: {
854
+ type: "solid",
855
+ colors: ["#FAFAF9"],
856
+ decorations: [
857
+ { type: "dots", color: "#1C1917", opacity: 0.12, size: 5e-3, spacing: 0.05 }
858
+ ]
859
+ },
860
+ headline: { fontWeight: "bold", color: "#1C1917", relativeSize: 0.052 },
861
+ deviceFrame: {
862
+ style: "iPhone",
863
+ bezelColor: "#1C1917",
864
+ cornerRadius: 55,
865
+ bezelWidth: 11,
866
+ screenRelativeWidth: 0.8,
867
+ verticalOffset: 0.22,
868
+ shadow: { color: "#1C191740", blur: 0.03, offsetY: 0.012 }
869
+ },
870
+ screenshots: [
871
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Ideas on\na clean grid", de: "Ideen auf\nklarem Raster", tr: "Temiz bir \u0131zgarada\nfikirler" } },
872
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Structure\nwithout clutter", de: "Struktur\nohne Ballast", tr: "Da\u011F\u0131n\u0131kl\u0131k olmadan\nd\xFCzen" } }
873
+ ]
874
+ }
875
+ },
876
+ {
877
+ id: "momentum",
878
+ name: "Momentum",
879
+ description: "Diagonal gradient with a tilted, rotated hero device.",
880
+ swatch: "linear-gradient(45deg, #0EA5E9, #6366F1)",
881
+ config: {
882
+ appName: "Momentum",
883
+ outputPrefix: "momentum",
884
+ languages: ["en", "de", "tr"],
885
+ outputSizes: STANDARD_SIZES,
886
+ background: {
887
+ type: "gradient",
888
+ colors: ["#0EA5E9", "#6366F1"],
889
+ direction: "bottomLeftToTopRight",
890
+ decorations: [
891
+ { type: "glow", x: 0.8, y: 0.15, size: 0.35, color: "#38BDF8", opacity: 0.45 }
892
+ ]
893
+ },
894
+ headline: { fontWeight: "heavy", color: "#FFFFFF", relativeSize: 0.05 },
895
+ subtitle: { text: "Momentum", fontWeight: "medium", color: "#BAE6FD", relativeSize: 0.024 },
896
+ deviceFrame: {
897
+ style: "iPhone",
898
+ bezelColor: "#1E1B4B",
899
+ cornerRadius: 55,
900
+ bezelWidth: 12,
901
+ screenRelativeWidth: 0.62,
902
+ verticalOffset: 0.3,
903
+ horizontalOffset: 0.22,
904
+ rotation: -6,
905
+ tilt: 10,
906
+ shadow: { color: "#00000059", blur: 0.05, offsetX: 0.02, offsetY: 0.02 }
907
+ },
908
+ screenshots: [
909
+ { id: "01", file: "screenshots/01.png", headlines: { en: "Momentum you\ncan actually see", de: "Schwung, den man\nwirklich sieht", tr: "Ger\xE7ekten g\xF6rebilece\u011Fin\nbir ivme" } },
910
+ { id: "02", file: "screenshots/02.png", headlines: { en: "Built for\nforward motion", de: "Gebaut f\xFCr\nVorw\xE4rtsdrang", tr: "\u0130leri hareket i\xE7in\ntasarland\u0131" } }
911
+ ]
912
+ }
913
+ },
914
+ {
915
+ id: "spectrum",
916
+ name: "Spectrum",
917
+ description: "Each screenshot gets its own background color story.",
918
+ swatch: "linear-gradient(90deg, #7C3AED, #DB2777, #0891B2)",
919
+ config: {
920
+ appName: "Spectrum",
921
+ outputPrefix: "spectrum",
922
+ languages: ["en", "de", "tr"],
923
+ outputSizes: STANDARD_SIZES,
924
+ background: { type: "gradient", colors: ["#7C3AED", "#DB2777"], direction: "topToBottom" },
925
+ headline: { fontWeight: "bold", color: "#FFFFFF", relativeSize: 0.05 },
926
+ subtitle: { text: "Spectrum", fontWeight: "medium", color: "#FBCFE8", relativeSize: 0.024 },
927
+ deviceFrame: {
928
+ style: "iPhone",
929
+ bezelColor: "#3B0764",
930
+ cornerRadius: 55,
931
+ bezelWidth: 12,
932
+ screenRelativeWidth: 0.72,
933
+ verticalOffset: 0.26,
934
+ shadow: {}
935
+ },
936
+ screenshots: [
937
+ { id: "01", file: "screenshots/01.png", headlines: { en: "A color for\nevery mood", de: "Eine Farbe f\xFCr\njede Stimmung", tr: "Her ruh haline\nbir renk" } },
938
+ {
939
+ id: "02",
940
+ file: "screenshots/02.png",
941
+ headlines: { en: "Switch scenes,\nkeep the flow", de: "Szenen wechseln,\nim Fluss bleiben", tr: "Sahne de\u011Fi\u015Ftir,\nak\u0131\u015F\u0131 koru" },
942
+ background: {
943
+ type: "gradient",
944
+ colors: ["#0891B2", "#2563EB"],
945
+ direction: "topToBottom",
946
+ decorations: [
947
+ { type: "glow", x: 0.5, y: 0.2, size: 0.35, color: "#67E8F9", opacity: 0.4 }
948
+ ]
949
+ }
950
+ }
951
+ ]
952
+ }
953
+ }
954
+ ];
955
+ function getTemplate(id) {
956
+ return TEMPLATES.find((t) => t.id === id);
957
+ }
958
+
959
+ // src/themes.ts
960
+ var THEMES = [
961
+ {
962
+ id: "indigo",
963
+ name: "Indigo Night",
964
+ swatch: "linear-gradient(135deg, #1E1B4B, #312E81)",
965
+ backgroundColors: ["#1E1B4B", "#312E81"],
966
+ headlineColor: "#EEF2FF",
967
+ subtitleColor: "#A5B4FC",
968
+ bezelColor: "#171532",
969
+ accent: "#818CF8"
970
+ },
971
+ {
972
+ id: "emerald",
973
+ name: "Emerald",
974
+ swatch: "linear-gradient(135deg, #064E3B, #059669)",
975
+ backgroundColors: ["#064E3B", "#059669"],
976
+ headlineColor: "#ECFDF5",
977
+ subtitleColor: "#6EE7B7",
978
+ bezelColor: "#022C22",
979
+ accent: "#34D399"
980
+ },
981
+ {
982
+ id: "coral",
983
+ name: "Coral Pop",
984
+ swatch: "linear-gradient(135deg, #9F1239, #FB7185)",
985
+ backgroundColors: ["#9F1239", "#FB7185"],
986
+ headlineColor: "#FFF1F2",
987
+ subtitleColor: "#FECDD3",
988
+ bezelColor: "#4C0519",
989
+ accent: "#FDA4AF"
990
+ },
991
+ {
992
+ id: "violet",
993
+ name: "Neon Violet",
994
+ swatch: "linear-gradient(135deg, #0F0A1F, #2E1065)",
995
+ backgroundColors: ["#0F0A1F", "#2E1065"],
996
+ headlineColor: "#F5F3FF",
997
+ subtitleColor: "#C4B5FD",
998
+ bezelColor: "#1E1145",
999
+ accent: "#A78BFA"
1000
+ },
1001
+ {
1002
+ id: "sand",
1003
+ name: "Warm Sand",
1004
+ swatch: "linear-gradient(135deg, #FAF5EF, #EAD9C2)",
1005
+ backgroundColors: ["#FAF5EF", "#EAD9C2"],
1006
+ headlineColor: "#292524",
1007
+ subtitleColor: "#B45309",
1008
+ bezelColor: "#292524",
1009
+ accent: "#D6A662"
1010
+ },
1011
+ {
1012
+ id: "slate",
1013
+ name: "Slate Mono",
1014
+ swatch: "linear-gradient(135deg, #F8FAFC, #E2E8F0)",
1015
+ backgroundColors: ["#F8FAFC", "#E2E8F0"],
1016
+ headlineColor: "#0F172A",
1017
+ subtitleColor: "#64748B",
1018
+ bezelColor: "#0F172A",
1019
+ accent: "#94A3B8"
1020
+ }
1021
+ ];
1022
+ function getTheme(id) {
1023
+ return THEMES.find((t) => t.id === id);
1024
+ }
1025
+ function themeBackground(bg, theme) {
1026
+ return {
1027
+ ...bg,
1028
+ colors: bg.type === "solid" ? [theme.backgroundColors[0]] : [...theme.backgroundColors],
1029
+ decorations: bg.decorations?.map((d) => ({ ...d, color: theme.accent }))
1030
+ };
1031
+ }
1032
+ function applyTheme(config, theme) {
1033
+ return {
1034
+ ...config,
1035
+ background: themeBackground(config.background, theme),
1036
+ headline: { ...config.headline, color: theme.headlineColor },
1037
+ subtitle: config.subtitle ? { ...config.subtitle, color: theme.subtitleColor } : void 0,
1038
+ deviceFrame: { ...config.deviceFrame, bezelColor: theme.bezelColor },
1039
+ screenshots: config.screenshots.map(
1040
+ (s) => s.background ? { ...s, background: themeBackground(s.background, theme) } : s
1041
+ )
1042
+ };
1043
+ }
1044
+ export {
1045
+ DEFAULT_FONT_FAMILY,
1046
+ FALLBACK_FONT_STACK,
1047
+ STANDARD_OUTPUT_SIZES,
1048
+ TEMPLATES,
1049
+ THEMES,
1050
+ applyTheme,
1051
+ backgroundSchema,
1052
+ createStarterConfig,
1053
+ cssFont,
1054
+ cssFontWeight,
1055
+ decorationSchema,
1056
+ deviceFrameRect,
1057
+ deviceFrameSchema,
1058
+ drawBackground,
1059
+ drawDecorations,
1060
+ drawDeviceFrame,
1061
+ drawHeadline,
1062
+ drawSubtitle,
1063
+ fontWeightSchema,
1064
+ getTemplate,
1065
+ getTheme,
1066
+ headlineSchema,
1067
+ mockupConfigSchema,
1068
+ outputSizeSchema,
1069
+ parseConfig,
1070
+ parseHexColor,
1071
+ parseHexColorWithAlpha,
1072
+ renderMockup,
1073
+ resolveScene,
1074
+ screenshotEntrySchema,
1075
+ shadowSchema,
1076
+ subtitleSchema,
1077
+ validateHeadlines
1078
+ };
1079
+ //# sourceMappingURL=index.js.map