@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.
- package/dist/cli.js +624 -455
- package/dist/index.d.ts +156 -2
- package/dist/index.js +624 -455
- package/dist/publish/index.d.ts +38 -9
- package/dist/publish/index.js +62 -130
- package/dist/qa.d.ts +25 -1
- package/dist/qa.js +121 -41
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +627 -389
- package/dist/{spec.schema-BxXBTOn-.d.ts → spec.schema-BUTof436.d.ts} +775 -463
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +85 -4
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -8,38 +8,8 @@ import { Cli, z as z3 } from "incur";
|
|
|
8
8
|
// src/publish/gist.ts
|
|
9
9
|
import { readFile } from "fs/promises";
|
|
10
10
|
import { basename } from "path";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var DEFAULT_RETRY_POLICY = {
|
|
14
|
-
maxRetries: 3,
|
|
15
|
-
baseMs: 500,
|
|
16
|
-
maxMs: 4e3
|
|
17
|
-
};
|
|
18
|
-
function sleep(ms) {
|
|
19
|
-
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
20
|
-
}
|
|
21
|
-
async function withRetry(operation, policy = DEFAULT_RETRY_POLICY) {
|
|
22
|
-
let attempt = 0;
|
|
23
|
-
let lastError;
|
|
24
|
-
while (attempt <= policy.maxRetries) {
|
|
25
|
-
try {
|
|
26
|
-
const value = await operation();
|
|
27
|
-
return { value, attempts: attempt + 1 };
|
|
28
|
-
} catch (error) {
|
|
29
|
-
lastError = error;
|
|
30
|
-
if (attempt >= policy.maxRetries) {
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
const backoff = Math.min(policy.baseMs * 2 ** attempt, policy.maxMs);
|
|
34
|
-
const jitter = Math.floor(Math.random() * 125);
|
|
35
|
-
await sleep(backoff + jitter);
|
|
36
|
-
attempt += 1;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
throw lastError;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/publish/gist.ts
|
|
11
|
+
import { withRetry } from "@spectratools/cli-shared/middleware";
|
|
12
|
+
import { createHttpClient } from "@spectratools/cli-shared/utils";
|
|
43
13
|
function requireGitHubToken(token) {
|
|
44
14
|
const resolved = token ?? process.env.GITHUB_TOKEN;
|
|
45
15
|
if (!resolved) {
|
|
@@ -47,28 +17,21 @@ function requireGitHubToken(token) {
|
|
|
47
17
|
}
|
|
48
18
|
return resolved;
|
|
49
19
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const text = await response.text();
|
|
63
|
-
throw new Error(
|
|
64
|
-
`GitHub Gist API ${path} failed (${response.status}): ${text || response.statusText}`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return await response.json();
|
|
68
|
-
}, retryPolicy);
|
|
69
|
-
}
|
|
20
|
+
var DEFAULT_RETRY = {
|
|
21
|
+
maxRetries: 3,
|
|
22
|
+
baseMs: 500,
|
|
23
|
+
maxMs: 4e3
|
|
24
|
+
};
|
|
25
|
+
var github = createHttpClient({
|
|
26
|
+
baseUrl: "https://api.github.com",
|
|
27
|
+
defaultHeaders: {
|
|
28
|
+
Accept: "application/vnd.github+json",
|
|
29
|
+
"User-Agent": "spectratools-graphic-designer"
|
|
30
|
+
}
|
|
31
|
+
});
|
|
70
32
|
async function publishToGist(options) {
|
|
71
33
|
const token = requireGitHubToken(options.token);
|
|
34
|
+
const retry = options.retryPolicy ?? DEFAULT_RETRY;
|
|
72
35
|
const [imageBuffer, metadataBuffer] = await Promise.all([
|
|
73
36
|
readFile(options.imagePath),
|
|
74
37
|
readFile(options.metadataPath)
|
|
@@ -106,27 +69,27 @@ async function publishToGist(options) {
|
|
|
106
69
|
};
|
|
107
70
|
const endpoint = options.gistId ? `/gists/${options.gistId}` : "/gists";
|
|
108
71
|
const method = options.gistId ? "PATCH" : "POST";
|
|
109
|
-
const published = await
|
|
110
|
-
endpoint,
|
|
111
|
-
{
|
|
72
|
+
const published = await withRetry(
|
|
73
|
+
() => github.request(endpoint, {
|
|
112
74
|
method,
|
|
113
|
-
body:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
75
|
+
body: payload,
|
|
76
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
77
|
+
}),
|
|
78
|
+
retry
|
|
117
79
|
);
|
|
118
80
|
return {
|
|
119
81
|
target: "gist",
|
|
120
|
-
gistId: published.
|
|
121
|
-
htmlUrl: published.
|
|
122
|
-
|
|
123
|
-
files: Object.keys(published.value.files ?? payload.files)
|
|
82
|
+
gistId: published.id,
|
|
83
|
+
htmlUrl: published.html_url,
|
|
84
|
+
files: Object.keys(published.files ?? payload.files)
|
|
124
85
|
};
|
|
125
86
|
}
|
|
126
87
|
|
|
127
88
|
// src/publish/github.ts
|
|
128
89
|
import { readFile as readFile2 } from "fs/promises";
|
|
129
90
|
import { basename as basename2, posix } from "path";
|
|
91
|
+
import { withRetry as withRetry2 } from "@spectratools/cli-shared/middleware";
|
|
92
|
+
import { HttpError, createHttpClient as createHttpClient2 } from "@spectratools/cli-shared/utils";
|
|
130
93
|
function requireGitHubToken2(token) {
|
|
131
94
|
const resolved = token ?? process.env.GITHUB_TOKEN;
|
|
132
95
|
if (!resolved) {
|
|
@@ -134,56 +97,18 @@ function requireGitHubToken2(token) {
|
|
|
134
97
|
}
|
|
135
98
|
return resolved;
|
|
136
99
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
148
|
-
if (!response.ok) {
|
|
149
|
-
const text = await response.text();
|
|
150
|
-
throw new Error(
|
|
151
|
-
`GitHub API ${path} failed (${response.status}): ${text || response.statusText}`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return await response.json();
|
|
155
|
-
}, retryPolicy);
|
|
156
|
-
}
|
|
157
|
-
async function githubJsonMaybe(path, token, retryPolicy) {
|
|
158
|
-
const { value, attempts } = await withRetry(async () => {
|
|
159
|
-
const response = await fetch(`https://api.github.com${path}`, {
|
|
160
|
-
headers: {
|
|
161
|
-
Accept: "application/vnd.github+json",
|
|
162
|
-
Authorization: `Bearer ${token}`,
|
|
163
|
-
"User-Agent": "spectratools-graphic-designer"
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
if (response.status === 404) {
|
|
167
|
-
return { found: false };
|
|
168
|
-
}
|
|
169
|
-
if (!response.ok) {
|
|
170
|
-
const text = await response.text();
|
|
171
|
-
throw new Error(
|
|
172
|
-
`GitHub API ${path} failed (${response.status}): ${text || response.statusText}`
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
const json = await response.json();
|
|
176
|
-
return { found: true, value: json };
|
|
177
|
-
}, retryPolicy);
|
|
178
|
-
if (!value.found) {
|
|
179
|
-
return { found: false, attempts };
|
|
100
|
+
var DEFAULT_RETRY2 = {
|
|
101
|
+
maxRetries: 3,
|
|
102
|
+
baseMs: 500,
|
|
103
|
+
maxMs: 4e3
|
|
104
|
+
};
|
|
105
|
+
var github2 = createHttpClient2({
|
|
106
|
+
baseUrl: "https://api.github.com",
|
|
107
|
+
defaultHeaders: {
|
|
108
|
+
Accept: "application/vnd.github+json",
|
|
109
|
+
"User-Agent": "spectratools-graphic-designer"
|
|
180
110
|
}
|
|
181
|
-
|
|
182
|
-
found: true,
|
|
183
|
-
value: value.value,
|
|
184
|
-
attempts
|
|
185
|
-
};
|
|
186
|
-
}
|
|
111
|
+
});
|
|
187
112
|
function parseRepo(repo) {
|
|
188
113
|
const [owner, name] = repo.split("/");
|
|
189
114
|
if (!owner || !name) {
|
|
@@ -199,9 +124,25 @@ function normalizePath(pathPrefix, filename) {
|
|
|
199
124
|
const trimmed = (pathPrefix ?? "artifacts").replace(/^\/+|\/+$/gu, "");
|
|
200
125
|
return posix.join(trimmed, filename);
|
|
201
126
|
}
|
|
127
|
+
async function githubJsonMaybe(path, token, retry) {
|
|
128
|
+
return withRetry2(async () => {
|
|
129
|
+
try {
|
|
130
|
+
const value = await github2.request(path, {
|
|
131
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
132
|
+
});
|
|
133
|
+
return { found: true, value };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
136
|
+
return { found: false };
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}, retry);
|
|
141
|
+
}
|
|
202
142
|
async function publishToGitHub(options) {
|
|
203
143
|
const token = requireGitHubToken2(options.token);
|
|
204
144
|
const branch = options.branch ?? "main";
|
|
145
|
+
const retry = options.retryPolicy ?? DEFAULT_RETRY2;
|
|
205
146
|
const commitMessage = options.commitMessage ?? "chore(graphic-designer): publish deterministic artifacts";
|
|
206
147
|
const [imageBuffer, metadataBuffer] = await Promise.all([
|
|
207
148
|
readFile2(options.imagePath),
|
|
@@ -219,16 +160,10 @@ async function publishToGitHub(options) {
|
|
|
219
160
|
content: metadataBuffer.toString("base64")
|
|
220
161
|
}
|
|
221
162
|
];
|
|
222
|
-
let totalAttempts = 0;
|
|
223
163
|
const files = [];
|
|
224
164
|
for (const upload of uploads) {
|
|
225
165
|
const existingPath = `${toApiContentPath(options.repo, upload.destination)}?ref=${encodeURIComponent(branch)}`;
|
|
226
|
-
const existing = await githubJsonMaybe(
|
|
227
|
-
existingPath,
|
|
228
|
-
token,
|
|
229
|
-
options.retryPolicy
|
|
230
|
-
);
|
|
231
|
-
totalAttempts += existing.attempts;
|
|
166
|
+
const existing = await githubJsonMaybe(existingPath, token, retry);
|
|
232
167
|
const body = {
|
|
233
168
|
message: `${commitMessage} (${basename2(upload.sourcePath)})`,
|
|
234
169
|
content: upload.content,
|
|
@@ -236,27 +171,24 @@ async function publishToGitHub(options) {
|
|
|
236
171
|
sha: existing.value?.sha
|
|
237
172
|
};
|
|
238
173
|
const putPath = toApiContentPath(options.repo, upload.destination);
|
|
239
|
-
const published = await
|
|
240
|
-
putPath,
|
|
241
|
-
{
|
|
174
|
+
const published = await withRetry2(
|
|
175
|
+
() => github2.request(putPath, {
|
|
242
176
|
method: "PUT",
|
|
243
|
-
body
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
177
|
+
body,
|
|
178
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
179
|
+
}),
|
|
180
|
+
retry
|
|
247
181
|
);
|
|
248
|
-
totalAttempts += published.attempts;
|
|
249
182
|
files.push({
|
|
250
183
|
path: upload.destination,
|
|
251
|
-
...published.
|
|
252
|
-
...published.
|
|
184
|
+
...published.content?.sha ? { sha: published.content.sha } : {},
|
|
185
|
+
...published.content?.html_url ? { htmlUrl: published.content.html_url } : {}
|
|
253
186
|
});
|
|
254
187
|
}
|
|
255
188
|
return {
|
|
256
189
|
target: "github",
|
|
257
190
|
repo: options.repo,
|
|
258
191
|
branch,
|
|
259
|
-
attempts: totalAttempts,
|
|
260
192
|
files
|
|
261
193
|
};
|
|
262
194
|
}
|
|
@@ -325,7 +257,106 @@ import { z as z2 } from "zod";
|
|
|
325
257
|
|
|
326
258
|
// src/themes/builtin.ts
|
|
327
259
|
import { z } from "zod";
|
|
328
|
-
|
|
260
|
+
|
|
261
|
+
// src/utils/color.ts
|
|
262
|
+
function parseChannel(hex, offset) {
|
|
263
|
+
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
264
|
+
}
|
|
265
|
+
function parseHexColor(hexColor) {
|
|
266
|
+
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
267
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
268
|
+
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
r: parseChannel(normalized, 0),
|
|
272
|
+
g: parseChannel(normalized, 2),
|
|
273
|
+
b: parseChannel(normalized, 4)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
|
|
277
|
+
var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
278
|
+
function toHex(n) {
|
|
279
|
+
return n.toString(16).padStart(2, "0");
|
|
280
|
+
}
|
|
281
|
+
function parseRgbaToHex(color) {
|
|
282
|
+
const match = rgbaRegex.exec(color);
|
|
283
|
+
if (!match) {
|
|
284
|
+
throw new Error(`Invalid rgb/rgba color: ${color}`);
|
|
285
|
+
}
|
|
286
|
+
const r = Number.parseInt(match[1], 10);
|
|
287
|
+
const g = Number.parseInt(match[2], 10);
|
|
288
|
+
const b = Number.parseInt(match[3], 10);
|
|
289
|
+
if (r > 255 || g > 255 || b > 255) {
|
|
290
|
+
throw new Error(`RGB channel values must be 0-255, got: ${color}`);
|
|
291
|
+
}
|
|
292
|
+
if (match[4] !== void 0) {
|
|
293
|
+
const a = Number.parseFloat(match[4]);
|
|
294
|
+
if (a < 0 || a > 1) {
|
|
295
|
+
throw new Error(`Alpha value must be 0-1, got: ${a}`);
|
|
296
|
+
}
|
|
297
|
+
const alphaByte = Math.round(a * 255);
|
|
298
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
|
|
299
|
+
}
|
|
300
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
301
|
+
}
|
|
302
|
+
function isRgbaColor(color) {
|
|
303
|
+
return rgbaRegex.test(color);
|
|
304
|
+
}
|
|
305
|
+
function isHexColor(color) {
|
|
306
|
+
return hexColorRegex.test(color);
|
|
307
|
+
}
|
|
308
|
+
function normalizeColor(color) {
|
|
309
|
+
if (isHexColor(color)) {
|
|
310
|
+
return color;
|
|
311
|
+
}
|
|
312
|
+
if (isRgbaColor(color)) {
|
|
313
|
+
return parseRgbaToHex(color);
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
|
|
316
|
+
}
|
|
317
|
+
function srgbToLinear(channel) {
|
|
318
|
+
const normalized = channel / 255;
|
|
319
|
+
if (normalized <= 0.03928) {
|
|
320
|
+
return normalized / 12.92;
|
|
321
|
+
}
|
|
322
|
+
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
323
|
+
}
|
|
324
|
+
function relativeLuminance(hexColor) {
|
|
325
|
+
const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
|
|
326
|
+
const rgb = parseHexColor(normalized);
|
|
327
|
+
const r = srgbToLinear(rgb.r);
|
|
328
|
+
const g = srgbToLinear(rgb.g);
|
|
329
|
+
const b = srgbToLinear(rgb.b);
|
|
330
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
331
|
+
}
|
|
332
|
+
function contrastRatio(foreground, background) {
|
|
333
|
+
const fg = relativeLuminance(foreground);
|
|
334
|
+
const bg = relativeLuminance(background);
|
|
335
|
+
const lighter = Math.max(fg, bg);
|
|
336
|
+
const darker = Math.min(fg, bg);
|
|
337
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
338
|
+
}
|
|
339
|
+
function blendColorWithOpacity(foreground, background, opacity) {
|
|
340
|
+
const fg = parseHexColor(foreground);
|
|
341
|
+
const bg = parseHexColor(background);
|
|
342
|
+
const r = Math.round(fg.r * opacity + bg.r * (1 - opacity));
|
|
343
|
+
const g = Math.round(fg.g * opacity + bg.g * (1 - opacity));
|
|
344
|
+
const b = Math.round(fg.b * opacity + bg.b * (1 - opacity));
|
|
345
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/themes/builtin.ts
|
|
349
|
+
var colorHexSchema = z.string().refine(
|
|
350
|
+
(v) => {
|
|
351
|
+
try {
|
|
352
|
+
normalizeColor(v);
|
|
353
|
+
return true;
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
359
|
+
).transform((v) => normalizeColor(v));
|
|
329
360
|
var fontFamilySchema = z.string().min(1).max(120);
|
|
330
361
|
var codeThemeSchema = z.object({
|
|
331
362
|
background: colorHexSchema,
|
|
@@ -556,7 +587,17 @@ function resolveTheme(theme) {
|
|
|
556
587
|
}
|
|
557
588
|
|
|
558
589
|
// src/spec.schema.ts
|
|
559
|
-
var colorHexSchema2 = z2.string().
|
|
590
|
+
var colorHexSchema2 = z2.string().refine(
|
|
591
|
+
(v) => {
|
|
592
|
+
try {
|
|
593
|
+
normalizeColor(v);
|
|
594
|
+
return true;
|
|
595
|
+
} catch {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
600
|
+
).transform((v) => normalizeColor(v));
|
|
560
601
|
var gradientStopSchema = z2.object({
|
|
561
602
|
offset: z2.number().min(0).max(1),
|
|
562
603
|
color: colorHexSchema2
|
|
@@ -748,6 +789,9 @@ var flowNodeElementSchema = z2.object({
|
|
|
748
789
|
label: z2.string().min(1).max(200),
|
|
749
790
|
sublabel: z2.string().min(1).max(300).optional(),
|
|
750
791
|
sublabelColor: colorHexSchema2.optional(),
|
|
792
|
+
sublabel2: z2.string().min(1).max(300).optional(),
|
|
793
|
+
sublabel2Color: colorHexSchema2.optional(),
|
|
794
|
+
sublabel2FontSize: z2.number().min(8).max(32).optional(),
|
|
751
795
|
labelColor: colorHexSchema2.optional(),
|
|
752
796
|
labelFontSize: z2.number().min(10).max(48).optional(),
|
|
753
797
|
color: colorHexSchema2.optional(),
|
|
@@ -756,7 +800,12 @@ var flowNodeElementSchema = z2.object({
|
|
|
756
800
|
cornerRadius: z2.number().min(0).max(64).optional(),
|
|
757
801
|
width: z2.number().int().min(40).max(800).optional(),
|
|
758
802
|
height: z2.number().int().min(30).max(600).optional(),
|
|
759
|
-
|
|
803
|
+
fillOpacity: z2.number().min(0).max(1).default(1),
|
|
804
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
805
|
+
badgeText: z2.string().min(1).max(32).optional(),
|
|
806
|
+
badgeColor: colorHexSchema2.optional(),
|
|
807
|
+
badgeBackground: colorHexSchema2.optional(),
|
|
808
|
+
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
|
|
760
809
|
}).strict();
|
|
761
810
|
var connectionElementSchema = z2.object({
|
|
762
811
|
type: z2.literal("connection"),
|
|
@@ -845,7 +894,15 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
845
894
|
nodeSpacing: z2.number().int().min(0).max(512).default(80),
|
|
846
895
|
rankSpacing: z2.number().int().min(0).max(512).default(120),
|
|
847
896
|
edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
|
|
848
|
-
aspectRatio: z2.number().min(0.5).max(3).optional()
|
|
897
|
+
aspectRatio: z2.number().min(0.5).max(3).optional(),
|
|
898
|
+
/** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
|
|
899
|
+
radialRoot: z2.string().min(1).max(120).optional(),
|
|
900
|
+
/** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
|
|
901
|
+
radialRadius: z2.number().positive().optional(),
|
|
902
|
+
/** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
|
|
903
|
+
radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
|
|
904
|
+
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
905
|
+
radialSortBy: z2.enum(["id", "connections"]).optional()
|
|
849
906
|
}).strict();
|
|
850
907
|
var gridLayoutConfigSchema = z2.object({
|
|
851
908
|
mode: z2.literal("grid"),
|
|
@@ -949,43 +1006,6 @@ function parseDesignSpec(input) {
|
|
|
949
1006
|
return designSpecSchema.parse(input);
|
|
950
1007
|
}
|
|
951
1008
|
|
|
952
|
-
// src/utils/color.ts
|
|
953
|
-
function parseChannel(hex, offset) {
|
|
954
|
-
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
955
|
-
}
|
|
956
|
-
function parseHexColor(hexColor) {
|
|
957
|
-
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
958
|
-
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
959
|
-
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
960
|
-
}
|
|
961
|
-
return {
|
|
962
|
-
r: parseChannel(normalized, 0),
|
|
963
|
-
g: parseChannel(normalized, 2),
|
|
964
|
-
b: parseChannel(normalized, 4)
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
function srgbToLinear(channel) {
|
|
968
|
-
const normalized = channel / 255;
|
|
969
|
-
if (normalized <= 0.03928) {
|
|
970
|
-
return normalized / 12.92;
|
|
971
|
-
}
|
|
972
|
-
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
973
|
-
}
|
|
974
|
-
function relativeLuminance(hexColor) {
|
|
975
|
-
const rgb = parseHexColor(hexColor);
|
|
976
|
-
const r = srgbToLinear(rgb.r);
|
|
977
|
-
const g = srgbToLinear(rgb.g);
|
|
978
|
-
const b = srgbToLinear(rgb.b);
|
|
979
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
980
|
-
}
|
|
981
|
-
function contrastRatio(foreground, background) {
|
|
982
|
-
const fg = relativeLuminance(foreground);
|
|
983
|
-
const bg = relativeLuminance(background);
|
|
984
|
-
const lighter = Math.max(fg, bg);
|
|
985
|
-
const darker = Math.min(fg, bg);
|
|
986
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
1009
|
// src/qa.ts
|
|
990
1010
|
function rectWithin(outer, inner) {
|
|
991
1011
|
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;
|
|
@@ -1252,6 +1272,382 @@ function loadFonts() {
|
|
|
1252
1272
|
// src/layout/elk.ts
|
|
1253
1273
|
import ELK from "elkjs";
|
|
1254
1274
|
|
|
1275
|
+
// src/primitives/shapes.ts
|
|
1276
|
+
function roundRectPath(ctx, rect, radius) {
|
|
1277
|
+
const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
|
|
1278
|
+
const right = rect.x + rect.width;
|
|
1279
|
+
const bottom = rect.y + rect.height;
|
|
1280
|
+
ctx.beginPath();
|
|
1281
|
+
ctx.moveTo(rect.x + r, rect.y);
|
|
1282
|
+
ctx.lineTo(right - r, rect.y);
|
|
1283
|
+
ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
|
|
1284
|
+
ctx.lineTo(right, bottom - r);
|
|
1285
|
+
ctx.quadraticCurveTo(right, bottom, right - r, bottom);
|
|
1286
|
+
ctx.lineTo(rect.x + r, bottom);
|
|
1287
|
+
ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
|
|
1288
|
+
ctx.lineTo(rect.x, rect.y + r);
|
|
1289
|
+
ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
|
|
1290
|
+
ctx.closePath();
|
|
1291
|
+
}
|
|
1292
|
+
function fillAndStroke(ctx, fill, stroke) {
|
|
1293
|
+
ctx.fillStyle = fill;
|
|
1294
|
+
ctx.fill();
|
|
1295
|
+
if (stroke) {
|
|
1296
|
+
ctx.strokeStyle = stroke;
|
|
1297
|
+
ctx.stroke();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
1301
|
+
roundRectPath(ctx, rect, radius);
|
|
1302
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1303
|
+
}
|
|
1304
|
+
function drawCircle(ctx, center2, radius, fill, stroke) {
|
|
1305
|
+
ctx.beginPath();
|
|
1306
|
+
ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
1307
|
+
ctx.closePath();
|
|
1308
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1309
|
+
}
|
|
1310
|
+
function drawDiamond(ctx, bounds, fill, stroke) {
|
|
1311
|
+
const cx = bounds.x + bounds.width / 2;
|
|
1312
|
+
const cy = bounds.y + bounds.height / 2;
|
|
1313
|
+
ctx.beginPath();
|
|
1314
|
+
ctx.moveTo(cx, bounds.y);
|
|
1315
|
+
ctx.lineTo(bounds.x + bounds.width, cy);
|
|
1316
|
+
ctx.lineTo(cx, bounds.y + bounds.height);
|
|
1317
|
+
ctx.lineTo(bounds.x, cy);
|
|
1318
|
+
ctx.closePath();
|
|
1319
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1320
|
+
}
|
|
1321
|
+
function drawPill(ctx, bounds, fill, stroke) {
|
|
1322
|
+
drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
|
|
1323
|
+
}
|
|
1324
|
+
function drawEllipse(ctx, bounds, fill, stroke) {
|
|
1325
|
+
const cx = bounds.x + bounds.width / 2;
|
|
1326
|
+
const cy = bounds.y + bounds.height / 2;
|
|
1327
|
+
ctx.beginPath();
|
|
1328
|
+
ctx.ellipse(
|
|
1329
|
+
cx,
|
|
1330
|
+
cy,
|
|
1331
|
+
Math.max(0, bounds.width / 2),
|
|
1332
|
+
Math.max(0, bounds.height / 2),
|
|
1333
|
+
0,
|
|
1334
|
+
0,
|
|
1335
|
+
Math.PI * 2
|
|
1336
|
+
);
|
|
1337
|
+
ctx.closePath();
|
|
1338
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1339
|
+
}
|
|
1340
|
+
function drawCylinder(ctx, bounds, fill, stroke) {
|
|
1341
|
+
const rx = Math.max(2, bounds.width / 2);
|
|
1342
|
+
const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
|
|
1343
|
+
const cx = bounds.x + bounds.width / 2;
|
|
1344
|
+
const topCy = bounds.y + ry;
|
|
1345
|
+
const bottomCy = bounds.y + bounds.height - ry;
|
|
1346
|
+
ctx.beginPath();
|
|
1347
|
+
ctx.moveTo(bounds.x, topCy);
|
|
1348
|
+
ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
|
|
1349
|
+
ctx.lineTo(bounds.x + bounds.width, bottomCy);
|
|
1350
|
+
ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
|
|
1351
|
+
ctx.closePath();
|
|
1352
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1353
|
+
if (stroke) {
|
|
1354
|
+
ctx.beginPath();
|
|
1355
|
+
ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
|
|
1356
|
+
ctx.closePath();
|
|
1357
|
+
ctx.strokeStyle = stroke;
|
|
1358
|
+
ctx.stroke();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function drawParallelogram(ctx, bounds, fill, stroke, skew) {
|
|
1362
|
+
const maxSkew = bounds.width * 0.45;
|
|
1363
|
+
const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
|
|
1364
|
+
ctx.beginPath();
|
|
1365
|
+
ctx.moveTo(bounds.x + skewX, bounds.y);
|
|
1366
|
+
ctx.lineTo(bounds.x + bounds.width, bounds.y);
|
|
1367
|
+
ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
|
|
1368
|
+
ctx.lineTo(bounds.x, bounds.y + bounds.height);
|
|
1369
|
+
ctx.closePath();
|
|
1370
|
+
fillAndStroke(ctx, fill, stroke);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/primitives/text.ts
|
|
1374
|
+
var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
|
|
1375
|
+
function resolveFont(requested, role) {
|
|
1376
|
+
if (SUPPORTED_FONT_FAMILIES.has(requested)) {
|
|
1377
|
+
return requested;
|
|
1378
|
+
}
|
|
1379
|
+
if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
|
|
1380
|
+
return "JetBrains Mono";
|
|
1381
|
+
}
|
|
1382
|
+
if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
|
|
1383
|
+
return "Space Grotesk";
|
|
1384
|
+
}
|
|
1385
|
+
return "Inter";
|
|
1386
|
+
}
|
|
1387
|
+
function applyFont(ctx, options) {
|
|
1388
|
+
ctx.font = `${options.weight} ${options.size}px ${options.family}`;
|
|
1389
|
+
}
|
|
1390
|
+
function wrapText(ctx, text, maxWidth, maxLines) {
|
|
1391
|
+
const trimmed = text.trim();
|
|
1392
|
+
if (!trimmed) {
|
|
1393
|
+
return { lines: [], truncated: false };
|
|
1394
|
+
}
|
|
1395
|
+
const words = trimmed.split(/\s+/u);
|
|
1396
|
+
const lines = [];
|
|
1397
|
+
let current = "";
|
|
1398
|
+
for (const word of words) {
|
|
1399
|
+
const trial = current.length > 0 ? `${current} ${word}` : word;
|
|
1400
|
+
if (ctx.measureText(trial).width <= maxWidth) {
|
|
1401
|
+
current = trial;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (current.length > 0) {
|
|
1405
|
+
lines.push(current);
|
|
1406
|
+
current = word;
|
|
1407
|
+
} else {
|
|
1408
|
+
lines.push(word);
|
|
1409
|
+
current = "";
|
|
1410
|
+
}
|
|
1411
|
+
if (lines.length >= maxLines) {
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (lines.length < maxLines && current.length > 0) {
|
|
1416
|
+
lines.push(current);
|
|
1417
|
+
}
|
|
1418
|
+
const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
|
|
1419
|
+
if (!wasTruncated) {
|
|
1420
|
+
return { lines, truncated: false };
|
|
1421
|
+
}
|
|
1422
|
+
const lastIndex = lines.length - 1;
|
|
1423
|
+
let truncatedLine = `${lines[lastIndex]}\u2026`;
|
|
1424
|
+
while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
|
|
1425
|
+
truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
|
|
1426
|
+
}
|
|
1427
|
+
lines[lastIndex] = truncatedLine;
|
|
1428
|
+
return { lines, truncated: true };
|
|
1429
|
+
}
|
|
1430
|
+
function drawTextBlock(ctx, options) {
|
|
1431
|
+
applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
|
|
1432
|
+
const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
|
|
1433
|
+
ctx.fillStyle = options.color;
|
|
1434
|
+
for (const [index, line] of wrapped.lines.entries()) {
|
|
1435
|
+
ctx.fillText(line, options.x, options.y + index * options.lineHeight);
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
height: wrapped.lines.length * options.lineHeight,
|
|
1439
|
+
truncated: wrapped.truncated
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function drawTextLabel(ctx, text, position, options) {
|
|
1443
|
+
applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
|
|
1444
|
+
const textWidth = Math.ceil(ctx.measureText(text).width);
|
|
1445
|
+
const rect = {
|
|
1446
|
+
x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
|
|
1447
|
+
y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
|
|
1448
|
+
width: textWidth + options.padding * 2,
|
|
1449
|
+
height: options.fontSize + options.padding * 2
|
|
1450
|
+
};
|
|
1451
|
+
drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
|
|
1452
|
+
ctx.fillStyle = options.color;
|
|
1453
|
+
ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
|
|
1454
|
+
return rect;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/renderers/flow-node.ts
|
|
1458
|
+
var BADGE_FONT_SIZE = 10;
|
|
1459
|
+
var BADGE_FONT_WEIGHT = 600;
|
|
1460
|
+
var BADGE_LETTER_SPACING = 1;
|
|
1461
|
+
var BADGE_PADDING_X = 8;
|
|
1462
|
+
var BADGE_PADDING_Y = 3;
|
|
1463
|
+
var BADGE_BORDER_RADIUS = 12;
|
|
1464
|
+
var BADGE_DEFAULT_COLOR = "#FFFFFF";
|
|
1465
|
+
var BADGE_PILL_HEIGHT = BADGE_FONT_SIZE + BADGE_PADDING_Y * 2;
|
|
1466
|
+
var BADGE_INSIDE_TOP_EXTRA = BADGE_PILL_HEIGHT + 6;
|
|
1467
|
+
function drawNodeShape(ctx, shape, bounds, fill, stroke, cornerRadius) {
|
|
1468
|
+
switch (shape) {
|
|
1469
|
+
case "box":
|
|
1470
|
+
drawRoundedRect(ctx, bounds, 0, fill, stroke);
|
|
1471
|
+
break;
|
|
1472
|
+
case "rounded-box":
|
|
1473
|
+
drawRoundedRect(ctx, bounds, cornerRadius, fill, stroke);
|
|
1474
|
+
break;
|
|
1475
|
+
case "diamond":
|
|
1476
|
+
drawDiamond(ctx, bounds, fill, stroke);
|
|
1477
|
+
break;
|
|
1478
|
+
case "circle": {
|
|
1479
|
+
const radius = Math.min(bounds.width, bounds.height) / 2;
|
|
1480
|
+
drawCircle(
|
|
1481
|
+
ctx,
|
|
1482
|
+
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
1483
|
+
radius,
|
|
1484
|
+
fill,
|
|
1485
|
+
stroke
|
|
1486
|
+
);
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
case "pill":
|
|
1490
|
+
drawPill(ctx, bounds, fill, stroke);
|
|
1491
|
+
break;
|
|
1492
|
+
case "cylinder":
|
|
1493
|
+
drawCylinder(ctx, bounds, fill, stroke);
|
|
1494
|
+
break;
|
|
1495
|
+
case "parallelogram":
|
|
1496
|
+
drawParallelogram(ctx, bounds, fill, stroke);
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function measureSpacedText(ctx, text, letterSpacing) {
|
|
1501
|
+
const base = ctx.measureText(text).width;
|
|
1502
|
+
const extraChars = [...text].length - 1;
|
|
1503
|
+
return extraChars > 0 ? base + extraChars * letterSpacing : base;
|
|
1504
|
+
}
|
|
1505
|
+
function drawSpacedText(ctx, text, centerX, centerY, letterSpacing) {
|
|
1506
|
+
const chars = [...text];
|
|
1507
|
+
if (chars.length === 0) return;
|
|
1508
|
+
const totalWidth = measureSpacedText(ctx, text, letterSpacing);
|
|
1509
|
+
let cursorX = centerX - totalWidth / 2;
|
|
1510
|
+
ctx.textAlign = "left";
|
|
1511
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1512
|
+
ctx.fillText(chars[i], cursorX, centerY);
|
|
1513
|
+
cursorX += ctx.measureText(chars[i]).width + (i < chars.length - 1 ? letterSpacing : 0);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function renderBadgePill(ctx, centerX, centerY, text, textColor, background, monoFont) {
|
|
1517
|
+
ctx.save();
|
|
1518
|
+
applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
|
|
1519
|
+
const textWidth = measureSpacedText(ctx, text, BADGE_LETTER_SPACING);
|
|
1520
|
+
const pillWidth = textWidth + BADGE_PADDING_X * 2;
|
|
1521
|
+
const pillHeight = BADGE_PILL_HEIGHT;
|
|
1522
|
+
const pillX = centerX - pillWidth / 2;
|
|
1523
|
+
const pillY = centerY - pillHeight / 2;
|
|
1524
|
+
ctx.fillStyle = background;
|
|
1525
|
+
ctx.beginPath();
|
|
1526
|
+
ctx.roundRect(pillX, pillY, pillWidth, pillHeight, BADGE_BORDER_RADIUS);
|
|
1527
|
+
ctx.fill();
|
|
1528
|
+
ctx.fillStyle = textColor;
|
|
1529
|
+
ctx.textBaseline = "middle";
|
|
1530
|
+
applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
|
|
1531
|
+
drawSpacedText(ctx, text, centerX, centerY, BADGE_LETTER_SPACING);
|
|
1532
|
+
ctx.restore();
|
|
1533
|
+
return pillWidth;
|
|
1534
|
+
}
|
|
1535
|
+
function renderFlowNode(ctx, node, bounds, theme) {
|
|
1536
|
+
const fillColor = node.color ?? theme.surfaceElevated;
|
|
1537
|
+
const borderColor = node.borderColor ?? theme.border;
|
|
1538
|
+
const borderWidth = node.borderWidth ?? 2;
|
|
1539
|
+
const cornerRadius = node.cornerRadius ?? 16;
|
|
1540
|
+
const labelColor = node.labelColor ?? theme.text;
|
|
1541
|
+
const sublabelColor = node.sublabelColor ?? theme.textMuted;
|
|
1542
|
+
const labelFontSize = node.labelFontSize ?? 20;
|
|
1543
|
+
const fillOpacity = node.fillOpacity ?? 1;
|
|
1544
|
+
const hasBadge = !!node.badgeText;
|
|
1545
|
+
const badgePosition = node.badgePosition ?? "inside-top";
|
|
1546
|
+
const badgeColor = node.badgeColor ?? BADGE_DEFAULT_COLOR;
|
|
1547
|
+
const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
|
|
1548
|
+
ctx.save();
|
|
1549
|
+
ctx.lineWidth = borderWidth;
|
|
1550
|
+
if (fillOpacity < 1) {
|
|
1551
|
+
ctx.globalAlpha = node.opacity * fillOpacity;
|
|
1552
|
+
drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
|
|
1553
|
+
ctx.globalAlpha = node.opacity;
|
|
1554
|
+
drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
|
|
1555
|
+
} else {
|
|
1556
|
+
ctx.globalAlpha = node.opacity;
|
|
1557
|
+
drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
|
|
1558
|
+
}
|
|
1559
|
+
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
1560
|
+
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
1561
|
+
const monoFont = resolveFont(theme.fonts.mono, "mono");
|
|
1562
|
+
const centerX = bounds.x + bounds.width / 2;
|
|
1563
|
+
const centerY = bounds.y + bounds.height / 2;
|
|
1564
|
+
const insideTopShift = hasBadge && badgePosition === "inside-top" ? BADGE_INSIDE_TOP_EXTRA / 2 : 0;
|
|
1565
|
+
const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
|
|
1566
|
+
const sublabel2FontSize = node.sublabel2FontSize ?? 11;
|
|
1567
|
+
const sublabel2Color = node.sublabel2Color ?? sublabelColor;
|
|
1568
|
+
const lineCount = node.sublabel2 ? 3 : node.sublabel ? 2 : 1;
|
|
1569
|
+
const labelToSublabelGap = Math.max(20, sublabelFontSize + 6);
|
|
1570
|
+
const sublabelToSublabel2Gap = sublabel2FontSize + 4;
|
|
1571
|
+
let textBlockHeight;
|
|
1572
|
+
if (lineCount === 1) {
|
|
1573
|
+
textBlockHeight = labelFontSize;
|
|
1574
|
+
} else if (lineCount === 2) {
|
|
1575
|
+
textBlockHeight = labelFontSize + labelToSublabelGap;
|
|
1576
|
+
} else {
|
|
1577
|
+
textBlockHeight = labelFontSize + labelToSublabelGap + sublabelToSublabel2Gap;
|
|
1578
|
+
}
|
|
1579
|
+
const labelY = lineCount === 1 ? centerY + labelFontSize * 0.3 + insideTopShift : centerY - textBlockHeight / 2 + labelFontSize * 0.8 + insideTopShift;
|
|
1580
|
+
ctx.textAlign = "center";
|
|
1581
|
+
applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
|
|
1582
|
+
ctx.fillStyle = labelColor;
|
|
1583
|
+
ctx.fillText(node.label, centerX, labelY);
|
|
1584
|
+
let textBoundsY = bounds.y + bounds.height / 2 - 18;
|
|
1585
|
+
let textBoundsHeight = 36;
|
|
1586
|
+
if (node.sublabel) {
|
|
1587
|
+
applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
|
|
1588
|
+
ctx.fillStyle = sublabelColor;
|
|
1589
|
+
ctx.fillText(node.sublabel, centerX, labelY + labelToSublabelGap);
|
|
1590
|
+
textBoundsY = bounds.y + bounds.height / 2 - 24;
|
|
1591
|
+
textBoundsHeight = 56;
|
|
1592
|
+
}
|
|
1593
|
+
if (node.sublabel2) {
|
|
1594
|
+
applyFont(ctx, { size: sublabel2FontSize, weight: 500, family: bodyFont });
|
|
1595
|
+
ctx.fillStyle = sublabel2Color;
|
|
1596
|
+
const sublabel2Y = node.sublabel ? labelY + labelToSublabelGap + sublabelToSublabel2Gap : labelY + labelToSublabelGap;
|
|
1597
|
+
ctx.fillText(node.sublabel2, centerX, sublabel2Y);
|
|
1598
|
+
textBoundsY = bounds.y + bounds.height / 2 - 30;
|
|
1599
|
+
textBoundsHeight = 72;
|
|
1600
|
+
}
|
|
1601
|
+
if (hasBadge && node.badgeText) {
|
|
1602
|
+
if (badgePosition === "inside-top") {
|
|
1603
|
+
const badgeCenterY = bounds.y + BADGE_PILL_HEIGHT / 2 + 8;
|
|
1604
|
+
renderBadgePill(
|
|
1605
|
+
ctx,
|
|
1606
|
+
centerX,
|
|
1607
|
+
badgeCenterY,
|
|
1608
|
+
node.badgeText,
|
|
1609
|
+
badgeColor,
|
|
1610
|
+
badgeBackground,
|
|
1611
|
+
monoFont
|
|
1612
|
+
);
|
|
1613
|
+
} else {
|
|
1614
|
+
const badgeCenterY = bounds.y - BADGE_PILL_HEIGHT / 2 - 4;
|
|
1615
|
+
renderBadgePill(
|
|
1616
|
+
ctx,
|
|
1617
|
+
centerX,
|
|
1618
|
+
badgeCenterY,
|
|
1619
|
+
node.badgeText,
|
|
1620
|
+
badgeColor,
|
|
1621
|
+
badgeBackground,
|
|
1622
|
+
monoFont
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
ctx.restore();
|
|
1627
|
+
const effectiveBg = fillOpacity < 1 ? blendColorWithOpacity(fillColor, theme.background, fillOpacity) : fillColor;
|
|
1628
|
+
return [
|
|
1629
|
+
{
|
|
1630
|
+
id: `flow-node-${node.id}`,
|
|
1631
|
+
kind: "flow-node",
|
|
1632
|
+
bounds,
|
|
1633
|
+
foregroundColor: labelColor,
|
|
1634
|
+
backgroundColor: effectiveBg
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
id: `flow-node-${node.id}-label`,
|
|
1638
|
+
kind: "text",
|
|
1639
|
+
bounds: {
|
|
1640
|
+
x: bounds.x + 8,
|
|
1641
|
+
y: textBoundsY,
|
|
1642
|
+
width: bounds.width - 16,
|
|
1643
|
+
height: textBoundsHeight
|
|
1644
|
+
},
|
|
1645
|
+
foregroundColor: labelColor,
|
|
1646
|
+
backgroundColor: effectiveBg
|
|
1647
|
+
}
|
|
1648
|
+
];
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1255
1651
|
// src/layout/estimates.ts
|
|
1256
1652
|
function estimateElementHeight(element) {
|
|
1257
1653
|
switch (element.type) {
|
|
@@ -1350,33 +1746,37 @@ function computeStackLayout(elements, config, safeFrame) {
|
|
|
1350
1746
|
|
|
1351
1747
|
// src/layout/elk.ts
|
|
1352
1748
|
function estimateFlowNodeSize(node) {
|
|
1749
|
+
const badgeExtra = node.badgeText && (node.badgePosition ?? "inside-top") === "inside-top" ? BADGE_INSIDE_TOP_EXTRA : 0;
|
|
1750
|
+
const sublabel2Extra = node.sublabel2 ? (node.sublabel2FontSize ?? 11) + 4 : 0;
|
|
1751
|
+
const extra = badgeExtra + sublabel2Extra;
|
|
1353
1752
|
if (node.width && node.height) {
|
|
1354
|
-
return { width: node.width, height: node.height };
|
|
1753
|
+
return { width: node.width, height: node.height + extra };
|
|
1355
1754
|
}
|
|
1356
1755
|
if (node.width) {
|
|
1756
|
+
const baseHeight = node.shape === "diamond" || node.shape === "circle" ? node.width : 60;
|
|
1357
1757
|
return {
|
|
1358
1758
|
width: node.width,
|
|
1359
|
-
height:
|
|
1759
|
+
height: baseHeight + extra
|
|
1360
1760
|
};
|
|
1361
1761
|
}
|
|
1362
1762
|
if (node.height) {
|
|
1363
1763
|
return {
|
|
1364
1764
|
width: node.shape === "diamond" || node.shape === "circle" ? node.height : 160,
|
|
1365
|
-
height: node.height
|
|
1765
|
+
height: node.height + extra
|
|
1366
1766
|
};
|
|
1367
1767
|
}
|
|
1368
1768
|
switch (node.shape) {
|
|
1369
1769
|
case "diamond":
|
|
1370
1770
|
case "circle":
|
|
1371
|
-
return { width: 100, height: 100 };
|
|
1771
|
+
return { width: 100 + extra, height: 100 + extra };
|
|
1372
1772
|
case "pill":
|
|
1373
|
-
return { width: 180, height: 56 };
|
|
1773
|
+
return { width: 180, height: 56 + extra };
|
|
1374
1774
|
case "cylinder":
|
|
1375
|
-
return { width: 140, height: 92 };
|
|
1775
|
+
return { width: 140, height: 92 + extra };
|
|
1376
1776
|
case "parallelogram":
|
|
1377
|
-
return { width: 180, height: 72 };
|
|
1777
|
+
return { width: 180, height: 72 + extra };
|
|
1378
1778
|
default:
|
|
1379
|
-
return { width: 170, height: 64 };
|
|
1779
|
+
return { width: 170, height: 64 + extra };
|
|
1380
1780
|
}
|
|
1381
1781
|
}
|
|
1382
1782
|
function splitLayoutFrames(safeFrame, direction, hasAuxiliary) {
|
|
@@ -1494,6 +1894,40 @@ function directionToElk(direction) {
|
|
|
1494
1894
|
return "DOWN";
|
|
1495
1895
|
}
|
|
1496
1896
|
}
|
|
1897
|
+
function radialCompactionToElk(compaction) {
|
|
1898
|
+
switch (compaction) {
|
|
1899
|
+
case "radial":
|
|
1900
|
+
return "RADIAL_COMPACTION";
|
|
1901
|
+
case "wedge":
|
|
1902
|
+
return "WEDGE_COMPACTION";
|
|
1903
|
+
default:
|
|
1904
|
+
return "NONE";
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function radialSortByToElk(sortBy) {
|
|
1908
|
+
switch (sortBy) {
|
|
1909
|
+
case "connections":
|
|
1910
|
+
return "POLAR_COORDINATE";
|
|
1911
|
+
default:
|
|
1912
|
+
return "ID";
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function buildRadialOptions(config) {
|
|
1916
|
+
const options = {};
|
|
1917
|
+
if (config.radialRoot) {
|
|
1918
|
+
options["elk.radial.centerOnRoot"] = "true";
|
|
1919
|
+
}
|
|
1920
|
+
if (config.radialRadius != null) {
|
|
1921
|
+
options["elk.radial.radius"] = String(config.radialRadius);
|
|
1922
|
+
}
|
|
1923
|
+
if (config.radialCompaction) {
|
|
1924
|
+
options["elk.radial.compaction.strategy"] = radialCompactionToElk(config.radialCompaction);
|
|
1925
|
+
}
|
|
1926
|
+
if (config.radialSortBy) {
|
|
1927
|
+
options["elk.radial.orderId"] = radialSortByToElk(config.radialSortBy);
|
|
1928
|
+
}
|
|
1929
|
+
return options;
|
|
1930
|
+
}
|
|
1497
1931
|
function fallbackForNoFlowNodes(nonFlow, safeFrame) {
|
|
1498
1932
|
const fallbackConfig = {
|
|
1499
1933
|
mode: "stack",
|
|
@@ -1529,6 +1963,11 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
1529
1963
|
elkNodeSizes.set(node.id, estimateFlowNodeSize(node));
|
|
1530
1964
|
}
|
|
1531
1965
|
const edgeIdToRouteKey = /* @__PURE__ */ new Map();
|
|
1966
|
+
const radialOptions = config.algorithm === "radial" ? buildRadialOptions(config) : {};
|
|
1967
|
+
const orderedFlowNodes = config.radialRoot && config.algorithm === "radial" ? [
|
|
1968
|
+
...flowNodes.filter((node) => node.id === config.radialRoot),
|
|
1969
|
+
...flowNodes.filter((node) => node.id !== config.radialRoot)
|
|
1970
|
+
] : flowNodes;
|
|
1532
1971
|
const elkGraph = {
|
|
1533
1972
|
id: "root",
|
|
1534
1973
|
layoutOptions: {
|
|
@@ -1538,9 +1977,10 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
1538
1977
|
"elk.layered.spacing.nodeNodeBetweenLayers": String(config.rankSpacing),
|
|
1539
1978
|
"elk.edgeRouting": edgeRoutingToElk(config.edgeRouting),
|
|
1540
1979
|
...config.aspectRatio ? { "elk.aspectRatio": String(config.aspectRatio) } : {},
|
|
1541
|
-
...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {}
|
|
1980
|
+
...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {},
|
|
1981
|
+
...radialOptions
|
|
1542
1982
|
},
|
|
1543
|
-
children:
|
|
1983
|
+
children: orderedFlowNodes.map((node) => {
|
|
1544
1984
|
const size = elkNodeSizes.get(node.id) ?? { width: 160, height: 60 };
|
|
1545
1985
|
return {
|
|
1546
1986
|
id: node.id,
|
|
@@ -1853,188 +2293,6 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
|
|
|
1853
2293
|
ctx.restore();
|
|
1854
2294
|
}
|
|
1855
2295
|
|
|
1856
|
-
// src/primitives/shapes.ts
|
|
1857
|
-
function roundRectPath(ctx, rect, radius) {
|
|
1858
|
-
const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
|
|
1859
|
-
const right = rect.x + rect.width;
|
|
1860
|
-
const bottom = rect.y + rect.height;
|
|
1861
|
-
ctx.beginPath();
|
|
1862
|
-
ctx.moveTo(rect.x + r, rect.y);
|
|
1863
|
-
ctx.lineTo(right - r, rect.y);
|
|
1864
|
-
ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
|
|
1865
|
-
ctx.lineTo(right, bottom - r);
|
|
1866
|
-
ctx.quadraticCurveTo(right, bottom, right - r, bottom);
|
|
1867
|
-
ctx.lineTo(rect.x + r, bottom);
|
|
1868
|
-
ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
|
|
1869
|
-
ctx.lineTo(rect.x, rect.y + r);
|
|
1870
|
-
ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
|
|
1871
|
-
ctx.closePath();
|
|
1872
|
-
}
|
|
1873
|
-
function fillAndStroke(ctx, fill, stroke) {
|
|
1874
|
-
ctx.fillStyle = fill;
|
|
1875
|
-
ctx.fill();
|
|
1876
|
-
if (stroke) {
|
|
1877
|
-
ctx.strokeStyle = stroke;
|
|
1878
|
-
ctx.stroke();
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
1882
|
-
roundRectPath(ctx, rect, radius);
|
|
1883
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1884
|
-
}
|
|
1885
|
-
function drawCircle(ctx, center2, radius, fill, stroke) {
|
|
1886
|
-
ctx.beginPath();
|
|
1887
|
-
ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
1888
|
-
ctx.closePath();
|
|
1889
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1890
|
-
}
|
|
1891
|
-
function drawDiamond(ctx, bounds, fill, stroke) {
|
|
1892
|
-
const cx = bounds.x + bounds.width / 2;
|
|
1893
|
-
const cy = bounds.y + bounds.height / 2;
|
|
1894
|
-
ctx.beginPath();
|
|
1895
|
-
ctx.moveTo(cx, bounds.y);
|
|
1896
|
-
ctx.lineTo(bounds.x + bounds.width, cy);
|
|
1897
|
-
ctx.lineTo(cx, bounds.y + bounds.height);
|
|
1898
|
-
ctx.lineTo(bounds.x, cy);
|
|
1899
|
-
ctx.closePath();
|
|
1900
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1901
|
-
}
|
|
1902
|
-
function drawPill(ctx, bounds, fill, stroke) {
|
|
1903
|
-
drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
|
|
1904
|
-
}
|
|
1905
|
-
function drawEllipse(ctx, bounds, fill, stroke) {
|
|
1906
|
-
const cx = bounds.x + bounds.width / 2;
|
|
1907
|
-
const cy = bounds.y + bounds.height / 2;
|
|
1908
|
-
ctx.beginPath();
|
|
1909
|
-
ctx.ellipse(
|
|
1910
|
-
cx,
|
|
1911
|
-
cy,
|
|
1912
|
-
Math.max(0, bounds.width / 2),
|
|
1913
|
-
Math.max(0, bounds.height / 2),
|
|
1914
|
-
0,
|
|
1915
|
-
0,
|
|
1916
|
-
Math.PI * 2
|
|
1917
|
-
);
|
|
1918
|
-
ctx.closePath();
|
|
1919
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1920
|
-
}
|
|
1921
|
-
function drawCylinder(ctx, bounds, fill, stroke) {
|
|
1922
|
-
const rx = Math.max(2, bounds.width / 2);
|
|
1923
|
-
const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
|
|
1924
|
-
const cx = bounds.x + bounds.width / 2;
|
|
1925
|
-
const topCy = bounds.y + ry;
|
|
1926
|
-
const bottomCy = bounds.y + bounds.height - ry;
|
|
1927
|
-
ctx.beginPath();
|
|
1928
|
-
ctx.moveTo(bounds.x, topCy);
|
|
1929
|
-
ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
|
|
1930
|
-
ctx.lineTo(bounds.x + bounds.width, bottomCy);
|
|
1931
|
-
ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
|
|
1932
|
-
ctx.closePath();
|
|
1933
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1934
|
-
if (stroke) {
|
|
1935
|
-
ctx.beginPath();
|
|
1936
|
-
ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
|
|
1937
|
-
ctx.closePath();
|
|
1938
|
-
ctx.strokeStyle = stroke;
|
|
1939
|
-
ctx.stroke();
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
function drawParallelogram(ctx, bounds, fill, stroke, skew) {
|
|
1943
|
-
const maxSkew = bounds.width * 0.45;
|
|
1944
|
-
const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
|
|
1945
|
-
ctx.beginPath();
|
|
1946
|
-
ctx.moveTo(bounds.x + skewX, bounds.y);
|
|
1947
|
-
ctx.lineTo(bounds.x + bounds.width, bounds.y);
|
|
1948
|
-
ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
|
|
1949
|
-
ctx.lineTo(bounds.x, bounds.y + bounds.height);
|
|
1950
|
-
ctx.closePath();
|
|
1951
|
-
fillAndStroke(ctx, fill, stroke);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
// src/primitives/text.ts
|
|
1955
|
-
var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
|
|
1956
|
-
function resolveFont(requested, role) {
|
|
1957
|
-
if (SUPPORTED_FONT_FAMILIES.has(requested)) {
|
|
1958
|
-
return requested;
|
|
1959
|
-
}
|
|
1960
|
-
if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
|
|
1961
|
-
return "JetBrains Mono";
|
|
1962
|
-
}
|
|
1963
|
-
if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
|
|
1964
|
-
return "Space Grotesk";
|
|
1965
|
-
}
|
|
1966
|
-
return "Inter";
|
|
1967
|
-
}
|
|
1968
|
-
function applyFont(ctx, options) {
|
|
1969
|
-
ctx.font = `${options.weight} ${options.size}px ${options.family}`;
|
|
1970
|
-
}
|
|
1971
|
-
function wrapText(ctx, text, maxWidth, maxLines) {
|
|
1972
|
-
const trimmed = text.trim();
|
|
1973
|
-
if (!trimmed) {
|
|
1974
|
-
return { lines: [], truncated: false };
|
|
1975
|
-
}
|
|
1976
|
-
const words = trimmed.split(/\s+/u);
|
|
1977
|
-
const lines = [];
|
|
1978
|
-
let current = "";
|
|
1979
|
-
for (const word of words) {
|
|
1980
|
-
const trial = current.length > 0 ? `${current} ${word}` : word;
|
|
1981
|
-
if (ctx.measureText(trial).width <= maxWidth) {
|
|
1982
|
-
current = trial;
|
|
1983
|
-
continue;
|
|
1984
|
-
}
|
|
1985
|
-
if (current.length > 0) {
|
|
1986
|
-
lines.push(current);
|
|
1987
|
-
current = word;
|
|
1988
|
-
} else {
|
|
1989
|
-
lines.push(word);
|
|
1990
|
-
current = "";
|
|
1991
|
-
}
|
|
1992
|
-
if (lines.length >= maxLines) {
|
|
1993
|
-
break;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
if (lines.length < maxLines && current.length > 0) {
|
|
1997
|
-
lines.push(current);
|
|
1998
|
-
}
|
|
1999
|
-
const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
|
|
2000
|
-
if (!wasTruncated) {
|
|
2001
|
-
return { lines, truncated: false };
|
|
2002
|
-
}
|
|
2003
|
-
const lastIndex = lines.length - 1;
|
|
2004
|
-
let truncatedLine = `${lines[lastIndex]}\u2026`;
|
|
2005
|
-
while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
|
|
2006
|
-
truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
|
|
2007
|
-
}
|
|
2008
|
-
lines[lastIndex] = truncatedLine;
|
|
2009
|
-
return { lines, truncated: true };
|
|
2010
|
-
}
|
|
2011
|
-
function drawTextBlock(ctx, options) {
|
|
2012
|
-
applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
|
|
2013
|
-
const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
|
|
2014
|
-
ctx.fillStyle = options.color;
|
|
2015
|
-
for (const [index, line] of wrapped.lines.entries()) {
|
|
2016
|
-
ctx.fillText(line, options.x, options.y + index * options.lineHeight);
|
|
2017
|
-
}
|
|
2018
|
-
return {
|
|
2019
|
-
height: wrapped.lines.length * options.lineHeight,
|
|
2020
|
-
truncated: wrapped.truncated
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
|
-
function drawTextLabel(ctx, text, position, options) {
|
|
2024
|
-
applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
|
|
2025
|
-
const textWidth = Math.ceil(ctx.measureText(text).width);
|
|
2026
|
-
const rect = {
|
|
2027
|
-
x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
|
|
2028
|
-
y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
|
|
2029
|
-
width: textWidth + options.padding * 2,
|
|
2030
|
-
height: options.fontSize + options.padding * 2
|
|
2031
|
-
};
|
|
2032
|
-
drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
|
|
2033
|
-
ctx.fillStyle = options.color;
|
|
2034
|
-
ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
|
|
2035
|
-
return rect;
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
2296
|
// src/renderers/card.ts
|
|
2039
2297
|
var TONE_BADGE_COLORS = {
|
|
2040
2298
|
neutral: "#334B83",
|
|
@@ -3362,92 +3620,6 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
3362
3620
|
return rendered;
|
|
3363
3621
|
}
|
|
3364
3622
|
|
|
3365
|
-
// src/renderers/flow-node.ts
|
|
3366
|
-
function renderFlowNode(ctx, node, bounds, theme) {
|
|
3367
|
-
const fillColor = node.color ?? theme.surfaceElevated;
|
|
3368
|
-
const borderColor = node.borderColor ?? theme.border;
|
|
3369
|
-
const borderWidth = node.borderWidth ?? 2;
|
|
3370
|
-
const cornerRadius = node.cornerRadius ?? 16;
|
|
3371
|
-
const labelColor = node.labelColor ?? theme.text;
|
|
3372
|
-
const sublabelColor = node.sublabelColor ?? theme.textMuted;
|
|
3373
|
-
const labelFontSize = node.labelFontSize ?? 20;
|
|
3374
|
-
ctx.save();
|
|
3375
|
-
ctx.globalAlpha = node.opacity;
|
|
3376
|
-
ctx.lineWidth = borderWidth;
|
|
3377
|
-
switch (node.shape) {
|
|
3378
|
-
case "box":
|
|
3379
|
-
drawRoundedRect(ctx, bounds, 0, fillColor, borderColor);
|
|
3380
|
-
break;
|
|
3381
|
-
case "rounded-box":
|
|
3382
|
-
drawRoundedRect(ctx, bounds, cornerRadius, fillColor, borderColor);
|
|
3383
|
-
break;
|
|
3384
|
-
case "diamond":
|
|
3385
|
-
drawDiamond(ctx, bounds, fillColor, borderColor);
|
|
3386
|
-
break;
|
|
3387
|
-
case "circle": {
|
|
3388
|
-
const radius = Math.min(bounds.width, bounds.height) / 2;
|
|
3389
|
-
drawCircle(
|
|
3390
|
-
ctx,
|
|
3391
|
-
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
3392
|
-
radius,
|
|
3393
|
-
fillColor,
|
|
3394
|
-
borderColor
|
|
3395
|
-
);
|
|
3396
|
-
break;
|
|
3397
|
-
}
|
|
3398
|
-
case "pill":
|
|
3399
|
-
drawPill(ctx, bounds, fillColor, borderColor);
|
|
3400
|
-
break;
|
|
3401
|
-
case "cylinder":
|
|
3402
|
-
drawCylinder(ctx, bounds, fillColor, borderColor);
|
|
3403
|
-
break;
|
|
3404
|
-
case "parallelogram":
|
|
3405
|
-
drawParallelogram(ctx, bounds, fillColor, borderColor);
|
|
3406
|
-
break;
|
|
3407
|
-
}
|
|
3408
|
-
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
3409
|
-
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
3410
|
-
const centerX = bounds.x + bounds.width / 2;
|
|
3411
|
-
const centerY = bounds.y + bounds.height / 2;
|
|
3412
|
-
const labelY = node.sublabel ? centerY - Math.max(4, labelFontSize * 0.2) : centerY + labelFontSize * 0.3;
|
|
3413
|
-
ctx.textAlign = "center";
|
|
3414
|
-
applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
|
|
3415
|
-
ctx.fillStyle = labelColor;
|
|
3416
|
-
ctx.fillText(node.label, centerX, labelY);
|
|
3417
|
-
let textBoundsY = bounds.y + bounds.height / 2 - 18;
|
|
3418
|
-
let textBoundsHeight = 36;
|
|
3419
|
-
if (node.sublabel) {
|
|
3420
|
-
const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
|
|
3421
|
-
applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
|
|
3422
|
-
ctx.fillStyle = sublabelColor;
|
|
3423
|
-
ctx.fillText(node.sublabel, centerX, labelY + Math.max(20, sublabelFontSize + 6));
|
|
3424
|
-
textBoundsY = bounds.y + bounds.height / 2 - 24;
|
|
3425
|
-
textBoundsHeight = 56;
|
|
3426
|
-
}
|
|
3427
|
-
ctx.restore();
|
|
3428
|
-
return [
|
|
3429
|
-
{
|
|
3430
|
-
id: `flow-node-${node.id}`,
|
|
3431
|
-
kind: "flow-node",
|
|
3432
|
-
bounds,
|
|
3433
|
-
foregroundColor: labelColor,
|
|
3434
|
-
backgroundColor: fillColor
|
|
3435
|
-
},
|
|
3436
|
-
{
|
|
3437
|
-
id: `flow-node-${node.id}-label`,
|
|
3438
|
-
kind: "text",
|
|
3439
|
-
bounds: {
|
|
3440
|
-
x: bounds.x + 8,
|
|
3441
|
-
y: textBoundsY,
|
|
3442
|
-
width: bounds.width - 16,
|
|
3443
|
-
height: textBoundsHeight
|
|
3444
|
-
},
|
|
3445
|
-
foregroundColor: labelColor,
|
|
3446
|
-
backgroundColor: fillColor
|
|
3447
|
-
}
|
|
3448
|
-
];
|
|
3449
|
-
}
|
|
3450
|
-
|
|
3451
3623
|
// src/renderers/image.ts
|
|
3452
3624
|
import { loadImage } from "@napi-rs/canvas";
|
|
3453
3625
|
function roundedRectPath2(ctx, bounds, radius) {
|
|
@@ -4827,7 +4999,6 @@ cli.command("publish", {
|
|
|
4827
4999
|
issueCount: z3.number()
|
|
4828
5000
|
}),
|
|
4829
5001
|
publish: z3.object({
|
|
4830
|
-
attempts: z3.number(),
|
|
4831
5002
|
summary: z3.string(),
|
|
4832
5003
|
url: z3.string().optional()
|
|
4833
5004
|
})
|
|
@@ -4880,7 +5051,6 @@ cli.command("publish", {
|
|
|
4880
5051
|
issueCount: qa.issues.length
|
|
4881
5052
|
},
|
|
4882
5053
|
publish: {
|
|
4883
|
-
attempts: gist.attempts,
|
|
4884
5054
|
summary: `Published ${gist.files.length} files to gist ${gist.gistId}.`,
|
|
4885
5055
|
url: gist.htmlUrl
|
|
4886
5056
|
}
|
|
@@ -4893,7 +5063,7 @@ cli.command("publish", {
|
|
|
4893
5063
|
retryable: false
|
|
4894
5064
|
});
|
|
4895
5065
|
}
|
|
4896
|
-
const
|
|
5066
|
+
const github3 = await publishToGitHub({
|
|
4897
5067
|
imagePath,
|
|
4898
5068
|
metadataPath,
|
|
4899
5069
|
repo: c.options.repo,
|
|
@@ -4901,7 +5071,7 @@ cli.command("publish", {
|
|
|
4901
5071
|
...c.options.pathPrefix ? { pathPrefix: c.options.pathPrefix } : {},
|
|
4902
5072
|
...c.options.description ? { commitMessage: c.options.description } : {}
|
|
4903
5073
|
});
|
|
4904
|
-
const url =
|
|
5074
|
+
const url = github3.files.find((file) => file.htmlUrl)?.htmlUrl;
|
|
4905
5075
|
return c.ok({
|
|
4906
5076
|
target: "github",
|
|
4907
5077
|
qa: {
|
|
@@ -4909,8 +5079,7 @@ cli.command("publish", {
|
|
|
4909
5079
|
issueCount: qa.issues.length
|
|
4910
5080
|
},
|
|
4911
5081
|
publish: {
|
|
4912
|
-
|
|
4913
|
-
summary: `Published ${github.files.length} files to ${github.repo}@${github.branch}.`,
|
|
5082
|
+
summary: `Published ${github3.files.length} files to ${github3.repo}@${github3.branch}.`,
|
|
4914
5083
|
url
|
|
4915
5084
|
}
|
|
4916
5085
|
});
|