@markdy/core 1.1.7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ The parser and AST types for [MarkdyScript](../../docs/SYNTAX.md) — a diagram-
7
7
  - **Zero runtime dependencies** — pure TypeScript, no DOM or platform APIs (~14 KB minzipped)
8
8
  - **Single-pass parser** — line-by-line state machine with strict `ParseError` line-number diagnostics
9
9
  - **17 Specialized Layout Engines** — `architecture`, `flowchart`, `tree`, `sequence`, `state`, `layers`, `nested`, `swimlane`, `timeline`, `gantt`, `medallion`, `flywheel`, `constellation`, `quadrant`, `pyramid`, `radar`, `venn`
10
- - **8 Semantic Themes** — `paper`, `editorial`, `midnight`, `blueprint`, `graphite`, `nebula`, `terminal`, `sketchy`
10
+ - **10 Semantic Themes** — `paper`, `editorial`, `midnight`, `blueprint`, `graphite`, `nebula`, `terminal`, `sketchy`, `ink`, `doodle`
11
11
  - **Content-Adaptive Canvas Sizing** — automatically calculates optimal aspect ratio and bounds based on diagram items and topology
12
12
  - **Well-Architected Governance & AST Diffing** — layer boundaries, deadlock cycle detection, gateway isolation, and semantic AST evolution
13
13
  - **Isomorphic** — runs in Node.js, Deno, Bun, edge runtimes, and the browser
