@markdy/core 1.3.2 → 1.4.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/dist/index.d.ts CHANGED
@@ -343,6 +343,8 @@ type TreeBus = {
343
343
  branchY: number;
344
344
  childXs: number[];
345
345
  childY: number;
346
+ childYs?: number[];
347
+ vertical?: boolean;
346
348
  };
347
349
  type TimedCue = {
348
350
  start: number;
package/dist/index.js CHANGED
@@ -758,6 +758,7 @@ var SCENE_KEYS = /* @__PURE__ */ new Set([
758
758
  "duration",
759
759
  "direction",
760
760
  "layout",
761
+ "rankdir",
761
762
  "type",
762
763
  ...PLAYER_FLAT_KEYS
763
764
  ]);
@@ -803,7 +804,6 @@ var SAFE = 48;
803
804
  var TITLE_BAND = 84;
804
805
  var NODE_W = 180;
805
806
  var NODE_H = 76;
806
- var VENN_NODE_SIZE = 220;
807
807
  var GROUP_PAD = 28;
808
808
  function estimateTextLines(words, charsPerLine) {
809
809
  if (words.length === 0) return 0;
@@ -943,7 +943,13 @@ function diagramType(ast) {
943
943
  return ast.meta.type ?? "architecture";
944
944
  }
945
945
  function effectiveTitleBand(ast) {
946
- return ast.meta.title && ast.meta.title.trim().length > 0 ? TITLE_BAND : 16;
946
+ if (!ast.meta.title || ast.meta.title.trim().length === 0) return 16;
947
+ const title = ast.meta.title.trim();
948
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
949
+ if (isVertical && title.length > 26) {
950
+ return TITLE_BAND + 52;
951
+ }
952
+ return TITLE_BAND;
947
953
  }
948
954
  function nodeShape(kind, dtype, props) {
949
955
  if (typeof props?.shape === "string") {
@@ -1251,66 +1257,108 @@ function layoutTree(ast, edges) {
1251
1257
  }
1252
1258
  for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
1253
1259
  const maxDepth = Math.max(...depth.values(), 0);
1254
- const subtreeSpan = /* @__PURE__ */ new Map();
1255
- function leafSpan(id) {
1256
- const w = nodeDims.get(id)?.width ?? NODE_W;
1257
- return w + 36;
1258
- }
1259
- function measureSubtree(id) {
1260
- const kids = children.get(id) ?? [];
1261
- const minSpan = leafSpan(id);
1262
- if (kids.length === 0) {
1263
- subtreeSpan.set(id, minSpan);
1264
- return minSpan;
1265
- }
1266
- let total = 0;
1267
- for (const kid of kids) {
1268
- total += measureSubtree(kid);
1269
- }
1270
- const span = Math.max(minSpan, total);
1271
- subtreeSpan.set(id, span);
1272
- return span;
1273
- }
1274
- let totalRootsWidth = 0;
1275
- for (const r of roots) {
1276
- totalRootsWidth += measureSubtree(r);
1277
- }
1260
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
1278
1261
  const contentW = ast.meta.width - SAFE * 2;
1279
1262
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1280
- const treeStartX = SAFE + Math.max(0, (contentW - totalRootsWidth) / 2);
1281
- const levelHeight = Math.max(maxNodeH + 40, Math.min(220, contentH / Math.max(maxDepth + 1, 1)));
1282
- const totalTreeHeight = maxDepth * levelHeight + maxNodeH;
1283
- const treeStartY = TITLE_BAND + Math.max(16, (contentH - totalTreeHeight) / 2);
1284
1263
  const nodePositions = /* @__PURE__ */ new Map();
1285
- function positionSubtree(id, leftX) {
1286
- const d = depth.get(id) ?? 0;
1287
- const y = treeStartY + d * levelHeight;
1288
- const kids = children.get(id) ?? [];
1289
- const span = subtreeSpan.get(id) ?? leafSpan(id);
1290
- const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1291
- if (kids.length === 0) {
1292
- const centerX = leftX + span / 2;
1293
- nodePositions.set(id, { x: centerX - dims.width / 2, y });
1294
- return;
1295
- }
1296
- let curX = leftX;
1297
- for (const kid of kids) {
1298
- const kidSpan = subtreeSpan.get(kid) ?? leafSpan(kid);
1299
- positionSubtree(kid, curX);
1300
- curX += kidSpan;
1301
- }
1302
- const firstChild = nodePositions.get(kids[0]);
1303
- const lastChild = nodePositions.get(kids[kids.length - 1]);
1304
- const firstDims = nodeDims.get(kids[0]) ?? { width: NODE_W, height: NODE_H };
1305
- const lastDims = nodeDims.get(kids[kids.length - 1]) ?? { width: NODE_W, height: NODE_H };
1306
- const parentCenterX = (firstChild.x + firstDims.width / 2 + lastChild.x + lastDims.width / 2) / 2;
1307
- nodePositions.set(id, { x: parentCenterX - dims.width / 2, y });
1308
- }
1309
- let currentRootLeft = treeStartX;
1310
- for (const r of roots) {
1311
- const span = subtreeSpan.get(r) ?? leafSpan(r);
1312
- positionSubtree(r, currentRootLeft);
1313
- currentRootLeft += span;
1264
+ if (isVertical) {
1265
+ let layoutSubtree2 = function(nodeId, d) {
1266
+ if (visited.has(nodeId)) return;
1267
+ visited.add(nodeId);
1268
+ const dims = nodeDims.get(nodeId);
1269
+ const x = treeStartX + d * indentStep;
1270
+ nodePositions.set(nodeId, { x, y: curY });
1271
+ const kids = (children.get(nodeId) ?? []).filter((k) => !visited.has(k));
1272
+ curY += dims.height + siblingGap;
1273
+ kids.forEach((kId, idx) => {
1274
+ layoutSubtree2(kId, d + 1);
1275
+ if (idx === kids.length - 1 && d === 0) {
1276
+ curY += branchGap - siblingGap;
1277
+ }
1278
+ });
1279
+ };
1280
+ var layoutSubtree = layoutSubtree2;
1281
+ const indentStep = 32;
1282
+ const siblingGap = 16;
1283
+ const branchGap = 28;
1284
+ const titleTop = effectiveTitleBand(ast) + 16;
1285
+ const maxTreeW = maxDepth * indentStep + Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
1286
+ const treeStartX = SAFE + Math.max(12, (contentW - maxTreeW) / 2);
1287
+ const visited = /* @__PURE__ */ new Set();
1288
+ let curY = titleTop;
1289
+ for (const r of roots) {
1290
+ layoutSubtree2(r, 0);
1291
+ curY += branchGap;
1292
+ }
1293
+ for (const id of nodeIds) {
1294
+ if (!visited.has(id)) {
1295
+ visited.add(id);
1296
+ const dims = nodeDims.get(id);
1297
+ nodePositions.set(id, {
1298
+ x: treeStartX,
1299
+ y: curY
1300
+ });
1301
+ curY += dims.height + siblingGap;
1302
+ }
1303
+ }
1304
+ } else {
1305
+ let leafSpan2 = function(id) {
1306
+ const w = nodeDims.get(id)?.width ?? NODE_W;
1307
+ return w + 36;
1308
+ }, measureSubtree2 = function(id) {
1309
+ const kids = children.get(id) ?? [];
1310
+ const minSpan = leafSpan2(id);
1311
+ if (kids.length === 0) {
1312
+ subtreeSpan.set(id, minSpan);
1313
+ return minSpan;
1314
+ }
1315
+ let total = 0;
1316
+ for (const kid of kids) {
1317
+ total += measureSubtree2(kid);
1318
+ }
1319
+ const span = Math.max(minSpan, total);
1320
+ subtreeSpan.set(id, span);
1321
+ return span;
1322
+ }, positionSubtree2 = function(id, leftX) {
1323
+ const d = depth.get(id) ?? 0;
1324
+ const y = treeStartY + d * levelHeight;
1325
+ const kids = children.get(id) ?? [];
1326
+ const span = subtreeSpan.get(id) ?? leafSpan2(id);
1327
+ const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1328
+ if (kids.length === 0) {
1329
+ const centerX = leftX + span / 2;
1330
+ nodePositions.set(id, { x: centerX - dims.width / 2, y });
1331
+ return;
1332
+ }
1333
+ let curX = leftX;
1334
+ for (const kid of kids) {
1335
+ const kidSpan = subtreeSpan.get(kid) ?? leafSpan2(kid);
1336
+ positionSubtree2(kid, curX);
1337
+ curX += kidSpan;
1338
+ }
1339
+ const firstChild = nodePositions.get(kids[0]);
1340
+ const lastChild = nodePositions.get(kids[kids.length - 1]);
1341
+ const firstDims = nodeDims.get(kids[0]) ?? { width: NODE_W, height: NODE_H };
1342
+ const lastDims = nodeDims.get(kids[kids.length - 1]) ?? { width: NODE_W, height: NODE_H };
1343
+ const parentCenterX = (firstChild.x + firstDims.width / 2 + lastChild.x + lastDims.width / 2) / 2;
1344
+ nodePositions.set(id, { x: parentCenterX - dims.width / 2, y });
1345
+ };
1346
+ var leafSpan = leafSpan2, measureSubtree = measureSubtree2, positionSubtree = positionSubtree2;
1347
+ const subtreeSpan = /* @__PURE__ */ new Map();
1348
+ let totalRootsWidth = 0;
1349
+ for (const r of roots) {
1350
+ totalRootsWidth += measureSubtree2(r);
1351
+ }
1352
+ const treeStartX = SAFE + Math.max(0, (contentW - totalRootsWidth) / 2);
1353
+ const levelHeight = Math.max(maxNodeH + 40, Math.min(220, contentH / Math.max(maxDepth + 1, 1)));
1354
+ const totalTreeHeight = maxDepth * levelHeight + maxNodeH;
1355
+ const treeStartY = TITLE_BAND + Math.max(16, (contentH - totalTreeHeight) / 2);
1356
+ let currentRootLeft = treeStartX;
1357
+ for (const r of roots) {
1358
+ const span = subtreeSpan.get(r) ?? leafSpan2(r);
1359
+ positionSubtree2(r, currentRootLeft);
1360
+ currentRootLeft += span;
1361
+ }
1314
1362
  }
1315
1363
  const nodes = [];
1316
1364
  for (const id of nodeIds) {
@@ -1489,45 +1537,85 @@ function layoutMedallion(ast, edges) {
1489
1537
  }
1490
1538
  const activeTiers = [...tiers.entries()].filter(([_, ids]) => ids.length > 0);
1491
1539
  const tierCount = activeTiers.length || 1;
1540
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
1492
1541
  const contentW = ast.meta.width - SAFE * 2;
1493
1542
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1494
1543
  const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
1495
- const availableColGap = tierCount > 1 ? (contentW - maxNodeW) / (tierCount - 1) : 0;
1496
- const colGap = Math.max(maxNodeW + 36, Math.min(360, availableColGap));
1497
- const totalW = (tierCount - 1) * colGap + maxNodeW;
1498
- const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
1544
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
1499
1545
  const nodes = [];
1500
- activeTiers.forEach(([_, ids], colIdx) => {
1501
- const colCount = ids.length;
1502
- const colTierMaxW = Math.max(...ids.map((id) => nodeDims.get(id).width));
1503
- const colTierMaxH = Math.max(...ids.map((id) => nodeDims.get(id).height));
1504
- const colX = startX + colIdx * colGap;
1505
- const rowSpacing = Math.max(colTierMaxH + 32, contentH / (colCount + 1));
1506
- const colH = (colCount - 1) * rowSpacing + colTierMaxH;
1507
- const startY = TITLE_BAND + Math.max(0, (contentH - colH) / 2);
1508
- ids.forEach((id, rowIdx) => {
1509
- const decl = ast.nodes[id];
1510
- const dims = nodeDims.get(id);
1511
- const rowY = startY + rowIdx * rowSpacing + (colTierMaxH - dims.height) / 2;
1512
- const nodeX = colX + (colTierMaxW - dims.width) / 2;
1513
- const focal = decl.props.focal === true || decl.props.accent === true;
1514
- nodes.push({
1515
- id,
1516
- kind: decl.kind,
1517
- role: nodeRole(decl.kind),
1518
- label: decl.label,
1519
- x: snapGrid(nodeX),
1520
- y: snapGrid(rowY),
1521
- width: dims.width,
1522
- height: dims.height,
1523
- style: resolveNodeStyle(decl, ast),
1524
- props: decl.props,
1525
- opacity: 0,
1526
- shape: "card",
1527
- focal
1546
+ if (isVertical) {
1547
+ const availableRowGap = tierCount > 1 ? (contentH - maxNodeH * tierCount) / (tierCount - 1) : 32;
1548
+ const rowGap = Math.max(32, Math.min(80, availableRowGap));
1549
+ const totalH = tierCount * maxNodeH + (tierCount - 1) * rowGap;
1550
+ const startY = TITLE_BAND + Math.max(16, (contentH - totalH) / 2);
1551
+ activeTiers.forEach(([_, ids], rowIdx) => {
1552
+ const rowCount = ids.length;
1553
+ const tierMaxH = Math.max(...ids.map((id) => nodeDims.get(id).height));
1554
+ const rowY = startY + rowIdx * (maxNodeH + rowGap);
1555
+ const colSpacing = 28;
1556
+ const totalRowNodesW = ids.reduce((acc, id) => acc + nodeDims.get(id).width, 0);
1557
+ const totalRowW = totalRowNodesW + Math.max(0, rowCount - 1) * colSpacing;
1558
+ let curX = SAFE + Math.max(0, (contentW - totalRowW) / 2);
1559
+ ids.forEach((id) => {
1560
+ const decl = ast.nodes[id];
1561
+ const dims = nodeDims.get(id);
1562
+ const nodeY = rowY + (tierMaxH - dims.height) / 2;
1563
+ const focal = decl.props.focal === true || decl.props.accent === true;
1564
+ nodes.push({
1565
+ id,
1566
+ kind: decl.kind,
1567
+ role: nodeRole(decl.kind),
1568
+ label: decl.label,
1569
+ x: snapGrid(curX),
1570
+ y: snapGrid(nodeY),
1571
+ width: dims.width,
1572
+ height: dims.height,
1573
+ style: resolveNodeStyle(decl, ast),
1574
+ props: decl.props,
1575
+ opacity: 0,
1576
+ shape: "card",
1577
+ focal
1578
+ });
1579
+ curX += dims.width + colSpacing;
1528
1580
  });
1529
1581
  });
1530
- });
1582
+ } else {
1583
+ const availableColGap = tierCount > 1 ? (contentW - maxNodeW) / (tierCount - 1) : 0;
1584
+ const colGap = Math.max(maxNodeW + 36, Math.min(360, availableColGap));
1585
+ const totalW = (tierCount - 1) * colGap + maxNodeW;
1586
+ const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
1587
+ activeTiers.forEach(([_, ids], colIdx) => {
1588
+ const colCount = ids.length;
1589
+ const colTierMaxW = Math.max(...ids.map((id) => nodeDims.get(id).width));
1590
+ const colTierMaxH = Math.max(...ids.map((id) => nodeDims.get(id).height));
1591
+ const colX = startX + colIdx * colGap;
1592
+ const rowSpacing = Math.max(colTierMaxH + 32, contentH / (colCount + 1));
1593
+ const colH = (colCount - 1) * rowSpacing + colTierMaxH;
1594
+ const startY = TITLE_BAND + Math.max(0, (contentH - colH) / 2);
1595
+ ids.forEach((id, rowIdx) => {
1596
+ const decl = ast.nodes[id];
1597
+ const dims = nodeDims.get(id);
1598
+ const rowY = startY + rowIdx * rowSpacing + (colTierMaxH - dims.height) / 2;
1599
+ const nodeX = colX + (colTierMaxW - dims.width) / 2;
1600
+ const focal = decl.props.focal === true || decl.props.accent === true;
1601
+ nodes.push({
1602
+ id,
1603
+ kind: decl.kind,
1604
+ role: nodeRole(decl.kind),
1605
+ label: decl.label,
1606
+ x: snapGrid(nodeX),
1607
+ y: snapGrid(rowY),
1608
+ width: dims.width,
1609
+ height: dims.height,
1610
+ style: resolveNodeStyle(decl, ast),
1611
+ props: decl.props,
1612
+ opacity: 0,
1613
+ shape: "card",
1614
+ focal
1615
+ });
1616
+ });
1617
+ });
1618
+ }
1531
1619
  return nodes;
1532
1620
  }
1533
1621
  function layoutQuadrant(ast) {
@@ -1724,6 +1812,40 @@ function layoutTimeline(ast) {
1724
1812
  for (const id of nodeIds) {
1725
1813
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1726
1814
  }
1815
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
1816
+ if (isVertical) {
1817
+ const topBand = effectiveTitleBand(ast);
1818
+ const contentW2 = ast.meta.width - SAFE * 2;
1819
+ const contentH2 = ast.meta.height - SAFE - topBand - SAFE;
1820
+ const centerX = SAFE + contentW2 / 2;
1821
+ const N = nodeIds.length;
1822
+ const spacingY = contentH2 / Math.max(N, 1);
1823
+ const nodes2 = [];
1824
+ nodeIds.forEach((id, idx) => {
1825
+ const decl = ast.nodes[id];
1826
+ const dims = nodeDims.get(id);
1827
+ const isLeft = N === 1 ? false : idx % 2 === 0;
1828
+ const x = N === 1 ? centerX - dims.width / 2 : isLeft ? centerX - dims.width - 24 : centerX + 24;
1829
+ const y = topBand + spacingY * idx + (spacingY - dims.height) / 2;
1830
+ const focal = decl.props.focal === true || decl.props.accent === true;
1831
+ nodes2.push({
1832
+ id,
1833
+ kind: decl.kind,
1834
+ role: nodeRole(decl.kind),
1835
+ label: decl.label,
1836
+ x: snapGrid(x),
1837
+ y: snapGrid(y),
1838
+ width: dims.width,
1839
+ height: dims.height,
1840
+ style: resolveNodeStyle(decl, ast),
1841
+ props: decl.props,
1842
+ opacity: 0,
1843
+ shape: "pill",
1844
+ focal
1845
+ });
1846
+ });
1847
+ return nodes2;
1848
+ }
1727
1849
  const contentW = ast.meta.width - SAFE * 2;
1728
1850
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1729
1851
  const baselineY = TITLE_BAND + contentH / 2;
@@ -1771,19 +1893,20 @@ function layoutGantt(ast) {
1771
1893
  const totalH = N * rowH;
1772
1894
  const startY = topBand + Math.max(16, (contentH - totalH) / 2);
1773
1895
  const nodes = [];
1896
+ const hasExplicitPhases = nodeIds.some((nid) => typeof ast.nodes[nid].props.phase === "number" || typeof ast.nodes[nid].props.span === "number");
1774
1897
  nodeIds.forEach((id, idx) => {
1775
1898
  const decl = ast.nodes[id];
1776
1899
  const dims = nodeDims.get(id);
1777
1900
  const phase = typeof decl.props.phase === "number" ? decl.props.phase : 0;
1778
1901
  const span = typeof decl.props.span === "number" ? decl.props.span : 1;
1779
- const totalPhases = Math.max(...nodeIds.map((nid) => {
1902
+ const totalPhases = hasExplicitPhases ? Math.max(...nodeIds.map((nid) => {
1780
1903
  const p = ast.nodes[nid].props.phase;
1781
1904
  const s = ast.nodes[nid].props.span;
1782
1905
  return (typeof p === "number" ? p : 0) + (typeof s === "number" ? s : 1);
1783
- }), 1);
1906
+ }), 1) : 1;
1784
1907
  const unitW = contentW / totalPhases;
1785
- const x = SAFE + phase * unitW;
1786
- const w = Math.max(span * unitW - 8, dims.width);
1908
+ const x = hasExplicitPhases ? SAFE + phase * unitW : SAFE;
1909
+ const w = hasExplicitPhases ? Math.max(span * unitW - 8, dims.width) : contentW;
1787
1910
  const barH = dims.height;
1788
1911
  const y = startY + idx * rowH + (rowH - barH) / 2;
1789
1912
  const focal = decl.props.focal === true || decl.props.accent === true;
@@ -1812,45 +1935,67 @@ function layoutVenn(ast) {
1812
1935
  for (const id of nodeIds) {
1813
1936
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1814
1937
  }
1938
+ const titleBand = effectiveTitleBand(ast);
1939
+ const bottomBand = ast.beats.some((b) => b.label && b.label.trim().length > 0) ? 44 : 0;
1815
1940
  const contentW = ast.meta.width - SAFE * 2;
1816
- const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1941
+ const contentH = ast.meta.height - titleBand - bottomBand - SAFE * 2;
1817
1942
  const centerX = SAFE + contentW / 2;
1818
- const centerY = TITLE_BAND + contentH / 2;
1943
+ const centerY = titleBand + SAFE + contentH / 2;
1819
1944
  const N = nodeIds.length;
1820
- const maxNodeDim = Math.max(...nodeIds.map((id) => Math.max(nodeDims.get(id).width, nodeDims.get(id).height)));
1821
- const vennSize = Math.max(VENN_NODE_SIZE, maxNodeDim);
1822
- const radius = Math.max(vennSize * 0.7, Math.min(contentW, contentH) * 0.24);
1823
1945
  const nodes = [];
1824
- nodeIds.forEach((id, idx) => {
1825
- const decl = ast.nodes[id];
1826
- const dims = nodeDims.get(id);
1827
- const circleSize = Math.max(vennSize, dims.width, dims.height);
1828
- let x, y;
1829
- if (N === 2) {
1830
- x = centerX + (idx === 0 ? -radius * 0.7 : radius * 0.7) - circleSize / 2;
1831
- y = centerY - circleSize / 2;
1832
- } else {
1946
+ if (N === 2) {
1947
+ const circleSize = snapGrid(Math.min(contentH * 0.85, contentW * 0.85 / 1.6), 16);
1948
+ const radius = circleSize * 0.35;
1949
+ nodeIds.forEach((id, idx) => {
1950
+ const decl = ast.nodes[id];
1951
+ const x = centerX + (idx === 0 ? -radius : radius) - circleSize / 2;
1952
+ const y = centerY - circleSize / 2;
1953
+ const focal = decl.props.focal === true || decl.props.accent === true;
1954
+ nodes.push({
1955
+ id,
1956
+ kind: decl.kind,
1957
+ role: nodeRole(decl.kind),
1958
+ label: decl.label,
1959
+ x: snapGrid(x),
1960
+ y: snapGrid(y),
1961
+ width: circleSize,
1962
+ height: circleSize,
1963
+ style: resolveNodeStyle(decl, ast),
1964
+ props: decl.props,
1965
+ opacity: 0,
1966
+ shape: "circle",
1967
+ focal
1968
+ });
1969
+ });
1970
+ } else {
1971
+ const maxCircleByH = contentH * 0.88 / 1.78;
1972
+ const maxCircleByW = contentW * 0.88 / 1.9;
1973
+ const circleSize = snapGrid(Math.min(320, Math.max(180, Math.min(maxCircleByH, maxCircleByW))), 16);
1974
+ const radius = circleSize * 0.52;
1975
+ const clusterCenterY = centerY + (N === 3 ? radius * 0.25 : 0);
1976
+ nodeIds.forEach((id, idx) => {
1977
+ const decl = ast.nodes[id];
1833
1978
  const angle = -Math.PI / 2 + idx * 2 * Math.PI / N;
1834
- x = centerX + radius * 0.85 * Math.cos(angle) - circleSize / 2;
1835
- y = centerY + radius * 0.85 * Math.sin(angle) - circleSize / 2;
1836
- }
1837
- const focal = decl.props.focal === true || decl.props.accent === true;
1838
- nodes.push({
1839
- id,
1840
- kind: decl.kind,
1841
- role: nodeRole(decl.kind),
1842
- label: decl.label,
1843
- x: snapGrid(x),
1844
- y: snapGrid(y),
1845
- width: circleSize,
1846
- height: circleSize,
1847
- style: resolveNodeStyle(decl, ast),
1848
- props: decl.props,
1849
- opacity: 0,
1850
- shape: "circle",
1851
- focal
1979
+ const x = centerX + radius * Math.cos(angle) - circleSize / 2;
1980
+ const y = clusterCenterY + radius * Math.sin(angle) - circleSize / 2;
1981
+ const focal = decl.props.focal === true || decl.props.accent === true;
1982
+ nodes.push({
1983
+ id,
1984
+ kind: decl.kind,
1985
+ role: nodeRole(decl.kind),
1986
+ label: decl.label,
1987
+ x: snapGrid(x),
1988
+ y: snapGrid(y),
1989
+ width: circleSize,
1990
+ height: circleSize,
1991
+ style: resolveNodeStyle(decl, ast),
1992
+ props: decl.props,
1993
+ opacity: 0,
1994
+ shape: "circle",
1995
+ focal
1996
+ });
1852
1997
  });
1853
- });
1998
+ }
1854
1999
  return nodes;
1855
2000
  }
1856
2001
  function layoutLayers(ast) {
@@ -1902,49 +2047,97 @@ function layoutNested(ast) {
1902
2047
  for (const id of nodeIds) {
1903
2048
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1904
2049
  }
1905
- const topHeadroom = 20;
1906
- const startY = TITLE_BAND + topHeadroom;
2050
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
2051
+ const topBand = effectiveTitleBand(ast);
2052
+ const topHeadroom = 16;
2053
+ const startY = topBand + topHeadroom;
1907
2054
  const contentW = ast.meta.width - SAFE * 2;
1908
2055
  const contentH = ast.meta.height - startY - SAFE;
1909
- const centerX = SAFE + contentW / 2;
1910
- const centerY = startY + contentH / 2;
1911
2056
  const N = nodeIds.length;
1912
- const padX = Math.min(54, contentW * 0.42 / Math.max(N, 1));
1913
- const padY = Math.min(48, contentH * 0.42 / Math.max(N, 1));
1914
2057
  const nodes = [];
1915
- nodeIds.forEach((id, idx) => {
1916
- const decl = ast.nodes[id];
1917
- const dims = nodeDims.get(id);
1918
- const isCore = idx === N - 1;
1919
- let x, y, w, h;
1920
- if (isCore) {
1921
- w = Math.max(dims.width, Math.min(320, contentW - idx * padX * 2));
1922
- h = Math.max(dims.height, 84);
1923
- x = centerX - w / 2;
1924
- y = centerY - h / 2 + (N > 1 ? 16 : 0);
1925
- } else {
1926
- x = SAFE + idx * padX;
1927
- y = startY + idx * padY;
1928
- w = Math.max(dims.width, contentW - idx * padX * 2);
1929
- h = Math.max(dims.height, contentH - idx * padY * 2);
1930
- }
1931
- const focal = decl.props.focal === true || decl.props.accent === true || isCore;
1932
- nodes.push({
1933
- id,
1934
- kind: decl.kind,
1935
- role: nodeRole(decl.kind),
1936
- label: decl.label,
1937
- x: snapGrid(x),
1938
- y: snapGrid(y),
1939
- width: snapGrid(w),
1940
- height: snapGrid(h),
1941
- style: resolveNodeStyle(decl, ast),
1942
- props: decl.props,
1943
- opacity: 0,
1944
- shape: isCore ? "card" : "container",
1945
- focal
2058
+ if (isVertical) {
2059
+ const stepX = Math.min(40, Math.max(24, contentW * 0.32 / Math.max(N - 1, 1)));
2060
+ const stepY = Math.min(60, Math.max(46, contentH * 0.4 / Math.max(N - 1, 1)));
2061
+ const bottomStepY = Math.min(36, Math.max(22, contentH * 0.22 / Math.max(N - 1, 1)));
2062
+ nodeIds.forEach((id, idx) => {
2063
+ const decl = ast.nodes[id];
2064
+ const dims = nodeDims.get(id);
2065
+ const isCore = idx === N - 1;
2066
+ let x, y, w, h;
2067
+ if (isCore) {
2068
+ const availLeft = SAFE + idx * stepX;
2069
+ const availWidth = Math.max(dims.width, contentW - idx * stepX * 2);
2070
+ const availTop = startY + idx * stepY;
2071
+ const availBottom = startY + contentH - idx * bottomStepY;
2072
+ const availHeight = Math.max(80, availBottom - availTop);
2073
+ w = Math.min(availWidth - 24, Math.max(dims.width, 240));
2074
+ h = Math.max(dims.height, 80);
2075
+ x = availLeft + (availWidth - w) / 2;
2076
+ y = availTop + (availHeight - h) / 2;
2077
+ } else {
2078
+ x = SAFE + idx * stepX;
2079
+ y = startY + idx * stepY;
2080
+ w = contentW - idx * stepX * 2;
2081
+ h = contentH - idx * (stepY + bottomStepY);
2082
+ }
2083
+ const focal = decl.props.focal === true || decl.props.accent === true || isCore;
2084
+ nodes.push({
2085
+ id,
2086
+ kind: decl.kind,
2087
+ role: nodeRole(decl.kind),
2088
+ label: decl.label,
2089
+ x: snapGrid(x),
2090
+ y: snapGrid(y),
2091
+ width: snapGrid(w),
2092
+ height: snapGrid(h),
2093
+ style: resolveNodeStyle(decl, ast),
2094
+ props: decl.props,
2095
+ opacity: 0,
2096
+ shape: isCore ? "card" : "container",
2097
+ focal
2098
+ });
1946
2099
  });
1947
- });
2100
+ } else {
2101
+ const padX = Math.min(54, contentW * 0.38 / Math.max(N, 1));
2102
+ const padY = Math.min(48, contentH * 0.38 / Math.max(N, 1));
2103
+ nodeIds.forEach((id, idx) => {
2104
+ const decl = ast.nodes[id];
2105
+ const dims = nodeDims.get(id);
2106
+ const isCore = idx === N - 1;
2107
+ let x, y, w, h;
2108
+ if (isCore) {
2109
+ const availLeft = SAFE + idx * padX;
2110
+ const availWidth = Math.max(dims.width, contentW - idx * padX * 2);
2111
+ const availTop = startY + idx * padY;
2112
+ const availHeight = Math.max(80, contentH - idx * padY * 2);
2113
+ w = Math.min(availWidth - 24, Math.max(dims.width, 240));
2114
+ h = Math.max(dims.height, 80);
2115
+ x = availLeft + (availWidth - w) / 2;
2116
+ y = availTop + (availHeight - h) / 2;
2117
+ } else {
2118
+ x = SAFE + idx * padX;
2119
+ y = startY + idx * padY;
2120
+ w = contentW - idx * padX * 2;
2121
+ h = contentH - idx * padY * 2;
2122
+ }
2123
+ const focal = decl.props.focal === true || decl.props.accent === true || isCore;
2124
+ nodes.push({
2125
+ id,
2126
+ kind: decl.kind,
2127
+ role: nodeRole(decl.kind),
2128
+ label: decl.label,
2129
+ x: snapGrid(x),
2130
+ y: snapGrid(y),
2131
+ width: snapGrid(w),
2132
+ height: snapGrid(h),
2133
+ style: resolveNodeStyle(decl, ast),
2134
+ props: decl.props,
2135
+ opacity: 0,
2136
+ shape: isCore ? "card" : "container",
2137
+ focal
2138
+ });
2139
+ });
2140
+ }
1948
2141
  return nodes;
1949
2142
  }
1950
2143
  function layoutRadar(ast) {
@@ -2062,24 +2255,45 @@ function computeTreeBuses(ast, nodes, edges) {
2062
2255
  children.get(edge.from).push(edge.to);
2063
2256
  parent.add(edge.to);
2064
2257
  }
2258
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
2065
2259
  const buses = [];
2066
2260
  for (const [parentId, childIds] of children) {
2067
2261
  const parentNode = byId.get(parentId);
2068
2262
  const childNodes = childIds.map((id) => byId.get(id)).filter(Boolean);
2069
2263
  if (!parentNode || childNodes.length === 0) continue;
2070
- const parentX = parentNode.x + parentNode.width / 2;
2071
- const parentY = parentNode.y + parentNode.height;
2072
- const childY = Math.min(...childNodes.map((node) => node.y));
2073
- buses.push({
2074
- id: `tree_bus_${parentId}`,
2075
- parentId,
2076
- childIds,
2077
- parentX: snapGrid(parentX),
2078
- parentY: snapGrid(parentY),
2079
- branchY: snapGrid((parentY + childY) / 2),
2080
- childXs: childNodes.map((node) => snapGrid(node.x + node.width / 2)),
2081
- childY: snapGrid(childY)
2082
- });
2264
+ if (isVertical) {
2265
+ const trunkX = snapGrid(parentNode.x + 18);
2266
+ const parentY = snapGrid(parentNode.y + parentNode.height);
2267
+ const childXs = childNodes.map((node) => snapGrid(node.x));
2268
+ const childYs = childNodes.map((node) => snapGrid(node.y + node.height / 2));
2269
+ buses.push({
2270
+ id: `tree_bus_${parentId}`,
2271
+ parentId,
2272
+ childIds,
2273
+ parentX: trunkX,
2274
+ parentY,
2275
+ branchY: parentY,
2276
+ childXs,
2277
+ childY: childYs[0],
2278
+ childYs,
2279
+ vertical: true
2280
+ });
2281
+ } else {
2282
+ const parentX = parentNode.x + parentNode.width / 2;
2283
+ const parentY = parentNode.y + parentNode.height;
2284
+ const childY = Math.min(...childNodes.map((node) => node.y));
2285
+ buses.push({
2286
+ id: `tree_bus_${parentId}`,
2287
+ parentId,
2288
+ childIds,
2289
+ parentX: snapGrid(parentX),
2290
+ parentY: snapGrid(parentY),
2291
+ branchY: snapGrid((parentY + childY) / 2),
2292
+ childXs: childNodes.map((node) => snapGrid(node.x + node.width / 2)),
2293
+ childY: snapGrid(childY),
2294
+ childYs: childNodes.map((node) => snapGrid(node.y))
2295
+ });
2296
+ }
2083
2297
  }
2084
2298
  return buses;
2085
2299
  }
@@ -2283,7 +2497,7 @@ function computeAdaptiveDimensions(ast, edges) {
2283
2497
  const maxNodeW = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.width)) : NODE_W;
2284
2498
  const maxNodeH = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.height)) : NODE_H;
2285
2499
  const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
2286
- const titleBand = hasTitle ? TITLE_BAND : 0;
2500
+ const titleBand = effectiveTitleBand(ast);
2287
2501
  const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
2288
2502
  const bottomBand = hasBeatCaptions ? 44 : 0;
2289
2503
  const titleW = hasTitle ? Math.max(320, ast.meta.title.trim().length * 11 + SAFE * 2) : 0;
@@ -2303,34 +2517,35 @@ function computeAdaptiveDimensions(ast, edges) {
2303
2517
  autoH = Math.max(320, Math.min(1800, requiredH));
2304
2518
  } else if (dtype === "medallion") {
2305
2519
  const tierSet = /* @__PURE__ */ new Set();
2520
+ const tierCounts = /* @__PURE__ */ new Map();
2306
2521
  for (const id of nodeIds) {
2307
2522
  const combined = `${id} ${ast.nodes[id].kind}`.toLowerCase();
2308
- if (combined.includes("bronze") || combined.includes("raw") || combined.includes("landing")) tierSet.add(1);
2309
- else if (combined.includes("silver") || combined.includes("clean") || combined.includes("curated") || combined.includes("conformed")) tierSet.add(2);
2310
- else if (combined.includes("gold") || combined.includes("agg") || combined.includes("mart") || combined.includes("analytics")) tierSet.add(3);
2311
- else if (combined.includes("bi") || combined.includes("dash") || combined.includes("model") || combined.includes("app") || combined.includes("consumer") || combined.includes("user") || combined.includes("client")) tierSet.add(4);
2312
- else tierSet.add(0);
2523
+ let t = 0;
2524
+ if (combined.includes("bronze") || combined.includes("raw") || combined.includes("landing")) t = 1;
2525
+ else if (combined.includes("silver") || combined.includes("clean") || combined.includes("curated") || combined.includes("conformed")) t = 2;
2526
+ else if (combined.includes("gold") || combined.includes("agg") || combined.includes("mart") || combined.includes("analytics")) t = 3;
2527
+ else if (combined.includes("bi") || combined.includes("dash") || combined.includes("model") || combined.includes("app") || combined.includes("consumer") || combined.includes("user") || combined.includes("client")) t = 4;
2528
+ tierSet.add(t);
2529
+ tierCounts.set(t, (tierCounts.get(t) ?? 0) + 1);
2313
2530
  }
2314
2531
  const tierCount = Math.max(tierSet.size, 4);
2315
- const tierW = Math.max(260, maxNodeW + 48);
2316
- const requiredW = Math.max(SAFE * 2 + tierCount * tierW, titleW);
2317
- const requiredH = titleBand + SAFE * 2 + Math.max(440, maxNodeH * 3 + 120) + bottomBand;
2318
- autoW = Math.max(640, Math.min(2800, requiredW));
2319
- autoH = Math.max(400, Math.min(1600, requiredH));
2532
+ const maxInTier = Math.max(...tierCounts.values(), 2);
2533
+ if (isVertical) {
2534
+ const nodeColSpacing = 28;
2535
+ const tierSpacing = 36;
2536
+ const tierH = Math.max(76, maxNodeH + 16);
2537
+ const requiredW = Math.max(SAFE * 2 + maxInTier * maxNodeW + (maxInTier - 1) * nodeColSpacing, titleW);
2538
+ const requiredH = titleBand + SAFE * 2 + tierCount * tierH + (tierCount - 1) * tierSpacing + bottomBand;
2539
+ autoW = Math.max(540, Math.min(1600, snapGrid(requiredW, 16)));
2540
+ autoH = Math.max(680, Math.min(2e3, snapGrid(requiredH, 16)));
2541
+ } else {
2542
+ const tierW = Math.max(260, maxNodeW + 48);
2543
+ const requiredW = Math.max(SAFE * 2 + tierCount * tierW, titleW);
2544
+ const requiredH = titleBand + SAFE * 2 + Math.max(440, maxNodeH * 3 + 120) + bottomBand;
2545
+ autoW = Math.max(640, Math.min(2800, requiredW));
2546
+ autoH = Math.max(400, Math.min(1600, requiredH));
2547
+ }
2320
2548
  } else if (dtype === "tree") {
2321
- let measureSubtree2 = function(id) {
2322
- const kids = children.get(id) ?? [];
2323
- if (kids.length === 0) {
2324
- subtreeSpan.set(id, minLeafWidth);
2325
- return minLeafWidth;
2326
- }
2327
- let total = 0;
2328
- for (const kid of kids) total += measureSubtree2(kid);
2329
- const span = Math.max(minLeafWidth, total);
2330
- subtreeSpan.set(id, span);
2331
- return span;
2332
- };
2333
- var measureSubtree = measureSubtree2;
2334
2549
  const children = /* @__PURE__ */ new Map();
2335
2550
  const hasParent = /* @__PURE__ */ new Set();
2336
2551
  for (const id of nodeIds) children.set(id, []);
@@ -2342,31 +2557,74 @@ function computeAdaptiveDimensions(ast, edges) {
2342
2557
  }
2343
2558
  }
2344
2559
  const roots = nodeIds.filter((id) => !hasParent.has(id));
2345
- const depth = /* @__PURE__ */ new Map();
2346
- const queue = [...roots];
2347
- for (const r of roots) depth.set(r, 0);
2348
- while (queue.length > 0) {
2349
- const cur = queue.shift();
2350
- const d = depth.get(cur) ?? 0;
2351
- for (const child of children.get(cur) ?? []) {
2352
- if (!depth.has(child)) {
2353
- depth.set(child, d + 1);
2354
- queue.push(child);
2560
+ const activeRoots = roots.length > 0 ? roots : nodeIds;
2561
+ if (isVertical) {
2562
+ const indentStep = 32;
2563
+ const siblingGap = 16;
2564
+ const branchGap = 28;
2565
+ const depth = /* @__PURE__ */ new Map();
2566
+ const q = [...activeRoots];
2567
+ for (const r of activeRoots) depth.set(r, 0);
2568
+ while (q.length > 0) {
2569
+ const cur = q.shift();
2570
+ const d = depth.get(cur) ?? 0;
2571
+ for (const child of children.get(cur) ?? []) {
2572
+ if (!depth.has(child)) {
2573
+ depth.set(child, d + 1);
2574
+ q.push(child);
2575
+ }
2355
2576
  }
2356
2577
  }
2578
+ const treeDepth = Math.max(...depth.values(), 0);
2579
+ const maxTreeW = treeDepth * indentStep + maxNodeW;
2580
+ const requiredW = Math.max(SAFE * 2 + maxTreeW + 36, titleW);
2581
+ let totalNodesH = 0;
2582
+ for (const id of nodeIds) {
2583
+ totalNodesH += computeNodeDimensions(ast.nodes[id]).height + siblingGap;
2584
+ }
2585
+ totalNodesH += Math.max(0, activeRoots.length) * branchGap;
2586
+ const requiredH = titleBand + SAFE * 2 + totalNodesH + bottomBand;
2587
+ autoW = Math.max(480, Math.min(1600, snapGrid(requiredW, 16)));
2588
+ autoH = Math.max(640, Math.min(2600, snapGrid(requiredH, 16)));
2589
+ } else {
2590
+ let measureSubtree2 = function(id) {
2591
+ const kids = children.get(id) ?? [];
2592
+ if (kids.length === 0) {
2593
+ subtreeSpan.set(id, minLeafWidth);
2594
+ return minLeafWidth;
2595
+ }
2596
+ let total = 0;
2597
+ for (const kid of kids) total += measureSubtree2(kid);
2598
+ const span = Math.max(minLeafWidth, total);
2599
+ subtreeSpan.set(id, span);
2600
+ return span;
2601
+ };
2602
+ var measureSubtree = measureSubtree2;
2603
+ const depth = /* @__PURE__ */ new Map();
2604
+ const queue = [...roots];
2605
+ for (const r of roots) depth.set(r, 0);
2606
+ while (queue.length > 0) {
2607
+ const cur = queue.shift();
2608
+ const d = depth.get(cur) ?? 0;
2609
+ for (const child of children.get(cur) ?? []) {
2610
+ if (!depth.has(child)) {
2611
+ depth.set(child, d + 1);
2612
+ queue.push(child);
2613
+ }
2614
+ }
2615
+ }
2616
+ for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
2617
+ const maxDepth = Math.max(...depth.values(), 0);
2618
+ const minLeafWidth = Math.max(NODE_W + 36, maxNodeW + 36);
2619
+ const subtreeSpan = /* @__PURE__ */ new Map();
2620
+ let totalRootsWidth = 0;
2621
+ for (const r of activeRoots) totalRootsWidth += measureSubtree2(r);
2622
+ const levelH = Math.max(130, maxNodeH + 54);
2623
+ const requiredW = Math.max(SAFE * 2 + totalRootsWidth, titleW);
2624
+ const requiredH = titleBand + SAFE * 2 + (maxDepth + 1) * levelH + bottomBand;
2625
+ autoW = Math.max(480, Math.min(2560, requiredW));
2626
+ autoH = Math.max(320, Math.min(1800, requiredH));
2357
2627
  }
2358
- for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
2359
- const maxDepth = Math.max(...depth.values(), 0);
2360
- const minLeafWidth = Math.max(NODE_W + 36, maxNodeW + 36);
2361
- const subtreeSpan = /* @__PURE__ */ new Map();
2362
- let totalRootsWidth = 0;
2363
- const activeRoots = roots.length > 0 ? roots : nodeIds;
2364
- for (const r of activeRoots) totalRootsWidth += measureSubtree2(r);
2365
- const levelH = Math.max(130, maxNodeH + 54);
2366
- const requiredW = Math.max(SAFE * 2 + totalRootsWidth, titleW);
2367
- const requiredH = titleBand + SAFE * 2 + (maxDepth + 1) * levelH + bottomBand;
2368
- autoW = Math.max(480, Math.min(2560, requiredW));
2369
- autoH = Math.max(320, Math.min(1800, requiredH));
2370
2628
  } else if (dtype === "swimlane") {
2371
2629
  const laneCount = Math.max(groupCount, 1);
2372
2630
  const maxInLane = Math.max(
@@ -2392,24 +2650,45 @@ function computeAdaptiveDimensions(ast, edges) {
2392
2650
  autoW = Math.max(480, Math.min(2e3, Math.max(SAFE * 2 + maxNodeW + 160, titleW)));
2393
2651
  const requiredH = titleBand + SAFE * 2 + layerCount * layerH + bottomBand;
2394
2652
  autoH = Math.max(320, Math.min(1440, requiredH));
2395
- } else if (dtype === "timeline" || dtype === "gantt") {
2653
+ } else if (dtype === "timeline") {
2396
2654
  const milestones = Math.max(nodeCount, 1);
2397
- let totalPhases = milestones;
2398
- if (dtype === "gantt") {
2399
- totalPhases = Math.max(...nodeIds.map((nid) => {
2655
+ if (isVertical) {
2656
+ const milestoneH = Math.max(76, maxNodeH);
2657
+ const stepY = Math.max(92, milestoneH + 20);
2658
+ const requiredW = Math.max(SAFE * 2 + maxNodeW * 2 + 64, titleW);
2659
+ const contentH = milestones * stepY;
2660
+ const requiredH = titleBand + SAFE * 2 + contentH + bottomBand;
2661
+ autoW = Math.max(540, Math.min(1600, snapGrid(requiredW, 16)));
2662
+ autoH = Math.max(640, Math.min(2200, snapGrid(requiredH, 16)));
2663
+ } else {
2664
+ const milestoneW = Math.max(220, maxNodeW + 36);
2665
+ const requiredW = Math.max(SAFE * 2 + milestones * milestoneW, titleW);
2666
+ const requiredH = titleBand + SAFE * 2 + Math.max(260, maxNodeH * 2 + 80) + bottomBand;
2667
+ autoW = Math.max(480, Math.min(2560, requiredW));
2668
+ autoH = Math.max(320, Math.min(1600, requiredH));
2669
+ }
2670
+ } else if (dtype === "gantt") {
2671
+ const milestones = Math.max(nodeCount, 1);
2672
+ const hasExplicitPhases = nodeIds.some((nid) => typeof ast.nodes[nid].props.phase === "number" || typeof ast.nodes[nid].props.span === "number");
2673
+ if (isVertical) {
2674
+ const rowH = Math.max(68, maxNodeH + 12);
2675
+ const contentH = milestones * rowH + 24;
2676
+ const requiredW = Math.max(SAFE * 2 + maxNodeW + 48, titleW, 540);
2677
+ const requiredH = titleBand + SAFE * 2 + contentH + bottomBand;
2678
+ autoW = Math.max(540, Math.min(1600, snapGrid(requiredW, 16)));
2679
+ autoH = Math.max(640, Math.min(2200, snapGrid(requiredH, 16)));
2680
+ } else {
2681
+ const totalPhases = hasExplicitPhases ? Math.max(...nodeIds.map((nid) => {
2400
2682
  const p = ast.nodes[nid].props.phase;
2401
2683
  const s = ast.nodes[nid].props.span;
2402
2684
  return (typeof p === "number" ? p : 0) + (typeof s === "number" ? s : 1);
2403
- }), milestones);
2685
+ }), 1) : Math.min(milestones, 6);
2686
+ const phaseColW = Math.max(120, Math.min(200, maxNodeW * 0.4));
2687
+ const requiredW = Math.max(SAFE * 2 + (hasExplicitPhases ? totalPhases * phaseColW : maxNodeW + 200), titleW);
2688
+ const requiredH = titleBand + SAFE * 2 + milestones * Math.max(76, maxNodeH + 16) + 40 + bottomBand;
2689
+ autoW = Math.max(680, Math.min(2560, snapGrid(requiredW, 16)));
2690
+ autoH = Math.max(480, Math.min(1600, snapGrid(requiredH, 16)));
2404
2691
  }
2405
- const milestoneW = Math.max(220, maxNodeW + 36);
2406
- const requiredW = Math.max(
2407
- SAFE * 2 + (dtype === "gantt" ? totalPhases * Math.max(160, maxNodeW + 16) : milestones * milestoneW),
2408
- titleW
2409
- );
2410
- const requiredH = dtype === "gantt" ? titleBand + SAFE * 2 + milestones * Math.max(76, maxNodeH + 16) + 40 + bottomBand : titleBand + SAFE * 2 + Math.max(260, maxNodeH * 2 + 80) + bottomBand;
2411
- autoW = Math.max(480, Math.min(2560, requiredW));
2412
- autoH = Math.max(320, Math.min(1600, requiredH));
2413
2692
  } else if (dtype === "loop" || dtype === "flywheel" || dtype === "radar" || dtype === "venn" || dtype === "constellation") {
2414
2693
  const N = Math.max(nodeCount, 3);
2415
2694
  const minRadius = Math.max(160, maxNodeW * 0.75);
@@ -2424,6 +2703,31 @@ function computeAdaptiveDimensions(ast, edges) {
2424
2703
  const requiredH = titleBand + SAFE * 2 + maxNodeH * 3 + bottomBand;
2425
2704
  autoW = Math.min(2400, requiredW);
2426
2705
  autoH = Math.min(1600, requiredH);
2706
+ } else if (dtype === "nested") {
2707
+ const N = Math.max(nodeCount, 1);
2708
+ const isVertical2 = ast.meta.direction === "TB" || ast.meta.direction === "BT";
2709
+ if (isVertical2) {
2710
+ const coreW = Math.max(280, maxNodeW + 48);
2711
+ const coreH = Math.max(76, maxNodeH + 16);
2712
+ const stepX = 36;
2713
+ const stepY = 56;
2714
+ const bottomStepY = 28;
2715
+ const requiredW = Math.max(SAFE * 2 + coreW + (N - 1) * stepX * 2, titleW);
2716
+ const contentH = (N - 1) * stepY + coreH + (N - 1) * bottomStepY + 48;
2717
+ const requiredH = titleBand + SAFE * 2 + contentH + bottomBand;
2718
+ autoW = Math.max(540, Math.min(1600, snapGrid(requiredW, 16)));
2719
+ autoH = Math.max(680, Math.min(2e3, snapGrid(requiredH, 16)));
2720
+ } else {
2721
+ const coreW = Math.max(300, maxNodeW + 48);
2722
+ const coreH = Math.max(84, maxNodeH + 24);
2723
+ const stepX = 48;
2724
+ const stepY = 40;
2725
+ const requiredW = Math.max(SAFE * 2 + coreW + (N - 1) * stepX * 2 + 100, titleW);
2726
+ const contentH = coreH + (N - 1) * stepY * 2 + 40;
2727
+ const requiredH = titleBand + SAFE * 2 + contentH + bottomBand;
2728
+ autoW = Math.max(720, Math.min(2400, snapGrid(requiredW, 16)));
2729
+ autoH = Math.max(460, Math.min(1400, snapGrid(requiredH, 16)));
2730
+ }
2427
2731
  } else {
2428
2732
  const forceVertical = dtype === "flowchart" && isVertical || isVertical;
2429
2733
  const ranks = assignRanks(nodeIds, effectiveEdges, forceVertical ? "TB" : "LR");
@@ -2472,7 +2776,8 @@ function compilePlan(ast, theme) {
2472
2776
  }
2473
2777
  const nodes = layoutNodes(ast, structuralEdges);
2474
2778
  const groupBoundaries = computeGroupBoundaries(ast, nodes);
2475
- const treeBuses = computeTreeBuses(ast, nodes, structuralEdges.filter((edge) => edge.structural));
2779
+ const treeEdges = structuralEdges.some((edge) => edge.structural) ? structuralEdges.filter((edge) => edge.structural) : structuralEdges;
2780
+ const treeBuses = computeTreeBuses(ast, nodes, treeEdges);
2476
2781
  const { cues, beats } = scheduleBeats(ast, structuralEdges);
2477
2782
  const sequence = buildSequencePlan(ast, cues);
2478
2783
  const shown = /* @__PURE__ */ new Set();
@@ -3696,11 +4001,11 @@ function parse(source, opts = {}) {
3696
4001
  meta.title = title;
3697
4002
  remainder = str.rest;
3698
4003
  }
3699
- const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
4004
+ const inlineLayout = remainder.match(/\b(?:layout|direction|rankdir)\s+(LR|RL|TB|BT)\b/i);
3700
4005
  if (inlineLayout) {
3701
4006
  meta.direction = inlineLayout[1].toUpperCase();
3702
4007
  meta.explicitDirection = true;
3703
- remainder = remainder.replace(/\blayout\s+(LR|RL|TB|BT)\b/i, " ");
4008
+ remainder = remainder.replace(/\b(?:layout|direction|rankdir)\s+(LR|RL|TB|BT)\b/i, " ");
3704
4009
  }
3705
4010
  const props = parseProps(remainder);
3706
4011
  for (const [k, v] of Object.entries(props)) {
@@ -3721,7 +4026,7 @@ function parse(source, opts = {}) {
3721
4026
  const val = String(v).toLowerCase();
3722
4027
  meta.theme = val;
3723
4028
  meta.explicitTheme = val !== "auto";
3724
- } else if (k === "direction" || k === "layout") {
4029
+ } else if (k === "direction" || k === "layout" || k === "rankdir") {
3725
4030
  meta.direction = String(v).toUpperCase();
3726
4031
  meta.explicitDirection = true;
3727
4032
  } else if (PLAYER_FLAT_KEY_SET.has(k)) {
@@ -3733,8 +4038,9 @@ function parse(source, opts = {}) {
3733
4038
  diagnostics.push({ severity: "warning", message: `unknown diagram type '${v}'`, line: lineNo });
3734
4039
  } else {
3735
4040
  meta.type = t;
3736
- if (t === "flowchart" && !props.direction && !props.layout && !inlineLayout) {
4041
+ if ((t === "flowchart" || t === "nested") && !props.direction && !props.layout && !props.rankdir && !inlineLayout) {
3737
4042
  meta.direction = "TB";
4043
+ meta.explicitDirection = true;
3738
4044
  }
3739
4045
  }
3740
4046
  }
@@ -5893,7 +6199,7 @@ function diagnoseMarkdyCode(code, options = {}) {
5893
6199
  });
5894
6200
  }
5895
6201
  }
5896
- const layoutMatch = trimmed.match(/\blayout\s*=?\s*(\w+)/i);
6202
+ const layoutMatch = trimmed.match(/\b(?:layout|direction|rankdir)\s*=?\s*(\w+)/i);
5897
6203
  if (layoutMatch) {
5898
6204
  const layoutVal = layoutMatch[1].toUpperCase();
5899
6205
  if (!LAYOUT_DIRECTIONS.includes(layoutVal)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.3.2",
3
+ "version": "1.4.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",