@markdy/core 1.3.3 → 1.4.1

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
@@ -804,7 +804,6 @@ var SAFE = 48;
804
804
  var TITLE_BAND = 84;
805
805
  var NODE_W = 180;
806
806
  var NODE_H = 76;
807
- var VENN_NODE_SIZE = 220;
808
807
  var GROUP_PAD = 28;
809
808
  function estimateTextLines(words, charsPerLine) {
810
809
  if (words.length === 0) return 0;
@@ -944,7 +943,13 @@ function diagramType(ast) {
944
943
  return ast.meta.type ?? "architecture";
945
944
  }
946
945
  function effectiveTitleBand(ast) {
947
- 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;
948
953
  }
949
954
  function nodeShape(kind, dtype, props) {
950
955
  if (typeof props?.shape === "string") {
@@ -1252,66 +1257,108 @@ function layoutTree(ast, edges) {
1252
1257
  }
1253
1258
  for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
1254
1259
  const maxDepth = Math.max(...depth.values(), 0);
1255
- const subtreeSpan = /* @__PURE__ */ new Map();
1256
- function leafSpan(id) {
1257
- const w = nodeDims.get(id)?.width ?? NODE_W;
1258
- return w + 36;
1259
- }
1260
- function measureSubtree(id) {
1261
- const kids = children.get(id) ?? [];
1262
- const minSpan = leafSpan(id);
1263
- if (kids.length === 0) {
1264
- subtreeSpan.set(id, minSpan);
1265
- return minSpan;
1266
- }
1267
- let total = 0;
1268
- for (const kid of kids) {
1269
- total += measureSubtree(kid);
1270
- }
1271
- const span = Math.max(minSpan, total);
1272
- subtreeSpan.set(id, span);
1273
- return span;
1274
- }
1275
- let totalRootsWidth = 0;
1276
- for (const r of roots) {
1277
- totalRootsWidth += measureSubtree(r);
1278
- }
1260
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
1279
1261
  const contentW = ast.meta.width - SAFE * 2;
1280
1262
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1281
- const treeStartX = SAFE + Math.max(0, (contentW - totalRootsWidth) / 2);
1282
- const levelHeight = Math.max(maxNodeH + 40, Math.min(220, contentH / Math.max(maxDepth + 1, 1)));
1283
- const totalTreeHeight = maxDepth * levelHeight + maxNodeH;
1284
- const treeStartY = TITLE_BAND + Math.max(16, (contentH - totalTreeHeight) / 2);
1285
1263
  const nodePositions = /* @__PURE__ */ new Map();
1286
- function positionSubtree(id, leftX) {
1287
- const d = depth.get(id) ?? 0;
1288
- const y = treeStartY + d * levelHeight;
1289
- const kids = children.get(id) ?? [];
1290
- const span = subtreeSpan.get(id) ?? leafSpan(id);
1291
- const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
1292
- if (kids.length === 0) {
1293
- const centerX = leftX + span / 2;
1294
- nodePositions.set(id, { x: centerX - dims.width / 2, y });
1295
- return;
1296
- }
1297
- let curX = leftX;
1298
- for (const kid of kids) {
1299
- const kidSpan = subtreeSpan.get(kid) ?? leafSpan(kid);
1300
- positionSubtree(kid, curX);
1301
- curX += kidSpan;
1302
- }
1303
- const firstChild = nodePositions.get(kids[0]);
1304
- const lastChild = nodePositions.get(kids[kids.length - 1]);
1305
- const firstDims = nodeDims.get(kids[0]) ?? { width: NODE_W, height: NODE_H };
1306
- const lastDims = nodeDims.get(kids[kids.length - 1]) ?? { width: NODE_W, height: NODE_H };
1307
- const parentCenterX = (firstChild.x + firstDims.width / 2 + lastChild.x + lastDims.width / 2) / 2;
1308
- nodePositions.set(id, { x: parentCenterX - dims.width / 2, y });
1309
- }
1310
- let currentRootLeft = treeStartX;
1311
- for (const r of roots) {
1312
- const span = subtreeSpan.get(r) ?? leafSpan(r);
1313
- positionSubtree(r, currentRootLeft);
1314
- 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
+ }
1315
1362
  }
1316
1363
  const nodes = [];
1317
1364
  for (const id of nodeIds) {
@@ -1490,45 +1537,85 @@ function layoutMedallion(ast, edges) {
1490
1537
  }
1491
1538
  const activeTiers = [...tiers.entries()].filter(([_, ids]) => ids.length > 0);
1492
1539
  const tierCount = activeTiers.length || 1;
1540
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
1493
1541
  const contentW = ast.meta.width - SAFE * 2;
1494
1542
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1495
1543
  const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id).width));
