@spectratools/graphic-designer-cli 0.8.0 → 0.9.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
@@ -1867,6 +1867,38 @@ function renderFlowNode(ctx, node, bounds, theme) {
1867
1867
  ctx.shadowOffsetX = 0;
1868
1868
  ctx.shadowOffsetY = 0;
1869
1869
  }
1870
+ if (node.accentColor) {
1871
+ const barWidth = node.accentBarWidth ?? 3;
1872
+ const effectiveRadius = node.shape === "box" ? 0 : cornerRadius;
1873
+ ctx.save();
1874
+ ctx.beginPath();
1875
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, effectiveRadius);
1876
+ ctx.clip();
1877
+ ctx.fillStyle = node.accentColor;
1878
+ ctx.fillRect(bounds.x, bounds.y, barWidth, bounds.height);
1879
+ ctx.restore();
1880
+ }
1881
+ if (node.glowColor) {
1882
+ const glowW = node.glowWidth ?? 16;
1883
+ const glowOp = node.glowOpacity ?? 0.15;
1884
+ ctx.save();
1885
+ ctx.beginPath();
1886
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius);
1887
+ ctx.clip();
1888
+ const barOffset = node.accentColor ? node.accentBarWidth ?? 3 : 0;
1889
+ const gradient = ctx.createLinearGradient(
1890
+ bounds.x + barOffset,
1891
+ bounds.y,
1892
+ bounds.x + barOffset + glowW,
1893
+ bounds.y
1894
+ );
1895
+ gradient.addColorStop(0, node.glowColor);
1896
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
1897
+ ctx.globalAlpha = glowOp;
1898
+ ctx.fillStyle = gradient;
1899
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
1900
+ ctx.restore();
1901
+ }
1870
1902
  const headingFont = resolveFont(theme.fonts.heading, "heading");
1871
1903
  const bodyFont = resolveFont(theme.fonts.body, "body");
1872
1904
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -4248,6 +4280,84 @@ function renderDrawCommands(ctx, commands, theme) {
4248
4280
  });
4249
4281
  break;
4250
4282
  }
4283
+ case "text-row": {
4284
+ const segments = command.segments;
4285
+ if (segments.length === 0) break;
4286
+ const resolveSegment = (seg) => ({
4287
+ text: seg.text,
4288
+ fontSize: seg.fontSize ?? command.defaultFontSize,
4289
+ fontWeight: seg.fontWeight ?? command.defaultFontWeight,
4290
+ fontFamily: resolveDrawFont(theme, seg.fontFamily ?? command.defaultFontFamily),
4291
+ color: seg.color ?? command.defaultColor
4292
+ });
4293
+ const measured = [];
4294
+ let totalWidth = 0;
4295
+ let maxAscent = 0;
4296
+ let maxDescent = 0;
4297
+ for (const seg of segments) {
4298
+ const resolved = resolveSegment(seg);
4299
+ applyFont(ctx, {
4300
+ size: resolved.fontSize,
4301
+ weight: resolved.fontWeight,
4302
+ family: resolved.fontFamily
4303
+ });
4304
+ const metrics = ctx.measureText(resolved.text);
4305
+ const width = metrics.width;
4306
+ const ascent = metrics.actualBoundingBoxAscent || 0;
4307
+ const descent = metrics.actualBoundingBoxDescent || 0;
4308
+ totalWidth += width;
4309
+ maxAscent = Math.max(maxAscent, ascent);
4310
+ maxDescent = Math.max(maxDescent, descent);
4311
+ measured.push({ width, resolved });
4312
+ }
4313
+ let cursorX;
4314
+ if (command.align === "center") {
4315
+ cursorX = command.x - totalWidth / 2;
4316
+ } else if (command.align === "right") {
4317
+ cursorX = command.x - totalWidth;
4318
+ } else {
4319
+ cursorX = command.x;
4320
+ }
4321
+ const startX = cursorX;
4322
+ withOpacity(ctx, command.opacity, () => {
4323
+ ctx.textBaseline = command.baseline;
4324
+ for (const { width, resolved } of measured) {
4325
+ applyFont(ctx, {
4326
+ size: resolved.fontSize,
4327
+ weight: resolved.fontWeight,
4328
+ family: resolved.fontFamily
4329
+ });
4330
+ ctx.fillStyle = resolved.color;
4331
+ ctx.textAlign = "left";
4332
+ ctx.fillText(resolved.text, cursorX, command.y);
4333
+ cursorX += width;
4334
+ }
4335
+ });
4336
+ const height = Math.max(1, maxAscent + maxDescent);
4337
+ let topY;
4338
+ if (command.baseline === "top") {
4339
+ topY = command.y;
4340
+ } else if (command.baseline === "middle") {
4341
+ topY = command.y - height / 2;
4342
+ } else if (command.baseline === "bottom") {
4343
+ topY = command.y - height;
4344
+ } else {
4345
+ topY = command.y - maxAscent;
4346
+ }
4347
+ rendered.push({
4348
+ id,
4349
+ kind: "draw",
4350
+ bounds: {
4351
+ x: startX,
4352
+ y: topY,
4353
+ width: Math.max(1, totalWidth),
4354
+ height
4355
+ },
4356
+ foregroundColor: command.defaultColor,
4357
+ backgroundColor: theme.background
4358
+ });
4359
+ break;
4360
+ }
4251
4361
  }
