@wireweave/core 1.0.0-beta.20260107134058 → 1.1.0-beta.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/renderer.cjs CHANGED
@@ -118,12 +118,17 @@ function generateContainerStyles(prefix) {
118
118
  padding: 16px;
119
119
  }
120
120
 
121
- /* Cards in flex rows should expand equally */
121
+ /* Cards in flex rows: respect explicit width, shrink if needed */
122
122
  .${prefix}-row > .${prefix}-card {
123
- flex: 1 1 0%;
123
+ flex: 0 1 auto;
124
124
  min-width: 0;
125
125
  }
126
126
 
127
+ /* Cards without explicit width should expand to fill space */
128
+ .${prefix}-row > .${prefix}-card-flex {
129
+ flex: 1 1 0%;
130
+ }
131
+
127
132
  .${prefix}-card-title {
128
133
  margin: 0 0 12px 0;
129
134
  font-size: 18px;
@@ -277,6 +282,11 @@ function generateTextStyles(prefix) {
277
282
  line-height: 1.25;
278
283
  }
279
284
 
285
+ /* Remove bottom margin when title is in a row (inline with other elements) */
286
+ .${prefix}-row .${prefix}-title {
287
+ margin-bottom: 0;
288
+ }
289
+
280
290
  h1.${prefix}-title { font-size: 36px; }
281
291
  h2.${prefix}-title { font-size: 30px; }
282
292
  h3.${prefix}-title { font-size: 24px; }
@@ -671,6 +681,30 @@ img.${prefix}-image {
671
681
  font-size: 14px;
672
682
  }
673
683
 
684
+ .${prefix}-placeholder-with-children {
685
+ position: relative;
686
+ }
687
+
688
+ .${prefix}-placeholder-label {
689
+ position: absolute;
690
+ top: 50%;
691
+ left: 50%;
692
+ transform: translate(-50%, -50%);
693
+ z-index: 0;
694
+ pointer-events: none;
695
+ }
696
+
697
+ .${prefix}-placeholder-overlay {
698
+ position: absolute;
699
+ top: 0;
700
+ left: 0;
701
+ right: 0;
702
+ bottom: 0;
703
+ z-index: 1;
704
+ display: flex;
705
+ flex-direction: column;
706
+ }
707
+
674
708
  .${prefix}-avatar {
675
709
  display: inline-flex;
676
710
  align-items: center;
@@ -786,12 +820,12 @@ svg.${prefix}-icon {
786
820
  display: block;
787
821
  }
788
822
 
789
- /* Icon size tokens */
823
+ /* Icon size tokens - matches SVG renderer */
790
824
  svg.${prefix}-icon-xs { width: 12px; height: 12px; }
791
- svg.${prefix}-icon-sm { width: 14px; height: 14px; }
792
- svg.${prefix}-icon-md { width: 16px; height: 16px; }
793
- svg.${prefix}-icon-lg { width: 20px; height: 20px; }
794
- svg.${prefix}-icon-xl { width: 24px; height: 24px; }
825
+ svg.${prefix}-icon-sm { width: 16px; height: 16px; }
826
+ svg.${prefix}-icon-md { width: 20px; height: 20px; }
827
+ svg.${prefix}-icon-lg { width: 24px; height: 24px; }
828
+ svg.${prefix}-icon-xl { width: 32px; height: 32px; }
795
829
 
796
830
  .${prefix}-icon svg {
797
831
  display: block;
@@ -1087,6 +1121,27 @@ function generateNavigationStyles(_theme, prefix) {
1087
1121
  cursor: not-allowed;
1088
1122
  }
1089
1123
 
1124
+ .${prefix}-nav-group {
1125
+ display: flex;
1126
+ flex-direction: column;
1127
+ gap: 4px;
1128
+ }
1129
+
1130
+ .${prefix}-nav-group-label {
1131
+ font-size: 11px;
1132
+ font-weight: 500;
1133
+ color: var(--${prefix}-muted);
1134
+ text-transform: uppercase;
1135
+ letter-spacing: 0.05em;
1136
+ padding: 8px 16px 4px;
1137
+ }
1138
+
1139
+ .${prefix}-nav-divider {
1140
+ margin: 8px 0;
1141
+ border: none;
1142
+ border-top: 1px solid var(--${prefix}-border);
1143
+ }
1144
+
1090
1145
  .${prefix}-tabs {
1091
1146
  border-bottom: 1px solid var(--${prefix}-border);
1092
1147
  }
@@ -1304,7 +1359,6 @@ function generateBaseStyles(prefix) {
1304
1359
  font-family: var(--${prefix}-font);
1305
1360
  color: var(--${prefix}-fg);
1306
1361
  background: var(--${prefix}-bg);
1307
- min-height: 100vh;
1308
1362
  box-sizing: border-box;
1309
1363
  position: relative;
1310
1364
  display: flex;
@@ -1315,10 +1369,19 @@ function generateBaseStyles(prefix) {
1315
1369
  overflow: hidden;
1316
1370
  }
1317
1371
 
1372
+ /* Col direct child of page should fill page height */
1373
+ .${prefix}-page > .${prefix}-col {
1374
+ flex: 1;
1375
+ min-height: 0;
1376
+ }
1377
+
1318
1378
  /* Row containing sidebar should fill remaining space */
1319
1379
  .${prefix}-page > .${prefix}-row:has(.${prefix}-sidebar),
1320
- .${prefix}-page > .${prefix}-row:has(.${prefix}-main) {
1380
+ .${prefix}-page > .${prefix}-row:has(.${prefix}-main),
1381
+ .${prefix}-page > .${prefix}-col > .${prefix}-row:has(.${prefix}-sidebar),
1382
+ .${prefix}-page > .${prefix}-col > .${prefix}-row:has(.${prefix}-main) {
1321
1383
  flex: 1;
1384
+ min-height: 0;
1322
1385
  align-items: stretch;
1323
1386
  }
1324
1387
 
@@ -1361,10 +1424,15 @@ function generateGridClasses(_theme, prefix) {
1361
1424
  box-sizing: border-box;
1362
1425
  }
1363
1426
 
1427
+ /* When explicit width is set, don't flex-grow */
1428
+ .${prefix}-row[style*="width:"],
1429
+ .${prefix}-col[style*="width:"] {
1430
+ flex: 0 0 auto;
1431
+ }
1432
+
1364
1433
  `;
1365
1434
  for (let i = 1; i <= 12; i++) {
1366
- const width = (i / 12 * 100).toFixed(4);
1367
- css += `.${prefix}-col-${i} { flex: 0 0 auto; width: ${width}%; }
1435
+ css += `.${prefix}-col-${i} { flex: ${i} 0 0%; min-width: 0; }
1368
1436
  `;
1369
1437
  }
1370
1438
  return css;
@@ -1532,6 +1600,20 @@ function generateLayoutClasses(prefix) {
1532
1600
  .${prefix}-main {
1533
1601
  flex: 1;
1534
1602
  padding: 16px;
1603
+ display: flex;
1604
+ flex-direction: column;
1605
+ }
1606
+
1607
+ /* Scrollable main content */
1608
+ .${prefix}-main.${prefix}-scroll {
1609
+ overflow-y: auto;
1610
+ overflow-x: hidden;
1611
+ }
1612
+
1613
+ /* Main content should align to top, not stretch to fill */
1614
+ /* But allow explicit flex=1 to override */
1615
+ .${prefix}-main > .${prefix}-col:not(.${prefix}-flex-1) {
1616
+ flex: 0 0 auto;
1535
1617
  }
1536
1618
 
1537
1619
  .${prefix}-footer {
@@ -1548,6 +1630,7 @@ function generateLayoutClasses(prefix) {
1548
1630
  border-right: 1px solid var(--${prefix}-border);
1549
1631
  padding: 16px 16px 16px 20px;
1550
1632
  flex-shrink: 0;
1633
+ align-self: stretch;
1551
1634
  }
1552
1635
 
1553
1636
  .${prefix}-sidebar-right {
@@ -48117,14 +48200,39 @@ var lucideIcons = {
48117
48200
  ]
48118
48201
  ]
48119
48202
  };
48203
+ var iconAliases = {
48204
+ "home": "house",
48205
+ "plus-square": "square-plus",
48206
+ "minus-square": "square-minus",
48207
+ "x-square": "square-x",
48208
+ "check-square": "square-check",
48209
+ "edit": "pencil",
48210
+ "edit-2": "pencil",
48211
+ "edit-3": "pencil-line",
48212
+ "trash": "trash-2",
48213
+ "delete": "trash-2",
48214
+ "close": "x",
48215
+ "menu": "menu",
48216
+ "hamburger": "menu",
48217
+ "dots": "more-horizontal",
48218
+ "dots-vertical": "more-vertical",
48219
+ "cog": "settings",
48220
+ "gear": "settings"
48221
+ };
48120
48222
  function getIconData(name) {
48121
48223
  if (lucideIcons[name]) {
48122
48224
  return lucideIcons[name];
48123
48225
  }
48226
+ if (iconAliases[name] && lucideIcons[iconAliases[name]]) {
48227
+ return lucideIcons[iconAliases[name]];
48228
+ }
48124
48229
  const kebabName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
48125
48230
  if (lucideIcons[kebabName]) {
48126
48231
  return lucideIcons[kebabName];
48127
48232
  }
48233
+ if (iconAliases[kebabName] && lucideIcons[iconAliases[kebabName]]) {
48234
+ return lucideIcons[iconAliases[kebabName]];
48235
+ }
48128
48236
  return void 0;
48129
48237
  }
48130
48238
  function renderIconSvg(data, _size = 24, strokeWidth = 2, className = "", styleAttr = "") {
@@ -48234,7 +48342,7 @@ var HtmlRenderer = class extends BaseRenderer {
48234
48342
  const title = node.title ? `<title>${this.escapeHtml(node.title)}</title>
48235
48343
  ` : "";
48236
48344
  const commonStyles = this.buildCommonStyles(node);
48237
- const viewportStyle = `width: ${viewport.width}px; height: ${viewport.height}px`;
48345
+ const viewportStyle = `position: relative; width: ${viewport.width}px; height: ${viewport.height}px; overflow: hidden`;
48238
48346
  const combinedStyle = commonStyles ? `${viewportStyle}; ${commonStyles}` : viewportStyle;
48239
48347
  const dataAttrs = `data-viewport-width="${viewport.width}" data-viewport-height="${viewport.height}" data-viewport-label="${viewport.label}"`;
48240
48348
  return `<div class="${classes}" style="${combinedStyle}" ${dataAttrs}>
@@ -48400,6 +48508,7 @@ ${children}
48400
48508
  renderMain(node) {
48401
48509
  const classes = this.buildClassString([
48402
48510
  `${this.prefix}-main`,
48511
+ node.scroll ? `${this.prefix}-scroll` : void 0,
48403
48512
  ...this.getCommonClasses(node)
48404
48513
  ]);
48405
48514
  const styles = this.buildCommonStyles(node);
@@ -48485,6 +48594,10 @@ ${children}
48485
48594
  /**
48486
48595
  * Build common inline styles for all values
48487
48596
  *
48597
+ * Position values (x, y) for absolute positioning:
48598
+ * - When x or y is specified, element gets position: absolute
48599
+ * - x → left, y → top
48600
+ *
48488
48601
  * Spacing values (p, m, gap) use token system:
48489
48602
  * - number: spacing token (e.g., p=4 → padding: 16px from token table)
48490
48603
  * - ValueWithUnit: direct CSS value (e.g., p=16px → padding: 16px)
@@ -48499,6 +48612,17 @@ ${children}
48499
48612
  */
48500
48613
  buildCommonStyles(props) {
48501
48614
  const styles = [];
48615
+ if (props.x !== void 0 || props.y !== void 0) {
48616
+ styles.push("position: absolute");
48617
+ if (props.x !== void 0) {
48618
+ const xValue = resolveSizeValueToCss(props.x);
48619
+ if (xValue) styles.push(`left: ${xValue}`);
48620
+ }
48621
+ if (props.y !== void 0) {
48622
+ const yValue = resolveSizeValueToCss(props.y);
48623
+ if (yValue) styles.push(`top: ${yValue}`);
48624
+ }
48625
+ }
48502
48626
  const wValue = resolveSizeValueToCss(props.w);
48503
48627
  if (wValue) {
48504
48628
  styles.push(`width: ${wValue}`);
@@ -48608,8 +48732,10 @@ ${children}
48608
48732
  // Container Node Renderers
48609
48733
  // ===========================================
48610
48734
  renderCard(node) {
48735
+ const hasExplicitWidth = node.w !== void 0;
48611
48736
  const classes = this.buildClassString([
48612
48737
  `${this.prefix}-card`,
48738
+ !hasExplicitWidth ? `${this.prefix}-card-flex` : void 0,
48613
48739
  node.shadow ? `${this.prefix}-card-shadow-${node.shadow}` : void 0,
48614
48740
  ...this.getCommonClasses(node)
48615
48741
  ]);
@@ -48946,11 +49072,19 @@ ${slider}`;
48946
49072
  renderPlaceholder(node) {
48947
49073
  const classes = this.buildClassString([
48948
49074
  `${this.prefix}-placeholder`,
49075
+ node.children && node.children.length > 0 ? `${this.prefix}-placeholder-with-children` : void 0,
48949
49076
  ...this.getCommonClasses(node)
48950
49077
  ]);
48951
49078
  const styles = this.buildCommonStyles(node);
48952
49079
  const styleAttr = styles ? ` style="${styles}"` : "";
48953
49080
  const label = node.label ? this.escapeHtml(node.label) : "Placeholder";
49081
+ if (node.children && node.children.length > 0) {
49082
+ const childrenHtml = this.renderChildren(node.children);
49083
+ return `<div class="${classes}"${styleAttr}>
49084
+ <span class="${this.prefix}-placeholder-label">${label}</span>
49085
+ <div class="${this.prefix}-placeholder-overlay">${childrenHtml}</div>
49086
+ </div>`;
49087
+ }
48954
49088
  return `<div class="${classes}"${styleAttr}>${label}</div>`;
48955
49089
  }
48956
49090
  renderAvatar(node) {
@@ -49196,6 +49330,12 @@ ${items}
49196
49330
  ]);
49197
49331
  const styles = this.buildCommonStyles(node);
49198
49332
  const styleAttr = styles ? ` style="${styles}"` : "";
49333
+ if (node.children && node.children.length > 0) {
49334
+ const content = this.renderNavChildren(node.children);
49335
+ return `<nav class="${classes}"${styleAttr}>
49336
+ ${content}
49337
+ </nav>`;
49338
+ }
49199
49339
  const items = node.items.map((item) => {
49200
49340
  if (typeof item === "string") {
49201
49341
  return `<a class="${this.prefix}-nav-link" href="#">${this.escapeHtml(item)}</a>`;
@@ -49205,12 +49345,48 @@ ${items}
49205
49345
  item.active ? `${this.prefix}-nav-link-active` : void 0,
49206
49346
  item.disabled ? `${this.prefix}-nav-link-disabled` : void 0
49207
49347
  ]);
49208
- return `<a class="${linkClasses}" href="${item.href || "#"}">${this.escapeHtml(item.label)}</a>`;
49348
+ const iconHtml = item.icon ? this.renderIconHtml(item.icon) + " " : "";
49349
+ return `<a class="${linkClasses}" href="${item.href || "#"}">${iconHtml}${this.escapeHtml(item.label)}</a>`;
49209
49350
  }).join("\n");
49210
49351
  return `<nav class="${classes}"${styleAttr}>
49211
49352
  ${items}
49212
49353
  </nav>`;
49213
49354
  }
49355
+ renderNavChildren(children) {
49356
+ return children.map((child) => {
49357
+ if (child.type === "divider") {
49358
+ return `<hr class="${this.prefix}-nav-divider" />`;
49359
+ }
49360
+ if (child.type === "group") {
49361
+ const groupItems = child.items.map((item) => {
49362
+ if (item.type === "divider") {
49363
+ return `<hr class="${this.prefix}-nav-divider" />`;
49364
+ }
49365
+ return this.renderNavItem(item);
49366
+ }).join("\n");
49367
+ return `<div class="${this.prefix}-nav-group">
49368
+ <div class="${this.prefix}-nav-group-label">${this.escapeHtml(child.label)}</div>
49369
+ ${groupItems}
49370
+ </div>`;
49371
+ }
49372
+ if (child.type === "item") {
49373
+ return this.renderNavItem(child);
49374
+ }
49375
+ return "";
49376
+ }).join("\n");
49377
+ }
49378
+ renderNavItem(item) {
49379
+ const linkClasses = this.buildClassString([
49380
+ `${this.prefix}-nav-link`,
49381
+ item.active ? `${this.prefix}-nav-link-active` : void 0,
49382
+ item.disabled ? `${this.prefix}-nav-link-disabled` : void 0
49383
+ ]);
49384
+ const iconHtml = item.icon ? this.renderIconHtml(item.icon) + " " : "";
49385
+ return `<a class="${linkClasses}" href="${item.href || "#"}">${iconHtml}${this.escapeHtml(item.label)}</a>`;
49386
+ }
49387
+ renderIconHtml(iconName) {
49388
+ return `<span class="${this.prefix}-icon" data-icon="${iconName}"></span>`;
49389
+ }
49214
49390
  renderTabs(node) {
49215
49391
  const classes = this.buildClassString([
49216
49392
  `${this.prefix}-tabs`,
@@ -49365,13 +49541,239 @@ function createHtmlRenderer(options) {
49365
49541
  return new HtmlRenderer(options);
49366
49542
  }
49367
49543
 
49544
+ // src/renderer/svg/flex-layout.ts
49545
+ function computeFlexLayout(items, config) {
49546
+ const { mainSize, crossSize, justifyContent, alignItems, gap } = config;
49547
+ const computed = items.map((props, index) => {
49548
+ let flexBasis;
49549
+ if (props.basis === "auto" || props.basis === "content") {
49550
+ flexBasis = props.contentSize;
49551
+ } else {
49552
+ flexBasis = props.basis;
49553
+ }
49554
+ flexBasis = clamp(flexBasis, props.minSize, props.maxSize);
49555
+ return {
49556
+ index,
49557
+ props,
49558
+ flexBasis,
49559
+ mainSize: flexBasis,
49560
+ crossSize: 0,
49561
+ mainPosition: 0,
49562
+ crossPosition: 0,
49563
+ frozen: false,
49564
+ scaledShrinkFactor: props.shrink * flexBasis
49565
+ };
49566
+ });
49567
+ const totalGap = Math.max(0, (items.length - 1) * gap);
49568
+ const totalFlexBasis = computed.reduce((sum, item) => sum + item.flexBasis, 0);
49569
+ let freeSpace = mainSize - totalFlexBasis - totalGap;
49570
+ if (freeSpace !== 0) {
49571
+ resolveFlexibleLengths(computed, freeSpace);
49572
+ }
49573
+ distributeMainAxis(computed, mainSize, totalGap, gap, justifyContent);
49574
+ const crossSizeMax = crossSize ?? Math.max(...computed.map((item) => item.props.contentSize), 0);
49575
+ alignCrossAxis(computed, crossSizeMax, alignItems);
49576
+ const mainSizeUsed = computed.reduce((sum, item) => sum + item.mainSize, 0) + totalGap;
49577
+ return {
49578
+ items: computed,
49579
+ mainSizeUsed,
49580
+ crossSizeMax
49581
+ };
49582
+ }
49583
+ function resolveFlexibleLengths(items, initialFreeSpace) {
49584
+ items.forEach((item) => {
49585
+ item.frozen = false;
49586
+ item.mainSize = item.flexBasis;
49587
+ });
49588
+ let freeSpace = initialFreeSpace;
49589
+ const isGrowing = freeSpace > 0;
49590
+ let iteration = 0;
49591
+ const maxIterations = items.length + 1;
49592
+ while (iteration < maxIterations) {
49593
+ iteration++;
49594
+ const unfrozen = items.filter((item) => !item.frozen);
49595
+ if (unfrozen.length === 0) break;
49596
+ let flexFactorSum;
49597
+ if (isGrowing) {
49598
+ flexFactorSum = unfrozen.reduce((sum, item) => sum + item.props.grow, 0);
49599
+ } else {
49600
+ flexFactorSum = unfrozen.reduce((sum, item) => sum + item.scaledShrinkFactor, 0);
49601
+ }
49602
+ if (flexFactorSum === 0) {
49603
+ unfrozen.forEach((item) => item.frozen = true);
49604
+ break;
49605
+ }
49606
+ const usedSpace = items.reduce((sum, item) => sum + item.mainSize, 0);
49607
+ const containerMainSize = usedSpace + freeSpace;
49608
+ freeSpace = containerMainSize - usedSpace;
49609
+ let totalViolation = 0;
49610
+ for (const item of unfrozen) {
49611
+ let flexFraction;
49612
+ if (isGrowing) {
49613
+ flexFraction = item.props.grow / flexFactorSum;
49614
+ } else {
49615
+ flexFraction = item.scaledShrinkFactor / flexFactorSum;
49616
+ }
49617
+ const deltaSize = freeSpace * flexFraction;
49618
+ const targetSize = item.flexBasis + deltaSize;
49619
+ const clampedSize = clamp(targetSize, item.props.minSize, item.props.maxSize);
49620
+ const violation = clampedSize - targetSize;
49621
+ item.mainSize = clampedSize;
49622
+ totalViolation += violation;
49623
+ if (violation !== 0) {
49624
+ item.frozen = true;
49625
+ }
49626
+ }
49627
+ if (Math.abs(totalViolation) < 0.01) {
49628
+ unfrozen.forEach((item) => item.frozen = true);
49629
+ break;
49630
+ }
49631
+ freeSpace = containerMainSize - items.reduce((sum, item) => sum + item.mainSize, 0);
49632
+ }
49633
+ }
49634
+ function distributeMainAxis(items, containerSize, totalGap, gap, justifyContent) {
49635
+ if (items.length === 0) return;
49636
+ const totalItemSize = items.reduce((sum, item) => sum + item.mainSize, 0);
49637
+ const freeSpace = containerSize - totalItemSize - totalGap;
49638
+ let position = 0;
49639
+ let itemGap = gap;
49640
+ let leadingSpace = 0;
49641
+ switch (justifyContent) {
49642
+ case "flex-start":
49643
+ leadingSpace = 0;
49644
+ break;
49645
+ case "flex-end":
49646
+ leadingSpace = freeSpace;
49647
+ break;
49648
+ case "center":
49649
+ leadingSpace = freeSpace / 2;
49650
+ break;
49651
+ case "space-between":
49652
+ leadingSpace = 0;
49653
+ if (items.length > 1) {
49654
+ itemGap = gap + freeSpace / (items.length - 1);
49655
+ }
49656
+ break;
49657
+ case "space-around":
49658
+ if (items.length > 0) {
49659
+ const spacePerItem = freeSpace / items.length;
49660
+ leadingSpace = spacePerItem / 2;
49661
+ itemGap = gap + spacePerItem;
49662
+ }
49663
+ break;
49664
+ case "space-evenly":
49665
+ if (items.length > 0) {
49666
+ const totalSpaces = items.length + 1;
49667
+ const spaceSize = freeSpace / totalSpaces;
49668
+ leadingSpace = spaceSize;
49669
+ itemGap = gap + spaceSize;
49670
+ }
49671
+ break;
49672
+ }
49673
+ position = leadingSpace;
49674
+ for (let i = 0; i < items.length; i++) {
49675
+ items[i].mainPosition = position;
49676
+ position += items[i].mainSize;
49677
+ if (i < items.length - 1) {
49678
+ position += itemGap;
49679
+ }
49680
+ }
49681
+ }
49682
+ function alignCrossAxis(items, containerCrossSize, alignItems) {
49683
+ for (const item of items) {
49684
+ const align = item.props.alignSelf ?? alignItems;
49685
+ const itemCrossSize = item.props.contentSize;
49686
+ item.crossSize = align === "stretch" ? containerCrossSize : itemCrossSize;
49687
+ switch (align) {
49688
+ case "flex-start":
49689
+ item.crossPosition = 0;
49690
+ break;
49691
+ case "flex-end":
49692
+ item.crossPosition = containerCrossSize - item.crossSize;
49693
+ break;
49694
+ case "center":
49695
+ item.crossPosition = (containerCrossSize - item.crossSize) / 2;
49696
+ break;
49697
+ case "stretch":
49698
+ item.crossPosition = 0;
49699
+ item.crossSize = containerCrossSize;
49700
+ break;
49701
+ case "baseline":
49702
+ item.crossPosition = 0;
49703
+ break;
49704
+ }
49705
+ }
49706
+ }
49707
+ function clamp(value, min, max) {
49708
+ return Math.max(min, Math.min(max, value));
49709
+ }
49710
+ function toJustifyContent(justify) {
49711
+ switch (justify) {
49712
+ case "start":
49713
+ return "flex-start";
49714
+ case "end":
49715
+ return "flex-end";
49716
+ case "center":
49717
+ return "center";
49718
+ case "between":
49719
+ return "space-between";
49720
+ case "around":
49721
+ return "space-around";
49722
+ case "evenly":
49723
+ return "space-evenly";
49724
+ default:
49725
+ return "flex-start";
49726
+ }
49727
+ }
49728
+ function toAlignItems(align) {
49729
+ switch (align) {
49730
+ case "start":
49731
+ return "flex-start";
49732
+ case "end":
49733
+ return "flex-end";
49734
+ case "center":
49735
+ return "center";
49736
+ case "stretch":
49737
+ return "stretch";
49738
+ case "baseline":
49739
+ return "baseline";
49740
+ default:
49741
+ return "stretch";
49742
+ }
49743
+ }
49744
+ function createFlexItemProps(contentSize, options = {}) {
49745
+ return {
49746
+ basis: options.basis ?? "auto",
49747
+ grow: options.grow ?? 0,
49748
+ shrink: options.shrink ?? 1,
49749
+ minSize: options.minSize ?? 0,
49750
+ maxSize: options.maxSize ?? Infinity,
49751
+ contentSize,
49752
+ alignSelf: options.alignSelf
49753
+ };
49754
+ }
49755
+ function createFlexGrowItemProps(contentSize, options = {}) {
49756
+ return {
49757
+ basis: options.basis ?? 0,
49758
+ grow: options.grow ?? 1,
49759
+ shrink: options.shrink ?? 1,
49760
+ minSize: options.minSize ?? 0,
49761
+ maxSize: options.maxSize ?? Infinity,
49762
+ contentSize,
49763
+ alignSelf: options.alignSelf
49764
+ };
49765
+ }
49766
+
49368
49767
  // src/renderer/svg/index.ts
49369
49768
  var SvgRenderer = class {
49370
49769
  options;
49371
49770
  theme;
49372
- currentX = 0;
49373
- currentY = 0;
49374
- contentWidth = 0;
49771
+ pageWidth = 0;
49772
+ pageHeight = 0;
49773
+ clipPathDefs = [];
49774
+ clipPathCounter = 0;
49775
+ // Default spacing values
49776
+ DEFAULT_GAP = 16;
49375
49777
  constructor(options = {}) {
49376
49778
  this.options = {
49377
49779
  width: options.width ?? 800,
@@ -49382,28 +49784,41 @@ var SvgRenderer = class {
49382
49784
  fontFamily: options.fontFamily ?? "system-ui, -apple-system, sans-serif"
49383
49785
  };
49384
49786
  this.theme = defaultTheme;
49385
- this.contentWidth = this.options.width - this.options.padding * 2;
49386
49787
  }
49387
49788
  /**
49388
49789
  * Render a wireframe document to SVG
49389
49790
  */
49390
49791
  render(doc) {
49391
- this.currentX = this.options.padding;
49392
- this.currentY = this.options.padding;
49792
+ this.clipPathDefs = [];
49793
+ this.clipPathCounter = 0;
49393
49794
  const firstPage = doc.children[0];
49394
49795
  let width = this.options.width;
49395
49796
  let height = this.options.height;
49396
- if (firstPage && (firstPage.viewport !== void 0 || firstPage.device !== void 0)) {
49397
- const viewport = resolveViewport(firstPage.viewport, firstPage.device);
49398
- width = viewport.width;
49399
- height = viewport.height;
49400
- this.contentWidth = width - this.options.padding * 2;
49797
+ if (firstPage) {
49798
+ const pageAny = firstPage;
49799
+ const hasExplicitWidth = pageAny.width !== void 0;
49800
+ const hasExplicitHeight = pageAny.height !== void 0;
49801
+ if (hasExplicitWidth || hasExplicitHeight) {
49802
+ if (hasExplicitWidth) {
49803
+ width = pageAny.width;
49804
+ }
49805
+ if (hasExplicitHeight) {
49806
+ height = pageAny.height;
49807
+ }
49808
+ } else if (firstPage.viewport !== void 0 || firstPage.device !== void 0) {
49809
+ const viewport = resolveViewport(firstPage.viewport, firstPage.device);
49810
+ width = viewport.width;
49811
+ height = viewport.height;
49812
+ }
49401
49813
  }
49814
+ this.pageWidth = width;
49815
+ this.pageHeight = height;
49402
49816
  const content = doc.children.map((page) => this.renderPage(page)).join("\n");
49817
+ const allDefs = this.generateDefs() + "\n" + this.clipPathDefs.join("\n");
49403
49818
  const svg = `<?xml version="1.0" encoding="UTF-8"?>
49404
49819
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
49405
49820
  <defs>
49406
- ${this.generateDefs()}
49821
+ ${allDefs}
49407
49822
  </defs>
49408
49823
  <rect width="100%" height="100%" fill="${this.options.background}"/>
49409
49824
  <g transform="scale(${this.options.scale})">
@@ -49413,7 +49828,7 @@ var SvgRenderer = class {
49413
49828
  return { svg, width, height };
49414
49829
  }
49415
49830
  /**
49416
- * Generate SVG defs (styles, patterns, etc.)
49831
+ * Generate SVG defs (styles)
49417
49832
  */
49418
49833
  generateDefs() {
49419
49834
  return `
@@ -49424,684 +49839,1500 @@ var SvgRenderer = class {
49424
49839
  </style>
49425
49840
  `;
49426
49841
  }
49842
+ // ===========================================
49843
+ // Page Layout
49844
+ // ===========================================
49845
+ renderPage(page) {
49846
+ const padding = this.options.padding;
49847
+ const contentWidth = this.pageWidth - padding * 2;
49848
+ const contentHeight = this.pageHeight - padding * 2;
49849
+ const constraints = {
49850
+ maxWidth: contentWidth,
49851
+ maxHeight: contentHeight
49852
+ };
49853
+ const childBoxes = [];
49854
+ let currentY = padding;
49855
+ const isCentered = page.centered === true;
49856
+ const hasHeader = page.children.some((c) => c.type === "Header");
49857
+ const hasFooter = page.children.some((c) => c.type === "Footer");
49858
+ const hasMain = page.children.some((c) => c.type === "Main");
49859
+ if (hasHeader || hasFooter || hasMain) {
49860
+ return this.renderPageWithFixedLayout(page, padding, contentWidth, contentHeight);
49861
+ }
49862
+ const measurements = page.children.map((child) => this.measureNode(child, constraints));
49863
+ const gap = this.getGap(page) || this.DEFAULT_GAP;
49864
+ const totalChildrenHeight = measurements.reduce((sum, m, i) => {
49865
+ return sum + m.height + (i > 0 ? gap : 0);
49866
+ }, 0);
49867
+ if (isCentered) {
49868
+ const availableHeight = contentHeight;
49869
+ currentY = padding + Math.max(0, (availableHeight - totalChildrenHeight) / 2);
49870
+ }
49871
+ for (let i = 0; i < page.children.length; i++) {
49872
+ const child = page.children[i];
49873
+ const measurement = measurements[i];
49874
+ let childX = padding;
49875
+ if (isCentered || this.shouldCenterHorizontally(child)) {
49876
+ childX = padding + (contentWidth - measurement.width) / 2;
49877
+ }
49878
+ const box = this.layoutNode(child, childX, currentY, constraints);
49879
+ childBoxes.push(box);
49880
+ currentY += box.height + gap;
49881
+ }
49882
+ const elements = [];
49883
+ for (const box of childBoxes) {
49884
+ elements.push(this.renderBox(box));
49885
+ }
49886
+ return elements.join("\n");
49887
+ }
49427
49888
  /**
49428
- * Render a page node
49889
+ * Render page with fixed header/footer layout
49890
+ * Header at top, Footer at bottom, Main fills remaining space
49429
49891
  */
49430
- renderPage(node) {
49892
+ renderPageWithFixedLayout(page, padding, contentWidth, contentHeight) {
49893
+ const constraints = {
49894
+ maxWidth: contentWidth,
49895
+ maxHeight: contentHeight
49896
+ };
49897
+ const header = page.children.find((c) => c.type === "Header");
49898
+ const footer = page.children.find((c) => c.type === "Footer");
49899
+ const otherChildren = page.children.filter((c) => c.type !== "Header" && c.type !== "Footer");
49431
49900
  const elements = [];
49432
- if (node.title) {
49433
- elements.push(this.renderPageTitle(node.title));
49901
+ let headerHeight = 0;
49902
+ let footerHeight = 0;
49903
+ let currentY = padding;
49904
+ if (header) {
49905
+ const headerMeasure = this.measureNode(header, constraints);
49906
+ headerHeight = headerMeasure.height;
49907
+ const headerBox = this.layoutNode(header, padding, currentY, constraints);
49908
+ elements.push(this.renderBox(headerBox));
49434
49909
  }
49435
- for (const child of node.children) {
49436
- elements.push(this.renderNode(child));
49910
+ if (footer) {
49911
+ const footerMeasure = this.measureNode(footer, constraints);
49912
+ footerHeight = footerMeasure.height;
49913
+ const footerY = this.pageHeight - padding - footerHeight;
49914
+ const footerBox = this.layoutNode(footer, padding, footerY, constraints);
49915
+ elements.push(this.renderBox(footerBox));
49916
+ }
49917
+ const mainStartY = currentY + headerHeight;
49918
+ const mainEndY = this.pageHeight - padding - footerHeight;
49919
+ const mainHeight = mainEndY - mainStartY;
49920
+ if (otherChildren.length > 0) {
49921
+ const mainConstraints = {
49922
+ maxWidth: contentWidth,
49923
+ maxHeight: mainHeight
49924
+ };
49925
+ const clipId = `main-clip-${this.clipPathCounter++}`;
49926
+ this.clipPathDefs.push(`<clipPath id="${clipId}"><rect x="${padding}" y="${mainStartY}" width="${contentWidth}" height="${mainHeight}"/></clipPath>`);
49927
+ const mainContent = [];
49928
+ mainContent.push(`<g clip-path="url(#${clipId})">`);
49929
+ let childY = mainStartY;
49930
+ const gap = this.getGap(page) || 0;
49931
+ for (const child of otherChildren) {
49932
+ const childBox = this.layoutNode(child, padding, childY, mainConstraints);
49933
+ mainContent.push(this.renderBox(childBox));
49934
+ childY += childBox.height + gap;
49935
+ }
49936
+ mainContent.push(`</g>`);
49937
+ elements.push(mainContent.join("\n"));
49437
49938
  }
49438
49939
  return elements.join("\n");
49439
49940
  }
49440
- /**
49441
- * Render page title
49442
- */
49443
- renderPageTitle(title) {
49444
- const fontSize = 24;
49445
- const y = this.currentY + fontSize;
49446
- this.currentY += fontSize + 16;
49447
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(title)}</text>`;
49941
+ shouldCenterHorizontally(node) {
49942
+ return node.type === "Card" || node.type === "Modal";
49448
49943
  }
49449
- /**
49450
- * Render any AST node
49451
- */
49452
- renderNode(node) {
49944
+ // ===========================================
49945
+ // Measurement Phase
49946
+ // ===========================================
49947
+ measureNode(node, constraints) {
49453
49948
  switch (node.type) {
49454
- // Layout nodes
49455
49949
  case "Row":
49456
- return this.renderRow(node);
49950
+ return this.measureRow(node, constraints);
49457
49951
  case "Col":
49458
- return this.renderCol(node);
49952
+ return this.measureCol(node, constraints);
49459
49953
  case "Header":
49460
- return this.renderHeader(node);
49461
- case "Main":
49462
- return this.renderMain(node);
49954
+ return this.measureHeader(node, constraints);
49463
49955
  case "Footer":
49464
- return this.renderFooter(node);
49956
+ return this.measureFooter(node, constraints);
49957
+ case "Main":
49958
+ return this.measureMain(node, constraints);
49465
49959
  case "Sidebar":
49466
- return this.renderSidebar(node);
49467
- // Container nodes
49960
+ return this.measureSidebar(node, constraints);
49468
49961
  case "Card":
49469
- return this.renderCard(node);
49962
+ return this.measureCard(node, constraints);
49470
49963
  case "Modal":
49471
- return this.renderModal(node);
49472
- // Text nodes
49473
- case "Text":
49474
- return this.renderText(node);
49964
+ return this.measureModal(node, constraints);
49475
49965
  case "Title":
49476
- return this.renderTitle(node);
49477
- case "Link":
49478
- return this.renderLink(node);
49479
- // Input nodes
49966
+ return this.measureTitle(node);
49967
+ case "Text":
49968
+ return this.measureText(node);
49969
+ case "Button":
49970
+ return this.measureButton(node, constraints);
49480
49971
  case "Input":
49481
- return this.renderInput(node);
49972
+ return this.measureInput(node, constraints);
49482
49973
  case "Textarea":
49483
- return this.renderTextarea(node);
49974
+ return this.measureTextarea(node, constraints);
49484
49975
  case "Select":
49485
- return this.renderSelect(node);
49976
+ return this.measureSelect(node, constraints);
49486
49977
  case "Checkbox":
49487
- return this.renderCheckbox(node);
49978
+ return this.measureCheckbox(node);
49488
49979
  case "Radio":
49489
- return this.renderRadio(node);
49980
+ return this.measureRadio(node);
49490
49981
  case "Switch":
49491
- return this.renderSwitch(node);
49492
- // Button
49493
- case "Button":
49494
- return this.renderButton(node);
49495
- // Display nodes
49982
+ return this.measureSwitch(node);
49983
+ case "Link":
49984
+ return this.measureLink(node);
49496
49985
  case "Image":
49497
- return this.renderImage(node);
49986
+ return this.measureImage(node, constraints);
49498
49987
  case "Placeholder":
49499
- return this.renderPlaceholder(node);
49988
+ return this.measurePlaceholder(node, constraints);
49500
49989
  case "Avatar":
49501
- return this.renderAvatar(node);
49990
+ return this.measureAvatar(node, constraints);
49502
49991
  case "Badge":
49503
- return this.renderBadge(node);
49504
- // Data nodes
49992
+ return this.measureBadge(node);
49505
49993
  case "Table":
49506
- return this.renderTable(node);
49994
+ return this.measureTable(node);
49507
49995
  case "List":
49508
- return this.renderList(node);
49509
- // Feedback nodes
49996
+ return this.measureList(node);
49510
49997
  case "Alert":
49511
- return this.renderAlert(node);
49998
+ return this.measureAlert(node, constraints);
49512
49999
  case "Progress":
49513
- return this.renderProgress(node);
50000
+ return this.measureProgress(node, constraints);
49514
50001
  case "Spinner":
49515
- return this.renderSpinner(node);
49516
- // Navigation nodes
50002
+ return this.measureSpinner(node);
49517
50003
  case "Nav":
49518
- return this.renderNav(node);
50004
+ return this.measureNav(node);
49519
50005
  case "Tabs":
49520
- return this.renderTabs(node);
50006
+ return this.measureTabs(node);
49521
50007
  case "Breadcrumb":
49522
- return this.renderBreadcrumb(node);
50008
+ return this.measureBreadcrumb(node);
50009
+ case "Icon":
50010
+ return this.measureIcon(node);
50011
+ case "Divider":
50012
+ return this.measureDivider(node, constraints);
49523
50013
  default:
49524
- return `<!-- Unsupported: ${node.type} -->`;
50014
+ return { width: 100, height: 40 };
50015
+ }
50016
+ }
50017
+ measureRow(node, constraints) {
50018
+ const padding = this.getPadding(node);
50019
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50020
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50021
+ const childMeasurements = node.children.map(
50022
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50023
+ );
50024
+ const maxChildHeight = Math.max(...childMeasurements.map((m) => m.height), 0);
50025
+ const totalChildWidth = childMeasurements.reduce((sum, m) => sum + m.width, 0);
50026
+ const totalGapWidth = Math.max(0, (node.children.length - 1) * gap);
50027
+ const contentWidth = totalChildWidth + totalGapWidth + padding.left + padding.right;
50028
+ const hasExplicitWidth = "w" in node && node.w !== void 0;
50029
+ const width = hasExplicitWidth ? constraints.maxWidth : Math.min(contentWidth, constraints.maxWidth);
50030
+ return {
50031
+ width,
50032
+ height: maxChildHeight + padding.top + padding.bottom
50033
+ };
50034
+ }
50035
+ measureCol(node, constraints) {
50036
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50037
+ const padding = this.getPadding(node);
50038
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50039
+ const childMeasurements = node.children.map(
50040
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50041
+ );
50042
+ const totalHeight = childMeasurements.reduce(
50043
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50044
+ 0
50045
+ );
50046
+ const maxChildWidth = Math.max(...childMeasurements.map((m) => m.width), 0);
50047
+ return {
50048
+ width: Math.max(maxChildWidth + padding.left + padding.right, constraints.maxWidth),
50049
+ height: totalHeight + padding.top + padding.bottom
50050
+ };
50051
+ }
50052
+ measureHeader(node, constraints) {
50053
+ const padding = this.getPadding(node);
50054
+ const height = this.resolveSize(node.h) || 56;
50055
+ return {
50056
+ width: constraints.maxWidth,
50057
+ height: height + padding.top + padding.bottom
50058
+ };
50059
+ }
50060
+ measureFooter(node, constraints) {
50061
+ const padding = this.getPadding(node);
50062
+ const height = this.resolveSize(node.h) || 60;
50063
+ return {
50064
+ width: constraints.maxWidth,
50065
+ height: height + padding.top + padding.bottom
50066
+ };
50067
+ }
50068
+ measureMain(node, constraints) {
50069
+ const padding = this.getPadding(node);
50070
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50071
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50072
+ const childMeasurements = node.children.map(
50073
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50074
+ );
50075
+ const totalHeight = childMeasurements.reduce(
50076
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50077
+ 0
50078
+ );
50079
+ return {
50080
+ width: constraints.maxWidth,
50081
+ height: totalHeight + padding.top + padding.bottom
50082
+ };
50083
+ }
50084
+ measureSidebar(node, constraints) {
50085
+ const width = this.resolveSize(node.w) || 200;
50086
+ const padding = this.getPadding(node);
50087
+ const gap = this.getGap(node) ?? 8;
50088
+ const innerWidth = width - padding.left - padding.right;
50089
+ const childMeasurements = node.children.map(
50090
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50091
+ );
50092
+ const contentHeight = childMeasurements.reduce(
50093
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50094
+ 0
50095
+ ) + padding.top + padding.bottom;
50096
+ const explicitHeight = this.resolveSize(node.h);
50097
+ const height = explicitHeight || constraints.maxHeight || contentHeight;
50098
+ return { width, height };
50099
+ }
50100
+ measureCard(node, constraints) {
50101
+ let cardWidth = this.resolveSize(node.w);
50102
+ if (!cardWidth) {
50103
+ cardWidth = Math.min(360, constraints.maxWidth);
50104
+ } else {
50105
+ cardWidth = Math.min(cardWidth, constraints.maxWidth);
50106
+ }
50107
+ const explicitHeight = this.resolveSize(node.h);
50108
+ if (explicitHeight) {
50109
+ return { width: cardWidth, height: explicitHeight };
50110
+ }
50111
+ const padding = this.getPadding(node);
50112
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50113
+ const innerWidth = cardWidth - padding.left - padding.right;
50114
+ const childMeasurements = node.children.map(
50115
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50116
+ );
50117
+ let contentHeight = 0;
50118
+ if (node.title) {
50119
+ contentHeight += 28;
50120
+ }
50121
+ contentHeight += childMeasurements.reduce(
50122
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50123
+ 0
50124
+ );
50125
+ return {
50126
+ width: cardWidth,
50127
+ height: contentHeight + padding.top + padding.bottom
50128
+ };
50129
+ }
50130
+ measureModal(node, constraints) {
50131
+ const width = this.resolveSize(node.w) || 400;
50132
+ const padding = this.getPadding(node);
50133
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50134
+ const innerWidth = width - padding.left - padding.right;
50135
+ const childMeasurements = node.children.map(
50136
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50137
+ );
50138
+ let contentHeight = node.title ? 40 : 0;
50139
+ contentHeight += childMeasurements.reduce(
50140
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50141
+ 0
50142
+ );
50143
+ return {
50144
+ width,
50145
+ height: contentHeight + padding.top + padding.bottom
50146
+ };
50147
+ }
50148
+ measureTitle(node) {
50149
+ const level = node.level || 1;
50150
+ const fontSize = this.getTitleFontSize(level);
50151
+ const textWidth = this.estimateTextWidth(node.content, fontSize);
50152
+ return { width: textWidth, height: fontSize + 8 };
50153
+ }
50154
+ measureText(node) {
50155
+ const fontSize = this.resolveFontSize(node.size);
50156
+ const textWidth = this.estimateTextWidth(node.content, fontSize);
50157
+ return { width: textWidth, height: fontSize + 8 };
50158
+ }
50159
+ measureButton(node, constraints) {
50160
+ const hasIcon = !!node.icon;
50161
+ const isIconOnly = hasIcon && !node.content.trim();
50162
+ const iconSize = 16;
50163
+ const padding = 16;
50164
+ let width;
50165
+ if (this.isFullWidth(node)) {
50166
+ width = constraints.maxWidth;
50167
+ } else if (isIconOnly) {
50168
+ width = iconSize + padding * 2;
50169
+ } else if (hasIcon) {
50170
+ width = Math.max(80, this.estimateTextWidth(node.content, 14) + iconSize + 48);
50171
+ } else {
50172
+ width = Math.max(80, this.estimateTextWidth(node.content, 14) + 32);
50173
+ }
50174
+ return { width, height: 40 };
50175
+ }
50176
+ measureInput(node, constraints) {
50177
+ let width;
50178
+ if (this.resolveSize(node.w)) {
50179
+ width = this.resolveSize(node.w);
50180
+ } else if (constraints.inHeader) {
50181
+ const placeholderWidth = node.placeholder ? this.estimateTextWidth(node.placeholder, 14) : 60;
50182
+ width = Math.max(120, placeholderWidth + 24 + 100);
50183
+ } else {
50184
+ width = constraints.maxWidth;
50185
+ }
50186
+ let height = 36;
50187
+ if (node.label) height += 24;
50188
+ return { width, height };
50189
+ }
50190
+ measureTextarea(node, constraints) {
50191
+ const width = this.resolveSize(node.w) || constraints.maxWidth;
50192
+ let height = node.rows ? node.rows * 24 : 100;
50193
+ if (node.label) height += 24;
50194
+ return { width, height };
50195
+ }
50196
+ measureSelect(node, constraints) {
50197
+ const width = this.resolveSize(node.w) || constraints.maxWidth;
50198
+ let height = 40;
50199
+ if (node.label) height += 24;
50200
+ return { width, height };
50201
+ }
50202
+ measureCheckbox(node) {
50203
+ const labelWidth = node.label ? this.estimateTextWidth(node.label, 14) + 8 : 0;
50204
+ return { width: 18 + labelWidth, height: 24 };
50205
+ }
50206
+ measureRadio(node) {
50207
+ const labelWidth = node.label ? this.estimateTextWidth(node.label, 14) + 8 : 0;
50208
+ return { width: 18 + labelWidth, height: 24 };
50209
+ }
50210
+ measureSwitch(node) {
50211
+ const labelWidth = node.label ? this.estimateTextWidth(node.label, 14) + 8 : 0;
50212
+ return { width: 44 + labelWidth, height: 28 };
50213
+ }
50214
+ measureLink(node) {
50215
+ const fontSize = 14;
50216
+ const textWidth = this.estimateTextWidth(node.content, fontSize);
50217
+ return { width: textWidth, height: fontSize + 8 };
50218
+ }
50219
+ measureImage(node, constraints) {
50220
+ let width;
50221
+ if (this.isFullWidth(node)) {
50222
+ width = constraints.maxWidth;
50223
+ } else {
50224
+ width = this.resolveSize(node.w) || 200;
50225
+ }
50226
+ const height = this.resolveSize(node.h) || 150;
50227
+ return { width, height };
50228
+ }
50229
+ measurePlaceholder(node, constraints) {
50230
+ let width;
50231
+ if (node.w !== void 0) {
50232
+ width = this.isFullWidth(node) ? constraints.maxWidth : this.resolveSize(node.w) || 200;
50233
+ } else {
50234
+ width = constraints.maxWidth || 200;
50235
+ }
50236
+ const height = this.resolveSize(node.h) || 100;
50237
+ return { width, height };
50238
+ }
50239
+ measureAvatar(node, constraints) {
50240
+ const sizes = { xs: 24, sm: 32, md: 40, lg: 48, xl: 64 };
50241
+ const defaultSize = constraints?.inHeader ? "sm" : "md";
50242
+ const size = sizes[node.size || defaultSize] || 40;
50243
+ return { width: size, height: size };
50244
+ }
50245
+ measureBadge(node) {
50246
+ const width = Math.max(24, this.estimateTextWidth(node.content, 12) + 16);
50247
+ return { width, height: 22 };
50248
+ }
50249
+ measureTable(node) {
50250
+ const columns = node.columns || [];
50251
+ const rows = node.rows || [];
50252
+ const rowCount = rows.length || 3;
50253
+ const colWidth = 120;
50254
+ const rowHeight = 40;
50255
+ return {
50256
+ width: columns.length * colWidth,
50257
+ height: (rowCount + 1) * rowHeight
50258
+ };
50259
+ }
50260
+ measureList(node) {
50261
+ const items = node.items || [];
50262
+ const maxItemWidth = Math.max(...items.map((item) => {
50263
+ const content = typeof item === "string" ? item : item.content;
50264
+ return this.estimateTextWidth(content, 14);
50265
+ }), 100);
50266
+ return { width: maxItemWidth + 24, height: items.length * 28 };
50267
+ }
50268
+ measureAlert(_node, constraints) {
50269
+ const width = Math.min(400, constraints.maxWidth);
50270
+ return { width, height: 48 };
50271
+ }
50272
+ measureProgress(node, constraints) {
50273
+ const width = Math.min(200, constraints.maxWidth);
50274
+ let height = 8;
50275
+ if (node.label) height += 24;
50276
+ return { width, height };
50277
+ }
50278
+ measureSpinner(node) {
50279
+ const sizes = { xs: 16, sm: 20, md: 24, lg: 32, xl: 40 };
50280
+ const size = sizes[node.size || "md"] || 24;
50281
+ return { width: size, height: size };
50282
+ }
50283
+ measureNav(node) {
50284
+ const items = node.items || [];
50285
+ if (node.vertical) {
50286
+ const maxWidth = Math.max(...items.map((item) => {
50287
+ const label = typeof item === "string" ? item : item.label;
50288
+ return this.estimateTextWidth(label, 14);
50289
+ }), 100);
50290
+ return { width: maxWidth, height: items.length * 32 };
50291
+ } else {
50292
+ const totalWidth = items.reduce((sum, item) => {
50293
+ const label = typeof item === "string" ? item : item.label;
50294
+ return sum + this.estimateTextWidth(label, 14) + 24;
50295
+ }, 0);
50296
+ return { width: totalWidth, height: 32 };
49525
50297
  }
49526
50298
  }
50299
+ measureTabs(node) {
50300
+ const items = node.items || [];
50301
+ const totalWidth = items.reduce((sum, item) => {
50302
+ const label = typeof item === "string" ? item : item;
50303
+ return sum + this.estimateTextWidth(label, 14) + 32;
50304
+ }, 0);
50305
+ return { width: totalWidth, height: 44 };
50306
+ }
50307
+ measureBreadcrumb(node) {
50308
+ const items = node.items || [];
50309
+ const totalWidth = items.reduce((sum, item, idx) => {
50310
+ const label = typeof item === "string" ? item : item.label;
50311
+ return sum + this.estimateTextWidth(label, 14) + (idx < items.length - 1 ? 24 : 0);
50312
+ }, 0);
50313
+ return { width: totalWidth, height: 28 };
50314
+ }
50315
+ measureIcon(node) {
50316
+ const sizes = { xs: 12, sm: 16, md: 20, lg: 24, xl: 32 };
50317
+ const size = sizes[node.size || "md"] || 20;
50318
+ return { width: size, height: size };
50319
+ }
50320
+ measureDivider(_node, constraints) {
50321
+ return { width: constraints.maxWidth, height: 1 };
50322
+ }
49527
50323
  // ===========================================
49528
- // Layout Renderers
50324
+ // Layout Phase
49529
50325
  // ===========================================
49530
- renderRow(node) {
49531
- const savedX = this.currentX;
49532
- const savedY = this.currentY;
49533
- const elements = [];
49534
- const totalSpan = node.children.reduce((sum, child) => {
49535
- if ("span" in child && typeof child.span === "number") {
49536
- return sum + child.span;
50326
+ layoutNode(node, x, y, constraints) {
50327
+ switch (node.type) {
50328
+ case "Row":
50329
+ return this.layoutRow(node, x, y, constraints);
50330
+ case "Col":
50331
+ return this.layoutCol(node, x, y, constraints);
50332
+ case "Header":
50333
+ return this.layoutHeader(node, x, y, constraints);
50334
+ case "Footer":
50335
+ return this.layoutFooter(node, x, y, constraints);
50336
+ case "Main":
50337
+ return this.layoutMain(node, x, y, constraints);
50338
+ case "Sidebar":
50339
+ return this.layoutSidebar(node, x, y, constraints);
50340
+ case "Card":
50341
+ return this.layoutCard(node, x, y, constraints);
50342
+ default:
50343
+ const size = this.measureNode(node, constraints);
50344
+ return {
50345
+ x,
50346
+ y,
50347
+ width: size.width,
50348
+ height: size.height,
50349
+ node,
50350
+ children: []
50351
+ };
50352
+ }
50353
+ }
50354
+ layoutRow(node, x, y, constraints) {
50355
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50356
+ const padding = this.getPadding(node);
50357
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50358
+ const justify = node.justify || "start";
50359
+ const align = node.align || (constraints.inHeader ? "center" : "start");
50360
+ const childMeasurements = node.children.map(
50361
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50362
+ );
50363
+ const flexItems = node.children.map((child, i) => {
50364
+ const measurement = childMeasurements[i];
50365
+ if ("w" in child && child.w !== void 0 && child.w !== "full") {
50366
+ const resolved = this.resolveSize(child.w);
50367
+ if (resolved !== void 0) {
50368
+ return createFlexItemProps(measurement.height, {
50369
+ basis: resolved,
50370
+ grow: 0,
50371
+ shrink: 0,
50372
+ minSize: resolved,
50373
+ maxSize: resolved,
50374
+ contentSize: measurement.width
50375
+ });
50376
+ }
49537
50377
  }
49538
- return sum + 1;
49539
- }, 0);
49540
- const colWidth = this.contentWidth / Math.max(totalSpan, 1);
49541
- let maxHeight = 0;
50378
+ if (this.isFlexContainer(child)) {
50379
+ return createFlexGrowItemProps(measurement.height, {
50380
+ basis: 0,
50381
+ grow: 1,
50382
+ shrink: 1,
50383
+ minSize: 0,
50384
+ maxSize: Infinity,
50385
+ contentSize: measurement.width
50386
+ });
50387
+ }
50388
+ if (constraints.inHeader && child.type === "Input") {
50389
+ return createFlexGrowItemProps(measurement.height, {
50390
+ basis: measurement.width,
50391
+ // auto = content size
50392
+ grow: 1,
50393
+ shrink: 1,
50394
+ minSize: 120,
50395
+ // min-width: 120px
50396
+ maxSize: Infinity,
50397
+ contentSize: measurement.width
50398
+ });
50399
+ }
50400
+ return createFlexItemProps(measurement.height, {
50401
+ basis: measurement.width,
50402
+ // auto = content size
50403
+ grow: 0,
50404
+ shrink: 1,
50405
+ minSize: 0,
50406
+ maxSize: Infinity,
50407
+ contentSize: measurement.width
50408
+ });
50409
+ });
50410
+ const flexConfig = {
50411
+ mainSize: innerWidth,
50412
+ crossSize: void 0,
50413
+ // Will use max child height
50414
+ direction: "row",
50415
+ justifyContent: toJustifyContent(justify),
50416
+ alignItems: toAlignItems(align),
50417
+ gap
50418
+ };
50419
+ const flexResult = computeFlexLayout(flexItems, flexConfig);
50420
+ const maxChildHeight = Math.max(...childMeasurements.map((m) => m.height), 0);
50421
+ const innerMaxHeight = constraints.maxHeight ? constraints.maxHeight - padding.top - padding.bottom : void 0;
50422
+ const effectiveHeight = innerMaxHeight || maxChildHeight;
50423
+ const rowHeight = effectiveHeight + padding.top + padding.bottom;
50424
+ const children = [];
50425
+ for (let i = 0; i < node.children.length; i++) {
50426
+ const child = node.children[i];
50427
+ const flexItem = flexResult.items[i];
50428
+ const measurement = childMeasurements[i];
50429
+ const childX = x + padding.left + flexItem.mainPosition;
50430
+ let childY = y + padding.top;
50431
+ switch (align) {
50432
+ case "center":
50433
+ childY = y + padding.top + (maxChildHeight - measurement.height) / 2;
50434
+ break;
50435
+ case "end":
50436
+ childY = y + padding.top + (maxChildHeight - measurement.height);
50437
+ break;
50438
+ }
50439
+ const childBox = this.layoutNode(child, childX, childY, {
50440
+ ...constraints,
50441
+ maxWidth: flexItem.mainSize,
50442
+ maxHeight: effectiveHeight
50443
+ });
50444
+ children.push(childBox);
50445
+ }
50446
+ return {
50447
+ x,
50448
+ y,
50449
+ width: constraints.maxWidth,
50450
+ height: rowHeight,
50451
+ node,
50452
+ children,
50453
+ padding
50454
+ };
50455
+ }
50456
+ layoutCol(node, x, y, constraints) {
50457
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50458
+ const padding = this.getPadding(node);
50459
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50460
+ const innerHeight = constraints.maxHeight ? constraints.maxHeight - padding.top - padding.bottom : void 0;
50461
+ const align = node.align || "stretch";
50462
+ const childMeasurements = node.children.map(
50463
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50464
+ );
50465
+ const flexItems = node.children.map((child, i) => {
50466
+ const measurement = childMeasurements[i];
50467
+ if ("h" in child && child.h !== void 0) {
50468
+ const resolved = this.resolveSize(child.h);
50469
+ if (resolved !== void 0) {
50470
+ return createFlexItemProps(measurement.width, {
50471
+ basis: resolved,
50472
+ grow: 0,
50473
+ shrink: 0,
50474
+ minSize: resolved,
50475
+ maxSize: resolved,
50476
+ contentSize: measurement.height
50477
+ });
50478
+ }
50479
+ }
50480
+ if (child.type === "Row" && this.rowContainsSidebarOrMain(child)) {
50481
+ return createFlexGrowItemProps(measurement.width, {
50482
+ basis: 0,
50483
+ grow: 1,
50484
+ shrink: 1,
50485
+ minSize: 0,
50486
+ maxSize: Infinity,
50487
+ contentSize: measurement.height
50488
+ });
50489
+ }
50490
+ return createFlexItemProps(measurement.width, {
50491
+ basis: measurement.height,
50492
+ // auto = content size
50493
+ grow: 0,
50494
+ shrink: 1,
50495
+ minSize: 0,
50496
+ maxSize: Infinity,
50497
+ contentSize: measurement.height
50498
+ });
50499
+ });
50500
+ const flexConfig = {
50501
+ mainSize: innerHeight ?? childMeasurements.reduce(
50502
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50503
+ 0
50504
+ ),
50505
+ crossSize: innerWidth,
50506
+ direction: "column",
50507
+ justifyContent: "flex-start",
50508
+ alignItems: toAlignItems(align),
50509
+ gap
50510
+ };
50511
+ const flexResult = computeFlexLayout(flexItems, flexConfig);
50512
+ const children = [];
50513
+ for (let i = 0; i < node.children.length; i++) {
50514
+ const child = node.children[i];
50515
+ const flexItem = flexResult.items[i];
50516
+ const measurement = childMeasurements[i];
50517
+ let childX = x + padding.left;
50518
+ let childWidth = align === "stretch" ? innerWidth : measurement.width;
50519
+ switch (align) {
50520
+ case "center":
50521
+ childX = x + padding.left + (innerWidth - measurement.width) / 2;
50522
+ break;
50523
+ case "end":
50524
+ childX = x + padding.left + (innerWidth - measurement.width);
50525
+ break;
50526
+ }
50527
+ const childY = y + padding.top + flexItem.mainPosition;
50528
+ const childBox = this.layoutNode(child, childX, childY, {
50529
+ ...constraints,
50530
+ maxWidth: childWidth,
50531
+ maxHeight: flexItem.mainSize
50532
+ });
50533
+ children.push(childBox);
50534
+ }
50535
+ const totalHeight = flexResult.mainSizeUsed;
50536
+ return {
50537
+ x,
50538
+ y,
50539
+ width: constraints.maxWidth,
50540
+ height: totalHeight + padding.top + padding.bottom,
50541
+ node,
50542
+ children,
50543
+ padding
50544
+ };
50545
+ }
50546
+ rowContainsSidebarOrMain(row) {
50547
+ return row.children.some(
50548
+ (child) => child.type === "Sidebar" || child.type === "Main"
50549
+ );
50550
+ }
50551
+ layoutHeader(node, x, y, constraints) {
50552
+ const padding = this.getPadding(node);
50553
+ const height = this.resolveSize(node.h) || 56;
50554
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50555
+ const children = [];
50556
+ let currentY = y + padding.top;
49542
50557
  for (const child of node.children) {
49543
- const span = "span" in child && typeof child.span === "number" ? child.span : 1;
49544
- const childWidth = colWidth * span;
49545
- const startY = this.currentY;
49546
- elements.push(this.renderNode(child));
49547
- const childHeight = this.currentY - startY;
49548
- maxHeight = Math.max(maxHeight, childHeight);
49549
- this.currentX += childWidth;
49550
- this.currentY = savedY;
50558
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50559
+ ...constraints,
50560
+ maxWidth: innerWidth,
50561
+ maxHeight: void 0,
50562
+ // Children use natural height
50563
+ inHeader: true
50564
+ // Pass header context for child sizing (e.g., smaller avatars)
50565
+ });
50566
+ const verticalOffset = (height - childBox.height) / 2;
50567
+ childBox.y = y + padding.top + verticalOffset;
50568
+ this.adjustChildrenY(childBox, verticalOffset);
50569
+ children.push(childBox);
50570
+ currentY += childBox.height;
49551
50571
  }
49552
- this.currentX = savedX;
49553
- this.currentY = savedY + maxHeight;
49554
- return elements.join("\n");
50572
+ return {
50573
+ x,
50574
+ y,
50575
+ width: constraints.maxWidth,
50576
+ height: height + padding.top + padding.bottom,
50577
+ node,
50578
+ children,
50579
+ padding
50580
+ };
49555
50581
  }
49556
- renderCol(node) {
49557
- return node.children.map((child) => this.renderNode(child)).join("\n");
50582
+ adjustChildrenY(box, offset) {
50583
+ for (const child of box.children) {
50584
+ child.y += offset;
50585
+ this.adjustChildrenY(child, offset);
50586
+ }
49558
50587
  }
49559
- renderHeader(node) {
49560
- const height = 60;
49561
- const x = this.currentX;
49562
- const y = this.currentY;
49563
- const savedY = this.currentY;
49564
- this.currentY += 16;
49565
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49566
- this.currentY = savedY + height + 8;
49567
- return `
49568
- <g transform="translate(${x}, ${y})">
49569
- <rect width="${this.contentWidth}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49570
- ${children}
49571
- </g>`;
50588
+ /**
50589
+ * Check if a node should flex to fill available space in a Row
50590
+ * Only layout containers without explicit width should flex
50591
+ */
50592
+ isFlexContainer(node) {
50593
+ if (node.type === "Main") return true;
50594
+ if (node.type === "Col" && !("w" in node && node.w !== void 0)) return true;
50595
+ return false;
49572
50596
  }
49573
- renderMain(node) {
49574
- return node.children.map((c) => this.renderNode(c)).join("\n");
50597
+ layoutFooter(node, x, y, constraints) {
50598
+ const padding = this.getPadding(node);
50599
+ const height = this.resolveSize(node.h) || 60;
50600
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50601
+ const children = [];
50602
+ let currentY = y + padding.top;
50603
+ for (const child of node.children) {
50604
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50605
+ ...constraints,
50606
+ maxWidth: innerWidth,
50607
+ maxHeight: void 0
50608
+ // Children use natural height
50609
+ });
50610
+ const verticalOffset = (height - childBox.height) / 2;
50611
+ childBox.y = y + padding.top + verticalOffset;
50612
+ this.adjustChildrenY(childBox, verticalOffset);
50613
+ children.push(childBox);
50614
+ currentY += childBox.height;
50615
+ }
50616
+ return {
50617
+ x,
50618
+ y,
50619
+ width: constraints.maxWidth,
50620
+ height: height + padding.top + padding.bottom,
50621
+ node,
50622
+ children,
50623
+ padding
50624
+ };
49575
50625
  }
49576
- renderFooter(node) {
49577
- const height = 60;
49578
- const x = this.currentX;
49579
- const y = this.currentY;
49580
- const savedY = this.currentY;
49581
- this.currentY += 16;
49582
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49583
- this.currentY = savedY + height + 8;
49584
- return `
49585
- <g transform="translate(${x}, ${y})">
49586
- <rect width="${this.contentWidth}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49587
- ${children}
49588
- </g>`;
50626
+ layoutMain(node, x, y, constraints) {
50627
+ const padding = this.getPadding(node);
50628
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50629
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50630
+ const childMeasurements = node.children.map(
50631
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50632
+ );
50633
+ const children = [];
50634
+ let currentY = y + padding.top;
50635
+ for (let i = 0; i < node.children.length; i++) {
50636
+ const child = node.children[i];
50637
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50638
+ ...constraints,
50639
+ maxWidth: innerWidth
50640
+ });
50641
+ children.push(childBox);
50642
+ currentY += childBox.height + gap;
50643
+ }
50644
+ const totalHeight = childMeasurements.reduce(
50645
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50646
+ 0
50647
+ );
50648
+ return {
50649
+ x,
50650
+ y,
50651
+ width: constraints.maxWidth,
50652
+ height: totalHeight + padding.top + padding.bottom,
50653
+ node,
50654
+ children,
50655
+ padding
50656
+ };
49589
50657
  }
49590
- renderSidebar(node) {
49591
- const width = 200;
49592
- const height = 300;
49593
- const x = this.currentX;
49594
- const y = this.currentY;
49595
- const savedY = this.currentY;
49596
- this.currentY += 16;
49597
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49598
- this.currentY = savedY + height + 8;
49599
- return `
49600
- <g transform="translate(${x}, ${y})">
49601
- <rect width="${width}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49602
- ${children}
49603
- </g>`;
50658
+ layoutSidebar(node, x, y, constraints) {
50659
+ const width = this.resolveSize(node.w) || 200;
50660
+ const padding = this.getPadding(node);
50661
+ const gap = this.getGap(node) ?? 8;
50662
+ const innerWidth = width - padding.left - padding.right;
50663
+ const children = [];
50664
+ let currentY = y + padding.top;
50665
+ for (const child of node.children) {
50666
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50667
+ ...constraints,
50668
+ maxWidth: innerWidth
50669
+ });
50670
+ children.push(childBox);
50671
+ currentY += childBox.height + gap;
50672
+ }
50673
+ const contentHeight = currentY - y - padding.top - gap + padding.bottom;
50674
+ const height = constraints.maxHeight || Math.max(contentHeight, 200);
50675
+ return {
50676
+ x,
50677
+ y,
50678
+ width,
50679
+ height,
50680
+ node,
50681
+ children,
50682
+ padding
50683
+ };
50684
+ }
50685
+ layoutCard(node, x, y, constraints) {
50686
+ let cardWidth = this.resolveSize(node.w);
50687
+ if (!cardWidth) {
50688
+ cardWidth = Math.min(360, constraints.maxWidth);
50689
+ }
50690
+ const padding = this.getPadding(node);
50691
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50692
+ const innerWidth = cardWidth - padding.left - padding.right;
50693
+ const children = [];
50694
+ let currentY = y + padding.top;
50695
+ if (node.title) {
50696
+ currentY += 28;
50697
+ }
50698
+ for (const child of node.children) {
50699
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50700
+ ...constraints,
50701
+ maxWidth: innerWidth
50702
+ });
50703
+ children.push(childBox);
50704
+ currentY += childBox.height + gap;
50705
+ }
50706
+ const totalHeight = currentY - y - gap + padding.bottom;
50707
+ return {
50708
+ x,
50709
+ y,
50710
+ width: cardWidth,
50711
+ height: totalHeight,
50712
+ node,
50713
+ children,
50714
+ padding
50715
+ };
49604
50716
  }
49605
50717
  // ===========================================
49606
- // Container Renderers
50718
+ // Render Phase
49607
50719
  // ===========================================
49608
- renderCard(node) {
49609
- const width = Math.min(300, this.contentWidth);
49610
- const x = this.currentX;
49611
- const y = this.currentY;
49612
- const savedY = this.currentY;
49613
- this.currentY += 16;
49614
- let titleSvg = "";
50720
+ renderBox(box) {
50721
+ if (!box.node) return "";
50722
+ switch (box.node.type) {
50723
+ case "Row":
50724
+ return this.renderRowBox(box);
50725
+ case "Col":
50726
+ return this.renderColBox(box);
50727
+ case "Header":
50728
+ return this.renderHeaderBox(box);
50729
+ case "Footer":
50730
+ return this.renderFooterBox(box);
50731
+ case "Main":
50732
+ return this.renderMainBox(box);
50733
+ case "Sidebar":
50734
+ return this.renderSidebarBox(box);
50735
+ case "Card":
50736
+ return this.renderCardBox(box);
50737
+ case "Modal":
50738
+ return this.renderModalBox(box);
50739
+ case "Title":
50740
+ return this.renderTitleBox(box);
50741
+ case "Text":
50742
+ return this.renderTextBox(box);
50743
+ case "Button":
50744
+ return this.renderButtonBox(box);
50745
+ case "Input":
50746
+ return this.renderInputBox(box);
50747
+ case "Textarea":
50748
+ return this.renderTextareaBox(box);
50749
+ case "Select":
50750
+ return this.renderSelectBox(box);
50751
+ case "Checkbox":
50752
+ return this.renderCheckboxBox(box);
50753
+ case "Radio":
50754
+ return this.renderRadioBox(box);
50755
+ case "Switch":
50756
+ return this.renderSwitchBox(box);
50757
+ case "Link":
50758
+ return this.renderLinkBox(box);
50759
+ case "Image":
50760
+ return this.renderImageBox(box);
50761
+ case "Placeholder":
50762
+ return this.renderPlaceholderBox(box);
50763
+ case "Avatar":
50764
+ return this.renderAvatarBox(box);
50765
+ case "Badge":
50766
+ return this.renderBadgeBox(box);
50767
+ case "Table":
50768
+ return this.renderTableBox(box);
50769
+ case "List":
50770
+ return this.renderListBox(box);
50771
+ case "Alert":
50772
+ return this.renderAlertBox(box);
50773
+ case "Progress":
50774
+ return this.renderProgressBox(box);
50775
+ case "Spinner":
50776
+ return this.renderSpinnerBox(box);
50777
+ case "Nav":
50778
+ return this.renderNavBox(box);
50779
+ case "Tabs":
50780
+ return this.renderTabsBox(box);
50781
+ case "Breadcrumb":
50782
+ return this.renderBreadcrumbBox(box);
50783
+ case "Icon":
50784
+ return this.renderIconBox(box);
50785
+ case "Divider":
50786
+ return this.renderDividerBox(box);
50787
+ default:
50788
+ return `<!-- Unsupported: ${box.node.type} -->`;
50789
+ }
50790
+ }
50791
+ renderRowBox(box) {
50792
+ const childrenSvg = box.children.map((child) => this.renderBox(child)).join("\n");
50793
+ return childrenSvg;
50794
+ }
50795
+ renderColBox(box) {
50796
+ const childrenSvg = box.children.map((child) => this.renderBox(child)).join("\n");
50797
+ return childrenSvg;
50798
+ }
50799
+ renderHeaderBox(box) {
50800
+ const node = box.node;
50801
+ const hasBorder = node.border !== false;
50802
+ let svg = `<g>
50803
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="${this.theme.colors.background}"/>`;
50804
+ if (hasBorder) {
50805
+ svg += `<line x1="${box.x}" y1="${box.y + box.height}" x2="${box.x + box.width}" y2="${box.y + box.height}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50806
+ }
50807
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50808
+ svg += `</g>`;
50809
+ return svg;
50810
+ }
50811
+ renderFooterBox(box) {
50812
+ const node = box.node;
50813
+ const hasBorder = node.border !== false;
50814
+ let svg = `<g>
50815
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="${this.theme.colors.background}"`;
50816
+ if (hasBorder) {
50817
+ svg += ` stroke="${this.theme.colors.border}" stroke-width="1"`;
50818
+ }
50819
+ svg += `/>`;
50820
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50821
+ svg += `</g>`;
50822
+ return svg;
50823
+ }
50824
+ renderMainBox(box) {
50825
+ const childrenSvg = box.children.map((child) => this.renderBox(child)).join("\n");
50826
+ return childrenSvg;
50827
+ }
50828
+ renderSidebarBox(box) {
50829
+ let svg = `<g>
50830
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50831
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50832
+ svg += `</g>`;
50833
+ return svg;
50834
+ }
50835
+ renderCardBox(box) {
50836
+ const node = box.node;
50837
+ const shadowAttr = node.shadow ? 'filter="url(#shadow)"' : "";
50838
+ let svg = `<g>
50839
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1" ${shadowAttr}/>`;
49615
50840
  if (node.title) {
49616
- const titleFontSize = 16;
49617
- titleSvg = `<text x="16" y="${titleFontSize + 12}" font-size="${titleFontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
49618
- this.currentY += titleFontSize + 8;
50841
+ const padding = this.getPadding(node);
50842
+ svg += `<text x="${box.x + padding.left}" y="${box.y + padding.top + 18}" font-size="16" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
49619
50843
  }
49620
- const childStartY = this.currentY - savedY;
49621
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49622
- const contentHeight = Math.max(this.currentY - savedY, 100);
49623
- this.currentY = savedY + contentHeight + 16;
49624
- return `
49625
- <g transform="translate(${x}, ${y})">
49626
- <rect width="${width}" height="${contentHeight}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49627
- ${titleSvg}
49628
- <g transform="translate(16, ${childStartY})">
49629
- ${children}
49630
- </g>
49631
- </g>`;
50844
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50845
+ svg += `</g>`;
50846
+ return svg;
49632
50847
  }
49633
- renderModal(node) {
49634
- const width = 400;
49635
- const height = 300;
49636
- const x = (this.options.width - width) / 2;
49637
- const y = (this.options.height - height) / 2;
49638
- let titleSvg = "";
50848
+ renderModalBox(box) {
50849
+ const node = box.node;
50850
+ const modalX = (this.pageWidth - box.width) / 2;
50851
+ const modalY = (this.pageHeight - box.height) / 2;
50852
+ let svg = `<g>
50853
+ <rect width="${this.pageWidth}" height="${this.pageHeight}" fill="rgba(0,0,0,0.5)"/>
50854
+ <rect x="${modalX}" y="${modalY}" width="${box.width}" height="${box.height}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49639
50855
  if (node.title) {
49640
- titleSvg = `<text x="20" y="30" font-size="18" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
50856
+ svg += `<text x="${modalX + 20}" y="${modalY + 30}" font-size="18" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
49641
50857
  }
49642
- const savedX = this.currentX;
49643
- const savedY = this.currentY;
49644
- this.currentX = 20;
49645
- this.currentY = 50;
49646
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49647
- this.currentX = savedX;
49648
- this.currentY = savedY;
49649
- return `
49650
- <g>
49651
- <rect width="100%" height="100%" fill="rgba(0,0,0,0.5)" opacity="0.5"/>
49652
- <g transform="translate(${x}, ${y})">
49653
- <rect width="${width}" height="${height}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49654
- ${titleSvg}
49655
- ${children}
49656
- </g>
49657
- </g>`;
50858
+ for (const child of box.children) {
50859
+ const adjustedChild = { ...child, x: modalX + (child.x - box.x), y: modalY + (child.y - box.y) };
50860
+ svg += this.renderBox(adjustedChild);
50861
+ }
50862
+ svg += `</g>`;
50863
+ return svg;
49658
50864
  }
49659
- // ===========================================
49660
- // Text Renderers
49661
- // ===========================================
49662
- renderText(node) {
50865
+ renderTitleBox(box) {
50866
+ const node = box.node;
50867
+ const level = node.level || 1;
50868
+ const fontSize = this.getTitleFontSize(level);
50869
+ const textY = box.y + fontSize;
50870
+ let textX = box.x;
50871
+ let anchor = "start";
50872
+ if (node.align === "center") {
50873
+ textX = box.x + box.width / 2;
50874
+ anchor = "middle";
50875
+ } else if (node.align === "right") {
50876
+ textX = box.x + box.width;
50877
+ anchor = "end";
50878
+ }
50879
+ return `<text x="${textX}" y="${textY}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}" text-anchor="${anchor}">${this.escapeXml(node.content)}</text>`;
50880
+ }
50881
+ renderTextBox(box) {
50882
+ const node = box.node;
49663
50883
  const fontSize = this.resolveFontSize(node.size);
49664
50884
  const fill = node.muted ? this.theme.colors.muted : this.theme.colors.foreground;
49665
50885
  const fontWeight = node.weight || "normal";
49666
- const y = this.currentY + fontSize;
49667
- this.currentY += fontSize + 8;
49668
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${fill}">${this.escapeXml(node.content)}</text>`;
49669
- }
49670
- renderTitle(node) {
49671
- const level = node.level || 1;
49672
- const fontSize = this.getTitleFontSize(level);
49673
- const y = this.currentY + fontSize;
49674
- this.currentY += fontSize + 12;
49675
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>`;
50886
+ const textY = box.y + fontSize;
50887
+ let textX = box.x;
50888
+ let anchor = "start";
50889
+ if (node.align === "center") {
50890
+ textX = box.x + box.width / 2;
50891
+ anchor = "middle";
50892
+ } else if (node.align === "right") {
50893
+ textX = box.x + box.width;
50894
+ anchor = "end";
50895
+ }
50896
+ return `<text x="${textX}" y="${textY}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${fill}" text-anchor="${anchor}">${this.escapeXml(node.content)}</text>`;
49676
50897
  }
49677
- renderLink(node) {
49678
- const fontSize = 14;
49679
- const y = this.currentY + fontSize;
49680
- this.currentY += fontSize + 8;
49681
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" fill="${this.theme.colors.primary}" text-decoration="underline">${this.escapeXml(node.content)}</text>`;
50898
+ renderButtonBox(box) {
50899
+ const node = box.node;
50900
+ const hasIcon = !!node.icon;
50901
+ const isIconOnly = hasIcon && !node.content.trim();
50902
+ let fill = this.theme.colors.primary;
50903
+ let textFill = "#ffffff";
50904
+ let strokeAttr = "";
50905
+ if (node.secondary) {
50906
+ fill = this.theme.colors.secondary;
50907
+ } else if (node.outline) {
50908
+ fill = "white";
50909
+ textFill = this.theme.colors.foreground;
50910
+ strokeAttr = `stroke="${this.theme.colors.border}" stroke-width="1"`;
50911
+ } else if (node.ghost) {
50912
+ fill = "transparent";
50913
+ textFill = this.theme.colors.foreground;
50914
+ }
50915
+ let svg = `<g>
50916
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="4" fill="${fill}" ${strokeAttr}/>`;
50917
+ if (hasIcon) {
50918
+ const iconData = getIconData(node.icon);
50919
+ if (iconData) {
50920
+ const iconSize = 16;
50921
+ const iconX = isIconOnly ? box.x + (box.width - iconSize) / 2 : box.x + 16;
50922
+ const iconY = box.y + (box.height - iconSize) / 2;
50923
+ svg += this.renderIconPaths(iconData, iconX, iconY, iconSize, textFill);
50924
+ }
50925
+ }
50926
+ if (!isIconOnly) {
50927
+ const textX = hasIcon ? box.x + 36 + (box.width - 52) / 2 : box.x + box.width / 2;
50928
+ svg += `<text x="${textX}" y="${box.y + box.height / 2 + 5}" font-size="14" fill="${textFill}" text-anchor="middle">${this.escapeXml(node.content)}</text>`;
50929
+ }
50930
+ svg += `</g>`;
50931
+ return svg;
49682
50932
  }
49683
- // ===========================================
49684
- // Input Renderers
49685
- // ===========================================
49686
- renderInput(node) {
49687
- const width = 280;
49688
- const height = 40;
49689
- const x = this.currentX;
49690
- let y = this.currentY;
49691
- let result = "";
50933
+ renderInputBox(box) {
50934
+ const node = box.node;
50935
+ let y = box.y;
50936
+ let svg = "<g>";
49692
50937
  if (node.label) {
49693
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50938
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49694
50939
  y += 24;
49695
50940
  }
50941
+ const inputHeight = 36;
49696
50942
  const placeholder = node.placeholder || "";
49697
- result += `
49698
- <g transform="translate(${x}, ${y})">
49699
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49700
- <text x="12" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
49701
- </g>`;
49702
- this.currentY = y + height + 12;
49703
- return result;
50943
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${inputHeight}" rx="4" fill="#f4f4f5"/>`;
50944
+ svg += `<text x="${box.x + 12}" y="${y + inputHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>`;
50945
+ svg += "</g>";
50946
+ return svg;
49704
50947
  }
49705
- renderTextarea(node) {
49706
- const width = 280;
49707
- const height = 100;
49708
- const x = this.currentX;
49709
- let y = this.currentY;
49710
- let result = "";
50948
+ renderTextareaBox(box) {
50949
+ const node = box.node;
50950
+ let y = box.y;
50951
+ let svg = "<g>";
49711
50952
  if (node.label) {
49712
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50953
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49713
50954
  y += 24;
49714
50955
  }
50956
+ const textareaHeight = box.height - (node.label ? 24 : 0);
49715
50957
  const placeholder = node.placeholder || "";
49716
- result += `
49717
- <g transform="translate(${x}, ${y})">
49718
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49719
- <text x="12" y="24" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
49720
- </g>`;
49721
- this.currentY = y + height + 12;
49722
- return result;
50958
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${textareaHeight}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50959
+ svg += `<text x="${box.x + 12}" y="${y + 24}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>`;
50960
+ svg += "</g>";
50961
+ return svg;
49723
50962
  }
49724
- renderSelect(node) {
49725
- const width = 280;
49726
- const height = 40;
49727
- const x = this.currentX;
49728
- let y = this.currentY;
49729
- let result = "";
50963
+ renderSelectBox(box) {
50964
+ const node = box.node;
50965
+ let y = box.y;
50966
+ let svg = "<g>";
49730
50967
  if (node.label) {
49731
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50968
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49732
50969
  y += 24;
49733
50970
  }
50971
+ const selectHeight = 40;
49734
50972
  const placeholder = node.placeholder || "Select...";
49735
- result += `
49736
- <g transform="translate(${x}, ${y})">
49737
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49738
- <text x="12" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
49739
- <path d="M${width - 24} ${height / 2 - 3} l6 6 l6 -6" fill="none" stroke="${this.theme.colors.muted}" stroke-width="1.5"/>
49740
- </g>`;
49741
- this.currentY = y + height + 12;
49742
- return result;
50973
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${selectHeight}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50974
+ svg += `<text x="${box.x + 12}" y="${y + selectHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>`;
50975
+ svg += `<path d="M${box.x + box.width - 24} ${y + selectHeight / 2 - 3} l6 6 l6 -6" fill="none" stroke="${this.theme.colors.muted}" stroke-width="1.5"/>`;
50976
+ svg += "</g>";
50977
+ return svg;
49743
50978
  }
49744
- renderCheckbox(node) {
49745
- const x = this.currentX;
49746
- const y = this.currentY;
50979
+ renderCheckboxBox(box) {
50980
+ const node = box.node;
49747
50981
  const size = 18;
49748
- let result = `
49749
- <g transform="translate(${x}, ${y})">
49750
- <rect width="${size}" height="${size}" rx="3" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50982
+ let svg = `<g>
50983
+ <rect x="${box.x}" y="${box.y}" width="${size}" height="${size}" rx="3" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49751
50984
  if (node.checked) {
49752
- result += `<path d="M4 9 L7 12 L14 5" fill="none" stroke="${this.theme.colors.foreground}" stroke-width="2"/>`;
50985
+ svg += `<path d="M${box.x + 4} ${box.y + 9} L${box.x + 7} ${box.y + 12} L${box.x + 14} ${box.y + 5}" fill="none" stroke="${this.theme.colors.foreground}" stroke-width="2"/>`;
49753
50986
  }
49754
50987
  if (node.label) {
49755
- result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50988
+ svg += `<text x="${box.x + size + 8}" y="${box.y + size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49756
50989
  }
49757
- result += "</g>";
49758
- this.currentY += size + 12;
49759
- return result;
50990
+ svg += "</g>";
50991
+ return svg;
49760
50992
  }
49761
- renderRadio(node) {
49762
- const x = this.currentX;
49763
- const y = this.currentY;
50993
+ renderRadioBox(box) {
50994
+ const node = box.node;
49764
50995
  const size = 18;
49765
50996
  const radius = size / 2;
49766
- let result = `
49767
- <g transform="translate(${x}, ${y})">
49768
- <circle cx="${radius}" cy="${radius}" r="${radius - 1}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50997
+ const cx = box.x + radius;
50998
+ const cy = box.y + radius;
50999
+ let svg = `<g>
51000
+ <circle cx="${cx}" cy="${cy}" r="${radius - 1}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49769
51001
  if (node.checked) {
49770
- result += `<circle cx="${radius}" cy="${radius}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
51002
+ svg += `<circle cx="${cx}" cy="${cy}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
49771
51003
  }
49772
51004
  if (node.label) {
49773
- result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
51005
+ svg += `<text x="${box.x + size + 8}" y="${box.y + size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49774
51006
  }
49775
- result += "</g>";
49776
- this.currentY += size + 12;
49777
- return result;
51007
+ svg += "</g>";
51008
+ return svg;
49778
51009
  }
49779
- renderSwitch(node) {
49780
- const x = this.currentX;
49781
- const y = this.currentY;
51010
+ renderSwitchBox(box) {
51011
+ const node = box.node;
49782
51012
  const width = 44;
49783
51013
  const height = 24;
49784
51014
  const radius = height / 2;
49785
51015
  const isOn = node.checked;
49786
51016
  const bgColor = isOn ? this.theme.colors.primary : this.theme.colors.border;
49787
- const knobX = isOn ? width - radius : radius;
49788
- let result = `
49789
- <g transform="translate(${x}, ${y})">
49790
- <rect width="${width}" height="${height}" rx="${radius}" fill="${bgColor}"/>
49791
- <circle cx="${knobX}" cy="${radius}" r="${radius - 3}" fill="white"/>`;
51017
+ const knobX = isOn ? box.x + width - radius : box.x + radius;
51018
+ let svg = `<g>
51019
+ <rect x="${box.x}" y="${box.y}" width="${width}" height="${height}" rx="${radius}" fill="${bgColor}"/>
51020
+ <circle cx="${knobX}" cy="${box.y + radius}" r="${radius - 3}" fill="white"/>`;
49792
51021
  if (node.label) {
49793
- result += `<text x="${width + 8}" y="${height - 6}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49794
- }
49795
- result += "</g>";
49796
- this.currentY += height + 12;
49797
- return result;
49798
- }
49799
- // ===========================================
49800
- // Button Renderer
49801
- // ===========================================
49802
- renderButton(node) {
49803
- const content = node.content;
49804
- const hasIcon = !!node.icon;
49805
- const isIconOnly = hasIcon && !content.trim();
49806
- const iconSize = 16;
49807
- const padding = isIconOnly ? 8 : 16;
49808
- let width;
49809
- if (isIconOnly) {
49810
- width = iconSize + padding * 2;
49811
- } else if (hasIcon) {
49812
- width = Math.max(80, content.length * 10 + iconSize + 40);
49813
- } else {
49814
- width = Math.max(80, content.length * 10 + 32);
49815
- }
49816
- const height = 36;
49817
- const x = this.currentX;
49818
- const y = this.currentY;
49819
- let fill = this.theme.colors.primary;
49820
- let textFill = "#ffffff";
49821
- let isOutline = false;
49822
- if (node.secondary) {
49823
- fill = this.theme.colors.secondary;
49824
- } else if (node.outline) {
49825
- fill = "white";
49826
- textFill = this.theme.colors.foreground;
49827
- isOutline = true;
49828
- } else if (node.ghost) {
49829
- fill = "transparent";
49830
- textFill = this.theme.colors.foreground;
51022
+ svg += `<text x="${box.x + width + 8}" y="${box.y + height - 6}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49831
51023
  }
49832
- this.currentY += height + 8;
49833
- const strokeAttr = isOutline ? `stroke="${this.theme.colors.border}" stroke-width="1"` : "";
49834
- let iconSvg = "";
49835
- if (hasIcon) {
49836
- const iconData = getIconData(node.icon);
49837
- if (iconData) {
49838
- const iconX = isIconOnly ? (width - iconSize) / 2 : padding;
49839
- const iconY = (height - iconSize) / 2;
49840
- iconSvg = this.renderIconPaths(iconData, iconX, iconY, iconSize, textFill);
49841
- }
49842
- }
49843
- const textX = hasIcon && !isIconOnly ? padding + iconSize + 8 + (width - padding - iconSize - 8 - padding) / 2 : width / 2;
49844
- const textContent = isIconOnly ? "" : `<text x="${textX}" y="${height / 2 + 5}" font-size="14" fill="${textFill}" text-anchor="middle">${this.escapeXml(content)}</text>`;
49845
- return `
49846
- <g transform="translate(${x}, ${y})">
49847
- <rect width="${width}" height="${height}" rx="4" fill="${fill}" ${strokeAttr}/>
49848
- ${iconSvg}
49849
- ${textContent}
49850
- </g>`;
51024
+ svg += "</g>";
51025
+ return svg;
49851
51026
  }
49852
- /**
49853
- * Render icon paths for SVG
49854
- */
49855
- renderIconPaths(data, x, y, size, color) {
49856
- const scale = size / 24;
49857
- const paths = data.map(([tag, attrs]) => {
49858
- const attrStr = Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
49859
- return `<${tag} ${attrStr} />`;
49860
- }).join("");
49861
- return `<g transform="translate(${x}, ${y}) scale(${scale})" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</g>`;
51027
+ renderLinkBox(box) {
51028
+ const node = box.node;
51029
+ const fontSize = 14;
51030
+ const textY = box.y + fontSize;
51031
+ return `<text x="${box.x}" y="${textY}" font-size="${fontSize}" fill="${this.theme.colors.primary}" text-decoration="underline">${this.escapeXml(node.content)}</text>`;
49862
51032
  }
49863
- // ===========================================
49864
- // Display Renderers
49865
- // ===========================================
49866
- renderImage(node) {
49867
- const width = node.w && typeof node.w === "number" ? node.w : 200;
49868
- const height = node.h && typeof node.h === "number" ? node.h : 150;
49869
- const x = this.currentX;
49870
- const y = this.currentY;
49871
- this.currentY += height + 12;
49872
- return `
49873
- <g transform="translate(${x}, ${y})">
49874
- <rect width="${width}" height="${height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49875
- <line x1="0" y1="0" x2="${width}" y2="${height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49876
- <line x1="${width}" y1="0" x2="0" y2="${height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49877
- <text x="${width / 2}" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}" text-anchor="middle">${this.escapeXml(node.alt || "Image")}</text>
51033
+ renderImageBox(box) {
51034
+ const node = box.node;
51035
+ return `<g>
51036
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
51037
+ <line x1="${box.x}" y1="${box.y}" x2="${box.x + box.width}" y2="${box.y + box.height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
51038
+ <line x1="${box.x + box.width}" y1="${box.y}" x2="${box.x}" y2="${box.y + box.height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
51039
+ <text x="${box.x + box.width / 2}" y="${box.y + box.height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}" text-anchor="middle">${this.escapeXml(node.alt || "Image")}</text>
49878
51040
  </g>`;
49879
51041
  }
49880
- renderPlaceholder(node) {
49881
- const width = node.w && typeof node.w === "number" ? node.w : 200;
49882
- const height = node.h && typeof node.h === "number" ? node.h : 100;
49883
- const x = this.currentX;
49884
- const y = this.currentY;
49885
- this.currentY += height + 12;
49886
- return `
49887
- <g transform="translate(${x}, ${y})">
49888
- <rect width="${width}" height="${height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1" stroke-dasharray="4,4"/>
49889
- <text x="${width / 2}" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}" text-anchor="middle">${this.escapeXml(node.label || "Placeholder")}</text>
51042
+ renderPlaceholderBox(box) {
51043
+ const node = box.node;
51044
+ const patternId = `placeholder-pattern-${this.clipPathCounter++}`;
51045
+ const patternDef = `<pattern id="${patternId}" width="14" height="14" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
51046
+ <rect width="14" height="14" fill="#f9f9f9"/>
51047
+ <rect width="7" height="14" fill="rgba(0,0,0,0.03)"/>
51048
+ </pattern>`;
51049
+ this.clipPathDefs.push(patternDef);
51050
+ return `<g>
51051
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="4" fill="url(#${patternId})" stroke="${this.theme.colors.muted}" stroke-width="1" stroke-dasharray="4,2"/>
51052
+ <text x="${box.x + box.width / 2}" y="${box.y + box.height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}" text-anchor="middle">${this.escapeXml(node.label || "Placeholder")}</text>
49890
51053
  </g>`;
49891
51054
  }
49892
- renderAvatar(node) {
49893
- const sizes = { xs: 24, sm: 32, md: 40, lg: 48, xl: 64 };
49894
- const size = sizes[node.size || "md"] || 40;
49895
- const radius = size / 2;
49896
- const x = this.currentX;
49897
- const y = this.currentY;
51055
+ renderAvatarBox(box) {
51056
+ const node = box.node;
51057
+ const radius = box.width / 2;
51058
+ const cx = box.x + radius;
51059
+ const cy = box.y + radius;
49898
51060
  const initial = node.name ? node.name.charAt(0).toUpperCase() : "?";
49899
- this.currentY += size + 12;
49900
- return `
49901
- <g transform="translate(${x}, ${y})">
49902
- <circle cx="${radius}" cy="${radius}" r="${radius}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49903
- <text x="${radius}" y="${radius + 5}" font-size="${size / 2.5}" fill="${this.theme.colors.foreground}" text-anchor="middle">${initial}</text>
51061
+ return `<g>
51062
+ <circle cx="${cx}" cy="${cy}" r="${radius}" fill="${this.theme.colors.foreground}"/>
51063
+ <text x="${cx}" y="${cy + 5}" font-size="${box.width / 2.5}" fill="${this.theme.colors.background}" text-anchor="middle">${initial}</text>
49904
51064
  </g>`;
49905
51065
  }
49906
- renderBadge(node) {
49907
- const content = node.content;
49908
- const width = Math.max(24, content.length * 8 + 16);
49909
- const height = 22;
49910
- const x = this.currentX;
49911
- const y = this.currentY;
49912
- const fill = this.theme.colors.muted;
49913
- const textFill = this.theme.colors.foreground;
49914
- this.currentY += height + 8;
49915
- return `
49916
- <g transform="translate(${x}, ${y})">
49917
- <rect width="${width}" height="${height}" rx="11" fill="${fill}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49918
- <text x="${width / 2}" y="${height / 2 + 4}" font-size="12" fill="${textFill}" text-anchor="middle">${this.escapeXml(content)}</text>
51066
+ renderBadgeBox(box) {
51067
+ const node = box.node;
51068
+ return `<g>
51069
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="11" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
51070
+ <text x="${box.x + box.width / 2}" y="${box.y + box.height / 2 + 4}" font-size="12" fill="${this.theme.colors.foreground}" text-anchor="middle">${this.escapeXml(node.content)}</text>
49919
51071
  </g>`;
49920
51072
  }
49921
- // ===========================================
49922
- // Data Renderers
49923
- // ===========================================
49924
- renderTable(node) {
51073
+ renderTableBox(box) {
51074
+ const node = box.node;
49925
51075
  const columns = node.columns || [];
49926
51076
  const rows = node.rows || [];
49927
51077
  const rowCount = rows.length || 3;
49928
51078
  const colWidth = 120;
49929
51079
  const rowHeight = 40;
49930
- const x = this.currentX;
49931
- const y = this.currentY;
49932
- let svg = `<g transform="translate(${x}, ${y})">`;
49933
- svg += `<rect width="${columns.length * colWidth}" height="${rowHeight}" fill="${this.theme.colors.muted}"/>`;
51080
+ let svg = `<g>`;
51081
+ svg += `<rect x="${box.x}" y="${box.y}" width="${columns.length * colWidth}" height="${rowHeight}" fill="${this.theme.colors.muted}"/>`;
49934
51082
  columns.forEach((col, i) => {
49935
- svg += `<text x="${i * colWidth + 12}" y="${rowHeight / 2 + 5}" font-size="14" font-weight="600">${this.escapeXml(col)}</text>`;
51083
+ svg += `<text x="${box.x + i * colWidth + 12}" y="${box.y + rowHeight / 2 + 5}" font-size="14" font-weight="600">${this.escapeXml(col)}</text>`;
49936
51084
  });
49937
- const displayRowCount = rows.length > 0 ? rows.length : Math.max(rowCount, 3);
51085
+ const displayRowCount = rows.length > 0 ? rows.length : rowCount;
49938
51086
  for (let rowIdx = 0; rowIdx < displayRowCount; rowIdx++) {
49939
- const rowY = (rowIdx + 1) * rowHeight;
49940
- svg += `<rect y="${rowY}" width="${columns.length * colWidth}" height="${rowHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51087
+ const rowY = box.y + (rowIdx + 1) * rowHeight;
51088
+ svg += `<rect x="${box.x}" y="${rowY}" width="${columns.length * colWidth}" height="${rowHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49941
51089
  columns.forEach((_, colIdx) => {
49942
51090
  const cellContent = rows[rowIdx] && rows[rowIdx][colIdx] ? String(typeof rows[rowIdx][colIdx] === "object" ? "..." : rows[rowIdx][colIdx]) : "\u2014";
49943
- svg += `<text x="${colIdx * colWidth + 12}" y="${rowY + rowHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(cellContent)}</text>`;
51091
+ svg += `<text x="${box.x + colIdx * colWidth + 12}" y="${rowY + rowHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(cellContent)}</text>`;
49944
51092
  });
49945
51093
  }
49946
51094
  svg += "</g>";
49947
- this.currentY += (displayRowCount + 1) * rowHeight + 16;
49948
51095
  return svg;
49949
51096
  }
49950
- renderList(node) {
49951
- const x = this.currentX;
49952
- let y = this.currentY;
51097
+ renderListBox(box) {
51098
+ const node = box.node;
49953
51099
  const items = node.items || [];
49954
51100
  const ordered = node.ordered || false;
49955
- let svg = `<g transform="translate(${x}, ${y})">`;
51101
+ let svg = "<g>";
49956
51102
  items.forEach((item, idx) => {
49957
51103
  const marker = ordered ? `${idx + 1}.` : "\u2022";
49958
51104
  const content = typeof item === "string" ? item : item.content;
49959
- svg += `<text x="0" y="${idx * 24 + 16}" font-size="14" fill="${this.theme.colors.foreground}">${marker} ${this.escapeXml(content)}</text>`;
51105
+ svg += `<text x="${box.x}" y="${box.y + idx * 28 + 16}" font-size="14" fill="${this.theme.colors.foreground}">${marker} ${this.escapeXml(content)}</text>`;
49960
51106
  });
49961
51107
  svg += "</g>";
49962
- this.currentY += items.length * 24 + 12;
49963
51108
  return svg;
49964
51109
  }
49965
- // ===========================================
49966
- // Feedback Renderers
49967
- // ===========================================
49968
- renderAlert(node) {
49969
- const width = Math.min(400, this.contentWidth);
49970
- const height = 48;
49971
- const x = this.currentX;
49972
- const y = this.currentY;
49973
- this.currentY += height + 12;
49974
- return `
49975
- <g transform="translate(${x}, ${y})">
49976
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49977
- <text x="16" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>
51110
+ renderAlertBox(box) {
51111
+ const node = box.node;
51112
+ return `<g>
51113
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
51114
+ <text x="${box.x + 16}" y="${box.y + box.height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>
49978
51115
  </g>`;
49979
51116
  }
49980
- renderProgress(node) {
49981
- const width = 200;
49982
- const height = 8;
49983
- const x = this.currentX;
49984
- let y = this.currentY;
49985
- let result = "";
51117
+ renderProgressBox(box) {
51118
+ const node = box.node;
51119
+ let y = box.y;
51120
+ let svg = "<g>";
49986
51121
  if (node.label) {
49987
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
51122
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49988
51123
  y += 24;
49989
51124
  }
51125
+ const barHeight = 8;
49990
51126
  const value = node.value || 0;
49991
51127
  const max = node.max || 100;
49992
51128
  const percent = Math.min(100, Math.max(0, value / max * 100));
49993
- result += `
49994
- <g transform="translate(${x}, ${y})">
49995
- <rect width="${width}" height="${height}" rx="${height / 2}" fill="${this.theme.colors.muted}"/>
49996
- <rect width="${width * percent / 100}" height="${height}" rx="${height / 2}" fill="${this.theme.colors.primary}"/>
49997
- </g>`;
49998
- this.currentY = y + height + 12;
49999
- return result;
51129
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${barHeight}" rx="${barHeight / 2}" fill="${this.theme.colors.muted}"/>`;
51130
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width * percent / 100}" height="${barHeight}" rx="${barHeight / 2}" fill="${this.theme.colors.primary}"/>`;
51131
+ svg += "</g>";
51132
+ return svg;
50000
51133
  }
50001
- renderSpinner(node) {
50002
- const sizes = { xs: 16, sm: 20, md: 24, lg: 32, xl: 40 };
50003
- const size = sizes[node.size || "md"] || 24;
50004
- const x = this.currentX + size / 2;
50005
- const y = this.currentY + size / 2;
50006
- const radius = size / 2 - 2;
50007
- this.currentY += size + 12;
50008
- return `
50009
- <g transform="translate(${x}, ${y})">
50010
- <circle r="${radius}" fill="none" stroke="${this.theme.colors.muted}" stroke-width="2"/>
50011
- <path d="M0,-${radius} A${radius},${radius} 0 0,1 ${radius},0" fill="none" stroke="${this.theme.colors.primary}" stroke-width="2" stroke-linecap="round"/>
51134
+ renderSpinnerBox(box) {
51135
+ const cx = box.x + box.width / 2;
51136
+ const cy = box.y + box.height / 2;
51137
+ const radius = box.width / 2 - 2;
51138
+ return `<g>
51139
+ <circle cx="${cx}" cy="${cy}" r="${radius}" fill="none" stroke="${this.theme.colors.muted}" stroke-width="2"/>
51140
+ <path d="M${cx},${cy - radius} A${radius},${radius} 0 0,1 ${cx + radius},${cy}" fill="none" stroke="${this.theme.colors.primary}" stroke-width="2" stroke-linecap="round"/>
50012
51141
  </g>`;
50013
51142
  }
50014
- // ===========================================
50015
- // Navigation Renderers
50016
- // ===========================================
50017
- renderNav(node) {
50018
- const items = node.items || [];
50019
- const x = this.currentX;
50020
- const y = this.currentY;
51143
+ renderNavBox(box) {
51144
+ const node = box.node;
50021
51145
  const vertical = node.vertical || false;
50022
- let svg = `<g transform="translate(${x}, ${y})">`;
51146
+ let svg = "<g>";
51147
+ if (node.children && node.children.length > 0) {
51148
+ let offsetY = 0;
51149
+ node.children.forEach((child) => {
51150
+ if (child.type === "divider") {
51151
+ offsetY += 16;
51152
+ svg += `<line x1="${box.x}" y1="${box.y + offsetY}" x2="${box.x + box.width - 32}" y2="${box.y + offsetY}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51153
+ offsetY += 8;
51154
+ } else if (child.type === "group") {
51155
+ svg += `<text x="${box.x}" y="${box.y + offsetY + 16}" font-size="11" font-weight="500" fill="${this.theme.colors.muted}" text-transform="uppercase">${this.escapeXml(child.label)}</text>`;
51156
+ offsetY += 28;
51157
+ child.items.forEach((item) => {
51158
+ if (item.type === "divider") {
51159
+ offsetY += 8;
51160
+ svg += `<line x1="${box.x}" y1="${box.y + offsetY}" x2="${box.x + box.width - 32}" y2="${box.y + offsetY}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51161
+ offsetY += 8;
51162
+ } else {
51163
+ const fill = item.active ? this.theme.colors.foreground : this.theme.colors.muted;
51164
+ const fontWeight = item.active ? 'font-weight="500"' : "";
51165
+ svg += `<text x="${box.x}" y="${box.y + offsetY + 16}" font-size="14" ${fontWeight} fill="${fill}">${this.escapeXml(item.label)}</text>`;
51166
+ offsetY += 32;
51167
+ }
51168
+ });
51169
+ } else if (child.type === "item") {
51170
+ const fill = child.active ? this.theme.colors.foreground : this.theme.colors.muted;
51171
+ const fontWeight = child.active ? 'font-weight="500"' : "";
51172
+ svg += `<text x="${box.x}" y="${box.y + offsetY + 16}" font-size="14" ${fontWeight} fill="${fill}">${this.escapeXml(child.label)}</text>`;
51173
+ offsetY += 32;
51174
+ }
51175
+ });
51176
+ svg += "</g>";
51177
+ return svg;
51178
+ }
51179
+ const items = node.items || [];
50023
51180
  if (vertical) {
50024
51181
  items.forEach((item, idx) => {
50025
51182
  const label = typeof item === "string" ? item : item.label;
50026
51183
  const isActive = typeof item === "object" && item.active;
50027
51184
  const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
50028
- svg += `<text x="0" y="${idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51185
+ svg += `<text x="${box.x}" y="${box.y + idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
50029
51186
  });
50030
- this.currentY += items.length * 32 + 12;
50031
51187
  } else {
50032
51188
  let offsetX = 0;
50033
51189
  items.forEach((item) => {
50034
51190
  const label = typeof item === "string" ? item : item.label;
50035
51191
  const isActive = typeof item === "object" && item.active;
50036
51192
  const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
50037
- svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
50038
- offsetX += label.length * 8 + 24;
51193
+ svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51194
+ offsetX += this.estimateTextWidth(label, 14) + 24;
50039
51195
  });
50040
- this.currentY += 32;
50041
51196
  }
50042
51197
  svg += "</g>";
50043
51198
  return svg;
50044
51199
  }
50045
- renderTabs(node) {
51200
+ renderTabsBox(box) {
51201
+ const node = box.node;
50046
51202
  const items = node.items || [];
50047
- const x = this.currentX;
50048
- const y = this.currentY;
50049
51203
  const tabHeight = 40;
50050
- let svg = `<g transform="translate(${x}, ${y})">`;
51204
+ let svg = "<g>";
50051
51205
  let offsetX = 0;
50052
- items.forEach((item) => {
51206
+ items.forEach((item, idx) => {
50053
51207
  const label = typeof item === "string" ? item : item;
50054
- const tabWidth = label.length * 10 + 24;
50055
- svg += `<rect x="${offsetX}" width="${tabWidth}" height="${tabHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50056
- svg += `<text x="${offsetX + tabWidth / 2}" y="${tabHeight / 2 + 5}" font-size="14" text-anchor="middle">${this.escapeXml(label)}</text>`;
51208
+ const tabWidth = this.estimateTextWidth(label, 14) + 32;
51209
+ const isFirst = idx === 0;
51210
+ svg += `<rect x="${box.x + offsetX}" y="${box.y}" width="${tabWidth}" height="${tabHeight}" fill="${isFirst ? "white" : this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51211
+ svg += `<text x="${box.x + offsetX + tabWidth / 2}" y="${box.y + tabHeight / 2 + 5}" font-size="14" text-anchor="middle" fill="${this.theme.colors.foreground}">${this.escapeXml(label)}</text>`;
50057
51212
  offsetX += tabWidth;
50058
51213
  });
50059
51214
  svg += "</g>";
50060
- this.currentY += tabHeight + 12;
50061
51215
  return svg;
50062
51216
  }
50063
- renderBreadcrumb(node) {
51217
+ renderBreadcrumbBox(box) {
51218
+ const node = box.node;
50064
51219
  const items = node.items || [];
50065
51220
  const separator = "/";
50066
- const x = this.currentX;
50067
- const y = this.currentY;
50068
- let svg = `<g transform="translate(${x}, ${y})">`;
51221
+ let svg = "<g>";
50069
51222
  let offsetX = 0;
50070
51223
  items.forEach((item, idx) => {
50071
51224
  const label = typeof item === "string" ? item : item.label;
50072
51225
  const isLast = idx === items.length - 1;
50073
51226
  const fill = isLast ? this.theme.colors.foreground : this.theme.colors.muted;
50074
- svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
50075
- offsetX += label.length * 8 + 8;
51227
+ svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51228
+ offsetX += this.estimateTextWidth(label, 14) + 8;
50076
51229
  if (!isLast) {
50077
- svg += `<text x="${offsetX}" y="16" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
51230
+ svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
50078
51231
  offsetX += 16;
50079
51232
  }
50080
51233
  });
50081
51234
  svg += "</g>";
50082
- this.currentY += 28;
50083
51235
  return svg;
50084
51236
  }
51237
+ renderIconBox(box) {
51238
+ const node = box.node;
51239
+ const iconData = getIconData(node.name);
51240
+ if (!iconData) {
51241
+ return `<!-- Icon not found: ${node.name} -->`;
51242
+ }
51243
+ const color = this.theme.colors.foreground;
51244
+ return this.renderIconPaths(iconData, box.x, box.y, box.width, color);
51245
+ }
51246
+ renderDividerBox(box) {
51247
+ return `<line x1="${box.x}" y1="${box.y}" x2="${box.x + box.width}" y2="${box.y}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51248
+ }
50085
51249
  // ===========================================
50086
51250
  // Utility Methods
50087
51251
  // ===========================================
50088
- getFontSize(size) {
50089
- const sizes = {
50090
- xs: 12,
50091
- sm: 14,
50092
- base: 16,
50093
- md: 16,
50094
- lg: 18,
50095
- xl: 20,
50096
- "2xl": 24,
50097
- "3xl": 30
50098
- };
50099
- return sizes[size] || 16;
51252
+ renderIconPaths(data, x, y, size, color) {
51253
+ const scale = size / 24;
51254
+ const paths = data.map(([tag, attrs]) => {
51255
+ const attrStr = Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
51256
+ return `<${tag} ${attrStr} />`;
51257
+ }).join("");
51258
+ return `<g transform="translate(${x}, ${y}) scale(${scale})" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</g>`;
51259
+ }
51260
+ getPadding(node) {
51261
+ const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 };
51262
+ if (!("p" in node) && !("px" in node) && !("py" in node)) {
51263
+ if (node.type === "Card" || node.type === "Header" || node.type === "Footer" || node.type === "Sidebar") {
51264
+ return { top: 16, right: 16, bottom: 16, left: 16 };
51265
+ }
51266
+ return defaultPadding;
51267
+ }
51268
+ let p = 0;
51269
+ if ("p" in node && node.p !== void 0) {
51270
+ p = this.resolveSpacing(node.p);
51271
+ }
51272
+ let px = p;
51273
+ let py = p;
51274
+ if ("px" in node && node.px !== void 0) {
51275
+ px = this.resolveSpacing(node.px);
51276
+ }
51277
+ if ("py" in node && node.py !== void 0) {
51278
+ py = this.resolveSpacing(node.py);
51279
+ }
51280
+ if (node.type === "Header") {
51281
+ return { top: 0, right: px, bottom: 0, left: px };
51282
+ }
51283
+ return { top: py, right: px, bottom: py, left: px };
51284
+ }
51285
+ getGap(node) {
51286
+ if ("gap" in node && node.gap !== void 0) {
51287
+ return this.resolveSpacing(node.gap);
51288
+ }
51289
+ return void 0;
51290
+ }
51291
+ resolveSpacing(value) {
51292
+ if (typeof value === "number") {
51293
+ return value * 4;
51294
+ }
51295
+ if (typeof value === "object" && value && "value" in value) {
51296
+ return value.value;
51297
+ }
51298
+ return 0;
51299
+ }
51300
+ resolveSize(value) {
51301
+ if (value === void 0 || value === null) return void 0;
51302
+ if (typeof value === "number") return value;
51303
+ if (typeof value === "string") {
51304
+ if (value === "full") return void 0;
51305
+ const num = parseFloat(value);
51306
+ if (!isNaN(num)) return num;
51307
+ }
51308
+ if (typeof value === "object" && value && "value" in value) {
51309
+ return value.value;
51310
+ }
51311
+ return void 0;
51312
+ }
51313
+ isFullWidth(node) {
51314
+ if ("w" in node) {
51315
+ return node.w === "full";
51316
+ }
51317
+ return false;
51318
+ }
51319
+ estimateTextWidth(text, fontSize) {
51320
+ return text.length * fontSize * 0.6;
50100
51321
  }
50101
51322
  resolveFontSize(size) {
50102
51323
  if (!size) return 16;
50103
51324
  if (typeof size === "string") {
50104
- return this.getFontSize(size);
51325
+ const sizes = {
51326
+ xs: 12,
51327
+ sm: 14,
51328
+ base: 16,
51329
+ md: 16,
51330
+ lg: 18,
51331
+ xl: 20,
51332
+ "2xl": 24,
51333
+ "3xl": 30
51334
+ };
51335
+ return sizes[size] || 16;
50105
51336
  }
50106
51337
  if (typeof size === "object" && "value" in size) {
50107
51338
  if (size.unit === "px") return size.value;
@@ -50112,15 +51343,8 @@ var SvgRenderer = class {
50112
51343
  return 16;
50113
51344
  }
50114
51345
  getTitleFontSize(level) {
50115
- const sizes = {
50116
- 1: 32,
50117
- 2: 28,
50118
- 3: 24,
50119
- 4: 20,
50120
- 5: 18,
50121
- 6: 16
50122
- };
50123
- return sizes[level] || 24;
51346
+ const sizes = { 1: 36, 2: 30, 3: 24, 4: 20, 5: 18, 6: 16 };
51347
+ return sizes[level] || 20;
50124
51348
  }
50125
51349
  escapeXml(str) {
50126
51350
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -50181,15 +51405,22 @@ function renderToSvg2(document, options = {}) {
50181
51405
  height = viewport.height;
50182
51406
  }
50183
51407
  }
50184
- const padding = options.padding ?? 20;
50185
51408
  const background = options.background ?? "#ffffff";
50186
51409
  const { html, css } = render(document, { theme: "light" });
50187
51410
  const svg = `<?xml version="1.0" encoding="UTF-8"?>
50188
51411
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
50189
51412
  viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
50190
51413
  <rect width="100%" height="100%" fill="${background}"/>
50191
- <foreignObject x="${padding}" y="${padding}" width="${width - padding * 2}" height="${height - padding * 2}">
50192
- <div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; overflow: hidden;">
51414
+ <foreignObject x="0" y="0" width="${width}" height="${height}">
51415
+ <div xmlns="http://www.w3.org/1999/xhtml" style="
51416
+ width: ${width}px;
51417
+ height: ${height}px;
51418
+ overflow: hidden;
51419
+ display: flex;
51420
+ justify-content: center;
51421
+ align-items: center;
51422
+ box-sizing: border-box;
51423
+ ">
50193
51424
  <style type="text/css">
50194
51425
  ${css}
50195
51426
  </style>