1496
- const availableColGap = tierCount > 1 ? (contentW - maxNodeW) / (tierCount - 1) : 0;
1497
- const colGap = Math.max(maxNodeW + 36, Math.min(360, availableColGap));
1498
- const totalW = (tierCount - 1) * colGap + maxNodeW;
1499
- const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
1544
+ const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id).height));
1500
1545
  const nodes = [];
1501
- activeTiers.forEach(([_, ids], colIdx) => {
1502
- const colCount = ids.length;
1503
- const colTierMaxW = Math.max(...ids.map((id) => nodeDims.get(id).width));
1504
- const colTierMaxH = Math.max(...ids.map((id) => nodeDims.get(id).height));
1505
- const colX = startX + colIdx * colGap;
1506
- const rowSpacing = Math.max(colTierMaxH + 32, contentH / (colCount + 1));
1507
- const colH = (colCount - 1) * rowSpacing + colTierMaxH;
1508
- const startY = TITLE_BAND + Math.max(0, (contentH - colH) / 2);
1509
- ids.forEach((id, rowIdx) => {
1510
- const decl = ast.nodes[id];
1511
- const dims = nodeDims.get(id);
1512
- const rowY = startY + rowIdx * rowSpacing + (colTierMaxH - dims.height) / 2;
1513
- const nodeX = colX + (colTierMaxW - dims.width) / 2;
1514
- const focal = decl.props.focal === true || decl.props.accent === true;
1515
- nodes.push({
1516
- id,
1517
- kind: decl.kind,
1518
- role: nodeRole(decl.kind),
1519
- label: decl.label,
1520
- x: snapGrid(nodeX),
1521
- y: snapGrid(rowY),
1522
- width: dims.width,
1523
- height: dims.height,
1524
- style: resolveNodeStyle(decl, ast),
1525
- props: decl.props,
1526
- opacity: 0,
1527
- shape: "card",
1528
- 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;
1529
1580
  });
1530
1581
  });
1531
- });
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
+ }
1532
1619
  return nodes;
1533
1620
  }