4252
4362
  }
4253
4363
  return rendered;
package/dist/index.js CHANGED
@@ -1880,6 +1880,38 @@ function renderFlowNode(ctx, node, bounds, theme) {
1880
1880
  ctx.shadowOffsetX = 0;
1881
1881
  ctx.shadowOffsetY = 0;
1882
1882
  }
1883
+ if (node.accentColor) {
1884
+ const barWidth = node.accentBarWidth ?? 3;
1885
+ const effectiveRadius = node.shape === "box" ? 0 : cornerRadius;
1886
+ ctx.save();
1887
+ ctx.beginPath();
1888
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, effectiveRadius);
1889
+ ctx.clip();
1890
+ ctx.fillStyle = node.accentColor;
1891
+ ctx.fillRect(bounds.x, bounds.y, barWidth, bounds.height);
1892
+ ctx.restore();
1893
+ }
1894
+ if (node.glowColor) {
1895
+ const glowW = node.glowWidth ?? 16;
1896
+ const glowOp = node.glowOpacity ?? 0.15;
1897
+ ctx.save();
1898
+ ctx.beginPath();
1899
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius);
1900
+ ctx.clip();
1901
+ const barOffset = node.accentColor ? node.accentBarWidth ?? 3 : 0;
1902
+ const gradient = ctx.createLinearGradient(
1903
+ bounds.x + barOffset,
1904
+ bounds.y,
1905
+ bounds.x + barOffset + glowW,
1906
+ bounds.y
1907
+ );
1908
+ gradient.addColorStop(0, node.glowColor);
1909
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
1910
+ ctx.globalAlpha = glowOp;
1911
+ ctx.fillStyle = gradient;
1912
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
1913
+ ctx.restore();
1914
+ }
1883
1915
  const headingFont = resolveFont(theme.fonts.heading, "heading");
1884
1916
  const bodyFont = resolveFont(theme.fonts.body, "body");
1885
1917
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -4265,6 +4297,84 @@ function renderDrawCommands(ctx, commands, theme) {
4265
4297
  });
4266
4298
  break;
4267
4299
  }
