@spectratools/graphic-designer-cli 0.3.1

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/qa.js ADDED
@@ -0,0 +1,901 @@
1
+ // src/qa.ts
2
+ import { readFile } from "fs/promises";
3
+ import { resolve } from "path";
4
+ import sharp from "sharp";
5
+
6
+ // src/code-style.ts
7
+ var CARBON_SURROUND_COLOR = "rgba(171, 184, 195, 1)";
8
+ var DEFAULT_STYLE = {
9
+ paddingVertical: 56,
10
+ paddingHorizontal: 56,
11
+ windowControls: "macos",
12
+ dropShadow: true,
13
+ dropShadowOffsetY: 20,
14
+ dropShadowBlurRadius: 68,
15
+ surroundColor: CARBON_SURROUND_COLOR,
16
+ fontSize: 14,
17
+ lineHeightPercent: 143,
18
+ scale: 2
19
+ };
20
+ function normalizeScale(scale) {
21
+ if (scale === 1 || scale === 2 || scale === 4) {
22
+ return scale;
23
+ }
24
+ return DEFAULT_STYLE.scale;
25
+ }
26
+ function resolveCodeBlockStyle(style) {
27
+ return {
28
+ paddingVertical: style?.paddingVertical ?? DEFAULT_STYLE.paddingVertical,
29
+ paddingHorizontal: style?.paddingHorizontal ?? DEFAULT_STYLE.paddingHorizontal,
30
+ windowControls: style?.windowControls ?? DEFAULT_STYLE.windowControls,
31
+ dropShadow: style?.dropShadow ?? DEFAULT_STYLE.dropShadow,
32
+ dropShadowOffsetY: style?.dropShadowOffsetY ?? DEFAULT_STYLE.dropShadowOffsetY,
33
+ dropShadowBlurRadius: style?.dropShadowBlurRadius ?? DEFAULT_STYLE.dropShadowBlurRadius,
34
+ surroundColor: style?.surroundColor ?? DEFAULT_STYLE.surroundColor,
35
+ fontSize: style?.fontSize ?? DEFAULT_STYLE.fontSize,
36
+ lineHeightPercent: style?.lineHeightPercent ?? DEFAULT_STYLE.lineHeightPercent,
37
+ scale: normalizeScale(style?.scale)
38
+ };
39
+ }
40
+ function resolveElementScale(element) {
41
+ if (element.type !== "code-block" && element.type !== "terminal") {
42
+ return null;
43
+ }
44
+ return resolveCodeBlockStyle(element.style).scale;
45
+ }
46
+ function resolveRenderScale(spec) {
47
+ let scale = 1;
48
+ for (const element of spec.elements) {
49
+ const elementScale = resolveElementScale(element);
50
+ if (elementScale === null) {
51
+ continue;
52
+ }
53
+ if (elementScale > scale) {
54
+ scale = elementScale;
55
+ }
56
+ }
57
+ return scale;
58
+ }
59
+
60
+ // src/spec.schema.ts
61
+ import { z as z2 } from "zod";
62
+
63
+ // src/themes/builtin.ts
64
+ import { z } from "zod";
65
+ var colorHexSchema = z.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
66
+ var fontFamilySchema = z.string().min(1).max(120);
67
+ var codeThemeSchema = z.object({
68
+ background: colorHexSchema,
69
+ text: colorHexSchema,
70
+ comment: colorHexSchema,
71
+ keyword: colorHexSchema,
72
+ string: colorHexSchema,
73
+ number: colorHexSchema,
74
+ function: colorHexSchema,
75
+ variable: colorHexSchema,
76
+ operator: colorHexSchema,
77
+ punctuation: colorHexSchema
78
+ }).strict();
79
+ var themeSchema = z.object({
80
+ background: colorHexSchema,
81
+ surface: colorHexSchema,
82
+ surfaceMuted: colorHexSchema,
83
+ surfaceElevated: colorHexSchema,
84
+ text: colorHexSchema,
85
+ textMuted: colorHexSchema,
86
+ textInverse: colorHexSchema,
87
+ primary: colorHexSchema,
88
+ secondary: colorHexSchema,
89
+ accent: colorHexSchema,
90
+ success: colorHexSchema,
91
+ warning: colorHexSchema,
92
+ error: colorHexSchema,
93
+ info: colorHexSchema,
94
+ border: colorHexSchema,
95
+ borderMuted: colorHexSchema,
96
+ code: codeThemeSchema,
97
+ fonts: z.object({
98
+ heading: fontFamilySchema,
99
+ body: fontFamilySchema,
100
+ mono: fontFamilySchema
101
+ }).strict()
102
+ }).strict();
103
+ var builtInThemeSchema = z.enum([
104
+ "dark",
105
+ "light",
106
+ "dracula",
107
+ "github-dark",
108
+ "one-dark",
109
+ "nord"
110
+ ]);
111
+ var baseDarkTheme = {
112
+ background: "#0B1020",
113
+ surface: "#111936",
114
+ surfaceMuted: "#1A2547",
115
+ surfaceElevated: "#202D55",
116
+ text: "#E8EEFF",
117
+ textMuted: "#AAB9E8",
118
+ textInverse: "#0B1020",
119
+ primary: "#7AA2FF",
120
+ secondary: "#65E4A3",
121
+ accent: "#65E4A3",
122
+ success: "#2FCB7E",
123
+ warning: "#F4B860",
124
+ error: "#F97070",
125
+ info: "#60A5FA",
126
+ border: "#32426E",
127
+ borderMuted: "#24345F",
128
+ code: {
129
+ background: "#0F172A",
130
+ text: "#E2E8F0",
131
+ comment: "#64748B",
132
+ keyword: "#C084FC",
133
+ string: "#86EFAC",
134
+ number: "#FCA5A5",
135
+ function: "#93C5FD",
136
+ variable: "#E2E8F0",
137
+ operator: "#F8FAFC",
138
+ punctuation: "#CBD5E1"
139
+ },
140
+ fonts: {
141
+ heading: "Space Grotesk",
142
+ body: "Inter",
143
+ mono: "JetBrains Mono"
144
+ }
145
+ };
146
+ var builtInThemes = {
147
+ dark: baseDarkTheme,
148
+ light: {
149
+ ...baseDarkTheme,
150
+ background: "#F8FAFC",
151
+ surface: "#FFFFFF",
152
+ surfaceMuted: "#EEF2FF",
153
+ surfaceElevated: "#FFFFFF",
154
+ text: "#0F172A",
155
+ textMuted: "#334155",
156
+ textInverse: "#F8FAFC",
157
+ border: "#CBD5E1",
158
+ borderMuted: "#E2E8F0",
159
+ code: {
160
+ ...baseDarkTheme.code,
161
+ background: "#F1F5F9",
162
+ text: "#0F172A",
163
+ variable: "#1E293B",
164
+ punctuation: "#334155",
165
+ operator: "#0F172A"
166
+ }
167
+ },
168
+ dracula: {
169
+ ...baseDarkTheme,
170
+ background: "#282A36",
171
+ surface: "#303247",
172
+ surfaceMuted: "#3A3D55",
173
+ surfaceElevated: "#44475A",
174
+ text: "#F8F8F2",
175
+ textMuted: "#BD93F9",
176
+ primary: "#8BE9FD",
177
+ accent: "#50FA7B",
178
+ secondary: "#FFB86C",
179
+ success: "#50FA7B",
180
+ warning: "#FFB86C",
181
+ error: "#FF5555",
182
+ info: "#8BE9FD",
183
+ border: "#44475A",
184
+ borderMuted: "#3A3D55"
185
+ },
186
+ "github-dark": {
187
+ ...baseDarkTheme,
188
+ background: "#0D1117",
189
+ surface: "#161B22",
190
+ surfaceMuted: "#1F2632",
191
+ surfaceElevated: "#21262D",
192
+ text: "#E6EDF3",
193
+ textMuted: "#8B949E",
194
+ primary: "#58A6FF",
195
+ accent: "#3FB950",
196
+ secondary: "#A5D6FF",
197
+ border: "#30363D",
198
+ borderMuted: "#21262D"
199
+ },
200
+ "one-dark": {
201
+ ...baseDarkTheme,
202
+ background: "#282C34",
203
+ surface: "#2F343F",
204
+ surfaceMuted: "#3A404C",
205
+ surfaceElevated: "#434A59",
206
+ text: "#ABB2BF",
207
+ textMuted: "#7F848E",
208
+ primary: "#61AFEF",
209
+ accent: "#98C379",
210
+ secondary: "#E5C07B",
211
+ warning: "#E5C07B",
212
+ error: "#E06C75",
213
+ border: "#4B5263",
214
+ borderMuted: "#3A404C"
215
+ },
216
+ nord: {
217
+ ...baseDarkTheme,
218
+ background: "#2E3440",
219
+ surface: "#3B4252",
220
+ surfaceMuted: "#434C5E",
221
+ surfaceElevated: "#4C566A",
222
+ text: "#ECEFF4",
223
+ textMuted: "#D8DEE9",
224
+ primary: "#88C0D0",
225
+ accent: "#A3BE8C",
226
+ secondary: "#81A1C1",
227
+ success: "#A3BE8C",
228
+ warning: "#EBCB8B",
229
+ error: "#BF616A",
230
+ info: "#5E81AC",
231
+ border: "#4C566A",
232
+ borderMuted: "#434C5E"
233
+ }
234
+ };
235
+ var defaultTheme = builtInThemes.dark;
236
+
237
+ // src/spec.schema.ts
238
+ var colorHexSchema2 = z2.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
239
+ var gradientStopSchema = z2.object({
240
+ offset: z2.number().min(0).max(1),
241
+ color: colorHexSchema2
242
+ }).strict();
243
+ var linearGradientSchema = z2.object({
244
+ type: z2.literal("linear"),
245
+ angle: z2.number().default(180),
246
+ stops: z2.array(gradientStopSchema).min(2)
247
+ }).strict();
248
+ var radialGradientSchema = z2.object({
249
+ type: z2.literal("radial"),
250
+ stops: z2.array(gradientStopSchema).min(2)
251
+ }).strict();
252
+ var gradientSchema = z2.discriminatedUnion("type", [linearGradientSchema, radialGradientSchema]);
253
+ var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
254
+ var drawRectSchema = z2.object({
255
+ type: z2.literal("rect"),
256
+ x: z2.number(),
257
+ y: z2.number(),
258
+ width: z2.number().positive(),
259
+ height: z2.number().positive(),
260
+ fill: colorHexSchema2.optional(),
261
+ stroke: colorHexSchema2.optional(),
262
+ strokeWidth: z2.number().min(0).max(32).default(0),
263
+ radius: z2.number().min(0).max(256).default(0),
264
+ opacity: z2.number().min(0).max(1).default(1)
265
+ }).strict();
266
+ var drawCircleSchema = z2.object({
267
+ type: z2.literal("circle"),
268
+ cx: z2.number(),
269
+ cy: z2.number(),
270
+ radius: z2.number().positive(),
271
+ fill: colorHexSchema2.optional(),
272
+ stroke: colorHexSchema2.optional(),
273
+ strokeWidth: z2.number().min(0).max(32).default(0),
274
+ opacity: z2.number().min(0).max(1).default(1)
275
+ }).strict();
276
+ var drawTextSchema = z2.object({
277
+ type: z2.literal("text"),
278
+ x: z2.number(),
279
+ y: z2.number(),
280
+ text: z2.string().min(1).max(500),
281
+ fontSize: z2.number().min(6).max(200).default(16),
282
+ fontWeight: z2.number().int().min(100).max(900).default(400),
283
+ fontFamily: drawFontFamilySchema.default("body"),
284
+ color: colorHexSchema2.default("#FFFFFF"),
285
+ align: z2.enum(["left", "center", "right"]).default("left"),
286
+ baseline: z2.enum(["top", "middle", "alphabetic", "bottom"]).default("alphabetic"),
287
+ letterSpacing: z2.number().min(-10).max(50).default(0),
288
+ maxWidth: z2.number().positive().optional(),
289
+ opacity: z2.number().min(0).max(1).default(1)
290
+ }).strict();
291
+ var drawLineSchema = z2.object({
292
+ type: z2.literal("line"),
293
+ x1: z2.number(),
294
+ y1: z2.number(),
295
+ x2: z2.number(),
296
+ y2: z2.number(),
297
+ color: colorHexSchema2.default("#FFFFFF"),
298
+ width: z2.number().min(0.5).max(32).default(2),
299
+ dash: z2.array(z2.number()).max(6).optional(),
300
+ arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
301
+ arrowSize: z2.number().min(4).max(32).default(10),
302
+ opacity: z2.number().min(0).max(1).default(1)
303
+ }).strict();
304
+ var drawPointSchema = z2.object({
305
+ x: z2.number(),
306
+ y: z2.number()
307
+ }).strict();
308
+ var drawBezierSchema = z2.object({
309
+ type: z2.literal("bezier"),
310
+ points: z2.array(drawPointSchema).min(2).max(20),
311
+ color: colorHexSchema2.default("#FFFFFF"),
312
+ width: z2.number().min(0.5).max(32).default(2),
313
+ dash: z2.array(z2.number()).max(6).optional(),
314
+ arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
315
+ arrowSize: z2.number().min(4).max(32).default(10),
316
+ opacity: z2.number().min(0).max(1).default(1)
317
+ }).strict();
318
+ var drawPathSchema = z2.object({
319
+ type: z2.literal("path"),
320
+ d: z2.string().min(1).max(4e3),
321
+ fill: colorHexSchema2.optional(),
322
+ stroke: colorHexSchema2.optional(),
323
+ strokeWidth: z2.number().min(0).max(32).default(0),
324
+ opacity: z2.number().min(0).max(1).default(1)
325
+ }).strict();
326
+ var drawBadgeSchema = z2.object({
327
+ type: z2.literal("badge"),
328
+ x: z2.number(),
329
+ y: z2.number(),
330
+ text: z2.string().min(1).max(64),
331
+ fontSize: z2.number().min(6).max(48).default(12),
332
+ fontFamily: drawFontFamilySchema.default("mono"),
333
+ color: colorHexSchema2.default("#FFFFFF"),
334
+ background: colorHexSchema2.default("#334B83"),
335
+ paddingX: z2.number().min(0).max(64).default(10),
336
+ paddingY: z2.number().min(0).max(32).default(4),
337
+ borderRadius: z2.number().min(0).max(64).default(12),
338
+ opacity: z2.number().min(0).max(1).default(1)
339
+ }).strict();
340
+ var drawGradientRectSchema = z2.object({
341
+ type: z2.literal("gradient-rect"),
342
+ x: z2.number(),
343
+ y: z2.number(),
344
+ width: z2.number().positive(),
345
+ height: z2.number().positive(),
346
+ gradient: gradientSchema,
347
+ radius: z2.number().min(0).max(256).default(0),
348
+ opacity: z2.number().min(0).max(1).default(1)
349
+ }).strict();
350
+ var drawCommandSchema = z2.discriminatedUnion("type", [
351
+ drawRectSchema,
352
+ drawCircleSchema,
353
+ drawTextSchema,
354
+ drawLineSchema,
355
+ drawBezierSchema,
356
+ drawPathSchema,
357
+ drawBadgeSchema,
358
+ drawGradientRectSchema
359
+ ]);
360
+ var defaultCanvas = {
361
+ width: 1200,
362
+ height: 675,
363
+ padding: 48
364
+ };
365
+ var defaultConstraints = {
366
+ minContrastRatio: 4.5,
367
+ minFooterSpacing: 16,
368
+ checkOverlaps: true,
369
+ maxTextTruncation: 0.1
370
+ };
371
+ var defaultAutoLayout = {
372
+ mode: "auto",
373
+ algorithm: "layered",
374
+ direction: "TB",
375
+ nodeSpacing: 80,
376
+ rankSpacing: 120,
377
+ edgeRouting: "polyline"
378
+ };
379
+ var defaultGridLayout = {
380
+ mode: "grid",
381
+ columns: 3,
382
+ gap: 24,
383
+ equalHeight: false
384
+ };
385
+ var defaultStackLayout = {
386
+ mode: "stack",
387
+ direction: "vertical",
388
+ gap: 24,
389
+ alignment: "stretch"
390
+ };
391
+ function inferLayout(elements, explicitLayout) {
392
+ if (explicitLayout) {
393
+ return explicitLayout;
394
+ }
395
+ const hasFlowNodes = elements.some((element) => element.type === "flow-node");
396
+ const hasConnections = elements.some((element) => element.type === "connection");
397
+ const hasOnlyCards = elements.every((element) => element.type === "card");
398
+ const hasCodeOrTerminal = elements.some(
399
+ (element) => element.type === "code-block" || element.type === "terminal"
400
+ );
401
+ if (hasFlowNodes && hasConnections) {
402
+ return defaultAutoLayout;
403
+ }
404
+ if (hasOnlyCards) {
405
+ return defaultGridLayout;
406
+ }
407
+ if (hasCodeOrTerminal) {
408
+ return defaultStackLayout;
409
+ }
410
+ return defaultGridLayout;
411
+ }
412
+ var cardElementSchema = z2.object({
413
+ type: z2.literal("card"),
414
+ id: z2.string().min(1).max(120),
415
+ title: z2.string().min(1).max(200),
416
+ body: z2.string().min(1).max(4e3),
417
+ badge: z2.string().min(1).max(64).optional(),
418
+ metric: z2.string().min(1).max(80).optional(),
419
+ tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
420
+ icon: z2.string().min(1).max(64).optional()
421
+ }).strict();
422
+ var flowNodeElementSchema = z2.object({
423
+ type: z2.literal("flow-node"),
424
+ id: z2.string().min(1).max(120),
425
+ shape: z2.enum(["box", "rounded-box", "diamond", "circle", "pill", "cylinder", "parallelogram"]),
426
+ label: z2.string().min(1).max(200),
427
+ sublabel: z2.string().min(1).max(300).optional(),
428
+ sublabelColor: colorHexSchema2.optional(),
429
+ labelColor: colorHexSchema2.optional(),
430
+ labelFontSize: z2.number().min(10).max(48).optional(),
431
+ color: colorHexSchema2.optional(),
432
+ borderColor: colorHexSchema2.optional(),
433
+ borderWidth: z2.number().min(0.5).max(8).optional(),
434
+ cornerRadius: z2.number().min(0).max(64).optional(),
435
+ width: z2.number().int().min(40).max(800).optional(),
436
+ height: z2.number().int().min(30).max(600).optional(),
437
+ opacity: z2.number().min(0).max(1).default(1)
438
+ }).strict();
439
+ var connectionElementSchema = z2.object({
440
+ type: z2.literal("connection"),
441
+ from: z2.string().min(1).max(120),
442
+ to: z2.string().min(1).max(120),
443
+ style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
444
+ arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
445
+ label: z2.string().min(1).max(200).optional(),
446
+ labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
447
+ color: colorHexSchema2.optional(),
448
+ width: z2.number().min(0.5).max(8).optional(),
449
+ arrowSize: z2.number().min(4).max(32).optional(),
450
+ opacity: z2.number().min(0).max(1).default(1)
451
+ }).strict();
452
+ var codeBlockStyleSchema = z2.object({
453
+ paddingVertical: z2.number().min(0).max(128).default(56),
454
+ paddingHorizontal: z2.number().min(0).max(128).default(56),
455
+ windowControls: z2.enum(["macos", "bw", "none"]).default("macos"),
456
+ dropShadow: z2.boolean().default(true),
457
+ dropShadowOffsetY: z2.number().min(0).max(100).default(20),
458
+ dropShadowBlurRadius: z2.number().min(0).max(200).default(68),
459
+ surroundColor: z2.string().optional(),
460
+ fontSize: z2.number().min(8).max(32).default(14),
461
+ lineHeightPercent: z2.number().min(100).max(200).default(143),
462
+ scale: z2.number().int().min(1).max(4).default(2)
463
+ }).partial();
464
+ var codeBlockElementSchema = z2.object({
465
+ type: z2.literal("code-block"),
466
+ id: z2.string().min(1).max(120),
467
+ code: z2.string().min(1),
468
+ language: z2.string().min(1).max(40),
469
+ theme: z2.string().min(1).max(80).optional(),
470
+ showLineNumbers: z2.boolean().default(false),
471
+ highlightLines: z2.array(z2.number().int().positive()).max(500).optional(),
472
+ startLine: z2.number().int().positive().default(1),
473
+ title: z2.string().min(1).max(200).optional(),
474
+ style: codeBlockStyleSchema.optional()
475
+ }).strict();
476
+ var terminalElementSchema = z2.object({
477
+ type: z2.literal("terminal"),
478
+ id: z2.string().min(1).max(120),
479
+ content: z2.string().min(1),
480
+ prompt: z2.string().min(1).max(24).optional(),
481
+ title: z2.string().min(1).max(200).optional(),
482
+ showPrompt: z2.boolean().default(true),
483
+ style: codeBlockStyleSchema.optional()
484
+ }).strict();
485
+ var textElementSchema = z2.object({
486
+ type: z2.literal("text"),
487
+ id: z2.string().min(1).max(120),
488
+ content: z2.string().min(1).max(4e3),
489
+ style: z2.enum(["heading", "subheading", "body", "caption", "code"]),
490
+ align: z2.enum(["left", "center", "right"]).default("left"),
491
+ color: colorHexSchema2.optional()
492
+ }).strict();
493
+ var shapeElementSchema = z2.object({
494
+ type: z2.literal("shape"),
495
+ id: z2.string().min(1).max(120),
496
+ shape: z2.enum(["rectangle", "rounded-rectangle", "circle", "ellipse", "line", "arrow"]),
497
+ fill: colorHexSchema2.optional(),
498
+ stroke: colorHexSchema2.optional(),
499
+ strokeWidth: z2.number().min(0).max(64).default(1)
500
+ }).strict();
501
+ var imageElementSchema = z2.object({
502
+ type: z2.literal("image"),
503
+ id: z2.string().min(1).max(120),
504
+ src: z2.string().min(1),
505
+ alt: z2.string().max(240).optional(),
506
+ fit: z2.enum(["contain", "cover", "fill", "none"]).default("contain"),
507
+ borderRadius: z2.number().min(0).default(0)
508
+ }).strict();
509
+ var elementSchema = z2.discriminatedUnion("type", [
510
+ cardElementSchema,
511
+ flowNodeElementSchema,
512
+ connectionElementSchema,
513
+ codeBlockElementSchema,
514
+ terminalElementSchema,
515
+ textElementSchema,
516
+ shapeElementSchema,
517
+ imageElementSchema
518
+ ]);
519
+ var autoLayoutConfigSchema = z2.object({
520
+ mode: z2.literal("auto"),
521
+ algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
522
+ direction: z2.enum(["TB", "BT", "LR", "RL"]).default("TB"),
523
+ nodeSpacing: z2.number().int().min(0).max(512).default(80),
524
+ rankSpacing: z2.number().int().min(0).max(512).default(120),
525
+ edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
526
+ aspectRatio: z2.number().min(0.5).max(3).optional()
527
+ }).strict();
528
+ var gridLayoutConfigSchema = z2.object({
529
+ mode: z2.literal("grid"),
530
+ columns: z2.number().int().min(1).max(12).default(3),
531
+ gap: z2.number().int().min(0).max(256).default(24),
532
+ cardMinHeight: z2.number().int().min(32).max(4096).optional(),
533
+ cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
534
+ equalHeight: z2.boolean().default(false)
535
+ }).strict();
536
+ var stackLayoutConfigSchema = z2.object({
537
+ mode: z2.literal("stack"),
538
+ direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
539
+ gap: z2.number().int().min(0).max(256).default(24),
540
+ alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
541
+ }).strict();
542
+ var manualPositionSchema = z2.object({
543
+ x: z2.number().int(),
544
+ y: z2.number().int(),
545
+ width: z2.number().int().positive().optional(),
546
+ height: z2.number().int().positive().optional()
547
+ }).strict();
548
+ var manualLayoutConfigSchema = z2.object({
549
+ mode: z2.literal("manual"),
550
+ positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
551
+ }).strict();
552
+ var layoutConfigSchema = z2.discriminatedUnion("mode", [
553
+ autoLayoutConfigSchema,
554
+ gridLayoutConfigSchema,
555
+ stackLayoutConfigSchema,
556
+ manualLayoutConfigSchema
557
+ ]);
558
+ var constraintsSchema = z2.object({
559
+ minContrastRatio: z2.number().min(3).max(21).default(4.5),
560
+ minFooterSpacing: z2.number().int().min(0).max(256).default(16),
561
+ checkOverlaps: z2.boolean().default(true),
562
+ maxTextTruncation: z2.number().min(0).max(1).default(0.1)
563
+ }).strict();
564
+ var headerSchema = z2.object({
565
+ eyebrow: z2.string().min(1).max(120).optional(),
566
+ title: z2.string().min(1).max(300),
567
+ subtitle: z2.string().min(1).max(400).optional(),
568
+ align: z2.enum(["left", "center", "right"]).default("center"),
569
+ titleLetterSpacing: z2.number().min(-2).max(20).default(0),
570
+ titleFontSize: z2.number().min(16).max(96).optional()
571
+ }).strict();
572
+ var footerSchema = z2.object({
573
+ text: z2.string().min(1).max(300),
574
+ tagline: z2.string().min(1).max(200).optional()
575
+ }).strict();
576
+ var decoratorSchema = z2.discriminatedUnion("type", [
577
+ z2.object({
578
+ type: z2.literal("rainbow-rule"),
579
+ y: z2.enum(["after-header", "before-footer", "custom"]).default("after-header"),
580
+ customY: z2.number().optional(),
581
+ thickness: z2.number().positive().max(64).default(2),
582
+ colors: z2.array(colorHexSchema2).min(2).optional(),
583
+ margin: z2.number().min(0).max(512).default(16)
584
+ }).strict(),
585
+ z2.object({
586
+ type: z2.literal("vignette"),
587
+ intensity: z2.number().min(0).max(1).default(0.3),
588
+ color: colorHexSchema2.default("#000000")
589
+ }).strict(),
590
+ z2.object({
591
+ type: z2.literal("gradient-overlay"),
592
+ gradient: gradientSchema,
593
+ opacity: z2.number().min(0).max(1).default(0.5)
594
+ }).strict()
595
+ ]);
596
+ var canvasSchema = z2.object({
597
+ width: z2.number().int().min(320).max(4096).default(defaultCanvas.width),
598
+ height: z2.number().int().min(180).max(4096).default(defaultCanvas.height),
599
+ padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
600
+ }).strict();
601
+ var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
602
+ var designSpecSchema = z2.object({
603
+ version: z2.literal(2).default(2),
604
+ canvas: canvasSchema.default(defaultCanvas),
605
+ theme: themeInputSchema.default("dark"),
606
+ background: z2.union([colorHexSchema2, gradientSchema]).optional(),
607
+ header: headerSchema.optional(),
608
+ elements: z2.array(elementSchema).default([]),
609
+ footer: footerSchema.optional(),
610
+ decorators: z2.array(decoratorSchema).default([]),
611
+ draw: z2.array(drawCommandSchema).max(200).default([]),
612
+ layout: layoutConfigSchema.optional(),
613
+ constraints: constraintsSchema.default(defaultConstraints)
614
+ }).strict().transform((spec) => ({
615
+ ...spec,
616
+ layout: inferLayout(spec.elements, spec.layout)
617
+ }));
618
+ function deriveSafeFrame(spec) {
619
+ return {
620
+ x: spec.canvas.padding,
621
+ y: spec.canvas.padding,
622
+ width: spec.canvas.width - spec.canvas.padding * 2,
623
+ height: spec.canvas.height - spec.canvas.padding * 2
624
+ };
625
+ }
626
+ function parseDesignSpec(input) {
627
+ return designSpecSchema.parse(input);
628
+ }
629
+
630
+ // src/utils/color.ts
631
+ function parseChannel(hex, offset) {
632
+ return Number.parseInt(hex.slice(offset, offset + 2), 16);
633
+ }
634
+ function parseHexColor(hexColor) {
635
+ const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
636
+ if (normalized.length !== 6 && normalized.length !== 8) {
637
+ throw new Error(`Unsupported color format: ${hexColor}`);
638
+ }
639
+ return {
640
+ r: parseChannel(normalized, 0),
641
+ g: parseChannel(normalized, 2),
642
+ b: parseChannel(normalized, 4)
643
+ };
644
+ }
645
+ function srgbToLinear(channel) {
646
+ const normalized = channel / 255;
647
+ if (normalized <= 0.03928) {
648
+ return normalized / 12.92;
649
+ }
650
+ return ((normalized + 0.055) / 1.055) ** 2.4;
651
+ }
652
+ function relativeLuminance(hexColor) {
653
+ const rgb = parseHexColor(hexColor);
654
+ const r = srgbToLinear(rgb.r);
655
+ const g = srgbToLinear(rgb.g);
656
+ const b = srgbToLinear(rgb.b);
657
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
658
+ }
659
+ function contrastRatio(foreground, background) {
660
+ const fg = relativeLuminance(foreground);
661
+ const bg = relativeLuminance(background);
662
+ const lighter = Math.max(fg, bg);
663
+ const darker = Math.min(fg, bg);
664
+ return (lighter + 0.05) / (darker + 0.05);
665
+ }
666
+
667
+ // src/qa.ts
668
+ function rectWithin(outer, inner) {
669
+ return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
670
+ }
671
+ function intersects(a, b) {
672
+ return !(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y);
673
+ }
674
+ function canvasRect(spec) {
675
+ return {
676
+ x: 0,
677
+ y: 0,
678
+ width: spec.canvas.width,
679
+ height: spec.canvas.height
680
+ };
681
+ }
682
+ function topLevelElements(elements) {
683
+ return elements.filter((element) => ["header", "card", "footer"].includes(element.kind));
684
+ }
685
+ function overlapCandidates(elements) {
686
+ return elements.filter(
687
+ (element) => [
688
+ "header",
689
+ "card",
690
+ "footer",
691
+ "flow-node",
692
+ "terminal",
693
+ "code-block",
694
+ "shape",
695
+ "image",
696
+ "draw"
697
+ ].includes(element.kind)
698
+ );
699
+ }
700
+ function loadMetadataFromString(raw) {
701
+ return JSON.parse(raw);
702
+ }
703
+ async function readMetadata(path) {
704
+ const raw = await readFile(resolve(path), "utf8");
705
+ return loadMetadataFromString(raw);
706
+ }
707
+ async function runQa(options) {
708
+ const spec = parseDesignSpec(options.spec);
709
+ const imagePath = resolve(options.imagePath);
710
+ const expectedSafeFrame = deriveSafeFrame(spec);
711
+ const expectedCanvas = canvasRect(spec);
712
+ const imageMetadata = await sharp(imagePath).metadata();
713
+ const issues = [];
714
+ const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
715
+ const expectedWidth = spec.canvas.width * expectedScale;
716
+ const expectedHeight = spec.canvas.height * expectedScale;
717
+ if (imageMetadata.width !== expectedWidth || imageMetadata.height !== expectedHeight) {
718
+ issues.push({
719
+ code: "DIMENSIONS_MISMATCH",
720
+ severity: "error",
721
+ message: `Image dimensions ${imageMetadata.width ?? "?"}x${imageMetadata.height ?? "?"} do not match expected ${expectedWidth}x${expectedHeight}.`,
722
+ details: {
723
+ expectedWidth,
724
+ expectedHeight,
725
+ actualWidth: imageMetadata.width ?? -1,
726
+ actualHeight: imageMetadata.height ?? -1
727
+ }
728
+ });
729
+ }
730
+ const layoutElements = options.metadata?.layout.elements;
731
+ if (!layoutElements || layoutElements.length === 0) {
732
+ issues.push({
733
+ code: "MISSING_LAYOUT",
734
+ severity: "warning",
735
+ message: "No layout metadata provided; overlap/clipping checks are partially skipped."
736
+ });
737
+ } else {
738
+ for (const element of layoutElements) {
739
+ if (element.truncated) {
740
+ issues.push({
741
+ code: "TEXT_TRUNCATED",
742
+ severity: "error",
743
+ message: `Text for ${element.id} was truncated during render.`,
744
+ elementId: element.id
745
+ });
746
+ }
747
+ const inCanvas = rectWithin(expectedCanvas, element.bounds);
748
+ const inSafe = rectWithin(expectedSafeFrame, element.bounds);
749
+ const requiresSafeFrameContainment = !element.allowOverlap && element.kind !== "draw";
750
+ if (element.kind === "draw" && !inCanvas) {
751
+ issues.push({
752
+ code: "DRAW_OUT_OF_BOUNDS",
753
+ severity: "warning",
754
+ message: `Draw command ${element.id} extends beyond canvas bounds.`,
755
+ elementId: element.id,
756
+ details: {
757
+ inCanvas,
758
+ x: element.bounds.x,
759
+ y: element.bounds.y,
760
+ width: element.bounds.width,
761
+ height: element.bounds.height
762
+ }
763
+ });
764
+ } else if (!inCanvas || requiresSafeFrameContainment && !inSafe) {
765
+ issues.push({
766
+ code: "ELEMENT_CLIPPED",
767
+ severity: "error",
768
+ message: `Element ${element.id} breaches ${!inCanvas ? "canvas bounds" : "safe frame"}.`,
769
+ elementId: element.id,
770
+ details: {
771
+ inCanvas,
772
+ inSafeFrame: inSafe,
773
+ x: element.bounds.x,
774
+ y: element.bounds.y,
775
+ width: element.bounds.width,
776
+ height: element.bounds.height
777
+ }
778
+ });
779
+ }
780
+ if (element.foregroundColor && element.backgroundColor) {
781
+ const ratio = contrastRatio(element.foregroundColor, element.backgroundColor);
782
+ if (ratio < spec.constraints.minContrastRatio) {
783
+ issues.push({
784
+ code: "LOW_CONTRAST",
785
+ severity: "error",
786
+ message: `Contrast ratio ${ratio.toFixed(2)} for ${element.id} is below threshold ${spec.constraints.minContrastRatio}.`,
787
+ elementId: element.id,
788
+ details: {
789
+ ratio: Number(ratio.toFixed(4)),
790
+ threshold: spec.constraints.minContrastRatio
791
+ }
792
+ });
793
+ }
794
+ }
795
+ }
796
+ if (spec.constraints.checkOverlaps) {
797
+ const blocks = overlapCandidates(layoutElements);
798
+ for (let i = 0; i < blocks.length; i += 1) {
799
+ for (let j = i + 1; j < blocks.length; j += 1) {
800
+ const first = blocks[i];
801
+ const second = blocks[j];
802
+ if (first.allowOverlap || second.allowOverlap) {
803
+ continue;
804
+ }
805
+ if (intersects(first.bounds, second.bounds)) {
806
+ const relaxed = first.kind === "draw" || second.kind === "draw";
807
+ issues.push({
808
+ code: "ELEMENT_OVERLAP",
809
+ severity: relaxed ? "warning" : "error",
810
+ message: `Elements ${first.id} and ${second.id} overlap.${relaxed ? " (Draw overlap is informational.)" : ""}`,
811
+ elementId: `${first.id}|${second.id}`
812
+ });
813
+ }
814
+ }
815
+ }
816
+ }
817
+ if (spec.footer) {
818
+ const footer = layoutElements.find((element) => element.id === "footer");
819
+ const nonFooter = topLevelElements(layoutElements).filter(
820
+ (element) => element.id !== "footer"
821
+ );
822
+ if (footer && nonFooter.length > 0) {
823
+ const highestBottom = Math.max(
824
+ ...nonFooter.map((element) => element.bounds.y + element.bounds.height)
825
+ );
826
+ const spacing = footer.bounds.y - highestBottom;
827
+ if (spacing < spec.constraints.minFooterSpacing) {
828
+ issues.push({
829
+ code: "FOOTER_SPACING",
830
+ severity: "error",
831
+ message: `Footer spacing ${spacing}px is below minimum ${spec.constraints.minFooterSpacing}px.`,
832
+ elementId: "footer",
833
+ details: {
834
+ spacing,
835
+ min: spec.constraints.minFooterSpacing
836
+ }
837
+ });
838
+ }
839
+ }
840
+ }
841
+ if (spec.header) {
842
+ const header = layoutElements.find((element) => element.id === "header");
843
+ if (header?.foregroundColor && header.backgroundColor) {
844
+ const ratio = contrastRatio(header.foregroundColor, header.backgroundColor);
845
+ if (ratio < spec.constraints.minContrastRatio) {
846
+ issues.push({
847
+ code: "LOW_CONTRAST",
848
+ severity: "error",
849
+ message: `Header contrast ratio ${ratio.toFixed(2)} is below threshold ${spec.constraints.minContrastRatio}.`,
850
+ elementId: "header"
851
+ });
852
+ }
853
+ }
854
+ }
855
+ if (!spec.footer && layoutElements.some((element) => element.id === "footer")) {
856
+ issues.push({
857
+ code: "FOOTER_SPACING",
858
+ severity: "warning",
859
+ message: "Metadata includes a footer element but the spec has no footer."
860
+ });
861
+ }
862
+ }
863
+ const footerSpacingPx = options.metadata?.layout.elements ? (() => {
864
+ const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
865
+ if (!footer) {
866
+ return void 0;
867
+ }
868
+ const nonFooter = topLevelElements(options.metadata.layout.elements).filter(
869
+ (element) => element.id !== "footer"
870
+ );
871
+ if (nonFooter.length === 0) {
872
+ return void 0;
873
+ }
874
+ const highestBottom = Math.max(
875
+ ...nonFooter.map((element) => element.bounds.y + element.bounds.height)
876
+ );
877
+ return footer.bounds.y - highestBottom;
878
+ })() : void 0;
879
+ return {
880
+ pass: issues.every((issue) => issue.severity !== "error"),
881
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
882
+ imagePath,
883
+ expected: {
884
+ width: expectedWidth,
885
+ height: expectedHeight,
886
+ scale: expectedScale,
887
+ minContrastRatio: spec.constraints.minContrastRatio,
888
+ minFooterSpacing: spec.constraints.minFooterSpacing
889
+ },
890
+ measured: {
891
+ ...imageMetadata.width !== void 0 ? { width: imageMetadata.width } : {},
892
+ ...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
893
+ ...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
894
+ },
895
+ issues
896
+ };
897
+ }
898
+ export {
899
+ readMetadata,
900
+ runQa
901
+ };