@wireweave/core 1.0.0-beta.20260110141318 → 1.1.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,15 +77,10 @@ function generateContainerStyles(prefix) {
77
77
  padding: 16px;
78
78
  }
79
79
 
80
- /* Cards in flex rows: respect explicit width, shrink if needed */
80
+ /* Cards in flex rows should expand equally */
81
81
  .${prefix}-row > .${prefix}-card {
82
- flex: 0 1 auto;
83
- min-width: 0;
84
- }
85
-
86
- /* Cards without explicit width should expand to fill space */
87
- .${prefix}-row > .${prefix}-card-flex {
88
82
  flex: 1 1 0%;
83
+ min-width: 0;
89
84
  }
90
85
 
91
86
  .${prefix}-card-title {
@@ -241,11 +236,6 @@ function generateTextStyles(prefix) {
241
236
  line-height: 1.25;
242
237
  }
243
238
 
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
-
249
239
  h1.${prefix}-title { font-size: 36px; }
250
240
  h2.${prefix}-title { font-size: 30px; }
251
241
  h3.${prefix}-title { font-size: 24px; }
@@ -640,30 +630,6 @@ img.${prefix}-image {
640
630
  font-size: 14px;
641
631
  }
642
632
 
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
-
667
633
  .${prefix}-avatar {
668
634
  display: inline-flex;
669
635
  align-items: center;
@@ -779,12 +745,12 @@ svg.${prefix}-icon {
779
745
  display: block;
780
746
  }
781
747
 
782
- /* Icon size tokens - matches SVG renderer */
748
+ /* Icon size tokens */
783
749
  svg.${prefix}-icon-xs { width: 12px; height: 12px; }
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; }
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; }
788
754
 
