@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.js CHANGED
@@ -77,12 +77,17 @@ function generateContainerStyles(prefix) {
77
77
  padding: 16px;
78
78
  }
79
79
 
80
- /* Cards in flex rows should expand equally */
80
+ /* Cards in flex rows: respect explicit width, shrink if needed */
81
81
  .${prefix}-row > .${prefix}-card {
82
- flex: 1 1 0%;
82
+ flex: 0 1 auto;
83
83
  min-width: 0;
84
84
  }
85
85
 
86
+ /* Cards without explicit width should expand to fill space */
87
+ .${prefix}-row > .${prefix}-card-flex {
88
+ flex: 1 1 0%;
89
+ }
90
+
86
91
  .${prefix}-card-title {
87
92
  margin: 0 0 12px 0;
88
93
  font-size: 18px;
@@ -236,6 +241,11 @@ function generateTextStyles(prefix) {
236
241
  line-height: 1.25;
237
242
  }
238
243
 
244
+ /* Remove bottom margin when title is in a row (inline with other elements) */
245
+ .${prefix}-row .${prefix}-title {
246
+ margin-bottom: 0;
247
+ }
248
+
239
249
  h1.${prefix}-title { font-size: 36px; }
240
250
  h2.${prefix}-title { font-size: 30px; }
241
251
  h3.${prefix}-title { font-size: 24px; }
@@ -630,6 +640,30 @@ img.${prefix}-image {
630
640
  font-size: 14px;
631
641
  }
632
642
 
643
+ .${prefix}-placeholder-with-children {
644
+ position: relative;
645
+ }
646
+
647
+ .${prefix}-placeholder-label {
648
+ position: absolute;
649
+ top: 50%;
650
+ left: 50%;
651
+ transform: translate(-50%, -50%);
652
+ z-index: 0;
653
+ pointer-events: none;
654
+ }
655
+
656
+ .${prefix}-placeholder-overlay {
657
+ position: absolute;
658
+ top: 0;
659
+ left: 0;
660
+ right: 0;
661
+ bottom: 0;
662
+ z-index: 1;
663
+ display: flex;
664
+ flex-direction: column;
665
+ }
666
+
633
667
  .${prefix}-avatar {
634
668
  display: inline-flex;
635
669
  align-items: center;
@@ -745,12 +779,12 @@ svg.${prefix}-icon {
745
779
  display: block;
746
780
  }
747
781
 
748
- /* Icon size tokens */
782
+ /* Icon size tokens - matches SVG renderer */
749
783
  svg.${prefix}-icon-xs { width: 12px; height: 12px; }
750
- svg.${prefix}-icon-sm { width: 14px; height: 14px; }
751
- svg.${prefix}-icon-md { width: 16px; height: 16px; }
752
- svg.${prefix}-icon-lg { width: 20px; height: 20px; }
753
- svg.${prefix}-icon-xl { width: 24px; height: 24px; }
784
+ svg.${prefix}-icon-sm { width: 16px; height: 16px; }
785
+ svg.${prefix}-icon-md { width: 20px; height: 20px; }
786
+ svg.${prefix}-icon-lg { width: 24px; height: 24px; }
787
+ svg.${prefix}-icon-xl { width: 32px; height: 32px; }
754
788
 
755
789
  .${prefix}-icon svg {
756
790
  display: block;
@@ -1046,6 +1080,27 @@ function generateNavigationStyles(_theme, prefix) {
1046
1080
  cursor: not-allowed;
1047
1081
  }
1048
1082
 
1083
+ .${prefix}-nav-group {
1084
+ display: flex;
1085
+ flex-direction: column;
1086
+ gap: 4px;
1087
+ }
1088
+
1089
+ .${prefix}-nav-group-label {
1090
+ font-size: 11px;
1091
+ font-weight: 500;
1092
+ color: var(--${prefix}-muted);
1093
+ text-transform: uppercase;
1094
+ letter-spacing: 0.05em;
1095
+ padding: 8px 16px 4px;
1096
+ }
1097
+
1098
+ .${prefix}-nav-divider {
1099
+ margin: 8px 0;
1100
+ border: none;
1101
+ border-top: 1px solid var(--${prefix}-border);
1102
+ }
1103
+
1049
1104
  .${prefix}-tabs {
1050
1105
  border-bottom: 1px solid var(--${prefix}-border);
1051
1106
  }
@@ -1263,7 +1318,6 @@ function generateBaseStyles(prefix) {
1263
1318
  font-family: var(--${prefix}-font);
1264
1319
  color: var(--${prefix}-fg);
1265
1320
  background: var(--${prefix}-bg);
1266
- min-height: 100vh;
1267
1321
  box-sizing: border-box;
1268
1322
  position: relative;
1269
1323
  display: flex;
@@ -1274,10 +1328,19 @@ function generateBaseStyles(prefix) {
1274
1328
  overflow: hidden;
1275
1329
  }
1276
1330
 
1331
+ /* Col direct child of page should fill page height */
1332
+ .${prefix}-page > .${prefix}-col {
1333
+ flex: 1;
1334
+ min-height: 0;
1335
+ }
1336
+
1277
1337
  /* Row containing sidebar should fill remaining space */
1278
1338
  .${prefix}-page > .${prefix}-row:has(.${prefix}-sidebar),
1279
- .${prefix}-page > .${prefix}-row:has(.${prefix}-main) {
1339
+ .${prefix}-page > .${prefix}-row:has(.${prefix}-main),
1340
+ .${prefix}-page > .${prefix}-col > .${prefix}-row:has(.${prefix}-sidebar),
1341
+ .${prefix}-page > .${prefix}-col > .${prefix}-row:has(.${prefix}-main) {
1280
1342
  flex: 1;
1343
+ min-height: 0;
1281
1344
  align-items: stretch;
1282
1345
  }
1283
1346
 
@@ -1320,10 +1383,15 @@ function generateGridClasses(_theme, prefix) {
1320
1383
  box-sizing: border-box;
1321
1384
  }
1322
1385
 
1386
+ /* When explicit width is set, don't flex-grow */
1387
+ .${prefix}-row[style*="width:"],
1388
+ .${prefix}-col[style*="width:"] {
1389
+ flex: 0 0 auto;
1390
+ }
1391
+
1323
1392
  `;
1324
1393
  for (let i = 1; i <= 12; i++) {
1325
- const width = (i / 12 * 100).toFixed(4);
1326
- css += `.${prefix}-col-${i} { flex: 0 0 auto; width: ${width}%; }
1394
+ css += `.${prefix}-col-${i} { flex: ${i} 0 0%; min-width: 0; }
1327
1395
  `;
1328
1396
  }
1329
1397
  return css;
@@ -1491,6 +1559,20 @@ function generateLayoutClasses(prefix) {
1491
1559
  .${prefix}-main {
1492
1560
  flex: 1;
1493
1561
  padding: 16px;
1562
+ display: flex;
1563
+ flex-direction: column;
1564
+ }
1565
+
1566
+ /* Scrollable main content */
1567
+ .${prefix}-main.${prefix}-scroll {
1568
+ overflow-y: auto;
1569
+ overflow-x: hidden;
1570
+ }
1571
+
1572
+ /* Main content should align to top, not stretch to fill */
1573
+ /* But allow explicit flex=1 to override */
1574
+ .${prefix}-main > .${prefix}-col:not(.${prefix}-flex-1) {
1575
+ flex: 0 0 auto;
1494
1576
  }
1495
1577
 
1496
1578
  .${prefix}-footer {
@@ -1507,6 +1589,7 @@ function generateLayoutClasses(prefix) {
1507
1589
  border-right: 1px solid var(--${prefix}-border);
1508
1590
  padding: 16px 16px 16px 20px;
1509
1591
  flex-shrink: 0;
1592
+ align-self: stretch;
1510
1593
  }
1511
1594
 
1512
1595
  .${prefix}-sidebar-right {
@@ -48076,14 +48159,39 @@ var lucideIcons = {
48076
48159
  ]
48077
48160
  ]
48078
48161
  };
48162
+ var iconAliases = {
48163
+ "home": "house",
48164
+ "plus-square": "square-plus",
48165
+ "minus-square": "square-minus",
48166
+ "x-square": "square-x",
48167
+ "check-square": "square-check",
48168
+ "edit": "pencil",
48169
+ "edit-2": "pencil",
48170
+ "edit-3": "pencil-line",
48171
+ "trash": "trash-2",
48172
+ "delete": "trash-2",
48173
+ "close": "x",
48174
+ "menu": "menu",
48175
+ "hamburger": "menu",
48176
+ "dots": "more-horizontal",
48177
+ "dots-vertical": "more-vertical",
48178
+ "cog": "settings",
48179
+ "gear": "settings"
48180
+ };
48079
48181
  function getIconData(name) {
48080
48182
  if (lucideIcons[name]) {
48081
48183
  return lucideIcons[name];
48082
48184
  }
48185
+ if (iconAliases[name] && lucideIcons[iconAliases[name]]) {
48186
+ return lucideIcons[iconAliases[name]];
48187
+ }
48083
48188
  const kebabName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
48084
48189
  if (lucideIcons[kebabName]) {
48085
48190
  return lucideIcons[kebabName];
48086
48191
  }
48192
+ if (iconAliases[kebabName] && lucideIcons[iconAliases[kebabName]]) {
48193
+ return lucideIcons[iconAliases[kebabName]];
48194
+ }
48087
48195
  return void 0;
48088
48196
  }
48089
48197
  function renderIconSvg(data, _size = 24, strokeWidth = 2, className = "", styleAttr = "") {
@@ -48193,7 +48301,7 @@ var HtmlRenderer = class extends BaseRenderer {
48193
48301
  const title = node.title ? `<title>${this.escapeHtml(node.title)}</title>
48194
48302
  ` : "";
48195
48303
  const commonStyles = this.buildCommonStyles(node);
48196
- const viewportStyle = `width: ${viewport.width}px; height: ${viewport.height}px`;
48304
+ const viewportStyle = `position: relative; width: ${viewport.width}px; height: ${viewport.height}px; overflow: hidden`;
48197
48305
  const combinedStyle = commonStyles ? `${viewportStyle}; ${commonStyles}` : viewportStyle;
48198
48306
  const dataAttrs = `data-viewport-width="${viewport.width}" data-viewport-height="${viewport.height}" data-viewport-label="${viewport.label}"`;
48199
48307
  return `<div class="${classes}" style="${combinedStyle}" ${dataAttrs}>
@@ -48359,6 +48467,7 @@ ${children}
48359
48467
  renderMain(node) {
48360
48468
  const classes = this.buildClassString([
48361
48469
  `${this.prefix}-main`,
48470
+ node.scroll ? `${this.prefix}-scroll` : void 0,
48362
48471
  ...this.getCommonClasses(node)
48363
48472
  ]);
48364
48473
  const styles = this.buildCommonStyles(node);
@@ -48444,6 +48553,10 @@ ${children}
48444
48553
  /**
48445
48554
  * Build common inline styles for all values
48446
48555
  *
48556
+ * Position values (x, y) for absolute positioning:
48557
+ * - When x or y is specified, element gets position: absolute
48558
+ * - x → left, y → top
48559
+ *
48447
48560
  * Spacing values (p, m, gap) use token system:
48448
48561
  * - number: spacing token (e.g., p=4 → padding: 16px from token table)
48449
48562
  * - ValueWithUnit: direct CSS value (e.g., p=16px → padding: 16px)
@@ -48458,6 +48571,17 @@ ${children}
48458
48571
  */
48459
48572
  buildCommonStyles(props) {
48460
48573
  const styles = [];
48574
+ if (props.x !== void 0 || props.y !== void 0) {
48575
+ styles.push("position: absolute");
48576
+ if (props.x !== void 0) {
48577
+ const xValue = resolveSizeValueToCss(props.x);
48578
+ if (xValue) styles.push(`left: ${xValue}`);
48579
+ }
48580
+ if (props.y !== void 0) {
48581
+ const yValue = resolveSizeValueToCss(props.y);
48582
+ if (yValue) styles.push(`top: ${yValue}`);
48583
+ }
48584
+ }
48461
48585
  const wValue = resolveSizeValueToCss(props.w);
48462
48586
  if (wValue) {
48463
48587
  styles.push(`width: ${wValue}`);
@@ -48567,8 +48691,10 @@ ${children}
48567
48691
  // Container Node Renderers
48568
48692
  // ===========================================
48569
48693
  renderCard(node) {
48694
+ const hasExplicitWidth = node.w !== void 0;
48570
48695
  const classes = this.buildClassString([
48571
48696
  `${this.prefix}-card`,
48697
+ !hasExplicitWidth ? `${this.prefix}-card-flex` : void 0,
48572
48698
  node.shadow ? `${this.prefix}-card-shadow-${node.shadow}` : void 0,
48573
48699
  ...this.getCommonClasses(node)
48574
48700
  ]);
@@ -48905,11 +49031,19 @@ ${slider}`;
48905
49031
  renderPlaceholder(node) {
48906
49032
  const classes = this.buildClassString([
48907
49033
  `${this.prefix}-placeholder`,
49034
+ node.children && node.children.length > 0 ? `${this.prefix}-placeholder-with-children` : void 0,
48908
49035
  ...this.getCommonClasses(node)
48909
49036
  ]);
48910
49037
  const styles = this.buildCommonStyles(node);
48911
49038
  const styleAttr = styles ? ` style="${styles}"` : "";
48912
49039
  const label = node.label ? this.escapeHtml(node.label) : "Placeholder";
49040
+ if (node.children && node.children.length > 0) {
49041
+ const childrenHtml = this.renderChildren(node.children);
49042
+ return `<div class="${classes}"${styleAttr}>
49043
+ <span class="${this.prefix}-placeholder-label">${label}</span>
49044
+ <div class="${this.prefix}-placeholder-overlay">${childrenHtml}</div>
49045
+ </div>`;
49046
+ }
48913
49047
  return `<div class="${classes}"${styleAttr}>${label}</div>`;
48914
49048
  }
48915
49049
  renderAvatar(node) {
@@ -49155,6 +49289,12 @@ ${items}
49155
49289
  ]);
49156
49290
  const styles = this.buildCommonStyles(node);
49157
49291
  const styleAttr = styles ? ` style="${styles}"` : "";
49292
+ if (node.children && node.children.length > 0) {
49293
+ const content = this.renderNavChildren(node.children);
49294
+ return `<nav class="${classes}"${styleAttr}>
49295
+ ${content}
49296
+ </nav>`;
49297
+ }
49158
49298
  const items = node.items.map((item) => {
49159
49299
  if (typeof item === "string") {
49160
49300
  return `<a class="${this.prefix}-nav-link" href="#">${this.escapeHtml(item)}</a>`;
@@ -49164,12 +49304,48 @@ ${items}
49164
49304
  item.active ? `${this.prefix}-nav-link-active` : void 0,
49165
49305
  item.disabled ? `${this.prefix}-nav-link-disabled` : void 0
49166
49306
  ]);
49167
- return `<a class="${linkClasses}" href="${item.href || "#"}">${this.escapeHtml(item.label)}</a>`;
49307
+ const iconHtml = item.icon ? this.renderIconHtml(item.icon) + " " : "";
49308
+ return `<a class="${linkClasses}" href="${item.href || "#"}">${iconHtml}${this.escapeHtml(item.label)}</a>`;
49168
49309
  }).join("\n");
49169
49310
  return `<nav class="${classes}"${styleAttr}>
49170
49311
  ${items}
49171
49312
  </nav>`;
49172
49313
  }
49314
+ renderNavChildren(children) {
49315
+ return children.map((child) => {
49316
+ if (child.type === "divider") {
49317
+ return `<hr class="${this.prefix}-nav-divider" />`;
49318
+ }
49319
+ if (child.type === "group") {
49320
+ const groupItems = child.items.map((item) => {
49321
+ if (item.type === "divider") {
49322
+ return `<hr class="${this.prefix}-nav-divider" />`;
49323
+ }
49324
+ return this.renderNavItem(item);
49325
+ }).join("\n");
49326
+ return `<div class="${this.prefix}-nav-group">
49327
+ <div class="${this.prefix}-nav-group-label">${this.escapeHtml(child.label)}</div>
49328
+ ${groupItems}
49329
+ </div>`;
49330
+ }
49331
+ if (child.type === "item") {
49332
+ return this.renderNavItem(child);
49333
+ }
49334
+ return "";
49335
+ }).join("\n");
49336
+ }
49337
+ renderNavItem(item) {
49338
+ const linkClasses = this.buildClassString([
49339
+ `${this.prefix}-nav-link`,
49340
+ item.active ? `${this.prefix}-nav-link-active` : void 0,
49341
+ item.disabled ? `${this.prefix}-nav-link-disabled` : void 0
49342
+ ]);
49343
+ const iconHtml = item.icon ? this.renderIconHtml(item.icon) + " " : "";
49344
+ return `<a class="${linkClasses}" href="${item.href || "#"}">${iconHtml}${this.escapeHtml(item.label)}</a>`;
49345
+ }
49346
+ renderIconHtml(iconName) {
49347
+ return `<span class="${this.prefix}-icon" data-icon="${iconName}"></span>`;
49348
+ }
49173
49349
  renderTabs(node) {
49174
49350
  const classes = this.buildClassString([
49175
49351
  `${this.prefix}-tabs`,
@@ -49324,13 +49500,239 @@ function createHtmlRenderer(options) {
49324
49500
  return new HtmlRenderer(options);
49325
49501
  }
49326
49502
 
49503
+ // src/renderer/svg/flex-layout.ts
49504
+ function computeFlexLayout(items, config) {
49505
+ const { mainSize, crossSize, justifyContent, alignItems, gap } = config;
49506
+ const computed = items.map((props, index) => {
49507
+ let flexBasis;
49508
+ if (props.basis === "auto" || props.basis === "content") {
49509
+ flexBasis = props.contentSize;
49510
+ } else {
49511
+ flexBasis = props.basis;
49512
+ }
49513
+ flexBasis = clamp(flexBasis, props.minSize, props.maxSize);
49514
+ return {
49515
+ index,
49516
+ props,
49517
+ flexBasis,
49518
+ mainSize: flexBasis,
49519
+ crossSize: 0,
49520
+ mainPosition: 0,
49521
+ crossPosition: 0,
49522
+ frozen: false,
49523
+ scaledShrinkFactor: props.shrink * flexBasis
49524
+ };
49525
+ });
49526
+ const totalGap = Math.max(0, (items.length - 1) * gap);
49527
+ const totalFlexBasis = computed.reduce((sum, item) => sum + item.flexBasis, 0);
49528
+ let freeSpace = mainSize - totalFlexBasis - totalGap;
49529
+ if (freeSpace !== 0) {
49530
+ resolveFlexibleLengths(computed, freeSpace);
49531
+ }
49532
+ distributeMainAxis(computed, mainSize, totalGap, gap, justifyContent);
49533
+ const crossSizeMax = crossSize ?? Math.max(...computed.map((item) => item.props.contentSize), 0);
49534
+ alignCrossAxis(computed, crossSizeMax, alignItems);
49535
+ const mainSizeUsed = computed.reduce((sum, item) => sum + item.mainSize, 0) + totalGap;
49536
+ return {
49537
+ items: computed,
49538
+ mainSizeUsed,
49539
+ crossSizeMax
49540
+ };
49541
+ }
49542
+ function resolveFlexibleLengths(items, initialFreeSpace) {
49543
+ items.forEach((item) => {
49544
+ item.frozen = false;
49545
+ item.mainSize = item.flexBasis;
49546
+ });
49547
+ let freeSpace = initialFreeSpace;
49548
+ const isGrowing = freeSpace > 0;
49549
+ let iteration = 0;
49550
+ const maxIterations = items.length + 1;
49551
+ while (iteration < maxIterations) {
49552
+ iteration++;
49553
+ const unfrozen = items.filter((item) => !item.frozen);
49554
+ if (unfrozen.length === 0) break;
49555
+ let flexFactorSum;
49556
+ if (isGrowing) {
49557
+ flexFactorSum = unfrozen.reduce((sum, item) => sum + item.props.grow, 0);
49558
+ } else {
49559
+ flexFactorSum = unfrozen.reduce((sum, item) => sum + item.scaledShrinkFactor, 0);
49560
+ }
49561
+ if (flexFactorSum === 0) {
49562
+ unfrozen.forEach((item) => item.frozen = true);
49563
+ break;
49564
+ }
49565
+ const usedSpace = items.reduce((sum, item) => sum + item.mainSize, 0);
49566
+ const containerMainSize = usedSpace + freeSpace;
49567
+ freeSpace = containerMainSize - usedSpace;
49568
+ let totalViolation = 0;
49569
+ for (const item of unfrozen) {
49570
+ let flexFraction;
49571
+ if (isGrowing) {
49572
+ flexFraction = item.props.grow / flexFactorSum;
49573
+ } else {
49574
+ flexFraction = item.scaledShrinkFactor / flexFactorSum;
49575
+ }
49576
+ const deltaSize = freeSpace * flexFraction;
49577
+ const targetSize = item.flexBasis + deltaSize;
49578
+ const clampedSize = clamp(targetSize, item.props.minSize, item.props.maxSize);
49579
+ const violation = clampedSize - targetSize;
49580
+ item.mainSize = clampedSize;
49581
+ totalViolation += violation;
49582
+ if (violation !== 0) {
49583
+ item.frozen = true;
49584
+ }
49585
+ }
49586
+ if (Math.abs(totalViolation) < 0.01) {
49587
+ unfrozen.forEach((item) => item.frozen = true);
49588
+ break;
49589
+ }
49590
+ freeSpace = containerMainSize - items.reduce((sum, item) => sum + item.mainSize, 0);
49591
+ }
49592
+ }
49593
+ function distributeMainAxis(items, containerSize, totalGap, gap, justifyContent) {
49594
+ if (items.length === 0) return;
49595
+ const totalItemSize = items.reduce((sum, item) => sum + item.mainSize, 0);
49596
+ const freeSpace = containerSize - totalItemSize - totalGap;
49597
+ let position = 0;
49598
+ let itemGap = gap;
49599
+ let leadingSpace = 0;
49600
+ switch (justifyContent) {
49601
+ case "flex-start":
49602
+ leadingSpace = 0;
49603
+ break;
49604
+ case "flex-end":
49605
+ leadingSpace = freeSpace;
49606
+ break;
49607
+ case "center":
49608
+ leadingSpace = freeSpace / 2;
49609
+ break;
49610
+ case "space-between":
49611
+ leadingSpace = 0;
49612
+ if (items.length > 1) {
49613
+ itemGap = gap + freeSpace / (items.length - 1);
49614
+ }
49615
+ break;
49616
+ case "space-around":
49617
+ if (items.length > 0) {
49618
+ const spacePerItem = freeSpace / items.length;
49619
+ leadingSpace = spacePerItem / 2;
49620
+ itemGap = gap + spacePerItem;
49621
+ }
49622
+ break;
49623
+ case "space-evenly":
49624
+ if (items.length > 0) {
49625
+ const totalSpaces = items.length + 1;
49626
+ const spaceSize = freeSpace / totalSpaces;
49627
+ leadingSpace = spaceSize;
49628
+ itemGap = gap + spaceSize;
49629
+ }
49630
+ break;
49631
+ }
49632
+ position = leadingSpace;
49633
+ for (let i = 0; i < items.length; i++) {
49634
+ items[i].mainPosition = position;
49635
+ position += items[i].mainSize;
49636
+ if (i < items.length - 1) {
49637
+ position += itemGap;
49638
+ }
49639
+ }
49640
+ }
49641
+ function alignCrossAxis(items, containerCrossSize, alignItems) {
49642
+ for (const item of items) {
49643
+ const align = item.props.alignSelf ?? alignItems;
49644
+ const itemCrossSize = item.props.contentSize;
49645
+ item.crossSize = align === "stretch" ? containerCrossSize : itemCrossSize;
49646
+ switch (align) {
49647
+ case "flex-start":
49648
+ item.crossPosition = 0;
49649
+ break;
49650
+ case "flex-end":
49651
+ item.crossPosition = containerCrossSize - item.crossSize;
49652
+ break;
49653
+ case "center":
49654
+ item.crossPosition = (containerCrossSize - item.crossSize) / 2;
49655
+ break;
49656
+ case "stretch":
49657
+ item.crossPosition = 0;
49658
+ item.crossSize = containerCrossSize;
49659
+ break;
49660
+ case "baseline":
49661
+ item.crossPosition = 0;
49662
+ break;
49663
+ }
49664
+ }
49665
+ }
49666
+ function clamp(value, min, max) {
49667
+ return Math.max(min, Math.min(max, value));
49668
+ }
49669
+ function toJustifyContent(justify) {
49670
+ switch (justify) {
49671
+ case "start":
49672
+ return "flex-start";
49673
+ case "end":
49674
+ return "flex-end";
49675
+ case "center":
49676
+ return "center";
49677
+ case "between":
49678
+ return "space-between";
49679
+ case "around":
49680
+ return "space-around";
49681
+ case "evenly":
49682
+ return "space-evenly";
49683
+ default:
49684
+ return "flex-start";
49685
+ }
49686
+ }
49687
+ function toAlignItems(align) {
49688
+ switch (align) {
49689
+ case "start":
49690
+ return "flex-start";
49691
+ case "end":
49692
+ return "flex-end";
49693
+ case "center":
49694
+ return "center";
49695
+ case "stretch":
49696
+ return "stretch";
49697
+ case "baseline":
49698
+ return "baseline";
49699
+ default:
49700
+ return "stretch";
49701
+ }
49702
+ }
49703
+ function createFlexItemProps(contentSize, options = {}) {
49704
+ return {
49705
+ basis: options.basis ?? "auto",
49706
+ grow: options.grow ?? 0,
49707
+ shrink: options.shrink ?? 1,
49708
+ minSize: options.minSize ?? 0,
49709
+ maxSize: options.maxSize ?? Infinity,
49710
+ contentSize,
49711
+ alignSelf: options.alignSelf
49712
+ };
49713
+ }
49714
+ function createFlexGrowItemProps(contentSize, options = {}) {
49715
+ return {
49716
+ basis: options.basis ?? 0,
49717
+ grow: options.grow ?? 1,
49718
+ shrink: options.shrink ?? 1,
49719
+ minSize: options.minSize ?? 0,
49720
+ maxSize: options.maxSize ?? Infinity,
49721
+ contentSize,
49722
+ alignSelf: options.alignSelf
49723
+ };
49724
+ }
49725
+
49327
49726
  // src/renderer/svg/index.ts
49328
49727
  var SvgRenderer = class {
49329
49728
  options;
49330
49729
  theme;
49331
- currentX = 0;
49332
- currentY = 0;
49333
- contentWidth = 0;
49730
+ pageWidth = 0;
49731
+ pageHeight = 0;
49732
+ clipPathDefs = [];
49733
+ clipPathCounter = 0;
49734
+ // Default spacing values
49735
+ DEFAULT_GAP = 16;
49334
49736
  constructor(options = {}) {
49335
49737
  this.options = {
49336
49738
  width: options.width ?? 800,
@@ -49341,28 +49743,41 @@ var SvgRenderer = class {
49341
49743
  fontFamily: options.fontFamily ?? "system-ui, -apple-system, sans-serif"
49342
49744
  };
49343
49745
  this.theme = defaultTheme;
49344
- this.contentWidth = this.options.width - this.options.padding * 2;
49345
49746
  }
49346
49747
  /**
49347
49748
  * Render a wireframe document to SVG
49348
49749
  */
49349
49750
  render(doc) {
49350
- this.currentX = this.options.padding;
49351
- this.currentY = this.options.padding;
49751
+ this.clipPathDefs = [];
49752
+ this.clipPathCounter = 0;
49352
49753
  const firstPage = doc.children[0];
49353
49754
  let width = this.options.width;
49354
49755
  let height = this.options.height;
49355
- if (firstPage && (firstPage.viewport !== void 0 || firstPage.device !== void 0)) {
49356
- const viewport = resolveViewport(firstPage.viewport, firstPage.device);
49357
- width = viewport.width;
49358
- height = viewport.height;
49359
- this.contentWidth = width - this.options.padding * 2;
49756
+ if (firstPage) {
49757
+ const pageAny = firstPage;
49758
+ const hasExplicitWidth = pageAny.width !== void 0;
49759
+ const hasExplicitHeight = pageAny.height !== void 0;
49760
+ if (hasExplicitWidth || hasExplicitHeight) {
49761
+ if (hasExplicitWidth) {
49762
+ width = pageAny.width;
49763
+ }
49764
+ if (hasExplicitHeight) {
49765
+ height = pageAny.height;
49766
+ }
49767
+ } else if (firstPage.viewport !== void 0 || firstPage.device !== void 0) {
49768
+ const viewport = resolveViewport(firstPage.viewport, firstPage.device);
49769
+ width = viewport.width;
49770
+ height = viewport.height;
49771
+ }
49360
49772
  }
49773
+ this.pageWidth = width;
49774
+ this.pageHeight = height;
49361
49775
  const content = doc.children.map((page) => this.renderPage(page)).join("\n");
49776
+ const allDefs = this.generateDefs() + "\n" + this.clipPathDefs.join("\n");
49362
49777
  const svg = `<?xml version="1.0" encoding="UTF-8"?>
49363
49778
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
49364
49779
  <defs>
49365
- ${this.generateDefs()}
49780
+ ${allDefs}
49366
49781
  </defs>
49367
49782
  <rect width="100%" height="100%" fill="${this.options.background}"/>
49368
49783
  <g transform="scale(${this.options.scale})">
@@ -49372,7 +49787,7 @@ var SvgRenderer = class {
49372
49787
  return { svg, width, height };
49373
49788
  }
49374
49789
  /**
49375
- * Generate SVG defs (styles, patterns, etc.)
49790
+ * Generate SVG defs (styles)
49376
49791
  */
49377
49792
  generateDefs() {
49378
49793
  return `
@@ -49383,684 +49798,1500 @@ var SvgRenderer = class {
49383
49798
  </style>
49384
49799
  `;
49385
49800
  }
49801
+ // ===========================================
49802
+ // Page Layout
49803
+ // ===========================================
49804
+ renderPage(page) {
49805
+ const padding = this.options.padding;
49806
+ const contentWidth = this.pageWidth - padding * 2;
49807
+ const contentHeight = this.pageHeight - padding * 2;
49808
+ const constraints = {
49809
+ maxWidth: contentWidth,
49810
+ maxHeight: contentHeight
49811
+ };
49812
+ const childBoxes = [];
49813
+ let currentY = padding;
49814
+ const isCentered = page.centered === true;
49815
+ const hasHeader = page.children.some((c) => c.type === "Header");
49816
+ const hasFooter = page.children.some((c) => c.type === "Footer");
49817
+ const hasMain = page.children.some((c) => c.type === "Main");
49818
+ if (hasHeader || hasFooter || hasMain) {
49819
+ return this.renderPageWithFixedLayout(page, padding, contentWidth, contentHeight);
49820
+ }
49821
+ const measurements = page.children.map((child) => this.measureNode(child, constraints));
49822
+ const gap = this.getGap(page) || this.DEFAULT_GAP;
49823
+ const totalChildrenHeight = measurements.reduce((sum, m, i) => {
49824
+ return sum + m.height + (i > 0 ? gap : 0);
49825
+ }, 0);
49826
+ if (isCentered) {
49827
+ const availableHeight = contentHeight;
49828
+ currentY = padding + Math.max(0, (availableHeight - totalChildrenHeight) / 2);
49829
+ }
49830
+ for (let i = 0; i < page.children.length; i++) {
49831
+ const child = page.children[i];
49832
+ const measurement = measurements[i];
49833
+ let childX = padding;
49834
+ if (isCentered || this.shouldCenterHorizontally(child)) {
49835
+ childX = padding + (contentWidth - measurement.width) / 2;
49836
+ }
49837
+ const box = this.layoutNode(child, childX, currentY, constraints);
49838
+ childBoxes.push(box);
49839
+ currentY += box.height + gap;
49840
+ }
49841
+ const elements = [];
49842
+ for (const box of childBoxes) {
49843
+ elements.push(this.renderBox(box));
49844
+ }
49845
+ return elements.join("\n");
49846
+ }
49386
49847
  /**
49387
- * Render a page node
49848
+ * Render page with fixed header/footer layout
49849
+ * Header at top, Footer at bottom, Main fills remaining space
49388
49850
  */
49389
- renderPage(node) {
49851
+ renderPageWithFixedLayout(page, padding, contentWidth, contentHeight) {
49852
+ const constraints = {
49853
+ maxWidth: contentWidth,
49854
+ maxHeight: contentHeight
49855
+ };
49856
+ const header = page.children.find((c) => c.type === "Header");
49857
+ const footer = page.children.find((c) => c.type === "Footer");
49858
+ const otherChildren = page.children.filter((c) => c.type !== "Header" && c.type !== "Footer");
49390
49859
  const elements = [];
49391
- if (node.title) {
49392
- elements.push(this.renderPageTitle(node.title));
49860
+ let headerHeight = 0;
49861
+ let footerHeight = 0;
49862
+ let currentY = padding;
49863
+ if (header) {
49864
+ const headerMeasure = this.measureNode(header, constraints);
49865
+ headerHeight = headerMeasure.height;
49866
+ const headerBox = this.layoutNode(header, padding, currentY, constraints);
49867
+ elements.push(this.renderBox(headerBox));
49393
49868
  }
49394
- for (const child of node.children) {
49395
- elements.push(this.renderNode(child));
49869
+ if (footer) {
49870
+ const footerMeasure = this.measureNode(footer, constraints);
49871
+ footerHeight = footerMeasure.height;
49872
+ const footerY = this.pageHeight - padding - footerHeight;
49873
+ const footerBox = this.layoutNode(footer, padding, footerY, constraints);
49874
+ elements.push(this.renderBox(footerBox));
49875
+ }
49876
+ const mainStartY = currentY + headerHeight;
49877
+ const mainEndY = this.pageHeight - padding - footerHeight;
49878
+ const mainHeight = mainEndY - mainStartY;
49879
+ if (otherChildren.length > 0) {
49880
+ const mainConstraints = {
49881
+ maxWidth: contentWidth,
49882
+ maxHeight: mainHeight
49883
+ };
49884
+ const clipId = `main-clip-${this.clipPathCounter++}`;
49885
+ this.clipPathDefs.push(`<clipPath id="${clipId}"><rect x="${padding}" y="${mainStartY}" width="${contentWidth}" height="${mainHeight}"/></clipPath>`);
49886
+ const mainContent = [];
49887
+ mainContent.push(`<g clip-path="url(#${clipId})">`);
49888
+ let childY = mainStartY;
49889
+ const gap = this.getGap(page) || 0;
49890
+ for (const child of otherChildren) {
49891
+ const childBox = this.layoutNode(child, padding, childY, mainConstraints);
49892
+ mainContent.push(this.renderBox(childBox));
49893
+ childY += childBox.height + gap;
49894
+ }
49895
+ mainContent.push(`</g>`);
49896
+ elements.push(mainContent.join("\n"));
49396
49897
  }
49397
49898
  return elements.join("\n");
49398
49899
  }
49399
- /**
49400
- * Render page title
49401
- */
49402
- renderPageTitle(title) {
49403
- const fontSize = 24;
49404
- const y = this.currentY + fontSize;
49405
- this.currentY += fontSize + 16;
49406
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(title)}</text>`;
49900
+ shouldCenterHorizontally(node) {
49901
+ return node.type === "Card" || node.type === "Modal";
49407
49902
  }
49408
- /**
49409
- * Render any AST node
49410
- */
49411
- renderNode(node) {
49903
+ // ===========================================
49904
+ // Measurement Phase
49905
+ // ===========================================
49906
+ measureNode(node, constraints) {
49412
49907
  switch (node.type) {
49413
- // Layout nodes
49414
49908
  case "Row":
49415
- return this.renderRow(node);
49909
+ return this.measureRow(node, constraints);
49416
49910
  case "Col":
49417
- return this.renderCol(node);
49911
+ return this.measureCol(node, constraints);
49418
49912
  case "Header":
49419
- return this.renderHeader(node);
49420
- case "Main":
49421
- return this.renderMain(node);
49913
+ return this.measureHeader(node, constraints);
49422
49914
  case "Footer":
49423
- return this.renderFooter(node);
49915
+ return this.measureFooter(node, constraints);
49916
+ case "Main":
49917
+ return this.measureMain(node, constraints);
49424
49918
  case "Sidebar":
49425
- return this.renderSidebar(node);
49426
- // Container nodes
49919
+ return this.measureSidebar(node, constraints);
49427
49920
  case "Card":
49428
- return this.renderCard(node);
49921
+ return this.measureCard(node, constraints);
49429
49922
  case "Modal":
49430
- return this.renderModal(node);
49431
- // Text nodes
49432
- case "Text":
49433
- return this.renderText(node);
49923
+ return this.measureModal(node, constraints);
49434
49924
  case "Title":
49435
- return this.renderTitle(node);
49436
- case "Link":
49437
- return this.renderLink(node);
49438
- // Input nodes
49925
+ return this.measureTitle(node);
49926
+ case "Text":
49927
+ return this.measureText(node);
49928
+ case "Button":
49929
+ return this.measureButton(node, constraints);
49439
49930
  case "Input":
49440
- return this.renderInput(node);
49931
+ return this.measureInput(node, constraints);
49441
49932
  case "Textarea":
49442
- return this.renderTextarea(node);
49933
+ return this.measureTextarea(node, constraints);
49443
49934
  case "Select":
49444
- return this.renderSelect(node);
49935
+ return this.measureSelect(node, constraints);
49445
49936
  case "Checkbox":
49446
- return this.renderCheckbox(node);
49937
+ return this.measureCheckbox(node);
49447
49938
  case "Radio":
49448
- return this.renderRadio(node);
49939
+ return this.measureRadio(node);
49449
49940
  case "Switch":
49450
- return this.renderSwitch(node);
49451
- // Button
49452
- case "Button":
49453
- return this.renderButton(node);
49454
- // Display nodes
49941
+ return this.measureSwitch(node);
49942
+ case "Link":
49943
+ return this.measureLink(node);
49455
49944
  case "Image":
49456
- return this.renderImage(node);
49945
+ return this.measureImage(node, constraints);
49457
49946
  case "Placeholder":
49458
- return this.renderPlaceholder(node);
49947
+ return this.measurePlaceholder(node, constraints);
49459
49948
  case "Avatar":
49460
- return this.renderAvatar(node);
49949
+ return this.measureAvatar(node, constraints);
49461
49950
  case "Badge":
49462
- return this.renderBadge(node);
49463
- // Data nodes
49951
+ return this.measureBadge(node);
49464
49952
  case "Table":
49465
- return this.renderTable(node);
49953
+ return this.measureTable(node);
49466
49954
  case "List":
49467
- return this.renderList(node);
49468
- // Feedback nodes
49955
+ return this.measureList(node);
49469
49956
  case "Alert":
49470
- return this.renderAlert(node);
49957
+ return this.measureAlert(node, constraints);
49471
49958
  case "Progress":
49472
- return this.renderProgress(node);
49959
+ return this.measureProgress(node, constraints);
49473
49960
  case "Spinner":
49474
- return this.renderSpinner(node);
49475
- // Navigation nodes
49961
+ return this.measureSpinner(node);
49476
49962
  case "Nav":
49477
- return this.renderNav(node);
49963
+ return this.measureNav(node);
49478
49964
  case "Tabs":
49479
- return this.renderTabs(node);
49965
+ return this.measureTabs(node);
49480
49966
  case "Breadcrumb":
49481
- return this.renderBreadcrumb(node);
49967
+ return this.measureBreadcrumb(node);
49968
+ case "Icon":
49969
+ return this.measureIcon(node);
49970
+ case "Divider":
49971
+ return this.measureDivider(node, constraints);
49482
49972
  default:
49483
- return `<!-- Unsupported: ${node.type} -->`;
49973
+ return { width: 100, height: 40 };
49974
+ }
49975
+ }
49976
+ measureRow(node, constraints) {
49977
+ const padding = this.getPadding(node);
49978
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
49979
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
49980
+ const childMeasurements = node.children.map(
49981
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
49982
+ );
49983
+ const maxChildHeight = Math.max(...childMeasurements.map((m) => m.height), 0);
49984
+ const totalChildWidth = childMeasurements.reduce((sum, m) => sum + m.width, 0);
49985
+ const totalGapWidth = Math.max(0, (node.children.length - 1) * gap);
49986
+ const contentWidth = totalChildWidth + totalGapWidth + padding.left + padding.right;
49987
+ const hasExplicitWidth = "w" in node && node.w !== void 0;
49988
+ const width = hasExplicitWidth ? constraints.maxWidth : Math.min(contentWidth, constraints.maxWidth);
49989
+ return {
49990
+ width,
49991
+ height: maxChildHeight + padding.top + padding.bottom
49992
+ };
49993
+ }
49994
+ measureCol(node, constraints) {
49995
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
49996
+ const padding = this.getPadding(node);
49997
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
49998
+ const childMeasurements = node.children.map(
49999
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50000
+ );
50001
+ const totalHeight = childMeasurements.reduce(
50002
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50003
+ 0
50004
+ );
50005
+ const maxChildWidth = Math.max(...childMeasurements.map((m) => m.width), 0);
50006
+ return {
50007
+ width: Math.max(maxChildWidth + padding.left + padding.right, constraints.maxWidth),
50008
+ height: totalHeight + padding.top + padding.bottom
50009
+ };
50010
+ }
50011
+ measureHeader(node, constraints) {
50012
+ const padding = this.getPadding(node);
50013
+ const height = this.resolveSize(node.h) || 56;
50014
+ return {
50015
+ width: constraints.maxWidth,
50016
+ height: height + padding.top + padding.bottom
50017
+ };
50018
+ }
50019
+ measureFooter(node, constraints) {
50020
+ const padding = this.getPadding(node);
50021
+ const height = this.resolveSize(node.h) || 60;
50022
+ return {
50023
+ width: constraints.maxWidth,
50024
+ height: height + padding.top + padding.bottom
50025
+ };
50026
+ }
50027
+ measureMain(node, constraints) {
50028
+ const padding = this.getPadding(node);
50029
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50030
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50031
+ const childMeasurements = node.children.map(
50032
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50033
+ );
50034
+ const totalHeight = childMeasurements.reduce(
50035
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50036
+ 0
50037
+ );
50038
+ return {
50039
+ width: constraints.maxWidth,
50040
+ height: totalHeight + padding.top + padding.bottom
50041
+ };
50042
+ }
50043
+ measureSidebar(node, constraints) {
50044
+ const width = this.resolveSize(node.w) || 200;
50045
+ const padding = this.getPadding(node);
50046
+ const gap = this.getGap(node) ?? 8;
50047
+ const innerWidth = width - padding.left - padding.right;
50048
+ const childMeasurements = node.children.map(
50049
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50050
+ );
50051
+ const contentHeight = childMeasurements.reduce(
50052
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50053
+ 0
50054
+ ) + padding.top + padding.bottom;
50055
+ const explicitHeight = this.resolveSize(node.h);
50056
+ const height = explicitHeight || constraints.maxHeight || contentHeight;
50057
+ return { width, height };
50058
+ }
50059
+ measureCard(node, constraints) {
50060
+ let cardWidth = this.resolveSize(node.w);
50061
+ if (!cardWidth) {
50062
+ cardWidth = Math.min(360, constraints.maxWidth);
50063
+ } else {
50064
+ cardWidth = Math.min(cardWidth, constraints.maxWidth);
50065
+ }
50066
+ const explicitHeight = this.resolveSize(node.h);
50067
+ if (explicitHeight) {
50068
+ return { width: cardWidth, height: explicitHeight };
50069
+ }
50070
+ const padding = this.getPadding(node);
50071
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50072
+ const innerWidth = cardWidth - padding.left - padding.right;
50073
+ const childMeasurements = node.children.map(
50074
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50075
+ );
50076
+ let contentHeight = 0;
50077
+ if (node.title) {
50078
+ contentHeight += 28;
50079
+ }
50080
+ contentHeight += childMeasurements.reduce(
50081
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50082
+ 0
50083
+ );
50084
+ return {
50085
+ width: cardWidth,
50086
+ height: contentHeight + padding.top + padding.bottom
50087
+ };
50088
+ }
50089
+ measureModal(node, constraints) {
50090
+ const width = this.resolveSize(node.w) || 400;
50091
+ const padding = this.getPadding(node);
50092
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50093
+ const innerWidth = width - padding.left - padding.right;
50094
+ const childMeasurements = node.children.map(
50095
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50096
+ );
50097
+ let contentHeight = node.title ? 40 : 0;
50098
+ contentHeight += childMeasurements.reduce(
50099
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50100
+ 0
50101
+ );
50102
+ return {
50103
+ width,
50104
+ height: contentHeight + padding.top + padding.bottom
50105
+ };
50106
+ }
50107
+ measureTitle(node) {
50108
+ const level = node.level || 1;
50109
+ const fontSize = this.getTitleFontSize(level);
50110
+ const textWidth = this.estimateTextWidth(node.content, fontSize);
50111
+ return { width: textWidth, height: fontSize + 8 };
50112
+ }
50113
+ measureText(node) {
50114
+ const fontSize = this.resolveFontSize(node.size);
50115
+ const textWidth = this.estimateTextWidth(node.content, fontSize);
50116
+ return { width: textWidth, height: fontSize + 8 };
50117
+ }
50118
+ measureButton(node, constraints) {
50119
+ const hasIcon = !!node.icon;
50120
+ const isIconOnly = hasIcon && !node.content.trim();
50121
+ const iconSize = 16;
50122
+ const padding = 16;
50123
+ let width;
50124
+ if (this.isFullWidth(node)) {
50125
+ width = constraints.maxWidth;
50126
+ } else if (isIconOnly) {
50127
+ width = iconSize + padding * 2;
50128
+ } else if (hasIcon) {
50129
+ width = Math.max(80, this.estimateTextWidth(node.content, 14) + iconSize + 48);
50130
+ } else {
50131
+ width = Math.max(80, this.estimateTextWidth(node.content, 14) + 32);
50132
+ }
50133
+ return { width, height: 40 };
50134
+ }
50135
+ measureInput(node, constraints) {
50136
+ let width;
50137
+ if (this.resolveSize(node.w)) {
50138
+ width = this.resolveSize(node.w);
50139
+ } else if (constraints.inHeader) {
50140
+ const placeholderWidth = node.placeholder ? this.estimateTextWidth(node.placeholder, 14) : 60;
50141
+ width = Math.max(120, placeholderWidth + 24 + 100);
50142
+ } else {
50143
+ width = constraints.maxWidth;
50144
+ }
50145
+ let height = 36;
50146
+ if (node.label) height += 24;
50147
+ return { width, height };
50148
+ }
50149
+ measureTextarea(node, constraints) {
50150
+ const width = this.resolveSize(node.w) || constraints.maxWidth;
50151
+ let height = node.rows ? node.rows * 24 : 100;
50152
+ if (node.label) height += 24;
50153
+ return { width, height };
50154
+ }
50155
+ measureSelect(node, constraints) {
50156
+ const width = this.resolveSize(node.w) || constraints.maxWidth;
50157
+ let height = 40;
50158
+ if (node.label) height += 24;
50159
+ return { width, height };
50160
+ }
50161
+ measureCheckbox(node) {
50162
+ const labelWidth = node.label ? this.estimateTextWidth(node.label, 14) + 8 : 0;
50163
+ return { width: 18 + labelWidth, height: 24 };
50164
+ }
50165
+ measureRadio(node) {
50166
+ const labelWidth = node.label ? this.estimateTextWidth(node.label, 14) + 8 : 0;
50167
+ return { width: 18 + labelWidth, height: 24 };
50168
+ }
50169
+ measureSwitch(node) {
50170
+ const labelWidth = node.label ? this.estimateTextWidth(node.label, 14) + 8 : 0;
50171
+ return { width: 44 + labelWidth, height: 28 };
50172
+ }
50173
+ measureLink(node) {
50174
+ const fontSize = 14;
50175
+ const textWidth = this.estimateTextWidth(node.content, fontSize);
50176
+ return { width: textWidth, height: fontSize + 8 };
50177
+ }
50178
+ measureImage(node, constraints) {
50179
+ let width;
50180
+ if (this.isFullWidth(node)) {
50181
+ width = constraints.maxWidth;
50182
+ } else {
50183
+ width = this.resolveSize(node.w) || 200;
50184
+ }
50185
+ const height = this.resolveSize(node.h) || 150;
50186
+ return { width, height };
50187
+ }
50188
+ measurePlaceholder(node, constraints) {
50189
+ let width;
50190
+ if (node.w !== void 0) {
50191
+ width = this.isFullWidth(node) ? constraints.maxWidth : this.resolveSize(node.w) || 200;
50192
+ } else {
50193
+ width = constraints.maxWidth || 200;
50194
+ }
50195
+ const height = this.resolveSize(node.h) || 100;
50196
+ return { width, height };
50197
+ }
50198
+ measureAvatar(node, constraints) {
50199
+ const sizes = { xs: 24, sm: 32, md: 40, lg: 48, xl: 64 };
50200
+ const defaultSize = constraints?.inHeader ? "sm" : "md";
50201
+ const size = sizes[node.size || defaultSize] || 40;
50202
+ return { width: size, height: size };
50203
+ }
50204
+ measureBadge(node) {
50205
+ const width = Math.max(24, this.estimateTextWidth(node.content, 12) + 16);
50206
+ return { width, height: 22 };
50207
+ }
50208
+ measureTable(node) {
50209
+ const columns = node.columns || [];
50210
+ const rows = node.rows || [];
50211
+ const rowCount = rows.length || 3;
50212
+ const colWidth = 120;
50213
+ const rowHeight = 40;
50214
+ return {
50215
+ width: columns.length * colWidth,
50216
+ height: (rowCount + 1) * rowHeight
50217
+ };
50218
+ }
50219
+ measureList(node) {
50220
+ const items = node.items || [];
50221
+ const maxItemWidth = Math.max(...items.map((item) => {
50222
+ const content = typeof item === "string" ? item : item.content;
50223
+ return this.estimateTextWidth(content, 14);
50224
+ }), 100);
50225
+ return { width: maxItemWidth + 24, height: items.length * 28 };
50226
+ }
50227
+ measureAlert(_node, constraints) {
50228
+ const width = Math.min(400, constraints.maxWidth);
50229
+ return { width, height: 48 };
50230
+ }
50231
+ measureProgress(node, constraints) {
50232
+ const width = Math.min(200, constraints.maxWidth);
50233
+ let height = 8;
50234
+ if (node.label) height += 24;
50235
+ return { width, height };
50236
+ }
50237
+ measureSpinner(node) {
50238
+ const sizes = { xs: 16, sm: 20, md: 24, lg: 32, xl: 40 };
50239
+ const size = sizes[node.size || "md"] || 24;
50240
+ return { width: size, height: size };
50241
+ }
50242
+ measureNav(node) {
50243
+ const items = node.items || [];
50244
+ if (node.vertical) {
50245
+ const maxWidth = Math.max(...items.map((item) => {
50246
+ const label = typeof item === "string" ? item : item.label;
50247
+ return this.estimateTextWidth(label, 14);
50248
+ }), 100);
50249
+ return { width: maxWidth, height: items.length * 32 };
50250
+ } else {
50251
+ const totalWidth = items.reduce((sum, item) => {
50252
+ const label = typeof item === "string" ? item : item.label;
50253
+ return sum + this.estimateTextWidth(label, 14) + 24;
50254
+ }, 0);
50255
+ return { width: totalWidth, height: 32 };
49484
50256
  }
49485
50257
  }
50258
+ measureTabs(node) {
50259
+ const items = node.items || [];
50260
+ const totalWidth = items.reduce((sum, item) => {
50261
+ const label = typeof item === "string" ? item : item;
50262
+ return sum + this.estimateTextWidth(label, 14) + 32;
50263
+ }, 0);
50264
+ return { width: totalWidth, height: 44 };
50265
+ }
50266
+ measureBreadcrumb(node) {
50267
+ const items = node.items || [];
50268
+ const totalWidth = items.reduce((sum, item, idx) => {
50269
+ const label = typeof item === "string" ? item : item.label;
50270
+ return sum + this.estimateTextWidth(label, 14) + (idx < items.length - 1 ? 24 : 0);
50271
+ }, 0);
50272
+ return { width: totalWidth, height: 28 };
50273
+ }
50274
+ measureIcon(node) {
50275
+ const sizes = { xs: 12, sm: 16, md: 20, lg: 24, xl: 32 };
50276
+ const size = sizes[node.size || "md"] || 20;
50277
+ return { width: size, height: size };
50278
+ }
50279
+ measureDivider(_node, constraints) {
50280
+ return { width: constraints.maxWidth, height: 1 };
50281
+ }
49486
50282
  // ===========================================
49487
- // Layout Renderers
50283
+ // Layout Phase
49488
50284
  // ===========================================
49489
- renderRow(node) {
49490
- const savedX = this.currentX;
49491
- const savedY = this.currentY;
49492
- const elements = [];
49493
- const totalSpan = node.children.reduce((sum, child) => {
49494
- if ("span" in child && typeof child.span === "number") {
49495
- return sum + child.span;
50285
+ layoutNode(node, x, y, constraints) {
50286
+ switch (node.type) {
50287
+ case "Row":
50288
+ return this.layoutRow(node, x, y, constraints);
50289
+ case "Col":
50290
+ return this.layoutCol(node, x, y, constraints);
50291
+ case "Header":
50292
+ return this.layoutHeader(node, x, y, constraints);
50293
+ case "Footer":
50294
+ return this.layoutFooter(node, x, y, constraints);
50295
+ case "Main":
50296
+ return this.layoutMain(node, x, y, constraints);
50297
+ case "Sidebar":
50298
+ return this.layoutSidebar(node, x, y, constraints);
50299
+ case "Card":
50300
+ return this.layoutCard(node, x, y, constraints);
50301
+ default:
50302
+ const size = this.measureNode(node, constraints);
50303
+ return {
50304
+ x,
50305
+ y,
50306
+ width: size.width,
50307
+ height: size.height,
50308
+ node,
50309
+ children: []
50310
+ };
50311
+ }
50312
+ }
50313
+ layoutRow(node, x, y, constraints) {
50314
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50315
+ const padding = this.getPadding(node);
50316
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50317
+ const justify = node.justify || "start";
50318
+ const align = node.align || (constraints.inHeader ? "center" : "start");
50319
+ const childMeasurements = node.children.map(
50320
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50321
+ );
50322
+ const flexItems = node.children.map((child, i) => {
50323
+ const measurement = childMeasurements[i];
50324
+ if ("w" in child && child.w !== void 0 && child.w !== "full") {
50325
+ const resolved = this.resolveSize(child.w);
50326
+ if (resolved !== void 0) {
50327
+ return createFlexItemProps(measurement.height, {
50328
+ basis: resolved,
50329
+ grow: 0,
50330
+ shrink: 0,
50331
+ minSize: resolved,
50332
+ maxSize: resolved,
50333
+ contentSize: measurement.width
50334
+ });
50335
+ }
49496
50336
  }
49497
- return sum + 1;
49498
- }, 0);
49499
- const colWidth = this.contentWidth / Math.max(totalSpan, 1);
49500
- let maxHeight = 0;
50337
+ if (this.isFlexContainer(child)) {
50338
+ return createFlexGrowItemProps(measurement.height, {
50339
+ basis: 0,
50340
+ grow: 1,
50341
+ shrink: 1,
50342
+ minSize: 0,
50343
+ maxSize: Infinity,
50344
+ contentSize: measurement.width
50345
+ });
50346
+ }
50347
+ if (constraints.inHeader && child.type === "Input") {
50348
+ return createFlexGrowItemProps(measurement.height, {
50349
+ basis: measurement.width,
50350
+ // auto = content size
50351
+ grow: 1,
50352
+ shrink: 1,
50353
+ minSize: 120,
50354
+ // min-width: 120px
50355
+ maxSize: Infinity,
50356
+ contentSize: measurement.width
50357
+ });
50358
+ }
50359
+ return createFlexItemProps(measurement.height, {
50360
+ basis: measurement.width,
50361
+ // auto = content size
50362
+ grow: 0,
50363
+ shrink: 1,
50364
+ minSize: 0,
50365
+ maxSize: Infinity,
50366
+ contentSize: measurement.width
50367
+ });
50368
+ });
50369
+ const flexConfig = {
50370
+ mainSize: innerWidth,
50371
+ crossSize: void 0,
50372
+ // Will use max child height
50373
+ direction: "row",
50374
+ justifyContent: toJustifyContent(justify),
50375
+ alignItems: toAlignItems(align),
50376
+ gap
50377
+ };
50378
+ const flexResult = computeFlexLayout(flexItems, flexConfig);
50379
+ const maxChildHeight = Math.max(...childMeasurements.map((m) => m.height), 0);
50380
+ const innerMaxHeight = constraints.maxHeight ? constraints.maxHeight - padding.top - padding.bottom : void 0;
50381
+ const effectiveHeight = innerMaxHeight || maxChildHeight;
50382
+ const rowHeight = effectiveHeight + padding.top + padding.bottom;
50383
+ const children = [];
50384
+ for (let i = 0; i < node.children.length; i++) {
50385
+ const child = node.children[i];
50386
+ const flexItem = flexResult.items[i];
50387
+ const measurement = childMeasurements[i];
50388
+ const childX = x + padding.left + flexItem.mainPosition;
50389
+ let childY = y + padding.top;
50390
+ switch (align) {
50391
+ case "center":
50392
+ childY = y + padding.top + (maxChildHeight - measurement.height) / 2;
50393
+ break;
50394
+ case "end":
50395
+ childY = y + padding.top + (maxChildHeight - measurement.height);
50396
+ break;
50397
+ }
50398
+ const childBox = this.layoutNode(child, childX, childY, {
50399
+ ...constraints,
50400
+ maxWidth: flexItem.mainSize,
50401
+ maxHeight: effectiveHeight
50402
+ });
50403
+ children.push(childBox);
50404
+ }
50405
+ return {
50406
+ x,
50407
+ y,
50408
+ width: constraints.maxWidth,
50409
+ height: rowHeight,
50410
+ node,
50411
+ children,
50412
+ padding
50413
+ };
50414
+ }
50415
+ layoutCol(node, x, y, constraints) {
50416
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50417
+ const padding = this.getPadding(node);
50418
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50419
+ const innerHeight = constraints.maxHeight ? constraints.maxHeight - padding.top - padding.bottom : void 0;
50420
+ const align = node.align || "stretch";
50421
+ const childMeasurements = node.children.map(
50422
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50423
+ );
50424
+ const flexItems = node.children.map((child, i) => {
50425
+ const measurement = childMeasurements[i];
50426
+ if ("h" in child && child.h !== void 0) {
50427
+ const resolved = this.resolveSize(child.h);
50428
+ if (resolved !== void 0) {
50429
+ return createFlexItemProps(measurement.width, {
50430
+ basis: resolved,
50431
+ grow: 0,
50432
+ shrink: 0,
50433
+ minSize: resolved,
50434
+ maxSize: resolved,
50435
+ contentSize: measurement.height
50436
+ });
50437
+ }
50438
+ }
50439
+ if (child.type === "Row" && this.rowContainsSidebarOrMain(child)) {
50440
+ return createFlexGrowItemProps(measurement.width, {
50441
+ basis: 0,
50442
+ grow: 1,
50443
+ shrink: 1,
50444
+ minSize: 0,
50445
+ maxSize: Infinity,
50446
+ contentSize: measurement.height
50447
+ });
50448
+ }
50449
+ return createFlexItemProps(measurement.width, {
50450
+ basis: measurement.height,
50451
+ // auto = content size
50452
+ grow: 0,
50453
+ shrink: 1,
50454
+ minSize: 0,
50455
+ maxSize: Infinity,
50456
+ contentSize: measurement.height
50457
+ });
50458
+ });
50459
+ const flexConfig = {
50460
+ mainSize: innerHeight ?? childMeasurements.reduce(
50461
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50462
+ 0
50463
+ ),
50464
+ crossSize: innerWidth,
50465
+ direction: "column",
50466
+ justifyContent: "flex-start",
50467
+ alignItems: toAlignItems(align),
50468
+ gap
50469
+ };
50470
+ const flexResult = computeFlexLayout(flexItems, flexConfig);
50471
+ const children = [];
50472
+ for (let i = 0; i < node.children.length; i++) {
50473
+ const child = node.children[i];
50474
+ const flexItem = flexResult.items[i];
50475
+ const measurement = childMeasurements[i];
50476
+ let childX = x + padding.left;
50477
+ let childWidth = align === "stretch" ? innerWidth : measurement.width;
50478
+ switch (align) {
50479
+ case "center":
50480
+ childX = x + padding.left + (innerWidth - measurement.width) / 2;
50481
+ break;
50482
+ case "end":
50483
+ childX = x + padding.left + (innerWidth - measurement.width);
50484
+ break;
50485
+ }
50486
+ const childY = y + padding.top + flexItem.mainPosition;
50487
+ const childBox = this.layoutNode(child, childX, childY, {
50488
+ ...constraints,
50489
+ maxWidth: childWidth,
50490
+ maxHeight: flexItem.mainSize
50491
+ });
50492
+ children.push(childBox);
50493
+ }
50494
+ const totalHeight = flexResult.mainSizeUsed;
50495
+ return {
50496
+ x,
50497
+ y,
50498
+ width: constraints.maxWidth,
50499
+ height: totalHeight + padding.top + padding.bottom,
50500
+ node,
50501
+ children,
50502
+ padding
50503
+ };
50504
+ }
50505
+ rowContainsSidebarOrMain(row) {
50506
+ return row.children.some(
50507
+ (child) => child.type === "Sidebar" || child.type === "Main"
50508
+ );
50509
+ }
50510
+ layoutHeader(node, x, y, constraints) {
50511
+ const padding = this.getPadding(node);
50512
+ const height = this.resolveSize(node.h) || 56;
50513
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50514
+ const children = [];
50515
+ let currentY = y + padding.top;
49501
50516
  for (const child of node.children) {
49502
- const span = "span" in child && typeof child.span === "number" ? child.span : 1;
49503
- const childWidth = colWidth * span;
49504
- const startY = this.currentY;
49505
- elements.push(this.renderNode(child));
49506
- const childHeight = this.currentY - startY;
49507
- maxHeight = Math.max(maxHeight, childHeight);
49508
- this.currentX += childWidth;
49509
- this.currentY = savedY;
50517
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50518
+ ...constraints,
50519
+ maxWidth: innerWidth,
50520
+ maxHeight: void 0,
50521
+ // Children use natural height
50522
+ inHeader: true
50523
+ // Pass header context for child sizing (e.g., smaller avatars)
50524
+ });
50525
+ const verticalOffset = (height - childBox.height) / 2;
50526
+ childBox.y = y + padding.top + verticalOffset;
50527
+ this.adjustChildrenY(childBox, verticalOffset);
50528
+ children.push(childBox);
50529
+ currentY += childBox.height;
49510
50530
  }
49511
- this.currentX = savedX;
49512
- this.currentY = savedY + maxHeight;
49513
- return elements.join("\n");
50531
+ return {
50532
+ x,
50533
+ y,
50534
+ width: constraints.maxWidth,
50535
+ height: height + padding.top + padding.bottom,
50536
+ node,
50537
+ children,
50538
+ padding
50539
+ };
49514
50540
  }
49515
- renderCol(node) {
49516
- return node.children.map((child) => this.renderNode(child)).join("\n");
50541
+ adjustChildrenY(box, offset) {
50542
+ for (const child of box.children) {
50543
+ child.y += offset;
50544
+ this.adjustChildrenY(child, offset);
50545
+ }
49517
50546
  }
49518
- renderHeader(node) {
49519
- const height = 60;
49520
- const x = this.currentX;
49521
- const y = this.currentY;
49522
- const savedY = this.currentY;
49523
- this.currentY += 16;
49524
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49525
- this.currentY = savedY + height + 8;
49526
- return `
49527
- <g transform="translate(${x}, ${y})">
49528
- <rect width="${this.contentWidth}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49529
- ${children}
49530
- </g>`;
50547
+ /**
50548
+ * Check if a node should flex to fill available space in a Row
50549
+ * Only layout containers without explicit width should flex
50550
+ */
50551
+ isFlexContainer(node) {
50552
+ if (node.type === "Main") return true;
50553
+ if (node.type === "Col" && !("w" in node && node.w !== void 0)) return true;
50554
+ return false;
49531
50555
  }
49532
- renderMain(node) {
49533
- return node.children.map((c) => this.renderNode(c)).join("\n");
50556
+ layoutFooter(node, x, y, constraints) {
50557
+ const padding = this.getPadding(node);
50558
+ const height = this.resolveSize(node.h) || 60;
50559
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50560
+ const children = [];
50561
+ let currentY = y + padding.top;
50562
+ for (const child of node.children) {
50563
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50564
+ ...constraints,
50565
+ maxWidth: innerWidth,
50566
+ maxHeight: void 0
50567
+ // Children use natural height
50568
+ });
50569
+ const verticalOffset = (height - childBox.height) / 2;
50570
+ childBox.y = y + padding.top + verticalOffset;
50571
+ this.adjustChildrenY(childBox, verticalOffset);
50572
+ children.push(childBox);
50573
+ currentY += childBox.height;
50574
+ }
50575
+ return {
50576
+ x,
50577
+ y,
50578
+ width: constraints.maxWidth,
50579
+ height: height + padding.top + padding.bottom,
50580
+ node,
50581
+ children,
50582
+ padding
50583
+ };
49534
50584
  }
49535
- renderFooter(node) {
49536
- const height = 60;
49537
- const x = this.currentX;
49538
- const y = this.currentY;
49539
- const savedY = this.currentY;
49540
- this.currentY += 16;
49541
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49542
- this.currentY = savedY + height + 8;
49543
- return `
49544
- <g transform="translate(${x}, ${y})">
49545
- <rect width="${this.contentWidth}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49546
- ${children}
49547
- </g>`;
50585
+ layoutMain(node, x, y, constraints) {
50586
+ const padding = this.getPadding(node);
50587
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50588
+ const innerWidth = constraints.maxWidth - padding.left - padding.right;
50589
+ const childMeasurements = node.children.map(
50590
+ (child) => this.measureNode(child, { ...constraints, maxWidth: innerWidth })
50591
+ );
50592
+ const children = [];
50593
+ let currentY = y + padding.top;
50594
+ for (let i = 0; i < node.children.length; i++) {
50595
+ const child = node.children[i];
50596
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50597
+ ...constraints,
50598
+ maxWidth: innerWidth
50599
+ });
50600
+ children.push(childBox);
50601
+ currentY += childBox.height + gap;
50602
+ }
50603
+ const totalHeight = childMeasurements.reduce(
50604
+ (sum, m, i) => sum + m.height + (i > 0 ? gap : 0),
50605
+ 0
50606
+ );
50607
+ return {
50608
+ x,
50609
+ y,
50610
+ width: constraints.maxWidth,
50611
+ height: totalHeight + padding.top + padding.bottom,
50612
+ node,
50613
+ children,
50614
+ padding
50615
+ };
49548
50616
  }
49549
- renderSidebar(node) {
49550
- const width = 200;
49551
- const height = 300;
49552
- const x = this.currentX;
49553
- const y = this.currentY;
49554
- const savedY = this.currentY;
49555
- this.currentY += 16;
49556
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49557
- this.currentY = savedY + height + 8;
49558
- return `
49559
- <g transform="translate(${x}, ${y})">
49560
- <rect width="${width}" height="${height}" fill="${this.theme.colors.background}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49561
- ${children}
49562
- </g>`;
50617
+ layoutSidebar(node, x, y, constraints) {
50618
+ const width = this.resolveSize(node.w) || 200;
50619
+ const padding = this.getPadding(node);
50620
+ const gap = this.getGap(node) ?? 8;
50621
+ const innerWidth = width - padding.left - padding.right;
50622
+ const children = [];
50623
+ let currentY = y + padding.top;
50624
+ for (const child of node.children) {
50625
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50626
+ ...constraints,
50627
+ maxWidth: innerWidth
50628
+ });
50629
+ children.push(childBox);
50630
+ currentY += childBox.height + gap;
50631
+ }
50632
+ const contentHeight = currentY - y - padding.top - gap + padding.bottom;
50633
+ const height = constraints.maxHeight || Math.max(contentHeight, 200);
50634
+ return {
50635
+ x,
50636
+ y,
50637
+ width,
50638
+ height,
50639
+ node,
50640
+ children,
50641
+ padding
50642
+ };
50643
+ }
50644
+ layoutCard(node, x, y, constraints) {
50645
+ let cardWidth = this.resolveSize(node.w);
50646
+ if (!cardWidth) {
50647
+ cardWidth = Math.min(360, constraints.maxWidth);
50648
+ }
50649
+ const padding = this.getPadding(node);
50650
+ const gap = this.getGap(node) ?? this.DEFAULT_GAP;
50651
+ const innerWidth = cardWidth - padding.left - padding.right;
50652
+ const children = [];
50653
+ let currentY = y + padding.top;
50654
+ if (node.title) {
50655
+ currentY += 28;
50656
+ }
50657
+ for (const child of node.children) {
50658
+ const childBox = this.layoutNode(child, x + padding.left, currentY, {
50659
+ ...constraints,
50660
+ maxWidth: innerWidth
50661
+ });
50662
+ children.push(childBox);
50663
+ currentY += childBox.height + gap;
50664
+ }
50665
+ const totalHeight = currentY - y - gap + padding.bottom;
50666
+ return {
50667
+ x,
50668
+ y,
50669
+ width: cardWidth,
50670
+ height: totalHeight,
50671
+ node,
50672
+ children,
50673
+ padding
50674
+ };
49563
50675
  }
49564
50676
  // ===========================================
49565
- // Container Renderers
50677
+ // Render Phase
49566
50678
  // ===========================================
49567
- renderCard(node) {
49568
- const width = Math.min(300, this.contentWidth);
49569
- const x = this.currentX;
49570
- const y = this.currentY;
49571
- const savedY = this.currentY;
49572
- this.currentY += 16;
49573
- let titleSvg = "";
50679
+ renderBox(box) {
50680
+ if (!box.node) return "";
50681
+ switch (box.node.type) {
50682
+ case "Row":
50683
+ return this.renderRowBox(box);
50684
+ case "Col":
50685
+ return this.renderColBox(box);
50686
+ case "Header":
50687
+ return this.renderHeaderBox(box);
50688
+ case "Footer":
50689
+ return this.renderFooterBox(box);
50690
+ case "Main":
50691
+ return this.renderMainBox(box);
50692
+ case "Sidebar":
50693
+ return this.renderSidebarBox(box);
50694
+ case "Card":
50695
+ return this.renderCardBox(box);
50696
+ case "Modal":
50697
+ return this.renderModalBox(box);
50698
+ case "Title":
50699
+ return this.renderTitleBox(box);
50700
+ case "Text":
50701
+ return this.renderTextBox(box);
50702
+ case "Button":
50703
+ return this.renderButtonBox(box);
50704
+ case "Input":
50705
+ return this.renderInputBox(box);
50706
+ case "Textarea":
50707
+ return this.renderTextareaBox(box);
50708
+ case "Select":
50709
+ return this.renderSelectBox(box);
50710
+ case "Checkbox":
50711
+ return this.renderCheckboxBox(box);
50712
+ case "Radio":
50713
+ return this.renderRadioBox(box);
50714
+ case "Switch":
50715
+ return this.renderSwitchBox(box);
50716
+ case "Link":
50717
+ return this.renderLinkBox(box);
50718
+ case "Image":
50719
+ return this.renderImageBox(box);
50720
+ case "Placeholder":
50721
+ return this.renderPlaceholderBox(box);
50722
+ case "Avatar":
50723
+ return this.renderAvatarBox(box);
50724
+ case "Badge":
50725
+ return this.renderBadgeBox(box);
50726
+ case "Table":
50727
+ return this.renderTableBox(box);
50728
+ case "List":
50729
+ return this.renderListBox(box);
50730
+ case "Alert":
50731
+ return this.renderAlertBox(box);
50732
+ case "Progress":
50733
+ return this.renderProgressBox(box);
50734
+ case "Spinner":
50735
+ return this.renderSpinnerBox(box);
50736
+ case "Nav":
50737
+ return this.renderNavBox(box);
50738
+ case "Tabs":
50739
+ return this.renderTabsBox(box);
50740
+ case "Breadcrumb":
50741
+ return this.renderBreadcrumbBox(box);
50742
+ case "Icon":
50743
+ return this.renderIconBox(box);
50744
+ case "Divider":
50745
+ return this.renderDividerBox(box);
50746
+ default:
50747
+ return `<!-- Unsupported: ${box.node.type} -->`;
50748
+ }
50749
+ }
50750
+ renderRowBox(box) {
50751
+ const childrenSvg = box.children.map((child) => this.renderBox(child)).join("\n");
50752
+ return childrenSvg;
50753
+ }
50754
+ renderColBox(box) {
50755
+ const childrenSvg = box.children.map((child) => this.renderBox(child)).join("\n");
50756
+ return childrenSvg;
50757
+ }
50758
+ renderHeaderBox(box) {
50759
+ const node = box.node;
50760
+ const hasBorder = node.border !== false;
50761
+ let svg = `<g>
50762
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="${this.theme.colors.background}"/>`;
50763
+ if (hasBorder) {
50764
+ 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"/>`;
50765
+ }
50766
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50767
+ svg += `</g>`;
50768
+ return svg;
50769
+ }
50770
+ renderFooterBox(box) {
50771
+ const node = box.node;
50772
+ const hasBorder = node.border !== false;
50773
+ let svg = `<g>
50774
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="${this.theme.colors.background}"`;
50775
+ if (hasBorder) {
50776
+ svg += ` stroke="${this.theme.colors.border}" stroke-width="1"`;
50777
+ }
50778
+ svg += `/>`;
50779
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50780
+ svg += `</g>`;
50781
+ return svg;
50782
+ }
50783
+ renderMainBox(box) {
50784
+ const childrenSvg = box.children.map((child) => this.renderBox(child)).join("\n");
50785
+ return childrenSvg;
50786
+ }
50787
+ renderSidebarBox(box) {
50788
+ let svg = `<g>
50789
+ <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"/>`;
50790
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50791
+ svg += `</g>`;
50792
+ return svg;
50793
+ }
50794
+ renderCardBox(box) {
50795
+ const node = box.node;
50796
+ const shadowAttr = node.shadow ? 'filter="url(#shadow)"' : "";
50797
+ let svg = `<g>
50798
+ <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}/>`;
49574
50799
  if (node.title) {
49575
- const titleFontSize = 16;
49576
- titleSvg = `<text x="16" y="${titleFontSize + 12}" font-size="${titleFontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
49577
- this.currentY += titleFontSize + 8;
50800
+ const padding = this.getPadding(node);
50801
+ 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>`;
49578
50802
  }
49579
- const childStartY = this.currentY - savedY;
49580
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49581
- const contentHeight = Math.max(this.currentY - savedY, 100);
49582
- this.currentY = savedY + contentHeight + 16;
49583
- return `
49584
- <g transform="translate(${x}, ${y})">
49585
- <rect width="${width}" height="${contentHeight}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49586
- ${titleSvg}
49587
- <g transform="translate(16, ${childStartY})">
49588
- ${children}
49589
- </g>
49590
- </g>`;
50803
+ svg += box.children.map((child) => this.renderBox(child)).join("\n");
50804
+ svg += `</g>`;
50805
+ return svg;
49591
50806
  }
49592
- renderModal(node) {
49593
- const width = 400;
49594
- const height = 300;
49595
- const x = (this.options.width - width) / 2;
49596
- const y = (this.options.height - height) / 2;
49597
- let titleSvg = "";
50807
+ renderModalBox(box) {
50808
+ const node = box.node;
50809
+ const modalX = (this.pageWidth - box.width) / 2;
50810
+ const modalY = (this.pageHeight - box.height) / 2;
50811
+ let svg = `<g>
50812
+ <rect width="${this.pageWidth}" height="${this.pageHeight}" fill="rgba(0,0,0,0.5)"/>
50813
+ <rect x="${modalX}" y="${modalY}" width="${box.width}" height="${box.height}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49598
50814
  if (node.title) {
49599
- titleSvg = `<text x="20" y="30" font-size="18" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
50815
+ svg += `<text x="${modalX + 20}" y="${modalY + 30}" font-size="18" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
49600
50816
  }
49601
- const savedX = this.currentX;
49602
- const savedY = this.currentY;
49603
- this.currentX = 20;
49604
- this.currentY = 50;
49605
- const children = node.children.map((c) => this.renderNode(c)).join("\n");
49606
- this.currentX = savedX;
49607
- this.currentY = savedY;
49608
- return `
49609
- <g>
49610
- <rect width="100%" height="100%" fill="rgba(0,0,0,0.5)" opacity="0.5"/>
49611
- <g transform="translate(${x}, ${y})">
49612
- <rect width="${width}" height="${height}" rx="8" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49613
- ${titleSvg}
49614
- ${children}
49615
- </g>
49616
- </g>`;
50817
+ for (const child of box.children) {
50818
+ const adjustedChild = { ...child, x: modalX + (child.x - box.x), y: modalY + (child.y - box.y) };
50819
+ svg += this.renderBox(adjustedChild);
50820
+ }
50821
+ svg += `</g>`;
50822
+ return svg;
49617
50823
  }
49618
- // ===========================================
49619
- // Text Renderers
49620
- // ===========================================
49621
- renderText(node) {
50824
+ renderTitleBox(box) {
50825
+ const node = box.node;
50826
+ const level = node.level || 1;
50827
+ const fontSize = this.getTitleFontSize(level);
50828
+ const textY = box.y + fontSize;
50829
+ let textX = box.x;
50830
+ let anchor = "start";
50831
+ if (node.align === "center") {
50832
+ textX = box.x + box.width / 2;
50833
+ anchor = "middle";
50834
+ } else if (node.align === "right") {
50835
+ textX = box.x + box.width;
50836
+ anchor = "end";
50837
+ }
50838
+ 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>`;
50839
+ }
50840
+ renderTextBox(box) {
50841
+ const node = box.node;
49622
50842
  const fontSize = this.resolveFontSize(node.size);
49623
50843
  const fill = node.muted ? this.theme.colors.muted : this.theme.colors.foreground;
49624
50844
  const fontWeight = node.weight || "normal";
49625
- const y = this.currentY + fontSize;
49626
- this.currentY += fontSize + 8;
49627
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${fill}">${this.escapeXml(node.content)}</text>`;
49628
- }
49629
- renderTitle(node) {
49630
- const level = node.level || 1;
49631
- const fontSize = this.getTitleFontSize(level);
49632
- const y = this.currentY + fontSize;
49633
- this.currentY += fontSize + 12;
49634
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>`;
50845
+ const textY = box.y + fontSize;
50846
+ let textX = box.x;
50847
+ let anchor = "start";
50848
+ if (node.align === "center") {
50849
+ textX = box.x + box.width / 2;
50850
+ anchor = "middle";
50851
+ } else if (node.align === "right") {
50852
+ textX = box.x + box.width;
50853
+ anchor = "end";
50854
+ }
50855
+ return `<text x="${textX}" y="${textY}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${fill}" text-anchor="${anchor}">${this.escapeXml(node.content)}</text>`;
49635
50856
  }
49636
- renderLink(node) {
49637
- const fontSize = 14;
49638
- const y = this.currentY + fontSize;
49639
- this.currentY += fontSize + 8;
49640
- return `<text x="${this.currentX}" y="${y}" font-size="${fontSize}" fill="${this.theme.colors.primary}" text-decoration="underline">${this.escapeXml(node.content)}</text>`;
50857
+ renderButtonBox(box) {
50858
+ const node = box.node;
50859
+ const hasIcon = !!node.icon;
50860
+ const isIconOnly = hasIcon && !node.content.trim();
50861
+ let fill = this.theme.colors.primary;
50862
+ let textFill = "#ffffff";
50863
+ let strokeAttr = "";
50864
+ if (node.secondary) {
50865
+ fill = this.theme.colors.secondary;
50866
+ } else if (node.outline) {
50867
+ fill = "white";
50868
+ textFill = this.theme.colors.foreground;
50869
+ strokeAttr = `stroke="${this.theme.colors.border}" stroke-width="1"`;
50870
+ } else if (node.ghost) {
50871
+ fill = "transparent";
50872
+ textFill = this.theme.colors.foreground;
50873
+ }
50874
+ let svg = `<g>
50875
+ <rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="4" fill="${fill}" ${strokeAttr}/>`;
50876
+ if (hasIcon) {
50877
+ const iconData = getIconData(node.icon);
50878
+ if (iconData) {
50879
+ const iconSize = 16;
50880
+ const iconX = isIconOnly ? box.x + (box.width - iconSize) / 2 : box.x + 16;
50881
+ const iconY = box.y + (box.height - iconSize) / 2;
50882
+ svg += this.renderIconPaths(iconData, iconX, iconY, iconSize, textFill);
50883
+ }
50884
+ }
50885
+ if (!isIconOnly) {
50886
+ const textX = hasIcon ? box.x + 36 + (box.width - 52) / 2 : box.x + box.width / 2;
50887
+ svg += `<text x="${textX}" y="${box.y + box.height / 2 + 5}" font-size="14" fill="${textFill}" text-anchor="middle">${this.escapeXml(node.content)}</text>`;
50888
+ }
50889
+ svg += `</g>`;
50890
+ return svg;
49641
50891
  }
49642
- // ===========================================
49643
- // Input Renderers
49644
- // ===========================================
49645
- renderInput(node) {
49646
- const width = 280;
49647
- const height = 40;
49648
- const x = this.currentX;
49649
- let y = this.currentY;
49650
- let result = "";
50892
+ renderInputBox(box) {
50893
+ const node = box.node;
50894
+ let y = box.y;
50895
+ let svg = "<g>";
49651
50896
  if (node.label) {
49652
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50897
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49653
50898
  y += 24;
49654
50899
  }
50900
+ const inputHeight = 36;
49655
50901
  const placeholder = node.placeholder || "";
49656
- result += `
49657
- <g transform="translate(${x}, ${y})">
49658
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49659
- <text x="12" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
49660
- </g>`;
49661
- this.currentY = y + height + 12;
49662
- return result;
50902
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${inputHeight}" rx="4" fill="#f4f4f5"/>`;
50903
+ svg += `<text x="${box.x + 12}" y="${y + inputHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>`;
50904
+ svg += "</g>";
50905
+ return svg;
49663
50906
  }
49664
- renderTextarea(node) {
49665
- const width = 280;
49666
- const height = 100;
49667
- const x = this.currentX;
49668
- let y = this.currentY;
49669
- let result = "";
50907
+ renderTextareaBox(box) {
50908
+ const node = box.node;
50909
+ let y = box.y;
50910
+ let svg = "<g>";
49670
50911
  if (node.label) {
49671
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50912
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49672
50913
  y += 24;
49673
50914
  }
50915
+ const textareaHeight = box.height - (node.label ? 24 : 0);
49674
50916
  const placeholder = node.placeholder || "";
49675
- result += `
49676
- <g transform="translate(${x}, ${y})">
49677
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49678
- <text x="12" y="24" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
49679
- </g>`;
49680
- this.currentY = y + height + 12;
49681
- return result;
50917
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${textareaHeight}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50918
+ svg += `<text x="${box.x + 12}" y="${y + 24}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>`;
50919
+ svg += "</g>";
50920
+ return svg;
49682
50921
  }
49683
- renderSelect(node) {
49684
- const width = 280;
49685
- const height = 40;
49686
- const x = this.currentX;
49687
- let y = this.currentY;
49688
- let result = "";
50922
+ renderSelectBox(box) {
50923
+ const node = box.node;
50924
+ let y = box.y;
50925
+ let svg = "<g>";
49689
50926
  if (node.label) {
49690
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50927
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49691
50928
  y += 24;
49692
50929
  }
50930
+ const selectHeight = 40;
49693
50931
  const placeholder = node.placeholder || "Select...";
49694
- result += `
49695
- <g transform="translate(${x}, ${y})">
49696
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49697
- <text x="12" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>
49698
- <path d="M${width - 24} ${height / 2 - 3} l6 6 l6 -6" fill="none" stroke="${this.theme.colors.muted}" stroke-width="1.5"/>
49699
- </g>`;
49700
- this.currentY = y + height + 12;
49701
- return result;
50932
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${selectHeight}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50933
+ svg += `<text x="${box.x + 12}" y="${y + selectHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(placeholder)}</text>`;
50934
+ 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"/>`;
50935
+ svg += "</g>";
50936
+ return svg;
49702
50937
  }
49703
- renderCheckbox(node) {
49704
- const x = this.currentX;
49705
- const y = this.currentY;
50938
+ renderCheckboxBox(box) {
50939
+ const node = box.node;
49706
50940
  const size = 18;
49707
- let result = `
49708
- <g transform="translate(${x}, ${y})">
49709
- <rect width="${size}" height="${size}" rx="3" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50941
+ let svg = `<g>
50942
+ <rect x="${box.x}" y="${box.y}" width="${size}" height="${size}" rx="3" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49710
50943
  if (node.checked) {
49711
- result += `<path d="M4 9 L7 12 L14 5" fill="none" stroke="${this.theme.colors.foreground}" stroke-width="2"/>`;
50944
+ 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"/>`;
49712
50945
  }
49713
50946
  if (node.label) {
49714
- result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50947
+ 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>`;
49715
50948
  }
49716
- result += "</g>";
49717
- this.currentY += size + 12;
49718
- return result;
50949
+ svg += "</g>";
50950
+ return svg;
49719
50951
  }
49720
- renderRadio(node) {
49721
- const x = this.currentX;
49722
- const y = this.currentY;
50952
+ renderRadioBox(box) {
50953
+ const node = box.node;
49723
50954
  const size = 18;
49724
50955
  const radius = size / 2;
49725
- let result = `
49726
- <g transform="translate(${x}, ${y})">
49727
- <circle cx="${radius}" cy="${radius}" r="${radius - 1}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50956
+ const cx = box.x + radius;
50957
+ const cy = box.y + radius;
50958
+ let svg = `<g>
50959
+ <circle cx="${cx}" cy="${cy}" r="${radius - 1}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49728
50960
  if (node.checked) {
49729
- result += `<circle cx="${radius}" cy="${radius}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
50961
+ svg += `<circle cx="${cx}" cy="${cy}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
49730
50962
  }
49731
50963
  if (node.label) {
49732
- result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50964
+ 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>`;
49733
50965
  }
49734
- result += "</g>";
49735
- this.currentY += size + 12;
49736
- return result;
50966
+ svg += "</g>";
50967
+ return svg;
49737
50968
  }
49738
- renderSwitch(node) {
49739
- const x = this.currentX;
49740
- const y = this.currentY;
50969
+ renderSwitchBox(box) {
50970
+ const node = box.node;
49741
50971
  const width = 44;
49742
50972
  const height = 24;
49743
50973
  const radius = height / 2;
49744
50974
  const isOn = node.checked;
49745
50975
  const bgColor = isOn ? this.theme.colors.primary : this.theme.colors.border;
49746
- const knobX = isOn ? width - radius : radius;
49747
- let result = `
49748
- <g transform="translate(${x}, ${y})">
49749
- <rect width="${width}" height="${height}" rx="${radius}" fill="${bgColor}"/>
49750
- <circle cx="${knobX}" cy="${radius}" r="${radius - 3}" fill="white"/>`;
50976
+ const knobX = isOn ? box.x + width - radius : box.x + radius;
50977
+ let svg = `<g>
50978
+ <rect x="${box.x}" y="${box.y}" width="${width}" height="${height}" rx="${radius}" fill="${bgColor}"/>
50979
+ <circle cx="${knobX}" cy="${box.y + radius}" r="${radius - 3}" fill="white"/>`;
49751
50980
  if (node.label) {
49752
- result += `<text x="${width + 8}" y="${height - 6}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49753
- }
49754
- result += "</g>";
49755
- this.currentY += height + 12;
49756
- return result;
49757
- }
49758
- // ===========================================
49759
- // Button Renderer
49760
- // ===========================================
49761
- renderButton(node) {
49762
- const content = node.content;
49763
- const hasIcon = !!node.icon;
49764
- const isIconOnly = hasIcon && !content.trim();
49765
- const iconSize = 16;
49766
- const padding = isIconOnly ? 8 : 16;
49767
- let width;
49768
- if (isIconOnly) {
49769
- width = iconSize + padding * 2;
49770
- } else if (hasIcon) {
49771
- width = Math.max(80, content.length * 10 + iconSize + 40);
49772
- } else {
49773
- width = Math.max(80, content.length * 10 + 32);
49774
- }
49775
- const height = 36;
49776
- const x = this.currentX;
49777
- const y = this.currentY;
49778
- let fill = this.theme.colors.primary;
49779
- let textFill = "#ffffff";
49780
- let isOutline = false;
49781
- if (node.secondary) {
49782
- fill = this.theme.colors.secondary;
49783
- } else if (node.outline) {
49784
- fill = "white";
49785
- textFill = this.theme.colors.foreground;
49786
- isOutline = true;
49787
- } else if (node.ghost) {
49788
- fill = "transparent";
49789
- textFill = this.theme.colors.foreground;
50981
+ 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>`;
49790
50982
  }
49791
- this.currentY += height + 8;
49792
- const strokeAttr = isOutline ? `stroke="${this.theme.colors.border}" stroke-width="1"` : "";
49793
- let iconSvg = "";
49794
- if (hasIcon) {
49795
- const iconData = getIconData(node.icon);
49796
- if (iconData) {
49797
- const iconX = isIconOnly ? (width - iconSize) / 2 : padding;
49798
- const iconY = (height - iconSize) / 2;
49799
- iconSvg = this.renderIconPaths(iconData, iconX, iconY, iconSize, textFill);
49800
- }
49801
- }
49802
- const textX = hasIcon && !isIconOnly ? padding + iconSize + 8 + (width - padding - iconSize - 8 - padding) / 2 : width / 2;
49803
- const textContent = isIconOnly ? "" : `<text x="${textX}" y="${height / 2 + 5}" font-size="14" fill="${textFill}" text-anchor="middle">${this.escapeXml(content)}</text>`;
49804
- return `
49805
- <g transform="translate(${x}, ${y})">
49806
- <rect width="${width}" height="${height}" rx="4" fill="${fill}" ${strokeAttr}/>
49807
- ${iconSvg}
49808
- ${textContent}
49809
- </g>`;
50983
+ svg += "</g>";
50984
+ return svg;
49810
50985
  }
49811
- /**
49812
- * Render icon paths for SVG
49813
- */
49814
- renderIconPaths(data, x, y, size, color) {
49815
- const scale = size / 24;
49816
- const paths = data.map(([tag, attrs]) => {
49817
- const attrStr = Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
49818
- return `<${tag} ${attrStr} />`;
49819
- }).join("");
49820
- return `<g transform="translate(${x}, ${y}) scale(${scale})" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</g>`;
50986
+ renderLinkBox(box) {
50987
+ const node = box.node;
50988
+ const fontSize = 14;
50989
+ const textY = box.y + fontSize;
50990
+ return `<text x="${box.x}" y="${textY}" font-size="${fontSize}" fill="${this.theme.colors.primary}" text-decoration="underline">${this.escapeXml(node.content)}</text>`;
49821
50991
  }
49822
- // ===========================================
49823
- // Display Renderers
49824
- // ===========================================
49825
- renderImage(node) {
49826
- const width = node.w && typeof node.w === "number" ? node.w : 200;
49827
- const height = node.h && typeof node.h === "number" ? node.h : 150;
49828
- const x = this.currentX;
49829
- const y = this.currentY;
49830
- this.currentY += height + 12;
49831
- return `
49832
- <g transform="translate(${x}, ${y})">
49833
- <rect width="${width}" height="${height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49834
- <line x1="0" y1="0" x2="${width}" y2="${height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49835
- <line x1="${width}" y1="0" x2="0" y2="${height}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49836
- <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>
50992
+ renderImageBox(box) {
50993
+ const node = box.node;
50994
+ return `<g>
50995
+ <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"/>
50996
+ <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"/>
50997
+ <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"/>
50998
+ <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>
49837
50999
  </g>`;
49838
51000
  }
49839
- renderPlaceholder(node) {
49840
- const width = node.w && typeof node.w === "number" ? node.w : 200;
49841
- const height = node.h && typeof node.h === "number" ? node.h : 100;
49842
- const x = this.currentX;
49843
- const y = this.currentY;
49844
- this.currentY += height + 12;
49845
- return `
49846
- <g transform="translate(${x}, ${y})">
49847
- <rect width="${width}" height="${height}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1" stroke-dasharray="4,4"/>
49848
- <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>
51001
+ renderPlaceholderBox(box) {
51002
+ const node = box.node;
51003
+ const patternId = `placeholder-pattern-${this.clipPathCounter++}`;
51004
+ const patternDef = `<pattern id="${patternId}" width="14" height="14" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
51005
+ <rect width="14" height="14" fill="#f9f9f9"/>
51006
+ <rect width="7" height="14" fill="rgba(0,0,0,0.03)"/>
51007
+ </pattern>`;
51008
+ this.clipPathDefs.push(patternDef);
51009
+ return `<g>
51010
+ <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"/>
51011
+ <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>
49849
51012
  </g>`;
49850
51013
  }
49851
- renderAvatar(node) {
49852
- const sizes = { xs: 24, sm: 32, md: 40, lg: 48, xl: 64 };
49853
- const size = sizes[node.size || "md"] || 40;
49854
- const radius = size / 2;
49855
- const x = this.currentX;
49856
- const y = this.currentY;
51014
+ renderAvatarBox(box) {
51015
+ const node = box.node;
51016
+ const radius = box.width / 2;
51017
+ const cx = box.x + radius;
51018
+ const cy = box.y + radius;
49857
51019
  const initial = node.name ? node.name.charAt(0).toUpperCase() : "?";
49858
- this.currentY += size + 12;
49859
- return `
49860
- <g transform="translate(${x}, ${y})">
49861
- <circle cx="${radius}" cy="${radius}" r="${radius}" fill="${this.theme.colors.muted}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49862
- <text x="${radius}" y="${radius + 5}" font-size="${size / 2.5}" fill="${this.theme.colors.foreground}" text-anchor="middle">${initial}</text>
51020
+ return `<g>
51021
+ <circle cx="${cx}" cy="${cy}" r="${radius}" fill="${this.theme.colors.foreground}"/>
51022
+ <text x="${cx}" y="${cy + 5}" font-size="${box.width / 2.5}" fill="${this.theme.colors.background}" text-anchor="middle">${initial}</text>
49863
51023
  </g>`;
49864
51024
  }
49865
- renderBadge(node) {
49866
- const content = node.content;
49867
- const width = Math.max(24, content.length * 8 + 16);
49868
- const height = 22;
49869
- const x = this.currentX;
49870
- const y = this.currentY;
49871
- const fill = this.theme.colors.muted;
49872
- const textFill = this.theme.colors.foreground;
49873
- this.currentY += height + 8;
49874
- return `
49875
- <g transform="translate(${x}, ${y})">
49876
- <rect width="${width}" height="${height}" rx="11" fill="${fill}" stroke="${this.theme.colors.border}" stroke-width="1"/>
49877
- <text x="${width / 2}" y="${height / 2 + 4}" font-size="12" fill="${textFill}" text-anchor="middle">${this.escapeXml(content)}</text>
51025
+ renderBadgeBox(box) {
51026
+ const node = box.node;
51027
+ return `<g>
51028
+ <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"/>
51029
+ <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>
49878
51030
  </g>`;
49879
51031
  }
49880
- // ===========================================
49881
- // Data Renderers
49882
- // ===========================================
49883
- renderTable(node) {
51032
+ renderTableBox(box) {
51033
+ const node = box.node;
49884
51034
  const columns = node.columns || [];
49885
51035
  const rows = node.rows || [];
49886
51036
  const rowCount = rows.length || 3;
49887
51037
  const colWidth = 120;
49888
51038
  const rowHeight = 40;
49889
- const x = this.currentX;
49890
- const y = this.currentY;
49891
- let svg = `<g transform="translate(${x}, ${y})">`;
49892
- svg += `<rect width="${columns.length * colWidth}" height="${rowHeight}" fill="${this.theme.colors.muted}"/>`;
51039
+ let svg = `<g>`;
51040
+ svg += `<rect x="${box.x}" y="${box.y}" width="${columns.length * colWidth}" height="${rowHeight}" fill="${this.theme.colors.muted}"/>`;
49893
51041
  columns.forEach((col, i) => {
49894
- svg += `<text x="${i * colWidth + 12}" y="${rowHeight / 2 + 5}" font-size="14" font-weight="600">${this.escapeXml(col)}</text>`;
51042
+ svg += `<text x="${box.x + i * colWidth + 12}" y="${box.y + rowHeight / 2 + 5}" font-size="14" font-weight="600">${this.escapeXml(col)}</text>`;
49895
51043
  });
49896
- const displayRowCount = rows.length > 0 ? rows.length : Math.max(rowCount, 3);
51044
+ const displayRowCount = rows.length > 0 ? rows.length : rowCount;
49897
51045
  for (let rowIdx = 0; rowIdx < displayRowCount; rowIdx++) {
49898
- const rowY = (rowIdx + 1) * rowHeight;
49899
- svg += `<rect y="${rowY}" width="${columns.length * colWidth}" height="${rowHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51046
+ const rowY = box.y + (rowIdx + 1) * rowHeight;
51047
+ svg += `<rect x="${box.x}" y="${rowY}" width="${columns.length * colWidth}" height="${rowHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
49900
51048
  columns.forEach((_, colIdx) => {
49901
51049
  const cellContent = rows[rowIdx] && rows[rowIdx][colIdx] ? String(typeof rows[rowIdx][colIdx] === "object" ? "..." : rows[rowIdx][colIdx]) : "\u2014";
49902
- svg += `<text x="${colIdx * colWidth + 12}" y="${rowY + rowHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(cellContent)}</text>`;
51050
+ 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>`;
49903
51051
  });
49904
51052
  }
49905
51053
  svg += "</g>";
49906
- this.currentY += (displayRowCount + 1) * rowHeight + 16;
49907
51054
  return svg;
49908
51055
  }
49909
- renderList(node) {
49910
- const x = this.currentX;
49911
- let y = this.currentY;
51056
+ renderListBox(box) {
51057
+ const node = box.node;
49912
51058
  const items = node.items || [];
49913
51059
  const ordered = node.ordered || false;
49914
- let svg = `<g transform="translate(${x}, ${y})">`;
51060
+ let svg = "<g>";
49915
51061
  items.forEach((item, idx) => {
49916
51062
  const marker = ordered ? `${idx + 1}.` : "\u2022";
49917
51063
  const content = typeof item === "string" ? item : item.content;
49918
- svg += `<text x="0" y="${idx * 24 + 16}" font-size="14" fill="${this.theme.colors.foreground}">${marker} ${this.escapeXml(content)}</text>`;
51064
+ svg += `<text x="${box.x}" y="${box.y + idx * 28 + 16}" font-size="14" fill="${this.theme.colors.foreground}">${marker} ${this.escapeXml(content)}</text>`;
49919
51065
  });
49920
51066
  svg += "</g>";
49921
- this.currentY += items.length * 24 + 12;
49922
51067
  return svg;
49923
51068
  }
49924
- // ===========================================
49925
- // Feedback Renderers
49926
- // ===========================================
49927
- renderAlert(node) {
49928
- const width = Math.min(400, this.contentWidth);
49929
- const height = 48;
49930
- const x = this.currentX;
49931
- const y = this.currentY;
49932
- this.currentY += height + 12;
49933
- return `
49934
- <g transform="translate(${x}, ${y})">
49935
- <rect width="${width}" height="${height}" rx="4" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>
49936
- <text x="16" y="${height / 2 + 5}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.content)}</text>
51069
+ renderAlertBox(box) {
51070
+ const node = box.node;
51071
+ return `<g>
51072
+ <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"/>
51073
+ <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>
49937
51074
  </g>`;
49938
51075
  }
49939
- renderProgress(node) {
49940
- const width = 200;
49941
- const height = 8;
49942
- const x = this.currentX;
49943
- let y = this.currentY;
49944
- let result = "";
51076
+ renderProgressBox(box) {
51077
+ const node = box.node;
51078
+ let y = box.y;
51079
+ let svg = "<g>";
49945
51080
  if (node.label) {
49946
- result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
51081
+ svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49947
51082
  y += 24;
49948
51083
  }
51084
+ const barHeight = 8;
49949
51085
  const value = node.value || 0;
49950
51086
  const max = node.max || 100;
49951
51087
  const percent = Math.min(100, Math.max(0, value / max * 100));
49952
- result += `
49953
- <g transform="translate(${x}, ${y})">
49954
- <rect width="${width}" height="${height}" rx="${height / 2}" fill="${this.theme.colors.muted}"/>
49955
- <rect width="${width * percent / 100}" height="${height}" rx="${height / 2}" fill="${this.theme.colors.primary}"/>
49956
- </g>`;
49957
- this.currentY = y + height + 12;
49958
- return result;
51088
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width}" height="${barHeight}" rx="${barHeight / 2}" fill="${this.theme.colors.muted}"/>`;
51089
+ svg += `<rect x="${box.x}" y="${y}" width="${box.width * percent / 100}" height="${barHeight}" rx="${barHeight / 2}" fill="${this.theme.colors.primary}"/>`;
51090
+ svg += "</g>";
51091
+ return svg;
49959
51092
  }
49960
- renderSpinner(node) {
49961
- const sizes = { xs: 16, sm: 20, md: 24, lg: 32, xl: 40 };
49962
- const size = sizes[node.size || "md"] || 24;
49963
- const x = this.currentX + size / 2;
49964
- const y = this.currentY + size / 2;
49965
- const radius = size / 2 - 2;
49966
- this.currentY += size + 12;
49967
- return `
49968
- <g transform="translate(${x}, ${y})">
49969
- <circle r="${radius}" fill="none" stroke="${this.theme.colors.muted}" stroke-width="2"/>
49970
- <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"/>
51093
+ renderSpinnerBox(box) {
51094
+ const cx = box.x + box.width / 2;
51095
+ const cy = box.y + box.height / 2;
51096
+ const radius = box.width / 2 - 2;
51097
+ return `<g>
51098
+ <circle cx="${cx}" cy="${cy}" r="${radius}" fill="none" stroke="${this.theme.colors.muted}" stroke-width="2"/>
51099
+ <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"/>
49971
51100
  </g>`;
49972
51101
  }
49973
- // ===========================================
49974
- // Navigation Renderers
49975
- // ===========================================
49976
- renderNav(node) {
49977
- const items = node.items || [];
49978
- const x = this.currentX;
49979
- const y = this.currentY;
51102
+ renderNavBox(box) {
51103
+ const node = box.node;
49980
51104
  const vertical = node.vertical || false;
49981
- let svg = `<g transform="translate(${x}, ${y})">`;
51105
+ let svg = "<g>";
51106
+ if (node.children && node.children.length > 0) {
51107
+ let offsetY = 0;
51108
+ node.children.forEach((child) => {
51109
+ if (child.type === "divider") {
51110
+ offsetY += 16;
51111
+ 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"/>`;
51112
+ offsetY += 8;
51113
+ } else if (child.type === "group") {
51114
+ 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>`;
51115
+ offsetY += 28;
51116
+ child.items.forEach((item) => {
51117
+ if (item.type === "divider") {
51118
+ offsetY += 8;
51119
+ 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"/>`;
51120
+ offsetY += 8;
51121
+ } else {
51122
+ const fill = item.active ? this.theme.colors.foreground : this.theme.colors.muted;
51123
+ const fontWeight = item.active ? 'font-weight="500"' : "";
51124
+ svg += `<text x="${box.x}" y="${box.y + offsetY + 16}" font-size="14" ${fontWeight} fill="${fill}">${this.escapeXml(item.label)}</text>`;
51125
+ offsetY += 32;
51126
+ }
51127
+ });
51128
+ } else if (child.type === "item") {
51129
+ const fill = child.active ? this.theme.colors.foreground : this.theme.colors.muted;
51130
+ const fontWeight = child.active ? 'font-weight="500"' : "";
51131
+ svg += `<text x="${box.x}" y="${box.y + offsetY + 16}" font-size="14" ${fontWeight} fill="${fill}">${this.escapeXml(child.label)}</text>`;
51132
+ offsetY += 32;
51133
+ }
51134
+ });
51135
+ svg += "</g>";
51136
+ return svg;
51137
+ }
51138
+ const items = node.items || [];
49982
51139
  if (vertical) {
49983
51140
  items.forEach((item, idx) => {
49984
51141
  const label = typeof item === "string" ? item : item.label;
49985
51142
  const isActive = typeof item === "object" && item.active;
49986
51143
  const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
49987
- svg += `<text x="0" y="${idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51144
+ svg += `<text x="${box.x}" y="${box.y + idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
49988
51145
  });
49989
- this.currentY += items.length * 32 + 12;
49990
51146
  } else {
49991
51147
  let offsetX = 0;
49992
51148
  items.forEach((item) => {
49993
51149
  const label = typeof item === "string" ? item : item.label;
49994
51150
  const isActive = typeof item === "object" && item.active;
49995
51151
  const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
49996
- svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
49997
- offsetX += label.length * 8 + 24;
51152
+ svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51153
+ offsetX += this.estimateTextWidth(label, 14) + 24;
49998
51154
  });
49999
- this.currentY += 32;
50000
51155
  }
50001
51156
  svg += "</g>";
50002
51157
  return svg;
50003
51158
  }
50004
- renderTabs(node) {
51159
+ renderTabsBox(box) {
51160
+ const node = box.node;
50005
51161
  const items = node.items || [];
50006
- const x = this.currentX;
50007
- const y = this.currentY;
50008
51162
  const tabHeight = 40;
50009
- let svg = `<g transform="translate(${x}, ${y})">`;
51163
+ let svg = "<g>";
50010
51164
  let offsetX = 0;
50011
- items.forEach((item) => {
51165
+ items.forEach((item, idx) => {
50012
51166
  const label = typeof item === "string" ? item : item;
50013
- const tabWidth = label.length * 10 + 24;
50014
- svg += `<rect x="${offsetX}" width="${tabWidth}" height="${tabHeight}" fill="white" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
50015
- svg += `<text x="${offsetX + tabWidth / 2}" y="${tabHeight / 2 + 5}" font-size="14" text-anchor="middle">${this.escapeXml(label)}</text>`;
51167
+ const tabWidth = this.estimateTextWidth(label, 14) + 32;
51168
+ const isFirst = idx === 0;
51169
+ 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"/>`;
51170
+ 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>`;
50016
51171
  offsetX += tabWidth;
50017
51172
  });
50018
51173
  svg += "</g>";
50019
- this.currentY += tabHeight + 12;
50020
51174
  return svg;
50021
51175
  }
50022
- renderBreadcrumb(node) {
51176
+ renderBreadcrumbBox(box) {
51177
+ const node = box.node;
50023
51178
  const items = node.items || [];
50024
51179
  const separator = "/";
50025
- const x = this.currentX;
50026
- const y = this.currentY;
50027
- let svg = `<g transform="translate(${x}, ${y})">`;
51180
+ let svg = "<g>";
50028
51181
  let offsetX = 0;
50029
51182
  items.forEach((item, idx) => {
50030
51183
  const label = typeof item === "string" ? item : item.label;
50031
51184
  const isLast = idx === items.length - 1;
50032
51185
  const fill = isLast ? this.theme.colors.foreground : this.theme.colors.muted;
50033
- svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
50034
- offsetX += label.length * 8 + 8;
51186
+ svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51187
+ offsetX += this.estimateTextWidth(label, 14) + 8;
50035
51188
  if (!isLast) {
50036
- svg += `<text x="${offsetX}" y="16" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
51189
+ svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
50037
51190
  offsetX += 16;
50038
51191
  }
50039
51192
  });
50040
51193
  svg += "</g>";
50041
- this.currentY += 28;
50042
51194
  return svg;
50043
51195
  }
51196
+ renderIconBox(box) {
51197
+ const node = box.node;
51198
+ const iconData = getIconData(node.name);
51199
+ if (!iconData) {
51200
+ return `<!-- Icon not found: ${node.name} -->`;
51201
+ }
51202
+ const color = this.theme.colors.foreground;
51203
+ return this.renderIconPaths(iconData, box.x, box.y, box.width, color);
51204
+ }
51205
+ renderDividerBox(box) {
51206
+ return `<line x1="${box.x}" y1="${box.y}" x2="${box.x + box.width}" y2="${box.y}" stroke="${this.theme.colors.border}" stroke-width="1"/>`;
51207
+ }
50044
51208
  // ===========================================
50045
51209
  // Utility Methods
50046
51210
  // ===========================================
50047
- getFontSize(size) {
50048
- const sizes = {
50049
- xs: 12,
50050
- sm: 14,
50051
- base: 16,
50052
- md: 16,
50053
- lg: 18,
50054
- xl: 20,
50055
- "2xl": 24,
50056
- "3xl": 30
50057
- };
50058
- return sizes[size] || 16;
51211
+ renderIconPaths(data, x, y, size, color) {
51212
+ const scale = size / 24;
51213
+ const paths = data.map(([tag, attrs]) => {
51214
+ const attrStr = Object.entries(attrs).map(([key, value]) => `${key}="${value}"`).join(" ");
51215
+ return `<${tag} ${attrStr} />`;
51216
+ }).join("");
51217
+ return `<g transform="translate(${x}, ${y}) scale(${scale})" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</g>`;
51218
+ }
51219
+ getPadding(node) {
51220
+ const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 };
51221
+ if (!("p" in node) && !("px" in node) && !("py" in node)) {
51222
+ if (node.type === "Card" || node.type === "Header" || node.type === "Footer" || node.type === "Sidebar") {
51223
+ return { top: 16, right: 16, bottom: 16, left: 16 };
51224
+ }
51225
+ return defaultPadding;
51226
+ }
51227
+ let p = 0;
51228
+ if ("p" in node && node.p !== void 0) {
51229
+ p = this.resolveSpacing(node.p);
51230
+ }
51231
+ let px = p;
51232
+ let py = p;
51233
+ if ("px" in node && node.px !== void 0) {
51234
+ px = this.resolveSpacing(node.px);
51235
+ }
51236
+ if ("py" in node && node.py !== void 0) {
51237
+ py = this.resolveSpacing(node.py);
51238
+ }
51239
+ if (node.type === "Header") {
51240
+ return { top: 0, right: px, bottom: 0, left: px };
51241
+ }
51242
+ return { top: py, right: px, bottom: py, left: px };
51243
+ }
51244
+ getGap(node) {
51245
+ if ("gap" in node && node.gap !== void 0) {
51246
+ return this.resolveSpacing(node.gap);
51247
+ }
51248
+ return void 0;
51249
+ }
51250
+ resolveSpacing(value) {
51251
+ if (typeof value === "number") {
51252
+ return value * 4;
51253
+ }
51254
+ if (typeof value === "object" && value && "value" in value) {
51255
+ return value.value;
51256
+ }
51257
+ return 0;
51258
+ }
51259
+ resolveSize(value) {
51260
+ if (value === void 0 || value === null) return void 0;
51261
+ if (typeof value === "number") return value;
51262
+ if (typeof value === "string") {
51263
+ if (value === "full") return void 0;
51264
+ const num = parseFloat(value);
51265
+ if (!isNaN(num)) return num;
51266
+ }
51267
+ if (typeof value === "object" && value && "value" in value) {
51268
+ return value.value;
51269
+ }
51270
+ return void 0;
51271
+ }
51272
+ isFullWidth(node) {
51273
+ if ("w" in node) {
51274
+ return node.w === "full";
51275
+ }
51276
+ return false;
51277
+ }
51278
+ estimateTextWidth(text, fontSize) {
51279
+ return text.length * fontSize * 0.6;
50059
51280
  }
50060
51281
  resolveFontSize(size) {
50061
51282
  if (!size) return 16;
50062
51283
  if (typeof size === "string") {
50063
- return this.getFontSize(size);
51284
+ const sizes = {
51285
+ xs: 12,
51286
+ sm: 14,
51287
+ base: 16,
51288
+ md: 16,
51289
+ lg: 18,
51290
+ xl: 20,
51291
+ "2xl": 24,
51292
+ "3xl": 30
51293
+ };
51294
+ return sizes[size] || 16;
50064
51295
  }
50065
51296
  if (typeof size === "object" && "value" in size) {
50066
51297
  if (size.unit === "px") return size.value;
@@ -50071,15 +51302,8 @@ var SvgRenderer = class {
50071
51302
  return 16;
50072
51303
  }
50073
51304
  getTitleFontSize(level) {
50074
- const sizes = {
50075
- 1: 32,
50076
- 2: 28,
50077
- 3: 24,
50078
- 4: 20,
50079
- 5: 18,
50080
- 6: 16
50081
- };
50082
- return sizes[level] || 24;
51305
+ const sizes = { 1: 36, 2: 30, 3: 24, 4: 20, 5: 18, 6: 16 };
51306
+ return sizes[level] || 20;
50083
51307
  }
50084
51308
  escapeXml(str) {
50085
51309
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -50140,15 +51364,22 @@ function renderToSvg2(document, options = {}) {
50140
51364
  height = viewport.height;
50141
51365
  }
50142
51366
  }
50143
- const padding = options.padding ?? 20;
50144
51367
  const background = options.background ?? "#ffffff";
50145
51368
  const { html, css } = render(document, { theme: "light" });
50146
51369
  const svg = `<?xml version="1.0" encoding="UTF-8"?>
50147
51370
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
50148
51371
  viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
50149
51372
  <rect width="100%" height="100%" fill="${background}"/>
50150
- <foreignObject x="${padding}" y="${padding}" width="${width - padding * 2}" height="${height - padding * 2}">
50151
- <div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; overflow: hidden;">
51373
+ <foreignObject x="0" y="0" width="${width}" height="${height}">
51374
+ <div xmlns="http://www.w3.org/1999/xhtml" style="
51375
+ width: ${width}px;
51376
+ height: ${height}px;
51377
+ overflow: hidden;
51378
+ display: flex;
51379
+ justify-content: center;
51380
+ align-items: center;
51381
+ box-sizing: border-box;
51382
+ ">
50152
51383
  <style type="text/css">
50153
51384
  ${css}
50154
51385
  </style>