@spectratools/graphic-designer-cli 0.11.0 → 0.12.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/README.md CHANGED
@@ -125,6 +125,7 @@ Plus a **freestyle draw layer** with 8 draw command types: `rect`, `circle`, `te
125
125
  - **auto** — ELK.js layout engine (layered, stress, force, radial, box algorithms)
126
126
  - **grid** — column-based grid layout
127
127
  - **stack** — vertical or horizontal stack
128
+ - **ellipse** — evenly spaced nodes on a configurable ellipse
128
129
  - **manual** — explicit x/y coordinates
129
130
 
130
131
  ### Connection Routing Modes
@@ -138,6 +139,36 @@ Per-connection `routing` supports:
138
139
 
139
140
  `layout.diagramCenter` can optionally override the center point used by `curve` and `arc` routing. When omitted, center is derived from the laid-out element centroid (fallback: canvas center).
140
141
 
142
+ ### Hexagonal Preset via Ellipse Layout
143
+
144
+ A six-node hexagonal arrangement is just an ellipse layout with equal angular spacing and matching routing hints:
145
+
146
+ ```ts
147
+ const spec = parseDesignSpec({
148
+ version: 2,
149
+ theme: 'dark',
150
+ elements: [
151
+ { type: 'flow-node', id: 'triage', label: 'Triage', shape: 'rounded-box' },
152
+ { type: 'flow-node', id: 'design', label: 'Design', shape: 'rounded-box' },
153
+ { type: 'flow-node', id: 'build', label: 'Build', shape: 'rounded-box' },
154
+ { type: 'flow-node', id: 'test', label: 'Test', shape: 'rounded-box' },
155
+ { type: 'flow-node', id: 'deploy', label: 'Deploy', shape: 'rounded-box' },
156
+ { type: 'flow-node', id: 'observe', label: 'Observe', shape: 'rounded-box' },
157
+ ],
158
+ layout: {
159
+ mode: 'ellipse',
160
+ cx: 600,
161
+ cy: 340,
162
+ rx: 280,
163
+ ry: 180,
164
+ startAngle: -90,
165
+ diagramCenter: { x: 600, y: 340 },
166
+ ellipseRx: 280,
167
+ ellipseRy: 180,
168
+ },
169
+ });
170
+ ```
171
+
141
172
  ## Programmatic Usage
142
173
 
143
174
  ```ts
@@ -157,7 +188,17 @@ const spec = parseDesignSpec({
157
188
  { type: 'flow-node', id: 'b', label: 'End', shape: 'rounded-box', color: '#059669' },
158
189
  { type: 'connection', from: 'a', to: 'b', label: 'next', routing: 'arc' },
159
190
  ],
160
- layout: { mode: 'auto', algorithm: 'layered', diagramCenter: { x: 600, y: 340 } },
191
+ layout: {
192
+ mode: 'ellipse',
193
+ cx: 600,
194
+ cy: 340,
195
+ rx: 280,
196
+ ry: 180,
197
+ startAngle: -90,
198
+ diagramCenter: { x: 600, y: 340 },
199
+ ellipseRx: 280,
200
+ ellipseRy: 180,
201
+ },
161
202
  });
162
203
 
163
204
  const render = await renderDesign(spec, { generatorVersion: '0.3.0' });
package/dist/cli.js CHANGED
@@ -766,6 +766,10 @@ var drawShadowSchema = z2.object({
766
766
  offsetY: z2.number().default(4)
767
767
  }).strict();
768
768
  var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