789
755
  .${prefix}-icon svg {
790
756
  display: block;
@@ -1080,27 +1046,6 @@ function generateNavigationStyles(_theme, prefix) {
1080
1046
  cursor: not-allowed;
1081
1047
  }
1082
1048
 
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
-
1104
1049
  .${prefix}-tabs {
1105
1050
  border-bottom: 1px solid var(--${prefix}-border);
1106
1051
  }
@@ -1318,6 +1263,7 @@ function generateBaseStyles(prefix) {
1318
1263
  font-family: var(--${prefix}-font);
1319
1264
  color: var(--${prefix}-fg);
1320
1265
  background: var(--${prefix}-bg);
1266
+ min-height: 100vh;
1321
1267
  box-sizing: border-box;
1322
1268
  position: relative;
1323
1269
  display: flex;
@@ -1328,19 +1274,10 @@ function generateBaseStyles(prefix) {
1328
1274
  overflow: hidden;
1329
1275
  }
1330
1276
 
1331
- /* Col direct child of page should fill page height */
1332
- .${prefix}-page > .${prefix}-col {
1333
- flex: 1;
1334
- min-height: 0;
1335
- }
1336
-
1337
1277
  /* Row containing sidebar should fill remaining space */
1338
1278
  .${prefix}-page > .${prefix}-row:has(.${prefix}-sidebar),
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) {
1279
+ .${prefix}-page > .${prefix}-row:has(.${prefix}-main) {
1342
1280
  flex: 1;
1343
- min-height: 0;
1344
1281
  align-items: stretch;
1345
1282
  }
1346
1283
 
@@ -1383,15 +1320,10 @@ function generateGridClasses(_theme, prefix) {
1383
1320
  box-sizing: border-box;
1384
1321
  }
1385
1322
 
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
-
1392
1323
  `;
1393
1324
  for (let i = 1; i <= 12; i++) {
1394
- css += `.${prefix}-col-${i} { flex: ${i} 0 0%; min-width: 0; }
1325
+ const width = (i / 12 * 100).toFixed(4);
1326
+ css += `.${prefix}-col-${i} { flex: 0 0 auto; width: ${width}%; }
1395
1327
  `;
1396
1328
  }
1397
1329
  return css;
@@ -1559,20 +1491,6 @@ function generateLayoutClasses(prefix) {
1559
1491
  .${prefix}-main {
1560
1492
  flex: 1;
1561
1493
  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;
1576
1494
  }
1577
1495
 
1578
1496
  .${prefix}-footer {
@@ -1589,7 +1507,6 @@ function generateLayoutClasses(prefix) {
1589
1507
  border-right: 1px solid var(--${prefix}-border);
1590
1508
  padding: 16px 16px 16px 20px;
1591
1509
  flex-shrink: 0;
1592
- align-self: stretch;
1593
1510
  }
1594
1511
 
1595
1512
  .${prefix}-sidebar-right {
@@ -48159,39 +48076,14 @@ var lucideIcons = {
48159
48076
  ]
48160
48077
  ]
48161
48078
  };
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
- };
48181
48079
  function getIconData(name) {
48182
48080
  if (lucideIcons[name]) {
48183
48081
  return lucideIcons[name];
48184
48082
  }
48185
- if (iconAliases[name] && lucideIcons[iconAliases[name]]) {
48186
- return lucideIcons[iconAliases[name]];
48187
- }
48188
48083
  const kebabName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
48189
48084
  if (lucideIcons[kebabName]) {
48190
48085
  return lucideIcons[kebabName];
48191
48086
  }
48192
- if (iconAliases[kebabName] && lucideIcons[iconAliases[kebabName]]) {
48193
- return lucideIcons[iconAliases[kebabName]];
48194
- }
48195
48087
  return void 0;
48196
48088
  }
48197
48089
  function renderIconSvg(data, _size = 24, strokeWidth = 2, className = "", styleAttr = "") {
@@ -48301,7 +48193,7 @@ var HtmlRenderer = class extends BaseRenderer {
48301
48193
  const title = node.title ? `<title>${this.escapeHtml(node.title)}</title>
48302
48194
  ` : "";
48303
48195
  const commonStyles = this.buildCommonStyles(node);
48304
- const viewportStyle = `position: relative; width: ${viewport.width}px; height: ${viewport.height}px; overflow: hidden`;
48196
+ const viewportStyle = `width: ${viewport.width}px; height: ${viewport.height}px`;
48305
48197
  const combinedStyle = commonStyles ? `${viewportStyle}; ${commonStyles}` : viewportStyle;
48306
48198
  const dataAttrs = `data-viewport-width="${viewport.width}" data-viewport-height="${viewport.height}" data-viewport-label="${viewport.label}"`;
48307
48199
  return `<div class="${classes}" style="${combinedStyle}" ${dataAttrs}>
@@ -48467,7 +48359,6 @@ ${children}
48467
48359
  renderMain(node) {
48468
48360
  const classes = this.buildClassString([
48469
48361
  `${this.prefix}-main`,
48470
- node.scroll ? `${this.prefix}-scroll` : void 0,
48471
48362
  ...this.getCommonClasses(node)
48472
48363
  ]);
48473
48364
  const styles = this.buildCommonStyles(node);
@@ -48553,10 +48444,6 @@ ${children}
48553
48444
  /**
48554
48445
  * Build common inline styles for all values
48555
48446
  *
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
- *
48560
48447
  * Spacing values (p, m, gap) use token system:
48561
48448
  * - number: spacing token (e.g., p=4 → padding: 16px from token table)
48562
48449
  * - ValueWithUnit: direct CSS value (e.g., p=16px → padding: 16px)
@@ -48571,17 +48458,6 @@ ${children}
48571
48458
  */
48572
48459
  buildCommonStyles(props) {
48573
48460
  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
- }
48585
48461
  const wValue = resolveSizeValueToCss(props.w);
48586
48462
  if (wValue) {
48587
48463
  styles.push(`width: ${wValue}`);
@@ -48691,10 +48567,8 @@ ${children}
48691
48567
  // Container Node Renderers
48692
48568
  // ===========================================
48693
48569
  renderCard(node) {
48694
- const hasExplicitWidth = node.w !== void 0;
48695
48570
  const classes = this.buildClassString([
48696
48571
  `${this.prefix}-card`,
48697
- !hasExplicitWidth ? `${this.prefix}-card-flex` : void 0,
48698
48572
  node.shadow ? `${this.prefix}-card-shadow-${node.shadow}` : void 0,
48699
48573
  ...this.getCommonClasses(node)
48700
48574
  ]);
@@ -49031,19 +48905,11 @@ ${slider}`;
49031
48905
  renderPlaceholder(node) {
49032
48906
  const classes = this.buildClassString([
49033
48907
  `${this.prefix}-placeholder`,
49034
- node.children && node.children.length > 0 ? `${this.prefix}-placeholder-with-children` : void 0,
49035
48908
  ...this.getCommonClasses(node)
49036
48909
  ]);
49037
48910
  const styles = this.buildCommonStyles(node);
49038
48911
  const styleAttr = styles ? ` style="${styles}"` : "";
49039
48912
  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
- }
49047
48913
  return `<div class="${classes}"${styleAttr}>${label}</div>`;
49048
48914
  }
49049
48915
  renderAvatar(node) {
@@ -49289,12 +49155,6 @@ ${items}
49289
49155
  ]);
49290
49156
  const styles = this.buildCommonStyles(node);
49291
49157
  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
- }
49298
49158
  const items = node.items.map((item) => {
49299
49159
  if (typeof item === "string") {
49300
49160
  return `<a class="${this.prefix}-nav-link" href="#">${this.escapeHtml(item)}</a>`;
@@ -49304,48 +49164,12 @@ ${content}
49304
49164
  item.active ? `${this.prefix}-nav-link-active` : void 0,
49305
49165
  item.disabled ? `${this.prefix}-nav-link-disabled` : void 0
49306
49166
  ]);
49307
- const iconHtml = item.icon ? this.renderIconHtml(item.icon) + " " : "";
49308
- return `<a class="${linkClasses}" href="${item.href || "#"}">${iconHtml}${this.escapeHtml(item.label)}</a>`;
49167
+ return `<a class="${linkClasses}" href="${item.href || "#"}">${this.escapeHtml(item.label)}</a>`;
49309
49168
  }).join("\n");
