@spectratools/graphic-designer-cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,4 @@
1
- interface RetryPolicy {
2
- maxRetries: number;
3
- baseMs: number;
4
- maxMs: number;
5
- }
1
+ import { RetryOptions } from '@spectratools/cli-shared/middleware';
6
2
 
7
3
  type GitHubPublishOptions = {
8
4
  imagePath: string;
@@ -12,19 +8,37 @@ type GitHubPublishOptions = {
12
8
  branch?: string;
13
9
  token?: string;
14
10
  commitMessage?: string;
15
- retryPolicy?: RetryPolicy;
11
+ retryPolicy?: RetryOptions;
16
12
  };
17
13
  type GitHubPublishResult = {
18
14
  target: 'github';
19
15
  repo: string;
20
16
  branch: string;
21
- attempts: number;
22
17
  files: Array<{
23
18
  path: string;
24
19
  htmlUrl?: string;
25
20
  sha?: string;
26
21
  }>;
27
22
  };
23
+ /**
24
+ * Publish rendered design artifacts to a GitHub repository.
25
+ *
26
+ * Uploads the PNG image and sidecar `.meta.json` metadata to the specified
27
+ * repository via the GitHub Contents API. Files are placed under
28
+ * {@link GitHubPublishOptions.pathPrefix} (defaults to `"artifacts"`). If a
29
+ * file already exists at the destination it is updated in-place (its SHA is
30
+ * fetched first for the update).
31
+ *
32
+ * Requires a GitHub token — either passed via
33
+ * {@link GitHubPublishOptions.token} or resolved from the `GITHUB_TOKEN`
34
+ * environment variable.
35
+ *
36
+ * @param options - Publish configuration including file paths, target repo,
37
+ * branch, commit message, and optional retry policy.
38
+ * @returns A {@link GitHubPublishResult} with the repo, branch, and per-file
39
+ * metadata (path, SHA, HTML URL).
40
+ * @throws When no GitHub token is available or the API request fails.
41
+ */
28
42
  declare function publishToGitHub(options: GitHubPublishOptions): Promise<GitHubPublishResult>;
29
43
 
30
44
  type GistPublishOptions = {
@@ -35,15 +49,30 @@ type GistPublishOptions = {
35
49
  filenamePrefix?: string;
36
50
  public?: boolean;
37
51
  token?: string;
38
- retryPolicy?: RetryPolicy;
52
+ retryPolicy?: RetryOptions;
39
53
  };
40
54
  type GistPublishResult = {
41
55
  target: 'gist';
42
56
  gistId: string;
43
57
  htmlUrl: string;
44
- attempts: number;
45
58
  files: string[];
46
59
  };
60
+ /**
61
+ * Publish rendered design artifacts to a GitHub Gist.
62
+ *
63
+ * Creates (or updates, when {@link GistPublishOptions.gistId} is set) a gist
64
+ * containing the PNG image as a base64 text file, the sidecar `.meta.json`
65
+ * metadata, and a Markdown README with an embedded preview.
66
+ *
67
+ * Requires a GitHub token — either passed via {@link GistPublishOptions.token}
68
+ * or resolved from the `GITHUB_TOKEN` environment variable.
69
+ *
70
+ * @param options - Publish configuration including file paths, gist settings,
71
+ * and optional retry policy.
72
+ * @returns A {@link GistPublishResult} with the gist ID, URL, attempt count,
73
+ * and list of created file names.
74
+ * @throws When no GitHub token is available or the API request fails.
75
+ */
47
76
  declare function publishToGist(options: GistPublishOptions): Promise<GistPublishResult>;
48
77
 
49
78
  export { type GistPublishOptions, type GistPublishResult, type GitHubPublishOptions, type GitHubPublishResult, publishToGist, publishToGitHub };
@@ -1,38 +1,8 @@
1
1
  // src/publish/github.ts
2
2
  import { readFile } from "fs/promises";
3
3
  import { basename, posix } from "path";
4
-
5
- // src/utils/retry.ts
6
- var DEFAULT_RETRY_POLICY = {
7
- maxRetries: 3,
8
- baseMs: 500,
9
- maxMs: 4e3
10
- };
11
- function sleep(ms) {
12
- return new Promise((resolve) => setTimeout(resolve, ms));
13
- }
14
- async function withRetry(operation, policy = DEFAULT_RETRY_POLICY) {
15
- let attempt = 0;
16
- let lastError;
17
- while (attempt <= policy.maxRetries) {
18
- try {
19
- const value = await operation();
20
- return { value, attempts: attempt + 1 };
21
- } catch (error) {
22
- lastError = error;
23
- if (attempt >= policy.maxRetries) {
24
- break;
25
- }
26
- const backoff = Math.min(policy.baseMs * 2 ** attempt, policy.maxMs);
27
- const jitter = Math.floor(Math.random() * 125);
28
- await sleep(backoff + jitter);
29
- attempt += 1;
30
- }
31
- }
32
- throw lastError;
33
- }
34
-
35
- // src/publish/github.ts
4
+ import { withRetry } from "@spectratools/cli-shared/middleware";
5
+ import { HttpError, createHttpClient } from "@spectratools/cli-shared/utils";
36
6
  function requireGitHubToken(token) {
37
7
  const resolved = token ?? process.env.GITHUB_TOKEN;
38
8
  if (!resolved) {
@@ -40,56 +10,18 @@ function requireGitHubToken(token) {
40
10
  }
41
11
  return resolved;
42
12
  }
43
- async function githubJson(path, init, token, retryPolicy) {
44
- return withRetry(async () => {
45
- const response = await fetch(`https://api.github.com${path}`, {
46
- ...init,
47
- headers: {
48
- Accept: "application/vnd.github+json",
49
- Authorization: `Bearer ${token}`,
50
- "User-Agent": "spectratools-graphic-designer",
51
- ...init.headers
52
- }
53
- });
54
- if (!response.ok) {
55
- const text = await response.text();
56
- throw new Error(
57
- `GitHub API ${path} failed (${response.status}): ${text || response.statusText}`
58
- );
59
- }
60
- return await response.json();
61
- }, retryPolicy);
62
- }
63
- async function githubJsonMaybe(path, token, retryPolicy) {
64
- const { value, attempts } = await withRetry(async () => {
65
- const response = await fetch(`https://api.github.com${path}`, {
66
- headers: {
67
- Accept: "application/vnd.github+json",
68
- Authorization: `Bearer ${token}`,
69
- "User-Agent": "spectratools-graphic-designer"
70
- }
71
- });
72
- if (response.status === 404) {
73
- return { found: false };
74
- }
75
- if (!response.ok) {
76
- const text = await response.text();
77
- throw new Error(
78
- `GitHub API ${path} failed (${response.status}): ${text || response.statusText}`
79
- );
80
- }
81
- const json = await response.json();
82
- return { found: true, value: json };
83
- }, retryPolicy);
84
- if (!value.found) {
85
- return { found: false, attempts };
13
+ var DEFAULT_RETRY = {
14
+ maxRetries: 3,
15
+ baseMs: 500,
16
+ maxMs: 4e3
17
+ };
18
+ var github = createHttpClient({
19
+ baseUrl: "https://api.github.com",
20
+ defaultHeaders: {
21
+ Accept: "application/vnd.github+json",
22
+ "User-Agent": "spectratools-graphic-designer"
86
23
  }
87
- return {
88
- found: true,
89
- value: value.value,
90
- attempts
91
- };
92
- }
24
+ });
93
25
  function parseRepo(repo) {
94
26
  const [owner, name] = repo.split("/");
95
27
  if (!owner || !name) {
@@ -105,9 +37,25 @@ function normalizePath(pathPrefix, filename) {
105
37
  const trimmed = (pathPrefix ?? "artifacts").replace(/^\/+|\/+$/gu, "");
106
38
  return posix.join(trimmed, filename);
107
39
  }
40
+ async function githubJsonMaybe(path, token, retry) {
41
+ return withRetry(async () => {
42
+ try {
43
+ const value = await github.request(path, {
44
+ headers: { Authorization: `Bearer ${token}` }
45
+ });
46
+ return { found: true, value };
47
+ } catch (err) {
48
+ if (err instanceof HttpError && err.status === 404) {
49
+ return { found: false };
50
+ }
51
+ throw err;
52
+ }
53
+ }, retry);
54
+ }
108
55
  async function publishToGitHub(options) {
109
56
  const token = requireGitHubToken(options.token);
110
57
  const branch = options.branch ?? "main";
58
+ const retry = options.retryPolicy ?? DEFAULT_RETRY;
111
59
  const commitMessage = options.commitMessage ?? "chore(graphic-designer): publish deterministic artifacts";
112
60
  const [imageBuffer, metadataBuffer] = await Promise.all([
113
61
  readFile(options.imagePath),
@@ -125,16 +73,10 @@ async function publishToGitHub(options) {
125
73
  content: metadataBuffer.toString("base64")
126
74
  }
127
75
  ];
128
- let totalAttempts = 0;
129
76
  const files = [];
130
77
  for (const upload of uploads) {
131
78
  const existingPath = `${toApiContentPath(options.repo, upload.destination)}?ref=${encodeURIComponent(branch)}`;
132
- const existing = await githubJsonMaybe(
133
- existingPath,
134
- token,
135
- options.retryPolicy
136
- );
137
- totalAttempts += existing.attempts;
79
+ const existing = await githubJsonMaybe(existingPath, token, retry);
138
80
  const body = {
139
81
  message: `${commitMessage} (${basename(upload.sourcePath)})`,
140
82
  content: upload.content,
@@ -142,27 +84,24 @@ async function publishToGitHub(options) {
142
84
  sha: existing.value?.sha
143
85
  };
144
86
  const putPath = toApiContentPath(options.repo, upload.destination);
145
- const published = await githubJson(
146
- putPath,
147
- {
87
+ const published = await withRetry(
88
+ () => github.request(putPath, {
148
89
  method: "PUT",
149
- body: JSON.stringify(body)
150
- },
151
- token,
152
- options.retryPolicy
90
+ body,
91
+ headers: { Authorization: `Bearer ${token}` }
92
+ }),
93
+ retry
153
94
  );
154
- totalAttempts += published.attempts;
155
95
  files.push({
156
96
  path: upload.destination,
157
- ...published.value.content?.sha ? { sha: published.value.content.sha } : {},
158
- ...published.value.content?.html_url ? { htmlUrl: published.value.content.html_url } : {}
97
+ ...published.content?.sha ? { sha: published.content.sha } : {},
98
+ ...published.content?.html_url ? { htmlUrl: published.content.html_url } : {}
159
99
  });
160
100
  }
161
101
  return {
162
102
  target: "github",
163
103
  repo: options.repo,
164
104
  branch,
165
- attempts: totalAttempts,
166
105
  files
167
106
  };
168
107
  }
@@ -170,6 +109,8 @@ async function publishToGitHub(options) {
170
109
  // src/publish/gist.ts
171
110
  import { readFile as readFile2 } from "fs/promises";
172
111
  import { basename as basename2 } from "path";
112
+ import { withRetry as withRetry2 } from "@spectratools/cli-shared/middleware";
113
+ import { createHttpClient as createHttpClient2 } from "@spectratools/cli-shared/utils";
173
114
  function requireGitHubToken2(token) {
174
115
  const resolved = token ?? process.env.GITHUB_TOKEN;
175
116
  if (!resolved) {
@@ -177,28 +118,21 @@ function requireGitHubToken2(token) {
177
118
  }
178
119
  return resolved;
179
120
  }
180
- async function gistJson(path, init, token, retryPolicy) {
181
- return withRetry(async () => {
182
- const response = await fetch(`https://api.github.com${path}`, {
183
- ...init,
184
- headers: {
185
- Accept: "application/vnd.github+json",
186
- Authorization: `Bearer ${token}`,
187
- "User-Agent": "spectratools-graphic-designer",
188
- ...init.headers
189
- }
190
- });
191
- if (!response.ok) {
192
- const text = await response.text();
193
- throw new Error(
194
- `GitHub Gist API ${path} failed (${response.status}): ${text || response.statusText}`
195
- );
196
- }
197
- return await response.json();
198
- }, retryPolicy);
199
- }
121
+ var DEFAULT_RETRY2 = {
122
+ maxRetries: 3,
123
+ baseMs: 500,
124
+ maxMs: 4e3
125
+ };
126
+ var github2 = createHttpClient2({
127
+ baseUrl: "https://api.github.com",
128
+ defaultHeaders: {
129
+ Accept: "application/vnd.github+json",
130
+ "User-Agent": "spectratools-graphic-designer"
131
+ }
132
+ });
200
133
  async function publishToGist(options) {
201
134
  const token = requireGitHubToken2(options.token);
135
+ const retry = options.retryPolicy ?? DEFAULT_RETRY2;
202
136
  const [imageBuffer, metadataBuffer] = await Promise.all([
203
137
  readFile2(options.imagePath),
204
138
  readFile2(options.metadataPath)
@@ -236,21 +170,19 @@ async function publishToGist(options) {
236
170
  };
237
171
  const endpoint = options.gistId ? `/gists/${options.gistId}` : "/gists";
238
172
  const method = options.gistId ? "PATCH" : "POST";
239
- const published = await gistJson(
240
- endpoint,
241
- {
173
+ const published = await withRetry2(
174
+ () => github2.request(endpoint, {
242
175
  method,
243
- body: JSON.stringify(payload)
244
- },
245
- token,
246
- options.retryPolicy
176
+ body: payload,
177
+ headers: { Authorization: `Bearer ${token}` }
178
+ }),
179
+ retry
247
180
  );
248
181
  return {
249
182
  target: "gist",
250
- gistId: published.value.id,
251
- htmlUrl: published.value.html_url,
252
- attempts: published.attempts,
253
- files: Object.keys(published.value.files ?? payload.files)
183
+ gistId: published.id,
184
+ htmlUrl: published.html_url,
185
+ files: Object.keys(published.files ?? payload.files)
254
186
  };
255
187
  }
256
188
  export {
package/dist/qa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BxXBTOn-.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BUTof436.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';
4
4
 
@@ -28,7 +28,31 @@ type QaReport = {
28
28
  };
29
29
  issues: QaIssue[];
30
30
  };
31
+ /**
32
+ * Read and parse a sidecar `.meta.json` file produced by
33
+ * {@link writeRenderArtifacts}.
34
+ *
35
+ * @param path - Absolute or relative path to the `.meta.json` sidecar file.
36
+ * @returns The parsed {@link RenderMetadata} object.
37
+ */
31
38
  declare function readMetadata(path: string): Promise<RenderMetadata>;
39
+ /**
40
+ * Run quality-assurance checks on a rendered design image.
41
+ *
42
+ * Validates the rendered PNG against the source spec and optional metadata.
43
+ * Checks include: dimension matching, element clipping against the safe frame,
44
+ * element overlap detection, WCAG contrast ratio, footer spacing, text
45
+ * truncation, and draw-command bounds.
46
+ *
47
+ * @param options - QA configuration.
48
+ * @param options.imagePath - Path to the rendered PNG image to validate.
49
+ * @param options.spec - The {@link DesignSpec} used to produce the image.
50
+ * @param options.metadata - Optional {@link RenderMetadata} from the render
51
+ * pass. When provided, layout-level checks (overlap, clipping, contrast) are
52
+ * performed against the element positions recorded in the metadata.
53
+ * @returns A {@link QaReport} summarising whether the image passes and listing
54
+ * any issues found.
55
+ */
32
56
  declare function runQa(options: {
33
57
  imagePath: string;
34
58
  spec: DesignSpec;
package/dist/qa.js CHANGED
@@ -62,7 +62,98 @@ import { z as z2 } from "zod";
62
62
 
63
63
  // src/themes/builtin.ts
64
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");
65
+
66
+ // src/utils/color.ts
67
+ function parseChannel(hex, offset) {
68
+ return Number.parseInt(hex.slice(offset, offset + 2), 16);
69
+ }
70
+ function parseHexColor(hexColor) {
71
+ const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
72
+ if (normalized.length !== 6 && normalized.length !== 8) {
73
+ throw new Error(`Unsupported color format: ${hexColor}`);
74
+ }
75
+ return {
76
+ r: parseChannel(normalized, 0),
77
+ g: parseChannel(normalized, 2),
78
+ b: parseChannel(normalized, 4)
79
+ };
80
+ }
81
+ var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
82
+ var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
83
+ function toHex(n) {
84
+ return n.toString(16).padStart(2, "0");
85
+ }
86
+ function parseRgbaToHex(color) {
87
+ const match = rgbaRegex.exec(color);
88
+ if (!match) {
89
+ throw new Error(`Invalid rgb/rgba color: ${color}`);
90
+ }
91
+ const r = Number.parseInt(match[1], 10);
92
+ const g = Number.parseInt(match[2], 10);
93
+ const b = Number.parseInt(match[3], 10);
94
+ if (r > 255 || g > 255 || b > 255) {
95
+ throw new Error(`RGB channel values must be 0-255, got: ${color}`);
96
+ }
97
+ if (match[4] !== void 0) {
98
+ const a = Number.parseFloat(match[4]);
99
+ if (a < 0 || a > 1) {
100
+ throw new Error(`Alpha value must be 0-1, got: ${a}`);
101
+ }
102
+ const alphaByte = Math.round(a * 255);
103
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
104
+ }
105
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
106
+ }
107
+ function isRgbaColor(color) {
108
+ return rgbaRegex.test(color);
109
+ }
110
+ function isHexColor(color) {
111
+ return hexColorRegex.test(color);
112
+ }
113
+ function normalizeColor(color) {
114
+ if (isHexColor(color)) {
115
+ return color;
116
+ }
117
+ if (isRgbaColor(color)) {
118
+ return parseRgbaToHex(color);
119
+ }
120
+ throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
121
+ }
122
+ function srgbToLinear(channel) {
123
+ const normalized = channel / 255;
124
+ if (normalized <= 0.03928) {
125
+ return normalized / 12.92;
126
+ }
127
+ return ((normalized + 0.055) / 1.055) ** 2.4;
128
+ }
129
+ function relativeLuminance(hexColor) {
130
+ const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
131
+ const rgb = parseHexColor(normalized);
132
+ const r = srgbToLinear(rgb.r);
133
+ const g = srgbToLinear(rgb.g);
134
+ const b = srgbToLinear(rgb.b);
135
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
136
+ }
137
+ function contrastRatio(foreground, background) {
138
+ const fg = relativeLuminance(foreground);
139
+ const bg = relativeLuminance(background);
140
+ const lighter = Math.max(fg, bg);
141
+ const darker = Math.min(fg, bg);
142
+ return (lighter + 0.05) / (darker + 0.05);
143
+ }
144
+
145
+ // src/themes/builtin.ts
146
+ var colorHexSchema = z.string().refine(
147
+ (v) => {
148
+ try {
149
+ normalizeColor(v);
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ },
155
+ { message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
156
+ ).transform((v) => normalizeColor(v));
66
157
  var fontFamilySchema = z.string().min(1).max(120);
67
158
  var codeThemeSchema = z.object({
68
159
  background: colorHexSchema,
@@ -235,7 +326,17 @@ var builtInThemes = {
235
326
  var defaultTheme = builtInThemes.dark;
236
327
 
237
328
  // 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");
329
+ var colorHexSchema2 = z2.string().refine(
330
+ (v) => {
331
+ try {
332
+ normalizeColor(v);
333
+ return true;
334
+ } catch {
335
+ return false;
336
+ }
337
+ },
338
+ { message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
339
+ ).transform((v) => normalizeColor(v));
239
340
  var gradientStopSchema = z2.object({
240
341
  offset: z2.number().min(0).max(1),
241
342
  color: colorHexSchema2
@@ -426,6 +527,9 @@ var flowNodeElementSchema = z2.object({
426
527
  label: z2.string().min(1).max(200),
427
528
  sublabel: z2.string().min(1).max(300).optional(),
428
529
  sublabelColor: colorHexSchema2.optional(),
530
+ sublabel2: z2.string().min(1).max(300).optional(),
531
+ sublabel2Color: colorHexSchema2.optional(),
532
+ sublabel2FontSize: z2.number().min(8).max(32).optional(),
429
533
  labelColor: colorHexSchema2.optional(),
430
534
  labelFontSize: z2.number().min(10).max(48).optional(),
431
535
  color: colorHexSchema2.optional(),
@@ -434,7 +538,12 @@ var flowNodeElementSchema = z2.object({
434
538
  cornerRadius: z2.number().min(0).max(64).optional(),
435
539
  width: z2.number().int().min(40).max(800).optional(),
436
540
  height: z2.number().int().min(30).max(600).optional(),
437
- opacity: z2.number().min(0).max(1).default(1)
541
+ fillOpacity: z2.number().min(0).max(1).default(1),
542
+ opacity: z2.number().min(0).max(1).default(1),
543
+ badgeText: z2.string().min(1).max(32).optional(),
544
+ badgeColor: colorHexSchema2.optional(),
545
+ badgeBackground: colorHexSchema2.optional(),
546
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
438
547
  }).strict();
439
548
  var connectionElementSchema = z2.object({
440
549
  type: z2.literal("connection"),
@@ -523,7 +632,15 @@ var autoLayoutConfigSchema = z2.object({
523
632
  nodeSpacing: z2.number().int().min(0).max(512).default(80),
524
633
  rankSpacing: z2.number().int().min(0).max(512).default(120),
525
634
  edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
526
- aspectRatio: z2.number().min(0.5).max(3).optional()
635
+ aspectRatio: z2.number().min(0.5).max(3).optional(),
636
+ /** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
637
+ radialRoot: z2.string().min(1).max(120).optional(),
638
+ /** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
639
+ radialRadius: z2.number().positive().optional(),
640
+ /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
641
+ radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
642
+ /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
643
+ radialSortBy: z2.enum(["id", "connections"]).optional()
527
644
  }).strict();
528
645
  var gridLayoutConfigSchema = z2.object({
529
646
  mode: z2.literal("grid"),
@@ -627,43 +744,6 @@ function parseDesignSpec(input) {
627
744
  return designSpecSchema.parse(input);
628
745
  }
629
746
 
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
747
  // src/qa.ts
668
748
  function rectWithin(outer, inner) {
669
749
  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;
@@ -1,3 +1,3 @@
1
- export { h as DEFAULT_GENERATOR_VERSION, z as LayoutSnapshot, a as Rect, R as RenderMetadata, J as RenderResult, d as RenderedElement, W as WrittenArtifacts, X as computeSpecHash, a9 as inferSidecarPath, ab as renderDesign, ad as writeRenderArtifacts } from './spec.schema-BxXBTOn-.js';
1
+ export { h as DEFAULT_GENERATOR_VERSION, z as LayoutSnapshot, a as Rect, R as RenderMetadata, J as RenderResult, d as RenderedElement, W as WrittenArtifacts, X as computeSpecHash, a9 as inferSidecarPath, ab as renderDesign, ad as writeRenderArtifacts } from './spec.schema-BUTof436.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';