sketchmark 0.1.1 → 0.1.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.

Potentially problematic release.


This version of sketchmark might be problematic. Click here for more details.

@@ -251,6 +251,16 @@ var AIDiagram = (function (exports) {
251
251
  s.fontSize = parseFloat(p["font-size"]);
252
252
  if (p["font-weight"])
253
253
  s.fontWeight = p["font-weight"];
254
+ if (p['text-align'])
255
+ s.textAlign = p['text-align'];
256
+ if (p['vertical-align'])
257
+ s.verticalAlign = p['vertical-align'];
258
+ if (p['line-height'])
259
+ s.lineHeight = parseFloat(p['line-height']);
260
+ if (p['letter-spacing'])
261
+ s.letterSpacing = parseFloat(p['letter-spacing']);
262
+ if (p.font)
263
+ s.font = p.font;
254
264
  if (p["dash"]) {
255
265
  const parts = p["dash"]
256
266
  .split(",")
@@ -477,6 +487,8 @@ var AIDiagram = (function (exports) {
477
487
  label: rawLabel.replace(/\\n/g, "\n"),
478
488
  theme: props.theme,
479
489
  style: propsToStyle(props),
490
+ ...(props.width ? { width: parseFloat(props.width) } : {}),
491
+ ...(props.height ? { height: parseFloat(props.height) } : {}),
480
492
  };
481
493
  }
482
494
  // ── parseGroup ───────────────────────────────────────────
@@ -1063,8 +1075,6 @@ var AIDiagram = (function (exports) {
1063
1075
  node.style = { ...ast.styles[node.id], ...node.style };
1064
1076
  }
1065
1077
  }
1066
- console.log("[parse] charts:", ast.charts.map((c) => c.id));
1067
- console.log("[parse] rootOrder:", ast.rootOrder.map((r) => r.kind + ":" + r.id));
1068
1078
  return ast;
1069
1079
  }
1070
1080
 
@@ -1140,6 +1150,8 @@ var AIDiagram = (function (exports) {
1140
1150
  y: 0,
1141
1151
  w: 0,
1142
1152
  h: 0,
1153
+ width: n.width,
1154
+ height: n.height,
1143
1155
  };
1144
1156
  });
1145
1157
  const charts = ast.charts.map((c) => {
@@ -1284,6 +1296,10 @@ var AIDiagram = (function (exports) {
1284
1296
  const maxChars = Math.max(...n.lines.map(l => l.length));
1285
1297
  n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
1286
1298
  n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
1299
+ if (n.width && n.w < n.width)
1300
+ n.w = n.width; // ← add
1301
+ if (n.height && n.h < n.height)
1302
+ n.h = n.height; // ← add
1287
1303
  }
1288
1304
  // ── Table auto-sizing ─────────────────────────────────────
1289
1305
  function sizeTable(t) {
@@ -2412,6 +2428,83 @@ var AIDiagram = (function (exports) {
2412
2428
  }
2413
2429
  const THEME_NAMES = Object.keys(PALETTES);
2414
2430
 
2431
+ // ============================================================
2432
+ // sketchmark — Font Registry
2433
+ // ============================================================
2434
+ // built-in named fonts — user can reference these by short name
2435
+ const BUILTIN_FONTS = {
2436
+ // hand-drawn
2437
+ caveat: {
2438
+ family: "'Caveat', cursive",
2439
+ url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
2440
+ },
2441
+ handlee: {
2442
+ family: "'Handlee', cursive",
2443
+ url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
2444
+ },
2445
+ 'indie-flower': {
2446
+ family: "'Indie Flower', cursive",
2447
+ url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
2448
+ },
2449
+ 'patrick-hand': {
2450
+ family: "'Patrick Hand', cursive",
2451
+ url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
2452
+ },
2453
+ // clean / readable
2454
+ 'dm-mono': {
2455
+ family: "'DM Mono', monospace",
2456
+ url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
2457
+ },
2458
+ 'jetbrains': {
2459
+ family: "'JetBrains Mono', monospace",
2460
+ url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
2461
+ },
2462
+ 'instrument': {
2463
+ family: "'Instrument Serif', serif",
2464
+ url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
2465
+ },
2466
+ 'playfair': {
2467
+ family: "'Playfair Display', serif",
2468
+ url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
2469
+ },
2470
+ // system fallbacks (no URL needed)
2471
+ system: { family: 'system-ui, sans-serif' },
2472
+ mono: { family: "'Courier New', monospace" },
2473
+ serif: { family: 'Georgia, serif' },
2474
+ };
2475
+ // default — what renders when no font is specified
2476
+ const DEFAULT_FONT = 'system-ui, sans-serif';
2477
+ // resolve a short name or pass-through a quoted CSS family
2478
+ function resolveFont(nameOrFamily) {
2479
+ const key = nameOrFamily.toLowerCase().trim();
2480
+ if (BUILTIN_FONTS[key])
2481
+ return BUILTIN_FONTS[key].family;
2482
+ return nameOrFamily; // treat as raw CSS font-family
2483
+ }
2484
+ // inject a <link> into <head> for a built-in font (browser only)
2485
+ function loadFont(name) {
2486
+ if (typeof document === 'undefined')
2487
+ return;
2488
+ const key = name.toLowerCase().trim();
2489
+ const def = BUILTIN_FONTS[key];
2490
+ if (!def?.url || def.loaded)
2491
+ return;
2492
+ if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
2493
+ return;
2494
+ const link = document.createElement('link');
2495
+ link.rel = 'stylesheet';
2496
+ link.href = def.url;
2497
+ link.setAttribute('data-sketchmark-font', key);
2498
+ document.head.appendChild(link);
2499
+ def.loaded = true;
2500
+ }
2501
+ // user registers their own font (already loaded via CSS/link)
2502
+ function registerFont(name, family, url) {
2503
+ BUILTIN_FONTS[name.toLowerCase()] = { family, url };
2504
+ if (url)
2505
+ loadFont(name);
2506
+ }
2507
+
2415
2508
  // ============================================================
2416
2509
  // sketchmark — SVG Renderer (rough.js hand-drawn)
2417
2510
  // ============================================================
@@ -2424,17 +2517,73 @@ var AIDiagram = (function (exports) {
2424
2517
  return h;
2425
2518
  }
2426
2519
  const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
2427
- // ── SVG helpers ───────────────────────────────────────────
2428
- function mkMultilineText(lines, x, cy, // vertical center of the whole block
2429
- sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18) {
2520
+ // ── Small helper: load + resolve font from style or fall back ─────────────
2521
+ function resolveStyleFont$1(style, fallback) {
2522
+ const raw = String(style["font"] ?? "");
2523
+ if (!raw)
2524
+ return fallback;
2525
+ loadFont(raw);
2526
+ return resolveFont(raw);
2527
+ }
2528
+ // ── SVG text helpers ──────────────────────────────────────────────────────
2529
+ /**
2530
+ * Single-line SVG text element.
2531
+ *
2532
+ * | param | maps to SVG attr |
2533
+ * |---------------|--------------------------|
2534
+ * txt | textContent |
2535
+ * x, y | x, y |
2536
+ * sz | font-size |
2537
+ * wt | font-weight |
2538
+ * col | fill |
2539
+ * anchor | text-anchor |
2540
+ * font | font-family |
2541
+ * letterSpacing | letter-spacing |
2542
+ */
2543
+ function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", font, letterSpacing) {
2544
+ const t = se("text");
2545
+ t.setAttribute("x", String(x));
2546
+ t.setAttribute("y", String(y));
2547
+ t.setAttribute("text-anchor", anchor);
2548
+ t.setAttribute("dominant-baseline", "middle");
2549
+ t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
2550
+ t.setAttribute("font-size", String(sz));
2551
+ t.setAttribute("font-weight", String(wt));
2552
+ t.setAttribute("fill", col);
2553
+ t.setAttribute("pointer-events", "none");
2554
+ t.setAttribute("user-select", "none");
2555
+ if (letterSpacing != null)
2556
+ t.setAttribute("letter-spacing", String(letterSpacing));
2557
+ t.textContent = txt;
2558
+ return t;
2559
+ }
2560
+ /**
2561
+ * Multi-line SVG text element using <tspan> per line.
2562
+ *
2563
+ * | param | maps to SVG attr |
2564
+ * |---------------|--------------------------|
2565
+ * lines | one <tspan> each |
2566
+ * x | tspan x |
2567
+ * cy | vertical centre of block |
2568
+ * sz | font-size |
2569
+ * wt | font-weight |
2570
+ * col | fill |
2571
+ * anchor | text-anchor |
2572
+ * lineH | dy between tspans (px) |
2573
+ * font | font-family |
2574
+ * letterSpacing | letter-spacing |
2575
+ */
2576
+ function mkMultilineText(lines, x, cy, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18, font, letterSpacing) {
2430
2577
  const t = se("text");
2431
2578
  t.setAttribute("text-anchor", anchor);
2432
- t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2579
+ t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
2433
2580
  t.setAttribute("font-size", String(sz));
2434
2581
  t.setAttribute("font-weight", String(wt));
2435
2582
  t.setAttribute("fill", col);
2436
2583
  t.setAttribute("pointer-events", "none");
2437
2584
  t.setAttribute("user-select", "none");
2585
+ if (letterSpacing != null)
2586
+ t.setAttribute("letter-spacing", String(letterSpacing));
2438
2587
  // vertically centre the whole block
2439
2588
  const totalH = (lines.length - 1) * lineH;
2440
2589
  const startY = cy - totalH / 2;
@@ -2448,21 +2597,6 @@ var AIDiagram = (function (exports) {
2448
2597
  });
2449
2598
  return t;
2450
2599
  }
2451
- function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle") {
2452
- const t = se("text");
2453
- t.setAttribute("x", String(x));
2454
- t.setAttribute("y", String(y));
2455
- t.setAttribute("text-anchor", anchor);
2456
- t.setAttribute("dominant-baseline", "middle");
2457
- t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2458
- t.setAttribute("font-size", String(sz));
2459
- t.setAttribute("font-weight", String(wt));
2460
- t.setAttribute("fill", col);
2461
- t.setAttribute("pointer-events", "none");
2462
- t.setAttribute("user-select", "none");
2463
- t.textContent = txt;
2464
- return t;
2465
- }
2466
2600
  function mkGroup(id, cls) {
2467
2601
  const g = se("g");
2468
2602
  if (id)
@@ -2471,7 +2605,7 @@ var AIDiagram = (function (exports) {
2471
2605
  g.setAttribute("class", cls);
2472
2606
  return g;
2473
2607
  }
2474
- // ── Arrow direction from connector ────────────────────────
2608
+ // ── Arrow direction from connector ────────────────────────────────────────
2475
2609
  function connMeta$1(connector) {
2476
2610
  if (connector === "--")
2477
2611
  return { arrowAt: "none", dashed: false };
@@ -2486,7 +2620,7 @@ var AIDiagram = (function (exports) {
2486
2620
  return { arrowAt: "start", dashed };
2487
2621
  return { arrowAt: "end", dashed };
2488
2622
  }
2489
- // ── Generic rect connection point ─────────────────────────
2623
+ // ── Generic rect connection point ─────────────────────────────────────────
2490
2624
  function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
2491
2625
  const cx = rx + rw / 2, cy = ry + rh / 2;
2492
2626
  const dx = ox - cx, dy = oy - cy;
@@ -2511,7 +2645,7 @@ var AIDiagram = (function (exports) {
2511
2645
  }
2512
2646
  return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
2513
2647
  }
2514
- // ── Group depth (for paint order) ─────────────────────────
2648
+ // ── Group depth (for paint order) ─────────────────────────────────────────
2515
2649
  function groupDepth$1(g, gm) {
2516
2650
  let d = 0;
2517
2651
  let cur = g;
@@ -2521,7 +2655,7 @@ var AIDiagram = (function (exports) {
2521
2655
  }
2522
2656
  return d;
2523
2657
  }
2524
- // ── Node shapes ───────────────────────────────────────────
2658
+ // ── Node shapes ───────────────────────────────────────────────────────────
2525
2659
  function renderShape$1(rc, n, palette) {
2526
2660
  const s = n.style ?? {};
2527
2661
  const fill = String(s.fill ?? palette.nodeFill);
@@ -2594,19 +2728,18 @@ var AIDiagram = (function (exports) {
2594
2728
  return [];
2595
2729
  case "image": {
2596
2730
  if (n.imageUrl) {
2597
- const img = document.createElementNS("http://www.w3.org/2000/svg", "image");
2731
+ const img = document.createElementNS(NS, "image");
2598
2732
  img.setAttribute("href", n.imageUrl);
2599
2733
  img.setAttribute("x", String(n.x + 1));
2600
2734
  img.setAttribute("y", String(n.y + 1));
2601
2735
  img.setAttribute("width", String(n.w - 2));
2602
2736
  img.setAttribute("height", String(n.h - 2));
2603
2737
  img.setAttribute("preserveAspectRatio", "xMidYMid meet");
2604
- // optional: clip to rounded rect
2605
2738
  const clipId = `clip-${n.id}`;
2606
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
2607
- const clip = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
2739
+ const defs = document.createElementNS(NS, "defs");
2740
+ const clip = document.createElementNS(NS, "clipPath");
2608
2741
  clip.setAttribute("id", clipId);
2609
- const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2742
+ const rect = document.createElementNS(NS, "rect");
2610
2743
  rect.setAttribute("x", String(n.x + 1));
2611
2744
  rect.setAttribute("y", String(n.y + 1));
2612
2745
  rect.setAttribute("width", String(n.w - 2));
@@ -2615,15 +2748,12 @@ var AIDiagram = (function (exports) {
2615
2748
  clip.appendChild(rect);
2616
2749
  defs.appendChild(clip);
2617
2750
  img.setAttribute("clip-path", `url(#${clipId})`);
2618
- // border box drawn on top
2619
2751
  const border = rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2620
2752
  ...opts,
2621
2753
  fill: "none",
2622
- fillStyle: "solid",
2623
2754
  });
2624
2755
  return [defs, img, border];
2625
2756
  }
2626
- // fallback: no URL → grey placeholder box
2627
2757
  return [
2628
2758
  rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2629
2759
  ...opts,
@@ -2636,7 +2766,7 @@ var AIDiagram = (function (exports) {
2636
2766
  return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
2637
2767
  }
2638
2768
  }
2639
- // ── Arrowhead ─────────────────────────────────────────────
2769
+ // ── Arrowhead ─────────────────────────────────────────────────────────────
2640
2770
  function arrowHead(rc, x, y, angle, col, seed) {
2641
2771
  const as = 12;
2642
2772
  return rc.polygon([
@@ -2660,14 +2790,22 @@ var AIDiagram = (function (exports) {
2660
2790
  }
2661
2791
  function renderToSVG(sg, container, options = {}) {
2662
2792
  if (typeof rough === "undefined") {
2663
- throw new Error('rough.js is not loaded. Add <script src="https://unpkg.com/roughjs/bundled/rough.js"></script>');
2793
+ throw new Error("rough.js is not loaded.");
2664
2794
  }
2665
2795
  const isDark = options.theme === "dark" ||
2666
2796
  (options.theme === "auto" &&
2667
2797
  window.matchMedia?.("(prefers-color-scheme:dark)").matches);
2668
- // Resolve palette: DSL config takes priority, then options.theme, then light
2669
2798
  const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
2670
2799
  const palette = resolvePalette(themeName);
2800
+ // ── Diagram-level font ──────────────────────────────────
2801
+ const diagramFont = (() => {
2802
+ const raw = String(sg.config["font"] ?? "");
2803
+ if (raw) {
2804
+ loadFont(raw);
2805
+ return resolveFont(raw);
2806
+ }
2807
+ return DEFAULT_FONT;
2808
+ })();
2671
2809
  BASE_ROUGH.roughness = options.roughness ?? 1.3;
2672
2810
  BASE_ROUGH.bowing = options.bowing ?? 0.7;
2673
2811
  let svg;
@@ -2684,14 +2822,7 @@ var AIDiagram = (function (exports) {
2684
2822
  svg.setAttribute("height", String(sg.height));
2685
2823
  svg.setAttribute("viewBox", `0 0 ${sg.width} ${sg.height}`);
2686
2824
  svg.style.fontFamily = "var(--font-sans, system-ui, sans-serif)";
2687
- // Background rect so exported SVGs have correct bg
2688
- // const bgRect = se("rect") as SVGRectElement;
2689
- // bgRect.setAttribute("x", "0");
2690
- // bgRect.setAttribute("y", "0");
2691
- // bgRect.setAttribute("width", String(sg.width));
2692
- // bgRect.setAttribute("height", String(sg.height));
2693
- // bgRect.setAttribute("fill", palette.background);
2694
- // svg.appendChild(bgRect);
2825
+ // ── Background ─────────────────────────────────────────
2695
2826
  if (!options.transparent) {
2696
2827
  const bgRect = se("rect");
2697
2828
  bgRect.setAttribute("x", "0");
@@ -2707,9 +2838,9 @@ var AIDiagram = (function (exports) {
2707
2838
  const titleColor = String(sg.config["title-color"] ?? palette.titleText);
2708
2839
  const titleSize = Number(sg.config["title-size"] ?? 18);
2709
2840
  const titleWeight = Number(sg.config["title-weight"] ?? 600);
2710
- svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor));
2841
+ svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor, "middle", diagramFont));
2711
2842
  }
2712
- // ── Groups (depth-sorted: outermost first) ────────────────
2843
+ // ── Groups ───────────────────────────────────────────────
2713
2844
  const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
2714
2845
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth$1(a, gmMap) - groupDepth$1(b, gmMap));
2715
2846
  const GL = mkGroup("grp-layer");
@@ -2729,8 +2860,14 @@ var AIDiagram = (function (exports) {
2729
2860
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
2730
2861
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
2731
2862
  }));
2732
- const labelColor = gs.color ? String(gs.color) : palette.groupLabel;
2733
- gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, 12, 500, labelColor, "start"));
2863
+ // ── Group label typography ──────────────────────────
2864
+ // supports: font, font-size, letter-spacing
2865
+ // always left-anchored (single line)
2866
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
2867
+ const gFontSize = Number(gs.fontSize ?? 12);
2868
+ const gFont = resolveStyleFont$1(gs, diagramFont);
2869
+ const gLetterSpacing = gs.letterSpacing;
2870
+ gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
2734
2871
  GL.appendChild(gg);
2735
2872
  }
2736
2873
  svg.appendChild(GL);
@@ -2784,7 +2921,13 @@ var AIDiagram = (function (exports) {
2784
2921
  bg.setAttribute("rx", "3");
2785
2922
  bg.setAttribute("opacity", "0.9");
2786
2923
  eg.appendChild(bg);
2787
- eg.appendChild(mkText(e.label, mx, my, 11, 400, palette.edgeLabelText));
2924
+ // ── Edge label typography ───────────────────────
2925
+ // supports: font, font-size, letter-spacing
2926
+ // always center-anchored (single line floating on edge)
2927
+ const eFontSize = Number(e.style?.fontSize ?? 11);
2928
+ const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
2929
+ const eLetterSpacing = e.style?.letterSpacing;
2930
+ eg.appendChild(mkText(e.label, mx, my, eFontSize, 400, palette.edgeLabelText, "middle", eFont, eLetterSpacing));
2788
2931
  }
2789
2932
  EL.appendChild(eg);
2790
2933
  }
@@ -2794,14 +2937,43 @@ var AIDiagram = (function (exports) {
2794
2937
  for (const n of sg.nodes) {
2795
2938
  const ng = mkGroup(`node-${n.id}`, "ng");
2796
2939
  renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
2940
+ // ── Node / text typography ─────────────────────────
2941
+ // supports: font, font-size, letter-spacing, text-align, line-height
2797
2942
  const fontSize = Number(n.style?.fontSize ?? (n.shape === "text" ? 13 : 14));
2798
2943
  const fontWeight = n.style?.fontWeight ?? (n.shape === "text" ? 400 : 500);
2944
+ const textColor = String(n.style?.color ??
2945
+ (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
2946
+ const nodeFont = resolveStyleFont$1(n.style ?? {}, diagramFont);
2947
+ const textAlign = String(n.style?.textAlign ?? "center");
2948
+ const anchorMap = {
2949
+ left: "start",
2950
+ center: "middle",
2951
+ right: "end",
2952
+ };
2953
+ const textAnchor = anchorMap[textAlign] ?? "middle";
2954
+ // line-height is a multiplier (e.g. 1.4 = 140% of font-size)
2955
+ const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
2956
+ const letterSpacing = n.style?.letterSpacing;
2957
+ // x shifts for left / right alignment
2958
+ const textX = textAlign === "left"
2959
+ ? n.x + 8
2960
+ : textAlign === "right"
2961
+ ? n.x + n.w - 8
2962
+ : n.x + n.w / 2;
2799
2963
  const lines = n.label.split("\n");
2964
+ const verticalAlign = String(n.style?.verticalAlign ?? "middle");
2965
+ const nodeBodyTop = n.y + 6;
2966
+ const nodeBodyBottom = n.y + n.h - 6;
2967
+ const nodeBodyMid = n.y + n.h / 2;
2968
+ const blockH = (lines.length - 1) * lineHeight;
2969
+ const textCY = verticalAlign === "top"
2970
+ ? nodeBodyTop + blockH / 2
2971
+ : verticalAlign === "bottom"
2972
+ ? nodeBodyBottom - blockH / 2
2973
+ : nodeBodyMid;
2800
2974
  ng.appendChild(lines.length > 1
2801
- ? mkMultilineText(lines, n.x + n.w / 2, n.y + n.h / 2, fontSize, fontWeight, String(n.style?.color ??
2802
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText)))
2803
- : mkText(n.label, n.x + n.w / 2, n.y + n.h / 2, fontSize, fontWeight, String(n.style?.color ??
2804
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText))));
2975
+ ? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight, nodeFont, letterSpacing)
2976
+ : mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor, nodeFont, letterSpacing));
2805
2977
  if (options.interactive) {
2806
2978
  ng.style.cursor = "pointer";
2807
2979
  ng.addEventListener("click", () => options.onNodeClick?.(n.id));
@@ -2827,7 +2999,12 @@ var AIDiagram = (function (exports) {
2827
2999
  const hdrText = String(gs.color ?? palette.tableHeaderText);
2828
3000
  const divCol = palette.tableDivider;
2829
3001
  const pad = t.labelH;
2830
- // Outer border
3002
+ // ── Table-level font (applies to label + all cells) ─
3003
+ // supports: font, font-size, letter-spacing
3004
+ const tFontSize = Number(gs.fontSize ?? 12);
3005
+ const tFont = resolveStyleFont$1(gs, diagramFont);
3006
+ const tLetterSpacing = gs.letterSpacing;
3007
+ // outer border
2831
3008
  tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
2832
3009
  ...BASE_ROUGH,
2833
3010
  seed: hashStr$3(t.id),
@@ -2836,20 +3013,19 @@ var AIDiagram = (function (exports) {
2836
3013
  stroke: strk,
2837
3014
  strokeWidth: 1.5,
2838
3015
  }));
2839
- // Label strip separator
3016
+ // label strip separator
2840
3017
  tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
2841
3018
  roughness: 0.6,
2842
3019
  seed: hashStr$3(t.id + "l"),
2843
3020
  stroke: strk,
2844
3021
  strokeWidth: 1,
2845
3022
  }));
2846
- // Label text
2847
- tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, 12, 500, textCol, "start"));
2848
- // Rows
3023
+ // ── Table label: font, font-size, letter-spacing (always left) ──
3024
+ tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, "start", tFont, tLetterSpacing));
3025
+ // rows
2849
3026
  let rowY = t.y + pad;
2850
3027
  for (const row of t.rows) {
2851
3028
  const rh = row.kind === "header" ? t.headerH : t.rowH;
2852
- // Header background fill
2853
3029
  if (row.kind === "header") {
2854
3030
  const hdrBg = se("rect");
2855
3031
  hdrBg.setAttribute("x", String(t.x + 1));
@@ -2859,19 +3035,34 @@ var AIDiagram = (function (exports) {
2859
3035
  hdrBg.setAttribute("fill", hdrFill);
2860
3036
  tg.appendChild(hdrBg);
2861
3037
  }
2862
- // Row separator
2863
3038
  tg.appendChild(rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
2864
3039
  roughness: 0.4,
2865
3040
  seed: hashStr$3(t.id + rowY),
2866
3041
  stroke: row.kind === "header" ? strk : divCol,
2867
3042
  strokeWidth: row.kind === "header" ? 1.2 : 0.6,
2868
3043
  }));
2869
- // Cell text + col separators
3044
+ // ── Cell text: font, font-size, letter-spacing, text-align ──
3045
+ // text-align applies to data rows; header is always centered
3046
+ const cellAlignProp = row.kind === "header" ? "center" : String(gs.textAlign ?? "center");
3047
+ const cellAnchorMap = {
3048
+ left: "start",
3049
+ center: "middle",
3050
+ right: "end",
3051
+ };
3052
+ const cellAnchor = cellAnchorMap[cellAlignProp] ?? "middle";
3053
+ const cellFw = row.kind === "header" ? 600 : 400;
3054
+ const cellColor = row.kind === "header" ? hdrText : textCol;
2870
3055
  let cx = t.x;
2871
3056
  row.cells.forEach((cell, i) => {
2872
3057
  const cw = t.colWidths[i] ?? 60;
2873
- const fw = row.kind === "header" ? 600 : 400;
2874
- tg.appendChild(mkText(cell, cx + cw / 2, rowY + rh / 2, 12, fw, row.kind === "header" ? hdrText : textCol));
3058
+ // x position shifts with alignment
3059
+ const cellX = cellAnchor === "start"
3060
+ ? cx + 6
3061
+ : cellAnchor === "end"
3062
+ ? cx + cw - 6
3063
+ : cx + cw / 2;
3064
+ // ← was missing tg.appendChild — cells were invisible before
3065
+ tg.appendChild(mkText(cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAnchor, tFont, tLetterSpacing));
2875
3066
  if (i < row.cells.length - 1) {
2876
3067
  tg.appendChild(rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
2877
3068
  roughness: 0.3,
@@ -2900,6 +3091,25 @@ var AIDiagram = (function (exports) {
2900
3091
  const strk = String(gs.stroke ?? palette.noteStroke);
2901
3092
  const fold = 14;
2902
3093
  const { x, y, w, h } = n;
3094
+ // ── Note typography ─────────────────────────────────
3095
+ // supports: font, font-size, letter-spacing, text-align, line-height
3096
+ const nFontSize = Number(gs.fontSize ?? 12);
3097
+ const nFont = resolveStyleFont$1(gs, diagramFont);
3098
+ const nLetterSpacing = gs.letterSpacing;
3099
+ const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
3100
+ const nTextAlign = String(gs.textAlign ?? "left");
3101
+ const nAnchorMap = {
3102
+ left: "start",
3103
+ center: "middle",
3104
+ right: "end",
3105
+ };
3106
+ const nAnchor = nAnchorMap[nTextAlign] ?? "start";
3107
+ // x position for the text block (pad from left, with alignment)
3108
+ const nTextX = nTextAlign === "right"
3109
+ ? x + w - fold - 6
3110
+ : nTextAlign === "center"
3111
+ ? x + (w - fold) / 2
3112
+ : x + 12;
2903
3113
  ng.appendChild(rc.polygon([
2904
3114
  [x, y],
2905
3115
  [x + w - fold, y],
@@ -2926,9 +3136,24 @@ var AIDiagram = (function (exports) {
2926
3136
  stroke: strk,
2927
3137
  strokeWidth: 0.8,
2928
3138
  }));
2929
- n.lines.forEach((line, i) => {
2930
- ng.appendChild(mkText(line, x + 12, y + 12 + i * 20 + 10, 12, 400, String(gs.color ?? palette.noteText), "start"));
2931
- });
3139
+ const nVerticalAlign = String(gs.verticalAlign ?? "top");
3140
+ const bodyTop = y + fold + 8; // below the fold triangle
3141
+ const bodyBottom = y + h - 8; // above bottom edge
3142
+ const bodyMid = (bodyTop + bodyBottom) / 2;
3143
+ const blockH = (n.lines.length - 1) * nLineHeight;
3144
+ const blockCY = nVerticalAlign === "bottom"
3145
+ ? bodyBottom - blockH / 2
3146
+ : nVerticalAlign === "middle"
3147
+ ? bodyMid
3148
+ : bodyTop + blockH / 2;
3149
+ // multiline: use mkMultilineText so line-height is respected
3150
+ if (n.lines.length > 1) {
3151
+ // vertical centre of the text block inside the note
3152
+ ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
3153
+ }
3154
+ else {
3155
+ ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
3156
+ }
2932
3157
  NoteL.appendChild(ng);
2933
3158
  }
2934
3159
  svg.appendChild(NoteL);
@@ -3205,22 +3430,69 @@ var AIDiagram = (function (exports) {
3205
3430
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
3206
3431
  return h;
3207
3432
  }
3208
- // ── Arrow direction from connector (mirrors svg/index.ts)
3433
+ // ── Small helper: load + resolve font from a style map ────────────────────
3434
+ function resolveStyleFont(style, fallback) {
3435
+ const raw = String(style['font'] ?? '');
3436
+ if (!raw)
3437
+ return fallback;
3438
+ loadFont(raw);
3439
+ return resolveFont(raw);
3440
+ }
3441
+ // ── Canvas text helpers ────────────────────────────────────────────────────
3442
+ /**
3443
+ * Draw a single line of text.
3444
+ * align: 'left' | 'center' | 'right' (maps to ctx.textAlign)
3445
+ */
3446
+ function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
3447
+ ctx.save();
3448
+ ctx.font = `${wt} ${sz}px ${font}`;
3449
+ ctx.fillStyle = col;
3450
+ ctx.textAlign = align;
3451
+ ctx.textBaseline = 'middle';
3452
+ if (letterSpacing) {
3453
+ // Canvas has no native letter-spacing — draw char by char
3454
+ const chars = txt.split('');
3455
+ const totalW = ctx.measureText(txt).width + letterSpacing * (chars.length - 1);
3456
+ let startX = align === 'center' ? x - totalW / 2
3457
+ : align === 'right' ? x - totalW
3458
+ : x;
3459
+ ctx.textAlign = 'left';
3460
+ for (const ch of chars) {
3461
+ ctx.fillText(ch, startX, y);
3462
+ startX += ctx.measureText(ch).width + letterSpacing;
3463
+ }
3464
+ }
3465
+ else {
3466
+ ctx.fillText(txt, x, y);
3467
+ }
3468
+ ctx.restore();
3469
+ }
3470
+ /**
3471
+ * Draw multiple lines of text, vertically centred around cy.
3472
+ */
3473
+ function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
3474
+ const totalH = (lines.length - 1) * lineH;
3475
+ const startY = cy - totalH / 2;
3476
+ lines.forEach((line, i) => {
3477
+ drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
3478
+ });
3479
+ }
3480
+ // ── Arrow direction ────────────────────────────────────────────────────────
3209
3481
  function connMeta(connector) {
3210
- if (connector === "--")
3211
- return { arrowAt: "none", dashed: false };
3212
- if (connector === "---")
3213
- return { arrowAt: "none", dashed: true };
3214
- const bidir = connector.includes("<") && connector.includes(">");
3482
+ if (connector === '--')
3483
+ return { arrowAt: 'none', dashed: false };
3484
+ if (connector === '---')
3485
+ return { arrowAt: 'none', dashed: true };
3486
+ const bidir = connector.includes('<') && connector.includes('>');
3215
3487
  if (bidir)
3216
- return { arrowAt: "both", dashed: connector.includes("--") };
3217
- const back = connector.startsWith("<");
3218
- const dashed = connector.includes("--");
3488
+ return { arrowAt: 'both', dashed: connector.includes('--') };
3489
+ const back = connector.startsWith('<');
3490
+ const dashed = connector.includes('--');
3219
3491
  if (back)
3220
- return { arrowAt: "start", dashed };
3221
- return { arrowAt: "end", dashed };
3492
+ return { arrowAt: 'start', dashed };
3493
+ return { arrowAt: 'end', dashed };
3222
3494
  }
3223
- // ── Generic rect connection point ─────────────────────────
3495
+ // ── Rect connection point ──────────────────────────────────────────────────
3224
3496
  function rectConnPoint(rx, ry, rw, rh, ox, oy) {
3225
3497
  const cx = rx + rw / 2, cy = ry + rh / 2;
3226
3498
  const dx = ox - cx, dy = oy - cy;
@@ -3233,19 +3505,16 @@ var AIDiagram = (function (exports) {
3233
3505
  return [cx + t * dx, cy + t * dy];
3234
3506
  }
3235
3507
  function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
3236
- return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
3508
+ return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
3237
3509
  }
3238
3510
  function getConnPoint(src, dstCX, dstCY) {
3239
- if ("shape" in src && src.shape) {
3511
+ if ('shape' in src && src.shape) {
3240
3512
  return connPoint(src, {
3241
- x: dstCX - 1,
3242
- y: dstCY - 1,
3243
- w: 2,
3244
- h: 2});
3513
+ x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
3245
3514
  }
3246
3515
  return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
3247
3516
  }
3248
- // ── Group depth (for paint order, outermost first) ────────
3517
+ // ── Group depth ────────────────────────────────────────────────────────────
3249
3518
  function groupDepth(g, gm) {
3250
3519
  let d = 0;
3251
3520
  let cur = g;
@@ -3255,80 +3524,57 @@ var AIDiagram = (function (exports) {
3255
3524
  }
3256
3525
  return d;
3257
3526
  }
3258
- // ── Node shapes ───────────────────────────────────────────
3527
+ // ── Node shapes ────────────────────────────────────────────────────────────
3259
3528
  function renderShape(rc, ctx, n, palette, R) {
3260
3529
  const s = n.style ?? {};
3261
3530
  const fill = String(s.fill ?? palette.nodeFill);
3262
3531
  const stroke = String(s.stroke ?? palette.nodeStroke);
3263
3532
  const opts = {
3264
- ...R,
3265
- seed: hashStr$1(n.id),
3266
- fill,
3267
- fillStyle: "solid",
3268
- stroke,
3269
- strokeWidth: Number(s.strokeWidth ?? 1.9),
3533
+ ...R, seed: hashStr$1(n.id),
3534
+ fill, fillStyle: 'solid',
3535
+ stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
3270
3536
  };
3271
3537
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
3272
3538
  const hw = n.w / 2 - 2;
3273
3539
  switch (n.shape) {
3274
- case "circle":
3540
+ case 'circle':
3275
3541
  rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
3276
3542
  break;
3277
- case "diamond":
3278
- rc.polygon([
3279
- [cx, n.y + 2],
3280
- [cx + hw, cy],
3281
- [cx, n.y + n.h - 2],
3282
- [cx - hw, cy],
3283
- ], opts);
3543
+ case 'diamond':
3544
+ rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
3284
3545
  break;
3285
- case "hexagon": {
3546
+ case 'hexagon': {
3286
3547
  const hw2 = hw * 0.56;
3287
3548
  rc.polygon([
3288
- [cx - hw2, n.y + 3],
3289
- [cx + hw2, n.y + 3],
3290
- [cx + hw, cy],
3291
- [cx + hw2, n.y + n.h - 3],
3292
- [cx - hw2, n.y + n.h - 3],
3293
- [cx - hw, cy],
3549
+ [cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
3550
+ [cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
3294
3551
  ], opts);
3295
3552
  break;
3296
3553
  }
3297
- case "triangle":
3298
- rc.polygon([
3299
- [cx, n.y + 3],
3300
- [n.x + n.w - 3, n.y + n.h - 3],
3301
- [n.x + 3, n.y + n.h - 3],
3302
- ], opts);
3554
+ case 'triangle':
3555
+ rc.polygon([[cx, n.y + 3], [n.x + n.w - 3, n.y + n.h - 3], [n.x + 3, n.y + n.h - 3]], opts);
3303
3556
  break;
3304
- case "cylinder": {
3557
+ case 'cylinder': {
3305
3558
  const eH = 18;
3306
3559
  rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
3307
3560
  rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
3308
- rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
3309
- ...opts,
3310
- roughness: 0.6,
3311
- fill: "none",
3312
- });
3561
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
3313
3562
  break;
3314
3563
  }
3315
- case "parallelogram":
3564
+ case 'parallelogram':
3316
3565
  rc.polygon([
3317
- [n.x + 18, n.y + 1],
3318
- [n.x + n.w - 1, n.y + 1],
3319
- [n.x + n.w - 18, n.y + n.h - 1],
3320
- [n.x + 1, n.y + n.h - 1],
3566
+ [n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
3567
+ [n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
3321
3568
  ], opts);
3322
3569
  break;
3323
- case "text":
3324
- break; // text nodes: no background shape
3325
- case "image": {
3570
+ case 'text':
3571
+ break;
3572
+ case 'image': {
3326
3573
  if (n.imageUrl) {
3327
3574
  const img = new Image();
3328
- img.crossOrigin = "anonymous";
3575
+ img.crossOrigin = 'anonymous';
3329
3576
  img.onload = () => {
3330
3577
  ctx.save();
3331
- // rounded clip
3332
3578
  ctx.beginPath();
3333
3579
  const r = 6;
3334
3580
  ctx.moveTo(n.x + r, n.y);
@@ -3344,21 +3590,12 @@ var AIDiagram = (function (exports) {
3344
3590
  ctx.clip();
3345
3591
  ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
3346
3592
  ctx.restore();
3347
- // border on top
3348
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
3349
- ...opts,
3350
- fill: "none",
3351
- });
3593
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
3352
3594
  };
3353
3595
  img.src = n.imageUrl;
3354
3596
  }
3355
3597
  else {
3356
- // placeholder
3357
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
3358
- ...opts,
3359
- fill: "#e0e0e0",
3360
- stroke: "#999999",
3361
- });
3598
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
3362
3599
  }
3363
3600
  return;
3364
3601
  }
@@ -3367,57 +3604,52 @@ var AIDiagram = (function (exports) {
3367
3604
  break;
3368
3605
  }
3369
3606
  }
3370
- // ── Arrowhead ─────────────────────────────────────────────
3607
+ // ── Arrowhead ─────────────────────────────────────────────────────────────
3371
3608
  function drawArrowHead(rc, x, y, angle, col, seed, R) {
3372
3609
  const as = 12;
3373
3610
  rc.polygon([
3374
3611
  [x, y],
3375
- [
3376
- x - as * Math.cos(angle - Math.PI / 6.5),
3377
- y - as * Math.sin(angle - Math.PI / 6.5),
3378
- ],
3379
- [
3380
- x - as * Math.cos(angle + Math.PI / 6.5),
3381
- y - as * Math.sin(angle + Math.PI / 6.5),
3382
- ],
3383
- ], {
3384
- roughness: 0.3,
3385
- seed,
3386
- fill: col,
3387
- fillStyle: "solid",
3388
- stroke: col,
3389
- strokeWidth: 0.8,
3390
- });
3612
+ [x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
3613
+ [x - as * Math.cos(angle + Math.PI / 6.5), y - as * Math.sin(angle + Math.PI / 6.5)],
3614
+ ], { roughness: 0.3, seed, fill: col, fillStyle: 'solid', stroke: col, strokeWidth: 0.8 });
3391
3615
  }
3616
+ // ── Public API ─────────────────────────────────────────────────────────────
3392
3617
  function renderToCanvas(sg, canvas, options = {}) {
3393
- if (typeof rough === "undefined")
3394
- throw new Error("rough.js not loaded");
3618
+ if (typeof rough === 'undefined')
3619
+ throw new Error('rough.js not loaded');
3395
3620
  const scale = options.scale ?? window.devicePixelRatio ?? 1;
3396
3621
  canvas.width = sg.width * scale;
3397
3622
  canvas.height = sg.height * scale;
3398
- canvas.style.width = sg.width + "px";
3399
- canvas.style.height = sg.height + "px";
3400
- const ctx = canvas.getContext("2d");
3623
+ canvas.style.width = sg.width + 'px';
3624
+ canvas.style.height = sg.height + 'px';
3625
+ const ctx = canvas.getContext('2d');
3401
3626
  ctx.scale(scale, scale);
3402
- if (options.transparent) {
3403
- ctx.clearRect(0, 0, canvas.width, canvas.height);
3404
- }
3405
- // ── Resolve palette (mirrors SVG renderer) ───────────────
3406
- const isDark = options.theme === "dark" ||
3407
- (options.theme === "auto" &&
3408
- window.matchMedia?.("(prefers-color-scheme:dark)").matches);
3409
- const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
3627
+ // ── Palette ──────────────────────────────────────────────
3628
+ const isDark = options.theme === 'dark' ||
3629
+ (options.theme === 'auto' &&
3630
+ typeof window !== 'undefined' &&
3631
+ window.matchMedia?.('(prefers-color-scheme:dark)').matches);
3632
+ const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? 'dark' : 'light'));
3410
3633
  const palette = resolvePalette(themeName);
3634
+ // ── Diagram-level font ───────────────────────────────────
3635
+ const diagramFont = (() => {
3636
+ const raw = String(sg.config['font'] ?? '');
3637
+ if (raw) {
3638
+ loadFont(raw);
3639
+ return resolveFont(raw);
3640
+ }
3641
+ return DEFAULT_FONT;
3642
+ })();
3643
+ // ── Background ───────────────────────────────────────────
3411
3644
  if (!options.transparent) {
3412
3645
  ctx.fillStyle = options.background ?? palette.background;
3413
3646
  ctx.fillRect(0, 0, sg.width, sg.height);
3414
3647
  }
3648
+ else {
3649
+ ctx.clearRect(0, 0, sg.width, sg.height);
3650
+ }
3415
3651
  const rc = rough.canvas(canvas);
3416
- const R = {
3417
- roughness: options.roughness ?? 1.3,
3418
- bowing: options.bowing ?? 0.7,
3419
- };
3420
- // ── Lookup maps ──────────────────────────────────────────
3652
+ const R = { roughness: options.roughness ?? 1.3, bowing: options.bowing ?? 0.7 };
3421
3653
  const nm = nodeMap(sg);
3422
3654
  const tm = tableMap(sg);
3423
3655
  const gm = groupMap(sg);
@@ -3425,36 +3657,30 @@ var AIDiagram = (function (exports) {
3425
3657
  const ntm = noteMap(sg);
3426
3658
  // ── Title ────────────────────────────────────────────────
3427
3659
  if (sg.title) {
3428
- ctx.save();
3429
- ctx.font = "600 18px system-ui, sans-serif";
3430
- ctx.fillStyle = palette.titleText;
3431
- ctx.textAlign = "center";
3432
- ctx.fillText(sg.title, sg.width / 2, 28);
3433
- ctx.restore();
3660
+ const titleSize = Number(sg.config['title-size'] ?? 18);
3661
+ drawText(ctx, sg.title, sg.width / 2, 28, titleSize, 600, palette.titleText, 'center', diagramFont);
3434
3662
  }
3435
- // ── Groups (depth-sorted: outermost first) ────────────────
3663
+ // ── Groups (outermost first) ─────────────────────────────
3436
3664
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
3437
3665
  for (const g of sortedGroups) {
3438
3666
  if (!g.w)
3439
3667
  continue;
3440
3668
  const gs = g.style ?? {};
3441
3669
  rc.rectangle(g.x, g.y, g.w, g.h, {
3442
- ...R,
3443
- roughness: 1.7,
3444
- bowing: 0.4,
3445
- seed: hashStr$1(g.id),
3670
+ ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
3446
3671
  fill: String(gs.fill ?? palette.groupFill),
3447
- fillStyle: "solid",
3672
+ fillStyle: 'solid',
3448
3673
  stroke: String(gs.stroke ?? palette.groupStroke),
3449
3674
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
3450
3675
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
3451
3676
  });
3452
- ctx.save();
3453
- ctx.font = "500 12px system-ui, sans-serif";
3454
- ctx.fillStyle = gs.color ? String(gs.color) : palette.groupLabel;
3455
- ctx.textAlign = "left";
3456
- ctx.fillText(g.label, g.x + 14, g.y + 16);
3457
- ctx.restore();
3677
+ // ── Group label: font, font-size, letter-spacing ─────
3678
+ // always left-anchored (single line)
3679
+ const gFontSize = Number(gs.fontSize ?? 12);
3680
+ const gFont = resolveStyleFont(gs, diagramFont);
3681
+ const gLetterSpacing = gs.letterSpacing;
3682
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
3683
+ drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
3458
3684
  }
3459
3685
  // ── Edges ─────────────────────────────────────────────────
3460
3686
  for (const e of sg.edges) {
@@ -3471,62 +3697,61 @@ var AIDiagram = (function (exports) {
3471
3697
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
3472
3698
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
3473
3699
  const HEAD = 13;
3474
- const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
3475
- const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
3476
- const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
3477
- const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
3700
+ const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
3701
+ const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
3702
+ const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
3703
+ const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
3478
3704
  rc.line(sx1, sy1, sx2, sy2, {
3479
- ...R,
3480
- roughness: 0.9,
3481
- seed: hashStr$1(e.from + e.to),
3705
+ ...R, roughness: 0.9, seed: hashStr$1(e.from + e.to),
3482
3706
  stroke: ecol,
3483
3707
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
3484
3708
  ...(dashed ? { strokeLineDash: [6, 5] } : {}),
3485
3709
  });
3486
3710
  const ang = Math.atan2(y2 - y1, x2 - x1);
3487
- if (arrowAt === "end" || arrowAt === "both")
3711
+ if (arrowAt === 'end' || arrowAt === 'both')
3488
3712
  drawArrowHead(rc, x2, y2, ang, ecol, hashStr$1(e.to));
3489
- if (arrowAt === "start" || arrowAt === "both")
3490
- drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + "back"));
3713
+ if (arrowAt === 'start' || arrowAt === 'both')
3714
+ drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + 'back'));
3491
3715
  if (e.label) {
3492
3716
  const mx = (x1 + x2) / 2 - ny * 14;
3493
3717
  const my = (y1 + y2) / 2 + nx * 14;
3718
+ // ── Edge label: font, font-size, letter-spacing ──
3719
+ // always center-anchored (single line)
3720
+ const eFontSize = Number(e.style?.fontSize ?? 11);
3721
+ const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
3722
+ const eLetterSpacing = e.style?.letterSpacing;
3494
3723
  ctx.save();
3495
- ctx.font = "400 11px system-ui, sans-serif";
3496
- ctx.textAlign = "center";
3724
+ ctx.font = `400 ${eFontSize}px ${eFont}`;
3497
3725
  const tw = ctx.measureText(e.label).width + 12;
3726
+ ctx.restore();
3498
3727
  ctx.fillStyle = palette.edgeLabelBg;
3499
3728
  ctx.fillRect(mx - tw / 2, my - 8, tw, 15);
3500
- ctx.fillStyle = palette.edgeLabelText;
3501
- ctx.fillText(e.label, mx, my + 3);
3502
- ctx.restore();
3729
+ drawText(ctx, e.label, mx, my + 3, eFontSize, 400, palette.edgeLabelText, 'center', eFont, eLetterSpacing);
3503
3730
  }
3504
3731
  }
3505
3732
  // ── Nodes ─────────────────────────────────────────────────
3506
3733
  for (const n of sg.nodes) {
3507
3734
  renderShape(rc, ctx, n, palette, R);
3508
- const s = n.style ?? {};
3509
- const fontSize = Number(s.fontSize ?? (n.shape === "text" ? 13 : 14));
3510
- const fontWeight = s.fontWeight ?? (n.shape === "text" ? 400 : 500);
3511
- const textColor = String(s.color ??
3512
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
3513
- ctx.save();
3514
- ctx.font = `${fontWeight} ${fontSize}px system-ui, sans-serif`;
3515
- ctx.fillStyle = textColor;
3516
- ctx.textAlign = "center";
3517
- ctx.textBaseline = "middle";
3518
- const lines = n.label.split("\n");
3519
- if (lines.length === 1) {
3520
- ctx.fillText(n.label, n.x + n.w / 2, n.y + n.h / 2);
3735
+ // ── Node / text typography ─────────────────────────
3736
+ // supports: font, font-size, letter-spacing, text-align, line-height
3737
+ const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
3738
+ const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
3739
+ const textColor = String(n.style?.color ??
3740
+ (n.shape === 'text' ? palette.edgeLabelText : palette.nodeText));
3741
+ const nodeFont = resolveStyleFont(n.style ?? {}, diagramFont);
3742
+ const textAlign = String(n.style?.textAlign ?? 'center');
3743
+ const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
3744
+ const letterSpacing = n.style?.letterSpacing;
3745
+ const textX = textAlign === 'left' ? n.x + 8
3746
+ : textAlign === 'right' ? n.x + n.w - 8
3747
+ : n.x + n.w / 2;
3748
+ const lines = n.label.split('\n');
3749
+ if (lines.length > 1) {
3750
+ drawMultilineText(ctx, lines, textX, n.y + n.h / 2, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
3521
3751
  }
3522
3752
  else {
3523
- const lineH = fontSize * 1.35;
3524
- const startY = n.y + n.h / 2 - ((lines.length - 1) * lineH) / 2;
3525
- lines.forEach((line, i) => {
3526
- ctx.fillText(line, n.x + n.w / 2, startY + i * lineH);
3527
- });
3753
+ drawText(ctx, n.label, textX, n.y + n.h / 2, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
3528
3754
  }
3529
- ctx.restore();
3530
3755
  }
3531
3756
  // ── Tables ────────────────────────────────────────────────
3532
3757
  for (const t of sg.tables) {
@@ -3535,65 +3760,53 @@ var AIDiagram = (function (exports) {
3535
3760
  const strk = String(gs.stroke ?? palette.tableStroke);
3536
3761
  const textCol = String(gs.color ?? palette.tableText);
3537
3762
  const pad = t.labelH;
3538
- // Outer border
3763
+ // ── Table-level font ────────────────────────────────
3764
+ // supports: font, font-size, letter-spacing (cells also support text-align)
3765
+ const tFontSize = Number(gs.fontSize ?? 12);
3766
+ const tFont = resolveStyleFont(gs, diagramFont);
3767
+ const tLetterSpacing = gs.letterSpacing;
3539
3768
  rc.rectangle(t.x, t.y, t.w, t.h, {
3540
- ...R,
3541
- seed: hashStr$1(t.id),
3542
- fill,
3543
- fillStyle: "solid",
3544
- stroke: strk,
3545
- strokeWidth: 1.5,
3769
+ ...R, seed: hashStr$1(t.id),
3770
+ fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.5,
3546
3771
  });
3547
- // Label strip separator
3548
3772
  rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
3549
- roughness: 0.6,
3550
- seed: hashStr$1(t.id + "l"),
3551
- stroke: strk,
3552
- strokeWidth: 1,
3773
+ roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
3553
3774
  });
3554
- // Label text
3555
- ctx.save();
3556
- ctx.font = "500 12px system-ui, sans-serif";
3557
- ctx.fillStyle = textCol;
3558
- ctx.textAlign = "left";
3559
- ctx.textBaseline = "middle";
3560
- ctx.fillText(t.label, t.x + 10, t.y + pad / 2);
3561
- ctx.restore();
3562
- // Rows
3775
+ // ── Table label: font, font-size, letter-spacing ────
3776
+ // always left-anchored
3777
+ drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
3563
3778
  let rowY = t.y + pad;
3564
3779
  for (const row of t.rows) {
3565
- const rh = row.kind === "header" ? t.headerH : t.rowH;
3566
- // Header background
3567
- if (row.kind === "header") {
3780
+ const rh = row.kind === 'header' ? t.headerH : t.rowH;
3781
+ if (row.kind === 'header') {
3568
3782
  ctx.fillStyle = palette.tableHeaderFill;
3569
3783
  ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
3570
3784
  }
3571
- // Row separator
3572
3785
  rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
3573
- roughness: 0.4,
3574
- seed: hashStr$1(t.id + rowY),
3575
- stroke: row.kind === "header" ? strk : palette.tableDivider,
3576
- strokeWidth: row.kind === "header" ? 1.2 : 0.6,
3786
+ roughness: 0.4, seed: hashStr$1(t.id + rowY),
3787
+ stroke: row.kind === 'header' ? strk : palette.tableDivider,
3788
+ strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
3577
3789
  });
3578
- // Cell text + column separators
3790
+ // ── Cell text: font, font-size, letter-spacing, text-align ──
3791
+ // header always centered; data rows respect gs.textAlign
3792
+ const cellAlignProp = (row.kind === 'header'
3793
+ ? 'center'
3794
+ : String(gs.textAlign ?? 'center'));
3795
+ const cellFw = row.kind === 'header' ? 600 : 400;
3796
+ const cellColor = row.kind === 'header'
3797
+ ? String(gs.color ?? palette.tableHeaderText)
3798
+ : textCol;
3579
3799
  let cx = t.x;
3580
3800
  row.cells.forEach((cell, i) => {
3581
3801
  const cw = t.colWidths[i] ?? 60;
3582
- const fw = row.kind === "header" ? 600 : 400;
3583
- ctx.save();
3584
- ctx.font = `${fw} 12px system-ui, sans-serif`;
3585
- ctx.fillStyle =
3586
- row.kind === "header" ? palette.tableHeaderText : textCol;
3587
- ctx.textAlign = "center";
3588
- ctx.textBaseline = "middle";
3589
- ctx.fillText(cell, cx + cw / 2, rowY + rh / 2);
3590
- ctx.restore();
3802
+ const cellX = cellAlignProp === 'left' ? cx + 6
3803
+ : cellAlignProp === 'right' ? cx + cw - 6
3804
+ : cx + cw / 2;
3805
+ drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
3591
3806
  if (i < row.cells.length - 1) {
3592
3807
  rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
3593
- roughness: 0.3,
3594
- seed: hashStr$1(t.id + "c" + i),
3595
- stroke: palette.tableDivider,
3596
- strokeWidth: 0.5,
3808
+ roughness: 0.3, seed: hashStr$1(t.id + 'c' + i),
3809
+ stroke: palette.tableDivider, strokeWidth: 0.5,
3597
3810
  });
3598
3811
  }
3599
3812
  cx += cw;
@@ -3608,44 +3821,37 @@ var AIDiagram = (function (exports) {
3608
3821
  const strk = String(gs.stroke ?? palette.noteStroke);
3609
3822
  const fold = 14;
3610
3823
  const { x, y, w, h } = n;
3611
- // Note body (folded corner polygon)
3612
3824
  rc.polygon([
3613
3825
  [x, y],
3614
3826
  [x + w - fold, y],
3615
3827
  [x + w, y + fold],
3616
3828
  [x + w, y + h],
3617
3829
  [x, y + h],
3618
- ], {
3619
- ...R,
3620
- seed: hashStr$1(n.id),
3621
- fill,
3622
- fillStyle: "solid",
3623
- stroke: strk,
3624
- strokeWidth: 1.2,
3625
- });
3626
- // Folded corner triangle
3830
+ ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.2 });
3627
3831
  rc.polygon([
3628
3832
  [x + w - fold, y],
3629
3833
  [x + w, y + fold],
3630
3834
  [x + w - fold, y + fold],
3631
- ], {
3632
- roughness: 0.4,
3633
- seed: hashStr$1(n.id + "f"),
3634
- fill: palette.noteFold,
3635
- fillStyle: "solid",
3636
- stroke: strk,
3637
- strokeWidth: 0.8,
3638
- });
3639
- // Text lines
3640
- ctx.save();
3641
- ctx.font = "400 12px system-ui, sans-serif";
3642
- ctx.fillStyle = String(gs.color ?? palette.noteText);
3643
- ctx.textAlign = "left";
3644
- ctx.textBaseline = "middle";
3645
- n.lines.forEach((line, i) => {
3646
- ctx.fillText(line, x + 12, y + 12 + i * 20 + 10);
3647
- });
3648
- ctx.restore();
3835
+ ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
3836
+ fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
3837
+ // ── Note typography ─────────────────────────────────
3838
+ // supports: font, font-size, letter-spacing, text-align, line-height
3839
+ const nFontSize = Number(gs.fontSize ?? 12);
3840
+ const nFont = resolveStyleFont(gs, diagramFont);
3841
+ const nLetterSpacing = gs.letterSpacing;
3842
+ const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
3843
+ const nTextAlign = String(gs.textAlign ?? 'left');
3844
+ const nColor = String(gs.color ?? palette.noteText);
3845
+ const nTextX = nTextAlign === 'right' ? x + w - fold - 6
3846
+ : nTextAlign === 'center' ? x + (w - fold) / 2
3847
+ : x + 12;
3848
+ if (n.lines.length > 1) {
3849
+ const blockCY = y + fold / 2 + (h - fold) / 2;
3850
+ drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
3851
+ }
3852
+ else {
3853
+ drawText(ctx, n.lines[0] ?? '', nTextX, y + h / 2, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
3854
+ }
3649
3855
  }
3650
3856
  // ── Charts ────────────────────────────────────────────────
3651
3857
  for (const c of sg.charts) {
@@ -3657,19 +3863,19 @@ var AIDiagram = (function (exports) {
3657
3863
  }, R);
3658
3864
  }
3659
3865
  }
3660
- // ── Export canvas to PNG blob ─────────────────────────────
3866
+ // ── Export helpers ─────────────────────────────────────────────────────────
3661
3867
  function canvasToPNGBlob(canvas) {
3662
3868
  return new Promise((resolve, reject) => {
3663
- canvas.toBlob((blob) => {
3869
+ canvas.toBlob(blob => {
3664
3870
  if (blob)
3665
3871
  resolve(blob);
3666
3872
  else
3667
- reject(new Error("Canvas toBlob failed"));
3668
- }, "image/png");
3873
+ reject(new Error('Canvas toBlob failed'));
3874
+ }, 'image/png');
3669
3875
  });
3670
3876
  }
3671
3877
  function canvasToPNGDataURL(canvas) {
3672
- return canvas.toDataURL("image/png");
3878
+ return canvas.toDataURL('image/png');
3673
3879
  }
3674
3880
 
3675
3881
  // ============================================================
@@ -4670,6 +4876,7 @@ var AIDiagram = (function (exports) {
4670
4876
 
4671
4877
  exports.ANIMATION_CSS = ANIMATION_CSS;
4672
4878
  exports.AnimationController = AnimationController;
4879
+ exports.BUILTIN_FONTS = BUILTIN_FONTS;
4673
4880
  exports.EventEmitter = EventEmitter;
4674
4881
  exports.PALETTES = PALETTES;
4675
4882
  exports.ParseError = ParseError;
@@ -4693,12 +4900,15 @@ var AIDiagram = (function (exports) {
4693
4900
  exports.layout = layout;
4694
4901
  exports.lerp = lerp;
4695
4902
  exports.listThemes = listThemes;
4903
+ exports.loadFont = loadFont;
4696
4904
  exports.nodeMap = nodeMap;
4697
4905
  exports.parse = parse;
4698
4906
  exports.parseHex = parseHex;
4907
+ exports.registerFont = registerFont;
4699
4908
  exports.render = render;
4700
4909
  exports.renderToCanvas = renderToCanvas;
4701
4910
  exports.renderToSVG = renderToSVG;
4911
+ exports.resolveFont = resolveFont;
4702
4912
  exports.resolvePalette = resolvePalette;
4703
4913
  exports.sleep = sleep;
4704
4914
  exports.svgToPNGDataURL = svgToPNGDataURL;