49310
49169
  return `<nav class="${classes}"${styleAttr}>
49311
49170
  ${items}
49312
49171
  </nav>`;
49313
49172
  }
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
- }
49349
49173
  renderTabs(node) {
49350
49174
  const classes = this.buildClassString([
49351
49175
  `${this.prefix}-tabs`,
@@ -49500,239 +49324,13 @@ function createHtmlRenderer(options) {
49500
49324
  return new HtmlRenderer(options);
49501
49325
  }
49502
49326
 
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
-
49726
49327
  // src/renderer/svg/index.ts
49727
49328
  var SvgRenderer = class {
49728
49329
  options;
49729
49330
  theme;
49730
- pageWidth = 0;
49731
- pageHeight = 0;
49732
- clipPathDefs = [];
49733
- clipPathCounter = 0;
49734
- // Default spacing values
49735
- DEFAULT_GAP = 16;
49331
+ currentX = 0;
49332
+ currentY = 0;
49333
+ contentWidth = 0;
49736
49334
  constructor(options = {}) {
49737
49335
  this.options = {
49738
49336
  width: options.width ?? 800,
@@ -49743,41 +49341,28 @@ var SvgRenderer = class {
49743
49341
  fontFamily: options.fontFamily ?? "system-ui, -apple-system, sans-serif"
49744
49342
  };
49745
49343
  this.theme = defaultTheme;
49344
+ this.contentWidth = this.options.width - this.options.padding * 2;
49746
49345
  }
49747
49346
  /**
49748
49347
  * Render a wireframe document to SVG
49749
49348
  */
49750
49349
  render(doc) {
49751
- this.clipPathDefs = [];
49752
- this.clipPathCounter = 0;
49350
+ this.currentX = this.options.padding;
49351
+ this.currentY = this.options.padding;
49753
49352
  const firstPage = doc.children[0];
49754
49353
  let width = this.options.width;
49755
49354
  let height = this.options.height;
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
- }
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;
49772
49360
  }
49773
- this.pageWidth = width;
49774
- this.pageHeight = height;
49775
49361
  const content = doc.children.map((page) => this.renderPage(page)).join("\n");
49776
- const allDefs = this.generateDefs() + "\n" + this.clipPathDefs.join("\n");
49777
49362
  const svg = `<?xml version="1.0" encoding="UTF-8"?>
49778
49363
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
49779
49364
  <defs>
49780
- ${allDefs}
49365
+ ${this.generateDefs()}
49781
49366
  </defs>
49782
49367
  <rect width="100%" height="100%" fill="${this.options.background}"/>
49783
49368
  <g transform="scale(${this.options.scale})">
@@ -49787,7 +49372,7 @@ var SvgRenderer = class {
49787
49372
  return { svg, width, height };
49788
49373
  }
49789
49374
  /**
49790
- * Generate SVG defs (styles)
49375
+ * Generate SVG defs (styles, patterns, etc.)
49791
49376
  */
49792
49377
  generateDefs() {
49793
49378
  return `
@@ -49798,1500 +49383,684 @@ var SvgRenderer = class {
49798
49383
  </style>
49799
49384
  `;
49800
49385
  }
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
- }
49847
49386
  /**
49848
- * Render page with fixed header/footer layout
49849
- * Header at top, Footer at bottom, Main fills remaining space
49387
+ * Render a page node
49850
49388
  */
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");
49389
+ renderPage(node) {
49859
49390
  const elements = [];
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));
49868
- }
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));
49391
+ if (node.title) {
49392
+ elements.push(this.renderPageTitle(node.title));
49875
49393
  }
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"));
49394
+ for (const child of node.children) {
49395
+ elements.push(this.renderNode(child));
49897
49396
  }
49898
49397
  return elements.join("\n");
49899
49398
  }
49900
- shouldCenterHorizontally(node) {
49901
- return node.type === "Card" || node.type === "Modal";
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>`;
49902
49407
  }
49903
- // ===========================================
49904
- // Measurement Phase
49905
- // ===========================================
49906
- measureNode(node, constraints) {
49408
+ /**
49409
+ * Render any AST node
49410
+ */
49411
+ renderNode(node) {
49907
49412
  switch (node.type) {
49413
+ // Layout nodes
49908
49414
  case "Row":
49909
- return this.measureRow(node, constraints);
49415
+ return this.renderRow(node);
49910
49416
  case "Col":
49911
- return this.measureCol(node, constraints);
49417
+ return this.renderCol(node);
49912
49418
  case "Header":
49913
- return this.measureHeader(node, constraints);
49914
- case "Footer":
49915
- return this.measureFooter(node, constraints);
49419
+ return this.renderHeader(node);
49916
49420
  case "Main":
49917
- return this.measureMain(node, constraints);
49421
+ return this.renderMain(node);
49422
+ case "Footer":
49423
+ return this.renderFooter(node);
49918
49424
  case "Sidebar":
49919
- return this.measureSidebar(node, constraints);
49425
+ return this.renderSidebar(node);
49426
+ // Container nodes
49920
49427
  case "Card":
49921
- return this.measureCard(node, constraints);
49428
+ return this.renderCard(node);
49922
49429
  case "Modal":
49923
- return this.measureModal(node, constraints);
49924
- case "Title":
49925
- return this.measureTitle(node);
49430
+ return this.renderModal(node);
49431
+ // Text nodes
49926
49432
  case "Text":
49927
- return this.measureText(node);
49928
- case "Button":
49929
- return this.measureButton(node, constraints);
49433
+ return this.renderText(node);
49434
+ case "Title":
49435
+ return this.renderTitle(node);
49436
+ case "Link":
49437
+ return this.renderLink(node);
49438
+ // Input nodes
49930
49439
  case "Input":
49931
- return this.measureInput(node, constraints);
49440
+ return this.renderInput(node);
49932
49441
  case "Textarea":
49933
- return this.measureTextarea(node, constraints);
49442
+ return this.renderTextarea(node);
49934
49443
  case "Select":
49935
- return this.measureSelect(node, constraints);
49444
+ return this.renderSelect(node);
49936
49445
  case "Checkbox":
49937
- return this.measureCheckbox(node);
49446
+ return this.renderCheckbox(node);
49938
49447
  case "Radio":
49939
- return this.measureRadio(node);
49448
+ return this.renderRadio(node);
49940
49449
  case "Switch":
49941
- return this.measureSwitch(node);
49942
- case "Link":
49943
- return this.measureLink(node);
49450
+ return this.renderSwitch(node);
49451
+ // Button
49452
+ case "Button":
49453
+ return this.renderButton(node);
49454
+ // Display nodes
49944
49455
  case "Image":
49945
- return this.measureImage(node, constraints);
49456
+ return this.renderImage(node);
49946
49457
  case "Placeholder":
49947
- return this.measurePlaceholder(node, constraints);
49458
+ return this.renderPlaceholder(node);
49948
49459
  case "Avatar":
49949
- return this.measureAvatar(node, constraints);
49460
+ return this.renderAvatar(node);
49950
49461
  case "Badge":
49951
- return this.measureBadge(node);
49462
+ return this.renderBadge(node);
49463
+ // Data nodes
49952
49464
  case "Table":
49953
- return this.measureTable(node);
49465
+ return this.renderTable(node);
49954
49466
  case "List":
49955
- return this.measureList(node);
49467
+ return this.renderList(node);
49468
+ // Feedback nodes
49956
49469
  case "Alert":
49957
- return this.measureAlert(node, constraints);
49470
+ return this.renderAlert(node);
49958
49471
  case "Progress":
49959
- return this.measureProgress(node, constraints);
49472
+ return this.renderProgress(node);
49960
49473
  case "Spinner":
49961
- return this.measureSpinner(node);
49474
+ return this.renderSpinner(node);
49475
+ // Navigation nodes
49962
49476
  case "Nav":
49963
- return this.measureNav(node);
49477
+ return this.renderNav(node);
49964
49478
  case "Tabs":
49965
- return this.measureTabs(node);
49479
+ return this.renderTabs(node);
49966
49480
  case "Breadcrumb":
49967
- return this.measureBreadcrumb(node);
49968
- case "Icon":
49969
- return this.measureIcon(node);
49970
- case "Divider":
49971
- return this.measureDivider(node, constraints);
49481
+ return this.renderBreadcrumb(node);
49972
49482
  default:
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 };
49483
+ return `<!-- Unsupported: ${node.type} -->`;
50256
49484
  }
50257
49485
  }
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
- }
50282
49486
  // ===========================================
50283
- // Layout Phase
49487
+ // Layout Renderers
50284
49488
  // ===========================================
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
- }
50336
- }
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
- }
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;
50438
49496
  }
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;
49497
+ return sum + 1;
49498
+ }, 0);
49499
+ const colWidth = this.contentWidth / Math.max(totalSpan, 1);
49500
+ let maxHeight = 0;
50516
49501
  for (const child of node.children) {
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;
50530
- }
50531
- return {
50532
- x,
50533
- y,
50534
- width: constraints.maxWidth,
50535
- height: height + padding.top + padding.bottom,
50536
- node,
50537
- children,
50538
- padding
50539
- };
50540
- }
50541
- adjustChildrenY(box, offset) {
50542
- for (const child of box.children) {
50543
- child.y += offset;
50544
- this.adjustChildrenY(child, offset);
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;
50545
49510
  }
49511
+ this.currentX = savedX;
49512
+ this.currentY = savedY + maxHeight;
49513
+ return elements.join("\n");
50546
49514
  }
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;
49515
+ renderCol(node) {
49516
+ return node.children.map((child) => this.renderNode(child)).join("\n");
50555
49517
  }
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
- };
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>`;
50584
49531
  }
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
- };
49532
+ renderMain(node) {
49533
+ return node.children.map((c) => this.renderNode(c)).join("\n");
50616
49534
  }
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
- };
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>`;
50643
49548
  }
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
- };
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>`;
50675
49563
  }
50676
49564
  // ===========================================
50677
- // Render Phase
49565
+ // Container Renderers
50678
49566
  // ===========================================
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}/>`;
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 = "";
50799
49574
  if (node.title) {
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>`;
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;
50802
49578
  }
50803
- svg += box.children.map((child) => this.renderBox(child)).join("\n");
50804
- svg += `</g>`;
50805
- return svg;
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>`;
50806
49591
  }
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"/>`;
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 = "";
50814
49598
  if (node.title) {
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>`;
50816
- }
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;
50823
- }
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";
49599
+ titleSvg = `<text x="20" y="30" font-size="18" font-weight="600" fill="${this.theme.colors.foreground}">${this.escapeXml(node.title)}</text>`;
50837
49600
  }
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>`;
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>`;
50839
49617
  }
50840
- renderTextBox(box) {
50841
- const node = box.node;
49618
+ // ===========================================
49619
+ // Text Renderers
49620
+ // ===========================================
49621
+ renderText(node) {
50842
49622
  const fontSize = this.resolveFontSize(node.size);
50843
49623
  const fill = node.muted ? this.theme.colors.muted : this.theme.colors.foreground;
50844
49624
  const fontWeight = node.weight || "normal";
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>`;
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>`;
50856
49628
  }
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;
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>`;
50891
49635
  }
