@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/index.js CHANGED
@@ -257,7 +257,106 @@ import { z as z2 } from "zod";
257
257
 
258
258
  // src/themes/builtin.ts
259
259
  import { z } from "zod";
260
- var colorHexSchema = z.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
260
+
261
+ // src/utils/color.ts
262
+ function parseChannel(hex, offset) {
263
+ return Number.parseInt(hex.slice(offset, offset + 2), 16);
264
+ }
265
+ function parseHexColor(hexColor) {
266
+ const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
267
+ if (normalized.length !== 6 && normalized.length !== 8) {
268
+ throw new Error(`Unsupported color format: ${hexColor}`);
269
+ }
270
+ return {
271
+ r: parseChannel(normalized, 0),
272
+ g: parseChannel(normalized, 2),
273
+ b: parseChannel(normalized, 4)
274
+ };
275
+ }
276
+ var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
277
+ var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
278
+ function toHex(n) {
279
+ return n.toString(16).padStart(2, "0");
280
+ }
281
+ function parseRgbaToHex(color) {
282
+ const match = rgbaRegex.exec(color);
283
+ if (!match) {
284
+ throw new Error(`Invalid rgb/rgba color: ${color}`);
285
+ }
286
+ const r = Number.parseInt(match[1], 10);
287
+ const g = Number.parseInt(match[2], 10);
288
+ const b = Number.parseInt(match[3], 10);
289
+ if (r > 255 || g > 255 || b > 255) {
290
+ throw new Error(`RGB channel values must be 0-255, got: ${color}`);
291
+ }
292
+ if (match[4] !== void 0) {
293
+ const a = Number.parseFloat(match[4]);
294
+ if (a < 0 || a > 1) {
295
+ throw new Error(`Alpha value must be 0-1, got: ${a}`);
296
+ }
297
+ const alphaByte = Math.round(a * 255);
298
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
299
+ }
300
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
301
+ }
302
+ function isRgbaColor(color) {
303
+ return rgbaRegex.test(color);
304
+ }
305
+ function isHexColor(color) {
306
+ return hexColorRegex.test(color);
307
+ }
308
+ function normalizeColor(color) {
309
+ if (isHexColor(color)) {
310
+ return color;
311
+ }
312
+ if (isRgbaColor(color)) {
313
+ return parseRgbaToHex(color);
314
+ }
315
+ throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
316
+ }
317
+ function srgbToLinear(channel) {
318
+ const normalized = channel / 255;
319
+ if (normalized <= 0.03928) {
320
+ return normalized / 12.92;
321
+ }
322
+ return ((normalized + 0.055) / 1.055) ** 2.4;
323
+ }
324
+ function relativeLuminance(hexColor) {
325
+ const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
326
+ const rgb = parseHexColor(normalized);
327
+ const r = srgbToLinear(rgb.r);
328
+ const g = srgbToLinear(rgb.g);
329
+ const b = srgbToLinear(rgb.b);
330
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
331
+ }
332
+ function contrastRatio(foreground, background) {
333
+ const fg = relativeLuminance(foreground);
334
+ const bg = relativeLuminance(background);
335
+ const lighter = Math.max(fg, bg);
336
+ const darker = Math.min(fg, bg);
337
+ return (lighter + 0.05) / (darker + 0.05);
338
+ }
339
+ function blendColorWithOpacity(foreground, background, opacity) {
340
+ const fg = parseHexColor(foreground);
341
+ const bg = parseHexColor(background);
342
+ const r = Math.round(fg.r * opacity + bg.r * (1 - opacity));
343
+ const g = Math.round(fg.g * opacity + bg.g * (1 - opacity));
344
+ const b = Math.round(fg.b * opacity + bg.b * (1 - opacity));
345
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
346
+ }
347
+
348
+ // src/themes/builtin.ts
349
+ var colorHexSchema = z.string().refine(
350
+ (v) => {
351
+ try {
352
+ normalizeColor(v);
353
+ return true;
354
+ } catch {
355
+ return false;
356
+ }
357
+ },
358
+ { message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
359
+ ).transform((v) => normalizeColor(v));
261
360
  var fontFamilySchema = z.string().min(1).max(120);
262
361
  var codeThemeSchema = z.object({
263
362
  background: colorHexSchema,
@@ -488,7 +587,17 @@ function resolveTheme(theme) {
488
587
  }
489
588
 
490
589
  // src/spec.schema.ts
491
- var colorHexSchema2 = z2.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
590
+ var colorHexSchema2 = z2.string().refine(
591
+ (v) => {
592
+ try {
593
+ normalizeColor(v);
594
+ return true;
595
+ } catch {
596
+ return false;
597
+ }
598
+ },
599
+ { message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
600
+ ).transform((v) => normalizeColor(v));
492
601
  var gradientStopSchema = z2.object({
493
602
  offset: z2.number().min(0).max(1),
494
603
  color: colorHexSchema2
@@ -680,6 +789,9 @@ var flowNodeElementSchema = z2.object({
680
789
  label: z2.string().min(1).max(200),
681
790
  sublabel: z2.string().min(1).max(300).optional(),
682
791
  sublabelColor: colorHexSchema2.optional(),
792
+ sublabel2: z2.string().min(1).max(300).optional(),
793
+ sublabel2Color: colorHexSchema2.optional(),
794
+ sublabel2FontSize: z2.number().min(8).max(32).optional(),
683
795
  labelColor: colorHexSchema2.optional(),
684
796
  labelFontSize: z2.number().min(10).max(48).optional(),
685
797
  color: colorHexSchema2.optional(),
@@ -688,7 +800,12 @@ var flowNodeElementSchema = z2.object({
688
800
  cornerRadius: z2.number().min(0).max(64).optional(),
689
801
  width: z2.number().int().min(40).max(800).optional(),
690
802
  height: z2.number().int().min(30).max(600).optional(),
691
- opacity: z2.number().min(0).max(1).default(1)
803
+ fillOpacity: z2.number().min(0).max(1).default(1),
804
+ opacity: z2.number().min(0).max(1).default(1),
805
+ badgeText: z2.string().min(1).max(32).optional(),
806
+ badgeColor: colorHexSchema2.optional(),
807
+ badgeBackground: colorHexSchema2.optional(),
808
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
692
809
  }).strict();
693
810
  var connectionElementSchema = z2.object({
694
811
  type: z2.literal("connection"),
@@ -777,7 +894,15 @@ var autoLayoutConfigSchema = z2.object({
777
894
  nodeSpacing: z2.number().int().min(0).max(512).default(80),
778
895
  rankSpacing: z2.number().int().min(0).max(512).default(120),
779
896
  edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
780
- aspectRatio: z2.number().min(0.5).max(3).optional()
897
+ aspectRatio: z2.number().min(0.5).max(3).optional(),
898
+ /** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
899
+ radialRoot: z2.string().min(1).max(120).optional(),
900
+ /** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
901
+ radialRadius: z2.number().positive().optional(),
902
+ /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
903
+ radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
904
+ /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
905
+ radialSortBy: z2.enum(["id", "connections"]).optional()
781
906
  }).strict();
782
907
  var gridLayoutConfigSchema = z2.object({
783
908
  mode: z2.literal("grid"),
@@ -881,43 +1006,6 @@ function parseDesignSpec(input) {
881
1006
  return designSpecSchema.parse(input);
882
1007
  }
883
1008
 
884
- // src/utils/color.ts
885
- function parseChannel(hex, offset) {
886
- return Number.parseInt(hex.slice(offset, offset + 2), 16);
887
- }
888
- function parseHexColor(hexColor) {
889
- const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
890
- if (normalized.length !== 6 && normalized.length !== 8) {
891
- throw new Error(`Unsupported color format: ${hexColor}`);
892
- }
893
- return {
894
- r: parseChannel(normalized, 0),
895
- g: parseChannel(normalized, 2),
896
- b: parseChannel(normalized, 4)
897
- };
898
- }
899
- function srgbToLinear(channel) {
900
- const normalized = channel / 255;
901
- if (normalized <= 0.03928) {
902
- return normalized / 12.92;
903
- }
904
- return ((normalized + 0.055) / 1.055) ** 2.4;
905
- }
906
- function relativeLuminance(hexColor) {
907
- const rgb = parseHexColor(hexColor);
908
- const r = srgbToLinear(rgb.r);
909
- const g = srgbToLinear(rgb.g);
910
- const b = srgbToLinear(rgb.b);
911
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
912
- }
913
- function contrastRatio(foreground, background) {
914
- const fg = relativeLuminance(foreground);
915
- const bg = relativeLuminance(background);
916
- const lighter = Math.max(fg, bg);
917
- const darker = Math.min(fg, bg);
918
- return (lighter + 0.05) / (darker + 0.05);
919
- }
920
-
921
1009
  // src/qa.ts
922
1010
  function rectWithin(outer, inner) {
923
1011
  return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
@@ -1184,6 +1272,382 @@ function loadFonts() {
1184
1272
  // src/layout/elk.ts
1185
1273
  import ELK from "elkjs";
1186
1274
 
1275
+ // src/primitives/shapes.ts
1276
+ function roundRectPath(ctx, rect, radius) {
1277
+ const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
1278
+ const right = rect.x + rect.width;
1279
+ const bottom = rect.y + rect.height;
1280
+ ctx.beginPath();
1281
+ ctx.moveTo(rect.x + r, rect.y);
1282
+ ctx.lineTo(right - r, rect.y);
1283
+ ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
1284
+ ctx.lineTo(right, bottom - r);
1285
+ ctx.quadraticCurveTo(right, bottom, right - r, bottom);
1286
+ ctx.lineTo(rect.x + r, bottom);
1287
+ ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
1288
+ ctx.lineTo(rect.x, rect.y + r);
1289
+ ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
1290
+ ctx.closePath();
1291
+ }
1292
+ function fillAndStroke(ctx, fill, stroke) {
1293
+ ctx.fillStyle = fill;
1294
+ ctx.fill();
1295
+ if (stroke) {
1296
+ ctx.strokeStyle = stroke;
1297
+ ctx.stroke();
1298
+ }
1299
+ }
1300
+ function drawRoundedRect(ctx, rect, radius, fill, stroke) {
1301
+ roundRectPath(ctx, rect, radius);
1302
+ fillAndStroke(ctx, fill, stroke);
1303
+ }
1304
+ function drawCircle(ctx, center2, radius, fill, stroke) {
1305
+ ctx.beginPath();
1306
+ ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
1307
+ ctx.closePath();
1308
+ fillAndStroke(ctx, fill, stroke);
1309
+ }
1310
+ function drawDiamond(ctx, bounds, fill, stroke) {
1311
+ const cx = bounds.x + bounds.width / 2;
1312
+ const cy = bounds.y + bounds.height / 2;
1313
+ ctx.beginPath();
1314
+ ctx.moveTo(cx, bounds.y);
1315
+ ctx.lineTo(bounds.x + bounds.width, cy);
1316
+ ctx.lineTo(cx, bounds.y + bounds.height);
1317
+ ctx.lineTo(bounds.x, cy);
1318
+ ctx.closePath();
1319
+ fillAndStroke(ctx, fill, stroke);
1320
+ }
1321
+ function drawPill(ctx, bounds, fill, stroke) {
1322
+ drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
1323
+ }
1324
+ function drawEllipse(ctx, bounds, fill, stroke) {
1325
+ const cx = bounds.x + bounds.width / 2;
1326
+ const cy = bounds.y + bounds.height / 2;
1327
+ ctx.beginPath();
1328
+ ctx.ellipse(
1329
+ cx,
1330
+ cy,
1331
+ Math.max(0, bounds.width / 2),
1332
+ Math.max(0, bounds.height / 2),
1333
+ 0,
1334
+ 0,
1335
+ Math.PI * 2
1336
+ );
1337
+ ctx.closePath();
1338
+ fillAndStroke(ctx, fill, stroke);
1339
+ }
1340
+ function drawCylinder(ctx, bounds, fill, stroke) {
1341
+ const rx = Math.max(2, bounds.width / 2);
1342
+ const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
1343
+ const cx = bounds.x + bounds.width / 2;
1344
+ const topCy = bounds.y + ry;
1345
+ const bottomCy = bounds.y + bounds.height - ry;
1346
+ ctx.beginPath();
1347
+ ctx.moveTo(bounds.x, topCy);
1348
+ ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
1349
+ ctx.lineTo(bounds.x + bounds.width, bottomCy);
1350
+ ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
1351
+ ctx.closePath();
1352
+ fillAndStroke(ctx, fill, stroke);
1353
+ if (stroke) {
1354
+ ctx.beginPath();
1355
+ ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
1356
+ ctx.closePath();
1357
+ ctx.strokeStyle = stroke;
1358
+ ctx.stroke();
1359
+ }
1360
+ }
1361
+ function drawParallelogram(ctx, bounds, fill, stroke, skew) {
1362
+ const maxSkew = bounds.width * 0.45;
1363
+ const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
1364
+ ctx.beginPath();
1365
+ ctx.moveTo(bounds.x + skewX, bounds.y);
1366
+ ctx.lineTo(bounds.x + bounds.width, bounds.y);
1367
+ ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
1368
+ ctx.lineTo(bounds.x, bounds.y + bounds.height);
1369
+ ctx.closePath();
1370
+ fillAndStroke(ctx, fill, stroke);
1371
+ }
1372
+
1373
+ // src/primitives/text.ts
1374
+ var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
1375
+ function resolveFont(requested, role) {
1376
+ if (SUPPORTED_FONT_FAMILIES.has(requested)) {
1377
+ return requested;
1378
+ }
1379
+ if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
1380
+ return "JetBrains Mono";
1381
+ }
1382
+ if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
1383
+ return "Space Grotesk";
1384
+ }
1385
+ return "Inter";
1386
+ }
1387
+ function applyFont(ctx, options) {
1388
+ ctx.font = `${options.weight} ${options.size}px ${options.family}`;
1389
+ }
1390
+ function wrapText(ctx, text, maxWidth, maxLines) {
1391
+ const trimmed = text.trim();
1392
+ if (!trimmed) {
1393
+ return { lines: [], truncated: false };
1394
+ }
1395
+ const words = trimmed.split(/\s+/u);
1396
+ const lines = [];
1397
+ let current = "";
1398
+ for (const word of words) {
1399
+ const trial = current.length > 0 ? `${current} ${word}` : word;
1400
+ if (ctx.measureText(trial).width <= maxWidth) {
1401
+ current = trial;
1402
+ continue;
1403
+ }
1404
+ if (current.length > 0) {
1405
+ lines.push(current);
1406
+ current = word;
1407
+ } else {
1408
+ lines.push(word);
1409
+ current = "";
1410
+ }
1411
+ if (lines.length >= maxLines) {
1412
+ break;
1413
+ }
1414
+ }
1415
+ if (lines.length < maxLines && current.length > 0) {
1416
+ lines.push(current);
1417
+ }
1418
+ const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
1419
+ if (!wasTruncated) {
1420
+ return { lines, truncated: false };
1421
+ }
1422
+ const lastIndex = lines.length - 1;
1423
+ let truncatedLine = `${lines[lastIndex]}\u2026`;
1424
+ while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
1425
+ truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
1426
+ }
1427
+ lines[lastIndex] = truncatedLine;
1428
+ return { lines, truncated: true };
1429
+ }
1430
+ function drawTextBlock(ctx, options) {
1431
+ applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
1432
+ const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
1433
+ ctx.fillStyle = options.color;
1434
+ for (const [index, line] of wrapped.lines.entries()) {
1435
+ ctx.fillText(line, options.x, options.y + index * options.lineHeight);
1436
+ }
1437
+ return {
1438
+ height: wrapped.lines.length * options.lineHeight,
1439
+ truncated: wrapped.truncated
1440
+ };
1441
+ }
1442
+ function drawTextLabel(ctx, text, position, options) {
1443
+ applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
1444
+ const textWidth = Math.ceil(ctx.measureText(text).width);
1445
+ const rect = {
1446
+ x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
1447
+ y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
1448
+ width: textWidth + options.padding * 2,
1449
+ height: options.fontSize + options.padding * 2
1450
+ };
1451
+ drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
1452
+ ctx.fillStyle = options.color;
1453
+ ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
1454
+ return rect;
1455
+ }
1456
+
1457
+ // src/renderers/flow-node.ts
1458
+ var BADGE_FONT_SIZE = 10;
1459
+ var BADGE_FONT_WEIGHT = 600;
1460
+ var BADGE_LETTER_SPACING = 1;
1461
+ var BADGE_PADDING_X = 8;
1462
+ var BADGE_PADDING_Y = 3;
1463
+ var BADGE_BORDER_RADIUS = 12;
1464
+ var BADGE_DEFAULT_COLOR = "#FFFFFF";
1465
+ var BADGE_PILL_HEIGHT = BADGE_FONT_SIZE + BADGE_PADDING_Y * 2;
1466
+ var BADGE_INSIDE_TOP_EXTRA = BADGE_PILL_HEIGHT + 6;
1467
+ function drawNodeShape(ctx, shape, bounds, fill, stroke, cornerRadius) {
1468
+ switch (shape) {
1469
+ case "box":
1470
+ drawRoundedRect(ctx, bounds, 0, fill, stroke);
1471
+ break;
1472
+ case "rounded-box":
1473
+ drawRoundedRect(ctx, bounds, cornerRadius, fill, stroke);
1474
+ break;
1475
+ case "diamond":
1476
+ drawDiamond(ctx, bounds, fill, stroke);
1477
+ break;
1478
+ case "circle": {
1479
+ const radius = Math.min(bounds.width, bounds.height) / 2;
1480
+ drawCircle(
1481
+ ctx,
1482
+ { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
1483
+ radius,
1484
+ fill,
1485
+ stroke
1486
+ );
1487
+ break;
1488
+ }
1489
+ case "pill":
1490
+ drawPill(ctx, bounds, fill, stroke);
1491
+ break;
1492
+ case "cylinder":
1493
+ drawCylinder(ctx, bounds, fill, stroke);
1494
+ break;
1495
+ case "parallelogram":
1496
+ drawParallelogram(ctx, bounds, fill, stroke);
1497
+ break;
1498
+ }
1499
+ }
1500
+ function measureSpacedText(ctx, text, letterSpacing) {
1501
+ const base = ctx.measureText(text).width;
1502
+ const extraChars = [...text].length - 1;
1503
+ return extraChars > 0 ? base + extraChars * letterSpacing : base;
1504
+ }
1505
+ function drawSpacedText(ctx, text, centerX, centerY, letterSpacing) {
1506
+ const chars = [...text];
1507
+ if (chars.length === 0) return;
1508
+ const totalWidth = measureSpacedText(ctx, text, letterSpacing);
1509
+ let cursorX = centerX - totalWidth / 2;
1510
+ ctx.textAlign = "left";
1511
+ for (let i = 0; i < chars.length; i++) {
1512
+ ctx.fillText(chars[i], cursorX, centerY);
1513
+ cursorX += ctx.measureText(chars[i]).width + (i < chars.length - 1 ? letterSpacing : 0);
1514
+ }
1515
+ }
1516
+ function renderBadgePill(ctx, centerX, centerY, text, textColor, background, monoFont) {
1517
+ ctx.save();
1518
+ applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
1519
+ const textWidth = measureSpacedText(ctx, text, BADGE_LETTER_SPACING);
1520
+ const pillWidth = textWidth + BADGE_PADDING_X * 2;
1521
+ const pillHeight = BADGE_PILL_HEIGHT;
1522
+ const pillX = centerX - pillWidth / 2;
1523
+ const pillY = centerY - pillHeight / 2;
1524
+ ctx.fillStyle = background;
1525
+ ctx.beginPath();
1526
+ ctx.roundRect(pillX, pillY, pillWidth, pillHeight, BADGE_BORDER_RADIUS);
1527
+ ctx.fill();
1528
+ ctx.fillStyle = textColor;
1529
+ ctx.textBaseline = "middle";
1530
+ applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
1531
+ drawSpacedText(ctx, text, centerX, centerY, BADGE_LETTER_SPACING);
1532
+ ctx.restore();
1533
+ return pillWidth;
1534
+ }
1535
+ function renderFlowNode(ctx, node, bounds, theme) {
1536
+ const fillColor = node.color ?? theme.surfaceElevated;
1537
+ const borderColor = node.borderColor ?? theme.border;
1538
+ const borderWidth = node.borderWidth ?? 2;
1539
+ const cornerRadius = node.cornerRadius ?? 16;
1540
+ const labelColor = node.labelColor ?? theme.text;
1541
+ const sublabelColor = node.sublabelColor ?? theme.textMuted;
1542
+ const labelFontSize = node.labelFontSize ?? 20;
1543
+ const fillOpacity = node.fillOpacity ?? 1;
1544
+ const hasBadge = !!node.badgeText;
1545
+ const badgePosition = node.badgePosition ?? "inside-top";
1546
+ const badgeColor = node.badgeColor ?? BADGE_DEFAULT_COLOR;
1547
+ const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
1548
+ ctx.save();
1549
+ ctx.lineWidth = borderWidth;
1550
+ if (fillOpacity < 1) {
1551
+ ctx.globalAlpha = node.opacity * fillOpacity;
1552
+ drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
1553
+ ctx.globalAlpha = node.opacity;
1554
+ drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
1555
+ } else {
1556
+ ctx.globalAlpha = node.opacity;
1557
+ drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
1558
+ }
1559
+ const headingFont = resolveFont(theme.fonts.heading, "heading");
1560
+ const bodyFont = resolveFont(theme.fonts.body, "body");
1561
+ const monoFont = resolveFont(theme.fonts.mono, "mono");
1562
+ const centerX = bounds.x + bounds.width / 2;
1563
+ const centerY = bounds.y + bounds.height / 2;
1564
+ const insideTopShift = hasBadge && badgePosition === "inside-top" ? BADGE_INSIDE_TOP_EXTRA / 2 : 0;
1565
+ const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
1566
+ const sublabel2FontSize = node.sublabel2FontSize ?? 11;
1567
+ const sublabel2Color = node.sublabel2Color ?? sublabelColor;
1568
+ const lineCount = node.sublabel2 ? 3 : node.sublabel ? 2 : 1;
1569
+ const labelToSublabelGap = Math.max(20, sublabelFontSize + 6);
1570
+ const sublabelToSublabel2Gap = sublabel2FontSize + 4;
1571
+ let textBlockHeight;
1572
+ if (lineCount === 1) {
1573
+ textBlockHeight = labelFontSize;
1574
+ } else if (lineCount === 2) {
1575
+ textBlockHeight = labelFontSize + labelToSublabelGap;
1576
+ } else {
1577
+ textBlockHeight = labelFontSize + labelToSublabelGap + sublabelToSublabel2Gap;
1578
+ }
1579
+ const labelY = lineCount === 1 ? centerY + labelFontSize * 0.3 + insideTopShift : centerY - textBlockHeight / 2 + labelFontSize * 0.8 + insideTopShift;
1580
+ ctx.textAlign = "center";
1581
+ applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
1582
+ ctx.fillStyle = labelColor;
1583
+ ctx.fillText(node.label, centerX, labelY);
1584
+ let textBoundsY = bounds.y + bounds.height / 2 - 18;
1585
+ let textBoundsHeight = 36;
1586
+ if (node.sublabel) {
1587
+ applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
1588
+ ctx.fillStyle = sublabelColor;
1589
+ ctx.fillText(node.sublabel, centerX, labelY + labelToSublabelGap);
1590
+ textBoundsY = bounds.y + bounds.height / 2 - 24;
1591
+ textBoundsHeight = 56;
1592
+ }
1593
+ if (node.sublabel2) {
1594
+ applyFont(ctx, { size: sublabel2FontSize, weight: 500, family: bodyFont });
1595
+ ctx.fillStyle = sublabel2Color;
1596
+ const sublabel2Y = node.sublabel ? labelY + labelToSublabelGap + sublabelToSublabel2Gap : labelY + labelToSublabelGap;
1597
+ ctx.fillText(node.sublabel2, centerX, sublabel2Y);
1598
+ textBoundsY = bounds.y + bounds.height / 2 - 30;
1599
+ textBoundsHeight = 72;
1600
+ }
1601
+ if (hasBadge && node.badgeText) {
1602
+ if (badgePosition === "inside-top") {
1603
+ const badgeCenterY = bounds.y + BADGE_PILL_HEIGHT / 2 + 8;
1604
+ renderBadgePill(
1605
+ ctx,
1606
+ centerX,
1607
+ badgeCenterY,
1608
+ node.badgeText,
1609
+ badgeColor,
1610
+ badgeBackground,
1611
+ monoFont
1612
+ );
1613
+ } else {
1614
+ const badgeCenterY = bounds.y - BADGE_PILL_HEIGHT / 2 - 4;
1615
+ renderBadgePill(
1616
+ ctx,
1617
+ centerX,
1618
+ badgeCenterY,
1619
+ node.badgeText,
1620
+ badgeColor,
1621
+ badgeBackground,
1622
+ monoFont
1623
+ );
1624
+ }
1625
+ }
1626
+ ctx.restore();
1627
+ const effectiveBg = fillOpacity < 1 ? blendColorWithOpacity(fillColor, theme.background, fillOpacity) : fillColor;
1628
+ return [
1629
+ {
1630
+ id: `flow-node-${node.id}`,
1631
+ kind: "flow-node",
1632
+ bounds,
1633
+ foregroundColor: labelColor,
1634
+ backgroundColor: effectiveBg
1635
+ },
1636
+ {
1637
+ id: `flow-node-${node.id}-label`,
1638
+ kind: "text",
1639
+ bounds: {
1640
+ x: bounds.x + 8,
1641
+ y: textBoundsY,
1642
+ width: bounds.width - 16,
1643
+ height: textBoundsHeight
1644
+ },
1645
+ foregroundColor: labelColor,
1646
+ backgroundColor: effectiveBg
1647
+ }
1648
+ ];
1649
+ }
1650
+
1187
1651
  // src/layout/estimates.ts
1188
1652
  function estimateElementHeight(element) {
1189
1653
  switch (element.type) {
@@ -1282,33 +1746,37 @@ function computeStackLayout(elements, config, safeFrame) {
1282
1746
 
1283
1747
  // src/layout/elk.ts
1284
1748
  function estimateFlowNodeSize(node) {
1749
+ const badgeExtra = node.badgeText && (node.badgePosition ?? "inside-top") === "inside-top" ? BADGE_INSIDE_TOP_EXTRA : 0;
1750
+ const sublabel2Extra = node.sublabel2 ? (node.sublabel2FontSize ?? 11) + 4 : 0;
1751
+ const extra = badgeExtra + sublabel2Extra;
1285
1752
  if (node.width && node.height) {
1286
- return { width: node.width, height: node.height };
1753
+ return { width: node.width, height: node.height + extra };
1287
1754
  }
1288
1755
  if (node.width) {
1756
+ const baseHeight = node.shape === "diamond" || node.shape === "circle" ? node.width : 60;
1289
1757
  return {
1290
1758
  width: node.width,
1291
- height: node.shape === "diamond" || node.shape === "circle" ? node.width : 60
1759
+ height: baseHeight + extra
1292
1760
  };
1293
1761
  }
1294
1762
  if (node.height) {
1295
1763
  return {
1296
1764
  width: node.shape === "diamond" || node.shape === "circle" ? node.height : 160,
1297
- height: node.height
1765
+ height: node.height + extra
1298
1766
  };
1299
1767
  }
1300
1768
  switch (node.shape) {
1301
1769
  case "diamond":
1302
1770
  case "circle":
1303
- return { width: 100, height: 100 };
1771
+ return { width: 100 + extra, height: 100 + extra };
1304
1772
  case "pill":
1305
- return { width: 180, height: 56 };
1773
+ return { width: 180, height: 56 + extra };
1306
1774
  case "cylinder":
1307
- return { width: 140, height: 92 };
1775
+ return { width: 140, height: 92 + extra };
1308
1776
  case "parallelogram":
1309
- return { width: 180, height: 72 };
1777
+ return { width: 180, height: 72 + extra };
1310
1778
  default:
1311
- return { width: 170, height: 64 };
1779
+ return { width: 170, height: 64 + extra };
1312
1780
  }
1313
1781
  }
1314
1782
  function splitLayoutFrames(safeFrame, direction, hasAuxiliary) {
@@ -1426,6 +1894,40 @@ function directionToElk(direction) {
1426
1894
  return "DOWN";
1427
1895
  }
1428
1896
  }
1897
+ function radialCompactionToElk(compaction) {
1898
+ switch (compaction) {
1899
+ case "radial":
1900
+ return "RADIAL_COMPACTION";
1901
+ case "wedge":
1902
+ return "WEDGE_COMPACTION";
1903
+ default:
1904
+ return "NONE";
1905
+ }
1906
+ }
1907
+ function radialSortByToElk(sortBy) {
1908
+ switch (sortBy) {
1909
+ case "connections":
1910
+ return "POLAR_COORDINATE";
1911
+ default:
1912
+ return "ID";
1913
+ }
1914
+ }
1915
+ function buildRadialOptions(config) {
1916
+ const options = {};
1917
+ if (config.radialRoot) {
1918
+ options["elk.radial.centerOnRoot"] = "true";
1919
+ }
1920
+ if (config.radialRadius != null) {
1921
+ options["elk.radial.radius"] = String(config.radialRadius);
1922
+ }
1923
+ if (config.radialCompaction) {
1924
+ options["elk.radial.compaction.strategy"] = radialCompactionToElk(config.radialCompaction);
1925
+ }
1926
+ if (config.radialSortBy) {
1927
+ options["elk.radial.orderId"] = radialSortByToElk(config.radialSortBy);
1928
+ }
1929
+ return options;
1930
+ }
1429
1931
  function fallbackForNoFlowNodes(nonFlow, safeFrame) {
1430
1932
  const fallbackConfig = {
1431
1933
  mode: "stack",
@@ -1461,6 +1963,11 @@ async function computeElkLayout(elements, config, safeFrame) {
1461
1963
  elkNodeSizes.set(node.id, estimateFlowNodeSize(node));
1462
1964
  }
1463
1965
  const edgeIdToRouteKey = /* @__PURE__ */ new Map();
1966
+ const radialOptions = config.algorithm === "radial" ? buildRadialOptions(config) : {};
1967
+ const orderedFlowNodes = config.radialRoot && config.algorithm === "radial" ? [
1968
+ ...flowNodes.filter((node) => node.id === config.radialRoot),
1969
+ ...flowNodes.filter((node) => node.id !== config.radialRoot)
1970
+ ] : flowNodes;
1464
1971
  const elkGraph = {
1465
1972
  id: "root",
1466
1973
  layoutOptions: {
@@ -1470,9 +1977,10 @@ async function computeElkLayout(elements, config, safeFrame) {
1470
1977
  "elk.layered.spacing.nodeNodeBetweenLayers": String(config.rankSpacing),
1471
1978
  "elk.edgeRouting": edgeRoutingToElk(config.edgeRouting),
1472
1979
  ...config.aspectRatio ? { "elk.aspectRatio": String(config.aspectRatio) } : {},
1473
- ...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {}
1980
+ ...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {},
1981
+ ...radialOptions
1474
1982
  },
1475
- children: flowNodes.map((node) => {
1983
+ children: orderedFlowNodes.map((node) => {
1476
1984
  const size = elkNodeSizes.get(node.id) ?? { width: 160, height: 60 };
1477
1985
  return {
1478
1986
  id: node.id,
@@ -1785,188 +2293,6 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
1785
2293
  ctx.restore();
1786
2294
  }
1787
2295
 
1788
- // src/primitives/shapes.ts
1789
- function roundRectPath(ctx, rect, radius) {
1790
- const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
1791
- const right = rect.x + rect.width;
1792
- const bottom = rect.y + rect.height;
1793
- ctx.beginPath();
1794
- ctx.moveTo(rect.x + r, rect.y);
1795
- ctx.lineTo(right - r, rect.y);
1796
- ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
1797
- ctx.lineTo(right, bottom - r);
1798
- ctx.quadraticCurveTo(right, bottom, right - r, bottom);
1799
- ctx.lineTo(rect.x + r, bottom);
1800
- ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
1801
- ctx.lineTo(rect.x, rect.y + r);
1802
- ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
1803
- ctx.closePath();
1804
- }
1805
- function fillAndStroke(ctx, fill, stroke) {
1806
- ctx.fillStyle = fill;
1807
- ctx.fill();
1808
- if (stroke) {
1809
- ctx.strokeStyle = stroke;
1810
- ctx.stroke();
1811
- }
1812
- }
1813
- function drawRoundedRect(ctx, rect, radius, fill, stroke) {
1814
- roundRectPath(ctx, rect, radius);
1815
- fillAndStroke(ctx, fill, stroke);
1816
- }
1817
- function drawCircle(ctx, center2, radius, fill, stroke) {
1818
- ctx.beginPath();
1819
- ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
1820
- ctx.closePath();
1821
- fillAndStroke(ctx, fill, stroke);
1822
- }
1823
- function drawDiamond(ctx, bounds, fill, stroke) {
1824
- const cx = bounds.x + bounds.width / 2;
1825
- const cy = bounds.y + bounds.height / 2;
1826
- ctx.beginPath();
1827
- ctx.moveTo(cx, bounds.y);
1828
- ctx.lineTo(bounds.x + bounds.width, cy);
1829
- ctx.lineTo(cx, bounds.y + bounds.height);
1830
- ctx.lineTo(bounds.x, cy);
1831
- ctx.closePath();
1832
- fillAndStroke(ctx, fill, stroke);
1833
- }
1834
- function drawPill(ctx, bounds, fill, stroke) {
1835
- drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
1836
- }
1837
- function drawEllipse(ctx, bounds, fill, stroke) {
1838
- const cx = bounds.x + bounds.width / 2;
1839
- const cy = bounds.y + bounds.height / 2;
1840
- ctx.beginPath();
1841
- ctx.ellipse(
1842
- cx,
1843
- cy,
1844
- Math.max(0, bounds.width / 2),
1845
- Math.max(0, bounds.height / 2),
1846
- 0,
1847
- 0,
1848
- Math.PI * 2
1849
- );
1850
- ctx.closePath();
1851
- fillAndStroke(ctx, fill, stroke);
1852
- }
1853
- function drawCylinder(ctx, bounds, fill, stroke) {
1854
- const rx = Math.max(2, bounds.width / 2);
1855
- const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
1856
- const cx = bounds.x + bounds.width / 2;
1857
- const topCy = bounds.y + ry;
1858
- const bottomCy = bounds.y + bounds.height - ry;
1859
- ctx.beginPath();
1860
- ctx.moveTo(bounds.x, topCy);
1861
- ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
1862
- ctx.lineTo(bounds.x + bounds.width, bottomCy);
1863
- ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
1864
- ctx.closePath();
1865
- fillAndStroke(ctx, fill, stroke);
1866
- if (stroke) {
1867
- ctx.beginPath();
1868
- ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
1869
- ctx.closePath();
1870
- ctx.strokeStyle = stroke;
1871
- ctx.stroke();
1872
- }
1873
- }
1874
- function drawParallelogram(ctx, bounds, fill, stroke, skew) {
1875
- const maxSkew = bounds.width * 0.45;
1876
- const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
1877
- ctx.beginPath();
1878
- ctx.moveTo(bounds.x + skewX, bounds.y);
1879
- ctx.lineTo(bounds.x + bounds.width, bounds.y);
1880
- ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
1881
- ctx.lineTo(bounds.x, bounds.y + bounds.height);
1882
- ctx.closePath();
1883
- fillAndStroke(ctx, fill, stroke);
1884
- }
1885
-
1886
- // src/primitives/text.ts
1887
- var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
1888
- function resolveFont(requested, role) {
1889
- if (SUPPORTED_FONT_FAMILIES.has(requested)) {
1890
- return requested;
1891
- }
1892
- if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
1893
- return "JetBrains Mono";
1894
- }
1895
- if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
1896
- return "Space Grotesk";
1897
- }
1898
- return "Inter";
1899
- }
1900
- function applyFont(ctx, options) {
1901
- ctx.font = `${options.weight} ${options.size}px ${options.family}`;
1902
- }
1903
- function wrapText(ctx, text, maxWidth, maxLines) {
1904
- const trimmed = text.trim();
1905
- if (!trimmed) {
1906
- return { lines: [], truncated: false };
1907
- }
1908
- const words = trimmed.split(/\s+/u);
1909
- const lines = [];
1910
- let current = "";
1911
- for (const word of words) {
1912
- const trial = current.length > 0 ? `${current} ${word}` : word;
1913
- if (ctx.measureText(trial).width <= maxWidth) {
1914
- current = trial;
1915
- continue;
1916
- }
1917
- if (current.length > 0) {
1918
- lines.push(current);
1919
- current = word;
1920
- } else {
1921
- lines.push(word);
1922
- current = "";
1923
- }
1924
- if (lines.length >= maxLines) {
1925
- break;
1926
- }
1927
- }
1928
- if (lines.length < maxLines && current.length > 0) {
1929
- lines.push(current);
1930
- }
1931
- const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
1932
- if (!wasTruncated) {
1933
- return { lines, truncated: false };
1934
- }
1935
- const lastIndex = lines.length - 1;
1936
- let truncatedLine = `${lines[lastIndex]}\u2026`;
1937
- while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
1938
- truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
1939
- }
1940
- lines[lastIndex] = truncatedLine;
1941
- return { lines, truncated: true };
1942
- }
1943
- function drawTextBlock(ctx, options) {
1944
- applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
1945
- const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
1946
- ctx.fillStyle = options.color;
1947
- for (const [index, line] of wrapped.lines.entries()) {
1948
- ctx.fillText(line, options.x, options.y + index * options.lineHeight);
1949
- }
1950
- return {
1951
- height: wrapped.lines.length * options.lineHeight,
1952
- truncated: wrapped.truncated
1953
- };
1954
- }
1955
- function drawTextLabel(ctx, text, position, options) {
1956
- applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
1957
- const textWidth = Math.ceil(ctx.measureText(text).width);
1958
- const rect = {
1959
- x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
1960
- y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
1961
- width: textWidth + options.padding * 2,
1962
- height: options.fontSize + options.padding * 2
1963
- };
1964
- drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
1965
- ctx.fillStyle = options.color;
1966
- ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
1967
- return rect;
1968
- }
1969
-
1970
2296
  // src/renderers/card.ts
1971
2297
  var TONE_BADGE_COLORS = {
1972
2298
  neutral: "#334B83",
@@ -3294,92 +3620,6 @@ function renderDrawCommands(ctx, commands, theme) {
3294
3620
  return rendered;
3295
3621
  }
3296
3622
 
3297
- // src/renderers/flow-node.ts
3298
- function renderFlowNode(ctx, node, bounds, theme) {
3299
- const fillColor = node.color ?? theme.surfaceElevated;
3300
- const borderColor = node.borderColor ?? theme.border;
3301
- const borderWidth = node.borderWidth ?? 2;
3302
- const cornerRadius = node.cornerRadius ?? 16;
3303
- const labelColor = node.labelColor ?? theme.text;
3304
- const sublabelColor = node.sublabelColor ?? theme.textMuted;
3305
- const labelFontSize = node.labelFontSize ?? 20;
3306
- ctx.save();
3307
- ctx.globalAlpha = node.opacity;
3308
- ctx.lineWidth = borderWidth;
3309
- switch (node.shape) {
3310
- case "box":
3311
- drawRoundedRect(ctx, bounds, 0, fillColor, borderColor);
3312
- break;
3313
- case "rounded-box":
3314
- drawRoundedRect(ctx, bounds, cornerRadius, fillColor, borderColor);
3315
- break;
3316
- case "diamond":
3317
- drawDiamond(ctx, bounds, fillColor, borderColor);
3318
- break;
3319
- case "circle": {
3320
- const radius = Math.min(bounds.width, bounds.height) / 2;
3321
- drawCircle(
3322
- ctx,
3323
- { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
3324
- radius,
3325
- fillColor,
3326
- borderColor
3327
- );
3328
- break;
3329
- }
3330
- case "pill":
3331
- drawPill(ctx, bounds, fillColor, borderColor);
3332
- break;
3333
- case "cylinder":
3334
- drawCylinder(ctx, bounds, fillColor, borderColor);
3335
- break;
3336
- case "parallelogram":
3337
- drawParallelogram(ctx, bounds, fillColor, borderColor);
3338
- break;
3339
- }
3340
- const headingFont = resolveFont(theme.fonts.heading, "heading");
3341
- const bodyFont = resolveFont(theme.fonts.body, "body");
3342
- const centerX = bounds.x + bounds.width / 2;
3343
- const centerY = bounds.y + bounds.height / 2;
3344
- const labelY = node.sublabel ? centerY - Math.max(4, labelFontSize * 0.2) : centerY + labelFontSize * 0.3;
3345
- ctx.textAlign = "center";
3346
- applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
3347
- ctx.fillStyle = labelColor;
3348
- ctx.fillText(node.label, centerX, labelY);
3349
- let textBoundsY = bounds.y + bounds.height / 2 - 18;
3350
- let textBoundsHeight = 36;
3351
- if (node.sublabel) {
3352
- const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
3353
- applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
3354
- ctx.fillStyle = sublabelColor;
3355
- ctx.fillText(node.sublabel, centerX, labelY + Math.max(20, sublabelFontSize + 6));
3356
- textBoundsY = bounds.y + bounds.height / 2 - 24;
3357
- textBoundsHeight = 56;
3358
- }
3359
- ctx.restore();
3360
- return [
3361
- {
3362
- id: `flow-node-${node.id}`,
3363
- kind: "flow-node",
3364
- bounds,
3365
- foregroundColor: labelColor,
3366
- backgroundColor: fillColor
3367
- },
3368
- {
3369
- id: `flow-node-${node.id}-label`,
3370
- kind: "text",
3371
- bounds: {
3372
- x: bounds.x + 8,
3373
- y: textBoundsY,
3374
- width: bounds.width - 16,
3375
- height: textBoundsHeight
3376
- },
3377
- foregroundColor: labelColor,
3378
- backgroundColor: fillColor
3379
- }
3380
- ];
3381
- }
3382
-
3383
3623
  // src/renderers/image.ts
3384
3624
  import { loadImage } from "@napi-rs/canvas";
3385
3625
  function roundedRectPath2(ctx, bounds, radius) {