sketchmark 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -259,10 +259,6 @@ function propsToStyle(p) {
259
259
  s.color = p.color;
260
260
  if (p.opacity)
261
261
  s.opacity = parseFloat(p.opacity);
262
- if (p.radius)
263
- s.radius = parseFloat(p.radius);
264
- if (p.shadow)
265
- s.shadow = p.shadow === "true";
266
262
  if (p["font-size"])
267
263
  s.fontSize = parseFloat(p["font-size"]);
268
264
  if (p["font-weight"])
@@ -279,8 +275,9 @@ function propsToStyle(p) {
279
275
  s.letterSpacing = parseFloat(p["letter-spacing"]);
280
276
  if (p.font)
281
277
  s.font = p.font;
282
- if (p["dash"]) {
283
- const parts = p["dash"]
278
+ const dashVal = p["dash"] || p["stroke-dash"];
279
+ if (dashVal) {
280
+ const parts = dashVal
284
281
  .split(",")
285
282
  .map(Number)
286
283
  .filter((n) => !isNaN(n));
@@ -851,7 +848,7 @@ function parse(src) {
851
848
  kind: "chart",
852
849
  id,
853
850
  chartType: chartType.replace("-chart", ""),
854
- title: props.title,
851
+ label: props.label ?? props.title,
855
852
  data: { headers, rows },
856
853
  width: props.width ? parseFloat(props.width) : undefined,
857
854
  height: props.height ? parseFloat(props.height) : undefined,
@@ -1343,7 +1340,7 @@ function buildSceneGraph(ast) {
1343
1340
  return {
1344
1341
  id: c.id,
1345
1342
  chartType: c.chartType,
1346
- title: c.title,
1343
+ label: c.label,
1347
1344
  data: c.data,
1348
1345
  style: { ...ast.styles[c.id], ...themeStyle, ...c.style },
1349
1346
  x: 0,
@@ -1487,13 +1484,22 @@ function sizeNode(n) {
1487
1484
  n.h = n.h || 50;
1488
1485
  break;
1489
1486
  case "text": {
1490
- // read fontSize from style if set, otherwise use default
1491
1487
  const fontSize = Number(n.style?.fontSize ?? 13);
1492
1488
  const charWidth = fontSize * 0.55;
1493
- const maxW = n.width ?? 400;
1494
- const approxLines = Math.ceil((n.label.length * charWidth) / (maxW - 16));
1495
- n.w = maxW;
1496
- n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + 8);
1489
+ const pad = Number(n.style?.padding ?? 8) * 2;
1490
+ if (n.width) {
1491
+ // User set width → word-wrap within it
1492
+ const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
1493
+ n.w = n.width;
1494
+ n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
1495
+ }
1496
+ else {
1497
+ // Auto-size to content
1498
+ const lines = n.label.split("\\n");
1499
+ const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
1500
+ n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
1501
+ n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
1502
+ }
1497
1503
  break;
1498
1504
  }
1499
1505
  default:
@@ -1710,17 +1716,16 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
1710
1716
  if (layout === "row") {
1711
1717
  const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1712
1718
  const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1713
- const maxH = Math.max(...hs);
1714
1719
  const { start, gaps } = distribute(ws, contentW, gap, justify);
1715
1720
  let x = contentX + start;
1716
1721
  for (let i = 0; i < kids.length; i++) {
1717
1722
  let y;
1718
1723
  switch (align) {
1719
1724
  case "center":
1720
- y = contentY + (maxH - hs[i]) / 2;
1725
+ y = contentY + (contentH - hs[i]) / 2;
1721
1726
  break;
1722
1727
  case "end":
1723
- y = contentY + maxH - hs[i];
1728
+ y = contentY + contentH - hs[i];
1724
1729
  break;
1725
1730
  default:
1726
1731
  y = contentY;
@@ -1741,17 +1746,16 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
1741
1746
  // column (default)
1742
1747
  const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1743
1748
  const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1744
- const maxW = Math.max(...ws);
1745
1749
  const { start, gaps } = distribute(hs, contentH, gap, justify);
1746
1750
  let y = contentY + start;
1747
1751
  for (let i = 0; i < kids.length; i++) {
1748
1752
  let x;
1749
1753
  switch (align) {
1750
1754
  case "center":
1751
- x = contentX + (maxW - ws[i]) / 2;
1755
+ x = contentX + (contentW - ws[i]) / 2;
1752
1756
  break;
1753
1757
  case "end":
1754
- x = contentX + maxW - ws[i];
1758
+ x = contentX + contentW - ws[i];
1755
1759
  break;
1756
1760
  default:
1757
1761
  x = contentX;
@@ -2071,7 +2075,7 @@ const CHART_COLORS = [
2071
2075
  '#7F77DD', '#D4537E', '#639922', '#E24B4A',
2072
2076
  ];
2073
2077
  function chartLayout(c) {
2074
- const titleH = c.title ? 24 : 8;
2078
+ const titleH = c.label ? 24 : 8;
2075
2079
  const padL = 44, padR = 12, padB = 28, padT = 6;
2076
2080
  const pw = c.w - padL - padR;
2077
2081
  const ph = c.h - titleH - padT - padB;
@@ -2175,13 +2179,13 @@ function mkG(id, cls) {
2175
2179
  g.setAttribute('class', cls);
2176
2180
  return g;
2177
2181
  }
2178
- function mkT(txt, x, y, sz = 10, wt = 400, col = '#4a2e10', anchor = 'middle') {
2182
+ function mkT(txt, x, y, sz = 10, wt = 400, col = '#4a2e10', anchor = 'middle', font = 'system-ui, sans-serif') {
2179
2183
  const t = se$1('text');
2180
2184
  t.setAttribute('x', String(x));
2181
2185
  t.setAttribute('y', String(y));
2182
2186
  t.setAttribute('text-anchor', anchor);
2183
2187
  t.setAttribute('dominant-baseline', 'middle');
2184
- t.setAttribute('font-family', 'system-ui, sans-serif');
2188
+ t.setAttribute('font-family', font);
2185
2189
  t.setAttribute('font-size', String(sz));
2186
2190
  t.setAttribute('font-weight', String(wt));
2187
2191
  t.setAttribute('fill', col);
@@ -2197,7 +2201,7 @@ function hashStr$4(s) {
2197
2201
  }
2198
2202
  const BASE = { roughness: 1.2, bowing: 0.7 };
2199
2203
  // ── Axes ───────────────────────────────────────────────────
2200
- function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol) {
2204
+ function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol, font = 'system-ui, sans-serif') {
2201
2205
  // Y axis
2202
2206
  g.appendChild(rc.line(px, py, px, py + ph, {
2203
2207
  roughness: 0.4, seed: hashStr$4(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
@@ -2216,7 +2220,7 @@ function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol) {
2216
2220
  g.appendChild(rc.line(px - 3, ty, px, ty, {
2217
2221
  roughness: 0.2, seed: hashStr$4(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
2218
2222
  }));
2219
- g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end'));
2223
+ g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end', font));
2220
2224
  }
2221
2225
  }
2222
2226
  function fmtNum$1(v) {
@@ -2225,7 +2229,7 @@ function fmtNum$1(v) {
2225
2229
  return String(v);
2226
2230
  }
2227
2231
  // ── Legend row ─────────────────────────────────────────────
2228
- function legend(g, labels, colors, x, y, labelCol) {
2232
+ function legend(g, labels, colors, x, y, labelCol, font = 'system-ui, sans-serif') {
2229
2233
  labels.forEach((lbl, i) => {
2230
2234
  const dot = se$1('rect');
2231
2235
  dot.setAttribute('x', String(x));
@@ -2235,7 +2239,7 @@ function legend(g, labels, colors, x, y, labelCol) {
2235
2239
  dot.setAttribute('fill', colors[i % colors.length]);
2236
2240
  dot.setAttribute('rx', '1');
2237
2241
  g.appendChild(dot);
2238
- g.appendChild(mkT(lbl, x + 12, y + i * 14 + 4, 9, 400, labelCol, 'start'));
2242
+ g.appendChild(mkT(lbl, x + 12, y + i * 14 + 4, 9, 400, labelCol, 'start', font));
2239
2243
  });
2240
2244
  }
2241
2245
  // ── Public entry ───────────────────────────────────────────
@@ -2246,6 +2250,11 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2246
2250
  const bgFill = String(s.fill ?? palette.nodeFill);
2247
2251
  const bgStroke = String(s.stroke ?? (isDark ? '#5a4a30' : '#c8b898'));
2248
2252
  const lc = String(s.color ?? palette.titleText);
2253
+ const cFont = String(s.font ? `${s.font}, system-ui, sans-serif` : 'system-ui, sans-serif');
2254
+ const cFontSize = Number(s.fontSize ?? 12);
2255
+ const cFontWeight = s.fontWeight ?? 600;
2256
+ if (s.opacity != null)
2257
+ cg.setAttribute('opacity', String(s.opacity));
2249
2258
  // Background box
2250
2259
  cg.appendChild(rc.rectangle(c.x, c.y, c.w, c.h, {
2251
2260
  ...BASE, seed: hashStr$4(c.id),
@@ -2254,17 +2263,17 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2254
2263
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
2255
2264
  }));
2256
2265
  // Title
2257
- if (c.title) {
2258
- cg.appendChild(mkT(c.title, c.x + c.w / 2, c.y + 14, 12, 600, lc));
2266
+ if (c.label) {
2267
+ cg.appendChild(mkT(c.label, c.x + c.w / 2, c.y + 14, cFontSize, cFontWeight, lc, 'middle', cFont));
2259
2268
  }
2260
2269
  const { px, py, pw, ph, cx, cy } = chartLayout(c);
2261
2270
  // ── Pie / Donut ──────────────────────────────────────────
2262
2271
  if (c.chartType === 'pie' || c.chartType === 'donut') {
2263
2272
  const { segments, total } = parsePie(c.data);
2264
- const r = Math.min(c.w * 0.38, (c.h - (c.title ? 24 : 8)) * 0.44);
2273
+ const r = Math.min(c.w * 0.38, (c.h - (c.label ? 24 : 8)) * 0.44);
2265
2274
  const ir = c.chartType === 'donut' ? r * 0.48 : 0;
2266
2275
  const legendX = c.x + 8;
2267
- const legendY = c.y + (c.title ? 28 : 12);
2276
+ const legendY = c.y + (c.label ? 28 : 12);
2268
2277
  let angle = -Math.PI / 2;
2269
2278
  for (const seg of segments) {
2270
2279
  const sweep = (seg.value / total) * Math.PI * 2;
@@ -2281,7 +2290,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2281
2290
  angle += sweep;
2282
2291
  }
2283
2292
  // Mini legend on left
2284
- legend(cg, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc);
2293
+ legend(cg, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
2285
2294
  return cg;
2286
2295
  }
2287
2296
  // ── Scatter ───────────────────────────────────────────────
@@ -2302,7 +2311,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2302
2311
  strokeWidth: 1.2,
2303
2312
  }));
2304
2313
  });
2305
- legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc);
2314
+ legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.label ? 28 : 12), lc, cFont);
2306
2315
  return cg;
2307
2316
  }
2308
2317
  // ── Bar / Line / Area ─────────────────────────────────────
@@ -2311,10 +2320,10 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2311
2320
  const toY = makeValueToY(allY, py, ph);
2312
2321
  const baseline = toY(0);
2313
2322
  const n = labels.length;
2314
- drawAxes$1(rc, cg, c, px, py, pw, ph, allY, lc);
2323
+ drawAxes$1(rc, cg, c, px, py, pw, ph, allY, lc, cFont);
2315
2324
  // X labels
2316
2325
  labels.forEach((lbl, i) => {
2317
- cg.appendChild(mkT(lbl, px + (i + 0.5) * (pw / n), py + ph + 14, 9, 400, lc));
2326
+ cg.appendChild(mkT(lbl, px + (i + 0.5) * (pw / n), py + ph + 14, 9, 400, lc, 'middle', cFont));
2318
2327
  });
2319
2328
  if (c.chartType === 'bar') {
2320
2329
  const groupW = pw / n;
@@ -2385,7 +2394,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
2385
2394
  }
2386
2395
  // Multi-series legend
2387
2396
  if (series.length > 1) {
2388
- legend(cg, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc);
2397
+ legend(cg, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc, cFont);
2389
2398
  }
2390
2399
  return cg;
2391
2400
  }
@@ -4851,6 +4860,14 @@ function hashStr$3(s) {
4851
4860
  return h;
4852
4861
  }
4853
4862
  const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
4863
+ /** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
4864
+ function darkenHex$1(hex, amount = 0.12) {
4865
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
4866
+ if (!m)
4867
+ return hex;
4868
+ const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
4869
+ 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")}`;
4870
+ }
4854
4871
  // ── Small helper: load + resolve font from style or fall back ─────────────
4855
4872
  function resolveStyleFont$1(style, fallback) {
4856
4873
  const raw = String(style["font"] ?? "");
@@ -5021,6 +5038,7 @@ function renderShape$1(rc, n, palette) {
5021
5038
  fillStyle: "solid",
5022
5039
  stroke,
5023
5040
  strokeWidth: Number(s.strokeWidth ?? 1.9),
5041
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5024
5042
  };
5025
5043
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
5026
5044
  const hw = n.w / 2 - 2;
@@ -5203,6 +5221,8 @@ function renderToSVG(sg, container, options = {}) {
5203
5221
  continue;
5204
5222
  const gs = g.style ?? {};
5205
5223
  const gg = mkGroup(`group-${g.id}`, "gg");
5224
+ if (gs.opacity != null)
5225
+ gg.setAttribute("opacity", String(gs.opacity));
5206
5226
  gg.appendChild(rc.rectangle(g.x, g.y, g.w, g.h, {
5207
5227
  ...BASE_ROUGH,
5208
5228
  roughness: 1.7,
@@ -5215,14 +5235,26 @@ function renderToSVG(sg, container, options = {}) {
5215
5235
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
5216
5236
  }));
5217
5237
  // ── Group label typography ──────────────────────────
5218
- // supports: font, font-size, letter-spacing
5219
- // always left-anchored (single line)
5220
5238
  const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
5221
5239
  const gFontSize = Number(gs.fontSize ?? 12);
5240
+ const gFontWeight = gs.fontWeight ?? 500;
5222
5241
  const gFont = resolveStyleFont$1(gs, diagramFont);
5223
5242
  const gLetterSpacing = gs.letterSpacing;
5243
+ const gPad = Number(gs.padding ?? 14);
5244
+ const gTextAlign = String(gs.textAlign ?? "left");
5245
+ const gAnchorMap = {
5246
+ left: "start",
5247
+ center: "middle",
5248
+ right: "end",
5249
+ };
5250
+ const gAnchor = gAnchorMap[gTextAlign] ?? "start";
5251
+ const gTextX = gTextAlign === "right"
5252
+ ? g.x + g.w - gPad
5253
+ : gTextAlign === "center"
5254
+ ? g.x + g.w / 2
5255
+ : g.x + gPad;
5224
5256
  if (g.label) {
5225
- gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
5257
+ gg.appendChild(mkText(g.label, gTextX, g.y + gPad, gFontSize, gFontWeight, gLabelColor, gAnchor, gFont, gLetterSpacing));
5226
5258
  }
5227
5259
  GL.appendChild(gg);
5228
5260
  }
@@ -5243,6 +5275,8 @@ function renderToSVG(sg, container, options = {}) {
5243
5275
  const [x1, y1] = getConnPoint$1(src, dstCX, dstCY);
5244
5276
  const [x2, y2] = getConnPoint$1(dst, srcCX, srcCY);
5245
5277
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
5278
+ if (e.style?.opacity != null)
5279
+ eg.setAttribute("opacity", String(e.style.opacity));
5246
5280
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
5247
5281
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
5248
5282
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
@@ -5283,7 +5317,9 @@ function renderToSVG(sg, container, options = {}) {
5283
5317
  const eFontSize = Number(e.style?.fontSize ?? 11);
5284
5318
  const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
5285
5319
  const eLetterSpacing = e.style?.letterSpacing;
5286
- eg.appendChild(mkText(e.label, mx, my, eFontSize, 400, palette.edgeLabelText, "middle", eFont, eLetterSpacing));
5320
+ const eFontWeight = e.style?.fontWeight ?? 400;
5321
+ const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
5322
+ eg.appendChild(mkText(e.label, mx, my, eFontSize, eFontWeight, eLabelColor, "middle", eFont, eLetterSpacing));
5287
5323
  }
5288
5324
  EL.appendChild(eg);
5289
5325
  }
@@ -5292,6 +5328,8 @@ function renderToSVG(sg, container, options = {}) {
5292
5328
  const NL = mkGroup("node-layer");
5293
5329
  for (const n of sg.nodes) {
5294
5330
  const ng = mkGroup(`node-${n.id}`, "ng");
5331
+ if (n.style?.opacity != null)
5332
+ ng.setAttribute("opacity", String(n.style.opacity));
5295
5333
  renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
5296
5334
  // ── Node / text typography ─────────────────────────
5297
5335
  // supports: font, font-size, letter-spacing, text-align, line-height
@@ -5310,18 +5348,19 @@ function renderToSVG(sg, container, options = {}) {
5310
5348
  // line-height is a multiplier (e.g. 1.4 = 140% of font-size)
5311
5349
  const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
5312
5350
  const letterSpacing = n.style?.letterSpacing;
5351
+ const pad = Number(n.style?.padding ?? 8);
5313
5352
  // x shifts for left / right alignment
5314
5353
  const textX = textAlign === "left"
5315
- ? n.x + 8
5354
+ ? n.x + pad
5316
5355
  : textAlign === "right"
5317
- ? n.x + n.w - 8
5356
+ ? n.x + n.w - pad
5318
5357
  : n.x + n.w / 2;
5319
5358
  const lines = n.shape === 'text' && !n.label.includes('\n')
5320
- ? wrapText$1(n.label, n.w - 16, fontSize)
5359
+ ? wrapText$1(n.label, n.w - pad * 2, fontSize)
5321
5360
  : n.label.split('\n');
5322
5361
  const verticalAlign = String(n.style?.verticalAlign ?? "middle");
5323
- const nodeBodyTop = n.y + 6;
5324
- const nodeBodyBottom = n.y + n.h - 6;
5362
+ const nodeBodyTop = n.y + pad;
5363
+ const nodeBodyBottom = n.y + n.h - pad;
5325
5364
  const nodeBodyMid = n.y + n.h / 2;
5326
5365
  const blockH = (lines.length - 1) * lineHeight;
5327
5366
  const textCY = verticalAlign === "top"
@@ -5353,15 +5392,19 @@ function renderToSVG(sg, container, options = {}) {
5353
5392
  const fill = String(gs.fill ?? palette.tableFill);
5354
5393
  const strk = String(gs.stroke ?? palette.tableStroke);
5355
5394
  const textCol = String(gs.color ?? palette.tableText);
5356
- const hdrFill = palette.tableHeaderFill;
5395
+ const hdrFill = gs.fill ? darkenHex$1(fill, 0.08) : palette.tableHeaderFill;
5357
5396
  const hdrText = String(gs.color ?? palette.tableHeaderText);
5358
5397
  const divCol = palette.tableDivider;
5359
5398
  const pad = t.labelH;
5399
+ const tStrokeWidth = Number(gs.strokeWidth ?? 1.5);
5400
+ const tFontWeight = gs.fontWeight ?? 500;
5360
5401
  // ── Table-level font (applies to label + all cells) ─
5361
5402
  // supports: font, font-size, letter-spacing
5362
5403
  const tFontSize = Number(gs.fontSize ?? 12);
5363
5404
  const tFont = resolveStyleFont$1(gs, diagramFont);
5364
5405
  const tLetterSpacing = gs.letterSpacing;
5406
+ if (gs.opacity != null)
5407
+ tg.setAttribute("opacity", String(gs.opacity));
5365
5408
  // outer border
5366
5409
  tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
5367
5410
  ...BASE_ROUGH,
@@ -5369,7 +5412,8 @@ function renderToSVG(sg, container, options = {}) {
5369
5412
  fill,
5370
5413
  fillStyle: "solid",
5371
5414
  stroke: strk,
5372
- strokeWidth: 1.5,
5415
+ strokeWidth: tStrokeWidth,
5416
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5373
5417
  }));
5374
5418
  // label strip separator
5375
5419
  tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
@@ -5378,8 +5422,8 @@ function renderToSVG(sg, container, options = {}) {
5378
5422
  stroke: strk,
5379
5423
  strokeWidth: 1,
5380
5424
  }));
5381
- // ── Table label: font, font-size, letter-spacing (always left) ──
5382
- tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, "start", tFont, tLetterSpacing));
5425
+ // ── Table label: font, font-size, font-weight, letter-spacing (always left) ──
5426
+ tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, "start", tFont, tLetterSpacing));
5383
5427
  // rows
5384
5428
  let rowY = t.y + pad;
5385
5429
  for (const row of t.rows) {
@@ -5408,7 +5452,7 @@ function renderToSVG(sg, container, options = {}) {
5408
5452
  right: "end",
5409
5453
  };
5410
5454
  const cellAnchor = cellAnchorMap[cellAlignProp] ?? "middle";
5411
- const cellFw = row.kind === "header" ? 600 : 400;
5455
+ const cellFw = row.kind === "header" ? 600 : (gs.fontWeight ?? 400);
5412
5456
  const cellColor = row.kind === "header" ? hdrText : textCol;
5413
5457
  let cx = t.x;
5414
5458
  row.cells.forEach((cell, i) => {
@@ -5447,27 +5491,31 @@ function renderToSVG(sg, container, options = {}) {
5447
5491
  const gs = n.style ?? {};
5448
5492
  const fill = String(gs.fill ?? palette.noteFill);
5449
5493
  const strk = String(gs.stroke ?? palette.noteStroke);
5494
+ const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
5450
5495
  const fold = 14;
5451
5496
  const { x, y, w, h } = n;
5497
+ if (gs.opacity != null)
5498
+ ng.setAttribute("opacity", String(gs.opacity));
5452
5499
  // ── Note typography ─────────────────────────────────
5453
- // supports: font, font-size, letter-spacing, text-align, line-height
5454
5500
  const nFontSize = Number(gs.fontSize ?? 12);
5501
+ const nFontWeight = gs.fontWeight ?? 400;
5455
5502
  const nFont = resolveStyleFont$1(gs, diagramFont);
5456
5503
  const nLetterSpacing = gs.letterSpacing;
5457
5504
  const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
5458
5505
  const nTextAlign = String(gs.textAlign ?? "left");
5506
+ const nPad = Number(gs.padding ?? 12);
5459
5507
  const nAnchorMap = {
5460
5508
  left: "start",
5461
5509
  center: "middle",
5462
5510
  right: "end",
5463
5511
  };
5464
5512
  const nAnchor = nAnchorMap[nTextAlign] ?? "start";
5465
- // x position for the text block (pad from left, with alignment)
5466
5513
  const nTextX = nTextAlign === "right"
5467
- ? x + w - fold - 6
5514
+ ? x + w - fold - nPad
5468
5515
  : nTextAlign === "center"
5469
5516
  ? x + (w - fold) / 2
5470
- : x + 12;
5517
+ : x + nPad;
5518
+ const nFoldPad = fold + nPad; // text starts below fold + user padding
5471
5519
  ng.appendChild(rc.polygon([
5472
5520
  [x, y],
5473
5521
  [x + w - fold, y],
@@ -5480,7 +5528,8 @@ function renderToSVG(sg, container, options = {}) {
5480
5528
  fill,
5481
5529
  fillStyle: "solid",
5482
5530
  stroke: strk,
5483
- strokeWidth: 1.2,
5531
+ strokeWidth: nStrokeWidth,
5532
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5484
5533
  }));
5485
5534
  ng.appendChild(rc.polygon([
5486
5535
  [x + w - fold, y],
@@ -5492,11 +5541,11 @@ function renderToSVG(sg, container, options = {}) {
5492
5541
  fill: palette.noteFold,
5493
5542
  fillStyle: "solid",
5494
5543
  stroke: strk,
5495
- strokeWidth: 0.8,
5544
+ strokeWidth: Math.min(nStrokeWidth, 0.8),
5496
5545
  }));
5497
5546
  const nVerticalAlign = String(gs.verticalAlign ?? "top");
5498
- const bodyTop = y + fold + 8; // below the fold triangle
5499
- const bodyBottom = y + h - 8; // above bottom edge
5547
+ const bodyTop = y + nFoldPad;
5548
+ const bodyBottom = y + h - nPad;
5500
5549
  const bodyMid = (bodyTop + bodyBottom) / 2;
5501
5550
  const blockH = (n.lines.length - 1) * nLineHeight;
5502
5551
  const blockCY = nVerticalAlign === "bottom"
@@ -5504,13 +5553,11 @@ function renderToSVG(sg, container, options = {}) {
5504
5553
  : nVerticalAlign === "middle"
5505
5554
  ? bodyMid
5506
5555
  : bodyTop + blockH / 2;
5507
- // multiline: use mkMultilineText so line-height is respected
5508
5556
  if (n.lines.length > 1) {
5509
- // vertical centre of the text block inside the note
5510
- ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
5557
+ ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
5511
5558
  }
5512
5559
  else {
5513
- ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
5560
+ ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
5514
5561
  }
5515
5562
  NoteL.appendChild(ng);
5516
5563
  }
@@ -5519,13 +5566,27 @@ function renderToSVG(sg, container, options = {}) {
5519
5566
  const MDL = mkGroup('markdown-layer');
5520
5567
  for (const m of sg.markdowns) {
5521
5568
  const mg = mkGroup(`markdown-${m.id}`, 'mdg');
5522
- const mFont = resolveStyleFont$1(m.style, diagramFont);
5523
- const baseColor = String(m.style?.color ?? palette.nodeText);
5524
- const textAlign = String(m.style?.textAlign ?? 'left');
5569
+ const gs = m.style ?? {};
5570
+ const mFont = resolveStyleFont$1(gs, diagramFont);
5571
+ const baseColor = String(gs.color ?? palette.nodeText);
5572
+ const textAlign = String(gs.textAlign ?? 'left');
5525
5573
  const anchor = textAlign === 'right' ? 'end'
5526
5574
  : textAlign === 'center' ? 'middle'
5527
5575
  : 'start';
5528
- const PAD = Number(m.style?.padding ?? 16);
5576
+ const PAD = Number(gs.padding ?? 16);
5577
+ const mLetterSpacing = gs.letterSpacing;
5578
+ if (gs.opacity != null)
5579
+ mg.setAttribute('opacity', String(gs.opacity));
5580
+ // Background + border
5581
+ if (gs.fill || gs.stroke) {
5582
+ mg.appendChild(rc.rectangle(m.x, m.y, m.w, m.h, {
5583
+ ...BASE_ROUGH, seed: hashStr$3(m.id),
5584
+ fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
5585
+ stroke: String(gs.stroke ?? 'none'),
5586
+ strokeWidth: Number(gs.strokeWidth ?? 1.2),
5587
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5588
+ }));
5589
+ }
5529
5590
  const textX = textAlign === 'right' ? m.x + m.w - PAD
5530
5591
  : textAlign === 'center' ? m.x + m.w / 2
5531
5592
  : m.x + PAD;
@@ -5548,6 +5609,8 @@ function renderToSVG(sg, container, options = {}) {
5548
5609
  t.setAttribute('fill', baseColor);
5549
5610
  t.setAttribute('pointer-events', 'none');
5550
5611
  t.setAttribute('user-select', 'none');
5612
+ if (mLetterSpacing != null)
5613
+ t.setAttribute('letter-spacing', String(mLetterSpacing));
5551
5614
  for (const run of line.runs) {
5552
5615
  const span = se('tspan');
5553
5616
  span.textContent = run.text;
@@ -5637,7 +5700,7 @@ function drawPieArc(rc, ctx, cx, cy, r, ir, startAngle, endAngle, color, seed) {
5637
5700
  });
5638
5701
  }
5639
5702
  // ── Axes ───────────────────────────────────────────────────
5640
- function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
5703
+ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R, font = 'system-ui, sans-serif') {
5641
5704
  const toY = makeValueToY(allY, py, ph);
5642
5705
  const baseline = toY(0);
5643
5706
  // Y axis
@@ -5651,7 +5714,7 @@ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
5651
5714
  continue;
5652
5715
  rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
5653
5716
  ctx.save();
5654
- ctx.font = '400 9px system-ui, sans-serif';
5717
+ ctx.font = `400 9px ${font}`;
5655
5718
  ctx.fillStyle = labelCol;
5656
5719
  ctx.textAlign = 'right';
5657
5720
  ctx.textBaseline = 'middle';
@@ -5660,9 +5723,9 @@ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
5660
5723
  }
5661
5724
  }
5662
5725
  // ── Legend ─────────────────────────────────────────────────
5663
- function drawLegend(ctx, labels, colors, x, y, labelCol) {
5726
+ function drawLegend(ctx, labels, colors, x, y, labelCol, font = 'system-ui, sans-serif') {
5664
5727
  ctx.save();
5665
- ctx.font = '400 9px system-ui, sans-serif';
5728
+ ctx.font = `400 9px ${font}`;
5666
5729
  ctx.textAlign = 'left';
5667
5730
  ctx.textBaseline = 'middle';
5668
5731
  labels.forEach((lbl, i) => {
@@ -5680,6 +5743,11 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5680
5743
  const bgFill = String(s.fill ?? pal.nodeFill);
5681
5744
  const bgStroke = String(s.stroke ?? (pal.nodeStroke === 'none' ? '#c8b898' : pal.nodeStroke));
5682
5745
  const lc = String(s.color ?? pal.labelText);
5746
+ const cFont = String(s.font ? `${s.font}, system-ui, sans-serif` : 'system-ui, sans-serif');
5747
+ const cFontSize = Number(s.fontSize ?? 12);
5748
+ const cFontWeight = s.fontWeight ?? 600;
5749
+ if (s.opacity != null)
5750
+ ctx.globalAlpha = Number(s.opacity);
5683
5751
  // Background
5684
5752
  rc.rectangle(c.x, c.y, c.w, c.h, {
5685
5753
  ...R, seed: hashStr$2(c.id),
@@ -5690,30 +5758,31 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5690
5758
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5691
5759
  });
5692
5760
  // Title
5693
- if (c.title) {
5761
+ if (c.label) {
5694
5762
  ctx.save();
5695
- ctx.font = '600 12px system-ui, sans-serif';
5763
+ ctx.font = `${cFontWeight} ${cFontSize}px ${cFont}`;
5696
5764
  ctx.fillStyle = lc;
5697
5765
  ctx.textAlign = 'center';
5698
5766
  ctx.textBaseline = 'middle';
5699
- ctx.fillText(c.title, c.x + c.w / 2, c.y + 14);
5767
+ ctx.fillText(c.label, c.x + c.w / 2, c.y + 14);
5700
5768
  ctx.restore();
5701
5769
  }
5702
5770
  const { px, py, pw, ph, cx, cy } = chartLayout(c);
5703
5771
  // ── Pie / Donut ──────────────────────────────────────────
5704
5772
  if (c.chartType === 'pie' || c.chartType === 'donut') {
5705
5773
  const { segments, total } = parsePie(c.data);
5706
- const r = Math.min(c.w * 0.38, (c.h - (c.title ? 24 : 8)) * 0.44);
5774
+ const r = Math.min(c.w * 0.38, (c.h - (c.label ? 24 : 8)) * 0.44);
5707
5775
  const ir = c.chartType === 'donut' ? r * 0.48 : 0;
5708
5776
  const legendX = c.x + 8;
5709
- const legendY = c.y + (c.title ? 28 : 12);
5777
+ const legendY = c.y + (c.label ? 28 : 12);
5710
5778
  let angle = -Math.PI / 2;
5711
5779
  segments.forEach((seg, i) => {
5712
5780
  const sweep = (seg.value / total) * Math.PI * 2;
5713
5781
  drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$2(c.id + seg.label + i));
5714
5782
  angle += sweep;
5715
5783
  });
5716
- drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc);
5784
+ drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
5785
+ ctx.globalAlpha = 1;
5717
5786
  return;
5718
5787
  }
5719
5788
  // ── Scatter ───────────────────────────────────────────────
@@ -5733,7 +5802,8 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5733
5802
  strokeWidth: 1.2,
5734
5803
  });
5735
5804
  });
5736
- drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc);
5805
+ drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.label ? 28 : 12), lc, cFont);
5806
+ ctx.globalAlpha = 1;
5737
5807
  return;
5738
5808
  }
5739
5809
  // ── Bar / Line / Area ─────────────────────────────────────
@@ -5742,10 +5812,10 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5742
5812
  const toY = makeValueToY(allY, py, ph);
5743
5813
  const baseline = toY(0);
5744
5814
  const n = labels.length;
5745
- drawAxes(rc, ctx, c, px, py, pw, ph, allY, lc, R);
5815
+ drawAxes(rc, ctx, c, px, py, pw, ph, allY, lc, R, cFont);
5746
5816
  // X labels
5747
5817
  ctx.save();
5748
- ctx.font = '400 9px system-ui, sans-serif';
5818
+ ctx.font = `400 9px ${cFont}`;
5749
5819
  ctx.fillStyle = lc;
5750
5820
  ctx.textAlign = 'center';
5751
5821
  ctx.textBaseline = 'top';
@@ -5822,8 +5892,9 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
5822
5892
  }
5823
5893
  // Multi-series legend
5824
5894
  if (series.length > 1) {
5825
- drawLegend(ctx, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc);
5895
+ drawLegend(ctx, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc, cFont);
5826
5896
  }
5897
+ ctx.globalAlpha = 1;
5827
5898
  }
5828
5899
 
5829
5900
  // ============================================================
@@ -5836,6 +5907,14 @@ function hashStr$1(s) {
5836
5907
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
5837
5908
  return h;
5838
5909
  }
5910
+ /** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
5911
+ function darkenHex(hex, amount = 0.12) {
5912
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
5913
+ if (!m)
5914
+ return hex;
5915
+ const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
5916
+ 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")}`;
5917
+ }
5839
5918
  // ── Small helper: load + resolve font from a style map ────────────────────
5840
5919
  function resolveStyleFont(style, fallback) {
5841
5920
  const raw = String(style['font'] ?? '');
@@ -5953,6 +6032,7 @@ function renderShape(rc, ctx, n, palette, R) {
5953
6032
  ...R, seed: hashStr$1(n.id),
5954
6033
  fill, fillStyle: 'solid',
5955
6034
  stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
6035
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5956
6036
  };
5957
6037
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
5958
6038
  const hw = n.w / 2 - 2;
@@ -6088,6 +6168,8 @@ function renderToCanvas(sg, canvas, options = {}) {
6088
6168
  if (!g.w)
6089
6169
  continue;
6090
6170
  const gs = g.style ?? {};
6171
+ if (gs.opacity != null)
6172
+ ctx.globalAlpha = Number(gs.opacity);
6091
6173
  rc.rectangle(g.x, g.y, g.w, g.h, {
6092
6174
  ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
6093
6175
  fill: String(gs.fill ?? palette.groupFill),
@@ -6096,16 +6178,21 @@ function renderToCanvas(sg, canvas, options = {}) {
6096
6178
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
6097
6179
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
6098
6180
  });
6099
- // ── Group label ──────────────────────────────────────
6100
- // Only render when label has content — empty label = no reserved space
6101
- // supports: font, font-size, letter-spacing (always left-anchored)
6102
6181
  if (g.label) {
6103
6182
  const gFontSize = Number(gs.fontSize ?? 12);
6183
+ const gFontWeight = gs.fontWeight ?? 500;
6104
6184
  const gFont = resolveStyleFont(gs, diagramFont);
6105
6185
  const gLetterSpacing = gs.letterSpacing;
6106
6186
  const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
6107
- drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
6187
+ const gPad = Number(gs.padding ?? 14);
6188
+ const gTextAlign = String(gs.textAlign ?? 'left');
6189
+ const gTextX = gTextAlign === 'right' ? g.x + g.w - gPad
6190
+ : gTextAlign === 'center' ? g.x + g.w / 2
6191
+ : g.x + gPad;
6192
+ drawText(ctx, g.label, gTextX, g.y + gPad + 2, gFontSize, gFontWeight, gLabelColor, gTextAlign, gFont, gLetterSpacing);
6108
6193
  }
6194
+ if (gs.opacity != null)
6195
+ ctx.globalAlpha = 1;
6109
6196
  }
6110
6197
  // ── Edges ─────────────────────────────────────────────────
6111
6198
  for (const e of sg.edges) {
@@ -6117,6 +6204,8 @@ function renderToCanvas(sg, canvas, options = {}) {
6117
6204
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
6118
6205
  const [x1, y1] = getConnPoint(src, dstCX, dstCY);
6119
6206
  const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
6207
+ if (e.style?.opacity != null)
6208
+ ctx.globalAlpha = Number(e.style.opacity);
6120
6209
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
6121
6210
  const { arrowAt, dashed } = connMeta(e.connector);
6122
6211
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
@@ -6145,17 +6234,22 @@ function renderToCanvas(sg, canvas, options = {}) {
6145
6234
  const eFontSize = Number(e.style?.fontSize ?? 11);
6146
6235
  const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
6147
6236
  const eLetterSpacing = e.style?.letterSpacing;
6237
+ const eFontWeight = e.style?.fontWeight ?? 400;
6238
+ const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
6148
6239
  ctx.save();
6149
- ctx.font = `400 ${eFontSize}px ${eFont}`;
6240
+ ctx.font = `${eFontWeight} ${eFontSize}px ${eFont}`;
6150
6241
  const tw = ctx.measureText(e.label).width + 12;
6151
6242
  ctx.restore();
6152
6243
  ctx.fillStyle = palette.edgeLabelBg;
6153
6244
  ctx.fillRect(mx - tw / 2, my - 8, tw, 15);
6154
- drawText(ctx, e.label, mx, my + 3, eFontSize, 400, palette.edgeLabelText, 'center', eFont, eLetterSpacing);
6245
+ drawText(ctx, e.label, mx, my + 3, eFontSize, eFontWeight, eLabelColor, 'center', eFont, eLetterSpacing);
6155
6246
  }
6247
+ ctx.globalAlpha = 1;
6156
6248
  }
6157
6249
  // ── Nodes ─────────────────────────────────────────────────
6158
6250
  for (const n of sg.nodes) {
6251
+ if (n.style?.opacity != null)
6252
+ ctx.globalAlpha = Number(n.style.opacity);
6159
6253
  renderShape(rc, ctx, n, palette, R);
6160
6254
  // ── Node / text typography ─────────────────────────
6161
6255
  // supports: font, font-size, letter-spacing, text-align,
@@ -6169,18 +6263,19 @@ function renderToCanvas(sg, canvas, options = {}) {
6169
6263
  const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
6170
6264
  const letterSpacing = n.style?.letterSpacing;
6171
6265
  const vertAlign = String(n.style?.verticalAlign ?? 'middle');
6266
+ const pad = Number(n.style?.padding ?? 8);
6172
6267
  // x shifts for left/right alignment
6173
- const textX = textAlign === 'left' ? n.x + 8
6174
- : textAlign === 'right' ? n.x + n.w - 8
6268
+ const textX = textAlign === 'left' ? n.x + pad
6269
+ : textAlign === 'right' ? n.x + n.w - pad
6175
6270
  : n.x + n.w / 2;
6176
6271
  // word-wrap for text shape; explicit \n for all others
6177
6272
  const rawLines = n.label.split('\n');
6178
6273
  const lines = n.shape === 'text' && rawLines.length === 1
6179
- ? wrapText(n.label, n.w - 16, fontSize)
6274
+ ? wrapText(n.label, n.w - pad * 2, fontSize)
6180
6275
  : rawLines;
6181
6276
  // vertical-align: compute textCY from top/middle/bottom
6182
- const nodeBodyTop = n.y + 6;
6183
- const nodeBodyBottom = n.y + n.h - 6;
6277
+ const nodeBodyTop = n.y + pad;
6278
+ const nodeBodyBottom = n.y + n.h - pad;
6184
6279
  const blockH = (lines.length - 1) * lineHeight;
6185
6280
  const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
6186
6281
  : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
@@ -6191,6 +6286,8 @@ function renderToCanvas(sg, canvas, options = {}) {
6191
6286
  else {
6192
6287
  drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
6193
6288
  }
6289
+ if (n.style?.opacity != null)
6290
+ ctx.globalAlpha = 1;
6194
6291
  }
6195
6292
  // ── Tables ────────────────────────────────────────────────
6196
6293
  for (const t of sg.tables) {
@@ -6200,25 +6297,28 @@ function renderToCanvas(sg, canvas, options = {}) {
6200
6297
  const textCol = String(gs.color ?? palette.tableText);
6201
6298
  const pad = t.labelH;
6202
6299
  // ── Table-level font ────────────────────────────────
6203
- // supports: font, font-size, letter-spacing
6204
- // cells also support text-align
6205
6300
  const tFontSize = Number(gs.fontSize ?? 12);
6206
6301
  const tFont = resolveStyleFont(gs, diagramFont);
6207
6302
  const tLetterSpacing = gs.letterSpacing;
6303
+ const tStrokeWidth = Number(gs.strokeWidth ?? 1.5);
6304
+ const tFontWeight = gs.fontWeight ?? 500;
6305
+ if (gs.opacity != null)
6306
+ ctx.globalAlpha = Number(gs.opacity);
6208
6307
  rc.rectangle(t.x, t.y, t.w, t.h, {
6209
6308
  ...R, seed: hashStr$1(t.id),
6210
- fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.5,
6309
+ fill, fillStyle: 'solid', stroke: strk, strokeWidth: tStrokeWidth,
6310
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6211
6311
  });
6212
6312
  rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
6213
6313
  roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
6214
6314
  });
6215
6315
  // ── Table label: always left-anchored ───────────────
6216
- drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
6316
+ drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, 'left', tFont, tLetterSpacing);
6217
6317
  let rowY = t.y + pad;
6218
6318
  for (const row of t.rows) {
6219
6319
  const rh = row.kind === 'header' ? t.headerH : t.rowH;
6220
6320
  if (row.kind === 'header') {
6221
- ctx.fillStyle = palette.tableHeaderFill;
6321
+ ctx.fillStyle = gs.fill ? darkenHex(fill, 0.08) : palette.tableHeaderFill;
6222
6322
  ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
6223
6323
  }
6224
6324
  rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
@@ -6231,7 +6331,7 @@ function renderToCanvas(sg, canvas, options = {}) {
6231
6331
  const cellAlignProp = (row.kind === 'header'
6232
6332
  ? 'center'
6233
6333
  : String(gs.textAlign ?? 'center'));
6234
- const cellFw = row.kind === 'header' ? 600 : 400;
6334
+ const cellFw = row.kind === 'header' ? 600 : (gs.fontWeight ?? 400);
6235
6335
  const cellColor = row.kind === 'header'
6236
6336
  ? String(gs.color ?? palette.tableHeaderText)
6237
6337
  : textCol;
@@ -6252,63 +6352,87 @@ function renderToCanvas(sg, canvas, options = {}) {
6252
6352
  });
6253
6353
  rowY += rh;
6254
6354
  }
6355
+ ctx.globalAlpha = 1;
6255
6356
  }
6256
6357
  // ── Notes ─────────────────────────────────────────────────
6257
6358
  for (const n of sg.notes) {
6258
6359
  const gs = n.style ?? {};
6259
6360
  const fill = String(gs.fill ?? palette.noteFill);
6260
6361
  const strk = String(gs.stroke ?? palette.noteStroke);
6362
+ const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
6261
6363
  const fold = 14;
6262
6364
  const { x, y, w, h } = n;
6365
+ if (gs.opacity != null)
6366
+ ctx.globalAlpha = Number(gs.opacity);
6263
6367
  rc.polygon([
6264
6368
  [x, y],
6265
6369
  [x + w - fold, y],
6266
6370
  [x + w, y + fold],
6267
6371
  [x + w, y + h],
6268
6372
  [x, y + h],
6269
- ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.2 });
6373
+ ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk,
6374
+ strokeWidth: nStrokeWidth,
6375
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6376
+ });
6270
6377
  rc.polygon([
6271
6378
  [x + w - fold, y],
6272
6379
  [x + w, y + fold],
6273
6380
  [x + w - fold, y + fold],
6274
6381
  ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
6275
- fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
6276
- // ── Note typography ─────────────────────────────────
6277
- // supports: font, font-size, letter-spacing, text-align,
6278
- // vertical-align, line-height
6382
+ fill: palette.noteFold, fillStyle: 'solid', stroke: strk,
6383
+ strokeWidth: Math.min(nStrokeWidth, 0.8),
6384
+ });
6279
6385
  const nFontSize = Number(gs.fontSize ?? 12);
6386
+ const nFontWeight = gs.fontWeight ?? 400;
6280
6387
  const nFont = resolveStyleFont(gs, diagramFont);
6281
6388
  const nLetterSpacing = gs.letterSpacing;
6282
6389
  const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
6283
6390
  const nTextAlign = String(gs.textAlign ?? 'left');
6284
6391
  const nVertAlign = String(gs.verticalAlign ?? 'top');
6285
6392
  const nColor = String(gs.color ?? palette.noteText);
6286
- const nTextX = nTextAlign === 'right' ? x + w - fold - 6
6393
+ const nPad = Number(gs.padding ?? 12);
6394
+ const nTextX = nTextAlign === 'right' ? x + w - fold - nPad
6287
6395
  : nTextAlign === 'center' ? x + (w - fold) / 2
6288
- : x + 12;
6289
- // vertical-align inside note body (below fold)
6290
- const bodyTop = y + fold + 8;
6291
- const bodyBottom = y + h - 8;
6396
+ : x + nPad;
6397
+ const nFoldPad = fold + nPad;
6398
+ const bodyTop = y + nFoldPad;
6399
+ const bodyBottom = y + h - nPad;
6292
6400
  const blockH = (n.lines.length - 1) * nLineHeight;
6293
6401
  const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
6294
6402
  : nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
6295
- : bodyTop + blockH / 2; // top (default)
6403
+ : bodyTop + blockH / 2;
6296
6404
  if (n.lines.length > 1) {
6297
- drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
6405
+ drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
6298
6406
  }
6299
6407
  else {
6300
- drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
6408
+ drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nFont, nLetterSpacing);
6301
6409
  }
6410
+ if (gs.opacity != null)
6411
+ ctx.globalAlpha = 1;
6302
6412
  }
6303
6413
  // ── Markdown blocks ────────────────────────────────────────
6304
6414
  // Renders prose with Markdown headings and bold/italic inline spans.
6305
6415
  // Canvas has no native bold-within-a-run, so each run is drawn
6306
6416
  // individually with its own ctx.font setting.
6307
6417
  for (const m of (sg.markdowns ?? [])) {
6308
- const mFont = resolveStyleFont(m.style, diagramFont);
6309
- const baseColor = String(m.style?.color ?? palette.nodeText);
6310
- const textAlign = String(m.style?.textAlign ?? 'left');
6311
- const PAD = Number(m.style?.padding ?? 16);
6418
+ const gs = m.style ?? {};
6419
+ const mFont = resolveStyleFont(gs, diagramFont);
6420
+ const baseColor = String(gs.color ?? palette.nodeText);
6421
+ const textAlign = String(gs.textAlign ?? 'left');
6422
+ const PAD = Number(gs.padding ?? 16);
6423
+ const mLetterSpacing = gs.letterSpacing;
6424
+ if (gs.opacity != null)
6425
+ ctx.globalAlpha = Number(gs.opacity);
6426
+ // Background + border
6427
+ if (gs.fill || gs.stroke) {
6428
+ rc.rectangle(m.x, m.y, m.w, m.h, {
6429
+ ...R, seed: hashStr$1(m.id),
6430
+ fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
6431
+ stroke: String(gs.stroke ?? 'none'),
6432
+ strokeWidth: Number(gs.strokeWidth ?? 1.2),
6433
+ ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6434
+ });
6435
+ }
6312
6436
  const anchorX = textAlign === 'right' ? m.x + m.w - PAD
6313
6437
  : textAlign === 'center' ? m.x + m.w / 2
6314
6438
  : m.x + PAD;
@@ -6326,14 +6450,29 @@ function renderToCanvas(sg, canvas, options = {}) {
6326
6450
  ctx.save();
6327
6451
  ctx.textBaseline = 'middle';
6328
6452
  ctx.fillStyle = baseColor;
6453
+ const ls = mLetterSpacing ?? 0;
6454
+ // measure run width including letter-spacing
6455
+ const runW = (run) => {
6456
+ return ctx.measureText(run.text).width + ls * run.text.length;
6457
+ };
6458
+ const drawRun = (run, rx) => {
6459
+ if (ls) {
6460
+ for (const ch of run.text) {
6461
+ ctx.fillText(ch, rx, lineY);
6462
+ rx += ctx.measureText(ch).width + ls;
6463
+ }
6464
+ }
6465
+ else {
6466
+ ctx.fillText(run.text, rx, lineY);
6467
+ }
6468
+ };
6329
6469
  if (textAlign === 'center' || textAlign === 'right') {
6330
- // Measure full line width first
6331
6470
  let totalW = 0;
6332
6471
  for (const run of line.runs) {
6333
6472
  const runStyle = run.italic ? 'italic ' : '';
6334
6473
  const runWeight = run.bold ? 700 : fontWeight;
6335
6474
  ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
6336
- totalW += ctx.measureText(run.text).width;
6475
+ totalW += runW(run);
6337
6476
  }
6338
6477
  let runX = textAlign === 'center' ? anchorX - totalW / 2 : anchorX - totalW;
6339
6478
  ctx.textAlign = 'left';
@@ -6341,25 +6480,25 @@ function renderToCanvas(sg, canvas, options = {}) {
6341
6480
  const runStyle = run.italic ? 'italic ' : '';
6342
6481
  const runWeight = run.bold ? 700 : fontWeight;
6343
6482
  ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
6344
- ctx.fillText(run.text, runX, lineY);
6345
- runX += ctx.measureText(run.text).width;
6483
+ drawRun(run, runX);
6484
+ runX += runW(run);
6346
6485
  }
6347
6486
  }
6348
6487
  else {
6349
- // left-aligned — draw runs left to right from anchorX
6350
6488
  let runX = anchorX;
6351
6489
  ctx.textAlign = 'left';
6352
6490
  for (const run of line.runs) {
6353
6491
  const runStyle = run.italic ? 'italic ' : '';
6354
6492
  const runWeight = run.bold ? 700 : fontWeight;
6355
6493
  ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
6356
- ctx.fillText(run.text, runX, lineY);
6357
- runX += ctx.measureText(run.text).width;
6494
+ drawRun(run, runX);
6495
+ runX += runW(run);
6358
6496
  }
6359
6497
  }
6360
6498
  ctx.restore();
6361
6499
  y += LINE_SPACING[line.kind];
6362
6500
  }
6501
+ ctx.globalAlpha = 1;
6363
6502
  }
6364
6503
  // ── Charts ────────────────────────────────────────────────
6365
6504
  for (const c of sg.charts) {