50892
- renderInputBox(box) {
50893
- const node = box.node;
50894
- let y = box.y;
50895
- let svg = "<g>";
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>`;
49641
+ }
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 = "";
50896
49651
  if (node.label) {
50897
- svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49652
+ result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50898
49653
  y += 24;
50899
49654
  }
50900
- const inputHeight = 36;
50901
49655
  const placeholder = node.placeholder || "";
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;
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;
50906
49663
  }
50907
- renderTextareaBox(box) {
50908
- const node = box.node;
50909
- let y = box.y;
50910
- let svg = "<g>";
49664
+ renderTextarea(node) {
49665
+ const width = 280;
49666
+ const height = 100;
49667
+ const x = this.currentX;
49668
+ let y = this.currentY;
49669
+ let result = "";
50911
49670
  if (node.label) {
50912
- svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49671
+ result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50913
49672
  y += 24;
50914
49673
  }
50915
- const textareaHeight = box.height - (node.label ? 24 : 0);
50916
49674
  const placeholder = node.placeholder || "";
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;
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;
50921
49682
  }
50922
- renderSelectBox(box) {
50923
- const node = box.node;
50924
- let y = box.y;
50925
- let svg = "<g>";
49683
+ renderSelect(node) {
49684
+ const width = 280;
49685
+ const height = 40;
49686
+ const x = this.currentX;
49687
+ let y = this.currentY;
49688
+ let result = "";
50926
49689
  if (node.label) {
50927
- svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49690
+ result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50928
49691
  y += 24;
50929
49692
  }
50930
- const selectHeight = 40;
50931
49693
  const placeholder = node.placeholder || "Select...";
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;
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;
50937
49702
  }
50938
- renderCheckboxBox(box) {
50939
- const node = box.node;
49703
+ renderCheckbox(node) {
49704
+ const x = this.currentX;
49705
+ const y = this.currentY;
50940
49706
  const size = 18;
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"/>`;
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"/>`;
50943
49710
  if (node.checked) {
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"/>`;
49711
+ result += `<path d="M4 9 L7 12 L14 5" fill="none" stroke="${this.theme.colors.foreground}" stroke-width="2"/>`;
50945
49712
  }
50946
49713
  if (node.label) {
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>`;
49714
+ result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50948
49715
  }
50949
- svg += "</g>";
50950
- return svg;
49716
+ result += "</g>";
49717
+ this.currentY += size + 12;
49718
+ return result;
50951
49719
  }
50952
- renderRadioBox(box) {
50953
- const node = box.node;
49720
+ renderRadio(node) {
49721
+ const x = this.currentX;
49722
+ const y = this.currentY;
50954
49723
  const size = 18;
50955
49724
  const radius = size / 2;
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"/>`;
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"/>`;
50960
49728
  if (node.checked) {
50961
- svg += `<circle cx="${cx}" cy="${cy}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
49729
+ result += `<circle cx="${radius}" cy="${radius}" r="${radius - 5}" fill="${this.theme.colors.foreground}"/>`;
50962
49730
  }
50963
49731
  if (node.label) {
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>`;
49732
+ result += `<text x="${size + 8}" y="${size - 3}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50965
49733
  }
50966
- svg += "</g>";
50967
- return svg;
49734
+ result += "</g>";
49735
+ this.currentY += size + 12;
49736
+ return result;
50968
49737
  }
50969
- renderSwitchBox(box) {
50970
- const node = box.node;
49738
+ renderSwitch(node) {
49739
+ const x = this.currentX;
49740
+ const y = this.currentY;
50971
49741
  const width = 44;
50972
49742
  const height = 24;
50973
49743
  const radius = height / 2;
50974
49744
  const isOn = node.checked;
50975
49745
  const bgColor = isOn ? this.theme.colors.primary : this.theme.colors.border;
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"/>`;
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"/>`;
50980
49751
  if (node.label) {
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>`;
49752
+ result += `<text x="${width + 8}" y="${height - 6}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
50982
49753
  }
50983
- svg += "</g>";
50984
- return svg;
49754
+ result += "</g>";
49755
+ this.currentY += height + 12;
49756
+ return result;
50985
49757
  }
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>`;
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;
49790
+ }
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>`;
49810
+ }
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>`;
50991
49821
  }
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>
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>
50999
49837
  </g>`;
51000
49838
  }
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>
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>
51012
49849
  </g>`;
51013
49850
  }
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;
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;
51019
49857
  const initial = node.name ? node.name.charAt(0).toUpperCase() : "?";
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>
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>
51023
49863
  </g>`;