1534
1621
  function layoutQuadrant(ast) {
@@ -1725,6 +1812,40 @@ function layoutTimeline(ast) {
1725
1812
  for (const id of nodeIds) {
1726
1813
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1727
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
+ }
1728
1849
  const contentW = ast.meta.width - SAFE * 2;
1729
1850
  const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1730
1851
  const baselineY = TITLE_BAND + contentH / 2;
@@ -1772,19 +1893,20 @@ function layoutGantt(ast) {
1772
1893
  const totalH = N * rowH;
1773
1894
  const startY = topBand + Math.max(16, (contentH - totalH) / 2);
1774
1895
  const nodes = [];
1896
+ const hasExplicitPhases = nodeIds.some((nid) => typeof ast.nodes[nid].props.phase === "number" || typeof ast.nodes[nid].props.span === "number");
1775
1897
  nodeIds.forEach((id, idx) => {
1776
1898
  const decl = ast.nodes[id];
1777
1899
  const dims = nodeDims.get(id);
1778
1900
  const phase = typeof decl.props.phase === "number" ? decl.props.phase : 0;
1779
1901
  const span = typeof decl.props.span === "number" ? decl.props.span : 1;
1780
- const totalPhases = Math.max(...nodeIds.map((nid) => {
1902
+ const totalPhases = hasExplicitPhases ? Math.max(...nodeIds.map((nid) => {
1781
1903
  const p = ast.nodes[nid].props.phase;
1782
1904
  const s = ast.nodes[nid].props.span;
1783
1905
  return (typeof p === "number" ? p : 0) + (typeof s === "number" ? s : 1);
1784
- }), 1);
1906
+ }), 1) : 1;
1785
1907
  const unitW = contentW / totalPhases;
1786
- const x = SAFE + phase * unitW;
1787
- 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;
1788
1910
  const barH = dims.height;
1789
1911
  const y = startY + idx * rowH + (rowH - barH) / 2;
1790
1912
  const focal = decl.props.focal === true || decl.props.accent === true;
@@ -1813,45 +1935,67 @@ function layoutVenn(ast) {
1813
1935
  for (const id of nodeIds) {
1814
1936
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1815
1937
  }
1938
+ const titleBand = effectiveTitleBand(ast);
1939
+ const bottomBand = ast.beats.some((b) => b.label && b.label.trim().length > 0) ? 44 : 0;
1816
1940
  const contentW = ast.meta.width - SAFE * 2;
1817
- const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
1941
+ const contentH = ast.meta.height - titleBand - bottomBand - SAFE * 2;
1818
1942
  const centerX = SAFE + contentW / 2;
1819
- const centerY = TITLE_BAND + contentH / 2;
1943
+ const centerY = titleBand + SAFE + contentH / 2;
1820
1944
  const N = nodeIds.length;
1821
- const maxNodeDim = Math.max(...nodeIds.map((id) => Math.max(nodeDims.get(id).width, nodeDims.get(id).height)));
1822
- const vennSize = Math.max(VENN_NODE_SIZE, maxNodeDim);
1823
- const radius = Math.max(vennSize * 0.7, Math.min(contentW, contentH) * 0.24);
1824
1945
  const nodes = [];
1825
- nodeIds.forEach((id, idx) => {
1826
- const decl = ast.nodes[id];
1827
- const dims = nodeDims.get(id);
1828
- const circleSize = Math.max(vennSize, dims.width, dims.height);
1829
- let x, y;
1830
- if (N === 2) {
1831
- x = centerX + (idx === 0 ? -radius * 0.7 : radius * 0.7) - circleSize / 2;
1832
- y = centerY - circleSize / 2;
1833
- } 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];
1834
1978
  const angle = -Math.PI / 2 + idx * 2 * Math.PI / N;
1835
- x = centerX + radius * 0.85 * Math.cos(angle) - circleSize / 2;
1836
- y = centerY + radius * 0.85 * Math.sin(angle) - circleSize / 2;
1837
- }
1838
- const focal = decl.props.focal === true || decl.props.accent === true;
1839
- nodes.push({
1840
- id,
1841
- kind: decl.kind,
1842
- role: nodeRole(decl.kind),
1843
- label: decl.label,
1844
- x: snapGrid(x),
1845
- y: snapGrid(y),
1846
- width: circleSize,
1847
- height: circleSize,
1848
- style: resolveNodeStyle(decl, ast),
1849
- props: decl.props,
1850
- opacity: 0,
1851
- shape: "circle",
1852
- 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
+ });
1853
1997
  });
1854
- });
1998
+ }
1855
1999
  return nodes;
1856
2000
  }
