sketchmark 0.2.0 → 0.2.2

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.cjs CHANGED
@@ -261,10 +261,6 @@ function propsToStyle(p) {
261
261
  s.color = p.color;
262
262
  if (p.opacity)
263
263
  s.opacity = parseFloat(p.opacity);
264
- if (p.radius)
265
- s.radius = parseFloat(p.radius);
266
- if (p.shadow)
267
- s.shadow = p.shadow === "true";
268
264
  if (p["font-size"])
269
265
  s.fontSize = parseFloat(p["font-size"]);
270
266
  if (p["font-weight"])
@@ -281,8 +277,9 @@ function propsToStyle(p) {
281
277
  s.letterSpacing = parseFloat(p["letter-spacing"]);
282
278
  if (p.font)
283
279
  s.font = p.font;
284
- if (p["dash"]) {
285
- const parts = p["dash"]
280
+ const dashVal = p["dash"] || p["stroke-dash"];
281
+ if (dashVal) {
282
+ const parts = dashVal
286
283
  .split(",")
287
284
  .map(Number)
288
285
  .filter((n) => !isNaN(n));
@@ -1489,13 +1486,22 @@ function sizeNode(n) {
1489
1486
  n.h = n.h || 50;
1490
1487
  break;
1491
1488
  case "text": {
1492
- // read fontSize from style if set, otherwise use default
1493
1489
  const fontSize = Number(n.style?.fontSize ?? 13);
1494
1490
  const charWidth = fontSize * 0.55;
1495
- const maxW = n.width ?? 400;
1496
- const approxLines = Math.ceil((n.label.length * charWidth) / (maxW - 16));
1497
- n.w = maxW;
1498
- n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + 8);
1491
+ const pad = Number(n.style?.padding ?? 8) * 2;
1492
+ if (n.width) {
1493
+ // User set width → word-wrap within it
1494
+ const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
1495
+ n.w = n.width;
1496
+ n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
1497
+ }
1498
+ else {
1499
+ // Auto-size to content
1500
+ const lines = n.label.split("\\n");
1501
+ const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
1502
+ n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
1503
+ n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
1504
+ }
1499
1505
  break;
1500
1506
  }
1501
1507
  default:
@@ -1712,17 +1718,16 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
1712
1718
  if (layout === "row") {
1713
1719
  const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1714
1720
  const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1715
- const maxH = Math.max(...hs);
1716
1721
  const { start, gaps } = distribute(ws, contentW, gap, justify);
1717
1722
  let x = contentX + start;
1718
1723
  for (let i = 0; i < kids.length; i++) {
1719
1724
  let y;
1720
1725
  switch (align) {
1721
1726
  case "center":
1722
- y = contentY + (maxH - hs[i]) / 2;
1727
+ y = contentY + (contentH - hs[i]) / 2;
1723
1728
  break;
1724
1729
  case "end":
1725
- y = contentY + maxH - hs[i];
1730
+ y = contentY + contentH - hs[i];
1726
1731
  break;
1727
1732
  default:
1728
1733
  y = contentY;
@@ -1743,17 +1748,16 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
1743
1748
  // column (default)
1744
1749
  const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1745
1750
  const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1746
- const maxW = Math.max(...ws);
1747
1751
  const { start, gaps } = distribute(hs, contentH, gap, justify);
1748
1752
  let y = contentY + start;
1749
1753
  for (let i = 0; i < kids.length; i++) {
1750
1754
  let x;
1751
1755
  switch (align) {
1752
1756
  case "center":
1753
- x = contentX + (maxW - ws[i]) / 2;
1757
+ x = contentX + (contentW - ws[i]) / 2;
1754
1758
  break;
1755
1759
  case "end":
1756
- x = contentX + maxW - ws[i];
1760
+ x = contentX + contentW - ws[i];
1757
1761
  break;
1758
1762
  default:
1759
1763
  x = contentX;
@@ -2177,13 +2181,13 @@ function mkG(id, cls) {
2177
2181
  g.setAttribute('class', cls);
2178
2182
  return g;
2179
2183
  }
2180
- function mkT(txt, x, y, sz = 10, wt = 400, col = '#4a2e10', anchor = 'middle') {
2184
+ function mkT(txt, x, y, sz = 10, wt = 400, col = '#4a2e10', anchor = 'middle', font = 'system-ui, sans-serif') {
2181
2185
  const t = se$1('text');
2182
2186
  t.setAttribute('x', String(x));
2183
2187
  t.setAttribute('y', String(y));
2184
2188
  t.setAttribute('text-anchor', anchor);
2185
2189
  t.setAttribute('dominant-baseline', 'middle');
2186
- t.setAttribute('font-family', 'system-ui, sans-serif');
2190
+ t.setAttribute('font-family', font);
2187
2191
  t.setAttribute('font-size', String(sz));
2188
2192
  t.setAttribute('font-weight', String(wt));
2189
2193
  t.setAttribute('fill', col);
@@ -2199,7 +2203,7 @@ function hashStr$4(s) {
2199
2203
  }
2200
2204
  const BASE = { roughness: 1.2, bowing: 0.7 };
2201
2205
  // ── Axes ───────────────────────────────────────────────────
2202
- function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol) {
2206
+ function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol, font = 'system-ui, sans-serif') {
2203
2207
  // Y axis
2204
2208
  g.appendChild(rc.line(px, py, px, py + ph, {
2205
2209
  roughness: 0.4, seed: hashStr$4(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
@@ -2218,7 +2222,7 @@ function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol) {
2218
2222
  g.appendChild(rc.line(px - 3, ty, px, ty, {
2219
2223
  roughness: 0.2, seed: hashStr$4(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
2220
2224
  }));
2221
- g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end'));
2225
+ g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end', font));
2222
2226
  }
2223
2227
  }
2224
2228
  function fmtNum$1(v) {
@@ -2227,7 +2231,7 @@ function fmtNum$1(v) {
2227
2231
  return String(v);
2228
2232
  }
2229
2233
  // ── Legend row ─────────────────────────────────────────────
2230
- function legend(g, labels, colors, x, y, labelCol) {
2234
+ function legend(g, labels, colors, x, y, labelCol, font = 'system-ui, sans-serif') {
2231
2235
  labels.forEach((lbl, i) => {
2232
2236
  const dot = se$1('rect');
2233
2237
  dot.setAttribute('x', String(x));
@@ -2237,7 +2241,7 @@ function legend(g, labels, colors, x, y, labelCol) {
2237
2241
  dot.setAttribute('fill', colors[i % colors.length]);
2238
2242
  dot.setAttribute('rx', '1');
2239
2243
  g.appendChild(dot);
2240
- g.appendChild(mkT(lbl, x + 12, y + i * 14 + 4, 9, 400, labelCol, 'start'));
2244
+ g.appendChild(mkT(lbl, x + 12, y + i * 14 + 4, 9, 400, labelCol, 'start', font));
2241
2245
  });
2242
2246
  }
2243
2247
  // ── Public entry ───────────────────────────────────────────
@@ -2248,6 +2252,11 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2248
2252
  const bgFill = String(s.fill ?? palette.nodeFill);
2249
2253
  const bgStroke = String(s.stroke ?? (isDark ? '#5a4a30' : '#c8b898'));
2250
2254
  const lc = String(s.color ?? palette.titleText);
2255
+ const cFont = String(s.font ? `${s.font}, system-ui, sans-serif` : 'system-ui, sans-serif');
2256
+ const cFontSize = Number(s.fontSize ?? 12);
2257
+ const cFontWeight = s.fontWeight ?? 600;
2258
+ if (s.opacity != null)
2259
+ cg.setAttribute('opacity', String(s.opacity));
2251
2260
  // Background box
2252
2261
  cg.appendChild(rc.rectangle(c.x, c.y, c.w, c.h, {
2253
2262
  ...BASE, seed: hashStr$4(c.id),
@@ -2257,7 +2266,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2257
2266
  }));
2258
2267
  // Title
2259
2268
  if (c.title) {
2260
- cg.appendChild(mkT(c.title, c.x + c.w / 2, c.y + 14, 12, 600, lc));
2269
+ cg.appendChild(mkT(c.title, c.x + c.w / 2, c.y + 14, cFontSize, cFontWeight, lc, 'middle', cFont));
2261
2270
  }
2262
2271
  const { px, py, pw, ph, cx, cy } = chartLayout(c);
2263
2272
  // ── Pie / Donut ──────────────────────────────────────────
@@ -2283,7 +2292,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2283
2292
  angle += sweep;
2284
2293
  }
2285
2294
  // Mini legend on left
2286
- legend(cg, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc);
2295
+ legend(cg, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
2287
2296
  return cg;
2288
2297
  }
2289
2298
  // ── Scatter ───────────────────────────────────────────────
@@ -2304,7 +2313,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2304
2313
  strokeWidth: 1.2,
2305
2314
  }));
2306
2315
  });
2307
- legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc);
2316
+ legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc, cFont);
2308
2317
  return cg;
2309
2318
  }
2310
2319
  // ── Bar / Line / Area ─────────────────────────────────────
@@ -2313,10 +2322,10 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2313
2322
  const toY = makeValueToY(allY, py, ph);
2314
2323
  const baseline = toY(0);
2315
2324
  const n = labels.length;
2316
- drawAxes$1(rc, cg, c, px, py, pw, ph, allY, lc);
2325
+ drawAxes$1(rc, cg, c, px, py, pw, ph, allY, lc, cFont);
2317
2326
  // X labels
2318
2327
  labels.forEach((lbl, i) => {
2319
- cg.appendChild(mkT(lbl, px + (i + 0.5) * (pw / n), py + ph + 14, 9, 400, lc));
2328
+ cg.appendChild(mkT(lbl, px + (i + 0.5) * (pw / n), py + ph + 14, 9, 400, lc, 'middle', cFont));
2320
2329
  });
2321
2330
  if (c.chartType === 'bar') {
2322
2331
  const groupW = pw / n;
@@ -2387,7 +2396,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2387
2396
  }
2388
2397
  // Multi-series legend
2389
2398
  if (series.length > 1) {
2390
- legend(cg, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc);
2399
+ legend(cg, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc, cFont);
2391
2400
  }
2392
2401
  return cg;
2393
2402
  }
@@ -4853,6 +4862,14 @@ function hashStr$3(s) {
4853
4862
  return h;
4854
4863
  }
4855
4864
  const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
4865
+ /** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
4866
+ function darkenHex$1(hex, amount = 0.12) {
4867
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
4868
+ if (!m)
4869
+ return hex;
4870
+ const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
4871
+ return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
4872
+ }
4856
4873
  // ── Small helper: load + resolve font from style or fall back ─────────────
4857
4874
  function resolveStyleFont$1(style, fallback) {
4858
4875
  const raw = String(style["font"] ?? "");
@@ -5023,6 +5040,7 @@ function renderShape$1(rc, n, palette) {
5023
5040
  fillStyle: "solid",
5024
5041
  stroke,
5025
5042
  strokeWidth: Number(s.strokeWidth ?? 1.9),
5043
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5026
5044
  };
5027
5045
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
5028
5046
  const hw = n.w / 2 - 2;
@@ -5205,6 +5223,8 @@ function renderToSVG(sg, container, options = {}) {
5205
5223
  continue;
5206
5224
  const gs = g.style ?? {};
5207
5225
  const gg = mkGroup(`group-${g.id}`, "gg");
5226
+ if (gs.opacity != null)
5227
+ gg.setAttribute("opacity", String(gs.opacity));
5208
5228
  gg.appendChild(rc.rectangle(g.x, g.y, g.w, g.h, {
5209
5229
  ...BASE_ROUGH,
5210
5230
  roughness: 1.7,
@@ -5217,14 +5237,26 @@ function renderToSVG(sg, container, options = {}) {
5217
5237
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
5218
5238
  }));
5219
5239
  // ── Group label typography ──────────────────────────
5220
- // supports: font, font-size, letter-spacing
5221
- // always left-anchored (single line)
5222
5240
  const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
5223
5241
  const gFontSize = Number(gs.fontSize ?? 12);
5242
+ const gFontWeight = gs.fontWeight ?? 500;
5224
5243
  const gFont = resolveStyleFont$1(gs, diagramFont);
5225
5244
  const gLetterSpacing = gs.letterSpacing;
5245
+ const gPad = Number(gs.padding ?? 14);
5246
+ const gTextAlign = String(gs.textAlign ?? "left");
5247
+ const gAnchorMap = {
5248
+ left: "start",
5249
+ center: "middle",
5250
+ right: "end",
5251
+ };
5252
+ const gAnchor = gAnchorMap[gTextAlign] ?? "start";
5253
+ const gTextX = gTextAlign === "right"
5254
+ ? g.x + g.w - gPad
5255
+ : gTextAlign === "center"
5256
+ ? g.x + g.w / 2
5257
+ : g.x + gPad;
5226
5258
  if (g.label) {
5227
- gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
5259
+ gg.appendChild(mkText(g.label, gTextX, g.y + gPad, gFontSize, gFontWeight, gLabelColor, gAnchor, gFont, gLetterSpacing));
5228
5260
  }
5229
5261
  GL.appendChild(gg);
5230
5262
  }
@@ -5245,6 +5277,8 @@ function renderToSVG(sg, container, options = {}) {
5245
5277
  const [x1, y1] = getConnPoint$1(src, dstCX, dstCY);
5246
5278
  const [x2, y2] = getConnPoint$1(dst, srcCX, srcCY);
5247
5279
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
5280
+ if (e.style?.opacity != null)
5281
+ eg.setAttribute("opacity", String(e.style.opacity));
5248
5282
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
5249
5283
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
5250
5284
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
@@ -5285,7 +5319,9 @@ function renderToSVG(sg, container, options = {}) {
5285
5319
  const eFontSize = Number(e.style?.fontSize ?? 11);
5286
5320
  const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
5287
5321
  const eLetterSpacing = e.style?.letterSpacing;
5288
- eg.appendChild(mkText(e.label, mx, my, eFontSize, 400, palette.edgeLabelText, "middle", eFont, eLetterSpacing));
5322
+ const eFontWeight = e.style?.fontWeight ?? 400;
5323
+ const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
5324
+ eg.appendChild(mkText(e.label, mx, my, eFontSize, eFontWeight, eLabelColor, "middle", eFont, eLetterSpacing));
5289
5325
  }
5290
5326
  EL.appendChild(eg);
5291
5327
  }
@@ -5294,6 +5330,8 @@ function renderToSVG(sg, container, options = {}) {
5294
5330
  const NL = mkGroup("node-layer");
5295
5331
  for (const n of sg.nodes) {
5296
5332
  const ng = mkGroup(`node-${n.id}`, "ng");
5333
+ if (n.style?.opacity != null)
5334
+ ng.setAttribute("opacity", String(n.style.opacity));
5297
5335
  renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
5298
5336
  // ── Node / text typography ─────────────────────────
5299
5337
  // supports: font, font-size, letter-spacing, text-align, line-height
@@ -5312,18 +5350,19 @@ function renderToSVG(sg, container, options = {}) {
5312
5350
  // line-height is a multiplier (e.g. 1.4 = 140% of font-size)
5313
5351
  const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
5314
5352
  const letterSpacing = n.style?.letterSpacing;
5353
+ const pad = Number(n.style?.padding ?? 8);
5315
5354
  // x shifts for left / right alignment
5316
5355
  const textX = textAlign === "left"
5317
- ? n.x + 8
5356
+ ? n.x + pad
5318
5357
  : textAlign === "right"
5319
- ? n.x + n.w - 8
5358
+ ? n.x + n.w - pad
5320
5359
  : n.x + n.w / 2;
5321
5360
  const lines = n.shape === 'text' && !n.label.includes('\n')
5322
- ? wrapText$1(n.label, n.w - 16, fontSize)
5361
+ ? wrapText$1(n.label, n.w - pad * 2, fontSize)
5323
5362
  : n.label.split('\n');
5324
5363
  const verticalAlign = String(n.style?.verticalAlign ?? "middle");
5325
- const nodeBodyTop = n.y + 6;
5326
- const nodeBodyBottom = n.y + n.h - 6;
5364
+ const nodeBodyTop = n.y + pad;
5365
+ const nodeBodyBottom = n.y + n.h - pad;
5327
5366
  const nodeBodyMid = n.y + n.h / 2;
5328
5367
  const blockH = (lines.length - 1) * lineHeight;
5329
5368
  const textCY = verticalAlign === "top"
@@ -5355,15 +5394,19 @@ function renderToSVG(sg, container, options = {}) {
5355
5394
  const fill = String(gs.fill ?? palette.tableFill);
5356
5395
  const strk = String(gs.stroke ?? palette.tableStroke);
5357
5396
  const textCol = String(gs.color ?? palette.tableText);
5358
- const hdrFill = palette.tableHeaderFill;
5397
+ const hdrFill = gs.fill ? darkenHex$1(fill, 0.08) : palette.tableHeaderFill;
5359
5398
  const hdrText = String(gs.color ?? palette.tableHeaderText);
5360
5399
  const divCol = palette.tableDivider;
5361
5400
  const pad = t.labelH;
5401
+ const tStrokeWidth = Number(gs.strokeWidth ?? 1.5);
5402
+ const tFontWeight = gs.fontWeight ?? 500;
5362
5403
  // ── Table-level font (applies to label + all cells) ─
5363
5404
  // supports: font, font-size, letter-spacing
5364
5405
  const tFontSize = Number(gs.fontSize ?? 12);
5365
5406
  const tFont = resolveStyleFont$1(gs, diagramFont);
5366
5407
  const tLetterSpacing = gs.letterSpacing;
5408
+ if (gs.opacity != null)
5409
+ tg.setAttribute("opacity", String(gs.opacity));
5367
5410
  // outer border
5368
5411
  tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
5369
5412
  ...BASE_ROUGH,
@@ -5371,7 +5414,8 @@ function renderToSVG(sg, container, options = {}) {
5371
5414
  fill,
5372
5415
  fillStyle: "solid",
5373
5416
  stroke: strk,
5374
- strokeWidth: 1.5,
5417
+ strokeWidth: tStrokeWidth,
5418
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5375
5419
  }));
5376
5420
  // label strip separator
5377
5421
  tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
@@ -5380,8 +5424,8 @@ function renderToSVG(sg, container, options = {}) {
5380
5424
  stroke: strk,
5381
5425
  strokeWidth: 1,
5382
5426
  }));
5383
- // ── Table label: font, font-size, letter-spacing (always left) ──
5384
- tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, "start", tFont, tLetterSpacing));
5427
+ // ── Table label: font, font-size, font-weight, letter-spacing (always left) ──
5428
+ tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, "start", tFont, tLetterSpacing));
5385
5429
  // rows
5386
5430
  let rowY = t.y + pad;
5387
5431
  for (const row of t.rows) {
@@ -5410,7 +5454,7 @@ function renderToSVG(sg, container, options = {}) {
5410
5454
  right: "end",
5411
5455
  };
5412
5456
  const cellAnchor = cellAnchorMap[cellAlignProp] ?? "middle";
5413
- const cellFw = row.kind === "header" ? 600 : 400;
5457
+ const cellFw = row.kind === "header" ? 600 : (gs.fontWeight ?? 400);
5414
5458
  const cellColor = row.kind === "header" ? hdrText : textCol;
5415
5459
  let cx = t.x;
5416
5460
  row.cells.forEach((cell, i) => {
@@ -5449,27 +5493,31 @@ function renderToSVG(sg, container, options = {}) {
5449
5493
  const gs = n.style ?? {};
5450
5494
  const fill = String(gs.fill ?? palette.noteFill);
5451
5495
  const strk = String(gs.stroke ?? palette.noteStroke);
5496
+ const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
5452
5497
  const fold = 14;
5453
5498
  const { x, y, w, h } = n;
5499
+ if (gs.opacity != null)
5500
+ ng.setAttribute("opacity", String(gs.opacity));
5454
5501
  // ── Note typography ─────────────────────────────────
5455
- // supports: font, font-size, letter-spacing, text-align, line-height
5456
5502
  const nFontSize = Number(gs.fontSize ?? 12);
5503
+ const nFontWeight = gs.fontWeight ?? 400;
5457
5504
  const nFont = resolveStyleFont$1(gs, diagramFont);
5458
5505
  const nLetterSpacing = gs.letterSpacing;
5459
5506
  const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
5460
5507
  const nTextAlign = String(gs.textAlign ?? "left");
5508
+ const nPad = Number(gs.padding ?? 12);
5461
5509
  const nAnchorMap = {
5462
5510
  left: "start",
5463
5511
  center: "middle",
5464
5512
  right: "end",
5465
5513
  };
5466
5514
  const nAnchor = nAnchorMap[nTextAlign] ?? "start";
5467
- // x position for the text block (pad from left, with alignment)
5468
5515
  const nTextX = nTextAlign === "right"
5469
- ? x + w - fold - 6
5516
+ ? x + w - fold - nPad
5470
5517
  : nTextAlign === "center"
5471
5518
  ? x + (w - fold) / 2
5472
- : x + 12;
5519
+ : x + nPad;
5520
+ const nFoldPad = fold + nPad; // text starts below fold + user padding
5473
5521
  ng.appendChild(rc.polygon([
5474
5522
  [x, y],
5475
5523
  [x + w - fold, y],
@@ -5482,7 +5530,8 @@ function renderToSVG(sg, container, options = {}) {
5482
5530
  fill,
5483
5531
  fillStyle: "solid",
5484
5532
  stroke: strk,
5485
- strokeWidth: 1.2,
5533
+ strokeWidth: nStrokeWidth,
5534
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5486
5535
  }));
5487
5536
  ng.appendChild(rc.polygon([
5488
5537
  [x + w - fold, y],
@@ -5494,11 +5543,11 @@ function renderToSVG(sg, container, options = {}) {
5494
5543
  fill: palette.noteFold,
5495
5544
  fillStyle: "solid",
5496
5545
  stroke: strk,
5497
- strokeWidth: 0.8,
5546
+ strokeWidth: Math.min(nStrokeWidth, 0.8),
5498
5547
  }));
5499
5548
  const nVerticalAlign = String(gs.verticalAlign ?? "top");
5500
- const bodyTop = y + fold + 8; // below the fold triangle
5501
- const bodyBottom = y + h - 8; // above bottom edge
5549
+ const bodyTop = y + nFoldPad;
5550
+ const bodyBottom = y + h - nPad;
5502
5551
  const bodyMid = (bodyTop + bodyBottom) / 2;
5503
5552
  const blockH = (n.lines.length - 1) * nLineHeight;
5504
5553
  const blockCY = nVerticalAlign === "bottom"
@@ -5506,13 +5555,11 @@ function renderToSVG(sg, container, options = {}) {
5506
5555
  : nVerticalAlign === "middle"
5507
5556
  ? bodyMid
5508
5557
  : bodyTop + blockH / 2;
5509
- // multiline: use mkMultilineText so line-height is respected
5510
5558
  if (n.lines.length > 1) {
5511
- // vertical centre of the text block inside the note
5512
- ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
5559
+ ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
5513
5560
  }
5514
5561
  else {
5515
- ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
5562
+ ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
5516
5563
  }
5517
5564
  NoteL.appendChild(ng);
5518
5565
  }
@@ -5521,13 +5568,27 @@ function renderToSVG(sg, container, options = {}) {
5521
5568
  const MDL = mkGroup('markdown-layer');
5522
5569
  for (const m of sg.markdowns) {
5523
5570
  const mg = mkGroup(`markdown-${m.id}`, 'mdg');
5524
- const mFont = resolveStyleFont$1(m.style, diagramFont);
5525
- const baseColor = String(m.style?.color ?? palette.nodeText);
5526
- const textAlign = String(m.style?.textAlign ?? 'left');
5571
+ const gs = m.style ?? {};
5572
+ const mFont = resolveStyleFont$1(gs, diagramFont);
5573
+ const baseColor = String(gs.color ?? palette.nodeText);
5574
+ const textAlign = String(gs.textAlign ?? 'left');
5527
5575
  const anchor = textAlign === 'right' ? 'end'
5528
5576
  : textAlign === 'center' ? 'middle'
5529
5577
  : 'start';
5530
- const PAD = Number(m.style?.padding ?? 16);
5578
+ const PAD = Number(gs.padding ?? 16);
5579
+ const mLetterSpacing = gs.letterSpacing;
5580
+ if (gs.opacity != null)
5581
+ mg.setAttribute('opacity', String(gs.opacity));
5582
+ // Background + border
5583
+ if (gs.fill || gs.stroke) {
5584
+ mg.appendChild(rc.rectangle(m.x, m.y, m.w, m.h, {
5585
+ ...BASE_ROUGH, seed: hashStr$3(m.id),
5586
+ fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
5587
+ stroke: String(gs.stroke ?? 'none'),
5588
+ strokeWidth: Number(gs.strokeWidth ?? 1.2),
5589
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5590
+ }));
5591
+ }
5531
5592
  const textX = textAlign === 'right' ? m.x + m.w - PAD
5532
5593
  : textAlign === 'center' ? m.x + m.w / 2
5533
5594
  : m.x + PAD;
@@ -5550,6 +5611,8 @@ function renderToSVG(sg, container, options = {}) {
5550
5611
  t.setAttribute('fill', baseColor);
5551
5612
  t.setAttribute('pointer-events', 'none');
5552
5613
  t.setAttribute('user-select', 'none');
5614
+ if (mLetterSpacing != null)
5615
+ t.setAttribute('letter-spacing', String(mLetterSpacing));
5553
5616
  for (const run of line.runs) {
5554
5617
  const span = se('tspan');
5555
5618
  span.textContent = run.text;
@@ -5639,7 +5702,7 @@ function drawPieArc(rc, ctx, cx, cy, r, ir, startAngle, endAngle, color, seed) {
5639
5702
  });
5640
5703
  }
5641
5704
  // ── Axes ───────────────────────────────────────────────────
5642
- function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
5705
+ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R, font = 'system-ui, sans-serif') {
5643
5706
  const toY = makeValueToY(allY, py, ph);
5644
5707
  const baseline = toY(0);
5645
5708
  // Y axis
@@ -5653,7 +5716,7 @@ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
5653
5716
  continue;
5654
5717
  rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
5655
5718
  ctx.save();
5656
- ctx.font = '400 9px system-ui, sans-serif';
5719
+ ctx.font = `400 9px ${font}`;
5657
5720
  ctx.fillStyle = labelCol;
5658
5721
  ctx.textAlign = 'right';
5659
5722
  ctx.textBaseline = 'middle';
@@ -5662,9 +5725,9 @@ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
5662
5725
  }
5663
5726
  }
5664
5727
  // ── Legend ─────────────────────────────────────────────────
5665
- function drawLegend(ctx, labels, colors, x, y, labelCol) {
5728
+ function drawLegend(ctx, labels, colors, x, y, labelCol, font = 'system-ui, sans-serif') {
5666
5729
  ctx.save();
5667
- ctx.font = '400 9px system-ui, sans-serif';
5730
+ ctx.font = `400 9px ${font}`;
5668
5731
  ctx.textAlign = 'left';
5669
5732
  ctx.textBaseline = 'middle';
5670
5733
  labels.forEach((lbl, i) => {
@@ -5682,6 +5745,11 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5682
5745
  const bgFill = String(s.fill ?? pal.nodeFill);
5683
5746
  const bgStroke = String(s.stroke ?? (pal.nodeStroke === 'none' ? '#c8b898' : pal.nodeStroke));
5684
5747
  const lc = String(s.color ?? pal.labelText);
5748
+ const cFont = String(s.font ? `${s.font}, system-ui, sans-serif` : 'system-ui, sans-serif');
5749
+ const cFontSize = Number(s.fontSize ?? 12);
5750
+ const cFontWeight = s.fontWeight ?? 600;
5751
+ if (s.opacity != null)
5752
+ ctx.globalAlpha = Number(s.opacity);
5685
5753
  // Background
5686
5754
  rc.rectangle(c.x, c.y, c.w, c.h, {
5687
5755
  ...R, seed: hashStr$2(c.id),
@@ -5694,7 +5762,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5694
5762
  // Title
5695
5763
  if (c.title) {
5696
5764
  ctx.save();
5697
- ctx.font = '600 12px system-ui, sans-serif';
5765
+ ctx.font = `${cFontWeight} ${cFontSize}px ${cFont}`;
5698
5766
  ctx.fillStyle = lc;
5699
5767
  ctx.textAlign = 'center';
5700
5768
  ctx.textBaseline = 'middle';
@@ -5715,7 +5783,8 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5715
5783
  drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$2(c.id + seg.label + i));
5716
5784
  angle += sweep;
5717
5785
  });
5718
- drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc);
5786
+ drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
5787
+ ctx.globalAlpha = 1;
5719
5788
  return;
5720
5789
  }
5721
5790
  // ── Scatter ───────────────────────────────────────────────
@@ -5735,7 +5804,8 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5735
5804
  strokeWidth: 1.2,
5736
5805
  });
5737
5806
  });
5738
- drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc);
5807
+ drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc, cFont);
5808
+ ctx.globalAlpha = 1;
5739
5809
  return;
5740
5810
  }
5741
5811
  // ── Bar / Line / Area ─────────────────────────────────────
@@ -5744,10 +5814,10 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5744
5814
  const toY = makeValueToY(allY, py, ph);
5745
5815
  const baseline = toY(0);
5746
5816
  const n = labels.length;
5747
- drawAxes(rc, ctx, c, px, py, pw, ph, allY, lc, R);
5817
+ drawAxes(rc, ctx, c, px, py, pw, ph, allY, lc, R, cFont);
5748
5818
  // X labels
5749
5819
  ctx.save();
5750
- ctx.font = '400 9px system-ui, sans-serif';
5820
+ ctx.font = `400 9px ${cFont}`;
5751
5821
  ctx.fillStyle = lc;
5752
5822
  ctx.textAlign = 'center';
5753
5823
  ctx.textBaseline = 'top';
@@ -5824,8 +5894,9 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5824
5894
  }
5825
5895
  // Multi-series legend
5826
5896
  if (series.length > 1) {
5827
- drawLegend(ctx, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc);
5897
+ drawLegend(ctx, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc, cFont);
5828
5898
  }
5899
+ ctx.globalAlpha = 1;
5829
5900
  }
5830
5901
 
5831
5902
  // ============================================================
@@ -5838,6 +5909,14 @@ function hashStr$1(s) {
5838
5909
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
5839
5910
  return h;
5840
5911
  }
5912
+ /** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
5913
+ function darkenHex(hex, amount = 0.12) {
5914
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
5915
+ if (!m)
5916
+ return hex;
5917
+ const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
5918
+ return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
5919
+ }
5841
5920
  // ── Small helper: load + resolve font from a style map ────────────────────
5842
5921
  function resolveStyleFont(style, fallback) {
5843
5922
  const raw = String(style['font'] ?? '');
@@ -5955,6 +6034,7 @@ function renderShape(rc, ctx, n, palette, R) {
5955
6034
  ...R, seed: hashStr$1(n.id),
5956
6035
  fill, fillStyle: 'solid',
5957
6036
  stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
6037
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5958
6038
  };
5959
6039
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
5960
6040
  const hw = n.w / 2 - 2;
@@ -6090,6 +6170,8 @@ function renderToCanvas(sg, canvas, options = {}) {
6090
6170
  if (!g.w)
6091
6171
  continue;
6092
6172
  const gs = g.style ?? {};
6173
+ if (gs.opacity != null)
6174
+ ctx.globalAlpha = Number(gs.opacity);
6093
6175
  rc.rectangle(g.x, g.y, g.w, g.h, {
6094
6176
  ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
6095
6177
  fill: String(gs.fill ?? palette.groupFill),
@@ -6098,16 +6180,21 @@ function renderToCanvas(sg, canvas, options = {}) {
6098
6180
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
6099
6181
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
6100
6182
  });
6101
- // ── Group label ──────────────────────────────────────
6102
- // Only render when label has content — empty label = no reserved space
6103
- // supports: font, font-size, letter-spacing (always left-anchored)
6104
6183
  if (g.label) {
6105
6184
  const gFontSize = Number(gs.fontSize ?? 12);
6185
+ const gFontWeight = gs.fontWeight ?? 500;
6106
6186
  const gFont = resolveStyleFont(gs, diagramFont);
6107
6187
  const gLetterSpacing = gs.letterSpacing;
6108
6188
  const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
6109
- drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
6189
+ const gPad = Number(gs.padding ?? 14);
6190
+ const gTextAlign = String(gs.textAlign ?? 'left');
6191
+ const gTextX = gTextAlign === 'right' ? g.x + g.w - gPad
6192
+ : gTextAlign === 'center' ? g.x + g.w / 2
6193
+ : g.x + gPad;
6194
+ drawText(ctx, g.label, gTextX, g.y + gPad + 2, gFontSize, gFontWeight, gLabelColor, gTextAlign, gFont, gLetterSpacing);
6110
6195
  }
6196
+ if (gs.opacity != null)
6197
+ ctx.globalAlpha = 1;
6111
6198
  }
6112
6199
  // ── Edges ─────────────────────────────────────────────────
6113
6200
  for (const e of sg.edges) {
@@ -6119,6 +6206,8 @@ function renderToCanvas(sg, canvas, options = {}) {
6119
6206
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
6120
6207
  const [x1, y1] = getConnPoint(src, dstCX, dstCY);
6121
6208
  const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
6209
+ if (e.style?.opacity != null)
6210
+ ctx.globalAlpha = Number(e.style.opacity);
6122
6211
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
6123
6212
  const { arrowAt, dashed } = connMeta(e.connector);
6124
6213
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
@@ -6147,17 +6236,22 @@ function renderToCanvas(sg, canvas, options = {}) {
6147
6236
  const eFontSize = Number(e.style?.fontSize ?? 11);
6148
6237
  const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
6149
6238
  const eLetterSpacing = e.style?.letterSpacing;
6239
+ const eFontWeight = e.style?.fontWeight ?? 400;
6240
+ const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
6150
6241
  ctx.save();
6151
- ctx.font = `400 ${eFontSize}px ${eFont}`;
6242
+ ctx.font = `${eFontWeight} ${eFontSize}px ${eFont}`;
6152
6243
  const tw = ctx.measureText(e.label).width + 12;
6153
6244
  ctx.restore();
6154
6245
  ctx.fillStyle = palette.edgeLabelBg;
6155
6246
  ctx.fillRect(mx - tw / 2, my - 8, tw, 15);
6156
- drawText(ctx, e.label, mx, my + 3, eFontSize, 400, palette.edgeLabelText, 'center', eFont, eLetterSpacing);
6247
+ drawText(ctx, e.label, mx, my + 3, eFontSize, eFontWeight, eLabelColor, 'center', eFont, eLetterSpacing);
6157
6248
  }
6249
+ ctx.globalAlpha = 1;
6158
6250
  }
6159
6251
  // ── Nodes ─────────────────────────────────────────────────
6160
6252
  for (const n of sg.nodes) {
6253
+ if (n.style?.opacity != null)
6254
+ ctx.globalAlpha = Number(n.style.opacity);
6161
6255
  renderShape(rc, ctx, n, palette, R);
6162
6256
  // ── Node / text typography ─────────────────────────
6163
6257
  // supports: font, font-size, letter-spacing, text-align,
@@ -6171,18 +6265,19 @@ function renderToCanvas(sg, canvas, options = {}) {
6171
6265
  const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
6172
6266
  const letterSpacing = n.style?.letterSpacing;
6173
6267
  const vertAlign = String(n.style?.verticalAlign ?? 'middle');
6268
+ const pad = Number(n.style?.padding ?? 8);
6174
6269
  // x shifts for left/right alignment
6175
- const textX = textAlign === 'left' ? n.x + 8
6176
- : textAlign === 'right' ? n.x + n.w - 8
6270
+ const textX = textAlign === 'left' ? n.x + pad
6271
+ : textAlign === 'right' ? n.x + n.w - pad
6177
6272
  : n.x + n.w / 2;
6178
6273
  // word-wrap for text shape; explicit \n for all others
6179
6274
  const rawLines = n.label.split('\n');
6180
6275
  const lines = n.shape === 'text' && rawLines.length === 1
6181
- ? wrapText(n.label, n.w - 16, fontSize)
6276
+ ? wrapText(n.label, n.w - pad * 2, fontSize)
6182
6277
  : rawLines;
6183
6278
  // vertical-align: compute textCY from top/middle/bottom
6184
- const nodeBodyTop = n.y + 6;
6185
- const nodeBodyBottom = n.y + n.h - 6;
6279
+ const nodeBodyTop = n.y + pad;
6280
+ const nodeBodyBottom = n.y + n.h - pad;
6186
6281
  const blockH = (lines.length - 1) * lineHeight;
6187
6282
  const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
6188
6283
  : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
@@ -6193,6 +6288,8 @@ function renderToCanvas(sg, canvas, options = {}) {
6193
6288
  else {
6194
6289
  drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
6195
6290
  }
6291
+ if (n.style?.opacity != null)
6292
+ ctx.globalAlpha = 1;
6196
6293
  }
6197
6294
  // ── Tables ────────────────────────────────────────────────
6198
6295
  for (const t of sg.tables) {
@@ -6202,25 +6299,28 @@ function renderToCanvas(sg, canvas, options = {}) {
6202
6299
  const textCol = String(gs.color ?? palette.tableText);
6203
6300
  const pad = t.labelH;
6204
6301
  // ── Table-level font ────────────────────────────────
6205
- // supports: font, font-size, letter-spacing
6206
- // cells also support text-align
6207
6302
  const tFontSize = Number(gs.fontSize ?? 12);
6208
6303
  const tFont = resolveStyleFont(gs, diagramFont);
6209
6304
  const tLetterSpacing = gs.letterSpacing;
6305
+ const tStrokeWidth = Number(gs.strokeWidth ?? 1.5);
6306
+ const tFontWeight = gs.fontWeight ?? 500;
6307
+ if (gs.opacity != null)
6308
+ ctx.globalAlpha = Number(gs.opacity);
6210
6309
  rc.rectangle(t.x, t.y, t.w, t.h, {
6211
6310
  ...R, seed: hashStr$1(t.id),
6212
- fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.5,
6311
+ fill, fillStyle: 'solid', stroke: strk, strokeWidth: tStrokeWidth,
6312
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6213
6313
  });
6214
6314
  rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
6215
6315
  roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
6216
6316
  });
6217
6317
  // ── Table label: always left-anchored ───────────────
6218
- drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
6318
+ drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, 'left', tFont, tLetterSpacing);
6219
6319
  let rowY = t.y + pad;
6220
6320
  for (const row of t.rows) {
6221
6321
  const rh = row.kind === 'header' ? t.headerH : t.rowH;
6222
6322
  if (row.kind === 'header') {
6223
- ctx.fillStyle = palette.tableHeaderFill;
6323
+ ctx.fillStyle = gs.fill ? darkenHex(fill, 0.08) : palette.tableHeaderFill;
6224
6324
  ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
6225
6325
  }
6226
6326
  rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
@@ -6233,7 +6333,7 @@ function renderToCanvas(sg, canvas, options = {}) {
6233
6333
  const cellAlignProp = (row.kind === 'header'
6234
6334
  ? 'center'
6235
6335
  : String(gs.textAlign ?? 'center'));
6236
- const cellFw = row.kind === 'header' ? 600 : 400;
6336
+ const cellFw = row.kind === 'header' ? 600 : (gs.fontWeight ?? 400);
6237
6337
  const cellColor = row.kind === 'header'
6238
6338
  ? String(gs.color ?? palette.tableHeaderText)
6239
6339
  : textCol;
@@ -6254,63 +6354,87 @@ function renderToCanvas(sg, canvas, options = {}) {
6254
6354
  });
6255
6355
  rowY += rh;
6256
6356
  }
6357
+ ctx.globalAlpha = 1;
6257
6358
  }
6258
6359
  // ── Notes ─────────────────────────────────────────────────
6259
6360
  for (const n of sg.notes) {
6260
6361
  const gs = n.style ?? {};
6261
6362
  const fill = String(gs.fill ?? palette.noteFill);
6262
6363
  const strk = String(gs.stroke ?? palette.noteStroke);
6364
+ const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
6263
6365
  const fold = 14;
6264
6366
  const { x, y, w, h } = n;
6367
+ if (gs.opacity != null)
6368
+ ctx.globalAlpha = Number(gs.opacity);
6265
6369
  rc.polygon([
6266
6370
  [x, y],
6267
6371
  [x + w - fold, y],
6268
6372
  [x + w, y + fold],
6269
6373
  [x + w, y + h],
6270
6374
  [x, y + h],
6271
- ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.2 });
6375
+ ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk,
6376
+ strokeWidth: nStrokeWidth,
6377
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6378
+ });
6272
6379
  rc.polygon([
6273
6380
  [x + w - fold, y],
6274
6381
  [x + w, y + fold],
6275
6382
  [x + w - fold, y + fold],
6276
6383
  ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
6277
- fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
6278
- // ── Note typography ─────────────────────────────────
6279
- // supports: font, font-size, letter-spacing, text-align,
6280
- // vertical-align, line-height
6384
+ fill: palette.noteFold, fillStyle: 'solid', stroke: strk,
6385
+ strokeWidth: Math.min(nStrokeWidth, 0.8),
6386
+ });
6281
6387
  const nFontSize = Number(gs.fontSize ?? 12);
6388
+ const nFontWeight = gs.fontWeight ?? 400;
6282
6389
  const nFont = resolveStyleFont(gs, diagramFont);
6283
6390
  const nLetterSpacing = gs.letterSpacing;
6284
6391
  const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
6285
6392
  const nTextAlign = String(gs.textAlign ?? 'left');
6286
6393
  const nVertAlign = String(gs.verticalAlign ?? 'top');
6287
6394
  const nColor = String(gs.color ?? palette.noteText);
6288
- const nTextX = nTextAlign === 'right' ? x + w - fold - 6
6395
+ const nPad = Number(gs.padding ?? 12);
6396
+ const nTextX = nTextAlign === 'right' ? x + w - fold - nPad
6289
6397
  : nTextAlign === 'center' ? x + (w - fold) / 2
6290
- : x + 12;
6291
- // vertical-align inside note body (below fold)
6292
- const bodyTop = y + fold + 8;
6293
- const bodyBottom = y + h - 8;
6398
+ : x + nPad;
6399
+ const nFoldPad = fold + nPad;
6400
+ const bodyTop = y + nFoldPad;
6401
+ const bodyBottom = y + h - nPad;
6294
6402
  const blockH = (n.lines.length - 1) * nLineHeight;
6295
6403
  const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
6296
6404
  : nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
6297
- : bodyTop + blockH / 2; // top (default)
6405
+ : bodyTop + blockH / 2;
6298
6406
  if (n.lines.length > 1) {
6299
- drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
6407
+ drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
6300
6408
  }
6301
6409
  else {
6302
- drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
6410
+ drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nFont, nLetterSpacing);
6303
6411
  }
6412
+ if (gs.opacity != null)
6413
+ ctx.globalAlpha = 1;
6304
6414
  }
6305
6415
  // ── Markdown blocks ────────────────────────────────────────
6306
6416
  // Renders prose with Markdown headings and bold/italic inline spans.
6307
6417
  // Canvas has no native bold-within-a-run, so each run is drawn
6308
6418
  // individually with its own ctx.font setting.
6309
6419
  for (const m of (sg.markdowns ?? [])) {
6310
- const mFont = resolveStyleFont(m.style, diagramFont);
6311
- const baseColor = String(m.style?.color ?? palette.nodeText);
6312
- const textAlign = String(m.style?.textAlign ?? 'left');
6313
- const PAD = Number(m.style?.padding ?? 16);
6420
+ const gs = m.style ?? {};
6421
+ const mFont = resolveStyleFont(gs, diagramFont);
6422
+ const baseColor = String(gs.color ?? palette.nodeText);
6423
+ const textAlign = String(gs.textAlign ?? 'left');
6424
+ const PAD = Number(gs.padding ?? 16);
6425
+ const mLetterSpacing = gs.letterSpacing;
6426
+ if (gs.opacity != null)
6427
+ ctx.globalAlpha = Number(gs.opacity);
6428
+ // Background + border
6429
+ if (gs.fill || gs.stroke) {
6430
+ rc.rectangle(m.x, m.y, m.w, m.h, {
6431
+ ...R, seed: hashStr$1(m.id),
6432
+ fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
6433
+ stroke: String(gs.stroke ?? 'none'),
6434
+ strokeWidth: Number(gs.strokeWidth ?? 1.2),
6435
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6436
+ });
6437
+ }
6314
6438
  const anchorX = textAlign === 'right' ? m.x + m.w - PAD
6315
6439
  : textAlign === 'center' ? m.x + m.w / 2
6316
6440
  : m.x + PAD;
@@ -6328,14 +6452,29 @@ function renderToCanvas(sg, canvas, options = {}) {
6328
6452
  ctx.save();
6329
6453
  ctx.textBaseline = 'middle';
6330
6454
  ctx.fillStyle = baseColor;
6455
+ const ls = mLetterSpacing ?? 0;
6456
+ // measure run width including letter-spacing
6457
+ const runW = (run) => {
6458
+ return ctx.measureText(run.text).width + ls * run.text.length;
6459
+ };
6460
+ const drawRun = (run, rx) => {
6461
+ if (ls) {
6462
+ for (const ch of run.text) {
6463
+ ctx.fillText(ch, rx, lineY);
6464
+ rx += ctx.measureText(ch).width + ls;
6465
+ }
6466
+ }
6467
+ else {
6468
+ ctx.fillText(run.text, rx, lineY);
6469
+ }
6470
+ };
6331
6471
  if (textAlign === 'center' || textAlign === 'right') {
6332
- // Measure full line width first
6333
6472
  let totalW = 0;
6334
6473
  for (const run of line.runs) {
6335
6474
  const runStyle = run.italic ? 'italic ' : '';
6336
6475
  const runWeight = run.bold ? 700 : fontWeight;
6337
6476
  ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
6338
- totalW += ctx.measureText(run.text).width;
6477
+ totalW += runW(run);
6339
6478
  }
6340
6479
  let runX = textAlign === 'center' ? anchorX - totalW / 2 : anchorX - totalW;
6341
6480
  ctx.textAlign = 'left';
@@ -6343,25 +6482,25 @@ function renderToCanvas(sg, canvas, options = {}) {
6343
6482
  const runStyle = run.italic ? 'italic ' : '';
6344
6483
  const runWeight = run.bold ? 700 : fontWeight;
6345
6484
  ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
6346
- ctx.fillText(run.text, runX, lineY);
6347
- runX += ctx.measureText(run.text).width;
6485
+ drawRun(run, runX);
6486
+ runX += runW(run);
6348
6487
  }
6349
6488
  }
6350
6489
  else {
6351
- // left-aligned — draw runs left to right from anchorX
6352
6490
  let runX = anchorX;
6353
6491
  ctx.textAlign = 'left';
6354
6492
  for (const run of line.runs) {
6355
6493
  const runStyle = run.italic ? 'italic ' : '';
6356
6494
  const runWeight = run.bold ? 700 : fontWeight;
6357
6495
  ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
6358
- ctx.fillText(run.text, runX, lineY);
6359
- runX += ctx.measureText(run.text).width;
6496
+ drawRun(run, runX);
6497
+ runX += runW(run);
6360
6498
  }
6361
6499
  }
6362
6500
  ctx.restore();
6363
6501
  y += LINE_SPACING[line.kind];
6364
6502
  }
6503
+ ctx.globalAlpha = 1;
6365
6504
  }
6366
6505
  // ── Charts ────────────────────────────────────────────────
6367
6506
  for (const c of sg.charts) {
@@ -6399,6 +6538,7 @@ const getEdgeEl = (svg, f, t) => getEl(svg, `edge-${f}-${t}`);
6399
6538
  const getTableEl = (svg, id) => getEl(svg, `table-${id}`);
6400
6539
  const getNoteEl = (svg, id) => getEl(svg, `note-${id}`);
6401
6540
  const getChartEl = (svg, id) => getEl(svg, `chart-${id}`);
6541
+ const getMarkdownEl = (svg, id) => getEl(svg, `markdown-${id}`);
6402
6542
  function resolveEl(svg, target) {
6403
6543
  // check edge first — target contains connector like "a-->b"
6404
6544
  const edge = parseEdgeTarget(target);
@@ -6410,6 +6550,7 @@ function resolveEl(svg, target) {
6410
6550
  getTableEl(svg, target) ??
6411
6551
  getNoteEl(svg, target) ??
6412
6552
  getChartEl(svg, target) ??
6553
+ getMarkdownEl(svg, target) ??
6413
6554
  null);
6414
6555
  }
6415
6556
  function pathLength(p) {
@@ -6420,6 +6561,15 @@ function pathLength(p) {
6420
6561
  return 200;
6421
6562
  }
6422
6563
  }
6564
+ function clearDashOverridesAfter(el, delayMs) {
6565
+ setTimeout(() => {
6566
+ el.querySelectorAll('path').forEach(p => {
6567
+ p.style.strokeDasharray = '';
6568
+ p.style.strokeDashoffset = '';
6569
+ p.style.transition = '';
6570
+ });
6571
+ }, delayMs);
6572
+ }
6423
6573
  // ── Arrow connector parser ────────────────────────────────
6424
6574
  const ARROW_CONNECTORS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
6425
6575
  function parseEdgeTarget(target) {
@@ -6527,32 +6677,39 @@ function clearEdgeDrawStyles(el) {
6527
6677
  });
6528
6678
  }
6529
6679
  function animateEdgeDraw(el, conn) {
6530
- const paths = Array.from(el.querySelectorAll("path"));
6680
+ const paths = Array.from(el.querySelectorAll('path'));
6531
6681
  if (!paths.length)
6532
6682
  return;
6533
6683
  const linePath = paths[0];
6534
6684
  const headPaths = paths.slice(1);
6535
6685
  const STROKE_DUR = 360;
6536
6686
  const len = pathLength(linePath);
6537
- const reversed = conn.startsWith("<") && !conn.includes(">");
6687
+ const reversed = conn.startsWith('<') && !conn.includes('>');
6538
6688
  linePath.style.strokeDasharray = `${len}`;
6539
6689
  linePath.style.strokeDashoffset = reversed ? `${-len}` : `${len}`;
6540
- linePath.style.transition = "none";
6541
- headPaths.forEach((p) => {
6542
- p.style.opacity = "0";
6543
- p.style.transition = "none";
6690
+ linePath.style.transition = 'none';
6691
+ headPaths.forEach(p => {
6692
+ p.style.opacity = '0';
6693
+ p.style.transition = 'none';
6544
6694
  });
6545
- el.classList.remove("draw-hidden");
6546
- el.classList.add("draw-reveal");
6547
- el.style.opacity = "1";
6695
+ el.classList.remove('draw-hidden');
6696
+ el.classList.add('draw-reveal');
6697
+ el.style.opacity = '1';
6548
6698
  requestAnimationFrame(() => requestAnimationFrame(() => {
6549
6699
  linePath.style.transition = `stroke-dashoffset ${STROKE_DUR}ms cubic-bezier(.4,0,.2,1)`;
6550
- linePath.style.strokeDashoffset = "0";
6700
+ linePath.style.strokeDashoffset = '0';
6551
6701
  setTimeout(() => {
6552
- headPaths.forEach((p) => {
6553
- p.style.transition = "opacity 120ms ease";
6554
- p.style.opacity = "1";
6702
+ headPaths.forEach(p => {
6703
+ p.style.transition = 'opacity 120ms ease';
6704
+ p.style.opacity = '1';
6555
6705
  });
6706
+ // ── ADD: clear inline dash overrides so SVG attribute
6707
+ // (stroke-dasharray="6,5" for dashed arrows) takes over again
6708
+ setTimeout(() => {
6709
+ linePath.style.strokeDasharray = '';
6710
+ linePath.style.strokeDashoffset = '';
6711
+ linePath.style.transition = '';
6712
+ }, 160);
6556
6713
  }, STROKE_DUR - 40);
6557
6714
  }));
6558
6715
  }
@@ -6576,6 +6733,7 @@ class AnimationController {
6576
6733
  this.drawTargetTables = new Set();
6577
6734
  this.drawTargetNotes = new Set();
6578
6735
  this.drawTargetCharts = new Set();
6736
+ this.drawTargetMarkdowns = new Set();
6579
6737
  for (const s of steps) {
6580
6738
  if (s.action !== "draw" || parseEdgeTarget(s.target))
6581
6739
  continue;
@@ -6596,6 +6754,10 @@ class AnimationController {
6596
6754
  this.drawTargetCharts.add(`chart-${s.target}`);
6597
6755
  this.drawTargetNodes.delete(`node-${s.target}`);
6598
6756
  }
6757
+ if (svg.querySelector(`#markdown-${s.target}`)) {
6758
+ this.drawTargetMarkdowns.add(`markdown-${s.target}`);
6759
+ this.drawTargetNodes.delete(`node-${s.target}`);
6760
+ }
6599
6761
  }
6600
6762
  this._clearAll();
6601
6763
  }
@@ -6767,7 +6929,24 @@ class AnimationController {
6767
6929
  });
6768
6930
  }
6769
6931
  });
6770
- this.svg.querySelectorAll(".tg, .ntg, .cg").forEach((el) => {
6932
+ // Markdown
6933
+ this.svg.querySelectorAll(".mdg").forEach((el) => {
6934
+ clearDrawStyles(el);
6935
+ el.style.transition = "none";
6936
+ el.style.opacity = "";
6937
+ if (this.drawTargetMarkdowns.has(el.id)) {
6938
+ el.classList.add("gg-hidden");
6939
+ }
6940
+ else {
6941
+ el.classList.remove("gg-hidden");
6942
+ requestAnimationFrame(() => {
6943
+ el.style.transition = "";
6944
+ });
6945
+ }
6946
+ });
6947
+ this.svg
6948
+ .querySelectorAll(".tg, .ntg, .cg, .mdg")
6949
+ .forEach((el) => {
6771
6950
  el.style.transform = "";
6772
6951
  el.style.transition = "";
6773
6952
  el.style.opacity = "";
@@ -6945,6 +7124,9 @@ class AnimationController {
6945
7124
  if (!firstPath?.style.strokeDasharray)
6946
7125
  prepareForDraw(groupEl);
6947
7126
  animateShapeDraw(groupEl, 550, 40);
7127
+ const pathCount = groupEl.querySelectorAll('path').length;
7128
+ const totalMs = pathCount * 40 + 550 + 120; // stagger + duration + buffer
7129
+ clearDashOverridesAfter(groupEl, totalMs);
6948
7130
  }
6949
7131
  return;
6950
7132
  }
@@ -6965,6 +7147,8 @@ class AnimationController {
6965
7147
  tableEl.classList.remove("gg-hidden");
6966
7148
  prepareForDraw(tableEl);
6967
7149
  animateShapeDraw(tableEl, 500, 40);
7150
+ const tablePathCount = tableEl.querySelectorAll('path').length;
7151
+ clearDashOverridesAfter(tableEl, tablePathCount * 40 + 500 + 120);
6968
7152
  }
6969
7153
  return;
6970
7154
  }
@@ -6985,6 +7169,8 @@ class AnimationController {
6985
7169
  noteEl.classList.remove("gg-hidden");
6986
7170
  prepareForDraw(noteEl);
6987
7171
  animateShapeDraw(noteEl, 420, 55);
7172
+ const notePathCount = noteEl.querySelectorAll('path').length;
7173
+ clearDashOverridesAfter(noteEl, notePathCount * 55 + 420 + 120);
6988
7174
  }
6989
7175
  return;
6990
7176
  }
@@ -7012,6 +7198,28 @@ class AnimationController {
7012
7198
  }
7013
7199
  return;
7014
7200
  }
7201
+ // ── Markdown ──────────────────────────────────────────
7202
+ const markdownEl = getMarkdownEl(this.svg, target);
7203
+ if (markdownEl) {
7204
+ if (silent) {
7205
+ markdownEl.style.transition = "none";
7206
+ markdownEl.style.opacity = "";
7207
+ markdownEl.classList.remove("gg-hidden");
7208
+ markdownEl.style.opacity = "1";
7209
+ requestAnimationFrame(() => requestAnimationFrame(() => {
7210
+ markdownEl.style.transition = "";
7211
+ }));
7212
+ }
7213
+ else {
7214
+ markdownEl.style.opacity = "0";
7215
+ markdownEl.classList.remove("gg-hidden");
7216
+ requestAnimationFrame(() => requestAnimationFrame(() => {
7217
+ markdownEl.style.transition = "opacity 500ms ease";
7218
+ markdownEl.style.opacity = "1";
7219
+ }));
7220
+ }
7221
+ return;
7222
+ }
7015
7223
  // ── Node draw ──────────────────────────────────────
7016
7224
  const nodeEl = getNodeEl(this.svg, target);
7017
7225
  if (!nodeEl)
@@ -7025,14 +7233,16 @@ class AnimationController {
7025
7233
  if (!firstPath?.style.strokeDasharray)
7026
7234
  prepareForDraw(nodeEl);
7027
7235
  animateShapeDraw(nodeEl, 420, 55);
7236
+ const nodePathCount = nodeEl.querySelectorAll('path').length;
7237
+ clearDashOverridesAfter(nodeEl, nodePathCount * 55 + 420 + 120);
7028
7238
  }
7029
7239
  }
7030
7240
  // ── erase ─────────────────────────────────────────────────
7031
7241
  _doErase(target) {
7032
7242
  const el = resolveEl(this.svg, target); // handles edges too now
7033
7243
  if (el) {
7034
- el.style.transition = 'opacity 0.4s';
7035
- el.style.opacity = '0';
7244
+ el.style.transition = "opacity 0.4s";
7245
+ el.style.opacity = "0";
7036
7246
  }
7037
7247
  }
7038
7248
  // ── show / hide ───────────────────────────────────────────
@@ -7060,10 +7270,10 @@ class AnimationController {
7060
7270
  return;
7061
7271
  // edge — color stroke
7062
7272
  if (parseEdgeTarget(target)) {
7063
- el.querySelectorAll('path, line, polyline').forEach(p => {
7273
+ el.querySelectorAll("path, line, polyline").forEach((p) => {
7064
7274
  p.style.stroke = color;
7065
7275
  });
7066
- el.querySelectorAll('polygon').forEach(p => {
7276
+ el.querySelectorAll("polygon").forEach((p) => {
7067
7277
  p.style.fill = color;
7068
7278
  p.style.stroke = color;
7069
7279
  });
@@ -7071,22 +7281,24 @@ class AnimationController {
7071
7281
  }
7072
7282
  // everything else — color fill
7073
7283
  let hit = false;
7074
- el.querySelectorAll('path, rect, ellipse, polygon').forEach(c => {
7075
- const attrFill = c.getAttribute('fill');
7076
- if (attrFill === 'none')
7284
+ el.querySelectorAll("path, rect, ellipse, polygon").forEach((c) => {
7285
+ const attrFill = c.getAttribute("fill");
7286
+ if (attrFill === "none")
7077
7287
  return;
7078
- if (attrFill === null && c.tagName === 'path')
7288
+ if (attrFill === null && c.tagName === "path")
7079
7289
  return;
7080
7290
  c.style.fill = color;
7081
7291
  hit = true;
7082
7292
  });
7083
7293
  if (!hit) {
7084
- el.querySelectorAll('text').forEach(t => { t.style.fill = color; });
7294
+ el.querySelectorAll("text").forEach((t) => {
7295
+ t.style.fill = color;
7296
+ });
7085
7297
  }
7086
7298
  }
7087
7299
  }
7088
7300
  const ANIMATION_CSS = `
7089
- .ng, .gg, .tg, .ntg, .cg, .eg {
7301
+ .ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
7090
7302
  transform-box: fill-box;
7091
7303
  transform-origin: center;
7092
7304
  transition: filter 0.3s, opacity 0.35s;
@@ -7097,9 +7309,10 @@ const ANIMATION_CSS = `
7097
7309
  .tg.hl path, .tg.hl rect,
7098
7310
  .ntg.hl path, .ntg.hl polygon,
7099
7311
  .cg.hl path, .cg.hl rect,
7312
+ .mdg.hl text,
7100
7313
  .eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
7101
7314
 
7102
- .ng.hl, .tg.hl, .ntg.hl, .cg.hl, .eg.hl {
7315
+ .ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
7103
7316
  animation: ng-pulse 1.4s ease-in-out infinite;
7104
7317
  }
7105
7318
  @keyframes ng-pulse {
@@ -7108,7 +7321,8 @@ const ANIMATION_CSS = `
7108
7321
  }
7109
7322
 
7110
7323
  /* fade */
7111
- .ng.faded, .gg.faded, .tg.faded, .ntg.faded, .cg.faded, .eg.faded { opacity: 0.22; }
7324
+ .ng.faded, .gg.faded, .tg.faded, .ntg.faded,
7325
+ .cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
7112
7326
 
7113
7327
  .ng.hidden { opacity: 0; pointer-events: none; }
7114
7328
  .eg.draw-hidden { opacity: 0; }
@@ -7117,6 +7331,7 @@ const ANIMATION_CSS = `
7117
7331
  .tg.gg-hidden { opacity: 0; }
7118
7332
  .ntg.gg-hidden { opacity: 0; }
7119
7333
  .cg.gg-hidden { opacity: 0; }
7334
+ .mdg.gg-hidden { opacity: 0; }
7120
7335
  `;
7121
7336
 
7122
7337
  // ============================================================