51024
49864
  }
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>
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>
51030
49878
  </g>`;
51031
49879
  }
51032
- renderTableBox(box) {
51033
- const node = box.node;
49880
+ // ===========================================
49881
+ // Data Renderers
49882
+ // ===========================================
49883
+ renderTable(node) {
51034
49884
  const columns = node.columns || [];
51035
49885
  const rows = node.rows || [];
51036
49886
  const rowCount = rows.length || 3;
51037
49887
  const colWidth = 120;
51038
49888
  const rowHeight = 40;
51039
- let svg = `<g>`;
51040
- svg += `<rect x="${box.x}" y="${box.y}" width="${columns.length * colWidth}" height="${rowHeight}" fill="${this.theme.colors.muted}"/>`;
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}"/>`;
51041
49893
  columns.forEach((col, i) => {
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>`;
49894
+ svg += `<text x="${i * colWidth + 12}" y="${rowHeight / 2 + 5}" font-size="14" font-weight="600">${this.escapeXml(col)}</text>`;
51043
49895
  });
51044
- const displayRowCount = rows.length > 0 ? rows.length : rowCount;
49896
+ const displayRowCount = rows.length > 0 ? rows.length : Math.max(rowCount, 3);
51045
49897
  for (let rowIdx = 0; rowIdx < displayRowCount; rowIdx++) {
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"/>`;
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"/>`;
51048
49900
  columns.forEach((_, colIdx) => {
51049
49901
  const cellContent = rows[rowIdx] && rows[rowIdx][colIdx] ? String(typeof rows[rowIdx][colIdx] === "object" ? "..." : rows[rowIdx][colIdx]) : "\u2014";
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>`;
49902
+ svg += `<text x="${colIdx * colWidth + 12}" y="${rowY + rowHeight / 2 + 5}" font-size="14" fill="${this.theme.colors.muted}">${this.escapeXml(cellContent)}</text>`;
51051
49903
  });
51052
49904
  }
51053
49905
  svg += "</g>";
49906
+ this.currentY += (displayRowCount + 1) * rowHeight + 16;
51054
49907
  return svg;
51055
49908
  }
51056
- renderListBox(box) {
51057
- const node = box.node;
49909
+ renderList(node) {
49910
+ const x = this.currentX;
49911
+ let y = this.currentY;
51058
49912
  const items = node.items || [];
51059
49913
  const ordered = node.ordered || false;
51060
- let svg = "<g>";
49914
+ let svg = `<g transform="translate(${x}, ${y})">`;
51061
49915
  items.forEach((item, idx) => {
51062
49916
  const marker = ordered ? `${idx + 1}.` : "\u2022";
51063
49917
  const content = typeof item === "string" ? item : item.content;
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>`;
49918
+ svg += `<text x="0" y="${idx * 24 + 16}" font-size="14" fill="${this.theme.colors.foreground}">${marker} ${this.escapeXml(content)}</text>`;
51065
49919
  });
51066
49920
  svg += "</g>";
49921
+ this.currentY += items.length * 24 + 12;
51067
49922
  return svg;
51068
49923
  }
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>
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>
51074
49937
  </g>`;
51075
49938
  }
51076
- renderProgressBox(box) {
51077
- const node = box.node;
51078
- let y = box.y;
51079
- let svg = "<g>";
49939
+ renderProgress(node) {
49940
+ const width = 200;
49941
+ const height = 8;
49942
+ const x = this.currentX;
49943
+ let y = this.currentY;
49944
+ let result = "";
51080
49945
  if (node.label) {
51081
- svg += `<text x="${box.x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
49946
+ result += `<text x="${x}" y="${y + 14}" font-size="14" fill="${this.theme.colors.foreground}">${this.escapeXml(node.label)}</text>`;
51082
49947
  y += 24;
51083
49948
  }