4300
+ case "text-row": {
4301
+ const segments = command.segments;
4302
+ if (segments.length === 0) break;
4303
+ const resolveSegment = (seg) => ({
4304
+ text: seg.text,
4305
+ fontSize: seg.fontSize ?? command.defaultFontSize,
4306
+ fontWeight: seg.fontWeight ?? command.defaultFontWeight,
4307
+ fontFamily: resolveDrawFont(theme, seg.fontFamily ?? command.defaultFontFamily),
4308
+ color: seg.color ?? command.defaultColor
4309
+ });
4310
+ const measured = [];
4311
+ let totalWidth = 0;
4312
+ let maxAscent = 0;
4313
+ let maxDescent = 0;
4314
+ for (const seg of segments) {
4315
+ const resolved = resolveSegment(seg);
4316
+ applyFont(ctx, {
4317
+ size: resolved.fontSize,
4318
+ weight: resolved.fontWeight,
4319
+ family: resolved.fontFamily
4320
+ });
4321
+ const metrics = ctx.measureText(resolved.text);
4322
+ const width = metrics.width;
4323
+ const ascent = metrics.actualBoundingBoxAscent || 0;
4324
+ const descent = metrics.actualBoundingBoxDescent || 0;
4325
+ totalWidth += width;
4326
+ maxAscent = Math.max(maxAscent, ascent);
4327
+ maxDescent = Math.max(maxDescent, descent);
4328
+ measured.push({ width, resolved });
4329
+ }
4330
+ let cursorX;
4331
+ if (command.align === "center") {
4332
+ cursorX = command.x - totalWidth / 2;
4333
+ } else if (command.align === "right") {
4334
+ cursorX = command.x - totalWidth;
4335
+ } else {
4336
+ cursorX = command.x;
4337
+ }
4338
+ const startX = cursorX;
4339
+ withOpacity(ctx, command.opacity, () => {
4340
+ ctx.textBaseline = command.baseline;
4341
+ for (const { width, resolved } of measured) {
4342
+ applyFont(ctx, {
4343
+ size: resolved.fontSize,
4344
+ weight: resolved.fontWeight,
4345
+ family: resolved.fontFamily
4346
+ });
4347
+ ctx.fillStyle = resolved.color;
4348
+ ctx.textAlign = "left";
4349
+ ctx.fillText(resolved.text, cursorX, command.y);
4350
+ cursorX += width;
4351
+ }
4352
+ });
4353
+ const height = Math.max(1, maxAscent + maxDescent);
4354
+ let topY;
4355
+ if (command.baseline === "top") {
4356
+ topY = command.y;
4357
+ } else if (command.baseline === "middle") {
4358
+ topY = command.y - height / 2;
4359
+ } else if (command.baseline === "bottom") {
4360
+ topY = command.y - height;
4361
+ } else {
4362
+ topY = command.y - maxAscent;
4363
+ }
4364
+ rendered.push({
4365
+ id,
4366
+ kind: "draw",
4367
+ bounds: {
4368
+ x: startX,
4369
+ y: topY,
4370
+ width: Math.max(1, totalWidth),
4371
+ height
4372
+ },
4373
+ foregroundColor: command.defaultColor,
4374
+ backgroundColor: theme.background
4375
+ });
4376
+ break;
4377
+ }
4268
4378
  }
4269
4379
  }
4270
4380
  return rendered;
package/dist/renderer.js CHANGED
@@ -473,6 +473,38 @@ function renderFlowNode(ctx, node, bounds, theme) {
473
473
  ctx.shadowOffsetX = 0;
474
474
  ctx.shadowOffsetY = 0;
475
475
  }
476
+ if (node.accentColor) {
477
+ const barWidth = node.accentBarWidth ?? 3;
478
+ const effectiveRadius = node.shape === "box" ? 0 : cornerRadius;
479
+ ctx.save();
480
+ ctx.beginPath();
481
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, effectiveRadius);
482
+ ctx.clip();
483
+ ctx.fillStyle = node.accentColor;
484
+ ctx.fillRect(bounds.x, bounds.y, barWidth, bounds.height);
485
+ ctx.restore();
486
+ }
487
+ if (node.glowColor) {
488
+ const glowW = node.glowWidth ?? 16;
489
+ const glowOp = node.glowOpacity ?? 0.15;
490
+ ctx.save();
491
+ ctx.beginPath();
492
+ ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius);
493
+ ctx.clip();
494
+ const barOffset = node.accentColor ? node.accentBarWidth ?? 3 : 0;
495
+ const gradient = ctx.createLinearGradient(
496
+ bounds.x + barOffset,
497
+ bounds.y,
498
+ bounds.x + barOffset + glowW,
499
+ bounds.y
500
+ );
501
+ gradient.addColorStop(0, node.glowColor);
502
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
503
+ ctx.globalAlpha = glowOp;
504
+ ctx.fillStyle = gradient;
505
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
506
+ ctx.restore();
507
+ }
476
508
  const headingFont = resolveFont(theme.fonts.heading, "heading");