1857
2001
  function layoutLayers(ast) {
@@ -1903,49 +2047,97 @@ function layoutNested(ast) {
1903
2047
  for (const id of nodeIds) {
1904
2048
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
1905
2049
  }
1906
- const topHeadroom = 20;
1907
- 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;
1908
2054
  const contentW = ast.meta.width - SAFE * 2;
1909
2055
  const contentH = ast.meta.height - startY - SAFE;
1910
- const centerX = SAFE + contentW / 2;
1911
- const centerY = startY + contentH / 2;
1912
2056
  const N = nodeIds.length;
1913
- const padX = Math.min(54, contentW * 0.42 / Math.max(N, 1));
1914
- const padY = Math.min(48, contentH * 0.42 / Math.max(N, 1));
1915
2057
  const nodes = [];
1916
- nodeIds.forEach((id, idx) => {
1917
- const decl = ast.nodes[id];
1918
- const dims = nodeDims.get(id);
1919
- const isCore = idx === N - 1;
1920
- let x, y, w, h;
1921
- if (isCore) {
1922
- w = Math.max(dims.width, Math.min(320, contentW - idx * padX * 2));
1923
- h = Math.max(dims.height, 84);
1924
- x = centerX - w / 2;
1925
- y = centerY - h / 2 + (N > 1 ? 16 : 0);
1926
- } else {
1927
- x = SAFE + idx * padX;
1928
- y = startY + idx * padY;
1929
- w = Math.max(dims.width, contentW - idx * padX * 2);
1930
- h = Math.max(dims.height, contentH - idx * padY * 2);
1931
- }
1932
- const focal = decl.props.focal === true || decl.props.accent === true || isCore;
1933
- nodes.push({
1934
- id,
1935
- kind: decl.kind,
1936
- role: nodeRole(decl.kind),
1937
- label: decl.label,
1938
- x: snapGrid(x),
1939
- y: snapGrid(y),
1940
- width: snapGrid(w),
1941
- height: snapGrid(h),
1942
- style: resolveNodeStyle(decl, ast),
1943
- props: decl.props,
1944
- opacity: 0,
1945
- shape: isCore ? "card" : "container",
1946
- 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
+ });
1947
2099
  });
1948
- });
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
+ }
1949
2141
  return nodes;
1950
2142
  }
1951
2143
  function layoutRadar(ast) {
@@ -2063,24 +2255,45 @@ function computeTreeBuses(ast, nodes, edges) {
2063
2255
  children.get(edge.from).push(edge.to);
2064
2256
  parent.add(edge.to);
2065
2257
  }
2258
+ const isVertical = ast.meta.direction === "TB" || ast.meta.direction === "BT";
2066
2259
  const buses = [];
2067
2260
  for (const [parentId, childIds] of children) {
2068
2261
  const parentNode = byId.get(parentId);
2069
2262
  const childNodes = childIds.map((id) => byId.get(id)).filter(Boolean);
2070
2263
  if (!parentNode || childNodes.length === 0) continue;
2071
- const parentX = parentNode.x + parentNode.width / 2;
2072
- const parentY = parentNode.y + parentNode.height;
2073
- const childY = Math.min(...childNodes.map((node) => node.y));
2074
- buses.push({
2075
- id: `tree_bus_${parentId}`,
2076
- parentId,
2077
- childIds,
2078
- parentX: snapGrid(parentX),
2079
- parentY: snapGrid(parentY),
2080
- branchY: snapGrid((parentY + childY) / 2),
2081
- childXs: childNodes.map((node) => snapGrid(node.x + node.width / 2)),
2082
- childY: snapGrid(childY)
2083
- });
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
+ }
2084
2297
  }
2085
2298
  return buses;
2086
2299
  }
@@ -2284,7 +2497,7 @@ function computeAdaptiveDimensions(ast, edges) {
2284
2497
  const maxNodeW = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.width)) : NODE_W;
2285
2498
  const maxNodeH = nodeDims.length > 0 ? Math.max(...nodeDims.map((d) => d.height)) : NODE_H;
2286
2499
  const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
2287
- const titleBand = hasTitle ? TITLE_BAND : 0;
2500
+ const titleBand = effectiveTitleBand(ast);
2288
2501
  const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
2289
2502
  const bottomBand = hasBeatCaptions ? 44 : 0;