51084
- const barHeight = 8;
51085
49949
  const value = node.value || 0;
51086
49950
  const max = node.max || 100;
51087
49951
  const percent = Math.min(100, Math.max(0, value / max * 100));
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;
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;
51092
49959
  }
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"/>
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"/>
51100
49971
  </g>`;
51101
49972
  }
51102
- renderNavBox(box) {
51103
- const node = box.node;
51104
- const vertical = node.vertical || false;
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
- }
49973
+ // ===========================================
49974
+ // Navigation Renderers
49975
+ // ===========================================
49976
+ renderNav(node) {
51138
49977
  const items = node.items || [];
49978
+ const x = this.currentX;
49979
+ const y = this.currentY;
49980
+ const vertical = node.vertical || false;
49981
+ let svg = `<g transform="translate(${x}, ${y})">`;
51139
49982
  if (vertical) {
51140
49983
  items.forEach((item, idx) => {
51141
49984
  const label = typeof item === "string" ? item : item.label;
51142
49985
  const isActive = typeof item === "object" && item.active;
51143
49986
  const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
51144
- svg += `<text x="${box.x}" y="${box.y + idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
49987
+ svg += `<text x="0" y="${idx * 32 + 16}" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
51145
49988
  });
49989
+ this.currentY += items.length * 32 + 12;
51146
49990
  } else {
51147
49991
  let offsetX = 0;
51148
49992
  items.forEach((item) => {
51149
49993
  const label = typeof item === "string" ? item : item.label;
51150
49994
  const isActive = typeof item === "object" && item.active;
51151
49995
  const fill = isActive ? this.theme.colors.foreground : this.theme.colors.muted;
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;
49996
+ svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
49997
+ offsetX += label.length * 8 + 24;
51154
49998
  });
49999
+ this.currentY += 32;
51155
50000
  }
51156
50001
  svg += "</g>";
51157
50002
  return svg;
51158
50003
  }
51159
- renderTabsBox(box) {
51160
- const node = box.node;
50004
+ renderTabs(node) {
51161
50005
  const items = node.items || [];
50006
+ const x = this.currentX;
50007
+ const y = this.currentY;
51162
50008
  const tabHeight = 40;
51163
- let svg = "<g>";
50009
+ let svg = `<g transform="translate(${x}, ${y})">`;
51164
50010
  let offsetX = 0;
51165
- items.forEach((item, idx) => {
50011
+ items.forEach((item) => {
51166
50012
  const label = typeof item === "string" ? item : item;
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>`;
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>`;
51171
50016
  offsetX += tabWidth;
51172
50017
  });
51173
50018
  svg += "</g>";
50019
+ this.currentY += tabHeight + 12;
51174
50020
  return svg;
51175
50021
  }
51176
- renderBreadcrumbBox(box) {
51177
- const node = box.node;
50022
+ renderBreadcrumb(node) {
51178
50023
  const items = node.items || [];
51179
50024
  const separator = "/";
51180
- let svg = "<g>";
50025
+ const x = this.currentX;
50026
+ const y = this.currentY;
50027
+ let svg = `<g transform="translate(${x}, ${y})">`;
51181
50028
  let offsetX = 0;
51182
50029
  items.forEach((item, idx) => {
51183
50030
  const label = typeof item === "string" ? item : item.label;
51184
50031
  const isLast = idx === items.length - 1;
51185
50032
  const fill = isLast ? this.theme.colors.foreground : this.theme.colors.muted;
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;
50033
+ svg += `<text x="${offsetX}" y="16" font-size="14" fill="${fill}">${this.escapeXml(label)}</text>`;
50034
+ offsetX += label.length * 8 + 8;
51188
50035
  if (!isLast) {
51189
- svg += `<text x="${box.x + offsetX}" y="${box.y + 16}" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
50036
+ svg += `<text x="${offsetX}" y="16" font-size="14" fill="${this.theme.colors.muted}">${separator}</text>`;
51190
50037
  offsetX += 16;
