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

Potentially problematic release.


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

@@ -25,6 +25,7 @@ var AIDiagram = (function (exports) {
25
25
  "step",
26
26
  "config",
27
27
  "theme",
28
+ "bare",
28
29
  "bar-chart",
29
30
  "line-chart",
30
31
  "pie-chart",
@@ -64,6 +65,7 @@ var AIDiagram = (function (exports) {
64
65
  "dag",
65
66
  "tree",
66
67
  "force",
68
+ "markdown",
67
69
  ]);
68
70
  const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
69
71
  // Characters that can start an arrow pattern — used to decide whether a '-'
@@ -101,6 +103,23 @@ var AIDiagram = (function (exports) {
101
103
  i++;
102
104
  continue;
103
105
  }
106
+ if (ch === '"' && peek(1) === '"' && peek(2) === '"') {
107
+ i += 3; // skip opening """
108
+ let raw = "";
109
+ while (i < src.length) {
110
+ if (src[i] === '"' && src[i + 1] === '"' && src[i + 2] === '"') {
111
+ i += 3; // skip closing """
112
+ break;
113
+ }
114
+ if (src[i] === "\n") {
115
+ line++;
116
+ lineStart = i + 1;
117
+ }
118
+ raw += src[i++];
119
+ }
120
+ add("STRING_BLOCK", raw);
121
+ continue;
122
+ }
104
123
  // Strings
105
124
  if (ch === '"' || ch === "'") {
106
125
  const q = ch;
@@ -251,6 +270,18 @@ var AIDiagram = (function (exports) {
251
270
  s.fontSize = parseFloat(p["font-size"]);
252
271
  if (p["font-weight"])
253
272
  s.fontWeight = p["font-weight"];
273
+ if (p["text-align"])
274
+ s.textAlign = p["text-align"];
275
+ if (p.padding)
276
+ s.padding = parseFloat(p.padding);
277
+ if (p["vertical-align"])
278
+ s.verticalAlign = p["vertical-align"];
279
+ if (p["line-height"])
280
+ s.lineHeight = parseFloat(p["line-height"]);
281
+ if (p["letter-spacing"])
282
+ s.letterSpacing = parseFloat(p["letter-spacing"]);
283
+ if (p.font)
284
+ s.font = p.font;
254
285
  if (p["dash"]) {
255
286
  const parts = p["dash"]
256
287
  .split(",")
@@ -288,6 +319,7 @@ var AIDiagram = (function (exports) {
288
319
  notes: [],
289
320
  charts: [],
290
321
  tables: [],
322
+ markdowns: [],
291
323
  styles: {},
292
324
  themes: {},
293
325
  config: {},
@@ -298,6 +330,7 @@ var AIDiagram = (function (exports) {
298
330
  const noteIds = new Set();
299
331
  const chartIds = new Set();
300
332
  const groupIds = new Set();
333
+ const markdownIds = new Set();
301
334
  let i = 0;
302
335
  const cur = () => flat[i] ?? flat[flat.length - 1];
303
336
  const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
@@ -477,6 +510,8 @@ var AIDiagram = (function (exports) {
477
510
  label: rawLabel.replace(/\\n/g, "\n"),
478
511
  theme: props.theme,
479
512
  style: propsToStyle(props),
513
+ ...(props.width ? { width: parseFloat(props.width) } : {}),
514
+ ...(props.height ? { height: parseFloat(props.height) } : {}),
480
515
  };
481
516
  }
482
517
  // ── parseGroup ───────────────────────────────────────────
@@ -509,7 +544,7 @@ var AIDiagram = (function (exports) {
509
544
  const group = {
510
545
  kind: "group",
511
546
  id,
512
- label: props.label ?? id,
547
+ label: props.label ?? "",
513
548
  children: [],
514
549
  layout: props.layout,
515
550
  columns: props.columns !== undefined ? parseInt(props.columns, 10) : undefined,
@@ -535,8 +570,18 @@ var AIDiagram = (function (exports) {
535
570
  break;
536
571
  const v = cur().value;
537
572
  // ── Nested group ──────────────────────────────────
538
- if (v === "group") {
573
+ if (v === "group" || v === "bare") {
574
+ const isBare = v === "bare";
539
575
  const nested = parseGroup();
576
+ if (isBare) {
577
+ nested.label = "";
578
+ nested.style = {
579
+ ...nested.style,
580
+ fill: nested.style?.fill ?? "none",
581
+ stroke: nested.style?.stroke ?? "none",
582
+ strokeWidth: nested.style?.strokeWidth ?? 0,
583
+ };
584
+ }
540
585
  ast.groups.push(nested);
541
586
  groupIds.add(nested.id);
542
587
  group.children.push({ kind: "group", id: nested.id });
@@ -558,6 +603,21 @@ var AIDiagram = (function (exports) {
558
603
  group.children.push({ kind: "note", id: note.id });
559
604
  continue;
560
605
  }
606
+ // ── Markdown ───────────────────────────────────────
607
+ if (v === "markdown") {
608
+ const md = parseMarkdown(id);
609
+ ast.markdowns.push(md);
610
+ markdownIds.add(md.id);
611
+ group.children.push({ kind: "markdown", id: md.id });
612
+ continue;
613
+ }
614
+ if (v === "bare") {
615
+ // treat exactly like 'group' but inject defaults
616
+ const grp = parseGroup(); // reuse parseGroup
617
+ grp.label = "";
618
+ grp.style = { ...grp.style, stroke: "none", fill: "none" };
619
+ // rest is identical to group handling
620
+ }
561
621
  // ── Chart ──────────────────────────────────────────
562
622
  if (CHART_TYPES.includes(v)) {
563
623
  const chart = parseChart(v);
@@ -601,71 +661,71 @@ var AIDiagram = (function (exports) {
601
661
  function parseStep() {
602
662
  skip();
603
663
  const toks = lineTokens();
604
- const action = (toks[0]?.value ?? 'highlight');
605
- let target = toks[1]?.value ?? '';
606
- if (toks[2]?.type === 'ARROW' && toks[3]) {
664
+ const action = (toks[0]?.value ?? "highlight");
665
+ let target = toks[1]?.value ?? "";
666
+ if (toks[2]?.type === "ARROW" && toks[3]) {
607
667
  target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
608
668
  }
609
- const step = { kind: 'step', action, target };
669
+ const step = { kind: "step", action, target };
610
670
  for (let j = 2; j < toks.length; j++) {
611
671
  const k = toks[j]?.value;
612
672
  const eq = toks[j + 1];
613
673
  const vt = toks[j + 2];
614
674
  // key=value form
615
- if (eq?.type === 'EQUALS' && vt) {
616
- if (k === 'dx') {
675
+ if (eq?.type === "EQUALS" && vt) {
676
+ if (k === "dx") {
617
677
  step.dx = parseFloat(vt.value);
618
678
  j += 2;
619
679
  continue;
620
680
  }
621
- if (k === 'dy') {
681
+ if (k === "dy") {
622
682
  step.dy = parseFloat(vt.value);
623
683
  j += 2;
624
684
  continue;
625
685
  }
626
- if (k === 'duration') {
686
+ if (k === "duration") {
627
687
  step.duration = parseFloat(vt.value);
628
688
  j += 2;
629
689
  continue;
630
690
  }
631
- if (k === 'delay') {
691
+ if (k === "delay") {
632
692
  step.delay = parseFloat(vt.value);
633
693
  j += 2;
634
694
  continue;
635
695
  }
636
- if (k === 'factor') {
696
+ if (k === "factor") {
637
697
  step.factor = parseFloat(vt.value);
638
698
  j += 2;
639
699
  continue;
640
700
  }
641
- if (k === 'deg') {
701
+ if (k === "deg") {
642
702
  step.deg = parseFloat(vt.value);
643
703
  j += 2;
644
704
  continue;
645
705
  }
646
- if (k === 'fill') {
706
+ if (k === "fill") {
647
707
  step.value = vt.value;
648
708
  j += 2;
649
709
  continue;
650
710
  }
651
- if (k === 'color') {
711
+ if (k === "color") {
652
712
  step.value = vt.value;
653
713
  j += 2;
654
714
  continue;
655
715
  }
656
716
  }
657
717
  // bare key value (legacy)
658
- if (k === 'delay' && eq?.type === 'NUMBER') {
718
+ if (k === "delay" && eq?.type === "NUMBER") {
659
719
  step.delay = parseFloat(eq.value);
660
720
  j++;
661
721
  continue;
662
722
  }
663
- if (k === 'duration' && eq?.type === 'NUMBER') {
723
+ if (k === "duration" && eq?.type === "NUMBER") {
664
724
  step.duration = parseFloat(eq.value);
665
725
  j++;
666
726
  continue;
667
727
  }
668
- if (k === 'trigger') {
728
+ if (k === "trigger") {
669
729
  step.trigger = eq?.value;
670
730
  j++;
671
731
  continue;
@@ -865,6 +925,40 @@ var AIDiagram = (function (exports) {
865
925
  skip();
866
926
  return table;
867
927
  }
928
+ // ── parseMarkdown ─────────────────────────────────────────
929
+ function parseMarkdown(groupId) {
930
+ skip(); // 'markdown'
931
+ const toks = lineTokens();
932
+ let id = groupId ? groupId + "_" + uid("md") : uid("md");
933
+ if (toks[0])
934
+ id = toks[0].value;
935
+ const props = {};
936
+ let j = 1;
937
+ while (j < toks.length - 1) {
938
+ const k = toks[j], eq = toks[j + 1];
939
+ if (eq?.type === "EQUALS" && j + 2 < toks.length) {
940
+ props[k.value] = toks[j + 2].value;
941
+ j += 3;
942
+ }
943
+ else
944
+ j++;
945
+ }
946
+ skipNL();
947
+ let content = "";
948
+ if (cur().type === "STRING_BLOCK") {
949
+ content = cur().value;
950
+ skip();
951
+ }
952
+ return {
953
+ kind: "markdown",
954
+ id,
955
+ content: content.trim(),
956
+ width: props.width ? parseFloat(props.width) : undefined,
957
+ height: props.height ? parseFloat(props.height) : undefined,
958
+ theme: props.theme,
959
+ style: propsToStyle(props),
960
+ };
961
+ }
868
962
  // ── Main parse loop ─────────────────────────────────────
869
963
  skipNL();
870
964
  if (cur().value === "diagram")
@@ -978,8 +1072,18 @@ var AIDiagram = (function (exports) {
978
1072
  continue;
979
1073
  }
980
1074
  // group
981
- if (v === "group") {
1075
+ if (v === "group" || v === "bare") {
1076
+ const isBare = v === "bare";
982
1077
  const grp = parseGroup();
1078
+ if (isBare) {
1079
+ grp.label = "";
1080
+ grp.style = {
1081
+ ...grp.style,
1082
+ fill: grp.style?.fill ?? "none",
1083
+ stroke: grp.style?.stroke ?? "none",
1084
+ strokeWidth: grp.style?.strokeWidth ?? 0,
1085
+ };
1086
+ }
983
1087
  ast.groups.push(grp);
984
1088
  groupIds.add(grp.id);
985
1089
  ast.rootOrder.push({ kind: "group", id: grp.id });
@@ -1014,6 +1118,13 @@ var AIDiagram = (function (exports) {
1014
1118
  ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
1015
1119
  continue;
1016
1120
  }
1121
+ if (v === "markdown") {
1122
+ const md = parseMarkdown();
1123
+ ast.markdowns.push(md);
1124
+ markdownIds.add(md.id);
1125
+ ast.rootOrder.push({ kind: "markdown", id: md.id });
1126
+ continue;
1127
+ }
1017
1128
  // edge: A -> B (MUST come before shape check)
1018
1129
  if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
1019
1130
  const nextTok = flat[i + 1];
@@ -1063,11 +1174,94 @@ var AIDiagram = (function (exports) {
1063
1174
  node.style = { ...ast.styles[node.id], ...node.style };
1064
1175
  }
1065
1176
  }
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
1177
  return ast;
1069
1178
  }
1070
1179
 
1180
+ // ============================================================
1181
+ // sketchmark — Markdown inline parser
1182
+ // Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
1183
+ // ============================================================
1184
+ // ── Font sizes per line kind ──────────────────────────────
1185
+ const LINE_FONT_SIZE = {
1186
+ h1: 40,
1187
+ h2: 28,
1188
+ h3: 20,
1189
+ p: 15,
1190
+ blank: 0,
1191
+ };
1192
+ const LINE_FONT_WEIGHT = {
1193
+ h1: 700,
1194
+ h2: 600,
1195
+ h3: 600,
1196
+ p: 400,
1197
+ blank: 400,
1198
+ };
1199
+ // Spacing below each line kind (px)
1200
+ const LINE_SPACING = {
1201
+ h1: 52,
1202
+ h2: 38,
1203
+ h3: 28,
1204
+ p: 22,
1205
+ blank: 10,
1206
+ };
1207
+ // ── Parse a full markdown string into lines ───────────────
1208
+ function parseMarkdownContent(content) {
1209
+ const raw = content.split('\n');
1210
+ const lines = [];
1211
+ for (const line of raw) {
1212
+ const t = line.trim();
1213
+ if (!t) {
1214
+ lines.push({ kind: 'blank', runs: [] });
1215
+ continue;
1216
+ }
1217
+ if (t.startsWith('### ')) {
1218
+ lines.push({ kind: 'h3', runs: parseInline(t.slice(4)) });
1219
+ }
1220
+ else if (t.startsWith('## ')) {
1221
+ lines.push({ kind: 'h2', runs: parseInline(t.slice(3)) });
1222
+ }
1223
+ else if (t.startsWith('# ')) {
1224
+ lines.push({ kind: 'h1', runs: parseInline(t.slice(2)) });
1225
+ }
1226
+ else {
1227
+ lines.push({ kind: 'p', runs: parseInline(t) });
1228
+ }
1229
+ }
1230
+ // strip leading/trailing blank lines
1231
+ while (lines.length && lines[0].kind === 'blank')
1232
+ lines.shift();
1233
+ while (lines.length && lines[lines.length - 1].kind === 'blank')
1234
+ lines.pop();
1235
+ return lines;
1236
+ }
1237
+ // ── Parse inline bold/italic spans ───────────────────────
1238
+ function parseInline(text) {
1239
+ const runs = [];
1240
+ // Order matters: check ** before *
1241
+ const re = /(\*\*(.+?)\*\*|\*(.+?)\*|[^*]+)/g;
1242
+ let m;
1243
+ while ((m = re.exec(text)) !== null) {
1244
+ if (m[0].startsWith('**')) {
1245
+ runs.push({ text: m[2], bold: true });
1246
+ }
1247
+ else if (m[0].startsWith('*')) {
1248
+ runs.push({ text: m[3], italic: true });
1249
+ }
1250
+ else {
1251
+ if (m[0])
1252
+ runs.push({ text: m[0] });
1253
+ }
1254
+ }
1255
+ return runs;
1256
+ }
1257
+ // ── Calculate natural height of a parsed block ────────────
1258
+ function calcMarkdownHeight(lines, pad = 16) {
1259
+ let h = pad * 2; // top + bottom
1260
+ for (const line of lines)
1261
+ h += LINE_SPACING[line.kind];
1262
+ return h;
1263
+ }
1264
+
1071
1265
  // ============================================================
1072
1266
  // sketchmark — Scene Graph
1073
1267
  // ============================================================
@@ -1140,6 +1334,8 @@ var AIDiagram = (function (exports) {
1140
1334
  y: 0,
1141
1335
  w: 0,
1142
1336
  h: 0,
1337
+ width: n.width,
1338
+ height: n.height,
1143
1339
  };
1144
1340
  });
1145
1341
  const charts = ast.charts.map((c) => {
@@ -1156,6 +1352,18 @@ var AIDiagram = (function (exports) {
1156
1352
  h: c.height ?? 240,
1157
1353
  };
1158
1354
  });
1355
+ const markdowns = (ast.markdowns ?? []).map(m => {
1356
+ const themeStyle = m.theme ? (ast.themes[m.theme] ?? {}) : {};
1357
+ return {
1358
+ id: m.id,
1359
+ content: m.content,
1360
+ lines: parseMarkdownContent(m.content),
1361
+ style: { ...themeStyle, ...m.style },
1362
+ width: m.width,
1363
+ height: m.height,
1364
+ x: 0, y: 0, w: 0, h: 0,
1365
+ };
1366
+ });
1159
1367
  // Set parentId for nested groups
1160
1368
  for (const g of groups) {
1161
1369
  for (const child of g.children) {
@@ -1186,6 +1394,7 @@ var AIDiagram = (function (exports) {
1186
1394
  tables,
1187
1395
  notes,
1188
1396
  charts,
1397
+ markdowns,
1189
1398
  animation: { steps: ast.steps, currentStep: -1 },
1190
1399
  styles: ast.styles,
1191
1400
  config: ast.config,
@@ -1210,6 +1419,9 @@ var AIDiagram = (function (exports) {
1210
1419
  function chartMap(sg) {
1211
1420
  return new Map(sg.charts.map((c) => [c.id, c]));
1212
1421
  }
1422
+ function markdownMap(sg) {
1423
+ return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
1424
+ }
1213
1425
 
1214
1426
  // ============================================================
1215
1427
  // sketchmark — Layout Engine (Flexbox-style, recursive)
@@ -1250,30 +1462,40 @@ var AIDiagram = (function (exports) {
1250
1462
  n.h = n.height;
1251
1463
  const labelW = Math.round(n.label.length * FONT_PX_PER_CHAR + BASE_PAD);
1252
1464
  switch (n.shape) {
1253
- case 'circle':
1465
+ case "circle":
1254
1466
  n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
1255
1467
  n.h = n.h || n.w;
1256
1468
  break;
1257
- case 'diamond':
1469
+ case "diamond":
1258
1470
  n.w = n.w || Math.max(130, Math.min(MAX_W, labelW + 30));
1259
1471
  n.h = n.h || Math.max(62, n.w * 0.46);
1260
1472
  break;
1261
- case 'hexagon':
1473
+ case "hexagon":
1262
1474
  n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
1263
1475
  n.h = n.h || Math.max(54, n.w * 0.44);
1264
1476
  break;
1265
- case 'triangle':
1477
+ case "triangle":
1266
1478
  n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
1267
- n.h = n.h || Math.max(64, n.w * 0.60);
1479
+ n.h = n.h || Math.max(64, n.w * 0.6);
1268
1480
  break;
1269
- case 'cylinder':
1481
+ case "cylinder":
1270
1482
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1271
1483
  n.h = n.h || 66;
1272
1484
  break;
1273
- case 'parallelogram':
1485
+ case "parallelogram":
1274
1486
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
1275
1487
  n.h = n.h || 50;
1276
1488
  break;
1489
+ case "text": {
1490
+ // read fontSize from style if set, otherwise use default
1491
+ const fontSize = Number(n.style?.fontSize ?? 13);
1492
+ 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);
1497
+ break;
1498
+ }
1277
1499
  default:
1278
1500
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1279
1501
  n.h = n.h || 52;
@@ -1281,9 +1503,13 @@ var AIDiagram = (function (exports) {
1281
1503
  }
1282
1504
  }
1283
1505
  function sizeNote(n) {
1284
- const maxChars = Math.max(...n.lines.map(l => l.length));
1506
+ const maxChars = Math.max(...n.lines.map((l) => l.length));
1285
1507
  n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
1286
1508
  n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
1509
+ if (n.width && n.w < n.width)
1510
+ n.w = n.width; // ← add
1511
+ if (n.height && n.h < n.height)
1512
+ n.h = n.height; // ← add
1287
1513
  }
1288
1514
  // ── Table auto-sizing ─────────────────────────────────────
1289
1515
  function sizeTable(t) {
@@ -1293,7 +1519,7 @@ var AIDiagram = (function (exports) {
1293
1519
  t.h = labelH + rowH;
1294
1520
  return;
1295
1521
  }
1296
- const numCols = Math.max(...rows.map(r => r.cells.length));
1522
+ const numCols = Math.max(...rows.map((r) => r.cells.length));
1297
1523
  const colW = Array(numCols).fill(MIN_COL_W);
1298
1524
  for (const row of rows) {
1299
1525
  row.cells.forEach((cell, i) => {
@@ -1302,110 +1528,128 @@ var AIDiagram = (function (exports) {
1302
1528
  }
1303
1529
  t.colWidths = colW;
1304
1530
  t.w = colW.reduce((s, w) => s + w, 0);
1305
- const nHeader = rows.filter(r => r.kind === 'header').length;
1306
- const nData = rows.filter(r => r.kind === 'data').length;
1531
+ const nHeader = rows.filter((r) => r.kind === "header").length;
1532
+ const nData = rows.filter((r) => r.kind === "data").length;
1307
1533
  t.h = labelH + nHeader * headerH + nData * rowH;
1308
1534
  }
1309
1535
  function sizeChart(c) {
1310
1536
  c.w = c.w || 320;
1311
1537
  c.h = c.h || 240;
1312
1538
  }
1539
+ function sizeMarkdown(m) {
1540
+ const pad = Number(m.style?.padding ?? 16);
1541
+ m.w = m.width ?? 400;
1542
+ m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
1543
+ }
1313
1544
  // ── Item size helpers ─────────────────────────────────────
1314
- function iW(r, nm, gm, tm, ntm, cm) {
1315
- if (r.kind === 'node')
1545
+ function iW(r, nm, gm, tm, ntm, cm, mdm) {
1546
+ if (r.kind === "node")
1316
1547
  return nm.get(r.id).w;
1317
- if (r.kind === 'table')
1548
+ if (r.kind === "table")
1318
1549
  return tm.get(r.id).w;
1319
- if (r.kind === 'note')
1550
+ if (r.kind === "note")
1320
1551
  return ntm.get(r.id).w;
1321
- if (r.kind === 'chart')
1552
+ if (r.kind === "chart")
1322
1553
  return cm.get(r.id).w;
1554
+ if (r.kind === "markdown")
1555
+ return mdm.get(r.id).w;
1323
1556
  return gm.get(r.id).w;
1324
1557
  }
1325
- function iH(r, nm, gm, tm, ntm, cm) {
1326
- if (r.kind === 'node')
1558
+ function iH(r, nm, gm, tm, ntm, cm, mdm) {
1559
+ if (r.kind === "node")
1327
1560
  return nm.get(r.id).h;
1328
- if (r.kind === 'table')
1561
+ if (r.kind === "table")
1329
1562
  return tm.get(r.id).h;
1330
- if (r.kind === 'note')
1563
+ if (r.kind === "note")
1331
1564
  return ntm.get(r.id).h;
1332
- if (r.kind === 'chart')
1565
+ if (r.kind === "chart")
1333
1566
  return cm.get(r.id).h;
1567
+ if (r.kind === "markdown")
1568
+ return mdm.get(r.id).h;
1334
1569
  return gm.get(r.id).h;
1335
1570
  }
1336
- function setPos(r, x, y, nm, gm, tm, ntm, cm) {
1337
- if (r.kind === 'node') {
1571
+ function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
1572
+ if (r.kind === "node") {
1338
1573
  const n = nm.get(r.id);
1339
1574
  n.x = Math.round(x);
1340
1575
  n.y = Math.round(y);
1341
1576
  return;
1342
1577
  }
1343
- if (r.kind === 'table') {
1578
+ if (r.kind === "table") {
1344
1579
  const t = tm.get(r.id);
1345
1580
  t.x = Math.round(x);
1346
1581
  t.y = Math.round(y);
1347
1582
  return;
1348
1583
  }
1349
- if (r.kind === 'note') {
1584
+ if (r.kind === "note") {
1350
1585
  const nt = ntm.get(r.id);
1351
1586
  nt.x = Math.round(x);
1352
1587
  nt.y = Math.round(y);
1353
1588
  return;
1354
1589
  }
1355
- if (r.kind === 'chart') {
1590
+ if (r.kind === "chart") {
1356
1591
  const c = cm.get(r.id);
1357
1592
  c.x = Math.round(x);
1358
1593
  c.y = Math.round(y);
1359
1594
  return;
1360
1595
  }
1596
+ if (r.kind === "markdown") {
1597
+ const md = mdm.get(r.id);
1598
+ md.x = Math.round(x);
1599
+ md.y = Math.round(y);
1600
+ return;
1601
+ }
1361
1602
  const g = gm.get(r.id);
1362
1603
  g.x = Math.round(x);
1363
1604
  g.y = Math.round(y);
1364
1605
  }
1365
1606
  // ── Pass 1: Measure (bottom-up) ───────────────────────────
1366
1607
  // Recursively computes w, h for a group from its children's sizes.
1367
- function measure(g, nm, gm, tm, ntm, cm) {
1608
+ function measure(g, nm, gm, tm, ntm, cm, mdm) {
1368
1609
  // Recurse into nested groups first; size tables before reading their dims
1369
1610
  for (const r of g.children) {
1370
- if (r.kind === 'group')
1371
- measure(gm.get(r.id), nm, gm, tm, ntm, cm);
1372
- if (r.kind === 'table')
1611
+ if (r.kind === "group")
1612
+ measure(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
1613
+ if (r.kind === "table")
1373
1614
  sizeTable(tm.get(r.id));
1374
- if (r.kind === 'note')
1615
+ if (r.kind === "note")
1375
1616
  sizeNote(ntm.get(r.id));
1376
- if (r.kind === 'chart')
1617
+ if (r.kind === "chart")
1377
1618
  sizeChart(cm.get(r.id));
1619
+ if (r.kind === "markdown")
1620
+ sizeMarkdown(mdm.get(r.id));
1378
1621
  }
1379
1622
  const { padding: pad, gap, columns, layout } = g;
1380
1623
  const kids = g.children;
1624
+ const labelH = g.label ? GROUP_LABEL_H : 0;
1381
1625
  if (!kids.length) {
1382
1626
  g.w = pad * 2;
1383
- g.h = pad * 2 + GROUP_LABEL_H;
1627
+ g.h = pad * 2 + labelH;
1384
1628
  if (g.width && g.w < g.width)
1385
1629
  g.w = g.width;
1386
1630
  if (g.height && g.h < g.height)
1387
1631
  g.h = g.height;
1388
1632
  return;
1389
1633
  }
1390
- const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1391
- const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1634
+ const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1635
+ const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1392
1636
  const n = kids.length;
1393
- if (layout === 'row') {
1637
+ if (layout === "row") {
1394
1638
  g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
1395
- g.h = Math.max(...hs) + pad * 2 + GROUP_LABEL_H;
1639
+ g.h = Math.max(...hs) + pad * 2 + labelH;
1396
1640
  }
1397
- else if (layout === 'grid') {
1641
+ else if (layout === "grid") {
1398
1642
  const cols = Math.max(1, columns);
1399
1643
  const rows = Math.ceil(n / cols);
1400
1644
  const cellW = Math.max(...ws);
1401
1645
  const cellH = Math.max(...hs);
1402
1646
  g.w = cols * cellW + (cols - 1) * gap + pad * 2;
1403
- g.h = rows * cellH + (rows - 1) * gap + pad * 2 + GROUP_LABEL_H;
1647
+ g.h = rows * cellH + (rows - 1) * gap + pad * 2 + labelH;
1404
1648
  }
1405
1649
  else {
1406
1650
  // column (default)
1407
1651
  g.w = Math.max(...ws) + pad * 2;
1408
- g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + GROUP_LABEL_H;
1652
+ g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + labelH;
1409
1653
  }
1410
1654
  // Clamp to minWidth / minHeight — this is what gives distribute() free
1411
1655
  // space to work with for justify=center/end/space-between/space-around
@@ -1420,21 +1664,32 @@ var AIDiagram = (function (exports) {
1420
1664
  const totalSize = sizes.reduce((s, v) => s + v, 0);
1421
1665
  const gapCount = n - 1;
1422
1666
  switch (justify) {
1423
- case 'center': {
1667
+ case "center": {
1424
1668
  const total = totalSize + gap * gapCount;
1425
- return { start: Math.max(0, (contentSize - total) / 2), gaps: Array(gapCount).fill(gap) };
1669
+ return {
1670
+ start: Math.max(0, (contentSize - total) / 2),
1671
+ gaps: Array(gapCount).fill(gap),
1672
+ };
1426
1673
  }
1427
- case 'end': {
1674
+ case "end": {
1428
1675
  const total = totalSize + gap * gapCount;
1429
- return { start: Math.max(0, contentSize - total), gaps: Array(gapCount).fill(gap) };
1676
+ return {
1677
+ start: Math.max(0, contentSize - total),
1678
+ gaps: Array(gapCount).fill(gap),
1679
+ };
1430
1680
  }
1431
- case 'space-between': {
1432
- const g2 = gapCount > 0 ? Math.max(gap, (contentSize - totalSize) / gapCount) : gap;
1681
+ case "space-between": {
1682
+ const g2 = gapCount > 0
1683
+ ? Math.max(gap, (contentSize - totalSize) / gapCount)
1684
+ : gap;
1433
1685
  return { start: 0, gaps: Array(gapCount).fill(g2) };
1434
1686
  }
1435
- case 'space-around': {
1687
+ case "space-around": {
1436
1688
  const space = n > 0 ? (contentSize - totalSize) / n : gap;
1437
- return { start: Math.max(0, space / 2), gaps: Array(gapCount).fill(Math.max(gap, space)) };
1689
+ return {
1690
+ start: Math.max(0, space / 2),
1691
+ gaps: Array(gapCount).fill(Math.max(gap, space)),
1692
+ };
1438
1693
  }
1439
1694
  default: // start
1440
1695
  return { start: 0, gaps: Array(gapCount).fill(gap) };
@@ -1442,70 +1697,73 @@ var AIDiagram = (function (exports) {
1442
1697
  }
1443
1698
  // ── Pass 2: Place (top-down) ──────────────────────────────
1444
1699
  // Assigns x, y to each child. Assumes g.x / g.y already set by parent.
1445
- function place(g, nm, gm, tm, ntm, cm) {
1700
+ function place(g, nm, gm, tm, ntm, cm, mdm) {
1446
1701
  const { padding: pad, gap, columns, layout, align, justify } = g;
1702
+ const labelH = g.label ? GROUP_LABEL_H : 0;
1447
1703
  const contentX = g.x + pad;
1448
- const contentY = g.y + GROUP_LABEL_H + pad;
1704
+ const contentY = g.y + labelH + pad;
1449
1705
  const contentW = g.w - pad * 2;
1450
- const contentH = g.h - pad * 2 - GROUP_LABEL_H;
1706
+ const contentH = g.h - pad * 2 - labelH;
1451
1707
  const kids = g.children;
1452
1708
  if (!kids.length)
1453
1709
  return;
1454
- if (layout === 'row') {
1455
- const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1456
- const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1710
+ if (layout === "row") {
1711
+ const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1712
+ const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1457
1713
  const maxH = Math.max(...hs);
1458
1714
  const { start, gaps } = distribute(ws, contentW, gap, justify);
1459
1715
  let x = contentX + start;
1460
1716
  for (let i = 0; i < kids.length; i++) {
1461
1717
  let y;
1462
1718
  switch (align) {
1463
- case 'center':
1719
+ case "center":
1464
1720
  y = contentY + (maxH - hs[i]) / 2;
1465
1721
  break;
1466
- case 'end':
1722
+ case "end":
1467
1723
  y = contentY + maxH - hs[i];
1468
1724
  break;
1469
- default: y = contentY;
1725
+ default:
1726
+ y = contentY;
1470
1727
  }
1471
- setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
1728
+ setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
1472
1729
  x += ws[i] + (i < gaps.length ? gaps[i] : 0);
1473
1730
  }
1474
1731
  }
1475
- else if (layout === 'grid') {
1732
+ else if (layout === "grid") {
1476
1733
  const cols = Math.max(1, columns);
1477
- const cellW = Math.max(...kids.map(r => iW(r, nm, gm, tm, ntm, cm)));
1478
- const cellH = Math.max(...kids.map(r => iH(r, nm, gm, tm, ntm, cm)));
1734
+ const cellW = Math.max(...kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm)));
1735
+ const cellH = Math.max(...kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm)));
1479
1736
  kids.forEach((ref, i) => {
1480
- setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm);
1737
+ setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm, mdm);
1481
1738
  });
1482
1739
  }
1483
1740
  else {
1484
1741
  // column (default)
1485
- const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1486
- const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1742
+ const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1743
+ const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1487
1744
  const maxW = Math.max(...ws);
1488
1745
  const { start, gaps } = distribute(hs, contentH, gap, justify);
1489
1746
  let y = contentY + start;
1490
1747
  for (let i = 0; i < kids.length; i++) {
1491
1748
  let x;
1492
1749
  switch (align) {
1493
- case 'center':
1750
+ case "center":
1494
1751
  x = contentX + (maxW - ws[i]) / 2;
1495
1752
  break;
1496
- case 'end':
1753
+ case "end":
1497
1754
  x = contentX + maxW - ws[i];
1498
1755
  break;
1499
- default: x = contentX;
1756
+ default:
1757
+ x = contentX;
1500
1758
  }
1501
- setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
1759
+ setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
1502
1760
  y += hs[i] + (i < gaps.length ? gaps[i] : 0);
1503
1761
  }
1504
1762
  }
1505
1763
  // Recurse into nested groups
1506
1764
  for (const r of kids) {
1507
- if (r.kind === 'group')
1508
- place(gm.get(r.id), nm, gm, tm, ntm, cm);
1765
+ if (r.kind === "group")
1766
+ place(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
1509
1767
  }
1510
1768
  }
1511
1769
  // ── Edge routing ──────────────────────────────────────────
@@ -1515,9 +1773,9 @@ var AIDiagram = (function (exports) {
1515
1773
  const dx = ox - cx, dy = oy - cy;
1516
1774
  if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
1517
1775
  return [cx, cy];
1518
- if (n.shape === 'circle') {
1776
+ if (n.shape === "circle") {
1519
1777
  const r = n.w * 0.44, len = Math.sqrt(dx * dx + dy * dy);
1520
- return [cx + dx / len * r, cy + dy / len * r];
1778
+ return [cx + (dx / len) * r, cy + (dy / len) * r];
1521
1779
  }
1522
1780
  const hw = n.w / 2 - 2, hh = n.h / 2 - 2;
1523
1781
  const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
@@ -1562,9 +1820,12 @@ var AIDiagram = (function (exports) {
1562
1820
  }
1563
1821
  function connPt(src, dstCX, dstCY) {
1564
1822
  // SceneNode has a .shape field; use the existing connPoint for it
1565
- if ('shape' in src && src.shape) {
1823
+ if ("shape" in src && src.shape) {
1566
1824
  return connPoint(src, {
1567
- x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
1825
+ x: dstCX - 1,
1826
+ y: dstCY - 1,
1827
+ w: 2,
1828
+ h: 2});
1568
1829
  }
1569
1830
  return rectConnPoint$2(src.x, src.y, src.w, src.h, dstCX, dstCY);
1570
1831
  }
@@ -1577,84 +1838,84 @@ var AIDiagram = (function (exports) {
1577
1838
  }
1578
1839
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
1579
1840
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
1580
- e.points = [
1581
- connPt(src, dstCX, dstCY),
1582
- connPt(dst, srcCX, srcCY),
1583
- ];
1841
+ e.points = [connPt(src, dstCX, dstCY), connPt(dst, srcCX, srcCY)];
1584
1842
  }
1585
1843
  }
1586
1844
  function computeBounds(sg, margin) {
1587
1845
  const allX = [
1588
- ...sg.nodes.map(n => n.x + n.w),
1589
- ...sg.groups.filter(g => g.w).map(g => g.x + g.w),
1590
- ...sg.tables.map(t => t.x + t.w),
1591
- ...sg.notes.map(n => n.x + n.w),
1592
- ...sg.charts.map(c => c.x + c.w)
1846
+ ...sg.nodes.map((n) => n.x + n.w),
1847
+ ...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
1848
+ ...sg.tables.map((t) => t.x + t.w),
1849
+ ...sg.notes.map((n) => n.x + n.w),
1850
+ ...sg.charts.map((c) => c.x + c.w),
1851
+ ...sg.markdowns.map((m) => m.x + m.w),
1593
1852
  ];
1594
1853
  const allY = [
1595
- ...sg.nodes.map(n => n.y + n.h),
1596
- ...sg.groups.filter(g => g.h).map(g => g.y + g.h),
1597
- ...sg.tables.map(t => t.y + t.h),
1598
- ...sg.notes.map(n => n.y + n.h),
1599
- ...sg.charts.map(c => c.y + c.h)
1854
+ ...sg.nodes.map((n) => n.y + n.h),
1855
+ ...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
1856
+ ...sg.tables.map((t) => t.y + t.h),
1857
+ ...sg.notes.map((n) => n.y + n.h),
1858
+ ...sg.charts.map((c) => c.y + c.h),
1859
+ ...sg.markdowns.map((m) => m.y + m.h),
1600
1860
  ];
1601
1861
  sg.width = (allX.length ? Math.max(...allX) : 400) + margin;
1602
1862
  sg.height = (allY.length ? Math.max(...allY) : 300) + margin;
1603
1863
  }
1604
1864
  // ── Public entry point ────────────────────────────────────
1605
1865
  function layout(sg) {
1606
- const GAP_MAIN = Number(sg.config['gap'] ?? DEFAULT_GAP_MAIN);
1607
- const MARGIN = Number(sg.config['margin'] ?? DEFAULT_MARGIN);
1866
+ const GAP_MAIN = Number(sg.config["gap"] ?? DEFAULT_GAP_MAIN);
1867
+ const MARGIN = Number(sg.config["margin"] ?? DEFAULT_MARGIN);
1608
1868
  const nm = nodeMap(sg);
1609
1869
  const gm = groupMap(sg);
1610
1870
  const tm = tableMap(sg);
1611
1871
  const ntm = noteMap(sg);
1612
1872
  const cm = chartMap(sg);
1613
- console.log('[layout] sg.charts:', sg.charts.map(c => c.id));
1614
- console.log('[layout] sg.rootOrder:', sg.rootOrder.map(r => r.kind + ':' + r.id));
1873
+ const mdm = markdownMap(sg);
1615
1874
  // 1. Size all nodes and tables
1616
1875
  sg.nodes.forEach(sizeNode);
1617
1876
  sg.tables.forEach(sizeTable);
1618
1877
  sg.notes.forEach(sizeNote);
1619
1878
  sg.charts.forEach(sizeChart);
1879
+ sg.markdowns.forEach(sizeMarkdown);
1620
1880
  // src/layout/index.ts — after sg.charts.forEach(sizeChart);
1621
1881
  // 2. Identify root vs nested items
1622
- const nestedGroupIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'group').map(c => c.id)));
1623
- const groupedNodeIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'node').map(c => c.id)));
1624
- const groupedTableIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'table').map(c => c.id)));
1625
- const groupedNoteIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'note').map(c => c.id)));
1626
- const groupedChartIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'chart').map(c => c.id)));
1627
- const rootGroups = sg.groups.filter(g => !nestedGroupIds.has(g.id));
1628
- const rootNodes = sg.nodes.filter(n => !groupedNodeIds.has(n.id));
1629
- const rootTables = sg.tables.filter(t => !groupedTableIds.has(t.id));
1630
- const rootNotes = sg.notes.filter(n => !groupedNoteIds.has(n.id));
1631
- const rootCharts = sg.charts.filter(c => !groupedChartIds.has(c.id));
1882
+ const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
1883
+ const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
1884
+ const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
1885
+ const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
1886
+ const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
1887
+ const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
1888
+ const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
1889
+ const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
1890
+ const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
1891
+ const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
1892
+ const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
1893
+ const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
1632
1894
  // 3. Measure root groups bottom-up
1633
1895
  for (const g of rootGroups)
1634
- measure(g, nm, gm, tm, ntm, cm);
1896
+ measure(g, nm, gm, tm, ntm, cm, mdm);
1635
1897
  // 4. Build root order
1636
1898
  // sg.rootOrder preserves DSL declaration order.
1637
1899
  // Fall back: groups, then nodes, then tables.
1638
1900
  const rootOrder = sg.rootOrder?.length
1639
1901
  ? sg.rootOrder
1640
1902
  : [
1641
- ...rootGroups.map(g => ({ kind: 'group', id: g.id })),
1642
- ...rootNodes.map(n => ({ kind: 'node', id: n.id })),
1643
- ...rootTables.map(t => ({ kind: 'table', id: t.id })),
1644
- ...rootNotes.map(n => ({ kind: 'note', id: n.id })),
1645
- ...rootCharts.map(c => ({ kind: 'chart', id: c.id }))
1903
+ ...rootGroups.map((g) => ({ kind: "group", id: g.id })),
1904
+ ...rootNodes.map((n) => ({ kind: "node", id: n.id })),
1905
+ ...rootTables.map((t) => ({ kind: "table", id: t.id })),
1906
+ ...rootNotes.map((n) => ({ kind: "note", id: n.id })),
1907
+ ...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
1908
+ ...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
1646
1909
  ];
1647
1910
  // 5. Root-level layout
1648
1911
  // sg.layout:
1649
1912
  // 'row' → items flow left to right (default)
1650
1913
  // 'column' → items flow top to bottom
1651
1914
  // 'grid' → config columns=N grid
1652
- const rootLayout = (sg.layout ?? 'row');
1653
- const rootCols = Number(sg.config['columns'] ?? 1);
1654
- const useGrid = rootLayout === 'grid' && rootCols > 0;
1655
- const useColumn = rootLayout === 'column';
1656
- console.log('[layout] sized charts:', sg.charts.map(c => `${c.id} w=${c.w} h=${c.h}`));
1657
- console.log('[layout] rootOrder chart refs:', rootOrder.filter(r => r.kind === 'chart'));
1915
+ const rootLayout = (sg.layout ?? "row");
1916
+ const rootCols = Number(sg.config["columns"] ?? 1);
1917
+ const useGrid = rootLayout === "grid" && rootCols > 0;
1918
+ const useColumn = rootLayout === "column";
1658
1919
  if (useGrid) {
1659
1920
  // ── Grid: per-row heights, per-column widths (no wasted space) ──
1660
1921
  const cols = rootCols;
@@ -1665,22 +1926,26 @@ var AIDiagram = (function (exports) {
1665
1926
  const col = idx % cols;
1666
1927
  const row = Math.floor(idx / cols);
1667
1928
  let w = 0, h = 0;
1668
- if (ref.kind === 'group') {
1929
+ if (ref.kind === "group") {
1669
1930
  w = gm.get(ref.id).w;
1670
1931
  h = gm.get(ref.id).h;
1671
1932
  }
1672
- else if (ref.kind === 'table') {
1933
+ else if (ref.kind === "table") {
1673
1934
  w = tm.get(ref.id).w;
1674
1935
  h = tm.get(ref.id).h;
1675
1936
  }
1676
- else if (ref.kind === 'note') {
1937
+ else if (ref.kind === "note") {
1677
1938
  w = ntm.get(ref.id).w;
1678
1939
  h = ntm.get(ref.id).h;
1679
1940
  }
1680
- else if (ref.kind === 'chart') {
1941
+ else if (ref.kind === "chart") {
1681
1942
  w = cm.get(ref.id).w;
1682
1943
  h = cm.get(ref.id).h;
1683
1944
  }
1945
+ else if (ref.kind === "markdown") {
1946
+ w = mdm.get(ref.id).w;
1947
+ h = mdm.get(ref.id).h;
1948
+ }
1684
1949
  else {
1685
1950
  w = nm.get(ref.id).w;
1686
1951
  h = nm.get(ref.id).h;
@@ -1703,22 +1968,26 @@ var AIDiagram = (function (exports) {
1703
1968
  rootOrder.forEach((ref, idx) => {
1704
1969
  const x = colX[idx % cols];
1705
1970
  const y = rowY[Math.floor(idx / cols)];
1706
- if (ref.kind === 'group') {
1971
+ if (ref.kind === "group") {
1707
1972
  gm.get(ref.id).x = x;
1708
1973
  gm.get(ref.id).y = y;
1709
1974
  }
1710
- else if (ref.kind === 'table') {
1975
+ else if (ref.kind === "table") {
1711
1976
  tm.get(ref.id).x = x;
1712
1977
  tm.get(ref.id).y = y;
1713
1978
  }
1714
- else if (ref.kind === 'note') {
1979
+ else if (ref.kind === "note") {
1715
1980
  ntm.get(ref.id).x = x;
1716
1981
  ntm.get(ref.id).y = y;
1717
1982
  }
1718
- else if (ref.kind === 'chart') {
1983
+ else if (ref.kind === "chart") {
1719
1984
  cm.get(ref.id).x = x;
1720
1985
  cm.get(ref.id).y = y;
1721
1986
  }
1987
+ else if (ref.kind === "markdown") {
1988
+ mdm.get(ref.id).x = x;
1989
+ mdm.get(ref.id).y = y;
1990
+ }
1722
1991
  else {
1723
1992
  nm.get(ref.id).x = x;
1724
1993
  nm.get(ref.id).y = y;
@@ -1730,44 +1999,52 @@ var AIDiagram = (function (exports) {
1730
1999
  let pos = MARGIN;
1731
2000
  for (const ref of rootOrder) {
1732
2001
  let w = 0, h = 0;
1733
- if (ref.kind === 'group') {
2002
+ if (ref.kind === "group") {
1734
2003
  w = gm.get(ref.id).w;
1735
2004
  h = gm.get(ref.id).h;
1736
2005
  }
1737
- else if (ref.kind === 'table') {
2006
+ else if (ref.kind === "table") {
1738
2007
  w = tm.get(ref.id).w;
1739
2008
  h = tm.get(ref.id).h;
1740
2009
  }
1741
- else if (ref.kind === 'note') {
2010
+ else if (ref.kind === "note") {
1742
2011
  w = ntm.get(ref.id).w;
1743
2012
  h = ntm.get(ref.id).h;
1744
2013
  }
1745
- else if (ref.kind === 'chart') {
2014
+ else if (ref.kind === "chart") {
1746
2015
  w = cm.get(ref.id).w;
1747
2016
  h = cm.get(ref.id).h;
1748
2017
  }
2018
+ else if (ref.kind === "markdown") {
2019
+ w = mdm.get(ref.id).w;
2020
+ h = mdm.get(ref.id).h;
2021
+ }
1749
2022
  else {
1750
2023
  w = nm.get(ref.id).w;
1751
2024
  h = nm.get(ref.id).h;
1752
2025
  }
1753
2026
  const x = useColumn ? MARGIN : pos;
1754
2027
  const y = useColumn ? pos : MARGIN;
1755
- if (ref.kind === 'group') {
2028
+ if (ref.kind === "group") {
1756
2029
  gm.get(ref.id).x = x;
1757
2030
  gm.get(ref.id).y = y;
1758
2031
  }
1759
- else if (ref.kind === 'table') {
2032
+ else if (ref.kind === "table") {
1760
2033
  tm.get(ref.id).x = x;
1761
2034
  tm.get(ref.id).y = y;
1762
2035
  }
1763
- else if (ref.kind === 'note') {
2036
+ else if (ref.kind === "note") {
1764
2037
  ntm.get(ref.id).x = x;
1765
2038
  ntm.get(ref.id).y = y;
1766
2039
  }
1767
- else if (ref.kind === 'chart') {
2040
+ else if (ref.kind === "chart") {
1768
2041
  cm.get(ref.id).x = x;
1769
2042
  cm.get(ref.id).y = y;
1770
2043
  }
2044
+ else if (ref.kind === "markdown") {
2045
+ mdm.get(ref.id).x = x;
2046
+ mdm.get(ref.id).y = y;
2047
+ }
1771
2048
  else {
1772
2049
  nm.get(ref.id).x = x;
1773
2050
  nm.get(ref.id).y = y;
@@ -1777,10 +2054,9 @@ var AIDiagram = (function (exports) {
1777
2054
  }
1778
2055
  // 6. Place children within each root group (top-down, recursive)
1779
2056
  for (const g of rootGroups)
1780
- place(g, nm, gm, tm, ntm, cm);
2057
+ place(g, nm, gm, tm, ntm, cm, mdm);
1781
2058
  // 7. Route edges and compute canvas size
1782
2059
  routeEdges(sg);
1783
- console.log('[layout] chart positions:', sg.charts.map(c => `${c.id} x=${c.x} y=${c.y}`));
1784
2060
  computeBounds(sg, MARGIN);
1785
2061
  return sg;
1786
2062
  }
@@ -2412,6 +2688,83 @@ var AIDiagram = (function (exports) {
2412
2688
  }
2413
2689
  const THEME_NAMES = Object.keys(PALETTES);
2414
2690
 
2691
+ // ============================================================
2692
+ // sketchmark — Font Registry
2693
+ // ============================================================
2694
+ // built-in named fonts — user can reference these by short name
2695
+ const BUILTIN_FONTS = {
2696
+ // hand-drawn
2697
+ caveat: {
2698
+ family: "'Caveat', cursive",
2699
+ url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
2700
+ },
2701
+ handlee: {
2702
+ family: "'Handlee', cursive",
2703
+ url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
2704
+ },
2705
+ 'indie-flower': {
2706
+ family: "'Indie Flower', cursive",
2707
+ url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
2708
+ },
2709
+ 'patrick-hand': {
2710
+ family: "'Patrick Hand', cursive",
2711
+ url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
2712
+ },
2713
+ // clean / readable
2714
+ 'dm-mono': {
2715
+ family: "'DM Mono', monospace",
2716
+ url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
2717
+ },
2718
+ 'jetbrains': {
2719
+ family: "'JetBrains Mono', monospace",
2720
+ url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
2721
+ },
2722
+ 'instrument': {
2723
+ family: "'Instrument Serif', serif",
2724
+ url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
2725
+ },
2726
+ 'playfair': {
2727
+ family: "'Playfair Display', serif",
2728
+ url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
2729
+ },
2730
+ // system fallbacks (no URL needed)
2731
+ system: { family: 'system-ui, sans-serif' },
2732
+ mono: { family: "'Courier New', monospace" },
2733
+ serif: { family: 'Georgia, serif' },
2734
+ };
2735
+ // default — what renders when no font is specified
2736
+ const DEFAULT_FONT = 'system-ui, sans-serif';
2737
+ // resolve a short name or pass-through a quoted CSS family
2738
+ function resolveFont(nameOrFamily) {
2739
+ const key = nameOrFamily.toLowerCase().trim();
2740
+ if (BUILTIN_FONTS[key])
2741
+ return BUILTIN_FONTS[key].family;
2742
+ return nameOrFamily; // treat as raw CSS font-family
2743
+ }
2744
+ // inject a <link> into <head> for a built-in font (browser only)
2745
+ function loadFont(name) {
2746
+ if (typeof document === 'undefined')
2747
+ return;
2748
+ const key = name.toLowerCase().trim();
2749
+ const def = BUILTIN_FONTS[key];
2750
+ if (!def?.url || def.loaded)
2751
+ return;
2752
+ if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
2753
+ return;
2754
+ const link = document.createElement('link');
2755
+ link.rel = 'stylesheet';
2756
+ link.href = def.url;
2757
+ link.setAttribute('data-sketchmark-font', key);
2758
+ document.head.appendChild(link);
2759
+ def.loaded = true;
2760
+ }
2761
+ // user registers their own font (already loaded via CSS/link)
2762
+ function registerFont(name, family, url) {
2763
+ BUILTIN_FONTS[name.toLowerCase()] = { family, url };
2764
+ if (url)
2765
+ loadFont(name);
2766
+ }
2767
+
2415
2768
  // ============================================================
2416
2769
  // sketchmark — SVG Renderer (rough.js hand-drawn)
2417
2770
  // ============================================================
@@ -2424,17 +2777,93 @@ var AIDiagram = (function (exports) {
2424
2777
  return h;
2425
2778
  }
2426
2779
  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) {
2780
+ // ── Small helper: load + resolve font from style or fall back ─────────────
2781
+ function resolveStyleFont$1(style, fallback) {
2782
+ const raw = String(style["font"] ?? "");
2783
+ if (!raw)
2784
+ return fallback;
2785
+ loadFont(raw);
2786
+ return resolveFont(raw);
2787
+ }
2788
+ function wrapText$1(text, maxWidth, fontSize) {
2789
+ const words = text.split(' ');
2790
+ const charsPerPx = fontSize * 0.55; // approximate
2791
+ const maxChars = Math.floor(maxWidth / charsPerPx);
2792
+ const lines = [];
2793
+ let current = '';
2794
+ for (const word of words) {
2795
+ const test = current ? `${current} ${word}` : word;
2796
+ if (test.length > maxChars && current) {
2797
+ lines.push(current);
2798
+ current = word;
2799
+ }
2800
+ else {
2801
+ current = test;
2802
+ }
2803
+ }
2804
+ if (current)
2805
+ lines.push(current);
2806
+ return lines;
2807
+ }
2808
+ // ── SVG text helpers ──────────────────────────────────────────────────────
2809
+ /**
2810
+ * Single-line SVG text element.
2811
+ *
2812
+ * | param | maps to SVG attr |
2813
+ * |---------------|--------------------------|
2814
+ * txt | textContent |
2815
+ * x, y | x, y |
2816
+ * sz | font-size |
2817
+ * wt | font-weight |
2818
+ * col | fill |
2819
+ * anchor | text-anchor |
2820
+ * font | font-family |
2821
+ * letterSpacing | letter-spacing |
2822
+ */
2823
+ function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", font, letterSpacing) {
2824
+ const t = se("text");
2825
+ t.setAttribute("x", String(x));
2826
+ t.setAttribute("y", String(y));
2827
+ t.setAttribute("text-anchor", anchor);
2828
+ t.setAttribute("dominant-baseline", "middle");
2829
+ t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
2830
+ t.setAttribute("font-size", String(sz));
2831
+ t.setAttribute("font-weight", String(wt));
2832
+ t.setAttribute("fill", col);
2833
+ t.setAttribute("pointer-events", "none");
2834
+ t.setAttribute("user-select", "none");
2835
+ if (letterSpacing != null)
2836
+ t.setAttribute("letter-spacing", String(letterSpacing));
2837
+ t.textContent = txt;
2838
+ return t;
2839
+ }
2840
+ /**
2841
+ * Multi-line SVG text element using <tspan> per line.
2842
+ *
2843
+ * | param | maps to SVG attr |
2844
+ * |---------------|--------------------------|
2845
+ * lines | one <tspan> each |
2846
+ * x | tspan x |
2847
+ * cy | vertical centre of block |
2848
+ * sz | font-size |
2849
+ * wt | font-weight |
2850
+ * col | fill |
2851
+ * anchor | text-anchor |
2852
+ * lineH | dy between tspans (px) |
2853
+ * font | font-family |
2854
+ * letterSpacing | letter-spacing |
2855
+ */
2856
+ function mkMultilineText(lines, x, cy, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18, font, letterSpacing) {
2430
2857
  const t = se("text");
2431
2858
  t.setAttribute("text-anchor", anchor);
2432
- t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2859
+ t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
2433
2860
  t.setAttribute("font-size", String(sz));
2434
2861
  t.setAttribute("font-weight", String(wt));
2435
2862
  t.setAttribute("fill", col);
2436
2863
  t.setAttribute("pointer-events", "none");
2437
2864
  t.setAttribute("user-select", "none");
2865
+ if (letterSpacing != null)
2866
+ t.setAttribute("letter-spacing", String(letterSpacing));
2438
2867
  // vertically centre the whole block
2439
2868
  const totalH = (lines.length - 1) * lineH;
2440
2869
  const startY = cy - totalH / 2;
@@ -2448,21 +2877,6 @@ var AIDiagram = (function (exports) {
2448
2877
  });
2449
2878
  return t;
2450
2879
  }
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
2880
  function mkGroup(id, cls) {
2467
2881
  const g = se("g");
2468
2882
  if (id)
@@ -2471,7 +2885,7 @@ var AIDiagram = (function (exports) {
2471
2885
  g.setAttribute("class", cls);
2472
2886
  return g;
2473
2887
  }
2474
- // ── Arrow direction from connector ────────────────────────
2888
+ // ── Arrow direction from connector ────────────────────────────────────────
2475
2889
  function connMeta$1(connector) {
2476
2890
  if (connector === "--")
2477
2891
  return { arrowAt: "none", dashed: false };
@@ -2486,7 +2900,7 @@ var AIDiagram = (function (exports) {
2486
2900
  return { arrowAt: "start", dashed };
2487
2901
  return { arrowAt: "end", dashed };
2488
2902
  }
2489
- // ── Generic rect connection point ─────────────────────────
2903
+ // ── Generic rect connection point ─────────────────────────────────────────
2490
2904
  function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
2491
2905
  const cx = rx + rw / 2, cy = ry + rh / 2;
2492
2906
  const dx = ox - cx, dy = oy - cy;
@@ -2511,7 +2925,7 @@ var AIDiagram = (function (exports) {
2511
2925
  }
2512
2926
  return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
2513
2927
  }
2514
- // ── Group depth (for paint order) ─────────────────────────
2928
+ // ── Group depth (for paint order) ─────────────────────────────────────────
2515
2929
  function groupDepth$1(g, gm) {
2516
2930
  let d = 0;
2517
2931
  let cur = g;
@@ -2521,7 +2935,7 @@ var AIDiagram = (function (exports) {
2521
2935
  }
2522
2936
  return d;
2523
2937
  }
2524
- // ── Node shapes ───────────────────────────────────────────
2938
+ // ── Node shapes ───────────────────────────────────────────────────────────
2525
2939
  function renderShape$1(rc, n, palette) {
2526
2940
  const s = n.style ?? {};
2527
2941
  const fill = String(s.fill ?? palette.nodeFill);
@@ -2594,19 +3008,18 @@ var AIDiagram = (function (exports) {
2594
3008
  return [];
2595
3009
  case "image": {
2596
3010
  if (n.imageUrl) {
2597
- const img = document.createElementNS("http://www.w3.org/2000/svg", "image");
3011
+ const img = document.createElementNS(NS, "image");
2598
3012
  img.setAttribute("href", n.imageUrl);
2599
3013
  img.setAttribute("x", String(n.x + 1));
2600
3014
  img.setAttribute("y", String(n.y + 1));
2601
3015
  img.setAttribute("width", String(n.w - 2));
2602
3016
  img.setAttribute("height", String(n.h - 2));
2603
3017
  img.setAttribute("preserveAspectRatio", "xMidYMid meet");
2604
- // optional: clip to rounded rect
2605
3018
  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");
3019
+ const defs = document.createElementNS(NS, "defs");
3020
+ const clip = document.createElementNS(NS, "clipPath");
2608
3021
  clip.setAttribute("id", clipId);
2609
- const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3022
+ const rect = document.createElementNS(NS, "rect");
2610
3023
  rect.setAttribute("x", String(n.x + 1));
2611
3024
  rect.setAttribute("y", String(n.y + 1));
2612
3025
  rect.setAttribute("width", String(n.w - 2));
@@ -2615,15 +3028,12 @@ var AIDiagram = (function (exports) {
2615
3028
  clip.appendChild(rect);
2616
3029
  defs.appendChild(clip);
2617
3030
  img.setAttribute("clip-path", `url(#${clipId})`);
2618
- // border box drawn on top
2619
3031
  const border = rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2620
3032
  ...opts,
2621
3033
  fill: "none",
2622
- fillStyle: "solid",
2623
3034
  });
2624
3035
  return [defs, img, border];
2625
3036
  }
2626
- // fallback: no URL → grey placeholder box
2627
3037
  return [
2628
3038
  rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2629
3039
  ...opts,
@@ -2636,7 +3046,7 @@ var AIDiagram = (function (exports) {
2636
3046
  return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
2637
3047
  }
2638
3048
  }
2639
- // ── Arrowhead ─────────────────────────────────────────────
3049
+ // ── Arrowhead ─────────────────────────────────────────────────────────────
2640
3050
  function arrowHead(rc, x, y, angle, col, seed) {
2641
3051
  const as = 12;
2642
3052
  return rc.polygon([
@@ -2660,14 +3070,22 @@ var AIDiagram = (function (exports) {
2660
3070
  }
2661
3071
  function renderToSVG(sg, container, options = {}) {
2662
3072
  if (typeof rough === "undefined") {
2663
- throw new Error('rough.js is not loaded. Add <script src="https://unpkg.com/roughjs/bundled/rough.js"></script>');
3073
+ throw new Error("rough.js is not loaded.");
2664
3074
  }
2665
3075
  const isDark = options.theme === "dark" ||
2666
3076
  (options.theme === "auto" &&
2667
3077
  window.matchMedia?.("(prefers-color-scheme:dark)").matches);
2668
- // Resolve palette: DSL config takes priority, then options.theme, then light
2669
3078
  const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
2670
3079
  const palette = resolvePalette(themeName);
3080
+ // ── Diagram-level font ──────────────────────────────────
3081
+ const diagramFont = (() => {
3082
+ const raw = String(sg.config["font"] ?? "");
3083
+ if (raw) {
3084
+ loadFont(raw);
3085
+ return resolveFont(raw);
3086
+ }
3087
+ return DEFAULT_FONT;
3088
+ })();
2671
3089
  BASE_ROUGH.roughness = options.roughness ?? 1.3;
2672
3090
  BASE_ROUGH.bowing = options.bowing ?? 0.7;
2673
3091
  let svg;
@@ -2684,14 +3102,7 @@ var AIDiagram = (function (exports) {
2684
3102
  svg.setAttribute("height", String(sg.height));
2685
3103
  svg.setAttribute("viewBox", `0 0 ${sg.width} ${sg.height}`);
2686
3104
  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);
3105
+ // ── Background ─────────────────────────────────────────
2695
3106
  if (!options.transparent) {
2696
3107
  const bgRect = se("rect");
2697
3108
  bgRect.setAttribute("x", "0");
@@ -2707,9 +3118,9 @@ var AIDiagram = (function (exports) {
2707
3118
  const titleColor = String(sg.config["title-color"] ?? palette.titleText);
2708
3119
  const titleSize = Number(sg.config["title-size"] ?? 18);
2709
3120
  const titleWeight = Number(sg.config["title-weight"] ?? 600);
2710
- svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor));
3121
+ svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor, "middle", diagramFont));
2711
3122
  }
2712
- // ── Groups (depth-sorted: outermost first) ────────────────
3123
+ // ── Groups ───────────────────────────────────────────────
2713
3124
  const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
2714
3125
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth$1(a, gmMap) - groupDepth$1(b, gmMap));
2715
3126
  const GL = mkGroup("grp-layer");
@@ -2729,8 +3140,16 @@ var AIDiagram = (function (exports) {
2729
3140
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
2730
3141
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
2731
3142
  }));
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"));
3143
+ // ── Group label typography ──────────────────────────
3144
+ // supports: font, font-size, letter-spacing
3145
+ // always left-anchored (single line)
3146
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
3147
+ const gFontSize = Number(gs.fontSize ?? 12);
3148
+ const gFont = resolveStyleFont$1(gs, diagramFont);
3149
+ const gLetterSpacing = gs.letterSpacing;
3150
+ if (g.label) {
3151
+ gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
3152
+ }
2734
3153
  GL.appendChild(gg);
2735
3154
  }
2736
3155
  svg.appendChild(GL);
@@ -2784,7 +3203,13 @@ var AIDiagram = (function (exports) {
2784
3203
  bg.setAttribute("rx", "3");
2785
3204
  bg.setAttribute("opacity", "0.9");
2786
3205
  eg.appendChild(bg);
2787
- eg.appendChild(mkText(e.label, mx, my, 11, 400, palette.edgeLabelText));
3206
+ // ── Edge label typography ───────────────────────
3207
+ // supports: font, font-size, letter-spacing
3208
+ // always center-anchored (single line floating on edge)
3209
+ const eFontSize = Number(e.style?.fontSize ?? 11);
3210
+ const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
3211
+ const eLetterSpacing = e.style?.letterSpacing;
3212
+ eg.appendChild(mkText(e.label, mx, my, eFontSize, 400, palette.edgeLabelText, "middle", eFont, eLetterSpacing));
2788
3213
  }
2789
3214
  EL.appendChild(eg);
2790
3215
  }
@@ -2794,14 +3219,45 @@ var AIDiagram = (function (exports) {
2794
3219
  for (const n of sg.nodes) {
2795
3220
  const ng = mkGroup(`node-${n.id}`, "ng");
2796
3221
  renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
3222
+ // ── Node / text typography ─────────────────────────
3223
+ // supports: font, font-size, letter-spacing, text-align, line-height
2797
3224
  const fontSize = Number(n.style?.fontSize ?? (n.shape === "text" ? 13 : 14));
2798
3225
  const fontWeight = n.style?.fontWeight ?? (n.shape === "text" ? 400 : 500);
2799
- const lines = n.label.split("\n");
3226
+ const textColor = String(n.style?.color ??
3227
+ (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
3228
+ const nodeFont = resolveStyleFont$1(n.style ?? {}, diagramFont);
3229
+ const textAlign = String(n.style?.textAlign ?? "center");
3230
+ const anchorMap = {
3231
+ left: "start",
3232
+ center: "middle",
3233
+ right: "end",
3234
+ };
3235
+ const textAnchor = anchorMap[textAlign] ?? "middle";
3236
+ // line-height is a multiplier (e.g. 1.4 = 140% of font-size)
3237
+ const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
3238
+ const letterSpacing = n.style?.letterSpacing;
3239
+ // x shifts for left / right alignment
3240
+ const textX = textAlign === "left"
3241
+ ? n.x + 8
3242
+ : textAlign === "right"
3243
+ ? n.x + n.w - 8
3244
+ : n.x + n.w / 2;
3245
+ const lines = n.shape === 'text' && !n.label.includes('\n')
3246
+ ? wrapText$1(n.label, n.w - 16, fontSize)
3247
+ : n.label.split('\n');
3248
+ const verticalAlign = String(n.style?.verticalAlign ?? "middle");
3249
+ const nodeBodyTop = n.y + 6;
3250
+ const nodeBodyBottom = n.y + n.h - 6;
3251
+ const nodeBodyMid = n.y + n.h / 2;
3252
+ const blockH = (lines.length - 1) * lineHeight;
3253
+ const textCY = verticalAlign === "top"
3254
+ ? nodeBodyTop + blockH / 2
3255
+ : verticalAlign === "bottom"
3256
+ ? nodeBodyBottom - blockH / 2
3257
+ : nodeBodyMid;
2800
3258
  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))));
3259
+ ? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight, nodeFont, letterSpacing)
3260
+ : mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor, nodeFont, letterSpacing));
2805
3261
  if (options.interactive) {
2806
3262
  ng.style.cursor = "pointer";
2807
3263
  ng.addEventListener("click", () => options.onNodeClick?.(n.id));
@@ -2827,7 +3283,12 @@ var AIDiagram = (function (exports) {
2827
3283
  const hdrText = String(gs.color ?? palette.tableHeaderText);
2828
3284
  const divCol = palette.tableDivider;
2829
3285
  const pad = t.labelH;
2830
- // Outer border
3286
+ // ── Table-level font (applies to label + all cells) ─
3287
+ // supports: font, font-size, letter-spacing
3288
+ const tFontSize = Number(gs.fontSize ?? 12);
3289
+ const tFont = resolveStyleFont$1(gs, diagramFont);
3290
+ const tLetterSpacing = gs.letterSpacing;
3291
+ // outer border
2831
3292
  tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
2832
3293
  ...BASE_ROUGH,
2833
3294
  seed: hashStr$3(t.id),
@@ -2836,20 +3297,19 @@ var AIDiagram = (function (exports) {
2836
3297
  stroke: strk,
2837
3298
  strokeWidth: 1.5,
2838
3299
  }));
2839
- // Label strip separator
3300
+ // label strip separator
2840
3301
  tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
2841
3302
  roughness: 0.6,
2842
3303
  seed: hashStr$3(t.id + "l"),
2843
3304
  stroke: strk,
2844
3305
  strokeWidth: 1,
2845
3306
  }));
2846
- // Label text
2847
- tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, 12, 500, textCol, "start"));
2848
- // Rows
3307
+ // ── Table label: font, font-size, letter-spacing (always left) ──
3308
+ tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, "start", tFont, tLetterSpacing));
3309
+ // rows
2849
3310
  let rowY = t.y + pad;
2850
3311
  for (const row of t.rows) {
2851
3312
  const rh = row.kind === "header" ? t.headerH : t.rowH;
2852
- // Header background fill
2853
3313
  if (row.kind === "header") {
2854
3314
  const hdrBg = se("rect");
2855
3315
  hdrBg.setAttribute("x", String(t.x + 1));
@@ -2859,19 +3319,34 @@ var AIDiagram = (function (exports) {
2859
3319
  hdrBg.setAttribute("fill", hdrFill);
2860
3320
  tg.appendChild(hdrBg);
2861
3321
  }
2862
- // Row separator
2863
3322
  tg.appendChild(rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
2864
3323
  roughness: 0.4,
2865
3324
  seed: hashStr$3(t.id + rowY),
2866
3325
  stroke: row.kind === "header" ? strk : divCol,
2867
3326
  strokeWidth: row.kind === "header" ? 1.2 : 0.6,
2868
3327
  }));
2869
- // Cell text + col separators
3328
+ // ── Cell text: font, font-size, letter-spacing, text-align ──
3329
+ // text-align applies to data rows; header is always centered
3330
+ const cellAlignProp = row.kind === "header" ? "center" : String(gs.textAlign ?? "center");
3331
+ const cellAnchorMap = {
3332
+ left: "start",
3333
+ center: "middle",
3334
+ right: "end",
3335
+ };
3336
+ const cellAnchor = cellAnchorMap[cellAlignProp] ?? "middle";
3337
+ const cellFw = row.kind === "header" ? 600 : 400;
3338
+ const cellColor = row.kind === "header" ? hdrText : textCol;
2870
3339
  let cx = t.x;
2871
3340
  row.cells.forEach((cell, i) => {
2872
3341
  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));
3342
+ // x position shifts with alignment
3343
+ const cellX = cellAnchor === "start"
3344
+ ? cx + 6
3345
+ : cellAnchor === "end"
3346
+ ? cx + cw - 6
3347
+ : cx + cw / 2;
3348
+ // ← was missing tg.appendChild — cells were invisible before
3349
+ tg.appendChild(mkText(cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAnchor, tFont, tLetterSpacing));
2875
3350
  if (i < row.cells.length - 1) {
2876
3351
  tg.appendChild(rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
2877
3352
  roughness: 0.3,
@@ -2900,6 +3375,25 @@ var AIDiagram = (function (exports) {
2900
3375
  const strk = String(gs.stroke ?? palette.noteStroke);
2901
3376
  const fold = 14;
2902
3377
  const { x, y, w, h } = n;
3378
+ // ── Note typography ─────────────────────────────────
3379
+ // supports: font, font-size, letter-spacing, text-align, line-height
3380
+ const nFontSize = Number(gs.fontSize ?? 12);
3381
+ const nFont = resolveStyleFont$1(gs, diagramFont);
3382
+ const nLetterSpacing = gs.letterSpacing;
3383
+ const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
3384
+ const nTextAlign = String(gs.textAlign ?? "left");
3385
+ const nAnchorMap = {
3386
+ left: "start",
3387
+ center: "middle",
3388
+ right: "end",
3389
+ };
3390
+ const nAnchor = nAnchorMap[nTextAlign] ?? "start";
3391
+ // x position for the text block (pad from left, with alignment)
3392
+ const nTextX = nTextAlign === "right"
3393
+ ? x + w - fold - 6
3394
+ : nTextAlign === "center"
3395
+ ? x + (w - fold) / 2
3396
+ : x + 12;
2903
3397
  ng.appendChild(rc.polygon([
2904
3398
  [x, y],
2905
3399
  [x + w - fold, y],
@@ -2926,12 +3420,75 @@ var AIDiagram = (function (exports) {
2926
3420
  stroke: strk,
2927
3421
  strokeWidth: 0.8,
2928
3422
  }));
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
- });
3423
+ const nVerticalAlign = String(gs.verticalAlign ?? "top");
3424
+ const bodyTop = y + fold + 8; // below the fold triangle
3425
+ const bodyBottom = y + h - 8; // above bottom edge
3426
+ const bodyMid = (bodyTop + bodyBottom) / 2;
3427
+ const blockH = (n.lines.length - 1) * nLineHeight;
3428
+ const blockCY = nVerticalAlign === "bottom"
3429
+ ? bodyBottom - blockH / 2
3430
+ : nVerticalAlign === "middle"
3431
+ ? bodyMid
3432
+ : bodyTop + blockH / 2;
3433
+ // multiline: use mkMultilineText so line-height is respected
3434
+ if (n.lines.length > 1) {
3435
+ // vertical centre of the text block inside the note
3436
+ ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
3437
+ }
3438
+ else {
3439
+ ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
3440
+ }
2932
3441
  NoteL.appendChild(ng);
2933
3442
  }
2934
3443
  svg.appendChild(NoteL);
3444
+ markdownMap(sg);
3445
+ const MDL = mkGroup('markdown-layer');
3446
+ for (const m of sg.markdowns) {
3447
+ const mg = mkGroup(`markdown-${m.id}`, 'mdg');
3448
+ const mFont = resolveStyleFont$1(m.style, diagramFont);
3449
+ const baseColor = String(m.style?.color ?? palette.nodeText);
3450
+ const textAlign = String(m.style?.textAlign ?? 'left');
3451
+ const anchor = textAlign === 'right' ? 'end'
3452
+ : textAlign === 'center' ? 'middle'
3453
+ : 'start';
3454
+ const PAD = Number(m.style?.padding ?? 16);
3455
+ const textX = textAlign === 'right' ? m.x + m.w - PAD
3456
+ : textAlign === 'center' ? m.x + m.w / 2
3457
+ : m.x + PAD;
3458
+ let y = m.y + PAD;
3459
+ for (const line of m.lines) {
3460
+ if (line.kind === 'blank') {
3461
+ y += LINE_SPACING.blank;
3462
+ continue;
3463
+ }
3464
+ const fontSize = LINE_FONT_SIZE[line.kind];
3465
+ const fontWeight = LINE_FONT_WEIGHT[line.kind];
3466
+ const t = se('text');
3467
+ t.setAttribute('x', String(textX));
3468
+ t.setAttribute('y', String(y + fontSize / 2));
3469
+ t.setAttribute('text-anchor', anchor);
3470
+ t.setAttribute('dominant-baseline', 'middle');
3471
+ t.setAttribute('font-family', mFont);
3472
+ t.setAttribute('font-size', String(fontSize));
3473
+ t.setAttribute('font-weight', String(fontWeight));
3474
+ t.setAttribute('fill', baseColor);
3475
+ t.setAttribute('pointer-events', 'none');
3476
+ t.setAttribute('user-select', 'none');
3477
+ for (const run of line.runs) {
3478
+ const span = se('tspan');
3479
+ span.textContent = run.text;
3480
+ if (run.bold)
3481
+ span.setAttribute('font-weight', '700');
3482
+ if (run.italic)
3483
+ span.setAttribute('font-style', 'italic');
3484
+ t.appendChild(span);
3485
+ }
3486
+ mg.appendChild(t);
3487
+ y += LINE_SPACING[line.kind];
3488
+ }
3489
+ MDL.appendChild(mg);
3490
+ }
3491
+ svg.appendChild(MDL);
2935
3492
  // ── Charts ────────────────────────────────────────────────
2936
3493
  const CL = mkGroup("chart-layer");
2937
3494
  for (const c of sg.charts) {
@@ -3205,22 +3762,83 @@ var AIDiagram = (function (exports) {
3205
3762
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
3206
3763
  return h;
3207
3764
  }
3208
- // ── Arrow direction from connector (mirrors svg/index.ts)
3765
+ // ── Small helper: load + resolve font from a style map ────────────────────
3766
+ function resolveStyleFont(style, fallback) {
3767
+ const raw = String(style['font'] ?? '');
3768
+ if (!raw)
3769
+ return fallback;
3770
+ loadFont(raw);
3771
+ return resolveFont(raw);
3772
+ }
3773
+ // ── Canvas text helpers ────────────────────────────────────────────────────
3774
+ function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
3775
+ ctx.save();
3776
+ ctx.font = `${wt} ${sz}px ${font}`;
3777
+ ctx.fillStyle = col;
3778
+ ctx.textAlign = align;
3779
+ ctx.textBaseline = 'middle';
3780
+ if (letterSpacing) {
3781
+ // Canvas has no native letter-spacing — draw char by char
3782
+ const chars = txt.split('');
3783
+ const totalW = ctx.measureText(txt).width + letterSpacing * (chars.length - 1);
3784
+ let startX = align === 'center' ? x - totalW / 2
3785
+ : align === 'right' ? x - totalW
3786
+ : x;
3787
+ ctx.textAlign = 'left';
3788
+ for (const ch of chars) {
3789
+ ctx.fillText(ch, startX, y);
3790
+ startX += ctx.measureText(ch).width + letterSpacing;
3791
+ }
3792
+ }
3793
+ else {
3794
+ ctx.fillText(txt, x, y);
3795
+ }
3796
+ ctx.restore();
3797
+ }
3798
+ function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
3799
+ const totalH = (lines.length - 1) * lineH;
3800
+ const startY = cy - totalH / 2;
3801
+ lines.forEach((line, i) => {
3802
+ drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
3803
+ });
3804
+ }
3805
+ // Soft word-wrap for `text` shape nodes
3806
+ function wrapText(text, maxWidth, fontSize) {
3807
+ const charWidth = fontSize * 0.55;
3808
+ const maxChars = Math.floor(maxWidth / charWidth);
3809
+ const words = text.split(' ');
3810
+ const lines = [];
3811
+ let current = '';
3812
+ for (const word of words) {
3813
+ const test = current ? `${current} ${word}` : word;
3814
+ if (test.length > maxChars && current) {
3815
+ lines.push(current);
3816
+ current = word;
3817
+ }
3818
+ else {
3819
+ current = test;
3820
+ }
3821
+ }
3822
+ if (current)
3823
+ lines.push(current);
3824
+ return lines.length ? lines : [text];
3825
+ }
3826
+ // ── Arrow direction ────────────────────────────────────────────────────────
3209
3827
  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(">");
3828
+ if (connector === '--')
3829
+ return { arrowAt: 'none', dashed: false };
3830
+ if (connector === '---')
3831
+ return { arrowAt: 'none', dashed: true };
3832
+ const bidir = connector.includes('<') && connector.includes('>');
3215
3833
  if (bidir)
3216
- return { arrowAt: "both", dashed: connector.includes("--") };
3217
- const back = connector.startsWith("<");
3218
- const dashed = connector.includes("--");
3834
+ return { arrowAt: 'both', dashed: connector.includes('--') };
3835
+ const back = connector.startsWith('<');
3836
+ const dashed = connector.includes('--');
3219
3837
  if (back)
3220
- return { arrowAt: "start", dashed };
3221
- return { arrowAt: "end", dashed };
3838
+ return { arrowAt: 'start', dashed };
3839
+ return { arrowAt: 'end', dashed };
3222
3840
  }
3223
- // ── Generic rect connection point ─────────────────────────
3841
+ // ── Rect connection point ──────────────────────────────────────────────────
3224
3842
  function rectConnPoint(rx, ry, rw, rh, ox, oy) {
3225
3843
  const cx = rx + rw / 2, cy = ry + rh / 2;
3226
3844
  const dx = ox - cx, dy = oy - cy;
@@ -3233,19 +3851,16 @@ var AIDiagram = (function (exports) {
3233
3851
  return [cx + t * dx, cy + t * dy];
3234
3852
  }
3235
3853
  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);
3854
+ return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
3237
3855
  }
3238
3856
  function getConnPoint(src, dstCX, dstCY) {
3239
- if ("shape" in src && src.shape) {
3857
+ if ('shape' in src && src.shape) {
3240
3858
  return connPoint(src, {
3241
- x: dstCX - 1,
3242
- y: dstCY - 1,
3243
- w: 2,
3244
- h: 2});
3859
+ x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
3245
3860
  }
3246
3861
  return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
3247
3862
  }
3248
- // ── Group depth (for paint order, outermost first) ────────
3863
+ // ── Group depth ────────────────────────────────────────────────────────────
3249
3864
  function groupDepth(g, gm) {
3250
3865
  let d = 0;
3251
3866
  let cur = g;
@@ -3255,80 +3870,57 @@ var AIDiagram = (function (exports) {
3255
3870
  }
3256
3871
  return d;
3257
3872
  }
3258
- // ── Node shapes ───────────────────────────────────────────
3873
+ // ── Node shapes ────────────────────────────────────────────────────────────
3259
3874
  function renderShape(rc, ctx, n, palette, R) {
3260
3875
  const s = n.style ?? {};
3261
3876
  const fill = String(s.fill ?? palette.nodeFill);
3262
3877
  const stroke = String(s.stroke ?? palette.nodeStroke);
3263
3878
  const opts = {
3264
- ...R,
3265
- seed: hashStr$1(n.id),
3266
- fill,
3267
- fillStyle: "solid",
3268
- stroke,
3269
- strokeWidth: Number(s.strokeWidth ?? 1.9),
3879
+ ...R, seed: hashStr$1(n.id),
3880
+ fill, fillStyle: 'solid',
3881
+ stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
3270
3882
  };
3271
3883
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
3272
3884
  const hw = n.w / 2 - 2;
3273
3885
  switch (n.shape) {
3274
- case "circle":
3886
+ case 'circle':
3275
3887
  rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
3276
3888
  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);
3889
+ case 'diamond':
3890
+ rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
3284
3891
  break;
3285
- case "hexagon": {
3892
+ case 'hexagon': {
3286
3893
  const hw2 = hw * 0.56;
3287
3894
  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],
3895
+ [cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
3896
+ [cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
3294
3897
  ], opts);
3295
3898
  break;
3296
3899
  }
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);
3900
+ case 'triangle':
3901
+ 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
3902
  break;
3304
- case "cylinder": {
3903
+ case 'cylinder': {
3305
3904
  const eH = 18;
3306
3905
  rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
3307
3906
  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
- });
3907
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
3313
3908
  break;
3314
3909
  }
3315
- case "parallelogram":
3910
+ case 'parallelogram':
3316
3911
  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],
3912
+ [n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
3913
+ [n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
3321
3914
  ], opts);
3322
3915
  break;
3323
- case "text":
3324
- break; // text nodes: no background shape
3325
- case "image": {
3916
+ case 'text':
3917
+ break; // no shape drawn
3918
+ case 'image': {
3326
3919
  if (n.imageUrl) {
3327
3920
  const img = new Image();
3328
- img.crossOrigin = "anonymous";
3921
+ img.crossOrigin = 'anonymous';
3329
3922
  img.onload = () => {
3330
3923
  ctx.save();
3331
- // rounded clip
3332
3924
  ctx.beginPath();
3333
3925
  const r = 6;
3334
3926
  ctx.moveTo(n.x + r, n.y);
@@ -3344,21 +3936,12 @@ var AIDiagram = (function (exports) {
3344
3936
  ctx.clip();
3345
3937
  ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
3346
3938
  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
- });
3939
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
3352
3940
  };
3353
3941
  img.src = n.imageUrl;
3354
3942
  }
3355
3943
  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
- });
3944
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
3362
3945
  }
3363
3946
  return;
3364
3947
  }
@@ -3367,57 +3950,52 @@ var AIDiagram = (function (exports) {
3367
3950
  break;
3368
3951
  }
3369
3952
  }
3370
- // ── Arrowhead ─────────────────────────────────────────────
3953
+ // ── Arrowhead ─────────────────────────────────────────────────────────────
3371
3954
  function drawArrowHead(rc, x, y, angle, col, seed, R) {
3372
3955
  const as = 12;
3373
3956
  rc.polygon([
3374
3957
  [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
- });
3958
+ [x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
3959
+ [x - as * Math.cos(angle + Math.PI / 6.5), y - as * Math.sin(angle + Math.PI / 6.5)],
3960
+ ], { roughness: 0.3, seed, fill: col, fillStyle: 'solid', stroke: col, strokeWidth: 0.8 });
3391
3961
  }
3962
+ // ── Public API ─────────────────────────────────────────────────────────────
3392
3963
  function renderToCanvas(sg, canvas, options = {}) {
3393
- if (typeof rough === "undefined")
3394
- throw new Error("rough.js not loaded");
3964
+ if (typeof rough === 'undefined')
3965
+ throw new Error('rough.js not loaded');
3395
3966
  const scale = options.scale ?? window.devicePixelRatio ?? 1;
3396
3967
  canvas.width = sg.width * scale;
3397
3968
  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");
3969
+ canvas.style.width = sg.width + 'px';
3970
+ canvas.style.height = sg.height + 'px';
3971
+ const ctx = canvas.getContext('2d');
3401
3972
  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"));
3973
+ // ── Palette ──────────────────────────────────────────────
3974
+ const isDark = options.theme === 'dark' ||
3975
+ (options.theme === 'auto' &&
3976
+ typeof window !== 'undefined' &&
3977
+ window.matchMedia?.('(prefers-color-scheme:dark)').matches);
3978
+ const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? 'dark' : 'light'));
3410
3979
  const palette = resolvePalette(themeName);
3980
+ // ── Diagram-level font ───────────────────────────────────
3981
+ const diagramFont = (() => {
3982
+ const raw = String(sg.config['font'] ?? '');
3983
+ if (raw) {
3984
+ loadFont(raw);
3985
+ return resolveFont(raw);
3986
+ }
3987
+ return DEFAULT_FONT;
3988
+ })();
3989
+ // ── Background ───────────────────────────────────────────
3411
3990
  if (!options.transparent) {
3412
3991
  ctx.fillStyle = options.background ?? palette.background;
3413
3992
  ctx.fillRect(0, 0, sg.width, sg.height);
3414
3993
  }
3994
+ else {
3995
+ ctx.clearRect(0, 0, sg.width, sg.height);
3996
+ }
3415
3997
  const rc = rough.canvas(canvas);
3416
- const R = {
3417
- roughness: options.roughness ?? 1.3,
3418
- bowing: options.bowing ?? 0.7,
3419
- };
3420
- // ── Lookup maps ──────────────────────────────────────────
3998
+ const R = { roughness: options.roughness ?? 1.3, bowing: options.bowing ?? 0.7 };
3421
3999
  const nm = nodeMap(sg);
3422
4000
  const tm = tableMap(sg);
3423
4001
  const gm = groupMap(sg);
@@ -3425,36 +4003,35 @@ var AIDiagram = (function (exports) {
3425
4003
  const ntm = noteMap(sg);
3426
4004
  // ── Title ────────────────────────────────────────────────
3427
4005
  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();
4006
+ const titleSize = Number(sg.config['title-size'] ?? 18);
4007
+ const titleWeight = Number(sg.config['title-weight'] ?? 600);
4008
+ const titleColor = String(sg.config['title-color'] ?? palette.titleText);
4009
+ drawText(ctx, sg.title, sg.width / 2, 28, titleSize, titleWeight, titleColor, 'center', diagramFont);
3434
4010
  }
3435
- // ── Groups (depth-sorted: outermost first) ────────────────
4011
+ // ── Groups (outermost first) ─────────────────────────────
3436
4012
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
3437
4013
  for (const g of sortedGroups) {
3438
4014
  if (!g.w)
3439
4015
  continue;
3440
4016
  const gs = g.style ?? {};
3441
4017
  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),
4018
+ ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
3446
4019
  fill: String(gs.fill ?? palette.groupFill),
3447
- fillStyle: "solid",
4020
+ fillStyle: 'solid',
3448
4021
  stroke: String(gs.stroke ?? palette.groupStroke),
3449
4022
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
3450
4023
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
3451
4024
  });
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();
4025
+ // ── Group label ──────────────────────────────────────
4026
+ // Only render when label has content — empty label = no reserved space
4027
+ // supports: font, font-size, letter-spacing (always left-anchored)
4028
+ if (g.label) {
4029
+ const gFontSize = Number(gs.fontSize ?? 12);
4030
+ const gFont = resolveStyleFont(gs, diagramFont);
4031
+ const gLetterSpacing = gs.letterSpacing;
4032
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
4033
+ drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
4034
+ }
3458
4035
  }
3459
4036
  // ── Edges ─────────────────────────────────────────────────
3460
4037
  for (const e of sg.edges) {
@@ -3471,62 +4048,75 @@ var AIDiagram = (function (exports) {
3471
4048
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
3472
4049
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
3473
4050
  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;
4051
+ const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
4052
+ const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
4053
+ const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
4054
+ const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
3478
4055
  rc.line(sx1, sy1, sx2, sy2, {
3479
- ...R,
3480
- roughness: 0.9,
3481
- seed: hashStr$1(e.from + e.to),
4056
+ ...R, roughness: 0.9, seed: hashStr$1(e.from + e.to),
3482
4057
  stroke: ecol,
3483
4058
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
3484
4059
  ...(dashed ? { strokeLineDash: [6, 5] } : {}),
3485
4060
  });
3486
4061
  const ang = Math.atan2(y2 - y1, x2 - x1);
3487
- if (arrowAt === "end" || arrowAt === "both")
4062
+ if (arrowAt === 'end' || arrowAt === 'both')
3488
4063
  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"));
4064
+ if (arrowAt === 'start' || arrowAt === 'both')
4065
+ drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + 'back'));
3491
4066
  if (e.label) {
3492
4067
  const mx = (x1 + x2) / 2 - ny * 14;
3493
4068
  const my = (y1 + y2) / 2 + nx * 14;
4069
+ // ── Edge label: font, font-size, letter-spacing ──
4070
+ // always center-anchored (single line)
4071
+ const eFontSize = Number(e.style?.fontSize ?? 11);
4072
+ const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
4073
+ const eLetterSpacing = e.style?.letterSpacing;
3494
4074
  ctx.save();
3495
- ctx.font = "400 11px system-ui, sans-serif";
3496
- ctx.textAlign = "center";
4075
+ ctx.font = `400 ${eFontSize}px ${eFont}`;
3497
4076
  const tw = ctx.measureText(e.label).width + 12;
4077
+ ctx.restore();
3498
4078
  ctx.fillStyle = palette.edgeLabelBg;
3499
4079
  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();
4080
+ drawText(ctx, e.label, mx, my + 3, eFontSize, 400, palette.edgeLabelText, 'center', eFont, eLetterSpacing);
3503
4081
  }
3504
4082
  }
3505
4083
  // ── Nodes ─────────────────────────────────────────────────
3506
4084
  for (const n of sg.nodes) {
3507
4085
  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);
4086
+ // ── Node / text typography ─────────────────────────
4087
+ // supports: font, font-size, letter-spacing, text-align,
4088
+ // vertical-align, line-height, word-wrap (text shape)
4089
+ const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
4090
+ const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
4091
+ const textColor = String(n.style?.color ??
4092
+ (n.shape === 'text' ? palette.edgeLabelText : palette.nodeText));
4093
+ const nodeFont = resolveStyleFont(n.style ?? {}, diagramFont);
4094
+ const textAlign = String(n.style?.textAlign ?? 'center');
4095
+ const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
4096
+ const letterSpacing = n.style?.letterSpacing;
4097
+ const vertAlign = String(n.style?.verticalAlign ?? 'middle');
4098
+ // x shifts for left/right alignment
4099
+ const textX = textAlign === 'left' ? n.x + 8
4100
+ : textAlign === 'right' ? n.x + n.w - 8
4101
+ : n.x + n.w / 2;
4102
+ // word-wrap for text shape; explicit \n for all others
4103
+ const rawLines = n.label.split('\n');
4104
+ const lines = n.shape === 'text' && rawLines.length === 1
4105
+ ? wrapText(n.label, n.w - 16, fontSize)
4106
+ : rawLines;
4107
+ // vertical-align: compute textCY from top/middle/bottom
4108
+ const nodeBodyTop = n.y + 6;
4109
+ const nodeBodyBottom = n.y + n.h - 6;
4110
+ const blockH = (lines.length - 1) * lineHeight;
4111
+ const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
4112
+ : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
4113
+ : n.y + n.h / 2; // middle (default)
4114
+ if (lines.length > 1) {
4115
+ drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
3521
4116
  }
3522
4117
  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
- });
4118
+ drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
3528
4119
  }
3529
- ctx.restore();
3530
4120
  }
3531
4121
  // ── Tables ────────────────────────────────────────────────
3532
4122
  for (const t of sg.tables) {
@@ -3535,65 +4125,53 @@ var AIDiagram = (function (exports) {
3535
4125
  const strk = String(gs.stroke ?? palette.tableStroke);
3536
4126
  const textCol = String(gs.color ?? palette.tableText);
3537
4127
  const pad = t.labelH;
3538
- // Outer border
4128
+ // ── Table-level font ────────────────────────────────
4129
+ // supports: font, font-size, letter-spacing
4130
+ // cells also support text-align
4131
+ const tFontSize = Number(gs.fontSize ?? 12);
4132
+ const tFont = resolveStyleFont(gs, diagramFont);
4133
+ const tLetterSpacing = gs.letterSpacing;
3539
4134
  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,
4135
+ ...R, seed: hashStr$1(t.id),
4136
+ fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.5,
3546
4137
  });
3547
- // Label strip separator
3548
4138
  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,
4139
+ roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
3553
4140
  });
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
4141
+ // ── Table label: always left-anchored ───────────────
4142
+ drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
3563
4143
  let rowY = t.y + pad;
3564
4144
  for (const row of t.rows) {
3565
- const rh = row.kind === "header" ? t.headerH : t.rowH;
3566
- // Header background
3567
- if (row.kind === "header") {
4145
+ const rh = row.kind === 'header' ? t.headerH : t.rowH;
4146
+ if (row.kind === 'header') {
3568
4147
  ctx.fillStyle = palette.tableHeaderFill;
3569
4148
  ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
3570
4149
  }
3571
- // Row separator
3572
4150
  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,
4151
+ roughness: 0.4, seed: hashStr$1(t.id + rowY),
4152
+ stroke: row.kind === 'header' ? strk : palette.tableDivider,
4153
+ strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
3577
4154
  });
3578
- // Cell text + column separators
4155
+ // ── Cell text ───────────────────────────────────
4156
+ // header always centered; data rows respect gs.textAlign
4157
+ const cellAlignProp = (row.kind === 'header'
4158
+ ? 'center'
4159
+ : String(gs.textAlign ?? 'center'));
4160
+ const cellFw = row.kind === 'header' ? 600 : 400;
4161
+ const cellColor = row.kind === 'header'
4162
+ ? String(gs.color ?? palette.tableHeaderText)
4163
+ : textCol;
3579
4164
  let cx = t.x;
3580
4165
  row.cells.forEach((cell, i) => {
3581
4166
  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();
4167
+ const cellX = cellAlignProp === 'left' ? cx + 6
4168
+ : cellAlignProp === 'right' ? cx + cw - 6
4169
+ : cx + cw / 2;
4170
+ drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
3591
4171
  if (i < row.cells.length - 1) {
3592
4172
  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,
4173
+ roughness: 0.3, seed: hashStr$1(t.id + 'c' + i),
4174
+ stroke: palette.tableDivider, strokeWidth: 0.5,
3597
4175
  });
3598
4176
  }
3599
4177
  cx += cw;
@@ -3608,44 +4186,106 @@ var AIDiagram = (function (exports) {
3608
4186
  const strk = String(gs.stroke ?? palette.noteStroke);
3609
4187
  const fold = 14;
3610
4188
  const { x, y, w, h } = n;
3611
- // Note body (folded corner polygon)
3612
4189
  rc.polygon([
3613
4190
  [x, y],
3614
4191
  [x + w - fold, y],
3615
4192
  [x + w, y + fold],
3616
4193
  [x + w, y + h],
3617
4194
  [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
4195
+ ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.2 });
3627
4196
  rc.polygon([
3628
4197
  [x + w - fold, y],
3629
4198
  [x + w, y + fold],
3630
4199
  [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();
4200
+ ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
4201
+ fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
4202
+ // ── Note typography ─────────────────────────────────
4203
+ // supports: font, font-size, letter-spacing, text-align,
4204
+ // vertical-align, line-height
4205
+ const nFontSize = Number(gs.fontSize ?? 12);
4206
+ const nFont = resolveStyleFont(gs, diagramFont);
4207
+ const nLetterSpacing = gs.letterSpacing;
4208
+ const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
4209
+ const nTextAlign = String(gs.textAlign ?? 'left');
4210
+ const nVertAlign = String(gs.verticalAlign ?? 'top');
4211
+ const nColor = String(gs.color ?? palette.noteText);
4212
+ const nTextX = nTextAlign === 'right' ? x + w - fold - 6
4213
+ : nTextAlign === 'center' ? x + (w - fold) / 2
4214
+ : x + 12;
4215
+ // vertical-align inside note body (below fold)
4216
+ const bodyTop = y + fold + 8;
4217
+ const bodyBottom = y + h - 8;
4218
+ const blockH = (n.lines.length - 1) * nLineHeight;
4219
+ const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
4220
+ : nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
4221
+ : bodyTop + blockH / 2; // top (default)
4222
+ if (n.lines.length > 1) {
4223
+ drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
4224
+ }
4225
+ else {
4226
+ drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
4227
+ }
4228
+ }
4229
+ // ── Markdown blocks ────────────────────────────────────────
4230
+ // Renders prose with Markdown headings and bold/italic inline spans.
4231
+ // Canvas has no native bold-within-a-run, so each run is drawn
4232
+ // individually with its own ctx.font setting.
4233
+ for (const m of (sg.markdowns ?? [])) {
4234
+ const mFont = resolveStyleFont(m.style, diagramFont);
4235
+ const baseColor = String(m.style?.color ?? palette.nodeText);
4236
+ const textAlign = String(m.style?.textAlign ?? 'left');
4237
+ const PAD = Number(m.style?.padding ?? 16);
4238
+ const anchorX = textAlign === 'right' ? m.x + m.w - PAD
4239
+ : textAlign === 'center' ? m.x + m.w / 2
4240
+ : m.x + PAD;
4241
+ let y = m.y + PAD;
4242
+ for (const line of m.lines) {
4243
+ if (line.kind === 'blank') {
4244
+ y += LINE_SPACING.blank;
4245
+ continue;
4246
+ }
4247
+ const fontSize = LINE_FONT_SIZE[line.kind];
4248
+ const fontWeight = LINE_FONT_WEIGHT[line.kind];
4249
+ const lineY = y + fontSize / 2;
4250
+ // Measure total run width for left-offset when runs mix bold/italic
4251
+ // Simple: draw each run consecutively from a computed start x
4252
+ ctx.save();
4253
+ ctx.textBaseline = 'middle';
4254
+ ctx.fillStyle = baseColor;
4255
+ if (textAlign === 'center' || textAlign === 'right') {
4256
+ // Measure full line width first
4257
+ let totalW = 0;
4258
+ for (const run of line.runs) {
4259
+ const runStyle = run.italic ? 'italic ' : '';
4260
+ const runWeight = run.bold ? 700 : fontWeight;
4261
+ ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
4262
+ totalW += ctx.measureText(run.text).width;
4263
+ }
4264
+ let runX = textAlign === 'center' ? anchorX - totalW / 2 : anchorX - totalW;
4265
+ ctx.textAlign = 'left';
4266
+ for (const run of line.runs) {
4267
+ const runStyle = run.italic ? 'italic ' : '';
4268
+ const runWeight = run.bold ? 700 : fontWeight;
4269
+ ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
4270
+ ctx.fillText(run.text, runX, lineY);
4271
+ runX += ctx.measureText(run.text).width;
4272
+ }
4273
+ }
4274
+ else {
4275
+ // left-aligned — draw runs left to right from anchorX
4276
+ let runX = anchorX;
4277
+ ctx.textAlign = 'left';
4278
+ for (const run of line.runs) {
4279
+ const runStyle = run.italic ? 'italic ' : '';
4280
+ const runWeight = run.bold ? 700 : fontWeight;
4281
+ ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
4282
+ ctx.fillText(run.text, runX, lineY);
4283
+ runX += ctx.measureText(run.text).width;
4284
+ }
4285
+ }
4286
+ ctx.restore();
4287
+ y += LINE_SPACING[line.kind];
4288
+ }
3649
4289
  }
3650
4290
  // ── Charts ────────────────────────────────────────────────
3651
4291
  for (const c of sg.charts) {
@@ -3657,19 +4297,19 @@ var AIDiagram = (function (exports) {
3657
4297
  }, R);
3658
4298
  }
3659
4299
  }
3660
- // ── Export canvas to PNG blob ─────────────────────────────
4300
+ // ── Export helpers ─────────────────────────────────────────────────────────
3661
4301
  function canvasToPNGBlob(canvas) {
3662
4302
  return new Promise((resolve, reject) => {
3663
- canvas.toBlob((blob) => {
4303
+ canvas.toBlob(blob => {
3664
4304
  if (blob)
3665
4305
  resolve(blob);
3666
4306
  else
3667
- reject(new Error("Canvas toBlob failed"));
3668
- }, "image/png");
4307
+ reject(new Error('Canvas toBlob failed'));
4308
+ }, 'image/png');
3669
4309
  });
3670
4310
  }
3671
4311
  function canvasToPNGDataURL(canvas) {
3672
- return canvas.toDataURL("image/png");
4312
+ return canvas.toDataURL('image/png');
3673
4313
  }
3674
4314
 
3675
4315
  // ============================================================
@@ -4604,6 +5244,89 @@ var AIDiagram = (function (exports) {
4604
5244
  }
4605
5245
  }
4606
5246
 
5247
+ // ============================================================
5248
+ // sketchmark — Encrypted sharing
5249
+ // Diagram DSL is encrypted in the browser.
5250
+ // The server stores an opaque blob it cannot read.
5251
+ // The decryption key lives only in the URL fragment (#key=...).
5252
+ // ============================================================
5253
+ const WORKER_URL = 'https://sketchmark.anmism7.workers.dev/';
5254
+ // ── Crypto helpers ────────────────────────────────────────
5255
+ async function generateKey() {
5256
+ return crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, // extractable so we can export to URL
5257
+ ['encrypt', 'decrypt']);
5258
+ }
5259
+ async function keyToBase64(key) {
5260
+ const raw = await crypto.subtle.exportKey('raw', key);
5261
+ return btoa(String.fromCharCode(...new Uint8Array(raw)));
5262
+ }
5263
+ async function base64ToKey(b64) {
5264
+ const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
5265
+ return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, // not extractable on the receiving end
5266
+ ['decrypt']);
5267
+ }
5268
+ // ── Encrypt ───────────────────────────────────────────────
5269
+ async function encryptDSL(dsl, key) {
5270
+ const iv = crypto.getRandomValues(new Uint8Array(12));
5271
+ const encoded = new TextEncoder().encode(dsl);
5272
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
5273
+ // prepend iv to the blob: [ iv (12 bytes) | ciphertext ]
5274
+ const result = new Uint8Array(12 + encrypted.byteLength);
5275
+ result.set(iv, 0);
5276
+ result.set(new Uint8Array(encrypted), 12);
5277
+ return result;
5278
+ }
5279
+ // ── Decrypt ───────────────────────────────────────────────
5280
+ async function decryptBlob(blob, key) {
5281
+ const iv = blob.slice(0, 12);
5282
+ const ciphertext = blob.slice(12);
5283
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
5284
+ return new TextDecoder().decode(decrypted);
5285
+ }
5286
+ // ── Public API ────────────────────────────────────────────
5287
+ /**
5288
+ * Encrypt DSL, upload to worker, return shareable URL.
5289
+ * The URL fragment (#key=...) never reaches the server.
5290
+ */
5291
+ async function shareDiagram(dsl) {
5292
+ const key = await generateKey();
5293
+ const blob = await encryptDSL(dsl, key);
5294
+ const keyB64 = await keyToBase64(key);
5295
+ const res = await fetch(`${WORKER_URL}/api/blob`, {
5296
+ method: 'POST',
5297
+ headers: { 'Content-Type': 'application/octet-stream' },
5298
+ body: blob.buffer,
5299
+ });
5300
+ if (!res.ok)
5301
+ throw new Error(`Upload failed: ${res.status}`);
5302
+ const { id } = await res.json();
5303
+ // key goes into the fragment — browser never sends this to any server
5304
+ return `${window.location.origin}/playground.html?s=${id}#key=${keyB64}`;
5305
+ }
5306
+ /**
5307
+ * Read ?s= and #key= from the current URL, fetch + decrypt the diagram.
5308
+ * Returns null if no share params found.
5309
+ */
5310
+ async function loadSharedDiagram() {
5311
+ const params = new URLSearchParams(window.location.search);
5312
+ const id = params.get('s');
5313
+ if (!id)
5314
+ return null;
5315
+ // key is in the fragment — parse manually, not via URLSearchParams
5316
+ // (URLSearchParams on hash strips the #)
5317
+ const fragment = window.location.hash.slice(1);
5318
+ const keyMatch = fragment.match(/key=([^&]+)/);
5319
+ if (!keyMatch)
5320
+ return null;
5321
+ const keyB64 = keyMatch[1];
5322
+ const res = await fetch(`${WORKER_URL}/api/blob/${id}`);
5323
+ if (!res.ok)
5324
+ throw new Error('Diagram not found or expired');
5325
+ const blob = await res.arrayBuffer();
5326
+ const key = await base64ToKey(keyB64);
5327
+ return decryptBlob(blob, key);
5328
+ }
5329
+
4607
5330
  // ============================================================
4608
5331
  // sketchmark — Public API
4609
5332
  // ============================================================
@@ -4670,6 +5393,7 @@ var AIDiagram = (function (exports) {
4670
5393
 
4671
5394
  exports.ANIMATION_CSS = ANIMATION_CSS;
4672
5395
  exports.AnimationController = AnimationController;
5396
+ exports.BUILTIN_FONTS = BUILTIN_FONTS;
4673
5397
  exports.EventEmitter = EventEmitter;
4674
5398
  exports.PALETTES = PALETTES;
4675
5399
  exports.ParseError = ParseError;
@@ -4693,13 +5417,19 @@ var AIDiagram = (function (exports) {
4693
5417
  exports.layout = layout;
4694
5418
  exports.lerp = lerp;
4695
5419
  exports.listThemes = listThemes;
5420
+ exports.loadFont = loadFont;
5421
+ exports.loadSharedDiagram = loadSharedDiagram;
5422
+ exports.markdownMap = markdownMap;
4696
5423
  exports.nodeMap = nodeMap;
4697
5424
  exports.parse = parse;
4698
5425
  exports.parseHex = parseHex;
5426
+ exports.registerFont = registerFont;
4699
5427
  exports.render = render;
4700
5428
  exports.renderToCanvas = renderToCanvas;
4701
5429
  exports.renderToSVG = renderToSVG;
5430
+ exports.resolveFont = resolveFont;
4702
5431
  exports.resolvePalette = resolvePalette;
5432
+ exports.shareDiagram = shareDiagram;
4703
5433
  exports.sleep = sleep;
4704
5434
  exports.svgToPNGDataURL = svgToPNGDataURL;
4705
5435
  exports.svgToString = svgToString;