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