51191
50038
  }
51192
50039
  });
51193
50040
  svg += "</g>";
50041
+ this.currentY += 28;
51194
50042
  return svg;
51195
50043
  }
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
- }
51208
50044
  // ===========================================
51209
50045
  // Utility Methods
51210
50046
  // ===========================================
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;
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;
51280
50059
  }
51281
50060
  resolveFontSize(size) {
51282
50061
  if (!size) return 16;
51283
50062
  if (typeof size === "string") {
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;
50063
+ return this.getFontSize(size);
51295
50064
  }
51296
50065
  if (typeof size === "object" && "value" in size) {
51297
50066
  if (size.unit === "px") return size.value;
@@ -51302,8 +50071,15 @@ var SvgRenderer = class {
51302
50071
  return 16;
51303
50072
  }
51304
50073
  getTitleFontSize(level) {
51305
- const sizes = { 1: 36, 2: 30, 3: 24, 4: 20, 5: 18, 6: 16 };
51306
- return sizes[level] || 20;
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;
51307
50083
  }
51308
50084
  escapeXml(str) {
51309
50085
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -51364,22 +50140,15 @@ function renderToSvg2(document, options = {}) {
51364
50140
  height = viewport.height;
51365
50141
  }
51366
50142
  }
50143
+ const padding = options.padding ?? 20;
51367
50144
  const background = options.background ?? "#ffffff";
51368
50145
  const { html, css } = render(document, { theme: "light" });
51369
50146
  const svg = `<?xml version="1.0" encoding="UTF-8"?>
51370
50147
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
51371
50148
  viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
51372
50149
  <rect width="100%" height="100%" fill="${background}"/>
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
- ">
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;">
51383
50152
  <style type="text/css">
51384
50153
  ${css}
51385
50154
  </style>