@@ -93,7 +93,7 @@ try {
93
93
  | `validateArchitectureRules` | `(ast) => Diagnostic[]` | Run Well-Architected rules: cycle detection, layer boundaries, and gateway checks |
94
94
  | `diffAST` | `(astA, astB) => ASTDiffResult` | Compare architecture versions, calculate diff metrics, and generate migration scenes |
95
95
  | `compressUrlState` / `decompressUrlState` | `(code, opts?) => string` | Zero-dependency URL hash state encoder for shareable playground links |
96
- | `THEMES` / `resolveTheme` | tokens / function | 8 Semantic theme palettes (`paper`, `editorial`, `nebula`, `midnight`, `blueprint`, `graphite`, `terminal`, `sketchy`) |
96
+ | `THEMES` / `resolveTheme` | tokens / function | 10 Semantic theme palettes (`paper`, `editorial`, `nebula`, `midnight`, `blueprint`, `graphite`, `terminal`, `sketchy`, `ink`, `doodle`) |
97
97
 
98
98
  ## Documentation
99
99
 
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ type PlayerControlsConfig = {
25
25
  resetView?: boolean;
26
26
  fullscreen?: boolean;
27
27
  svg?: boolean;
28
+ gif?: boolean;
28
29
  share?: boolean;
29
30
  /** Show the MarkdyScript source code when clicked. */
30
31
  code?: boolean;
@@ -82,6 +83,8 @@ type SceneMeta = {
82
83
  explicitWidth?: boolean;
83
84
  /** Whether height was explicitly specified by the author in the script. */
84
85
  explicitHeight?: boolean;
86
+ /** Whether theme was explicitly specified by the author in the script. */
87
+ explicitTheme?: boolean;
85
88
  /** Opt-in diagram mode; defaults to architecture. */
86
89
  type?: DiagramType;
87
90
  /** Playback, controls, interaction, and chrome behavior. Source of truth. */
package/dist/index.js CHANGED
@@ -397,6 +397,7 @@ var CONTROL_KEYS = [
397
397
  "resetView",
398
398
  "fullscreen",
399
399
  "svg",
400
+ "gif",
400
401
  "share",
401
402
  "code",
402
403
  "theme"
@@ -448,6 +449,11 @@ var CONTROLS = {
448
449
  svg: { group: "controls", key: "svg", type: "boolean" },
449
450
  exportSvg: { group: "controls", key: "svg", type: "boolean" },
450
451
  export_svg: { group: "controls", key: "svg", type: "boolean" },
452
+ gif: { group: "controls", key: "gif", type: "boolean" },
453
+ exportGif: { group: "controls", key: "gif", type: "boolean" },
454
+ export_gif: { group: "controls", key: "gif", type: "boolean" },
455
+ gifButton: { group: "controls", key: "gif", type: "boolean" },
456
+ gif_button: { group: "controls", key: "gif", type: "boolean" },
451
457
  share: { group: "controls", key: "share", type: "boolean" },
452
458
  shareLink: { group: "controls", key: "share", type: "boolean" },
453
459
  share_link: { group: "controls", key: "share", type: "boolean" },
@@ -528,6 +534,11 @@ var FLAT = {
528
534
  fullscreen_button: CONTROLS.fullscreen_button,
529
535
  exportSvg: CONTROLS.exportSvg,
530
536
  export_svg: CONTROLS.export_svg,
537
+ gif: CONTROLS.gif,
538
+ exportGif: CONTROLS.exportGif,
539
+ export_gif: CONTROLS.export_gif,
540
+ gifButton: CONTROLS.gifButton,
541
+ gif_button: CONTROLS.gif_button,
531
542
  shareLink: CONTROLS.shareLink,
532
543
  share_link: CONTROLS.share_link,
533
544
  code: CONTROLS.code,
@@ -643,6 +654,7 @@ function resolvePlayer(config = {}, overrides = {}) {
643
654
  resetView: resolveControl(configuredControls.resetView),
644
655
  fullscreen: resolveControl(configuredControls.fullscreen),
645
656
  svg: resolveControl(configuredControls.svg),
657
+ gif: resolveControl(configuredControls.gif, false),
646
658
  share: resolveControl(configuredControls.share),
647
659
  code: resolveControl(configuredControls.code, false),
648
660
  theme: resolveControl(configuredControls.theme, hostControlDefault)
@@ -791,6 +803,38 @@ var NODE_W = 180;
791
803
  var NODE_H = 76;
792
804
  var VENN_NODE_SIZE = 220;
793
805
  var GROUP_PAD = 28;
806
+ function computeNodeDimensions(decl, baseW = 180, baseH = 76) {
807
+ if (typeof decl.props?.width === "number") {
808
+ return {
809
+ width: decl.props.width,
810
+ height: typeof decl.props.height === "number" ? decl.props.height : baseH
811
+ };
812
+ }
813
+ const labelLen = (decl.label || "").length;
814
+ const tech = decl.props?.tech ?? decl.props?.sub;
815
+ const techLen = tech ? String(tech).length : 0;
816
+ const val = decl.props?.value ?? decl.props?.metric;
817
+ const valLen = val ? String(val).length : 0;
818
+ if (decl.kind === "dot") return { width: 64, height: 64 };
819
+ if (decl.kind === "matrix") return { width: 220, height: 96 };
820
+ const valueW = valLen > 0 ? Math.max(50, valLen * 9.5 + 16) : 0;
821
+ const neededLabelChars = labelLen > 18 ? Math.ceil(labelLen / 2) : labelLen;
822
+ const maxChars = Math.max(neededLabelChars, techLen);
823
+ const neededTextW = Math.max(88, maxChars * 7.8);
824
+ const calculatedW = 56 + neededTextW + (valueW > 0 ? valueW + 14 : 0) + 16;
825
+ const minW = Math.max(baseW, Math.min(360, calculatedW));
826
+ let width = Math.ceil(minW / 8) * 8;
827
+ let height = baseH;
828
+ if (labelLen > 24 && techLen > 0) {
829
+ height = Math.max(height, 84);
830
+ }
831
+ return { width, height };
832
+ }
833
+ function resolveNodeStyle(decl, ast) {
834
+ if (!decl.style) return void 0;
835
+ if (typeof decl.style === "string") return ast.styles[decl.style]?.props;
836
+ return decl.style;
837
+ }
794
838
  var DEFAULTS = {
795
839
  show: 0.35,
796
840
  hide: 0.35,
@@ -805,7 +849,13 @@ var DEFAULTS = {
805
849
  function diagramType(ast) {
806
850
  return ast.meta.type ?? "architecture";
807
851
  }
808
- function nodeShape(kind, dtype) {
852
+ function nodeShape(kind, dtype, props) {
853
+ if (typeof props?.shape === "string") {
854
+ const s = props.shape.toLowerCase();
855
+ if (s === "diamond" || s === "circle" || s === "pill" || s === "terminal" || s === "rounded" || s === "card" || s === "container") {
856
+ return s;
857
+ }
858
+ }
809
859
  if (kind === "terminal") return "terminal";
810
860
  if (kind === "dot" || kind === "marker") return "circle";
811
861
  if (kind === "token_strip" || kind === "chips") return "pill";
@@ -940,22 +990,31 @@ function layoutRanked(ast, edges, opts) {
940
990
  });
941
991
  }
942
992
  const isVertical = direction === "TB" || direction === "BT";
993
+ const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
994
+ const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
995
+ const titleBand = hasTitle ? TITLE_BAND : 0;
996
+ const bottomBand = hasBeatCaptions ? 44 : 0;
943
997
  const contentW = ast.meta.width - SAFE * 2;
944
- const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
945
998
  const maxRank = Math.max(...byRank.keys(), 0);
946
999
  const rankCount = maxRank + 1;
947
1000
  const maxInRank = Math.max(...[...byRank.values()].map((v) => v.length), 1);
948
1001
  const nodes = [];
1002
+ const nodeDims = /* @__PURE__ */ new Map();
1003
+ for (const id of nodeIds) {
1004
+ nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1005
+ }
949
1006
  if (opts.columnLayout) {
950
1007
  const count = nodeIds.length;
951
- const colSpacing = Math.max(NODE_W + 32, contentW / Math.max(count, 1));
952
- const totalW2 = (count - 1) * colSpacing + NODE_W;
1008
+ const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id)?.width ?? NODE_W));
1009
+ const colSpacing = Math.max(maxNodeW + 32, contentW / Math.max(count, 1));
1010
+ const totalW2 = (count - 1) * colSpacing + maxNodeW;
953
1011
  const startX2 = SAFE + Math.max(0, (contentW - totalW2) / 2);
954
1012
  nodeIds.forEach((id, idx) => {
955
1013
  const decl = ast.nodes[id];
956
1014
  const role = nodeRole(decl.kind);
957
- const x = startX2 + idx * colSpacing;
958
- const y = TITLE_BAND + 32;
1015
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1016
+ const x = startX2 + idx * colSpacing + (maxNodeW - dims.width) / 2;
1017
+ const y = (hasTitle ? titleBand : 20) + 32;
959
1018
  nodes.push({
960
1019
  id,
961
1020
  kind: decl.kind,
@@ -963,12 +1022,12 @@ function layoutRanked(ast, edges, opts) {
963
1022
  label: decl.label,
964
1023
  x: snapGrid(x),
965
1024
  y: snapGrid(y),
966
- width: NODE_W,
967
- height: NODE_H,
968
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1025
+ width: dims.width,
1026
+ height: dims.height,
1027
+ style: resolveNodeStyle(decl, ast),
969
1028
  props: decl.props,
970
1029
  opacity: 0,
971
- shape: nodeShape(decl.kind, dtype),
1030
+ shape: nodeShape(decl.kind, dtype, decl.props),
972
1031
  focal: decl.props.focal === true || decl.props.accent === true,
973
1032
  column: idx
974
1033
  });
@@ -976,18 +1035,22 @@ function layoutRanked(ast, edges, opts) {
976
1035
  return nodes;
977
1036
  }
978
1037
  if (isVertical) {
979
- const rowGap = Math.max(NODE_H + 40, Math.min(180, contentH / Math.max(rankCount, 1)));
980
- const totalH = (rankCount - 1) * rowGap + NODE_H;
1038
+ const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1039
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id)?.height ?? NODE_H));
1040
+ const rowGap = Math.max(maxNodeH + 40, Math.min(200, contentH / Math.max(rankCount, 1)));
1041
+ const totalH = (rankCount - 1) * rowGap + maxNodeH;
981
1042
  const startY = TITLE_BAND + Math.max(0, (contentH - totalH) / 2);
982
1043
  for (const [rank, ids] of [...byRank.entries()].sort((a, b) => a[0] - b[0])) {
983
1044
  const rowCount = ids.length;
984
- const colSpacing = Math.max(NODE_W + 36, Math.min(260, contentW / Math.max(rowCount, 1)));
985
- const rankW = (rowCount - 1) * colSpacing + NODE_W;
1045
+ const maxRowW = Math.max(...ids.map((id) => nodeDims.get(id)?.width ?? NODE_W));
1046
+ const colSpacing = Math.max(maxRowW + 36, Math.min(320, contentW / Math.max(rowCount, 1)));
1047
+ const rankW = (rowCount - 1) * colSpacing + maxRowW;
986
1048
  const rankStartX = SAFE + Math.max(0, (contentW - rankW) / 2);
987
1049
  const y = startY + rank * rowGap;
988
1050
  ids.forEach((id, idx) => {
989
1051
  const decl = ast.nodes[id];
990
1052
  const role = nodeRole(decl.kind);
1053
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
991
1054
  const x = rankStartX + idx * colSpacing;
992
1055
  const focal = decl.props.focal === true || decl.props.accent === true;
993
1056
  nodes.push({
@@ -997,32 +1060,44 @@ function layoutRanked(ast, edges, opts) {
997
1060
  label: decl.label,
998
1061
  x: snapGrid(x),
999
1062
  y: snapGrid(y),
1000
- width: NODE_W,
1001
- height: NODE_H,
1002
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1063
+ width: dims.width,
1064
+ height: dims.height,
1065
+ style: resolveNodeStyle(decl, ast),
1003
1066
  props: decl.props,
1004
1067
  opacity: 0,
1005
- shape: nodeShape(decl.kind, dtype),
1068
+ shape: nodeShape(decl.kind, dtype, decl.props),
1006
1069
  focal
1007
1070
  });
1008
1071
  });
1009
1072
  }
1010
1073
  return nodes;
1011
1074
  }
1012
- const maxPossibleStep = rankCount > 1 ? (contentW - NODE_W) / (rankCount - 1) : 0;
1013
- const colGap = rankCount > 1 ? Math.min(300, Math.max(50, maxPossibleStep)) : 0;
1014
- const totalW = (rankCount - 1) * colGap + NODE_W;
1075
+ const rankWidths = /* @__PURE__ */ new Map();
1076
+ for (const [rank, ids] of byRank.entries()) {
1077
+ const maxW = Math.max(...ids.map((id) => nodeDims.get(id)?.width ?? NODE_W));
1078
+ rankWidths.set(rank, maxW);
1079
+ }
1080
+ const totalRankWidths = [...rankWidths.values()].reduce((a, b) => a + b, 0);
1081
+ const minColGap = 56;
1082
+ const availableGapW = Math.max(0, contentW - totalRankWidths);
1083
+ const colGap = rankCount > 1 ? Math.max(minColGap, Math.min(160, availableGapW / (rankCount - 1))) : 0;
1084
+ const totalW = totalRankWidths + (rankCount - 1) * colGap;
1015
1085
  const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
1016
- for (const [rank, ids] of [...byRank.entries()].sort((a, b) => a[0] - b[0])) {
1086
+ const availableH = Math.max(NODE_H, ast.meta.height - titleBand - bottomBand);
1087
+ let currentRankX = startX;
1088
+ for (let rank = 0; rank < rankCount; rank++) {
1089
+ const ids = byRank.get(rank) ?? [];
1090
+ const rankW = rankWidths.get(rank) ?? NODE_W;
1017
1091
  const rowCount = ids.length;
1018
- const maxPossibleRowStep = rowCount > 1 ? (contentH - NODE_H) / (rowCount - 1) : 0;
1019
- const rowSpacing = rowCount > 1 ? Math.min(150, Math.max(36, maxPossibleRowStep)) : 0;
1020
- const colH = (rowCount - 1) * rowSpacing + NODE_H;
1021
- const colStartY = TITLE_BAND + Math.max(0, (contentH - colH) / 2);
1022
- const x = startX + rank * colGap;
1092
+ const maxH = Math.max(...ids.map((id) => nodeDims.get(id)?.height ?? NODE_H), NODE_H);
1093
+ const maxPossibleRowStep = rowCount > 1 ? (availableH - maxH) / (rowCount - 1) : 0;
1094
+ const rowSpacing = rowCount > 1 ? Math.min(180, Math.max(maxH + 28, maxPossibleRowStep)) : 0;
1095
+ const colH = (rowCount - 1) * rowSpacing + maxH;
1096
+ const colStartY = titleBand + Math.max(0, (availableH - colH) / 2);
1023
1097
  ids.forEach((id, idx) => {
1024
1098
  const decl = ast.nodes[id];
1025
1099
  const role = nodeRole(decl.kind);
1100
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1026
1101
  const y = colStartY + idx * rowSpacing;
1027
1102
  const focal = decl.props.focal === true || decl.props.accent === true;
1028
1103
  nodes.push({
@@ -1030,17 +1105,18 @@ function layoutRanked(ast, edges, opts) {
1030
1105
  kind: decl.kind,
1031
1106
  role,
1032
1107
  label: decl.label,
1033
- x: snapGrid(x),
1108
+ x: snapGrid(currentRankX),
1034
1109
  y: snapGrid(y),
1035
- width: NODE_W,
1036
- height: NODE_H,
1037
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1110
+ width: dims.width,
1111
+ height: dims.height,
1112
+ style: resolveNodeStyle(decl, ast),
1038
1113
  props: decl.props,
1039
1114
  opacity: 0,
1040
- shape: nodeShape(decl.kind, dtype),
1115
+ shape: nodeShape(decl.kind, dtype, decl.props),
1041
1116
  focal
1042
1117
  });
1043
1118
  });
1119
+ currentRankX += rankW + colGap;
1044
1120
  }
1045
1121
  return nodes;
1046
1122
  }
@@ -1141,7 +1217,7 @@ function layoutTree(ast, edges) {
1141
1217
  y: snapGrid(pos.y),
1142
1218
  width: NODE_W,
1143
1219
  height: NODE_H,
1144
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1220
+ style: resolveNodeStyle(decl, ast),
1145
1221
  props: decl.props,
1146
1222
  opacity: 0,
1147
1223
  shape: "card",
@@ -1209,10 +1285,10 @@ function layoutConstellation(ast) {
1209
1285
  y: snapGrid(y),
1210
1286
  width: NODE_W,
1211
1287
  height: NODE_H,
1212
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1288
+ style: resolveNodeStyle(decl, ast),
1213
1289
  props: decl.props,
1214
1290
  opacity: 0,
1215
- shape: nodeShape(decl.kind, "constellation"),
1291
+ shape: nodeShape(decl.kind, "constellation", decl.props),
1216
1292
  focal: isFocal || decl.props.focal === true || decl.props.accent === true
1217
1293
  });
1218
1294
  }
@@ -1257,7 +1333,7 @@ function layoutLoop(ast) {
1257
1333
  y: snapGrid(y),
1258
1334
  width: isHub ? snapGrid(NODE_W * 1.25) : NODE_W,
1259
1335
  height: isHub ? snapGrid(NODE_H * 1.15) : NODE_H,
1260
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1336
+ style: resolveNodeStyle(decl, ast),
1261
1337
  props: decl.props,
1262
1338
  opacity: 0,
1263
1339
  shape: isHub ? "pill" : "card",
@@ -1311,7 +1387,7 @@ function layoutMedallion(ast, edges) {
1311
1387
  y: snapGrid(rowY),
1312
1388
  width: NODE_W,
1313
1389
  height: NODE_H,
1314
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1390
+ style: resolveNodeStyle(decl, ast),
1315
1391
  props: decl.props,
1316
1392
  opacity: 0,
1317
1393
  shape: "card",
@@ -1373,7 +1449,7 @@ function layoutQuadrant(ast) {
1373
1449
  y: snapGrid(y),
1374
1450
  width: NODE_W,
1375
1451
  height: NODE_H,
1376
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1452
+ style: resolveNodeStyle(decl, ast),
1377
1453
  props: decl.props,
1378
1454
  opacity: 0,
1379
1455
  shape: "card",
@@ -1426,7 +1502,7 @@ function layoutSwimlane(ast, edges) {
1426
1502
  y: snapGrid(laneY),
1427
1503
  width: NODE_W,
1428
1504
  height: NODE_H,
1429
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1505
+ style: resolveNodeStyle(decl, ast),
1430
1506
  props: decl.props,
1431
1507
  opacity: 0,
1432
1508
  shape: "card",
@@ -1475,7 +1551,7 @@ function layoutPyramid(ast) {
1475
1551
  y: snapGrid(tierY),
1476
1552
  width: snapGrid(nodeW),
1477
1553
  height: NODE_H,
1478
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1554
+ style: resolveNodeStyle(decl, ast),
1479
1555
  props: decl.props,
1480
1556
  opacity: 0,
1481
1557
  shape: "card",
@@ -1508,7 +1584,7 @@ function layoutTimeline(ast) {
1508
1584
  y: snapGrid(y),
1509
1585
  width: NODE_W,
1510
1586
  height: NODE_H,
1511
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1587
+ style: resolveNodeStyle(decl, ast),
1512
1588
  props: decl.props,
1513
1589
  opacity: 0,
1514
1590
  shape: "pill",
@@ -1551,7 +1627,7 @@ function layoutGantt(ast) {
1551
1627
  y: snapGrid(y),
1552
1628
  width: snapGrid(w),
1553
1629
  height: barH,
1554
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1630
+ style: resolveNodeStyle(decl, ast),
1555
1631
  props: decl.props,
1556
1632
  opacity: 0,
1557
1633
  shape: "pill",
@@ -1591,7 +1667,7 @@ function layoutVenn(ast) {
1591
1667
  y: snapGrid(y),
1592
1668
  width: VENN_NODE_SIZE,
1593
1669
  height: VENN_NODE_SIZE,
1594
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1670
+ style: resolveNodeStyle(decl, ast),
1595
1671
  props: decl.props,
1596
1672
  opacity: 0,
1597
1673
  shape: "circle",
@@ -1625,7 +1701,7 @@ function layoutLayers(ast) {
1625
1701
  y: snapGrid(y),
1626
1702
  width: snapGrid(layerW),
1627
1703
  height: snapGrid(layerH),
1628
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1704
+ style: resolveNodeStyle(decl, ast),
1629
1705
  props: decl.props,
1630
1706
  opacity: 0,
1631
1707
  shape: "rounded",
@@ -1672,7 +1748,7 @@ function layoutNested(ast) {
1672
1748
  y: snapGrid(y),
1673
1749
  width: snapGrid(w),
1674
1750
  height: snapGrid(h),
1675
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1751
+ style: resolveNodeStyle(decl, ast),
1676
1752
  props: decl.props,
1677
1753
  opacity: 0,
1678
1754
  shape: isCore ? "card" : "container",
@@ -1706,7 +1782,7 @@ function layoutRadar(ast) {
1706
1782
  y: snapGrid(y),
1707
1783
  width: NODE_W,
1708
1784
  height: NODE_H,
1709
- style: decl.style ? ast.styles[decl.style]?.props : void 0,
1785
+ style: resolveNodeStyle(decl, ast),
1710
1786
  props: decl.props,
1711
1787
  opacity: 0,
1712
1788
  shape: "rounded",
@@ -2094,17 +2170,32 @@ function computeAdaptiveDimensions(ast, edges) {
2094
2170
  autoH = Math.max(720, Math.min(2e3, requiredH));
2095
2171
  }
2096
2172
  } else {
2097
- const requiredW = SAFE * 2 + rankCount * NODE_W + (rankCount - 1) * 44 + groupPaddingBonus;
2098
- const requiredH = SAFE * 2 + TITLE_BAND + maxInRank * NODE_H + (maxInRank - 1) * 36 + groupPaddingBonus;
2099
- if (nodeCount <= 2 && rankCount <= 2) {
2173
+ const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
2174
+ const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
2175
+ const titleBand = hasTitle ? TITLE_BAND : 0;
2176
+ const bottomBand = hasBeatCaptions ? 44 : 0;
2177
+ const maxRankW = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).width));
2178
+ const maxRankH = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).height));
2179
+ const requiredW = SAFE * 2 + rankCount * maxRankW + (rankCount - 1) * 56 + groupPaddingBonus;
2180
+ const requiredH = SAFE * 2 + titleBand + bottomBand + maxInRank * maxRankH + (maxInRank - 1) * 44 + groupPaddingBonus;
2181
+ if (maxInRank === 1 && !hasTitle) {
2182
+ autoW = Math.max(1024, Math.min(2560, requiredW));
2183
+ autoH = Math.max(hasBeatCaptions ? 240 : 208, Math.min(360, requiredH + 24));
2184
+ } else if (maxInRank === 2 && !hasTitle) {
2185
+ autoW = Math.max(1024, Math.min(2560, requiredW));
2186
+ autoH = Math.max(hasBeatCaptions ? 368 : 336, Math.min(480, requiredH + 24));
2187
+ } else if (maxInRank === 3 && !hasTitle) {
2188
+ autoW = Math.max(1152, Math.min(2560, requiredW));
2189
+ autoH = Math.max(hasBeatCaptions ? 480 : 448, Math.min(640, requiredH + 24));
2190
+ } else if (nodeCount <= 2 && rankCount <= 2) {
2100
2191
  autoW = 1024;
2101
- autoH = 576;
2192
+ autoH = hasTitle ? 576 : 384;
2102
2193
  } else if (nodeCount <= 4 && rankCount <= 3 && maxInRank <= 2) {
2103
2194
  autoW = 1152;
2104
- autoH = 648;
2195
+ autoH = hasTitle ? 648 : 448;
2105
2196
  } else {
2106
2197
  autoW = Math.max(1152, Math.min(2560, requiredW));
2107
- autoH = Math.max(648, Math.min(1600, requiredH));
2198
+ autoH = Math.max(hasTitle ? 648 : maxInRank <= 2 ? 448 : 576, Math.min(1600, requiredH));
2108
2199
  }
2109
2200
  }
2110
2201
  }
@@ -2440,6 +2531,116 @@ var THEMES = {
2440
2531
  },
2441
2532
  radiusMd: 4,
2442
2533
  spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
2534
+ },
2535
+ ink: {
2536
+ name: "ink",
2537
+ canvas: "#f8fafd",
2538
+ surface: "#ffffff",
2539
+ surfaceRaised: "#eff6ff",
2540
+ border: "#1d4ed8",
2541
+ text: "#0f172a",
2542
+ textMuted: "#3b629b",
2543
+ paper: "#f8fafd",
2544
+ ink: "#1e40af",
2545
+ muted: "#4776b2",
2546
+ rule: "rgba(29, 78, 216, 0.14)",
2547
+ soft: "#60a5fa",
2548
+ link: "#2563eb",
2549
+ gridMinor: "rgba(37, 99, 235, 0.05)",
2550
+ gridMajor: "rgba(37, 99, 235, 0.10)",
2551
+ vignette: "rgba(219, 234, 254, 0.45)",
2552
+ accent: "#1d4ed8",
2553
+ accentTint: "rgba(29, 78, 216, 0.08)",
2554
+ nodeSurface: "#ffffff",
2555
+ nodeSurfaceRaised: "#f4f8ff",
2556
+ hairline: "rgba(29, 78, 216, 0.20)",
2557
+ shadow: "rgba(15, 35, 90, 0.09)",
2558
+ labelPlate: "#ffffff",
2559
+ flatCards: false,
2560
+ roles: {
2561
+ ...ROLE_COLORS,
2562
+ compute: "#1d4ed8",
2563
+ code: "#2563eb",
2564
+ client: "#0284c7",
2565
+ data: "#0369a1",
2566
+ messaging: "#4338ca",
2567
+ network: "#0284c7",
2568
+ security: "#1e3a8a",
2569
+ platform: "#3b82f6",
2570
+ observability: "#0891b2",
2571
+ delivery: "#4f46e5",
2572
+ flow: "#2563eb",
2573
+ distributed: "#475569"
2574
+ },
2575
+ edges: {
2576
+ request: "#1d4ed8",
2577
+ response: "#3b82f6",
2578
+ event: "#0284c7",
2579
+ dependency: "#64748b"
2580
+ },
2581
+ fonts: {
2582
+ title: 'ui-serif, "Charter", "Newsreader", "Georgia", "Songti SC", "Noto Serif", serif',
2583
+ nodeName: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif',
2584
+ mono: '"JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace'
2585
+ },
2586
+ radiusMd: 6,
2587
+ spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 },
2588
+ series: ["#1d4ed8", "#0284c7", "#4338ca", "#0891b2", "#1e3a8a"]
2589
+ },
2590
+ doodle: {
2591
+ name: "doodle",
2592
+ canvas: "#fffef9",
2593
+ surface: "#ffffff",
2594
+ surfaceRaised: "#fdf8ec",
2595
+ border: "#18181b",
2596
+ text: "#18181b",
2597
+ textMuted: "#52525b",
2598
+ paper: "#fffef9",
2599
+ ink: "#18181b",
2600
+ muted: "#52525b",
2601
+ rule: "rgba(24, 24, 27, 0.16)",
2602
+ soft: "#71717a",
2603
+ link: "#f43f5e",
2604
+ gridMinor: "rgba(24, 24, 27, 0.05)",
2605
+ gridMajor: "rgba(24, 24, 27, 0.08)",
2606
+ vignette: "rgba(245, 235, 210, 0.35)",
2607
+ accent: "#f43f5e",
2608
+ accentTint: "rgba(244, 63, 94, 0.12)",
2609
+ nodeSurface: "#ffffff",
2610
+ nodeSurfaceRaised: "#fffdf5",
2611
+ hairline: "rgba(24, 24, 27, 0.25)",
2612
+ shadow: "rgba(24, 24, 27, 0.15)",
2613
+ labelPlate: "#ffffff",
2614
+ flatCards: true,
2615
+ roles: {
2616
+ ...ROLE_COLORS,
2617
+ compute: "#2563eb",
2618
+ code: "#2563eb",
2619
+ client: "#f43f5e",
2620
+ data: "#10b981",
2621
+ messaging: "#8b5cf6",
2622
+ network: "#06b6d4",
2623
+ security: "#ef4444",
2624
+ platform: "#f59e0b",
2625
+ observability: "#14b8a6",
2626
+ delivery: "#f59e0b",
2627
+ flow: "#ec4899",
2628
+ distributed: "#71717a"
2629
+ },
2630
+ edges: {
2631
+ request: "#18181b",
2632
+ response: "#71717a",
2633
+ event: "#f43f5e",
2634
+ dependency: "#a1a1aa"
2635
+ },
2636
+ fonts: {
2637
+ title: '"Caveat", "Comic Neue", "Chalkboard SE", "Patrick Hand", "Comic Sans MS", cursive, sans-serif',
2638
+ nodeName: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif',
2639
+ mono: '"JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace'
2640
+ },
2641
+ radiusMd: 12,
2642
+ spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 },
2643
+ series: ["#f43f5e", "#2563eb", "#10b981", "#8b5cf6", "#f59e0b"]
2443
2644
  }
2444
2645
  };
2445
2646
  function resolveTheme(name) {
@@ -3005,7 +3206,7 @@ function validateReferences(ast) {
3005
3206
  }
3006
3207
  }
3007
3208
  for (const node of Object.values(ast.nodes)) {
3008
- if (node.style && !ast.styles[node.style]) {
3209
+ if (typeof node.style === "string" && !ast.styles[node.style]) {
3009
3210
  pushWarning(ast.diagnostics, seen, node.line, `node '${node.id}' references unknown style '${node.style}'`);
3010
3211
  }
3011
3212
  }
@@ -3109,6 +3310,7 @@ function parse(source, opts = {}) {
3109
3310
  height: 720,
3110
3311
  fps: 60,
3111
3312
  theme: "paper",
3313
+ explicitTheme: false,
3112
3314
  direction: "LR"
3113
3315
  };
3114
3316
  const styles = {};
@@ -3207,6 +3409,7 @@ function parse(source, opts = {}) {
3207
3409
  const str = parseStringToken(rest2);
3208
3410
  if (str) {
3209
3411
  title = str.value;
3412
+ meta.title = title;
3210
3413
  remainder = str.rest;
3211
3414
  }
3212
3415
  const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
@@ -3229,8 +3432,11 @@ function parse(source, opts = {}) {
3229
3432
  } else if (k === "fps") {
3230
3433
  meta.fps = Number(v);
3231
3434
  } else if (k === "duration") meta.duration = Number(v);
3232
- else if (k === "theme") meta.theme = String(v);
3233
- else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
3435
+ else if (k === "theme") {
3436
+ const val = String(v).toLowerCase();
3437
+ meta.theme = val;
3438
+ meta.explicitTheme = val !== "auto";
3439
+ } else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
3234
3440
  else if (PLAYER_FLAT_KEY_SET.has(k)) {
3235
3441
  const error = applyPlayerSetting(meta.player ??= {}, "player", k, String(v));
3236
3442
  if (error) diagnostics.push({ severity: "warning", message: error, line: lineNo });
@@ -3249,6 +3455,16 @@ function parse(source, opts = {}) {
3249
3455
  i++;
3250
3456
  continue;
3251
3457
  }
3458
+ if (/^theme\s*[:=]?\s*([a-zA-Z0-9_-]+)/i.test(line)) {
3459
+ const match = line.match(/^theme\s*[:=]?\s*([a-zA-Z0-9_-]+)/i);
3460
+ if (match) {
3461
+ const val = match[1].toLowerCase();
3462
+ meta.theme = val;
3463
+ meta.explicitTheme = val !== "auto";
3464
+ }
3465
+ i++;
3466
+ continue;
3467
+ }
3252
3468
  const directiveMatch = line.match(PLAYER_DIRECTIVE_RE);
3253
3469
  if (directiveMatch) {
3254
3470
  const error = applyPlayerSetting(meta.player ??= {}, "player", directiveMatch[1], directiveMatch[2] ?? "");
@@ -4621,7 +4837,7 @@ function diagnoseMarkdyCode(code, options = {}) {
4621
4837
  snippet: trimmed,
4622
4838
  suggestion: closeTheme ? `Use theme '${closeTheme.match}'.` : `Choose from: ${THEME_NAMES.join(", ")}.`,
4623
4839
  didYouMean: closeTheme?.match,
4624
- ruleExplanation: "Supported themes: paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal.",
4840
+ ruleExplanation: "Supported themes: paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal, ink, doodle.",
4625
4841
  fix: closeTheme ? { original: themeMatch[0], replacement: `theme=${closeTheme.match}` } : void 0
4626
4842
  });
4627
4843
  }
@@ -5517,7 +5733,9 @@ function getIntelliCodeCompletions(docText, cursorLine, cursorCol) {
5517
5733
  { name: "editorial", desc: "High-contrast serif luxury publication layout" },
5518
5734
  { name: "graphite", desc: "Sleek slate monochrome engineering aesthetic" },
5519
5735
  { name: "terminal", desc: "Retro phosphor CRT hacker terminal green on black" },
5520
- { name: "sketchy", desc: "Hand-drawn sketchy whiteboard marker design" }
5736
+ { name: "sketchy", desc: "Hand-drawn sketchy whiteboard marker design" },
5737
+ { name: "ink", desc: "Monochromatic blue ink style inspired by ballpoint pens, fountain pens, cyanotypes & porcelain" },
5738
+ { name: "doodle", desc: "Playful hand-drawn doodle sketchbook with felt-tip marker pens and comic block shadows" }
5521
5739
  ];
5522
5740
  for (const t of themeEntries) {
5523
5741
  items.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "Diagram-native MarkdyScript parser and compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",