477
509
  const bodyFont = resolveFont(theme.fonts.body, "body");
478
510
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -3086,6 +3118,84 @@ function renderDrawCommands(ctx, commands, theme) {
3086
3118
  });
3087
3119
  break;
3088
3120
  }
3121
+ case "text-row": {
3122
+ const segments = command.segments;
3123
+ if (segments.length === 0) break;
3124
+ const resolveSegment = (seg) => ({
3125
+ text: seg.text,
3126
+ fontSize: seg.fontSize ?? command.defaultFontSize,
3127
+ fontWeight: seg.fontWeight ?? command.defaultFontWeight,
3128
+ fontFamily: resolveDrawFont(theme, seg.fontFamily ?? command.defaultFontFamily),
3129
+ color: seg.color ?? command.defaultColor
3130
+ });
3131
+ const measured = [];
3132
+ let totalWidth = 0;
3133
+ let maxAscent = 0;
3134
+ let maxDescent = 0;
3135
+ for (const seg of segments) {
3136
+ const resolved = resolveSegment(seg);
3137
+ applyFont(ctx, {
3138
+ size: resolved.fontSize,
3139
+ weight: resolved.fontWeight,
3140
+ family: resolved.fontFamily
3141
+ });
3142
+ const metrics = ctx.measureText(resolved.text);
3143
+ const width = metrics.width;
3144
+ const ascent = metrics.actualBoundingBoxAscent || 0;
3145
+ const descent = metrics.actualBoundingBoxDescent || 0;
3146
+ totalWidth += width;
3147
+ maxAscent = Math.max(maxAscent, ascent);
3148
+ maxDescent = Math.max(maxDescent, descent);
3149
+ measured.push({ width, resolved });
3150
+ }
3151
+ let cursorX;
3152
+ if (command.align === "center") {
3153
+ cursorX = command.x - totalWidth / 2;
3154
+ } else if (command.align === "right") {
3155
+ cursorX = command.x - totalWidth;
3156
+ } else {
3157
+ cursorX = command.x;
3158
+ }
3159
+ const startX = cursorX;
3160
+ withOpacity(ctx, command.opacity, () => {
3161
+ ctx.textBaseline = command.baseline;
3162
+ for (const { width, resolved } of measured) {
3163
+ applyFont(ctx, {
3164
+ size: resolved.fontSize,
3165
+ weight: resolved.fontWeight,
3166
+ family: resolved.fontFamily
3167
+ });
3168
+ ctx.fillStyle = resolved.color;
3169
+ ctx.textAlign = "left";
3170
+ ctx.fillText(resolved.text, cursorX, command.y);
3171
+ cursorX += width;
3172
+ }
3173
+ });
3174
+ const height = Math.max(1, maxAscent + maxDescent);
3175
+ let topY;
3176
+ if (command.baseline === "top") {
3177
+ topY = command.y;
3178
+ } else if (command.baseline === "middle") {
3179
+ topY = command.y - height / 2;
3180
+ } else if (command.baseline === "bottom") {
3181
+ topY = command.y - height;
3182
+ } else {
3183
+ topY = command.y - maxAscent;
3184
+ }
3185
+ rendered.push({
3186
+ id,
3187
+ kind: "draw",
3188
+ bounds: {
3189
+ x: startX,
3190
+ y: topY,
3191
+ width: Math.max(1, totalWidth),
3192
+ height
3193
+ },
3194
+ foregroundColor: command.defaultColor,
3195
+ backgroundColor: theme.background
3196
+ });
3197
+ break;
3198
+ }
3089
3199
  }
3090
3200
  }
3091
3201
  return rendered;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/graphic-designer-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Deterministic visual content generator — code screenshots, terminal shots, flowcharts, and infographics. No browser dependency.",
5
5
  "type": "module",
6
6
  "license": "MIT",