@spectratools/graphic-designer-cli 0.3.2 → 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 CHANGED
@@ -259,7 +259,106 @@ import { z as z2 } from "zod";
259
259
 
260
260
  // src/themes/builtin.ts
261
261
  import { z } from "zod";
262
- var colorHexSchema = z.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
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));
263
362
  var fontFamilySchema = z.string().min(1).max(120);
264
363
  var codeThemeSchema = z.object({
265
364
  background: colorHexSchema,
@@ -480,7 +579,17 @@ function resolveTheme(theme) {
480
579
  }
481
580
 
482
581
  // src/spec.schema.ts
483
- var colorHexSchema2 = z2.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
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));
484
593
  var gradientStopSchema = z2.object({
485
594
  offset: z2.number().min(0).max(1),
486
595
  color: colorHexSchema2
@@ -671,6 +780,9 @@ var flowNodeElementSchema = z2.object({
671
780
  label: z2.string().min(1).max(200),
672
781
  sublabel: z2.string().min(1).max(300).optional(),
673
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(),
674
786
  labelColor: colorHexSchema2.optional(),
675
787
  labelFontSize: z2.number().min(10).max(48).optional(),
676
788
  color: colorHexSchema2.optional(),
@@ -679,7 +791,12 @@ var flowNodeElementSchema = z2.object({
679
791
  cornerRadius: z2.number().min(0).max(64).optional(),
680
792
  width: z2.number().int().min(40).max(800).optional(),
681
793
  height: z2.number().int().min(30).max(600).optional(),
682
- opacity: z2.number().min(0).max(1).default(1)
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")
683
800
  }).strict();
684
801
  var connectionElementSchema = z2.object({
685
802
  type: z2.literal("connection"),
@@ -768,7 +885,15 @@ var autoLayoutConfigSchema = z2.object({
768
885
  nodeSpacing: z2.number().int().min(0).max(512).default(80),
769
886
  rankSpacing: z2.number().int().min(0).max(512).default(120),
770
887
  edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
771
- 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()
772
897
  }).strict();
773
898
  var gridLayoutConfigSchema = z2.object({
774
899
  mode: z2.literal("grid"),
@@ -872,43 +997,6 @@ function parseDesignSpec(input) {
872
997
  return designSpecSchema.parse(input);
873
998
  }
874
999
 
875
- // src/utils/color.ts
876
- function parseChannel(hex, offset) {
877
- return Number.parseInt(hex.slice(offset, offset + 2), 16);
878
- }
879
- function parseHexColor(hexColor) {
880
- const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
881
- if (normalized.length !== 6 && normalized.length !== 8) {
882
- throw new Error(`Unsupported color format: ${hexColor}`);
883
- }
884
- return {
885
- r: parseChannel(normalized, 0),
886
- g: parseChannel(normalized, 2),
887
- b: parseChannel(normalized, 4)
888
- };
889
- }
890
- function srgbToLinear(channel) {
891
- const normalized = channel / 255;
892
- if (normalized <= 0.03928) {
893
- return normalized / 12.92;
894
- }
895
- return ((normalized + 0.055) / 1.055) ** 2.4;
896
- }
897
- function relativeLuminance(hexColor) {
898
- const rgb = parseHexColor(hexColor);
899
- const r = srgbToLinear(rgb.r);
900
- const g = srgbToLinear(rgb.g);
901
- const b = srgbToLinear(rgb.b);
902
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
903
- }
904
- function contrastRatio(foreground, background) {
905
- const fg = relativeLuminance(foreground);
906
- const bg = relativeLuminance(background);
907
- const lighter = Math.max(fg, bg);
908
- const darker = Math.min(fg, bg);
909
- return (lighter + 0.05) / (darker + 0.05);
910
- }
911
-
912
1000
  // src/qa.ts
913
1001
  function rectWithin(outer, inner) {
914
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;
@@ -1175,6 +1263,382 @@ function loadFonts() {
1175
1263
  // src/layout/elk.ts
1176
1264
  import ELK from "elkjs";
1177
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
+
1178
1642
  // src/layout/estimates.ts
1179
1643
  function estimateElementHeight(element) {
1180
1644
  switch (element.type) {
@@ -1273,33 +1737,37 @@ function computeStackLayout(elements, config, safeFrame) {
1273
1737
 
1274
1738
  // src/layout/elk.ts
1275
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;
1276
1743
  if (node.width && node.height) {
1277
- return { width: node.width, height: node.height };
1744
+ return { width: node.width, height: node.height + extra };
1278
1745
  }
1279
1746
  if (node.width) {
1747
+ const baseHeight = node.shape === "diamond" || node.shape === "circle" ? node.width : 60;
1280
1748
  return {
1281
1749
  width: node.width,
1282
- height: node.shape === "diamond" || node.shape === "circle" ? node.width : 60
1750
+ height: baseHeight + extra
1283
1751
  };
1284
1752
  }
1285
1753
  if (node.height) {
1286
1754
  return {
1287
1755
  width: node.shape === "diamond" || node.shape === "circle" ? node.height : 160,
1288
- height: node.height
1756
+ height: node.height + extra
1289
1757
  };
1290
1758
  }
1291
1759
  switch (node.shape) {
1292
1760
  case "diamond":
1293
1761
  case "circle":
1294
- return { width: 100, height: 100 };
1762
+ return { width: 100 + extra, height: 100 + extra };
1295
1763
  case "pill":
1296
- return { width: 180, height: 56 };
1764
+ return { width: 180, height: 56 + extra };
1297
1765
  case "cylinder":
1298
- return { width: 140, height: 92 };
1766
+ return { width: 140, height: 92 + extra };
1299
1767
  case "parallelogram":
1300
- return { width: 180, height: 72 };
1768
+ return { width: 180, height: 72 + extra };
1301
1769
  default:
1302
- return { width: 170, height: 64 };
1770
+ return { width: 170, height: 64 + extra };
1303
1771
  }
1304
1772
  }
1305
1773
  function splitLayoutFrames(safeFrame, direction, hasAuxiliary) {
@@ -1417,6 +1885,40 @@ function directionToElk(direction) {
1417
1885
  return "DOWN";
1418
1886
  }
1419
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
+ }
1420
1922
  function fallbackForNoFlowNodes(nonFlow, safeFrame) {
1421
1923
  const fallbackConfig = {
1422
1924
  mode: "stack",
@@ -1452,6 +1954,11 @@ async function computeElkLayout(elements, config, safeFrame) {
1452
1954
  elkNodeSizes.set(node.id, estimateFlowNodeSize(node));
1453
1955
  }
1454
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;
1455
1962
  const elkGraph = {
1456
1963
  id: "root",
1457
1964
  layoutOptions: {
@@ -1461,9 +1968,10 @@ async function computeElkLayout(elements, config, safeFrame) {
1461
1968
  "elk.layered.spacing.nodeNodeBetweenLayers": String(config.rankSpacing),
1462
1969
  "elk.edgeRouting": edgeRoutingToElk(config.edgeRouting),
1463
1970
  ...config.aspectRatio ? { "elk.aspectRatio": String(config.aspectRatio) } : {},
1464
- ...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
1465
1973
  },
1466
- children: flowNodes.map((node) => {
1974
+ children: orderedFlowNodes.map((node) => {
1467
1975
  const size = elkNodeSizes.get(node.id) ?? { width: 160, height: 60 };
1468
1976
  return {
1469
1977
  id: node.id,
@@ -1776,188 +2284,6 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
1776
2284
  ctx.restore();
1777
2285
  }
1778
2286
 
1779
- // src/primitives/shapes.ts
1780
- function roundRectPath(ctx, rect, radius) {
1781
- const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
1782
- const right = rect.x + rect.width;
1783
- const bottom = rect.y + rect.height;
1784
- ctx.beginPath();
1785
- ctx.moveTo(rect.x + r, rect.y);
1786
- ctx.lineTo(right - r, rect.y);
1787
- ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
1788
- ctx.lineTo(right, bottom - r);
1789
- ctx.quadraticCurveTo(right, bottom, right - r, bottom);
1790
- ctx.lineTo(rect.x + r, bottom);
1791
- ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
1792
- ctx.lineTo(rect.x, rect.y + r);
1793
- ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
1794
- ctx.closePath();
1795
- }
1796
- function fillAndStroke(ctx, fill, stroke) {
1797
- ctx.fillStyle = fill;
1798
- ctx.fill();
1799
- if (stroke) {
1800
- ctx.strokeStyle = stroke;
1801
- ctx.stroke();
1802
- }
1803
- }
1804
- function drawRoundedRect(ctx, rect, radius, fill, stroke) {
1805
- roundRectPath(ctx, rect, radius);
1806
- fillAndStroke(ctx, fill, stroke);
1807
- }
1808
- function drawCircle(ctx, center2, radius, fill, stroke) {
1809
- ctx.beginPath();
1810
- ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
1811
- ctx.closePath();
1812
- fillAndStroke(ctx, fill, stroke);
1813
- }
1814
- function drawDiamond(ctx, bounds, fill, stroke) {
1815
- const cx = bounds.x + bounds.width / 2;
1816
- const cy = bounds.y + bounds.height / 2;
1817
- ctx.beginPath();
1818
- ctx.moveTo(cx, bounds.y);
1819
- ctx.lineTo(bounds.x + bounds.width, cy);
1820
- ctx.lineTo(cx, bounds.y + bounds.height);
1821
- ctx.lineTo(bounds.x, cy);
1822
- ctx.closePath();
1823
- fillAndStroke(ctx, fill, stroke);
1824
- }
1825
- function drawPill(ctx, bounds, fill, stroke) {
1826
- drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
1827
- }
1828
- function drawEllipse(ctx, bounds, fill, stroke) {
1829
- const cx = bounds.x + bounds.width / 2;
1830
- const cy = bounds.y + bounds.height / 2;
1831
- ctx.beginPath();
1832
- ctx.ellipse(
1833
- cx,
1834
- cy,
1835
- Math.max(0, bounds.width / 2),
1836
- Math.max(0, bounds.height / 2),
1837
- 0,
1838
- 0,
1839
- Math.PI * 2
1840
- );
1841
- ctx.closePath();
1842
- fillAndStroke(ctx, fill, stroke);
1843
- }
1844
- function drawCylinder(ctx, bounds, fill, stroke) {
1845
- const rx = Math.max(2, bounds.width / 2);
1846
- const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
1847
- const cx = bounds.x + bounds.width / 2;
1848
- const topCy = bounds.y + ry;
1849
- const bottomCy = bounds.y + bounds.height - ry;
1850
- ctx.beginPath();
1851
- ctx.moveTo(bounds.x, topCy);
1852
- ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
1853
- ctx.lineTo(bounds.x + bounds.width, bottomCy);
1854
- ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
1855
- ctx.closePath();
1856
- fillAndStroke(ctx, fill, stroke);
1857
- if (stroke) {
1858
- ctx.beginPath();
1859
- ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
1860
- ctx.closePath();
1861
- ctx.strokeStyle = stroke;
1862
- ctx.stroke();
1863
- }
1864
- }
1865
- function drawParallelogram(ctx, bounds, fill, stroke, skew) {
1866
- const maxSkew = bounds.width * 0.45;
1867
- const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
1868
- ctx.beginPath();
1869
- ctx.moveTo(bounds.x + skewX, bounds.y);
1870
- ctx.lineTo(bounds.x + bounds.width, bounds.y);
1871
- ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
1872
- ctx.lineTo(bounds.x, bounds.y + bounds.height);
1873
- ctx.closePath();
1874
- fillAndStroke(ctx, fill, stroke);
1875
- }
1876
-
1877
- // src/primitives/text.ts
1878
- var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
1879
- function resolveFont(requested, role) {
1880
- if (SUPPORTED_FONT_FAMILIES.has(requested)) {
1881
- return requested;
1882
- }
1883
- if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
1884
- return "JetBrains Mono";
1885
- }
1886
- if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
1887
- return "Space Grotesk";
1888
- }
1889
- return "Inter";
1890
- }
1891
- function applyFont(ctx, options) {
1892
- ctx.font = `${options.weight} ${options.size}px ${options.family}`;
1893
- }
1894
- function wrapText(ctx, text, maxWidth, maxLines) {
1895
- const trimmed = text.trim();
1896
- if (!trimmed) {
1897
- return { lines: [], truncated: false };
1898
- }
1899
- const words = trimmed.split(/\s+/u);
1900
- const lines = [];
1901
- let current = "";
1902
- for (const word of words) {
1903
- const trial = current.length > 0 ? `${current} ${word}` : word;
1904
- if (ctx.measureText(trial).width <= maxWidth) {
1905
- current = trial;
1906
- continue;
1907
- }
1908
- if (current.length > 0) {
1909
- lines.push(current);
1910
- current = word;
1911
- } else {
1912
- lines.push(word);
1913
- current = "";
1914
- }
1915
- if (lines.length >= maxLines) {
1916
- break;
1917
- }
1918
- }
1919
- if (lines.length < maxLines && current.length > 0) {
1920
- lines.push(current);
1921
- }
1922
- const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
1923
- if (!wasTruncated) {
1924
- return { lines, truncated: false };
1925
- }
1926
- const lastIndex = lines.length - 1;
1927
- let truncatedLine = `${lines[lastIndex]}\u2026`;
1928
- while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
1929
- truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
1930
- }
1931
- lines[lastIndex] = truncatedLine;
1932
- return { lines, truncated: true };
1933
- }
1934
- function drawTextBlock(ctx, options) {
1935
- applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
1936
- const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
1937
- ctx.fillStyle = options.color;
1938
- for (const [index, line] of wrapped.lines.entries()) {
1939
- ctx.fillText(line, options.x, options.y + index * options.lineHeight);
1940
- }
1941
- return {
1942
- height: wrapped.lines.length * options.lineHeight,
1943
- truncated: wrapped.truncated
1944
- };
1945
- }
1946
- function drawTextLabel(ctx, text, position, options) {
1947
- applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
1948
- const textWidth = Math.ceil(ctx.measureText(text).width);
1949
- const rect = {
1950
- x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
1951
- y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
1952
- width: textWidth + options.padding * 2,
1953
- height: options.fontSize + options.padding * 2
1954
- };
1955
- drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
1956
- ctx.fillStyle = options.color;
1957
- ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
1958
- return rect;
1959
- }
1960
-
1961
2287
  // src/renderers/card.ts
1962
2288
  var TONE_BADGE_COLORS = {
1963
2289
  neutral: "#334B83",
@@ -3281,92 +3607,6 @@ function renderDrawCommands(ctx, commands, theme) {
3281
3607
  return rendered;
3282
3608
  }
3283
3609
 
3284
- // src/renderers/flow-node.ts
3285
- function renderFlowNode(ctx, node, bounds, theme) {
3286
- const fillColor = node.color ?? theme.surfaceElevated;
3287
- const borderColor = node.borderColor ?? theme.border;
3288
- const borderWidth = node.borderWidth ?? 2;
3289
- const cornerRadius = node.cornerRadius ?? 16;
3290
- const labelColor = node.labelColor ?? theme.text;
3291
- const sublabelColor = node.sublabelColor ?? theme.textMuted;
3292
- const labelFontSize = node.labelFontSize ?? 20;
3293
- ctx.save();
3294
- ctx.globalAlpha = node.opacity;
3295
- ctx.lineWidth = borderWidth;
3296
- switch (node.shape) {
3297
- case "box":
3298
- drawRoundedRect(ctx, bounds, 0, fillColor, borderColor);
3299
- break;
3300
- case "rounded-box":
3301
- drawRoundedRect(ctx, bounds, cornerRadius, fillColor, borderColor);
3302
- break;
3303
- case "diamond":
3304
- drawDiamond(ctx, bounds, fillColor, borderColor);
3305
- break;
3306
- case "circle": {
3307
- const radius = Math.min(bounds.width, bounds.height) / 2;
3308
- drawCircle(
3309
- ctx,
3310
- { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
3311
- radius,
3312
- fillColor,
3313
- borderColor
3314
- );
3315
- break;
3316
- }
3317
- case "pill":
3318
- drawPill(ctx, bounds, fillColor, borderColor);
3319
- break;
3320
- case "cylinder":
3321
- drawCylinder(ctx, bounds, fillColor, borderColor);
3322
- break;
3323
- case "parallelogram":
3324
- drawParallelogram(ctx, bounds, fillColor, borderColor);
3325
- break;
3326
- }
3327
- const headingFont = resolveFont(theme.fonts.heading, "heading");
3328
- const bodyFont = resolveFont(theme.fonts.body, "body");
3329
- const centerX = bounds.x + bounds.width / 2;
3330
- const centerY = bounds.y + bounds.height / 2;
3331
- const labelY = node.sublabel ? centerY - Math.max(4, labelFontSize * 0.2) : centerY + labelFontSize * 0.3;
3332
- ctx.textAlign = "center";
3333
- applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
3334
- ctx.fillStyle = labelColor;
3335
- ctx.fillText(node.label, centerX, labelY);
3336
- let textBoundsY = bounds.y + bounds.height / 2 - 18;
3337
- let textBoundsHeight = 36;
3338
- if (node.sublabel) {
3339
- const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
3340
- applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
3341
- ctx.fillStyle = sublabelColor;
3342
- ctx.fillText(node.sublabel, centerX, labelY + Math.max(20, sublabelFontSize + 6));
3343
- textBoundsY = bounds.y + bounds.height / 2 - 24;
3344
- textBoundsHeight = 56;
3345
- }
3346
- ctx.restore();
3347
- return [
3348
- {
3349
- id: `flow-node-${node.id}`,
3350
- kind: "flow-node",
3351
- bounds,
3352
- foregroundColor: labelColor,
3353
- backgroundColor: fillColor
3354
- },
3355
- {
3356
- id: `flow-node-${node.id}-label`,
3357
- kind: "text",
3358
- bounds: {
3359
- x: bounds.x + 8,
3360
- y: textBoundsY,
3361
- width: bounds.width - 16,
3362
- height: textBoundsHeight
3363
- },
3364
- foregroundColor: labelColor,
3365
- backgroundColor: fillColor
3366
- }
3367
- ];
3368
- }
3369
-
3370
3610
  // src/renderers/image.ts
3371
3611
  import { loadImage } from "@napi-rs/canvas";
3372
3612
  function roundedRectPath2(ctx, bounds, radius) {