2290
2503
  const titleW = hasTitle ? Math.max(320, ast.meta.title.trim().length * 11 + SAFE * 2) : 0;
@@ -2304,34 +2517,35 @@ function computeAdaptiveDimensions(ast, edges) {
2304
2517
  autoH = Math.max(320, Math.min(1800, requiredH));
2305
2518
  } else if (dtype === "medallion") {
2306
2519
  const tierSet = /* @__PURE__ */ new Set();
2520
+ const tierCounts = /* @__PURE__ */ new Map();
2307
2521
  for (const id of nodeIds) {
2308
2522
  const combined = `${id} ${ast.nodes[id].kind}`.toLowerCase();
2309
- if (combined.includes("bronze") || combined.includes("raw") || combined.includes("landing")) tierSet.add(1);
2310
- else if (combined.includes("silver") || combined.includes("clean") || combined.includes("curated") || combined.includes("conformed")) tierSet.add(2);
2311
- else if (combined.includes("gold") || combined.includes("agg") || combined.includes("mart") || combined.includes("analytics")) tierSet.add(3);
2312
- 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);
2313
- 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);
2314
2530
  }
2315
2531
  const tierCount = Math.max(tierSet.size, 4);
2316
- const tierW = Math.max(260, maxNodeW + 48);
2317
- const requiredW = Math.max(SAFE * 2 + tierCount * tierW, titleW);
2318
- const requiredH = titleBand + SAFE * 2 + Math.max(440, maxNodeH * 3 + 120) + bottomBand;
2319
- autoW = Math.max(640, Math.min(2800, requiredW));
2320
- 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
+ }
2321
2548
  } else if (dtype === "tree") {
2322
- let measureSubtree2 = function(id) {
2323
- const kids = children.get(id) ?? [];
2324
- if (kids.length === 0) {
2325
- subtreeSpan.set(id, minLeafWidth);
2326
- return minLeafWidth;
2327
- }
2328
- let total = 0;
2329
- for (const kid of kids) total += measureSubtree2(kid);
2330
- const span = Math.max(minLeafWidth, total);
2331
- subtreeSpan.set(id, span);
2332
- return span;
2333
- };
2334
- var measureSubtree = measureSubtree2;
2335
2549
  const children = /* @__PURE__ */ new Map();
2336
2550
  const hasParent = /* @__PURE__ */ new Set();
2337
2551
  for (const id of nodeIds) children.set(id, []);
@@ -2343,31 +2557,74 @@ function computeAdaptiveDimensions(ast, edges) {
2343
2557
  }
2344
2558
  }
2345
2559
  const roots = nodeIds.filter((id) => !hasParent.has(id));
2346
- const depth = /* @__PURE__ */ new Map();
2347
- const queue = [...roots];
2348
- for (const r of roots) depth.set(r, 0);
2349
- while (queue.length > 0) {
2350
- const cur = queue.shift();
2351
- const d = depth.get(cur) ?? 0;
2352
- for (const child of children.get(cur) ?? []) {
2353
- if (!depth.has(child)) {
2354
- depth.set(child, d + 1);
2355
- 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
+ }
2356
2576
  }
2357
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));
2358
2627
  }