769
+ var strokeGradientSchema = z2.object({
770
+ from: colorHexSchema2,
771
+ to: colorHexSchema2
772
+ }).strict();
769
773
  var drawRectSchema = z2.object({
770
774
  type: z2.literal("rect"),
771
775
  x: z2.number(),
@@ -813,6 +817,7 @@ var drawLineSchema = z2.object({
813
817
  x2: z2.number(),
814
818
  y2: z2.number(),
815
819
  color: colorHexSchema2.default("#FFFFFF"),
820
+ strokeGradient: strokeGradientSchema.optional(),
816
821
  width: z2.number().min(0.5).max(32).default(2),
817
822
  dash: z2.array(z2.number()).max(6).optional(),
818
823
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -843,6 +848,7 @@ var drawBezierSchema = z2.object({
843
848
  type: z2.literal("bezier"),
844
849
  points: z2.array(drawPointSchema).min(2).max(20),
845
850
  color: colorHexSchema2.default("#FFFFFF"),
851
+ strokeGradient: strokeGradientSchema.optional(),
846
852
  width: z2.number().min(0.5).max(32).default(2),
847
853
  dash: z2.array(z2.number()).max(6).optional(),
848
854
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -1189,6 +1195,20 @@ var stackLayoutConfigSchema = z2.object({
1189
1195
  /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1190
1196
  ellipseRy: z2.number().positive().optional()
1191
1197
  }).strict();
1198
+ var ellipseLayoutConfigSchema = z2.object({
1199
+ mode: z2.literal("ellipse"),
1200
+ cx: z2.number().optional(),
1201
+ cy: z2.number().optional(),
1202
+ rx: z2.number().positive(),
1203
+ ry: z2.number().positive(),
1204
+ startAngle: z2.number().default(-90),
1205
+ /** Explicit center used by curve/arc connection routing. */
1206
+ diagramCenter: diagramCenterSchema.optional(),
1207
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1208
+ ellipseRx: z2.number().positive().optional(),
1209
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1210
+ ellipseRy: z2.number().positive().optional()
1211
+ }).strict();
1192
1212
  var manualPositionSchema = z2.object({
1193
1213
  x: z2.number().int(),
1194
1214
  y: z2.number().int(),
@@ -1209,6 +1229,7 @@ var layoutConfigSchema = z2.discriminatedUnion("mode", [
1209
1229
  autoLayoutConfigSchema,
1210
1230
  gridLayoutConfigSchema,
1211
1231
  stackLayoutConfigSchema,
1232
+ ellipseLayoutConfigSchema,
1212
1233
  manualLayoutConfigSchema
1213
1234
  ]);
1214
1235
  var constraintsSchema = z2.object({
@@ -2439,6 +2460,35 @@ async function computeElkLayout(elements, config, safeFrame) {
2439
2460
  };
2440
2461
  }
2441
2462
 
2463
+ // src/layout/ellipse.ts
2464
+ function clampDimension(estimated, max) {
2465
+ return Math.max(1, Math.min(max, Math.floor(estimated)));
2466
+ }
2467
+ function computeEllipseLayout(elements, config, safeFrame) {
2468
+ const placeable = elements.filter((element) => element.type !== "connection");
2469
+ const positions = /* @__PURE__ */ new Map();
2470
+ if (placeable.length === 0) {
2471
+ return { positions };
2472
+ }
2473
+ const cx = config.cx ?? safeFrame.x + safeFrame.width / 2;
2474
+ const cy = config.cy ?? safeFrame.y + safeFrame.height / 2;
2475
+ const stepDegrees = 360 / placeable.length;
2476
+ for (const [index, element] of placeable.entries()) {
2477
+ const angleRadians = (config.startAngle + index * stepDegrees) * Math.PI / 180;
2478
+ const centerX = cx + config.rx * Math.cos(angleRadians);
2479
+ const centerY = cy + config.ry * Math.sin(angleRadians);
2480
+ const width = clampDimension(estimateElementWidth(element), safeFrame.width);
2481
+ const height = clampDimension(estimateElementHeight(element), safeFrame.height);
2482
+ positions.set(element.id, {
2483
+ x: Math.round(centerX - width / 2),
2484
+ y: Math.round(centerY - height / 2),
2485
+ width,
2486
+ height
2487
+ });
2488
+ }
2489
+ return { positions };
2490
+ }
2491
+
2442
2492
  // src/layout/grid.ts
2443
2493
  function computeGridLayout(elements, config, safeFrame) {
2444
2494
  const placeable = elements.filter((element) => element.type !== "connection");
@@ -2534,6 +2584,8 @@ async function computeLayout(elements, layout, safeFrame) {
2534
2584
  return computeGridLayout(elements, layout, safeFrame);
2535
2585
  case "stack":
2536
2586
  return computeStackLayout(elements, layout, safeFrame);
2587
+ case "ellipse":
2588
+ return computeEllipseLayout(elements, layout, safeFrame);
2537
2589
  case "manual":
2538
2590
  return computeManualLayout(elements, layout, safeFrame);
2539
2591
  default:
@@ -3894,6 +3946,24 @@ function fromPoints(points) {
3894
3946
  function resolveDrawFont(theme, family) {
3895
3947
  return resolveFont(theme.fonts[family], family);
3896
3948
  }
3949
+ function createDrawStrokeGradient(ctx, start, end, strokeGradient) {
3950
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
3951
+ gradient.addColorStop(0, strokeGradient.from);
3952
+ gradient.addColorStop(1, strokeGradient.to);
3953
+ return gradient;
3954
+ }
3955
+ function resolveDrawStroke(ctx, start, end, color, strokeGradient) {
3956
+ if (!strokeGradient) {
3957
+ return color;
3958
+ }
3959
+ return createDrawStrokeGradient(ctx, start, end, strokeGradient);
3960
+ }
3961
+ function resolveArrowFill(color, strokeGradient, position) {
3962
+ if (!strokeGradient) {
3963
+ return color;
3964
+ }
3965
+ return position === "start" ? strokeGradient.from : strokeGradient.to;
3966
+ }
3897
3967
  function measureSpacedTextWidth(ctx, text, letterSpacing) {
3898
3968
  const chars = [...text];
3899
3969
  if (chars.length === 0) {
@@ -4172,18 +4242,31 @@ function renderDrawCommands(ctx, commands, theme) {
4172
4242
  const from = { x: command.x1, y: command.y1 };
4173
4243
  const to = { x: command.x2, y: command.y2 };
4174
4244
  const lineAngle = angleBetween(from, to);
4245
+ const stroke = resolveDrawStroke(ctx, from, to, command.color, command.strokeGradient);
4175
4246
  withOpacity(ctx, command.opacity, () => {
4176
4247
  applyDrawShadow(ctx, command.shadow);
4177
4248
  drawLine(ctx, from, to, {
4178
- color: command.color,
4249
+ color: stroke,
4179
4250
  width: command.width,
4180
4251
  ...command.dash ? { dash: command.dash } : {}
4181
4252
  });
4182
4253
  if (command.arrow === "end" || command.arrow === "both") {
4183
- drawArrowhead(ctx, to, lineAngle, command.arrowSize, command.color);
4254
+ drawArrowhead(
4255
+ ctx,
4256
+ to,
4257
+ lineAngle,
4258
+ command.arrowSize,
4259
+ resolveArrowFill(command.color, command.strokeGradient, "end")
4260
+ );
4184
4261
  }
4185
4262
  if (command.arrow === "start" || command.arrow === "both") {
4186
- drawArrowhead(ctx, from, lineAngle + Math.PI, command.arrowSize, command.color);
4263
+ drawArrowhead(
4264
+ ctx,
4265
+ from,
4266
+ lineAngle + Math.PI,
4267
+ command.arrowSize,
4268
+ resolveArrowFill(command.color, command.strokeGradient, "start")
4269
+ );
4187
4270
  }
4188
4271
  });
4189
4272
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -4191,7 +4274,7 @@ function renderDrawCommands(ctx, commands, theme) {
4191
4274
  id,
4192
4275
  kind: "draw",
4193
4276
  bounds: expandRect(fromPoints([from, to]), Math.max(command.width / 2, arrowPadding)),
4194
- foregroundColor: command.color
4277
+ foregroundColor: command.strokeGradient?.from ?? command.color
4195
4278
  });
4196
4279
  break;
4197
4280
  }
@@ -4225,10 +4308,17 @@ function renderDrawCommands(ctx, commands, theme) {
4225
4308
  }
4226
4309
  case "bezier": {
4227
4310
  const points = command.points;
4311
+ const stroke = resolveDrawStroke(
4312
+ ctx,
4313
+ points[0],
4314
+ points[points.length - 1],
4315
+ command.color,
4316
+ command.strokeGradient
4317
+ );
4228
4318
  withOpacity(ctx, command.opacity, () => {
4229
4319
  applyDrawShadow(ctx, command.shadow);
4230
4320
  drawBezier(ctx, points, {
4231
- color: command.color,
4321
+ color: stroke,
4232
4322
  width: command.width,
4233
4323
  ...command.dash ? { dash: command.dash } : {}
4234
4324
  });
@@ -4240,11 +4330,17 @@ function renderDrawCommands(ctx, commands, theme) {
4240
4330
  points[points.length - 1],
4241
4331
  endAngle,
4242
4332
  command.arrowSize,
4243
- command.color
4333
+ resolveArrowFill(command.color, command.strokeGradient, "end")
4244
4334
  );
4245
4335
  }
4246
4336
  if (command.arrow === "start" || command.arrow === "both") {
4247
- drawArrowhead(ctx, points[0], startAngle + Math.PI, command.arrowSize, command.color);
4337
+ drawArrowhead(
4338
+ ctx,
4339
+ points[0],
4340
+ startAngle + Math.PI,
4341
+ command.arrowSize,
4342
+ resolveArrowFill(command.color, command.strokeGradient, "start")
4343
+ );
4248
4344
  }
4249
4345
  });
4250
4346
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -4252,7 +4348,7 @@ function renderDrawCommands(ctx, commands, theme) {
4252
4348
  id,
4253
4349
  kind: "draw",
4254
4350
  bounds: expandRect(fromPoints(points), Math.max(command.width / 2, arrowPadding)),
4255
- foregroundColor: command.color
4351
+ foregroundColor: command.strokeGradient?.from ?? command.color
4256
4352
  });
4257
4353
  break;
4258
4354
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Cli } from 'incur';
2
- import { T as ThemeInput, D as DesignSpec, a as Rect$1, b as DrawCommand, c as Theme, d as RenderedElement, A as AnchorHint, C as ConnectionElement } from './spec.schema-CYlOLxmK.js';
3
- export { e as AutoLayoutConfig, B as BuiltInTheme, f as CardElement, g as CodeBlockElement, h as ConstraintSpec, i as DEFAULT_GENERATOR_VERSION, j as DEFAULT_RAINBOW_COLORS, k as Decorator, l as DesignCardSpec, m as DesignSafeFrame, n as DesignTheme, o as DiagramElement, p as DiagramLayout, q as DiagramSpec, r as DrawArc, s as DrawBadge, t as DrawBezier, u as DrawCircle, v as DrawFontFamily, w as DrawGradientRect, x as DrawLine, y as DrawPath, z as DrawPoint, E as DrawRect, F as DrawShadow, G as DrawText, H as DrawTextRow, I as DrawTextRowSegment, J as Element, K as FlowNodeElement, L as Gradient, M as GradientOverlayDecorator, N as GradientSpec, O as GradientStop, P as GridLayoutConfig, Q as ImageElement, S as IterationMeta, U as LayoutConfig, V as LayoutSnapshot, W as ManualLayoutConfig, X as RainbowRuleDecorator, Y as RenderDesignOptions, R as RenderMetadata, Z as RenderResult, _ as ShapeElement, $ as StackLayoutConfig, a0 as TerminalElement, a1 as TextElement, a2 as ThemeInput, a3 as VignetteDecorator, a4 as WrittenArtifacts, a5 as builtInThemeBackgrounds, a6 as builtInThemes, a7 as computeSpecHash, a8 as connectionElementSchema, a9 as defaultAutoLayout, aa as defaultCanvas, ab as defaultConstraints, ac as defaultGridLayout, ad as defaultLayout, ae as defaultStackLayout, af as defaultTheme, ag as deriveSafeFrame, ah as designSpecSchema, ai as diagramElementSchema, aj as diagramLayoutSchema, ak as diagramSpecSchema, al as drawGradientRect, am as drawRainbowRule, an as drawVignette, ao as flowNodeElementSchema, ap as inferLayout, aq as inferSidecarPath, ar as parseDesignSpec, as as parseDiagramSpec, at as renderDesign, au as resolveTheme, av as writeRenderArtifacts } from './spec.schema-CYlOLxmK.js';
2
+ import { T as ThemeInput, D as DesignSpec, a as Rect$1, b as DrawCommand, c as Theme, d as RenderedElement, A as AnchorHint, C as ConnectionElement } from './spec.schema-BkbcnVcm.js';
3
+ export { e as AutoLayoutConfig, B as BuiltInTheme, f as CardElement, g as CodeBlockElement, h as ConstraintSpec, i as DEFAULT_GENERATOR_VERSION, j as DEFAULT_RAINBOW_COLORS, k as Decorator, l as DesignCardSpec, m as DesignSafeFrame, n as DesignTheme, o as DiagramElement, p as DiagramLayout, q as DiagramSpec, r as DrawArc, s as DrawBadge, t as DrawBezier, u as DrawCircle, v as DrawFontFamily, w as DrawGradientRect, x as DrawLine, y as DrawPath, z as DrawPoint, E as DrawRect, F as DrawShadow, G as DrawText, H as DrawTextRow, I as DrawTextRowSegment, J as Element, K as EllipseLayoutConfig, L as FlowNodeElement, M as Gradient, N as GradientOverlayDecorator, O as GradientSpec, P as GradientStop, Q as GridLayoutConfig, S as ImageElement, U as IterationMeta, V as LayoutConfig, W as LayoutSnapshot, X as ManualLayoutConfig, Y as RainbowRuleDecorator, Z as RenderDesignOptions, R as RenderMetadata, _ as RenderResult, $ as ShapeElement, a0 as StackLayoutConfig, a1 as TerminalElement, a2 as TextElement, a3 as ThemeInput, a4 as VignetteDecorator, a5 as WrittenArtifacts, a6 as builtInThemeBackgrounds, a7 as builtInThemes, a8 as computeSpecHash, a9 as connectionElementSchema, aa as defaultAutoLayout, ab as defaultCanvas, ac as defaultConstraints, ad as defaultGridLayout, ae as defaultLayout, af as defaultStackLayout, ag as defaultTheme, ah as deriveSafeFrame, ai as designSpecSchema, aj as diagramElementSchema, ak as diagramLayoutSchema, al as diagramSpecSchema, am as drawGradientRect, an as drawRainbowRule, ao as drawVignette, ap as flowNodeElementSchema, aq as inferLayout, ar as inferSidecarPath, as as parseDesignSpec, at as parseDiagramSpec, au as renderDesign, av as resolveTheme, aw as writeRenderArtifacts } from './spec.schema-BkbcnVcm.js';
4
4
  import { SKRSContext2D } from '@napi-rs/canvas';
5
5
  export { QaIssue, QaReferenceResult, QaReport, QaSeverity, readMetadata, runQa } from './qa.js';
6
6
  import { Highlighter } from 'shiki';
package/dist/index.js CHANGED
@@ -775,6 +775,10 @@ var drawShadowSchema = z2.object({
775
775
  offsetY: z2.number().default(4)
776
776
  }).strict();
777
777
  var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
778
+ var strokeGradientSchema = z2.object({
779
+ from: colorHexSchema2,
780
+ to: colorHexSchema2
781
+ }).strict();
778
782
  var drawRectSchema = z2.object({
779
783
  type: z2.literal("rect"),
780
784
  x: z2.number(),
@@ -822,6 +826,7 @@ var drawLineSchema = z2.object({
822
826
  x2: z2.number(),
823
827
  y2: z2.number(),
824
828
  color: colorHexSchema2.default("#FFFFFF"),
829
+ strokeGradient: strokeGradientSchema.optional(),
825
830
  width: z2.number().min(0.5).max(32).default(2),
826
831
  dash: z2.array(z2.number()).max(6).optional(),
827
832
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -852,6 +857,7 @@ var drawBezierSchema = z2.object({
852
857
  type: z2.literal("bezier"),
853
858
  points: z2.array(drawPointSchema).min(2).max(20),
854
859
  color: colorHexSchema2.default("#FFFFFF"),
860
+ strokeGradient: strokeGradientSchema.optional(),
855
861
  width: z2.number().min(0.5).max(32).default(2),
856
862
  dash: z2.array(z2.number()).max(6).optional(),
857
863
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -1199,6 +1205,20 @@ var stackLayoutConfigSchema = z2.object({
1199
1205
  /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1200
1206
  ellipseRy: z2.number().positive().optional()
1201
1207
  }).strict();
1208
+ var ellipseLayoutConfigSchema = z2.object({
1209
+ mode: z2.literal("ellipse"),
1210
+ cx: z2.number().optional(),
1211
+ cy: z2.number().optional(),
1212
+ rx: z2.number().positive(),
1213
+ ry: z2.number().positive(),
1214
+ startAngle: z2.number().default(-90),
1215
+ /** Explicit center used by curve/arc connection routing. */
1216
+ diagramCenter: diagramCenterSchema.optional(),
1217
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
1218
+ ellipseRx: z2.number().positive().optional(),
1219
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
1220
+ ellipseRy: z2.number().positive().optional()
1221
+ }).strict();
1202
1222
  var manualPositionSchema = z2.object({
1203
1223
  x: z2.number().int(),
1204
1224
  y: z2.number().int(),
@@ -1219,6 +1239,7 @@ var layoutConfigSchema = z2.discriminatedUnion("mode", [
1219
1239
  autoLayoutConfigSchema,
1220
1240
  gridLayoutConfigSchema,
1221
1241
  stackLayoutConfigSchema,
1242
+ ellipseLayoutConfigSchema,
1222
1243
  manualLayoutConfigSchema
1223
1244
  ]);
1224
1245
  var constraintsSchema = z2.object({
@@ -2452,6 +2473,35 @@ async function computeElkLayout(elements, config, safeFrame) {
2452
2473
  };
2453
2474
  }
2454
2475
 
2476
+ // src/layout/ellipse.ts
2477
+ function clampDimension(estimated, max) {
2478
+ return Math.max(1, Math.min(max, Math.floor(estimated)));
2479
+ }
2480
+ function computeEllipseLayout(elements, config, safeFrame) {
2481
+ const placeable = elements.filter((element) => element.type !== "connection");
2482
+ const positions = /* @__PURE__ */ new Map();
2483
+ if (placeable.length === 0) {
2484
+ return { positions };
2485
+ }
2486
+ const cx = config.cx ?? safeFrame.x + safeFrame.width / 2;
2487
+ const cy = config.cy ?? safeFrame.y + safeFrame.height / 2;
2488
+ const stepDegrees = 360 / placeable.length;
2489
+ for (const [index, element] of placeable.entries()) {
2490
+ const angleRadians = (config.startAngle + index * stepDegrees) * Math.PI / 180;
2491
+ const centerX = cx + config.rx * Math.cos(angleRadians);
2492
+ const centerY = cy + config.ry * Math.sin(angleRadians);
2493
+ const width = clampDimension(estimateElementWidth(element), safeFrame.width);
2494
+ const height = clampDimension(estimateElementHeight(element), safeFrame.height);
2495
+ positions.set(element.id, {
2496
+ x: Math.round(centerX - width / 2),
2497
+ y: Math.round(centerY - height / 2),
2498
+ width,
2499
+ height
2500
+ });
2501
+ }
2502
+ return { positions };
2503
+ }
2504
+
2455
2505
  // src/layout/grid.ts
2456
2506
  function computeGridLayout(elements, config, safeFrame) {
2457
2507
  const placeable = elements.filter((element) => element.type !== "connection");
@@ -2547,6 +2597,8 @@ async function computeLayout(elements, layout, safeFrame) {
2547
2597
  return computeGridLayout(elements, layout, safeFrame);
2548
2598
  case "stack":
2549
2599
  return computeStackLayout(elements, layout, safeFrame);
2600
+ case "ellipse":
2601
+ return computeEllipseLayout(elements, layout, safeFrame);
2550
2602
  case "manual":
2551
2603
  return computeManualLayout(elements, layout, safeFrame);
2552
2604
  default:
@@ -3962,6 +4014,24 @@ function fromPoints(points) {
3962
4014
  function resolveDrawFont(theme, family) {
3963
4015
  return resolveFont(theme.fonts[family], family);
3964
4016
  }
4017
+ function createDrawStrokeGradient(ctx, start, end, strokeGradient) {
4018
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
4019
+ gradient.addColorStop(0, strokeGradient.from);
4020
+ gradient.addColorStop(1, strokeGradient.to);
4021
+ return gradient;
4022
+ }
4023
+ function resolveDrawStroke(ctx, start, end, color, strokeGradient) {
4024
+ if (!strokeGradient) {
4025
+ return color;
4026
+ }
4027
+ return createDrawStrokeGradient(ctx, start, end, strokeGradient);
4028
+ }
4029
+ function resolveArrowFill(color, strokeGradient, position) {
4030
+ if (!strokeGradient) {
4031
+ return color;
4032
+ }
4033
+ return position === "start" ? strokeGradient.from : strokeGradient.to;
4034
+ }
3965
4035
  function measureSpacedTextWidth(ctx, text, letterSpacing) {
3966
4036
  const chars = [...text];
3967
4037
  if (chars.length === 0) {
@@ -4240,18 +4310,31 @@ function renderDrawCommands(ctx, commands, theme) {
4240
4310
  const from = { x: command.x1, y: command.y1 };
4241
4311
  const to = { x: command.x2, y: command.y2 };
4242
4312
  const lineAngle = angleBetween(from, to);
4313
+ const stroke = resolveDrawStroke(ctx, from, to, command.color, command.strokeGradient);
4243
4314
  withOpacity(ctx, command.opacity, () => {
4244
4315
  applyDrawShadow(ctx, command.shadow);
4245
4316
  drawLine(ctx, from, to, {
4246
- color: command.color,
4317
+ color: stroke,
4247
4318
  width: command.width,
4248
4319
  ...command.dash ? { dash: command.dash } : {}
4249
4320
  });
4250
4321
  if (command.arrow === "end" || command.arrow === "both") {
4251
- drawArrowhead(ctx, to, lineAngle, command.arrowSize, command.color);
4322
+ drawArrowhead(
4323
+ ctx,
4324
+ to,
4325
+ lineAngle,
4326
+ command.arrowSize,
4327
+ resolveArrowFill(command.color, command.strokeGradient, "end")
4328
+ );
4252
4329
  }
4253
4330
  if (command.arrow === "start" || command.arrow === "both") {
4254
- drawArrowhead(ctx, from, lineAngle + Math.PI, command.arrowSize, command.color);
4331
+ drawArrowhead(
4332
+ ctx,
4333
+ from,
4334
+ lineAngle + Math.PI,
4335
+ command.arrowSize,
4336
+ resolveArrowFill(command.color, command.strokeGradient, "start")
4337
+ );
4255
4338
  }
4256
4339
  });
4257
4340
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -4259,7 +4342,7 @@ function renderDrawCommands(ctx, commands, theme) {
4259
4342
  id,
4260
4343
  kind: "draw",
4261
4344
  bounds: expandRect(fromPoints([from, to]), Math.max(command.width / 2, arrowPadding)),
4262
- foregroundColor: command.color
4345
+ foregroundColor: command.strokeGradient?.from ?? command.color
4263
4346
  });
4264
4347
  break;
4265
4348
  }
@@ -4293,10 +4376,17 @@ function renderDrawCommands(ctx, commands, theme) {
4293
4376
  }
4294
4377
  case "bezier": {
4295
4378
  const points = command.points;
4379
+ const stroke = resolveDrawStroke(
4380
+ ctx,
4381
+ points[0],
4382
+ points[points.length - 1],
4383
+ command.color,
4384
+ command.strokeGradient
4385
+ );
4296
4386
  withOpacity(ctx, command.opacity, () => {
4297
4387
  applyDrawShadow(ctx, command.shadow);
4298
4388
  drawBezier(ctx, points, {
4299
- color: command.color,
4389
+ color: stroke,
4300
4390
  width: command.width,
4301
4391
  ...command.dash ? { dash: command.dash } : {}
4302
4392
  });
@@ -4308,11 +4398,17 @@ function renderDrawCommands(ctx, commands, theme) {
4308
4398
  points[points.length - 1],
4309
4399
  endAngle,
4310
4400
  command.arrowSize,
4311
- command.color
4401
+ resolveArrowFill(command.color, command.strokeGradient, "end")
4312
4402
  );
4313
4403
  }
4314
4404
  if (command.arrow === "start" || command.arrow === "both") {
4315
- drawArrowhead(ctx, points[0], startAngle + Math.PI, command.arrowSize, command.color);
4405
+ drawArrowhead(
4406
+ ctx,
4407
+ points[0],
4408
+ startAngle + Math.PI,
4409
+ command.arrowSize,
4410
+ resolveArrowFill(command.color, command.strokeGradient, "start")
4411
+ );
4316
4412
  }
4317
4413
  });
4318
4414
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -4320,7 +4416,7 @@ function renderDrawCommands(ctx, commands, theme) {
4320
4416
  id,
4321
4417
  kind: "draw",
4322
4418
  bounds: expandRect(fromPoints(points), Math.max(command.width / 2, arrowPadding)),
4323
- foregroundColor: command.color
4419
+ foregroundColor: command.strokeGradient?.from ?? command.color
4324
4420
  });
4325
4421
  break;
4326
4422
  }
package/dist/qa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as RenderMetadata, D as DesignSpec } from './spec.schema-CYlOLxmK.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BkbcnVcm.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';
4
4
 
package/dist/qa.js CHANGED
@@ -509,6 +509,10 @@ var drawShadowSchema = z2.object({
509
509
  offsetY: z2.number().default(4)
510
510
  }).strict();
511
511
  var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
512
+ var strokeGradientSchema = z2.object({
513
+ from: colorHexSchema2,
514
+ to: colorHexSchema2
515
+ }).strict();
512
516
  var drawRectSchema = z2.object({
513
517
  type: z2.literal("rect"),
514
518
  x: z2.number(),
@@ -556,6 +560,7 @@ var drawLineSchema = z2.object({
556
560
  x2: z2.number(),
557
561
  y2: z2.number(),
558
562
  color: colorHexSchema2.default("#FFFFFF"),
563
+ strokeGradient: strokeGradientSchema.optional(),
559
564
  width: z2.number().min(0.5).max(32).default(2),
560
565
  dash: z2.array(z2.number()).max(6).optional(),
561
566
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -586,6 +591,7 @@ var drawBezierSchema = z2.object({
586
591
  type: z2.literal("bezier"),
587
592
  points: z2.array(drawPointSchema).min(2).max(20),
588
593
  color: colorHexSchema2.default("#FFFFFF"),
594
+ strokeGradient: strokeGradientSchema.optional(),
589
595
  width: z2.number().min(0.5).max(32).default(2),
590
596
  dash: z2.array(z2.number()).max(6).optional(),
591
597
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -932,6 +938,20 @@ var stackLayoutConfigSchema = z2.object({
932
938
  /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
933
939
  ellipseRy: z2.number().positive().optional()
934
940
  }).strict();
941
+ var ellipseLayoutConfigSchema = z2.object({
942
+ mode: z2.literal("ellipse"),
943
+ cx: z2.number().optional(),
944
+ cy: z2.number().optional(),
945
+ rx: z2.number().positive(),
946
+ ry: z2.number().positive(),
947
+ startAngle: z2.number().default(-90),
948
+ /** Explicit center used by curve/arc connection routing. */
949
+ diagramCenter: diagramCenterSchema.optional(),
950
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
951
+ ellipseRx: z2.number().positive().optional(),
952
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
953
+ ellipseRy: z2.number().positive().optional()
954
+ }).strict();
935
955
  var manualPositionSchema = z2.object({
936
956
  x: z2.number().int(),
937
957
  y: z2.number().int(),
@@ -952,6 +972,7 @@ var layoutConfigSchema = z2.discriminatedUnion("mode", [
952
972
  autoLayoutConfigSchema,
953
973
  gridLayoutConfigSchema,
954
974
  stackLayoutConfigSchema,
975
+ ellipseLayoutConfigSchema,
955
976
  manualLayoutConfigSchema
956
977
  ]);
957
978
  var constraintsSchema = z2.object({
@@ -1,3 +1,3 @@
1
- export { i as DEFAULT_GENERATOR_VERSION, S as IterationMeta, V as LayoutSnapshot, a as Rect, Y as RenderDesignOptions, R as RenderMetadata, Z as RenderResult, d as RenderedElement, a4 as WrittenArtifacts, a7 as computeSpecHash, aq as inferSidecarPath, at as renderDesign, av as writeRenderArtifacts } from './spec.schema-CYlOLxmK.js';
1
+ export { i as DEFAULT_GENERATOR_VERSION, U as IterationMeta, W as LayoutSnapshot, a as Rect, Z as RenderDesignOptions, R as RenderMetadata, _ as RenderResult, d as RenderedElement, a5 as WrittenArtifacts, a8 as computeSpecHash, ar as inferSidecarPath, au as renderDesign, aw as writeRenderArtifacts } from './spec.schema-BkbcnVcm.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';