2359
- for (const id of nodeIds) if (!depth.has(id)) depth.set(id, 0);
2360
- const maxDepth = Math.max(...depth.values(), 0);
2361
- const minLeafWidth = Math.max(NODE_W + 36, maxNodeW + 36);
2362
- const subtreeSpan = /* @__PURE__ */ new Map();
2363
- let totalRootsWidth = 0;
2364
- const activeRoots = roots.length > 0 ? roots : nodeIds;
2365
- for (const r of activeRoots) totalRootsWidth += measureSubtree2(r);
2366
- const levelH = Math.max(130, maxNodeH + 54);
2367
- const requiredW = Math.max(SAFE * 2 + totalRootsWidth, titleW);
2368
- const requiredH = titleBand + SAFE * 2 + (maxDepth + 1) * levelH + bottomBand;
2369
- autoW = Math.max(480, Math.min(2560, requiredW));
2370
- autoH = Math.max(320, Math.min(1800, requiredH));
2371
2628
  } else if (dtype === "swimlane") {
2372
2629
  const laneCount = Math.max(groupCount, 1);
2373
2630
  const maxInLane = Math.max(
@@ -2393,24 +2650,45 @@ function computeAdaptiveDimensions(ast, edges) {
2393
2650
  autoW = Math.max(480, Math.min(2e3, Math.max(SAFE * 2 + maxNodeW + 160, titleW)));
2394
2651
  const requiredH = titleBand + SAFE * 2 + layerCount * layerH + bottomBand;
2395
2652
  autoH = Math.max(320, Math.min(1440, requiredH));
2396
- } else if (dtype === "timeline" || dtype === "gantt") {
2653
+ } else if (dtype === "timeline") {
2397
2654
  const milestones = Math.max(nodeCount, 1);
2398
- let totalPhases = milestones;
2399
- if (dtype === "gantt") {
2400
- 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) => {
2401
2682
  const p = ast.nodes[nid].props.phase;
2402
2683
  const s = ast.nodes[nid].props.span;
2403
2684
  return (typeof p === "number" ? p : 0) + (typeof s === "number" ? s : 1);
2404
- }), 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)));
2405
2691
  }
2406
- const milestoneW = Math.max(220, maxNodeW + 36);
2407
- const requiredW = Math.max(
2408
- SAFE * 2 + (dtype === "gantt" ? totalPhases * Math.max(160, maxNodeW + 16) : milestones * milestoneW),
2409
- titleW
2410
- );
2411
- 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;
2412
- autoW = Math.max(480, Math.min(2560, requiredW));
2413
- autoH = Math.max(320, Math.min(1600, requiredH));
2414
2692
  } else if (dtype === "loop" || dtype === "flywheel" || dtype === "radar" || dtype === "venn" || dtype === "constellation") {
2415
2693
  const N = Math.max(nodeCount, 3);
2416
2694
  const minRadius = Math.max(160, maxNodeW * 0.75);
@@ -2425,6 +2703,31 @@ function computeAdaptiveDimensions(ast, edges) {
2425
2703
  const requiredH = titleBand + SAFE * 2 + maxNodeH * 3 + bottomBand;
2426
2704
  autoW = Math.min(2400, requiredW);
2427
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
+ }
2428
2731
  } else {
2429
2732
  const forceVertical = dtype === "flowchart" && isVertical || isVertical;
2430
2733
  const ranks = assignRanks(nodeIds, effectiveEdges, forceVertical ? "TB" : "LR");
@@ -2473,7 +2776,8 @@ function compilePlan(ast, theme) {
2473
2776
  }
2474
2777
  const nodes = layoutNodes(ast, structuralEdges);
2475
2778
  const groupBoundaries = computeGroupBoundaries(ast, nodes);
2476
- 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);
2477
2781
  const { cues, beats } = scheduleBeats(ast, structuralEdges);
2478
2782
  const sequence = buildSequencePlan(ast, cues);
2479
2783
  const shown = /* @__PURE__ */ new Set();
@@ -3734,7 +4038,7 @@ function parse(source, opts = {}) {
3734
4038
  diagnostics.push({ severity: "warning", message: `unknown diagram type '${v}'`, line: lineNo });
3735
4039
  } else {
3736
4040
  meta.type = t;
3737
- if (t === "flowchart" && !props.direction && !props.layout && !props.rankdir && !inlineLayout) {
4041
+ if ((t === "flowchart" || t === "nested") && !props.direction && !props.layout && !props.rankdir && !inlineLayout) {
3738
4042
  meta.direction = "TB";
3739
4043
  meta.explicitDirection = true;
3740
4044
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
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",